Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support using any value from owned resource in path with {{.owned.xxx… #39

Merged
merged 4 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)]
kuanf marked this conversation as resolved.
Show resolved Hide resolved
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,
},
},
},
},
},
}
)