Skip to content

Commit

Permalink
support using any value from owned resource in path with {{.owned.xxx… (
Browse files Browse the repository at this point in the history
#39)

* support using any value from owned resource in path with {{.owned.xxx.xxx.xx}}

Signed-off-by: Kuan Feng <kuan@ca.ibm.com>

* update sample for user reference

Signed-off-by: Kuan Feng <kuan@ca.ibm.com>

* fix function name with camel case

Signed-off-by: Kuan Feng <kuan@ca.ibm.com>

* add comment to code

Signed-off-by: Kuan Feng <kuan@ca.ibm.com>

---------

Signed-off-by: Kuan Feng <kuan@ca.ibm.com>
  • Loading branch information
kuanf authored Aug 3, 2023
1 parent 8f968dc commit efe6b62
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 25 deletions.
34 changes: 28 additions & 6 deletions config/samples/devops_v1alpha1_operatorresourcemapping.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# This is an sample to describe the syntax/features of ORM, it should not be used directly
apiVersion: devops.turbonomic.io/v1alpha1
kind: OperatorResourceMapping
metadata:
Expand All @@ -15,16 +16,37 @@ spec:
kind: yourkind
name: myoperand
mappings:
patterns:
- ownerPath: ".spec.template.spec.containers[?(@.name==container1)].resources"
selectors: # define selectors for patterns
my_components:
matchExpressions:
- key: my.org/group
operator: In
values:
- api
- auth
- db
patterns: # patterns to generate mappings in status
# pattern to map deployment resource to its operand(owner)
- ownerPath: ".spec.component1.resources"
owned:
apiVersion: apps/v1
kind: Deployment
name: yourdeploy
path: ".spec.template.spec.containers[?(@.name=container1)].resources"
- ownerPath: ".spec.template.spec.containers[?(@.name==container2)].resources"
path: ".spec.template.spec.containers[?(@.name=component1)].resources"
# pattern to use predefined selector to describe multiple mappings,
# assuming the container name and field in operand are all using deployment name as its name
- ownerPath: ".spec.{{.owned.name}}.resources"
owned:
apiVersion: apps/v1
kind: Deployment
name: yourseconddeploy
path: ".spec.template.spec.containers[?(@.name=container2)].resources"
selector: my_components
path: ".spec.template.spec.containers[?(@.name={{.owned.name}})].resources"
# pattern to use predefined selector to describe multiple mappings,
# the container name in each deployment is its deployment name plus suffix "-container"
# the field in operand is defined in a label "appname" of the deployment
- ownerPath: ".spec.{{.owned.labels.appname}}.resources"
owned:
apiVersion: apps/v1
kind: Deployment
selector: my_components
path: ".spec.template.spec.containers[?(@.name={{.owned.name}}-container)].resources"
145 changes: 137 additions & 8 deletions registry/orm.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
var (
errorMessageMultipleSourceForOwner = "allow 1 and only 1 input from owned.name, owned.selector, owner.labelSelector"
errorMessageUnknownSelector = "unknown selector"
errorMessageSyntaxError = "syntax error in pattern "
)

func (or *ResourceMappingRegistry) SeekTopOwnersResourcePathsForOwnedResourcePath(owned devopsv1alpha1.ResourcePath) []devopsv1alpha1.ResourcePath {
Expand Down Expand Up @@ -70,7 +71,16 @@ func (or *ResourceMappingRegistry) SeekTopOwnersResourcePathsForOwnedResourcePat

}

const predefinedOwnedResourceName = ".owned.name"
const predefinedquotestart = "{{"
const predefinedquoteend = "}}"
const predefinedownedprefix = ".owned"

var predefinedshorthand map[string]string = map[string]string{
".name": ".metadata.name",
".labels": ".metadata.labels",
".annotations": ".metadata.annotations",
}

const predefinedParameterPlaceHolder = ".."

func (or *ResourceMappingRegistry) validateORMOwner(orm *devopsv1alpha1.OperatorResourceMapping) (*unstructured.Unstructured, error) {
Expand Down Expand Up @@ -326,6 +336,7 @@ func (or *ResourceMappingRegistry) ValidateAndRegisterORM(orm *devopsv1alpha1.Op
}

allpatterns := []devopsv1alpha1.Pattern{}
allowned := make(map[types.NamespacedName]*unstructured.Unstructured)
for _, p := range orm.Spec.Mappings.Patterns {

if or.staticCheckPattern(&p, orm.Spec.Mappings.Selectors, orm.Spec.Mappings.Parameters) != nil {
Expand All @@ -340,6 +351,16 @@ func (or *ResourceMappingRegistry) ValidateAndRegisterORM(orm *devopsv1alpha1.Op
// TODO: avoid to retrieve same source repeatedly
if k.Name != "" {
allpatterns = append(allpatterns, *p.DeepCopy())
var obj *unstructured.Unstructured
obj, err = kubernetes.Toolbox.GetResourceWithGVK(p.OwnedResourcePath.GroupVersionKind(), k)
if err != nil {
rLog.Error(err, "getting resource", "source", p.OwnedResourcePath)
continue
}
allowned[types.NamespacedName{
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
}] = obj
} else {
var srcObjs []unstructured.Unstructured
selector := p.OwnedResourcePath.LabelSelector
Expand All @@ -364,6 +385,10 @@ func (or *ResourceMappingRegistry) ValidateAndRegisterORM(orm *devopsv1alpha1.Op
newp.OwnedResourcePath.ObjectReference.Name = obj.GetName()
newp.OwnedResourcePath.ObjectReference.Namespace = obj.GetNamespace()
allpatterns = append(allpatterns, newp)
allowned[types.NamespacedName{
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
}] = obj.DeepCopy()
}
}
}
Expand All @@ -375,7 +400,12 @@ func (or *ResourceMappingRegistry) ValidateAndRegisterORM(orm *devopsv1alpha1.Op

for _, p := range allpatterns {

patterns := populatePatterns(orm.Spec.Mappings.Parameters, p)
var patterns []devopsv1alpha1.Pattern
patterns, err = populatePatterns(allowned, orm.Spec.Mappings.Parameters, p)
if err != nil {
return orm, owner, err
}

for _, pattern := range patterns {
err = or.registerOwnershipMapping(pattern.OwnerPath, pattern.OwnedResourcePath.Path,
types.NamespacedName{Name: orm.Name, Namespace: orm.Namespace},
Expand Down Expand Up @@ -413,11 +443,24 @@ func (or *ResourceMappingRegistry) retrieveObjectEntryForOwnerAndORM(owner corev
return retrieveObjectEntryForObjectAndORMFromRegistry(or.ownerRegistry, owner, orm)
}

func populatePatterns(parameters map[string][]string, pattern devopsv1alpha1.Pattern) []devopsv1alpha1.Pattern {
func processShorthandVariables(str string) string {
result := str

for k, v := range predefinedshorthand {
short := predefinedquotestart + predefinedownedprefix + k + predefinedquoteend
full := predefinedquotestart + predefinedownedprefix + v + predefinedquoteend
result = strings.ReplaceAll(result, short, full)
}

return result
}

func populatePatterns(ownedMap map[types.NamespacedName]*unstructured.Unstructured, parameters map[string][]string, pattern devopsv1alpha1.Pattern) ([]devopsv1alpha1.Pattern, error) {
var err error
var allpatterns []devopsv1alpha1.Pattern

pattern.OwnerPath = strings.ReplaceAll(pattern.OwnerPath, "{{"+predefinedOwnedResourceName+"}}", pattern.OwnedResourcePath.Name)
pattern.OwnedResourcePath.Path = strings.ReplaceAll(pattern.OwnedResourcePath.Path, "{{"+predefinedOwnedResourceName+"}}", pattern.OwnedResourcePath.Name)
pattern.OwnerPath = processShorthandVariables(pattern.OwnerPath)
pattern.OwnedResourcePath.Path = processShorthandVariables(pattern.OwnedResourcePath.Path)

allpatterns = append(allpatterns, pattern)

Expand All @@ -433,13 +476,99 @@ func populatePatterns(parameters map[string][]string, pattern devopsv1alpha1.Pat
for _, p := range prevpatterns {
for _, v := range values {
newp := p.DeepCopy()
newp.OwnerPath = strings.ReplaceAll(p.OwnerPath, "{{"+name+"}}", v)
newp.OwnedResourcePath.Path = strings.ReplaceAll(p.OwnedResourcePath.Path, "{{"+name+"}}", v)
newp.OwnerPath = strings.ReplaceAll(p.OwnerPath, predefinedquotestart+name+predefinedquoteend, v)
newp.OwnedResourcePath.Path = strings.ReplaceAll(p.OwnedResourcePath.Path, predefinedquotestart+name+predefinedquoteend, v)
allpatterns = append(allpatterns, *newp)
}
}
}
return allpatterns

// retrieve and replace generic string value from owned resource
prevpatterns = allpatterns
allpatterns = []devopsv1alpha1.Pattern{}
for _, p := range prevpatterns {
more := true
ownedKey := types.NamespacedName{
Namespace: p.OwnedResourcePath.Namespace,
Name: p.OwnedResourcePath.Name,
}
ownedObj := ownedMap[ownedKey]
for more {
more = false
var found bool
p.OwnerPath, found, err = fillOwnedResourceValue(ownedObj, p.OwnerPath)
if err != nil {
return allpatterns, err
}
if found {
more = true
}

p.OwnedResourcePath.Path, found, err = fillOwnedResourceValue(ownedObj, p.OwnedResourcePath.Path)
if err != nil {
return allpatterns, err
}
if found {
more = true
}
}
allpatterns = append(allpatterns, p)
}

return allpatterns, nil
}

// fillOwnedResourceValue - use the value from owned resource to fill the variables defined in ORM pattern path
// e.g. replace all {{.owned.metadata.namespace}} with the namespace of owned resource.
// input parameters
// - obj, the owned resource object.
// - path, the path in ORM pattern w/o variables
// return values
// - string, the updated path
// - bool, true: variable found and replacement happened; false: original path returned
// - error, errors found during value extraction
func fillOwnedResourceValue(obj *unstructured.Unstructured, path string) (string, bool, error) {
start := strings.Index(path, predefinedquotestart)
if start == -1 {
return path, false, nil
}

end := strings.Index(path, predefinedquoteend)
if end == -1 {
return path, false, errors.New(errorMessageSyntaxError + path)
}

// description to variables used here:
// full is {{.owned.xxx.xxx.xxx}} - to identify the variable
// content is .owned.xxx.xxx.xxx from full - for syntax checking
// objPath is .xxx.xxx.xxx from content - real path in the object to retrieve the value
full := path[start : end+len(predefinedquoteend)]
content := full[len(predefinedquotestart) : len(full)-len(predefinedquoteend)]

if strings.Index(content, predefinedownedprefix) != 0 {
return path, false, errors.New(errorMessageSyntaxError + path)
}

objPath := content[len(predefinedownedprefix):]
v, found, err := ormutils.NestedField(obj.Object, objPath)

if err != nil {
return path, false, err
}

if !found {
return path, false, nil
}

switch v.(type) {
case string:
break
default:
return path, false, errors.New(errorMessageSyntaxError + path)

}

return strings.ReplaceAll(path, full, v.(string)), false, nil
}

// ValidateOwnedPathEnabled checks if the annotation is set to "disabled", otherwise it is enabled
Expand Down
19 changes: 19 additions & 0 deletions registry/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ var _ = BeforeSuite(func() {
err = cli.Create(ctx, &_t_ormMixed)
Expect(err).ToNot(HaveOccurred())

err = cli.Create(ctx, &_t_ormWithVar)
Expect(err).ToNot(HaveOccurred())

err = kubernetes.InitToolbox(cfg, testEnv.Scheme, nil)
Expect(err).ToNot(HaveOccurred())

Expand Down Expand Up @@ -304,6 +307,22 @@ var _ = Describe("ORM Test", func() {
Expect(omv.Reason).To(BeEmpty())

})

It("can register orm with var in owned path and show status", func() {
ormobj, ownerobj, err = rmr.ValidateAndRegisterORM(&_t_ormWithVar)
Expect(err).ToNot(HaveOccurred())
Expect(ownerobj).ToNot(BeNil())
Expect(ormobj).ToNot(BeNil())

rmr.SetORMStatusForOwner(ownerobj, ormobj)
Expect(ormobj.Status.State).To(BeEquivalentTo(devopsv1alpha1.ORMTypeOK))
Expect(ormobj.Status.Owner).To(BeEquivalentTo(_t_ownerref))
Expect(len(ormobj.Status.OwnerMappingValues)).To(BeEquivalentTo(1))
omv := ormobj.Status.OwnerMappingValues[0]
Expect(omv.Message).To(BeEmpty())
Expect(omv.Reason).To(BeEmpty())
})

})

func TestUtils(t *testing.T) {
Expand Down
57 changes: 46 additions & 11 deletions registry/suite_test_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,21 @@ import (
var (
_t_ownerpath = ".spec.template.spec.containers[?(@.name==\"" + _t_ownerref.Name + "\")].resources"
_t_ownedpath = ".spec.template.spec.containers[?(@.name==\"" + _t_ownedref.Name + "\")].resources"
_t_ormkey = types.NamespacedName{
Namespace: "default",

_t_namespace = "default"

_t_ormkey = types.NamespacedName{
Namespace: _t_namespace,
Name: "orm",
}
_t_ownerref = corev1.ObjectReference{
Namespace: "default",
Namespace: _t_namespace,
Name: "owner",
Kind: "Deployment",
APIVersion: "apps/v1",
}
_t_ownedref = corev1.ObjectReference{
Namespace: "default",
Namespace: _t_namespace,
Name: "owned",
Kind: "Deployment",
APIVersion: "apps/v1",
Expand Down Expand Up @@ -92,7 +95,7 @@ var (
_t_owned = appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: _t_ownedref.Name,
Namespace: _t_ownerref.Namespace,
Namespace: _t_ownedref.Namespace,
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
Expand Down Expand Up @@ -144,17 +147,17 @@ var (
},
}

_t_ownedOmitPath = ".spec.template.spec.containers[?(@.name==\"" + _t_ownerref.Name + "\")].ports[?(@.protocol==\"TCP\")].containerPort"
_t_ownedOmitPath = ".spec.template.spec.containers[?(@.name==\"" + _t_ownedref.Name + "\")].ports[?(@.protocol==\"TCP\")].containerPort"
_t_ownerOmitPath = ".spec.template.spec.containers[?(@.name==\"" + _t_ownerref.Name + "\")].ports[?(@.protocol==\"TCP\")].containerPort"

_t_ormWitOmitPathKey = types.NamespacedName{
_t_ormWithOmitPathKey = types.NamespacedName{
Name: "ormomitpath",
Namespace: "default",
Namespace: _t_namespace,
}
_t_ormWithOmitPath = devopsv1alpha1.OperatorResourceMapping{
ObjectMeta: metav1.ObjectMeta{
Name: _t_ormWitOmitPathKey.Name,
Namespace: _t_ormWitOmitPathKey.Namespace,
Name: _t_ormWithOmitPathKey.Name,
Namespace: _t_ormWithOmitPathKey.Namespace,
},
Spec: devopsv1alpha1.OperatorResourceMappingSpec{
Owner: devopsv1alpha1.ObjectLocator{
Expand All @@ -178,7 +181,7 @@ var (

_t_ormMixedKey = types.NamespacedName{
Name: "ormmixed",
Namespace: "default",
Namespace: _t_namespace,
}
_t_ormMixed = devopsv1alpha1.OperatorResourceMapping{
ObjectMeta: metav1.ObjectMeta{
Expand Down Expand Up @@ -213,4 +216,36 @@ var (
},
},
}

_t_ownerpathwithvar = ".spec.template.spec.containers[?(@.name==\"" + _t_ownerref.Name + "\")].resources"
_t_ownedpathwithvar = ".spec.template.spec.containers[?(@.name==\"" + "{{.owned.name}}" + "\")].resources"

_t_ormWithVarKey = types.NamespacedName{
Name: "ormwithvar",
Namespace: _t_namespace,
}
_t_ormWithVar = devopsv1alpha1.OperatorResourceMapping{
ObjectMeta: metav1.ObjectMeta{
Name: _t_ormWithVarKey.Name,
Namespace: _t_ormWithVarKey.Namespace,
},
Spec: devopsv1alpha1.OperatorResourceMappingSpec{
Owner: devopsv1alpha1.ObjectLocator{
ObjectReference: _t_ownerref,
},
Mappings: devopsv1alpha1.MappingPatterns{
Patterns: []devopsv1alpha1.Pattern{
{
OwnerPath: _t_ownerpathwithvar,
OwnedResourcePath: devopsv1alpha1.OwnedResourcePath{
ObjectLocator: devopsv1alpha1.ObjectLocator{
ObjectReference: _t_ownedref,
},
Path: _t_ownedpathwithvar,
},
},
},
},
},
}
)

0 comments on commit efe6b62

Please sign in to comment.