From da54ee583fcd22f48995887e733f472bd516bad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Kesser?= Date: Thu, 16 May 2024 12:41:47 +0200 Subject: [PATCH 1/3] feat: allow overwrites of array fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Kesser --- ...sition-xcluster.eks.aws.example.cloud.yaml | 2 +- package/EKS-Cluster/definition.yaml | 2 +- pkg/generator/generator.go | 150 ++++++++++++++---- pkg/generator/generattor_test.go | 130 +++++++++++++++ 4 files changed, 255 insertions(+), 29 deletions(-) create mode 100644 pkg/generator/generattor_test.go diff --git a/package/EKS-Cluster/composition-xcluster.eks.aws.example.cloud.yaml b/package/EKS-Cluster/composition-xcluster.eks.aws.example.cloud.yaml index c17e432..9f2b7d9 100644 --- a/package/EKS-Cluster/composition-xcluster.eks.aws.example.cloud.yaml +++ b/package/EKS-Cluster/composition-xcluster.eks.aws.example.cloud.yaml @@ -1,7 +1,7 @@ ## WARNING: This file was autogenerated! ## Manual modifications will be overwritten ## unless ignore: true is set in generate.yaml! -## Last Modification: 10:09:00 on 05-08-2024. +## Last Modification: 10:57:26 on 05-16-2024. apiVersion: apiextensions.crossplane.io/v1 kind: Composition diff --git a/package/EKS-Cluster/definition.yaml b/package/EKS-Cluster/definition.yaml index 46451f4..50e3a12 100644 --- a/package/EKS-Cluster/definition.yaml +++ b/package/EKS-Cluster/definition.yaml @@ -1,7 +1,7 @@ ## WARNING: This file was autogenerated! ## Manual modifications will be overwritten ## unless ignore: true is set in generate.yaml! -## Last Modification: 10:09:00 on 05-08-2024. +## Last Modification: 10:57:26 on 05-16-2024. apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index 17c6c5c..518e0c3 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -5,7 +5,9 @@ import ( "encoding/json" "errors" "fmt" + "slices" "sort" + "strconv" "strings" p "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1" @@ -52,7 +54,7 @@ type OverrideFieldDefinition struct { Schema *v1.JSONSchemaProps Required bool Replacement bool - PathSegments []string + PathSegments []pathSegemnt Patches []p.PatchSetPatch OriginalEnum []v1.JSON Overwrites *t.OverrideFieldInClaim @@ -732,63 +734,157 @@ func (g *XGenerator) generateBase(comp t.Composition) []byte { } } - //overwites := map[string]interface{}{} - for _, overwite := range g.OverrideFields { + base = generateOverriteFields(base, g.OverrideFields) + + object, err := json.Marshal(base) + if err != nil { + fmt.Println("error") + } + + return object +} + +func generateOverriteFields(base map[string]interface{}, overrideFields []t.OverrideField) map[string]interface{} { + for _, overwite := range overrideFields { if overwite.Value != nil { - //overwites[overwite.Path] = overwite.Value path := splitPath(overwite.Path) - c := &base + var current interface{} + current = base pathLength := len(path) + for i := 0; i < pathLength-1; i++ { - p := path[i] - //for _, p := range path { - if (*c)[p] == nil { - (*c)[p] = map[string]interface{}{} + segment := path[i] + property := path[i].path + if segment.pathType == "object" { + if current.(map[string]interface{})[property] == nil { + current.(map[string]interface{})[property] = map[string]interface{}{} + } + + current = current.(map[string]interface{})[property].(map[string]interface{}) + } else if segment.pathType == "array" { + if current.(map[string]interface{})[property] == nil { + current.(map[string]interface{})[property] = []map[string]interface{}{} + } + + var b interface{} + b = current.(map[string]interface{})[property].([]map[string]interface{}) + currentSize := len(b.([]map[string]interface{})) + wantedSize := segment.arrayPosition + 1 + if currentSize < wantedSize { + sizeToGrow := wantedSize - currentSize + b = slices.Grow(b.([]map[string]interface{}), sizeToGrow) + b = b.([]map[string]interface{})[:cap(b.([]map[string]interface{}))] + b.([]map[string]interface{})[segment.arrayPosition] = map[string]interface{}{} + } + current.(map[string]interface{})[property] = b + current = b.([]map[string]interface{})[segment.arrayPosition] } - b := (*c)[p].(map[string]interface{}) - c = &b } - (*c)[path[pathLength-1]] = overwite.Value - } - } + segment := path[pathLength-1] + if segment.pathType == "object" { + (current).(map[string]interface{})[path[pathLength-1].path] = overwite.Value + } + if segment.pathType == "array" { + property := path[pathLength-1].path - object, err := json.Marshal(base) - if err != nil { - fmt.Println("error") + if (current.(map[string]interface{}))[property] == nil { + (current.(map[string]interface{}))[property] = []interface{}{} + } + + var b interface{} + b = (current.(map[string]interface{}))[property].([]interface{}) + currentSize := len(b.([]interface{})) + wantedSize := segment.arrayPosition + 1 + if currentSize < wantedSize { + sizeToGrow := wantedSize - currentSize + b = slices.Grow(b.([]interface{}), sizeToGrow) + b = b.([]interface{})[:cap(b.([]interface{}))] + (b.([]interface{})[segment.arrayPosition]) = overwite.Value + } + current.(map[string]interface{})[property] = b + + } + } } + return base +} - return object +type pathSegemnt struct { + path string + pathType string + arrayPosition int } -func splitPath(path string) []string { +func splitPath(path string) []pathSegemnt { inString := false - result := []string{} + result := []pathSegemnt{} current := "" escaped := false for _, r := range path { switch r { case '"': - current += string(r) inString = !inString escaped = false case '\\': - current += string(r) escaped = true case '.': + if current != "" { + if !inString && !escaped { + segment := pathSegemnt{ + path: current, + pathType: "object", + } + result = append(result, segment) + current = "" + } else { + current += string(r) + } + } + case '[': if !inString && !escaped { - result = append(result, current) + segment := pathSegemnt{ + path: current, + pathType: "object", + } + result = append(result, segment) current = "" } else { current += string(r) } + case ']': + if !inString && !escaped { + lastSegemnt := result[len(result)-1] + arrayIndex, err := strconv.Atoi(current) + if err == nil { + lastSegemnt.pathType = "array" + lastSegemnt.arrayPosition = arrayIndex + result[len(result)-1] = lastSegemnt + } else { + segment := pathSegemnt{ + path: current, + pathType: "object", + } + result = append(result, segment) + } + current = "" + + } else { + current += string(r) + } default: current += string(r) escaped = false } } - result = append(result, current) + if current != "" { + segment := pathSegemnt{ + path: current, + pathType: "object", + } + result = append(result, segment) + } return result } @@ -832,7 +928,7 @@ func (g *XGenerator) generateAdditonalPipelineStep(s t.PipelineStep) (*c.Pipelin func (g *XGenerator) overwrittenFields(schema *v1.JSONSchemaProps, path string) error { for _, o := range g.overrideFieldDefinitions { - if o.PathSegments[0] == path && !o.IgnoreInClaim { + if o.PathSegments[0].path == path && !o.IgnoreInClaim { err := overwrittenFields(schema, path, o, 1) if err != nil { return err @@ -845,7 +941,7 @@ func (g *XGenerator) overwrittenFields(schema *v1.JSONSchemaProps, path string) func overwrittenFields(schema *v1.JSONSchemaProps, path string, definition *OverrideFieldDefinition, level int) error { if len(definition.PathSegments)-1 > level { if schema.Type == "object" { - pathSegment := definition.PathSegments[level] + pathSegment := definition.PathSegments[level].path prop, ok := schema.Properties[pathSegment] if !ok { schema.Properties[pathSegment] = v1.JSONSchemaProps{ @@ -860,7 +956,7 @@ func overwrittenFields(schema *v1.JSONSchemaProps, path string, definition *Over } } } else { - pathSegment := definition.PathSegments[level] + pathSegment := definition.PathSegments[level].path if definition.Schema == nil { return fmt.Errorf("schema must be given for new property: %s", definition.ClaimPath) } diff --git a/pkg/generator/generattor_test.go b/pkg/generator/generattor_test.go new file mode 100644 index 0000000..7b2f14a --- /dev/null +++ b/pkg/generator/generattor_test.go @@ -0,0 +1,130 @@ +package generator + +import ( + "encoding/json" + "testing" + + tp "github.com/crossplane-contrib/x-generation/pkg/types" +) + +func Test_generateOverriteFields(t *testing.T) { + tests := []struct { + name string + base map[string]interface{} + overridePaths []tp.OverrideField + want string + }{ + { + name: "Should render path", + base: map[string]interface{}{}, + overridePaths: []tp.OverrideField{ + { + Path: "spec.forProvider.certificateAuthorityConfiguration", + Value: "testA", + }, + }, + want: `{"spec": {"forProvider": {"certificateAuthorityConfiguration":"testA"}}}`, + }, + { + name: "Should not override existing path", + base: map[string]interface{}{ + "spec": map[string]interface{}{ + "forProvider": map[string]interface{}{ + "test": "testValue", + }, + }, + }, + overridePaths: []tp.OverrideField{ + { + Path: "spec.forProvider.certificateAuthorityConfiguration", + Value: "testA", + }, + }, + want: `{"spec": {"forProvider": {"test":"testValue", "certificateAuthorityConfiguration":"testA"}}}`, + }, + { + name: "Should create arrays of strings", + base: map[string]interface{}{}, + overridePaths: []tp.OverrideField{ + { + Path: "spec.forProvider.certificateAuthorityConfiguration[0]", + Value: "testA", + }, + }, + want: `{"spec": {"forProvider": { "certificateAuthorityConfiguration": ["testA"]}}}`, + }, + { + name: "Should create arrays of arrays", + base: map[string]interface{}{}, + overridePaths: []tp.OverrideField{ + { + Path: "spec.forProvider.certificateAuthorityConfiguration[0].subproperty[0]", + Value: "testA", + }, + }, + want: `{"spec": {"forProvider": { "certificateAuthorityConfiguration": [{"subproperty":["testA"]}]}}}`, + }, + { + name: "Should create arrays of arrays with properies", + base: map[string]interface{}{}, + overridePaths: []tp.OverrideField{ + { + Path: "spec.forProvider.certificateAuthorityConfiguration[0].subproperty[0].arg", + Value: "testA", + }, + }, + want: `{"spec": {"forProvider": { "certificateAuthorityConfiguration": [{"subproperty":[{"arg":"testA"}]}]}}}`, + }, + { + name: "Should render path", + base: map[string]interface{}{}, + overridePaths: []tp.OverrideField{ + { + Path: "spec.forProvider[\"certificateAuthorityConfiguration\"]", + Value: "testA", + }, + }, + want: `{"spec": {"forProvider": {"certificateAuthorityConfiguration":"testA"}}}`, + }, + { + name: "Should render path", + base: map[string]interface{}{}, + overridePaths: []tp.OverrideField{ + { + Path: "spec.forProvider[\"certificateAuthority.Configuration\"]", + Value: "testA", + }, + }, + want: `{"spec": {"forProvider": {"certificateAuthority.Configuration":"testA"}}}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := generateOverriteFields(tt.base, tt.overridePaths) + object, err := json.Marshal(got) + if err != nil { + t.Errorf("error marshalling object %v", err) + } + + // unmarshal and marshal wanted value to be independent of formatting and oder of properties + var wantobject interface{} + err = json.Unmarshal([]byte(tt.want), &wantobject) + if err != nil { + t.Errorf("want is not valid json %v", err) + } + + wantbyte, err := json.Marshal(wantobject) + + if err != nil { + t.Errorf("error marshalling object %v", err) + } + + gotString := string(object) + wantString := string(wantbyte) + if gotString != wantString { + t.Errorf("Expaced object does not match got\n%v, want\n%v", gotString, wantString) + } + }) + } +} From 099ddb97d648c9377054dd0ef474b1667dfae4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Kesser?= Date: Thu, 16 May 2024 13:58:32 +0200 Subject: [PATCH 2/3] fix: fix typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Kesser --- pkg/generator/generator.go | 22 +++++++++++----------- pkg/generator/generattor_test.go | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index 518e0c3..63e2281 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -54,7 +54,7 @@ type OverrideFieldDefinition struct { Schema *v1.JSONSchemaProps Required bool Replacement bool - PathSegments []pathSegemnt + PathSegments []pathSegment Patches []p.PatchSetPatch OriginalEnum []v1.JSON Overwrites *t.OverrideFieldInClaim @@ -734,17 +734,17 @@ func (g *XGenerator) generateBase(comp t.Composition) []byte { } } - base = generateOverriteFields(base, g.OverrideFields) + base = applyOverrideFields(base, g.OverrideFields) object, err := json.Marshal(base) if err != nil { - fmt.Println("error") + fmt.Printf("unable to marshal base: %v\n", err) } return object } -func generateOverriteFields(base map[string]interface{}, overrideFields []t.OverrideField) map[string]interface{} { +func applyOverrideFields(base map[string]interface{}, overrideFields []t.OverrideField) map[string]interface{} { for _, overwite := range overrideFields { if overwite.Value != nil { path := splitPath(overwite.Path) @@ -809,15 +809,15 @@ func generateOverriteFields(base map[string]interface{}, overrideFields []t.Over return base } -type pathSegemnt struct { +type pathSegment struct { path string pathType string arrayPosition int } -func splitPath(path string) []pathSegemnt { +func splitPath(path string) []pathSegment { inString := false - result := []pathSegemnt{} + result := []pathSegment{} current := "" escaped := false for _, r := range path { @@ -831,7 +831,7 @@ func splitPath(path string) []pathSegemnt { case '.': if current != "" { if !inString && !escaped { - segment := pathSegemnt{ + segment := pathSegment{ path: current, pathType: "object", } @@ -843,7 +843,7 @@ func splitPath(path string) []pathSegemnt { } case '[': if !inString && !escaped { - segment := pathSegemnt{ + segment := pathSegment{ path: current, pathType: "object", } @@ -861,7 +861,7 @@ func splitPath(path string) []pathSegemnt { lastSegemnt.arrayPosition = arrayIndex result[len(result)-1] = lastSegemnt } else { - segment := pathSegemnt{ + segment := pathSegment{ path: current, pathType: "object", } @@ -879,7 +879,7 @@ func splitPath(path string) []pathSegemnt { } } if current != "" { - segment := pathSegemnt{ + segment := pathSegment{ path: current, pathType: "object", } diff --git a/pkg/generator/generattor_test.go b/pkg/generator/generattor_test.go index 7b2f14a..54f4c6d 100644 --- a/pkg/generator/generattor_test.go +++ b/pkg/generator/generattor_test.go @@ -101,7 +101,7 @@ func Test_generateOverriteFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := generateOverriteFields(tt.base, tt.overridePaths) + got := applyOverrideFields(tt.base, tt.overridePaths) object, err := json.Marshal(got) if err != nil { t.Errorf("error marshalling object %v", err) From c60b0716fa985ca83fd42dee082a7fec71ad3866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Kesser?= Date: Thu, 16 May 2024 14:27:15 +0200 Subject: [PATCH 3/3] fix: renamed test function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Kesser --- pkg/generator/generattor_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/generator/generattor_test.go b/pkg/generator/generattor_test.go index 54f4c6d..f2aa73a 100644 --- a/pkg/generator/generattor_test.go +++ b/pkg/generator/generattor_test.go @@ -7,7 +7,7 @@ import ( tp "github.com/crossplane-contrib/x-generation/pkg/types" ) -func Test_generateOverriteFields(t *testing.T) { +func Test_generateOverrideFields(t *testing.T) { tests := []struct { name string base map[string]interface{}