diff --git a/internal/workload/v1/api.go b/internal/workload/v1/api.go index ab36490..685dd4b 100644 --- a/internal/workload/v1/api.go +++ b/internal/workload/v1/api.go @@ -10,6 +10,8 @@ import ( "io" "reflect" "strings" + + "github.com/vmware-tanzu-labs/operator-builder/internal/workload/v1/markers" ) var ErrOverwriteExistingValue = errors.New("an attempt to overwrite existing value was made") @@ -18,7 +20,7 @@ type APIFields struct { Name string StructName string manifestName string - Type FieldType + Type markers.FieldType Tags string Comments []string Markers []string @@ -28,7 +30,7 @@ type APIFields struct { Last bool } -func (api *APIFields) AddField(path string, fieldType FieldType, comments []string, sample interface{}, hasDefault bool) error { +func (api *APIFields) AddField(path string, fieldType markers.FieldType, comments []string, sample interface{}, hasDefault bool) error { obj := api parts := strings.Split(path, ".") @@ -41,7 +43,7 @@ func (api *APIFields) AddField(path string, fieldType FieldType, comments []stri if obj.Children != nil { for i := range obj.Children { if obj.Children[i].manifestName == part { - if obj.Children[i].Type != FieldStruct { + if obj.Children[i].Type != markers.FieldStruct { return fmt.Errorf("%w for api field %s", ErrOverwriteExistingValue, path) } @@ -54,7 +56,7 @@ func (api *APIFields) AddField(path string, fieldType FieldType, comments []stri } if !foundMatch { - child := obj.newChild(part, FieldStruct, sample) + child := obj.newChild(part, markers.FieldStruct, sample) child.Markers = append(child.Markers, "+kubebuilder:validation:Optional") @@ -159,7 +161,7 @@ func (api *APIFields) hasRequiredField() bool { func (api *APIFields) generateAPISpecField(b io.StringWriter, kind string) { typeName := api.Type.String() - if api.Type == FieldStruct { + if api.Type == markers.FieldStruct { typeName = kind + api.StructName } @@ -175,7 +177,7 @@ func (api *APIFields) generateAPISpecField(b io.StringWriter, kind string) { } func (api *APIFields) generateAPIStruct(b io.StringWriter, kind string) { - if api.Type == FieldStruct { + if api.Type == markers.FieldStruct { mustWrite(b.WriteString(fmt.Sprintf("type %s %s{\n", kind+api.StructName, api.Type.String()))) for _, child := range api.Children { @@ -224,23 +226,43 @@ func (api *APIFields) isEqual(input *APIFields) bool { return false } +// getSampleValue exists to solve the problem of the sample value being a brittle, generic interface +// which can change when we move from proper typed objects to pointers. This function serves to +// solve both use cases. +func (api *APIFields) getSampleValue(sampleVal interface{}) string { + switch t := sampleVal.(type) { + case *string: + if api.Type == markers.FieldString { + return fmt.Sprintf(`%q`, *t) + } + + return *t + case *int: + return fmt.Sprintf(`%v`, *t) + case *bool: + return fmt.Sprintf(`%v`, *t) + case string: + if api.Type == markers.FieldString { + return fmt.Sprintf(`%q`, t) + } + + return t + default: + return fmt.Sprintf(`%v`, t) + } +} + func (api *APIFields) setSample(sampleVal interface{}) { switch api.Type { - case FieldString: - api.Sample = fmt.Sprintf("%s: %q", api.manifestName, sampleVal) - case FieldStruct: + case markers.FieldStruct: api.Sample = fmt.Sprintf("%s:", api.manifestName) default: - api.Sample = fmt.Sprintf("%s: %v", api.manifestName, sampleVal) + api.Sample = fmt.Sprintf("%s: %v", api.manifestName, api.getSampleValue(sampleVal)) } } func (api *APIFields) setDefault(sampleVal interface{}) { - if api.Type == FieldString { - api.Default = fmt.Sprintf("%q", sampleVal) - } else { - api.Default = fmt.Sprintf("%v", sampleVal) - } + api.Default = api.getSampleValue(sampleVal) if len(api.Markers) == 0 { api.Markers = append( @@ -264,7 +286,7 @@ func (api *APIFields) setCommentsAndDefault(comments []string, sampleVal interfa } } -func (api *APIFields) newChild(name string, fieldType FieldType, sample interface{}) *APIFields { +func (api *APIFields) newChild(name string, fieldType markers.FieldType, sample interface{}) *APIFields { child := &APIFields{ Name: strings.Title(name), manifestName: name, diff --git a/internal/workload/v1/api_internal_test.go b/internal/workload/v1/api_internal_test.go index f4a8292..6d121f3 100644 --- a/internal/workload/v1/api_internal_test.go +++ b/internal/workload/v1/api_internal_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/vmware-tanzu-labs/operator-builder/internal/workload/v1/markers" ) func TestAPIFields_GenerateSampleSpec(t *testing.T) { @@ -16,7 +18,7 @@ func TestAPIFields_GenerateSampleSpec(t *testing.T) { type fields struct { Name string manifestName string - Type FieldType + Type markers.FieldType Tags string Comments []string Markers []string @@ -319,12 +321,139 @@ func Test_mustWrite(t *testing.T) { } } +func TestAPIFields_getSampleValue(t *testing.T) { + t.Parallel() + + testString := "testString" + testInt := 1 + testBool := true + + type fields struct { + Name string + StructName string + manifestName string + Type markers.FieldType + Tags string + Comments []string + Markers []string + Children []*APIFields + Default string + Sample string + Last bool + } + + type args struct { + sampleVal interface{} + } + + tests := []struct { + name string + fields fields + args args + want string + }{ + { + name: "test string value", + args: args{ + sampleVal: testString, + }, + fields: fields{ + Type: markers.FieldString, + }, + want: fmt.Sprintf("%q", testString), + }, + { + name: "test pointer to string value", + args: args{ + sampleVal: &testString, + }, + fields: fields{ + Type: markers.FieldString, + }, + want: fmt.Sprintf("%q", testString), + }, + { + name: "test int value", + args: args{ + sampleVal: testInt, + }, + fields: fields{ + Type: markers.FieldInt, + }, + want: fmt.Sprintf("%v", testInt), + }, + { + name: "test pointer to int value", + args: args{ + sampleVal: &testInt, + }, + fields: fields{ + Type: markers.FieldInt, + }, + want: fmt.Sprintf("%v", testInt), + }, + { + name: "test bool value", + args: args{ + sampleVal: testBool, + }, + fields: fields{ + Type: markers.FieldBool, + }, + want: fmt.Sprintf("%v", testBool), + }, + { + name: "test pointer to bool value", + args: args{ + sampleVal: &testBool, + }, + fields: fields{ + Type: markers.FieldBool, + }, + want: fmt.Sprintf("%v", testBool), + }, + { + name: "test other value", + args: args{ + sampleVal: []string{"test", "get", "sample"}, + }, + fields: fields{ + Type: markers.FieldUnknownType, + }, + want: "[test get sample]", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + api := &APIFields{ + Name: tt.fields.Name, + StructName: tt.fields.StructName, + manifestName: tt.fields.manifestName, + Type: tt.fields.Type, + Tags: tt.fields.Tags, + Comments: tt.fields.Comments, + Markers: tt.fields.Markers, + Children: tt.fields.Children, + Default: tt.fields.Default, + Sample: tt.fields.Sample, + Last: tt.fields.Last, + } + if got := api.getSampleValue(tt.args.sampleVal); got != tt.want { + t.Errorf("APIFields.getSampleValue() = %v, want %v", got, tt.want) + } + }) + } +} + func TestAPIFields_setSample(t *testing.T) { t.Parallel() type fields struct { manifestName string - Type FieldType + Type markers.FieldType Sample string } @@ -345,11 +474,11 @@ func TestAPIFields_setSample(t *testing.T) { }, fields: fields{ manifestName: "string", - Type: FieldString, + Type: markers.FieldString, }, expect: &APIFields{ manifestName: "string", - Type: FieldString, + Type: markers.FieldString, Sample: "string: \"string\"", }, }, @@ -360,26 +489,26 @@ func TestAPIFields_setSample(t *testing.T) { }, fields: fields{ manifestName: "struct", - Type: FieldStruct, + Type: markers.FieldStruct, }, expect: &APIFields{ manifestName: "struct", - Type: FieldStruct, + Type: markers.FieldStruct, Sample: "struct:", }, }, { name: "set other sample", args: args{ - sampleVal: "other", + sampleVal: []string{"test", "sample"}, }, fields: fields{ manifestName: "other", }, expect: &APIFields{ manifestName: "other", - Type: FieldUnknownType, - Sample: "other: other", + Type: markers.FieldUnknownType, + Sample: "other: [test sample]", }, }, } @@ -404,7 +533,7 @@ func TestAPIFields_setDefault(t *testing.T) { type fields struct { manifestName string - Type FieldType + Type markers.FieldType Markers []string Default string Sample string @@ -427,7 +556,7 @@ func TestAPIFields_setDefault(t *testing.T) { }, fields: fields{ manifestName: "string", - Type: FieldString, + Type: markers.FieldString, Markers: []string{ "marker1", "marker2", @@ -435,7 +564,7 @@ func TestAPIFields_setDefault(t *testing.T) { }, expect: &APIFields{ manifestName: "string", - Type: FieldString, + Type: markers.FieldString, Sample: "string: \"string\"", Default: "\"string\"", Markers: []string{ @@ -447,20 +576,20 @@ func TestAPIFields_setDefault(t *testing.T) { { name: "set default for other", args: args{ - sampleVal: "other", + sampleVal: []string{"other"}, }, fields: fields{ manifestName: "other", }, expect: &APIFields{ manifestName: "other", - Type: FieldUnknownType, - Sample: "other: other", - Default: "other", + Type: markers.FieldUnknownType, + Sample: "other: [other]", + Default: "[other]", Markers: []string{ - "+kubebuilder:default=other", + "+kubebuilder:default=[other]", "+kubebuilder:validation:Optional", - "(Default: other)", + "(Default: [other])", }, }, }, @@ -488,7 +617,7 @@ func TestAPIFields_setCommentsAndDefault(t *testing.T) { type fields struct { manifestName string - Type FieldType + Type markers.FieldType Comments []string Markers []string Default string @@ -519,7 +648,7 @@ func TestAPIFields_setCommentsAndDefault(t *testing.T) { }, fields: fields{ manifestName: "string", - Type: FieldString, + Type: markers.FieldString, Comments: []string{ "comment1", "comment2", @@ -527,7 +656,7 @@ func TestAPIFields_setCommentsAndDefault(t *testing.T) { }, expect: &APIFields{ manifestName: "string", - Type: FieldString, + Type: markers.FieldString, Sample: "string: \"string\"", Default: "\"string\"", Markers: []string{ @@ -578,11 +707,15 @@ func TestAPIFields_setCommentsAndDefault(t *testing.T) { func TestAPIFields_newChild(t *testing.T) { t.Parallel() + testString := "string" + testInt := 1 + testBool := true + type fields struct { Name string StructName string manifestName string - Type FieldType + Type markers.FieldType Tags string Comments []string Markers []string @@ -593,7 +726,7 @@ func TestAPIFields_newChild(t *testing.T) { type args struct { name string - fieldType FieldType + fieldType markers.FieldType sample interface{} } @@ -607,14 +740,32 @@ func TestAPIFields_newChild(t *testing.T) { name: "new child for string", args: args{ name: "string", - fieldType: FieldString, + fieldType: markers.FieldString, sample: "string", }, fields: fields{}, want: &APIFields{ Name: "String", manifestName: "string", - Type: FieldString, + Type: markers.FieldString, + Sample: "string: \"string\"", + Tags: "`json:\"string,omitempty\"`", + Comments: []string{}, + Markers: []string{}, + }, + }, + { + name: "new child for string pointer", + args: args{ + name: "string", + fieldType: markers.FieldString, + sample: &testString, + }, + fields: fields{}, + want: &APIFields{ + Name: "String", + manifestName: "string", + Type: markers.FieldString, Sample: "string: \"string\"", Tags: "`json:\"string,omitempty\"`", Comments: []string{}, @@ -625,15 +776,15 @@ func TestAPIFields_newChild(t *testing.T) { name: "new child for unknown", args: args{ name: "unknown", - fieldType: FieldUnknownType, - sample: "unknown", + fieldType: markers.FieldUnknownType, + sample: []string{"test", "unknown"}, }, fields: fields{}, want: &APIFields{ Name: "Unknown", manifestName: "unknown", - Type: FieldUnknownType, - Sample: "unknown: unknown", + Type: markers.FieldUnknownType, + Sample: "unknown: [test unknown]", Tags: "`json:\"unknown,omitempty\"`", Comments: []string{}, Markers: []string{}, @@ -643,15 +794,33 @@ func TestAPIFields_newChild(t *testing.T) { name: "new child for int", args: args{ name: "int", - fieldType: FieldInt, - sample: "int", + fieldType: markers.FieldInt, + sample: 1, }, fields: fields{}, want: &APIFields{ Name: "Int", manifestName: "int", - Type: FieldInt, - Sample: "int: int", + Type: markers.FieldInt, + Sample: "int: 1", + Tags: "`json:\"int,omitempty\"`", + Comments: []string{}, + Markers: []string{}, + }, + }, + { + name: "new child for int", + args: args{ + name: "int", + fieldType: markers.FieldInt, + sample: &testInt, + }, + fields: fields{}, + want: &APIFields{ + Name: "Int", + manifestName: "int", + Type: markers.FieldInt, + Sample: "int: 1", Tags: "`json:\"int,omitempty\"`", Comments: []string{}, Markers: []string{}, @@ -661,15 +830,33 @@ func TestAPIFields_newChild(t *testing.T) { name: "new child for bool", args: args{ name: "bool", - fieldType: FieldBool, - sample: "bool", + fieldType: markers.FieldBool, + sample: true, + }, + fields: fields{}, + want: &APIFields{ + Name: "Bool", + manifestName: "bool", + Type: markers.FieldBool, + Sample: "bool: true", + Tags: "`json:\"bool,omitempty\"`", + Comments: []string{}, + Markers: []string{}, + }, + }, + { + name: "new child for bool pointer", + args: args{ + name: "bool", + fieldType: markers.FieldBool, + sample: &testBool, }, fields: fields{}, want: &APIFields{ Name: "Bool", manifestName: "bool", - Type: FieldBool, - Sample: "bool: bool", + Type: markers.FieldBool, + Sample: "bool: true", Tags: "`json:\"bool,omitempty\"`", Comments: []string{}, Markers: []string{}, @@ -679,14 +866,14 @@ func TestAPIFields_newChild(t *testing.T) { name: "new child for struct", args: args{ name: "struct", - fieldType: FieldStruct, + fieldType: markers.FieldStruct, sample: "struct", }, fields: fields{}, want: &APIFields{ Name: "Struct", manifestName: "struct", - Type: FieldStruct, + Type: markers.FieldStruct, Sample: "struct:", Tags: "`json:\"struct,omitempty\"`", Comments: []string{}, @@ -724,7 +911,7 @@ func TestAPIFields_isEqual(t *testing.T) { Name string StructName string manifestName string - Type FieldType + Type markers.FieldType Tags string Comments []string Markers []string @@ -747,11 +934,11 @@ func TestAPIFields_isEqual(t *testing.T) { name: "different field types are not equal", args: args{ input: &APIFields{ - Type: FieldString, + Type: markers.FieldString, }, }, fields: fields{ - Type: FieldStruct, + Type: markers.FieldStruct, }, want: false, }, @@ -854,7 +1041,7 @@ func TestAPIFields_AddField(t *testing.T) { Name string StructName string manifestName string - Type FieldType + Type markers.FieldType Tags string Comments []string Markers []string @@ -865,7 +1052,7 @@ func TestAPIFields_AddField(t *testing.T) { type args struct { path string - fieldType FieldType + fieldType markers.FieldType comments []string sample interface{} hasDefault bool @@ -881,7 +1068,7 @@ func TestAPIFields_AddField(t *testing.T) { name: "valid nested", args: args{ path: "nested.path", - fieldType: FieldString, + fieldType: markers.FieldString, comments: []string{"test"}, sample: "test", hasDefault: true, @@ -890,11 +1077,11 @@ func TestAPIFields_AddField(t *testing.T) { Comments: []string{"test1", "test2"}, Children: []*APIFields{ { - Type: FieldStruct, + Type: markers.FieldStruct, manifestName: "nested", Children: []*APIFields{ { - Type: FieldString, + Type: markers.FieldString, manifestName: "path", }, }, @@ -907,7 +1094,7 @@ func TestAPIFields_AddField(t *testing.T) { name: "valid flat", args: args{ path: "path", - fieldType: FieldString, + fieldType: markers.FieldString, comments: []string{"test"}, sample: "test", hasDefault: true, @@ -916,7 +1103,7 @@ func TestAPIFields_AddField(t *testing.T) { Comments: []string{"test1", "test2"}, Children: []*APIFields{ { - Type: FieldString, + Type: markers.FieldString, manifestName: "path", }, }, @@ -927,7 +1114,7 @@ func TestAPIFields_AddField(t *testing.T) { name: "valid missing", args: args{ path: "path", - fieldType: FieldString, + fieldType: markers.FieldString, comments: []string{"test"}, sample: "test", hasDefault: true, @@ -941,7 +1128,7 @@ func TestAPIFields_AddField(t *testing.T) { name: "valid missing nested", args: args{ path: "nested.path", - fieldType: FieldString, + fieldType: markers.FieldString, comments: []string{"test"}, sample: "test", hasDefault: true, @@ -955,7 +1142,7 @@ func TestAPIFields_AddField(t *testing.T) { name: "ovveride flat value results in an error", args: args{ path: "nested.path", - fieldType: FieldString, + fieldType: markers.FieldString, comments: []string{"test"}, sample: "test", hasDefault: true, @@ -974,7 +1161,7 @@ func TestAPIFields_AddField(t *testing.T) { name: "invalid nested inequal child", args: args{ path: "nested.path", - fieldType: FieldString, + fieldType: markers.FieldString, comments: []string{"test"}, sample: "test", hasDefault: true, @@ -983,11 +1170,11 @@ func TestAPIFields_AddField(t *testing.T) { Comments: []string{"test1", "test2"}, Children: []*APIFields{ { - Type: FieldStruct, + Type: markers.FieldStruct, manifestName: "nested", Children: []*APIFields{ { - Type: FieldString, + Type: markers.FieldString, manifestName: "path", Default: "value", }, diff --git a/internal/workload/v1/collection.go b/internal/workload/v1/collection.go index bd5dcd0..c129225 100644 --- a/internal/workload/v1/collection.go +++ b/internal/workload/v1/collection.go @@ -10,6 +10,7 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "github.com/vmware-tanzu-labs/operator-builder/internal/utils" + "github.com/vmware-tanzu-labs/operator-builder/internal/workload/v1/markers" ) const ( @@ -155,7 +156,7 @@ func (c *WorkloadCollection) SetRBAC() { } func (c *WorkloadCollection) SetResources(workloadPath string) error { - err := c.Spec.processManifests(FieldMarkerType, CollectionMarkerType) + err := c.Spec.processManifests(markers.FieldMarkerType, markers.CollectionMarkerType) if err != nil { return err } @@ -163,7 +164,7 @@ func (c *WorkloadCollection) SetResources(workloadPath string) error { for _, cpt := range c.Spec.Components { for _, csr := range cpt.Spec.Resources { // add to spec fields if not present - err := c.Spec.processMarkers(csr, CollectionMarkerType) + err := c.Spec.processMarkers(csr, markers.CollectionMarkerType) if err != nil { return err } diff --git a/internal/workload/v1/component.go b/internal/workload/v1/component.go index 7c6e3fb..90380d5 100644 --- a/internal/workload/v1/component.go +++ b/internal/workload/v1/component.go @@ -10,6 +10,7 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "github.com/vmware-tanzu-labs/operator-builder/internal/utils" + "github.com/vmware-tanzu-labs/operator-builder/internal/workload/v1/markers" ) var ErrNoComponentsOnComponent = errors.New("cannot set component workloads on a component workload - only on collections") @@ -137,7 +138,7 @@ func (c *ComponentWorkload) SetRBAC() { } func (c *ComponentWorkload) SetResources(workloadPath string) error { - err := c.Spec.processManifests(FieldMarkerType) + err := c.Spec.processManifests(markers.FieldMarkerType) if err != nil { return err } diff --git a/internal/workload/v1/config.go b/internal/workload/v1/config.go index 04fe405..f1b0947 100644 --- a/internal/workload/v1/config.go +++ b/internal/workload/v1/config.go @@ -12,6 +12,8 @@ import ( "github.com/go-playground/validator" "gopkg.in/yaml.v3" + + "github.com/vmware-tanzu-labs/operator-builder/internal/workload/v1/markers" ) var ( @@ -114,9 +116,9 @@ func ProcessAPIConfig(workloadConfig string) (WorkloadAPIBuilder, error) { workload.SetNames() workload.SetRBAC() - markers := &markerCollection{ - fieldMarkers: []*FieldMarker{}, - collectionFieldMarkers: []*CollectionFieldMarker{}, + fieldMarkers := &markers.MarkerCollection{ + FieldMarkers: []*markers.FieldMarker{}, + CollectionFieldMarkers: []*markers.CollectionFieldMarker{}, } for _, component := range components { @@ -131,21 +133,21 @@ func ProcessAPIConfig(workloadConfig string) (WorkloadAPIBuilder, error) { component.SetNames() component.SetRBAC() - markers.fieldMarkers = append(markers.fieldMarkers, component.Spec.FieldMarkers...) - markers.collectionFieldMarkers = append(markers.collectionFieldMarkers, component.Spec.CollectionFieldMarkers...) + fieldMarkers.FieldMarkers = append(fieldMarkers.FieldMarkers, component.Spec.FieldMarkers...) + fieldMarkers.CollectionFieldMarkers = append(fieldMarkers.CollectionFieldMarkers, component.Spec.CollectionFieldMarkers...) } if collection != nil { - markers.fieldMarkers = append(markers.fieldMarkers, collection.Spec.FieldMarkers...) - markers.collectionFieldMarkers = append(markers.collectionFieldMarkers, collection.Spec.CollectionFieldMarkers...) + fieldMarkers.FieldMarkers = append(fieldMarkers.FieldMarkers, collection.Spec.FieldMarkers...) + fieldMarkers.CollectionFieldMarkers = append(fieldMarkers.CollectionFieldMarkers, collection.Spec.CollectionFieldMarkers...) - if err := collection.Spec.processResourceMarkers(markers); err != nil { + if err := collection.Spec.processResourceMarkers(fieldMarkers); err != nil { return nil, err } } for _, component := range components { - if err := component.Spec.processResourceMarkers(markers); err != nil { + if err := component.Spec.processResourceMarkers(fieldMarkers); err != nil { return nil, err } } diff --git a/internal/workload/v1/markers.go b/internal/workload/v1/markers.go deleted file mode 100644 index 03f0578..0000000 --- a/internal/workload/v1/markers.go +++ /dev/null @@ -1,465 +0,0 @@ -// Copyright 2021 VMware, Inc. -// SPDX-License-Identifier: MIT - -package v1 - -import ( - "errors" - "fmt" - "regexp" - "strings" - - "gopkg.in/yaml.v3" - - "github.com/vmware-tanzu-labs/operator-builder/internal/markers/inspect" - "github.com/vmware-tanzu-labs/operator-builder/internal/markers/marker" -) - -var ( - ErrReservedFieldMarker = errors.New("field marker cannot be used and is reserved for internal purposes") - ErrNumberResourceMarkers = errors.New("expected only 1 resource marker") - ErrAssociateResourceMarker = errors.New("unable to associate resource marker with field marker") -) - -const ( - FieldMarkerType MarkerType = iota - CollectionMarkerType - ResourceMarkerType -) - -const ( - collectionFieldMarker = "+operator-builder:collection:field" - fieldMarker = "+operator-builder:field" - resourceMarker = "+operator-builder:resource" - - collectionFieldSpecPrefix = "collection.Spec" - fieldSpecPrefix = "parent.Spec" - - resourceMarkerCollectionFieldName = "collectionField" - resourceMarkerFieldName = "field" -) - -type MarkerType int - -type FieldMarker struct { - Name string - Type FieldType - Description *string - Default interface{} `marker:",optional"` - Replace *string - - forCollection bool - originalValue interface{} -} - -type ResourceMarker struct { - Field *string - CollectionField *string - Value interface{} - Include *bool - - sourceCodeVar string - sourceCodeValue string - fieldMarker interface{} -} - -type markerCollection struct { - fieldMarkers []*FieldMarker - collectionFieldMarkers []*CollectionFieldMarker -} - -var ( - ErrMismatchedMarkerTypes = errors.New("resource marker and field marker have mismatched types") - ErrResourceMarkerUnknownValueType = errors.New("resource marker 'value' is of unknown type") - ErrResourceMarkerMissingFieldValue = errors.New("resource marker missing 'collectionField', 'field' or 'value'") - ErrResourceMarkerMissingInclude = errors.New("resource marker missing 'include' value") - ErrResourceMarkerMissingFieldMarker = errors.New("resource marker has no associated 'field' or 'collectionField' marker") - ErrFieldMarkerInvalidType = errors.New("field marker type is invalid") -) - -//nolint:gocritic //needed to implement string interface -func (fm FieldMarker) String() string { - return fmt.Sprintf("FieldMarker{Name: %s Type: %v Description: %q Default: %v}", - fm.Name, - fm.Type, - *fm.Description, - fm.Default, - ) -} - -func defineFieldMarker(registry *marker.Registry) error { - fieldMarker, err := marker.Define(fieldMarker, FieldMarker{}) - if err != nil { - return fmt.Errorf("%w", err) - } - - registry.Add(fieldMarker) - - return nil -} - -type CollectionFieldMarker FieldMarker - -//nolint:gocritic //needed to implement string interface -func (cfm CollectionFieldMarker) String() string { - return fmt.Sprintf("CollectionFieldMarker{Name: %s Type: %v Description: %q Default: %v}", - cfm.Name, - cfm.Type, - *cfm.Description, - cfm.Default, - ) -} - -func defineCollectionFieldMarker(registry *marker.Registry) error { - collectionMarker, err := marker.Define(collectionFieldMarker, CollectionFieldMarker{}) - if err != nil { - return fmt.Errorf("%w", err) - } - - registry.Add(collectionMarker) - - return nil -} - -//nolint:gocritic //needed to implement string interface -func (rm ResourceMarker) String() string { - return fmt.Sprintf("ResourceMarker{Field: %s CollectionField: %s Value: %v Include: %v}", - *rm.Field, - *rm.CollectionField, - rm.Value, - *rm.Include, - ) -} - -func defineResourceMarker(registry *marker.Registry) error { - resourceMarker, err := marker.Define(resourceMarker, ResourceMarker{}) - if err != nil { - return fmt.Errorf("%w", err) - } - - registry.Add(resourceMarker) - - return nil -} - -func reservedMarkerNames() []string { - return []string{ - "collection", - "collection.Name", - "collection.Namespace", - } -} - -func isReservedMarker(name string) bool { - for _, reserved := range reservedMarkerNames() { - if name == reserved { - return true - } - } - - return false -} - -func InitializeMarkerInspector(markerTypes ...MarkerType) (*inspect.Inspector, error) { - registry := marker.NewRegistry() - - var err error - - for _, markerType := range markerTypes { - switch markerType { - case FieldMarkerType: - err = defineFieldMarker(registry) - case CollectionMarkerType: - err = defineCollectionFieldMarker(registry) - case ResourceMarkerType: - err = defineResourceMarker(registry) - } - } - - if err != nil { - return nil, err - } - - return inspect.NewInspector(registry), nil -} - -//nolint:gocognit -func TransformYAML(results ...*inspect.YAMLResult) error { - const varTag = "!!var" - - const strTag = "!!str" - - var key *yaml.Node - - var value *yaml.Node - - for _, r := range results { - if len(r.Nodes) > 1 { - key = r.Nodes[0] - value = r.Nodes[1] - } else { - key = r.Nodes[0] - value = r.Nodes[0] - } - - replaceText := strings.TrimSuffix(r.MarkerText, "\n") - replaceText = strings.ReplaceAll(replaceText, "\n", "\n#") - - key.FootComment = "" - - switch t := r.Object.(type) { - case FieldMarker: - if isReservedMarker(t.Name) { - return fmt.Errorf("%s %w", t.Name, ErrReservedFieldMarker) - } - - if t.Description != nil { - *t.Description = strings.TrimPrefix(*t.Description, "\n") - key.HeadComment = key.HeadComment + "\n# " + *t.Description - } - - key.HeadComment = strings.ReplaceAll(key.HeadComment, replaceText, "controlled by field: "+t.Name) - value.LineComment = strings.ReplaceAll(value.LineComment, replaceText, "controlled by field: "+t.Name) - - t.originalValue = value.Value - - if t.Replace != nil { - value.Tag = strTag - - re, err := regexp.Compile(*t.Replace) - if err != nil { - return fmt.Errorf("unable to convert %s to regex, %w", *t.Replace, err) - } - - value.Value = re.ReplaceAllString(value.Value, fmt.Sprintf("!!start parent.Spec.%s !!end", strings.Title((t.Name)))) - } else { - value.Tag = varTag - value.Value = getResourceDefinitionVar(strings.Title(t.Name), false) - } - - r.Object = t - - case CollectionFieldMarker: - if isReservedMarker(t.Name) { - return fmt.Errorf("%s %w", t.Name, ErrReservedFieldMarker) - } - - if t.Description != nil { - *t.Description = strings.TrimPrefix(*t.Description, "\n") - key.HeadComment = "# " + *t.Description - } - - key.HeadComment = strings.ReplaceAll(key.HeadComment, replaceText, "controlled by collection field: "+t.Name) - value.LineComment = strings.ReplaceAll(value.LineComment, replaceText, "controlled by collection field: "+t.Name) - - t.originalValue = value.Value - - if t.Replace != nil { - value.Tag = strTag - - re, err := regexp.Compile(*t.Replace) - if err != nil { - return fmt.Errorf("unable to convert %s to regex, %w", *t.Replace, err) - } - - value.Value = re.ReplaceAllString(value.Value, fmt.Sprintf("!!start collection.Spec.%s !!end", strings.Title((t.Name)))) - } else { - value.Tag = varTag - value.Value = getResourceDefinitionVar(strings.Title(t.Name), true) - } - - r.Object = t - } - } - - return nil -} - -func containsMarkerType(s []MarkerType, e MarkerType) bool { - for _, a := range s { - if a == e { - return true - } - } - - return false -} - -func inspectMarkersForYAML(yamlContent []byte, markerTypes ...MarkerType) ([]*yaml.Node, []*inspect.YAMLResult, error) { - insp, err := InitializeMarkerInspector(markerTypes...) - if err != nil { - return nil, nil, fmt.Errorf("%w; error initializing markers %v", err, markerTypes) - } - - nodes, results, err := insp.InspectYAML(yamlContent, TransformYAML) - if err != nil { - return nil, nil, fmt.Errorf("%w; error inspecting YAML for markers %v", err, markerTypes) - } - - return nodes, results, nil -} - -func getResourceDefinitionVar(path string, forCollectionMarker bool) string { - // return the collectionFieldSpecPrefix only on a non-collection child resource - // with a collection marker - if forCollectionMarker { - return fmt.Sprintf("%s.%s", collectionFieldSpecPrefix, strings.Title(path)) - } - - return fmt.Sprintf("%s.%s", fieldSpecPrefix, strings.Title(path)) -} - -func (rm *ResourceMarker) setSourceCodeVar() { - if rm.Field != nil { - rm.sourceCodeVar = getResourceDefinitionVar(*rm.Field, false) - } else { - rm.sourceCodeVar = getResourceDefinitionVar(*rm.CollectionField, true) - } -} - -func (rm *ResourceMarker) hasField() bool { - var hasField, hasCollectionField bool - - if rm.Field != nil { - if *rm.Field != "" { - hasField = true - } - } - - if rm.CollectionField != nil { - if *rm.CollectionField != "" { - hasCollectionField = true - } - } - - return hasField || hasCollectionField -} - -func (rm *ResourceMarker) hasValue() bool { - return rm.Value != nil -} - -func (rm *ResourceMarker) associateFieldMarker(markers *markerCollection) { - // return immediately if our entire workload spec has no field markers - if len(markers.collectionFieldMarkers) == 0 && len(markers.fieldMarkers) == 0 { - return - } - - // associate first relevant field marker with this marker - for _, fm := range markers.fieldMarkers { - if rm.Field != nil { - if fm.Name == *rm.Field { - rm.fieldMarker = fm - - return - } - } - - if fm.forCollection { - if rm.CollectionField != nil { - if fm.Name == *rm.CollectionField { - rm.fieldMarker = fm - - return - } - } - } - } - - // associate first relevant collection field marker with this marker - for _, cm := range markers.collectionFieldMarkers { - if rm.CollectionField != nil { - if cm.Name == *rm.CollectionField { - rm.fieldMarker = cm - - return - } - } - } -} - -func (rm *ResourceMarker) validate() error { - // check include field for a provided value - // NOTE: this field is mandatory now, but could be optional later, so we return - // an error here rather than using a pointer to a bool to control the mandate. - if rm.Include == nil { - return fmt.Errorf("%w for marker %s", ErrResourceMarkerMissingInclude, rm) - } - - if rm.fieldMarker == nil { - return fmt.Errorf("%w for marker %s", ErrResourceMarkerMissingFieldMarker, rm) - } - - // ensure that both a field and value exist - if !rm.hasField() || !rm.hasValue() { - return fmt.Errorf("%w for marker %s", ErrResourceMarkerMissingFieldValue, rm) - } - - return nil -} - -func (rm *ResourceMarker) process() error { - if err := rm.validate(); err != nil { - return err - } - - var fieldType string - - // determine if our associated field marker is a collection or regular field marker and - // set appropriate variables - switch marker := rm.fieldMarker.(type) { - case *CollectionFieldMarker: - fieldType = marker.Type.String() - - rm.setSourceCodeVar() - case *FieldMarker: - fieldType = marker.Type.String() - - rm.setSourceCodeVar() - default: - return fmt.Errorf("%w; type %T for marker %s", ErrFieldMarkerInvalidType, fieldMarker, rm) - } - - // set the sourceCodeValue to check against - switch value := rm.Value.(type) { - case string: - if fieldType != "string" { - return fmt.Errorf("%w; expected: string, got: %s for marker %s", ErrMismatchedMarkerTypes, fieldType, rm) - } - - rm.sourceCodeValue = fmt.Sprintf("%q", value) - case int: - if fieldType != "int" { - return fmt.Errorf("%w; expected: int, got: %s for marker %s", ErrMismatchedMarkerTypes, fieldType, rm) - } - - rm.sourceCodeValue = fmt.Sprintf("%v", value) - case bool: - if fieldType != "bool" { - return fmt.Errorf("%w; expected: bool, got: %s for marker %s", ErrMismatchedMarkerTypes, fieldType, rm) - } - - rm.sourceCodeValue = fmt.Sprintf("%v", value) - default: - return ErrResourceMarkerUnknownValueType - } - - return nil -} - -// filterResourceMarkers removes all comments from the results as resource markers -// are required to exist on the same line. -func filterResourceMarkers(results []*inspect.YAMLResult) []*ResourceMarker { - var markers []*ResourceMarker - - for _, marker := range results { - switch marker := marker.Object.(type) { - case ResourceMarker: - markers = append(markers, &marker) - default: - continue - } - } - - return markers -} diff --git a/internal/workload/v1/markers/collection_field_marker.go b/internal/workload/v1/markers/collection_field_marker.go new file mode 100644 index 0000000..57a0e72 --- /dev/null +++ b/internal/workload/v1/markers/collection_field_marker.go @@ -0,0 +1,111 @@ +// Copyright 2021 VMware, Inc. +// SPDX-License-Identifier: MIT + +package markers + +import ( + "fmt" + + "github.com/vmware-tanzu-labs/operator-builder/internal/markers/marker" +) + +const ( + CollectionFieldMarkerPrefix = "+operator-builder:collection:field" + CollectionFieldSpecPrefix = "collection.Spec" +) + +// CollectionFieldMarker is an object which represents a marker that is associated with a +// collection field that exsists within a manifest. A CollectionFieldMarker is discovered +// when a manifest is parsed and matches the constants defined by the collectionFieldMarker +// constant above. It is represented identically to a FieldMarker with the caveat that it +// is discovered different via a different prefix. +type CollectionFieldMarker FieldMarker + +//nolint:gocritic //needed to implement string interface +func (cfm CollectionFieldMarker) String() string { + return fmt.Sprintf("CollectionFieldMarker{Name: %s Type: %v Description: %q Default: %v}", + cfm.Name, + cfm.Type, + cfm.GetDescription(), + cfm.Default, + ) +} + +// defineCollectionFieldMarker will define a CollectionFieldMarker and add it a registry of markers. +func defineCollectionFieldMarker(registry *marker.Registry) error { + collectionMarker, err := marker.Define(CollectionFieldMarkerPrefix, CollectionFieldMarker{}) + if err != nil { + return fmt.Errorf("%w", err) + } + + registry.Add(collectionMarker) + + return nil +} + +// +// FieldMarkerProcessor interface methods. +// +func (cfm *CollectionFieldMarker) GetName() string { + return cfm.Name +} + +func (cfm *CollectionFieldMarker) GetDefault() interface{} { + return cfm.Default +} + +func (cfm *CollectionFieldMarker) GetDescription() string { + if cfm.Description == nil { + return "" + } + + return *cfm.Description +} + +func (cfm *CollectionFieldMarker) GetFieldType() FieldType { + return cfm.Type +} + +func (cfm *CollectionFieldMarker) GetReplaceText() string { + if cfm.Replace == nil { + return "" + } + + return *cfm.Replace +} + +func (cfm *CollectionFieldMarker) GetSpecPrefix() string { + return CollectionFieldSpecPrefix +} + +func (cfm *CollectionFieldMarker) GetSourceCodeVariable() string { + return cfm.sourceCodeVar +} + +func (cfm *CollectionFieldMarker) GetOriginalValue() interface{} { + return cfm.originalValue +} + +func (cfm *CollectionFieldMarker) IsCollectionFieldMarker() bool { + return true +} + +func (cfm *CollectionFieldMarker) IsFieldMarker() bool { + return false +} + +func (cfm *CollectionFieldMarker) IsForCollection() bool { + return cfm.forCollection +} + +func (cfm *CollectionFieldMarker) SetOriginalValue(value string) { + cfm.originalValue = &value +} + +func (cfm *CollectionFieldMarker) SetDescription(description string) { + cfm.Description = &description +} + +func (cfm *CollectionFieldMarker) SetForCollection(forCollection bool) { + cfm.forCollection = forCollection +} diff --git a/internal/workload/v1/markers/collection_field_marker_internal_test.go b/internal/workload/v1/markers/collection_field_marker_internal_test.go new file mode 100644 index 0000000..376b241 --- /dev/null +++ b/internal/workload/v1/markers/collection_field_marker_internal_test.go @@ -0,0 +1,702 @@ +// Copyright 2021 VMware, Inc. +// SPDX-License-Identifier: MIT + +package markers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/vmware-tanzu-labs/operator-builder/internal/markers/marker" +) + +func TestCollectionFieldMarker_String(t *testing.T) { + t.Parallel() + + testString := "cfm test" + + type fields struct { + Name string + Type FieldType + Description *string + Default interface{} + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure collection field string output matches expected", + fields: fields{ + Name: "test", + Type: FieldString, + Description: &testString, + Default: "test", + }, + want: "CollectionFieldMarker{Name: test Type: string Description: \"cfm test\" Default: test}", + }, + { + name: "ensure collection field with nil values output matches expected", + fields: fields{ + Name: "test", + Type: FieldString, + Description: nil, + Default: "test", + }, + want: "CollectionFieldMarker{Name: test Type: string Description: \"\" Default: test}", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := CollectionFieldMarker{ + Name: tt.fields.Name, + Type: tt.fields.Type, + Description: tt.fields.Description, + Default: tt.fields.Default, + } + if got := cfm.String(); got != tt.want { + t.Errorf("CollectionFieldMarker.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_defineCollectionFieldMarker(t *testing.T) { + t.Parallel() + + type args struct { + registry *marker.Registry + } + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "ensure valid registry can properly add a collection field marker", + args: args{ + registry: marker.NewRegistry(), + }, + wantErr: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if err := defineCollectionFieldMarker(tt.args.registry); (err != nil) != tt.wantErr { + t.Errorf("defineCollectionFieldMarker() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCollectionFieldMarker_GetDefault(t *testing.T) { + t.Parallel() + + cfmDefault := "this is a collection default value" + + type fields struct { + Default interface{} + } + + tests := []struct { + name string + fields fields + want interface{} + }{ + { + name: "ensure collection field default returns as expected", + fields: fields{ + Default: cfmDefault, + }, + want: cfmDefault, + }, + { + name: "ensure collection field default with nil value returns as expected", + fields: fields{ + Default: nil, + }, + want: nil, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{ + Default: tt.fields.Default, + } + if got := cfm.GetDefault(); got != tt.want { + t.Errorf("CollectionFieldMarker.GetDefault() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCollectionFieldMarker_GetDescription(t *testing.T) { + t.Parallel() + + type fields struct { + Name string + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure collection field name returns as expected", + fields: fields{ + Name: "test", + }, + want: "test", + }, + { + name: "ensure collection field name with empty value returns as expected", + fields: fields{ + Name: "", + }, + want: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{ + Name: tt.fields.Name, + } + if got := cfm.GetName(); got != tt.want { + t.Errorf("CollectionFieldMarker.GetName() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCollectionFieldMarker_GetName(t *testing.T) { + t.Parallel() + + cfmDescription := "test collection description" + + type fields struct { + Description *string + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure collection field description returns as expected", + fields: fields{ + Description: &cfmDescription, + }, + want: cfmDescription, + }, + { + name: "ensure collection field description with nil value returns as expected", + fields: fields{ + Description: nil, + }, + want: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{ + Description: tt.fields.Description, + } + if got := cfm.GetDescription(); got != tt.want { + t.Errorf("CollectionFieldMarker.GetDescription() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCollectionFieldMarker_GetFieldType(t *testing.T) { + t.Parallel() + + type fields struct { + Type FieldType + } + + tests := []struct { + name string + fields fields + want FieldType + }{ + { + name: "ensure collection field type string returns as expected", + fields: fields{ + Type: FieldString, + }, + want: FieldString, + }, + { + name: "ensure collection field type struct returns as expected", + fields: fields{ + Type: FieldStruct, + }, + want: FieldStruct, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{ + Type: tt.fields.Type, + } + if got := cfm.GetFieldType(); got != tt.want { + t.Errorf("CollectionFieldMarker.GetFieldType() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCollectionFieldMarker_GetReplaceText(t *testing.T) { + t.Parallel() + + cfmReplace := "test collection replace" + + type fields struct { + Replace *string + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure collection field replace text returns as expected", + fields: fields{ + Replace: &cfmReplace, + }, + want: cfmReplace, + }, + { + name: "ensure collection field replace text with empty value returns as expected", + fields: fields{ + Replace: nil, + }, + want: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{ + Replace: tt.fields.Replace, + } + if got := cfm.GetReplaceText(); got != tt.want { + t.Errorf("CollectionFieldMarker.GetReplaceText() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCollectionFieldMarker_GetSpecPrefix(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want string + }{ + { + name: "ensure collection field returns correct spec prefix", + want: CollectionFieldSpecPrefix, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{} + if got := cfm.GetSpecPrefix(); got != tt.want { + t.Errorf("CollectionFieldMarker.GetSpecPrefix() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCollectionFieldMarker_GetOriginalValue(t *testing.T) { + t.Parallel() + + type fields struct { + originalValue interface{} + } + + tests := []struct { + name string + fields fields + want interface{} + }{ + { + name: "ensure collection field original value string returns as expected", + fields: fields{ + originalValue: "test", + }, + want: "test", + }, + { + name: "ensure collection field original value integer returns as expected", + fields: fields{ + originalValue: 1, + }, + want: 1, + }, + { + name: "ensure collection field original value negative integer returns as expected", + fields: fields{ + originalValue: -1, + }, + want: -1, + }, + { + name: "ensure collection field original value nil returns as expected", + fields: fields{ + originalValue: nil, + }, + want: nil, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{ + originalValue: tt.fields.originalValue, + } + if got := cfm.GetOriginalValue(); got != tt.want { + t.Errorf("CollectionFieldMarker.GetOriginalValue() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCollectionFieldMarker_GetSourceCodeVariable(t *testing.T) { + t.Parallel() + + type fields struct { + sourceCodeVar string + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "source code variable for field marker returns correctly", + fields: fields{ + sourceCodeVar: "this.Is.A.Test", + }, + want: "this.Is.A.Test", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{ + sourceCodeVar: tt.fields.sourceCodeVar, + } + if got := cfm.GetSourceCodeVariable(); got != tt.want { + t.Errorf("CollectionFieldMarker.GetSourceCodeVariable() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCollectionFieldMarker_IsCollectionFieldMarker(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want bool + }{ + { + name: "ensure a collection field marker is always a collection field marker", + want: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{} + if got := cfm.IsCollectionFieldMarker(); got != tt.want { + t.Errorf("CollectionFieldMarker.IsCollectionFieldMarker() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCollectionFieldMarker_IsFieldMarker(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want bool + }{ + { + name: "ensure a collection field marker is never a field marker", + want: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{} + if got := cfm.IsFieldMarker(); got != tt.want { + t.Errorf("CollectionFieldMarker.IsFieldMarker() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCollectionFieldMarker_IsForCollection(t *testing.T) { + t.Parallel() + + type fields struct { + forCollection bool + } + + tests := []struct { + name string + fields fields + want bool + }{ + { + // this is technically an invalid test case as collection field markers on collections + // are automatically converted to field markers. test this anyway. + name: "ensure collection field for collection returns true", + fields: fields{ + forCollection: true, + }, + want: true, + }, + { + name: "ensure collection field for non-collection returns false", + fields: fields{ + forCollection: false, + }, + want: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{ + forCollection: tt.fields.forCollection, + } + if got := cfm.IsForCollection(); got != tt.want { + t.Errorf("CollectionFieldMarker.IsForCollection() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCollectionFieldMarker_SetOriginalValue(t *testing.T) { + t.Parallel() + + cfmOriginalValue := "testCollectionUpdateOriginal" + + type fields struct { + originalValue interface{} + } + + type args struct { + value string + } + + tests := []struct { + name string + fields fields + args args + want *CollectionFieldMarker + }{ + { + name: "ensure collection field original value with string already set is set as expected", + fields: fields{ + originalValue: "test", + }, + args: args{ + value: cfmOriginalValue, + }, + want: &CollectionFieldMarker{ + originalValue: &cfmOriginalValue, + }, + }, + { + name: "ensure collection field original value wihtout string already set is set as expected", + args: args{ + value: cfmOriginalValue, + }, + want: &CollectionFieldMarker{ + originalValue: &cfmOriginalValue, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{ + originalValue: tt.fields.originalValue, + } + cfm.SetOriginalValue(tt.args.value) + assert.Equal(t, tt.want, cfm) + }) + } +} + +func TestCollectionFieldMarker_SetDescription(t *testing.T) { + t.Parallel() + + cfmSetDescription := "testCollectionUpdate" + cfmSetDescriptionExist := "testCollectionExist" + + type fields struct { + Description *string + } + + type args struct { + description string + } + + tests := []struct { + name string + fields fields + args args + want *CollectionFieldMarker + }{ + { + name: "ensure collection field description with string already set is set as expected", + fields: fields{ + Description: &cfmSetDescriptionExist, + }, + args: args{ + description: cfmSetDescription, + }, + want: &CollectionFieldMarker{ + Description: &cfmSetDescription, + }, + }, + { + name: "ensure collection field description wihtout string already set is set as expected", + args: args{ + description: cfmSetDescription, + }, + want: &CollectionFieldMarker{ + Description: &cfmSetDescription, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{ + Description: tt.fields.Description, + } + cfm.SetDescription(tt.args.description) + assert.Equal(t, tt.want, cfm) + }) + } +} + +func TestCollectionFieldMarker_SetForCollection(t *testing.T) { + t.Parallel() + + type fields struct { + forCollection bool + } + + type args struct { + forCollection bool + } + + tests := []struct { + name string + fields fields + args args + want *CollectionFieldMarker + }{ + { + name: "ensure collection field for collection already set is set as expected (true > false)", + fields: fields{ + forCollection: true, + }, + args: args{ + forCollection: false, + }, + want: &CollectionFieldMarker{ + forCollection: false, + }, + }, + { + name: "ensure collection field for collection already set is set as expected (false > true)", + fields: fields{ + forCollection: false, + }, + args: args{ + forCollection: true, + }, + want: &CollectionFieldMarker{ + forCollection: true, + }, + }, + { + name: "ensure collection field for collection wihtout already set is set as expected", + args: args{ + forCollection: true, + }, + want: &CollectionFieldMarker{ + forCollection: true, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfm := &CollectionFieldMarker{ + forCollection: tt.fields.forCollection, + } + cfm.SetForCollection(tt.args.forCollection) + assert.Equal(t, tt.want, cfm) + }) + } +} diff --git a/internal/workload/v1/markers/field_marker.go b/internal/workload/v1/markers/field_marker.go new file mode 100644 index 0000000..aafb852 --- /dev/null +++ b/internal/workload/v1/markers/field_marker.go @@ -0,0 +1,127 @@ +// Copyright 2021 VMware, Inc. +// SPDX-License-Identifier: MIT + +package markers + +import ( + "errors" + "fmt" + + "github.com/vmware-tanzu-labs/operator-builder/internal/markers/marker" +) + +var ( + ErrFieldMarkerReserved = errors.New("field marker cannot be used and is reserved for internal purposes") + ErrFieldMarkerInvalidType = errors.New("field marker type is invalid") +) + +const ( + FieldMarkerPrefix = "+operator-builder:field" + FieldSpecPrefix = "parent.Spec" +) + +// FieldMarker is an object which represents a marker that is associated with a field +// that exsists within a manifest. A FieldMarker is discovered when a manifest is parsed +// and matches the constants defined by the fieldMarker constant above. +type FieldMarker struct { + // inputs from the marker itself + Name string + Type FieldType + Description *string + Default interface{} `marker:",optional"` + Replace *string + + // other values which we use to pass information + forCollection bool + sourceCodeVar string + originalValue interface{} +} + +//nolint:gocritic //needed to implement string interface +func (fm FieldMarker) String() string { + return fmt.Sprintf("FieldMarker{Name: %s Type: %v Description: %q Default: %v}", + fm.Name, + fm.Type, + fm.GetDescription(), + fm.Default, + ) +} + +// defineFieldMarker will define a FieldMarker and add it a registry of markers. +func defineFieldMarker(registry *marker.Registry) error { + fieldMarker, err := marker.Define(FieldMarkerPrefix, FieldMarker{}) + if err != nil { + return fmt.Errorf("%w", err) + } + + registry.Add(fieldMarker) + + return nil +} + +// +// FieldMarker Processor interface methods. +// +func (fm *FieldMarker) GetName() string { + return fm.Name +} + +func (fm *FieldMarker) GetDefault() interface{} { + return fm.Default +} + +func (fm *FieldMarker) GetDescription() string { + if fm.Description == nil { + return "" + } + + return *fm.Description +} + +func (fm *FieldMarker) GetFieldType() FieldType { + return fm.Type +} + +func (fm *FieldMarker) GetReplaceText() string { + if fm.Replace == nil { + return "" + } + + return *fm.Replace +} + +func (fm *FieldMarker) GetSpecPrefix() string { + return FieldSpecPrefix +} + +func (fm *FieldMarker) GetOriginalValue() interface{} { + return fm.originalValue +} + +func (fm *FieldMarker) GetSourceCodeVariable() string { + return fm.sourceCodeVar +} + +func (fm *FieldMarker) IsCollectionFieldMarker() bool { + return false +} + +func (fm *FieldMarker) IsFieldMarker() bool { + return true +} + +func (fm *FieldMarker) IsForCollection() bool { + return fm.forCollection +} + +func (fm *FieldMarker) SetOriginalValue(value string) { + fm.originalValue = &value +} + +func (fm *FieldMarker) SetDescription(description string) { + fm.Description = &description +} + +func (fm *FieldMarker) SetForCollection(forCollection bool) { + fm.forCollection = forCollection +} diff --git a/internal/workload/v1/markers/field_marker_internal_test.go b/internal/workload/v1/markers/field_marker_internal_test.go new file mode 100644 index 0000000..1f11cbb --- /dev/null +++ b/internal/workload/v1/markers/field_marker_internal_test.go @@ -0,0 +1,700 @@ +// Copyright 2021 VMware, Inc. +// SPDX-License-Identifier: MIT + +package markers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/vmware-tanzu-labs/operator-builder/internal/markers/marker" +) + +func TestFieldMarker_String(t *testing.T) { + t.Parallel() + + testString := "fm test" + + type fields struct { + Name string + Type FieldType + Description *string + Default interface{} + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure field string output matches expected", + fields: fields{ + Name: "test", + Type: FieldString, + Description: &testString, + Default: "test", + }, + want: "FieldMarker{Name: test Type: string Description: \"fm test\" Default: test}", + }, + { + name: "ensure field with nil values output matches expected", + fields: fields{ + Name: "test", + Type: FieldString, + Description: nil, + Default: "test", + }, + want: "FieldMarker{Name: test Type: string Description: \"\" Default: test}", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := FieldMarker{ + Name: tt.fields.Name, + Type: tt.fields.Type, + Description: tt.fields.Description, + Default: tt.fields.Default, + } + if got := fm.String(); got != tt.want { + t.Errorf("FieldMarker.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_defineFieldMarker(t *testing.T) { + t.Parallel() + + type args struct { + registry *marker.Registry + } + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "ensure valid registry can properly add a field marker", + args: args{ + registry: marker.NewRegistry(), + }, + wantErr: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if err := defineFieldMarker(tt.args.registry); (err != nil) != tt.wantErr { + t.Errorf("defineFieldMarker() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestFieldMarker_GetDefault(t *testing.T) { + t.Parallel() + + fmDefault := "this is a field default value" + + type fields struct { + Default interface{} + } + + tests := []struct { + name string + fields fields + want interface{} + }{ + { + name: "ensure field default returns as expected", + fields: fields{ + Default: fmDefault, + }, + want: fmDefault, + }, + { + name: "ensure field default with nil value returns as expected", + fields: fields{ + Default: nil, + }, + want: nil, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{ + Default: tt.fields.Default, + } + if got := fm.GetDefault(); got != tt.want { + t.Errorf("FieldMarker.GetDefault() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFieldMarker_GetDescription(t *testing.T) { + t.Parallel() + + type fields struct { + Name string + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure field name returns as expected", + fields: fields{ + Name: "test", + }, + want: "test", + }, + { + name: "ensure field name with empty value returns as expected", + fields: fields{ + Name: "", + }, + want: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{ + Name: tt.fields.Name, + } + if got := fm.GetName(); got != tt.want { + t.Errorf("FieldMarker.GetName() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFieldMarker_GetName(t *testing.T) { + t.Parallel() + + fmDescription := "test description" + + type fields struct { + Description *string + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure field description returns as expected", + fields: fields{ + Description: &fmDescription, + }, + want: "test description", + }, + { + name: "ensure field description with nil value returns as expected", + fields: fields{ + Description: nil, + }, + want: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{ + Description: tt.fields.Description, + } + if got := fm.GetDescription(); got != tt.want { + t.Errorf("FieldMarker.GetDescription() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFieldMarker_GetFieldType(t *testing.T) { + t.Parallel() + + type fields struct { + Type FieldType + } + + tests := []struct { + name string + fields fields + want FieldType + }{ + { + name: "ensure field type string returns as expected", + fields: fields{ + Type: FieldString, + }, + want: FieldString, + }, + { + name: "ensure field type struct returns as expected", + fields: fields{ + Type: FieldStruct, + }, + want: FieldStruct, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{ + Type: tt.fields.Type, + } + if got := fm.GetFieldType(); got != tt.want { + t.Errorf("FieldMarker.GetFieldType() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFieldMarker_GetReplaceText(t *testing.T) { + t.Parallel() + + fmReplace := "test replace" + + type fields struct { + Replace *string + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure field replace text returns as expected", + fields: fields{ + Replace: &fmReplace, + }, + want: fmReplace, + }, + { + name: "ensure field replace text with empty value returns as expected", + fields: fields{ + Replace: nil, + }, + want: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{ + Replace: tt.fields.Replace, + } + if got := fm.GetReplaceText(); got != tt.want { + t.Errorf("FieldMarker.GetReplaceText() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFieldMarker_GetSpecPrefix(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want string + }{ + { + name: "ensure field returns correct spec prefix", + want: FieldSpecPrefix, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{} + if got := fm.GetSpecPrefix(); got != tt.want { + t.Errorf("FieldMarker.GetSpecPrefix() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFieldMarker_GetOriginalValue(t *testing.T) { + t.Parallel() + + type fields struct { + originalValue interface{} + } + + tests := []struct { + name string + fields fields + want interface{} + }{ + { + name: "ensure field original value string returns as expected", + fields: fields{ + originalValue: "test", + }, + want: "test", + }, + { + name: "ensure field original value integer returns as expected", + fields: fields{ + originalValue: 1, + }, + want: 1, + }, + { + name: "ensure field original value negative integer returns as expected", + fields: fields{ + originalValue: -1, + }, + want: -1, + }, + { + name: "ensure field original value nil returns as expected", + fields: fields{ + originalValue: nil, + }, + want: nil, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{ + originalValue: tt.fields.originalValue, + } + if got := fm.GetOriginalValue(); got != tt.want { + t.Errorf("FieldMarker.GetOriginalValue() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFieldMarker_GetSourceCodeVariable(t *testing.T) { + t.Parallel() + + type fields struct { + sourceCodeVar string + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "source code variable for field marker returns correctly", + fields: fields{ + sourceCodeVar: "this.Is.A.Test", + }, + want: "this.Is.A.Test", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{ + sourceCodeVar: tt.fields.sourceCodeVar, + } + if got := fm.GetSourceCodeVariable(); got != tt.want { + t.Errorf("FieldMarker.GetSourceCodeVariable() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFieldMarker_IsCollectionFieldMarker(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want bool + }{ + { + name: "ensure a field marker is never a collection field marker", + want: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{} + if got := fm.IsCollectionFieldMarker(); got != tt.want { + t.Errorf("FieldMarker.IsCollectionFieldMarker() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFieldMarker_IsFieldMarker(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want bool + }{ + { + name: "ensure a field marker is always a field marker", + want: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{} + if got := fm.IsFieldMarker(); got != tt.want { + t.Errorf("FieldMarker.IsFieldMarker() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFieldMarker_IsForCollection(t *testing.T) { + t.Parallel() + + type fields struct { + forCollection bool + } + + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "ensure field for collection returns true", + fields: fields{ + forCollection: true, + }, + want: true, + }, + { + name: "ensure field for non-collection returns false", + fields: fields{ + forCollection: false, + }, + want: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{ + forCollection: tt.fields.forCollection, + } + if got := fm.IsForCollection(); got != tt.want { + t.Errorf("FieldMarker.IsForCollection() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFieldMarker_SetOriginalValue(t *testing.T) { + t.Parallel() + + fmOriginalValue := "testUpdateOriginal" + + type fields struct { + originalValue interface{} + } + + type args struct { + value string + } + + tests := []struct { + name string + fields fields + args args + want *FieldMarker + }{ + { + name: "ensure field original value with string already set is set as expected", + fields: fields{ + originalValue: "test", + }, + args: args{ + value: fmOriginalValue, + }, + want: &FieldMarker{ + originalValue: &fmOriginalValue, + }, + }, + { + name: "ensure field original value wihtout string already set is set as expected", + args: args{ + value: fmOriginalValue, + }, + want: &FieldMarker{ + originalValue: &fmOriginalValue, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{ + originalValue: tt.fields.originalValue, + } + fm.SetOriginalValue(tt.args.value) + assert.Equal(t, tt.want, fm) + }) + } +} + +func TestFieldMarker_SetDescription(t *testing.T) { + t.Parallel() + + fmSetDescription := "testUpdate" + fmSetDescriptionExist := "testExist" + + type fields struct { + Description *string + } + + type args struct { + description string + } + + tests := []struct { + name string + fields fields + args args + want *FieldMarker + }{ + { + name: "ensure field description with string already set is set as expected", + fields: fields{ + Description: &fmSetDescriptionExist, + }, + args: args{ + description: fmSetDescription, + }, + want: &FieldMarker{ + Description: &fmSetDescription, + }, + }, + { + name: "ensure field description wihtout string already set is set as expected", + args: args{ + description: fmSetDescription, + }, + want: &FieldMarker{ + Description: &fmSetDescription, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{ + Description: tt.fields.Description, + } + fm.SetDescription(tt.args.description) + assert.Equal(t, tt.want, fm) + }) + } +} + +func TestFieldMarker_SetForCollection(t *testing.T) { + t.Parallel() + + type fields struct { + forCollection bool + } + + type args struct { + forCollection bool + } + + tests := []struct { + name string + fields fields + args args + want *FieldMarker + }{ + { + name: "ensure field for collection already set is set as expected (true > false)", + fields: fields{ + forCollection: true, + }, + args: args{ + forCollection: false, + }, + want: &FieldMarker{ + forCollection: false, + }, + }, + { + name: "ensure field for collection already set is set as expected (false > true)", + fields: fields{ + forCollection: false, + }, + args: args{ + forCollection: true, + }, + want: &FieldMarker{ + forCollection: true, + }, + }, + { + name: "ensure field for collection wihtout already set is set as expected", + args: args{ + forCollection: true, + }, + want: &FieldMarker{ + forCollection: true, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fm := &FieldMarker{ + forCollection: tt.fields.forCollection, + } + fm.SetForCollection(tt.args.forCollection) + assert.Equal(t, tt.want, fm) + }) + } +} diff --git a/internal/workload/v1/field_type.go b/internal/workload/v1/markers/field_types.go similarity index 74% rename from internal/workload/v1/field_type.go rename to internal/workload/v1/markers/field_types.go index 1243f9e..90e7c13 100644 --- a/internal/workload/v1/field_type.go +++ b/internal/workload/v1/markers/field_types.go @@ -1,7 +1,7 @@ // Copyright 2021 VMware, Inc. // SPDX-License-Identifier: MIT -package v1 +package markers import ( "errors" @@ -10,6 +10,8 @@ import ( var ErrUnableToParseFieldType = errors.New("unable to parse field") +// FieldType defines the types of fields for a field marker that are accepted +// during parsing of a manifest. type FieldType int const ( @@ -20,6 +22,8 @@ const ( FieldStruct ) +// UnmarshalMarkerArg will convert the type argument within a field or collection +// field marker into its underlying FieldType object. func (f *FieldType) UnmarshalMarkerArg(in string) error { types := map[string]FieldType{ "": FieldUnknownType, @@ -41,6 +45,7 @@ func (f *FieldType) UnmarshalMarkerArg(in string) error { return fmt.Errorf("%w, %s into FieldType", ErrUnableToParseFieldType, in) } +// String simply returns a FieldType in string format. func (f FieldType) String() string { types := map[FieldType]string{ FieldUnknownType: "", diff --git a/internal/workload/v1/field_type_internal_test.go b/internal/workload/v1/markers/field_types_internal_test.go similarity index 99% rename from internal/workload/v1/field_type_internal_test.go rename to internal/workload/v1/markers/field_types_internal_test.go index 562c56e..74fe328 100644 --- a/internal/workload/v1/field_type_internal_test.go +++ b/internal/workload/v1/markers/field_types_internal_test.go @@ -1,7 +1,7 @@ // Copyright 2021 VMware, Inc. // SPDX-License-Identifier: MIT -package v1 +package markers import ( "testing" diff --git a/internal/workload/v1/markers/markers.go b/internal/workload/v1/markers/markers.go new file mode 100644 index 0000000..31e4b05 --- /dev/null +++ b/internal/workload/v1/markers/markers.go @@ -0,0 +1,250 @@ +// Copyright 2021 VMware, Inc. +// SPDX-License-Identifier: MIT + +package markers + +import ( + "fmt" + "regexp" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/vmware-tanzu-labs/operator-builder/internal/markers/inspect" + markerparser "github.com/vmware-tanzu-labs/operator-builder/internal/markers/marker" +) + +// MarkerType defines the types of markers that are accepted by the parser. +type MarkerType int + +const ( + FieldMarkerType MarkerType = iota + CollectionMarkerType + ResourceMarkerType + UnknownMarkerType +) + +// FieldMarkerProcessor is an interface that requires specific methods that are +// necessary for parsing a field marker or a collection field marker. +type FieldMarkerProcessor interface { + GetName() string + GetDefault() interface{} + GetDescription() string + GetFieldType() FieldType + GetOriginalValue() interface{} + GetReplaceText() string + GetSpecPrefix() string + GetSourceCodeVariable() string + + IsCollectionFieldMarker() bool + IsFieldMarker() bool + IsForCollection() bool + + SetDescription(string) + SetOriginalValue(string) + SetForCollection(bool) +} + +// MarkerProcessor is a more generic interface that requires specific methods that are +// necessary for parsing any type of marker. +type MarkerProcessor interface { + GetName() string + GetSpecPrefix() string +} + +// MarkerCollection is an object that stores a set of markers. +type MarkerCollection struct { + FieldMarkers []*FieldMarker + CollectionFieldMarkers []*CollectionFieldMarker +} + +// ContainsMarkerType will determine if a given marker type exists within +// a set of marker types. +func ContainsMarkerType(s []MarkerType, e MarkerType) bool { + for _, a := range s { + if a == e { + return true + } + } + + return false +} + +// InspectForYAML will inspect yamlContent for a set of markers. It will find +// all of the markers within the yamlContent and return the resultant lines and +// any associated errors. +func InspectForYAML(yamlContent []byte, markerTypes ...MarkerType) ([]*yaml.Node, []*inspect.YAMLResult, error) { + insp, err := initializeMarkerInspector(markerTypes...) + if err != nil { + return nil, nil, fmt.Errorf("%w; error initializing markers %v", err, markerTypes) + } + + nodes, results, err := insp.InspectYAML(yamlContent, transformYAML) + if err != nil { + return nil, nil, fmt.Errorf("%w; error inspecting YAML for markers %v", err, markerTypes) + } + + return nodes, results, nil +} + +// initializeMarkerInspector will create a new registry and initialize an inspector +// for specific marker types. +func initializeMarkerInspector(markerTypes ...MarkerType) (*inspect.Inspector, error) { + registry := markerparser.NewRegistry() + + var err error + + for _, markerType := range markerTypes { + switch markerType { + case FieldMarkerType: + err = defineFieldMarker(registry) + case CollectionMarkerType: + err = defineCollectionFieldMarker(registry) + case ResourceMarkerType: + err = defineResourceMarker(registry) + } + } + + if err != nil { + return nil, err + } + + return inspect.NewInspector(registry), nil +} + +// transformYAML will transform a YAML result into the proper format for scaffolding +// resultant code and API definitions. +func transformYAML(results ...*inspect.YAMLResult) error { + for _, result := range results { + // convert to interface + var marker FieldMarkerProcessor + + switch t := result.Object.(type) { + case FieldMarker: + t.sourceCodeVar = getSourceCodeVariable(&t) + marker = &t + case CollectionFieldMarker: + t.sourceCodeVar = getSourceCodeVariable(&t) + marker = &t + default: + continue + } + + // get common variables and confirm that we are not working with a reserved marker + if isReserved(marker.GetName()) { + return fmt.Errorf("%s %w", marker.GetName(), ErrFieldMarkerReserved) + } + + key, value := getKeyValue(result) + + setComments(marker, result, key, value) + + if err := setValue(marker, value); err != nil { + return fmt.Errorf("%w; error setting value for marker %s", err, result.MarkerText) + } + + result.Object = marker + } + + return nil +} + +// reservedMarkers represents a list of markers which cannot be used +// within a manifest. They are reserved for internal purposes. If any of the +// reservedMarkers are found, we will throw an error and notify the user. +func reservedMarkers() []string { + return []string{ + "collection", + "collection.name", + "collection.namespace", + } +} + +// isReserved is a convenience method which returns whether or not a marker, given +// the fieldName as a string, is reserved for internal purposes. +func isReserved(fieldName string) bool { + for _, reserved := range reservedMarkers() { + if strings.Title(fieldName) == strings.Title(reserved) { + return true + } + } + + return false +} + +// getSourceCodeFieldVariable gets a full variable name for a marker as it is intended to be +// passed into the generate package to generate the source code. This includes particular +// tags that are needed by the generator to properly identify when a variable starts and ends. +func getSourceCodeFieldVariable(marker FieldMarkerProcessor) string { + return fmt.Sprintf("!!start %s !!end", marker.GetSourceCodeVariable()) +} + +// getSourceCodeVariable gets a full variable name for a marker as it is intended to be +// scaffolded in the source code. +func getSourceCodeVariable(marker MarkerProcessor) string { + return fmt.Sprintf("%s.%s", marker.GetSpecPrefix(), strings.Title((marker.GetName()))) +} + +// getKeyValue gets the key and value from a YAML result. +func getKeyValue(result *inspect.YAMLResult) (key, value *yaml.Node) { + if len(result.Nodes) > 1 { + return result.Nodes[0], result.Nodes[1] + } + + return result.Nodes[0], result.Nodes[0] +} + +// setComments sets the comments for use by the resultant code. +func setComments(marker FieldMarkerProcessor, result *inspect.YAMLResult, key, value *yaml.Node) { + // update the description to ensure new lines are commented + if marker.GetDescription() != "" { + marker.SetDescription(strings.TrimPrefix(marker.GetDescription(), "\n")) + key.HeadComment = key.HeadComment + "\n# " + marker.GetDescription() + } + + // set replace text to ensure that our markers are commented + replaceText := strings.TrimSuffix(result.MarkerText, "\n") + replaceText = strings.ReplaceAll(replaceText, "\n", "\n#") + + // set the append text to notify the user where a marker was originated from in their source code + var appendText string + switch t := marker.(type) { + case *FieldMarker: + appendText = "controlled by field: " + t.Name + case *CollectionFieldMarker: + appendText = "controlled by collection field: " + t.Name + } + + // set the comments on the yaml nodes + key.FootComment = "" + key.HeadComment = strings.ReplaceAll(key.HeadComment, replaceText, appendText) + value.LineComment = strings.ReplaceAll(value.LineComment, replaceText, appendText) +} + +// setValue will set the value appropriately. This is based on whether the marker has +// requested replacement text. +func setValue(marker FieldMarkerProcessor, value *yaml.Node) error { + const varTag = "!!var" + + const strTag = "!!str" + + markerReplaceText := marker.GetReplaceText() + + marker.SetOriginalValue(value.Value) + + if markerReplaceText != "" { + value.Tag = strTag + + re, err := regexp.Compile(markerReplaceText) + if err != nil { + return fmt.Errorf("unable to convert %s to regex, %w", markerReplaceText, err) + } + + value.Value = re.ReplaceAllString(value.Value, getSourceCodeFieldVariable(marker)) + } else { + value.Tag = varTag + value.Value = marker.GetSourceCodeVariable() + } + + return nil +} diff --git a/internal/workload/v1/markers/markers_internal_test.go b/internal/workload/v1/markers/markers_internal_test.go new file mode 100644 index 0000000..4755af2 --- /dev/null +++ b/internal/workload/v1/markers/markers_internal_test.go @@ -0,0 +1,734 @@ +// Copyright 2021 VMware, Inc. +// SPDX-License-Identifier: MIT + +package markers + +import ( + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" + + "github.com/vmware-tanzu-labs/operator-builder/internal/markers/inspect" + "github.com/vmware-tanzu-labs/operator-builder/internal/markers/parser" +) + +func TestContainsMarkerType(t *testing.T) { + t.Parallel() + + knownMarkerTypes := []MarkerType{ + FieldMarkerType, + CollectionMarkerType, + ResourceMarkerType, + } + + type args struct { + s []MarkerType + e MarkerType + } + + tests := []struct { + name string + args args + want bool + }{ + { + name: "ensure missing marker type returns false", + args: args{ + s: knownMarkerTypes, + e: UnknownMarkerType, + }, + want: false, + }, + { + name: "ensure non-missing marker type returns true", + args: args{ + s: knownMarkerTypes, + e: FieldMarkerType, + }, + want: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := ContainsMarkerType(tt.args.s, tt.args.e); got != tt.want { + t.Errorf("ContainsMarkerType() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResourceMarker_hasField(t *testing.T) { + t.Parallel() + + testPath := "test.has.field" + testEmpty := "" + + type fields struct { + Field *string + CollectionField *string + } + + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "resource marker with field returns true", + fields: fields{ + Field: &testPath, + }, + want: true, + }, + { + name: "resource marker with collection field returns true", + fields: fields{ + CollectionField: &testPath, + }, + want: true, + }, + { + name: "resource marker with empty field and collection field returns false", + fields: fields{ + Field: &testEmpty, + CollectionField: &testEmpty, + }, + want: false, + }, + { + name: "resource marker without field or collection field returns false", + fields: fields{}, + want: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rm := &ResourceMarker{ + Field: tt.fields.Field, + CollectionField: tt.fields.CollectionField, + } + if got := rm.hasField(); got != tt.want { + t.Errorf("ResourceMarker.hasField() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResourceMarker_hasValue(t *testing.T) { + t.Parallel() + + testValue := "test.has.value" + + type fields struct { + Value interface{} + } + + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "resource marker with nil value returns false", + fields: fields{ + Value: nil, + }, + want: false, + }, + { + name: "resource marker with value returns true", + fields: fields{ + Value: &testValue, + }, + want: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rm := &ResourceMarker{ + Value: tt.fields.Value, + } + if got := rm.hasValue(); got != tt.want { + t.Errorf("ResourceMarker.hasValue() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_isReserved(t *testing.T) { + t.Parallel() + + type args struct { + fieldName string + } + + tests := []struct { + name string + args args + want bool + }{ + { + name: "ensure reserved field returns true", + args: args{ + fieldName: "collection.name", + }, + want: true, + }, + { + name: "ensure reserved field as a title returns true", + args: args{ + fieldName: "collection.Name", + }, + want: true, + }, + { + name: "ensure non-reserved field returns false", + args: args{ + fieldName: "collection.nonReserved", + }, + want: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := isReserved(tt.args.fieldName); got != tt.want { + t.Errorf("isReserved() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getSourceCodeFieldVariable(t *testing.T) { + t.Parallel() + + fieldMarkerTest := &FieldMarker{ + Name: "field.marker", + sourceCodeVar: "parent.Spec.Field.Marker", + } + + collectionFieldMarkerTest := &CollectionFieldMarker{ + Name: "collection", + sourceCodeVar: "collection.Spec.Collection", + } + + type args struct { + marker FieldMarkerProcessor + } + + tests := []struct { + name string + args args + want string + }{ + { + name: "ensure field marker returns a correct source code variable field", + args: args{ + marker: fieldMarkerTest, + }, + want: "!!start parent.Spec.Field.Marker !!end", + }, + { + name: "ensure collection field marker returns a correct source code variable field", + args: args{ + marker: collectionFieldMarkerTest, + }, + want: "!!start collection.Spec.Collection !!end", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := getSourceCodeFieldVariable(tt.args.marker); got != tt.want { + t.Errorf("getSourceCodeFieldVariable() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getSourceCodeVariable(t *testing.T) { + t.Parallel() + + fieldMarkerTest := &FieldMarker{ + Name: "this.is.a.highly.nested.field", + } + + collectionFieldMarkerTest := &CollectionFieldMarker{ + Name: "flat", + } + + fieldMarkerField := "test.field.marker.field" + collectionFieldMarkerField := "test.collection.field.marker.field" + + resourceMarkerFieldTest := &ResourceMarker{ + Field: &fieldMarkerField, + } + + resourceMarkerCollectionFieldTest := &ResourceMarker{ + CollectionField: &collectionFieldMarkerField, + } + + type args struct { + marker MarkerProcessor + } + + tests := []struct { + name string + args args + want string + }{ + { + name: "ensure field marker returns a correct source code variable", + args: args{ + marker: fieldMarkerTest, + }, + want: "parent.Spec.This.Is.A.Highly.Nested.Field", + }, + { + name: "ensure collection field marker returns a correct source code variable", + args: args{ + marker: collectionFieldMarkerTest, + }, + want: "collection.Spec.Flat", + }, + { + name: "ensure resource marker with field marker field returns a correct source code variable", + args: args{ + marker: resourceMarkerFieldTest, + }, + want: "parent.Spec.Test.Field.Marker.Field", + }, + { + name: "ensure resource marker with collection field marker field returns a correct source code variable", + args: args{ + marker: resourceMarkerCollectionFieldTest, + }, + want: "collection.Spec.Test.Collection.Field.Marker.Field", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := getSourceCodeVariable(tt.args.marker); got != tt.want { + t.Errorf("getSourceCodeVariable() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getKeyValue(t *testing.T) { + t.Parallel() + + testYamlNode := &yaml.Node{ + Tag: "testTag", + Value: "testValue", + } + + testOtherYamlNode := &yaml.Node{ + Tag: "testTag2", + Value: "testValue2", + } + + type args struct { + result *inspect.YAMLResult + } + + tests := []struct { + name string + args args + wantKey *yaml.Node + wantValue *yaml.Node + }{ + { + name: "ensure flat result returns same key and value", + args: args{ + result: &inspect.YAMLResult{ + Nodes: []*yaml.Node{testYamlNode}, + }, + }, + wantKey: testYamlNode, + wantValue: testYamlNode, + }, + { + name: "ensure multiple result returns correct key and value", + args: args{ + result: &inspect.YAMLResult{ + Nodes: []*yaml.Node{ + testYamlNode, + testOtherYamlNode, + }, + }, + }, + wantKey: testYamlNode, + wantValue: testOtherYamlNode, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + gotKey, gotValue := getKeyValue(tt.args.result) + if !reflect.DeepEqual(gotKey, tt.wantKey) { + t.Errorf("getKeyValue() gotKey = %v, want %v", gotKey, tt.wantKey) + } + if !reflect.DeepEqual(gotValue, tt.wantValue) { + t.Errorf("getKeyValue() gotValue = %v, want %v", gotValue, tt.wantValue) + } + }) + } +} + +func Test_setValue(t *testing.T) { + t.Parallel() + + //nolint: goconst + testInvalidReplaceText := "*&^%" + testReplaceText := "" + + type args struct { + marker FieldMarkerProcessor + value *yaml.Node + } + + tests := []struct { + name string + args args + wantErr bool + want *yaml.Node + }{ + { + name: "ensure value is set appropriately when replace text is not requested", + args: args{ + marker: &FieldMarker{ + Name: "test.field", + sourceCodeVar: "parent.Spec.Test.Field", + }, + value: &yaml.Node{ + Tag: "testTag", + Value: "test value", + }, + }, + wantErr: false, + want: &yaml.Node{ + Tag: "!!var", + Value: "parent.Spec.Test.Field", + }, + }, + { + name: "ensure value is set appropriately when replace text is requested", + args: args{ + marker: &FieldMarker{ + Name: "test.field", + Replace: &testReplaceText, + sourceCodeVar: "parent.Spec.Test.Field", + }, + value: &yaml.Node{ + Tag: "testTag", + Value: "test value", + }, + }, + wantErr: false, + want: &yaml.Node{ + Tag: "!!str", + Value: "test !!start parent.Spec.Test.Field !!end value", + }, + }, + { + name: "ensure invalid replace text returns an error", + args: args{ + marker: &FieldMarker{ + Name: "test.field", + Replace: &testInvalidReplaceText, + sourceCodeVar: "parent.Spec.Test.Field", + }, + value: &yaml.Node{ + Tag: "testTag", + Value: "test value", + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if err := setValue(tt.args.marker, tt.args.value); (err != nil) != tt.wantErr { + t.Errorf("setValue() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.want != nil { + assert.Equal(t, tt.want, tt.args.value) + } + }) + } +} + +func Test_setComments(t *testing.T) { + t.Parallel() + + testDescription := "\n this\n is\n a\n test" + testHeadCommentDescription := "\n# this\n# is\n# a\n# test" + testName := "test.comment.field" + testMarkerPrefix := "+operator-builder:field:default=\"my-field\",type=string" + testMarkerText := fmt.Sprintf("%s,name=%s,description=`%s`", testMarkerPrefix, testName, testDescription) + testHeadComment := fmt.Sprintf("# %s,name=%s,description=`%s`", testMarkerPrefix, testName, testHeadCommentDescription) + + type args struct { + marker FieldMarkerProcessor + result *inspect.YAMLResult + key *yaml.Node + value *yaml.Node + } + + tests := []struct { + name string + args args + wantKey *yaml.Node + wantValue *yaml.Node + }{ + { + name: "ensure head comment is set correctly with a description", + args: args{ + marker: &FieldMarker{ + Name: testName, + Description: &testHeadCommentDescription, + }, + result: &inspect.YAMLResult{ + Result: &parser.Result{ + MarkerText: testMarkerText, + }, + }, + key: &yaml.Node{ + HeadComment: testHeadComment, + }, + value: &yaml.Node{ + LineComment: testHeadComment, + }, + }, + wantKey: &yaml.Node{ + FootComment: "", + HeadComment: "# controlled by field: test.comment.field\n# # this\n# is\n# a\n# test", + }, + }, + { + name: "ensure head comment is set correctly without a description", + args: args{ + marker: &FieldMarker{ + Name: testName, + }, + result: &inspect.YAMLResult{ + Result: &parser.Result{ + MarkerText: testMarkerText, + }, + }, + key: &yaml.Node{ + HeadComment: testHeadComment, + }, + value: &yaml.Node{ + LineComment: testHeadComment, + }, + }, + wantKey: &yaml.Node{ + FootComment: "", + HeadComment: "# controlled by field: test.comment.field", + }, + }, + { + name: "ensure line comment is set correctly with a description", + args: args{ + marker: &CollectionFieldMarker{ + Name: testName, + Description: &testHeadCommentDescription, + }, + result: &inspect.YAMLResult{ + Result: &parser.Result{ + MarkerText: testMarkerText, + }, + }, + key: &yaml.Node{ + HeadComment: testHeadComment, + }, + value: &yaml.Node{ + LineComment: testHeadComment, + }, + }, + wantValue: &yaml.Node{ + LineComment: "# controlled by collection field: test.comment.field", + }, + }, + { + name: "ensure line comment is set correctly without a description", + args: args{ + marker: &CollectionFieldMarker{ + Name: testName, + }, + result: &inspect.YAMLResult{ + Result: &parser.Result{ + MarkerText: testMarkerText, + }, + }, + key: &yaml.Node{ + HeadComment: testHeadComment, + }, + value: &yaml.Node{ + LineComment: testHeadComment, + }, + }, + wantValue: &yaml.Node{ + LineComment: "# controlled by collection field: test.comment.field", + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + setComments(tt.args.marker, tt.args.result, tt.args.key, tt.args.value) + if tt.wantKey != nil { + assert.Equal(t, tt.wantKey, tt.args.key) + } + if tt.wantValue != nil { + assert.Equal(t, tt.wantValue, tt.args.value) + } + }) + } +} + +func Test_transformYAML(t *testing.T) { + t.Parallel() + + badReplaceText := "*&^%" + + type args struct { + results []*inspect.YAMLResult + } + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "ensure valid marker does not return error", + args: args{ + results: []*inspect.YAMLResult{ + { + Result: &parser.Result{ + MarkerText: "test", + Object: FieldMarker{ + Name: "real.field", + }, + }, + Nodes: []*yaml.Node{ + { + Tag: "test", + Value: "test", + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "ensure invalid object skips and returns no error", + args: args{ + results: []*inspect.YAMLResult{ + { + Result: &parser.Result{ + MarkerText: "test", + Object: "this is a string no a marker", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "ensure invalid field marker returns an error", + args: args{ + results: []*inspect.YAMLResult{ + { + Result: &parser.Result{ + MarkerText: "test", + Object: FieldMarker{ + Name: "collection.name", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "ensure invalid collection field marker returns an error", + args: args{ + results: []*inspect.YAMLResult{ + { + Result: &parser.Result{ + MarkerText: "test", + Object: CollectionFieldMarker{ + Name: "collection.name", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "ensure failure while attempting to set value", + args: args{ + results: []*inspect.YAMLResult{ + { + Result: &parser.Result{ + MarkerText: "test", + Object: CollectionFieldMarker{ + Name: "real.field", + Replace: &badReplaceText, + }, + }, + Nodes: []*yaml.Node{ + { + Tag: "test", + Value: "test", + }, + }, + }, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if err := transformYAML(tt.args.results...); (err != nil) != tt.wantErr { + t.Errorf("transformYAML() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/workload/v1/markers/resource_marker.go b/internal/workload/v1/markers/resource_marker.go new file mode 100644 index 0000000..1b10062 --- /dev/null +++ b/internal/workload/v1/markers/resource_marker.go @@ -0,0 +1,279 @@ +// Copyright 2021 VMware, Inc. +// SPDX-License-Identifier: MIT + +package markers + +import ( + "errors" + "fmt" + + "github.com/vmware-tanzu-labs/operator-builder/internal/markers/marker" +) + +var ( + ErrResourceMarkerInvalid = errors.New("resource marker is invalid") + ErrResourceMarkerCount = errors.New("expected only 1 resource marker") + ErrResourceMarkerAssociation = errors.New("unable to associate resource marker with 'field' or 'collectionField' marker") + ErrResourceMarkerTypeMismatch = errors.New("resource marker and field marker have mismatched types") + ErrResourceMarkerInvalidType = errors.New("expected resource marker type") + ErrResourceMarkerUnknownValueType = errors.New("resource marker 'value' is of unknown type") + ErrResourceMarkerMissingFieldValue = errors.New("resource marker missing 'collectionField', 'field' or 'value'") + ErrResourceMarkerMissingInclude = errors.New("resource marker missing 'include' value") +) + +const ( + ResourceMarkerPrefix = "+operator-builder:resource" + ResourceMarkerCollectionFieldName = "collectionField" + ResourceMarkerFieldName = "field" +) + +// If we have a valid resource marker, we will either include or exclude the +// related object based on the inputs on the resource marker itself. These are +// the resultant code snippets based on that logic. +const ( + includeCode = `if %s != %s { + return []client.Object{}, nil + }` + + excludeCode = `if %s == %s { + return []client.Object{}, nil + }` +) + +// ResourceMarker is an object which represents a marker for an entire resource. It +// allows actions against a resource. A ResourceMarker is discovered when a manifest +// is parsed and matches the constants defined by the collectionFieldMarker +// constant above. +type ResourceMarker struct { + // inputs from the marker itself + Field *string + CollectionField *string + Value interface{} + Include *bool + + // other field which we use to pass information + includeCode string + fieldMarker FieldMarkerProcessor +} + +// String simply returns the marker as it should be printed in string format. +func (rm ResourceMarker) String() string { + var fieldString, collectionFieldString string + + var includeBool bool + + // set the values if they have been provided otherwise take the zero values + if rm.Field != nil { + fieldString = *rm.Field + } + + if rm.CollectionField != nil { + collectionFieldString = *rm.CollectionField + } + + if rm.Include != nil { + includeBool = *rm.Include + } + + return fmt.Sprintf("ResourceMarker{Field: %s CollectionField: %s Value: %v Include: %v}", + fieldString, + collectionFieldString, + rm.Value, + includeBool, + ) +} + +// defineResourceMarker will define a ResourceMarker and add it a registry of markers. +func defineResourceMarker(registry *marker.Registry) error { + resourceMarker, err := marker.Define(ResourceMarkerPrefix, ResourceMarker{}) + if err != nil { + return fmt.Errorf("%w", err) + } + + registry.Add(resourceMarker) + + return nil +} + +// GetIncludeCode is a convenience function to return the include code of the resource marker. +func (rm *ResourceMarker) GetIncludeCode() string { + return rm.includeCode +} + +// GetName is a convenience function to return the name of the associated field marker. +func (rm *ResourceMarker) GetName() string { + if rm.GetField() != "" { + return rm.GetField() + } + + return rm.GetCollectionField() +} + +// GetCollectionField is a convenience function to return the collection field as a string. +func (rm *ResourceMarker) GetCollectionField() string { + if rm.CollectionField == nil { + return "" + } + + return *rm.CollectionField +} + +// GetField is a convenience function to return the field as a string. +func (rm *ResourceMarker) GetField() string { + if rm.Field == nil { + return "" + } + + return *rm.Field +} + +// GetSpecPrefix is a convenience function to return the spec prefix of a requested +// variable for a resource marker. +func (rm *ResourceMarker) GetSpecPrefix() string { + if rm.Field != nil { + return FieldSpecPrefix + } + + return CollectionFieldSpecPrefix +} + +// Process will process a resource marker from a collection of collection field markers +// and field markers, associate them together and set the appropriate fields. +func (rm *ResourceMarker) Process(markers *MarkerCollection) error { + // ensure we have a valid field marker before continuing to process + if err := rm.validate(); err != nil { + return fmt.Errorf("%w; %s", err, ErrResourceMarkerInvalid) + } + + // associate field markers from a collection of markers to this resource marker + if fieldMarker := rm.getFieldMarker(markers); fieldMarker != nil { + rm.fieldMarker = fieldMarker + } else { + return fmt.Errorf("%w; %s", ErrResourceMarkerAssociation, rm) + } + + // set the source code value and return + if err := rm.setSourceCode(); err != nil { + return fmt.Errorf("%w; error setting source code value for resource marker: %v", err, rm) + } + + return nil +} + +// validate checks for a valid resource marker and returns an error if the +// resource marker is invalid. +func (rm *ResourceMarker) validate() error { + // check include field for a provided value + // NOTE: this field is mandatory now, but could be optional later, so we return + // an error here rather than using a pointer to a bool to control the mandate. + if rm.Include == nil { + return fmt.Errorf("%w for marker %s", ErrResourceMarkerMissingInclude, rm) + } + + // ensure that both a field and value exist + if !rm.hasField() || !rm.hasValue() { + return fmt.Errorf("%w for marker %s", ErrResourceMarkerMissingFieldValue, rm) + } + + return nil +} + +// hasField determines whether or not a parsed resource marker has either a field +// or a collection field. One or the other is needed for processing a resource +// marker. +func (rm *ResourceMarker) hasField() bool { + return rm.GetName() != "" +} + +// hasValue determines whether or not a parsed resource marker has a value +// to check against. +func (rm *ResourceMarker) hasValue() bool { + return rm.Value != nil +} + +// isAssociated returns whether a given marker is associated with a given resource +// marker. +func (rm *ResourceMarker) isAssociated(fromMarker FieldMarkerProcessor) bool { + var field string + + switch { + case fromMarker.IsCollectionFieldMarker(): + field = rm.GetCollectionField() + case fromMarker.IsFieldMarker() && fromMarker.IsForCollection(): + if rm.GetCollectionField() != "" { + field = rm.GetCollectionField() + } else { + field = rm.GetField() + } + default: + field = rm.GetField() + } + + return field == fromMarker.GetName() +} + +// getFieldMarker will return the associated collection marker or field marker +// with a particular resource marker given a collection of markers. +func (rm *ResourceMarker) getFieldMarker(markers *MarkerCollection) FieldMarkerProcessor { + // return immediately if the marker collection we are trying to associate is empty + if len(markers.CollectionFieldMarkers) == 0 && len(markers.FieldMarkers) == 0 { + return nil + } + + // attempt to associate the field marker first + for _, fm := range markers.FieldMarkers { + if associatedWith := rm.isAssociated(fm); associatedWith { + return fm + } + } + + // attempt to associate a field marker from a collection field next + for _, cfm := range markers.CollectionFieldMarkers { + if associatedWith := rm.isAssociated(cfm); associatedWith { + return cfm + } + } + + return nil +} + +// setSourceCode sets the source code to use as generated by the resource marker. +func (rm *ResourceMarker) setSourceCode() error { + var sourceCodeVar, sourceCodeValue string + + // get the source code variable + sourceCodeVar = getSourceCodeVariable(rm) + + // set the source code value and ensure the types match + switch value := rm.Value.(type) { + case string, int, bool: + fieldMarkerType := rm.fieldMarker.GetFieldType().String() + resourceMarkerType := fmt.Sprintf("%T", value) + + if fieldMarkerType != resourceMarkerType { + return fmt.Errorf("%w; expected: %s, got: %s for marker %s", + ErrResourceMarkerTypeMismatch, + resourceMarkerType, + fieldMarkerType, + rm, + ) + } + + if fieldMarkerType == "string" { + sourceCodeValue = fmt.Sprintf("%q", value) + } else { + sourceCodeValue = fmt.Sprintf("%v", value) + } + default: + return ErrResourceMarkerUnknownValueType + } + + // set the include code for this marker + if *rm.Include { + rm.includeCode = fmt.Sprintf(includeCode, sourceCodeVar, sourceCodeValue) + } else { + rm.includeCode = fmt.Sprintf(excludeCode, sourceCodeVar, sourceCodeValue) + } + + return nil +} diff --git a/internal/workload/v1/markers/resource_marker_internal_test.go b/internal/workload/v1/markers/resource_marker_internal_test.go new file mode 100644 index 0000000..e95ee5a --- /dev/null +++ b/internal/workload/v1/markers/resource_marker_internal_test.go @@ -0,0 +1,986 @@ +// Copyright 2021 VMware, Inc. +// SPDX-License-Identifier: MIT + +package markers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/vmware-tanzu-labs/operator-builder/internal/markers/marker" +) + +func TestResourceMarker_String(t *testing.T) { + t.Parallel() + + testField := "test" + testIncludeTrue := true + + type fields struct { + Field *string + CollectionField *string + Value interface{} + Include *bool + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure resource marker with set field values output matches expected", + fields: fields{ + Field: &testField, + Value: "test", + Include: &testIncludeTrue, + }, + want: "ResourceMarker{Field: test CollectionField: Value: test Include: true}", + }, + { + name: "ensure resource marker with set collection field values output matches expected", + fields: fields{ + CollectionField: &testField, + Value: "test", + Include: &testIncludeTrue, + }, + want: "ResourceMarker{Field: CollectionField: test Value: test Include: true}", + }, + { + name: "ensure resource marker with nil values output matches expected", + fields: fields{ + Field: nil, + CollectionField: nil, + Value: nil, + Include: nil, + }, + want: "ResourceMarker{Field: CollectionField: Value: Include: false}", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rm := ResourceMarker{ + Field: tt.fields.Field, + CollectionField: tt.fields.CollectionField, + Value: tt.fields.Value, + Include: tt.fields.Include, + } + if got := rm.String(); got != tt.want { + t.Errorf("ResourceMarker.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_defineResourceMarker(t *testing.T) { + t.Parallel() + + type args struct { + registry *marker.Registry + } + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "ensure valid registry can properly add a resource marker", + args: args{ + registry: marker.NewRegistry(), + }, + wantErr: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if err := defineResourceMarker(tt.args.registry); (err != nil) != tt.wantErr { + t.Errorf("defineResourceMarker() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestResourceMarker_GetIncludeCode(t *testing.T) { + t.Parallel() + + type fields struct { + includeCode string + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure resource include code returns as expected", + fields: fields{ + includeCode: "resource marker include code", + }, + want: "resource marker include code", + }, + { + name: "ensure resource include code with empty value returns as expected", + fields: fields{ + includeCode: "", + }, + want: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rm := &ResourceMarker{ + includeCode: tt.fields.includeCode, + } + if got := rm.GetIncludeCode(); got != tt.want { + t.Errorf("FieldMarker.GetIncludeCode() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResourceMarker_GetSpecPrefix(t *testing.T) { + t.Parallel() + + field := "this.is.a.spec.test.prefix" + + type fields struct { + Field *string + CollectionField *string + Value interface{} + Include *bool + includeCode string + fieldMarker FieldMarkerProcessor + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure a resource marker with field marker returns field marker prefix", + fields: fields{ + Field: &field, + }, + want: FieldSpecPrefix, + }, + { + name: "ensure a resource marker with collection field marker returns collection field marker prefix", + fields: fields{ + CollectionField: &field, + }, + want: CollectionFieldSpecPrefix, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rm := &ResourceMarker{ + Field: tt.fields.Field, + CollectionField: tt.fields.CollectionField, + Value: tt.fields.Value, + Include: tt.fields.Include, + includeCode: tt.fields.includeCode, + fieldMarker: tt.fields.fieldMarker, + } + if got := rm.GetSpecPrefix(); got != tt.want { + t.Errorf("ResourceMarker.GetSpecPrefix() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResourceMarker_GetField(t *testing.T) { + t.Parallel() + + rm := "test.field" + + type fields struct { + Field *string + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure field with value returns as expected", + fields: fields{ + Field: &rm, + }, + want: rm, + }, + { + name: "ensure field with nil value returns as expected", + fields: fields{ + Field: nil, + }, + want: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rm := &ResourceMarker{ + Field: tt.fields.Field, + } + if got := rm.GetField(); got != tt.want { + t.Errorf("ResourceMarker.GetField() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResourceMarker_GetCollectionField(t *testing.T) { + t.Parallel() + + rm := "test.collection.field" + + type fields struct { + CollectionField *string + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure collection field with value returns as expected", + fields: fields{ + CollectionField: &rm, + }, + want: rm, + }, + { + name: "ensure collection field with nil value returns as expected", + fields: fields{ + CollectionField: nil, + }, + want: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rm := &ResourceMarker{ + CollectionField: tt.fields.CollectionField, + } + if got := rm.GetCollectionField(); got != tt.want { + t.Errorf("ResourceMarker.GetCollectionField() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResourceMarker_GetName(t *testing.T) { + t.Parallel() + + field := "test.get.name.field" + collectionField := "test.get.name.collection.field" + + type fields struct { + Field *string + CollectionField *string + } + + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ensure field with value returns as field value", + fields: fields{ + Field: &field, + }, + want: field, + }, + { + name: "ensure collection field with value returns collection field value", + fields: fields{ + CollectionField: &collectionField, + }, + want: collectionField, + }, + { + name: "ensure marker with both values returns field value", + fields: fields{ + Field: &field, + CollectionField: &collectionField, + }, + want: field, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rm := &ResourceMarker{ + Field: tt.fields.Field, + CollectionField: tt.fields.CollectionField, + } + if got := rm.GetName(); got != tt.want { + t.Errorf("ResourceMarker.GetName() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResourceMarker_validate(t *testing.T) { + t.Parallel() + + testField := "test.validate" + testValue := "testValue" + testInclude := true + + type fields struct { + Field *string + CollectionField *string + Value interface{} + Include *bool + fieldMarker FieldMarkerProcessor + } + + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "valid resource marker does not produce error", + fields: fields{ + Field: &testField, + Value: &testValue, + Include: &testInclude, + fieldMarker: nil, + }, + wantErr: false, + }, + { + name: "nil include value produces error", + fields: fields{ + Field: &testField, + Value: &testValue, + fieldMarker: nil, + }, + wantErr: true, + }, + { + name: "missing field produces error", + fields: fields{ + Value: &testValue, + Include: &testInclude, + fieldMarker: nil, + }, + wantErr: true, + }, + { + name: "missing value produces error", + fields: fields{ + Field: &testField, + Include: &testInclude, + fieldMarker: nil, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rm := &ResourceMarker{ + Field: tt.fields.Field, + CollectionField: tt.fields.CollectionField, + Value: tt.fields.Value, + Include: tt.fields.Include, + fieldMarker: tt.fields.fieldMarker, + } + if err := rm.validate(); (err != nil) != tt.wantErr { + t.Errorf("ResourceMarker.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestResourceMarker_isAssociated(t *testing.T) { + t.Parallel() + + randomString := "thisIsRandom" + + // this is the test of a standard marker which would be discovered in any manifest type + // standalone = no collection involved + // component = the field was set on itself with operator-builder:field marker + // collection = the field was set on itself with operator-builder:field or + // operator-builder:collection:field marker. It is important to note + // that all field markers on a collection are immediately discovered + // as field markers, regardless of how they are labeled. + testFieldMarker := &FieldMarker{ + Name: "test", + Type: FieldString, + forCollection: false, + } + + // this is the test of a standard marker which was discovered on a collection + testFieldMarkerOnCollection := &FieldMarker{ + Name: "test.collection", + Type: FieldString, + forCollection: true, + } + + // this is the test of a standard collection marker. + testCollectionMarker := &CollectionFieldMarker{ + Name: "test", + Type: FieldString, + forCollection: false, + } + + type fields struct { + Field *string + CollectionField *string + Value interface{} + Include *bool + includeCode string + fieldMarker FieldMarkerProcessor + } + + type args struct { + fromMarker FieldMarkerProcessor + } + + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "operator-builder:resource:field=test is associated with a field marker", + fields: fields{ + Field: &testFieldMarker.Name, + }, + args: args{ + fromMarker: testFieldMarker, + }, + want: true, + }, + { + name: "operator-builder:resource:field=test is not associated with a collection field marker", + fields: fields{ + Field: &testCollectionMarker.Name, + }, + args: args{ + fromMarker: testCollectionMarker, + }, + want: false, + }, + { + name: "operator-builder:resource:field=test with random string is not associated with a field marker", + fields: fields{ + Field: &randomString, + }, + args: args{ + fromMarker: testFieldMarker, + }, + want: false, + }, + { + name: "operator-builder:resource:field=test with random string is not associated with a collection field marker", + fields: fields{ + CollectionField: &randomString, + }, + args: args{ + fromMarker: testCollectionMarker, + }, + want: false, + }, + { + name: "operator-builder:resource:field=test with nil is not associated with a field marker", + fields: fields{ + Field: nil, + }, + args: args{ + fromMarker: testFieldMarker, + }, + want: false, + }, + { + name: "operator-builder:resource:field=test with nil is not associated with a collection field marker", + fields: fields{ + CollectionField: nil, + }, + args: args{ + fromMarker: testCollectionMarker, + }, + want: false, + }, + { + name: "operator-builder:resource:collectionField=testCollection is associated with a field marker", + fields: fields{ + CollectionField: &testCollectionMarker.Name, + }, + args: args{ + fromMarker: testCollectionMarker, + }, + want: true, + }, + { + name: "operator-builder:resource:collectionField=test is associated with a field marker from a collection", + fields: fields{ + CollectionField: &testFieldMarkerOnCollection.Name, + }, + args: args{ + fromMarker: testFieldMarkerOnCollection, + }, + want: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rm := &ResourceMarker{ + Field: tt.fields.Field, + CollectionField: tt.fields.CollectionField, + Value: tt.fields.Value, + Include: tt.fields.Include, + includeCode: tt.fields.includeCode, + fieldMarker: tt.fields.fieldMarker, + } + if got := rm.isAssociated(tt.args.fromMarker); got != tt.want { + t.Errorf("ResourceMarker.isAssociated() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResourceMarker_getFieldMarker(t *testing.T) { + t.Parallel() + + fieldOne := "fieldOne" + fieldTwo := "fieldTwo" + fieldMissing := "missing" + + testMarkers := &MarkerCollection{ + CollectionFieldMarkers: []*CollectionFieldMarker{ + { + Name: fieldOne, + Type: FieldString, + }, + { + Name: fieldTwo, + Type: FieldString, + }, + }, + FieldMarkers: []*FieldMarker{ + { + Name: fieldOne, + Type: FieldString, + forCollection: false, + }, + { + Name: fieldOne, + Type: FieldString, + forCollection: true, + }, + { + Name: fieldTwo, + Type: FieldString, + forCollection: false, + }, + }, + } + + type fields struct { + Field *string + CollectionField *string + Value interface{} + Include *bool + fieldMarker FieldMarkerProcessor + } + + type args struct { + markers *MarkerCollection + } + + tests := []struct { + name string + fields fields + args args + want FieldMarkerProcessor + }{ + { + name: "resource marker with field within its component returns field marker", + args: args{ + markers: testMarkers, + }, + fields: fields{ + Field: &fieldOne, + }, + want: &FieldMarker{ + Name: fieldOne, + Type: FieldString, + }, + }, + { + name: "resource marker with collection field returns collection field marker", + args: args{ + markers: testMarkers, + }, + fields: fields{ + CollectionField: &fieldTwo, + }, + want: &CollectionFieldMarker{ + Name: fieldTwo, + Type: FieldString, + }, + }, + { + name: "resource marker with field within its component returns second field marker", + args: args{ + markers: testMarkers, + }, + fields: fields{ + Field: &fieldTwo, + }, + want: &FieldMarker{ + Name: fieldTwo, + Type: FieldString, + }, + }, + { + name: "resource marker with collection field request returns field marker from collection", + args: args{ + markers: testMarkers, + }, + fields: fields{ + CollectionField: &fieldOne, + }, + want: &FieldMarker{ + Name: fieldOne, + Type: FieldString, + forCollection: true, + }, + }, + { + name: "resource marker with missing field returns nil", + args: args{ + markers: testMarkers, + }, + fields: fields{ + Field: &fieldMissing, + }, + want: nil, + }, + { + name: "resource marker with missing collection field returns nil", + args: args{ + markers: testMarkers, + }, + fields: fields{ + CollectionField: &fieldMissing, + }, + want: nil, + }, + { + name: "marker collection without any markers returns nil", + args: args{ + markers: &MarkerCollection{}, + }, + fields: fields{}, + want: nil, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rm := &ResourceMarker{ + Field: tt.fields.Field, + CollectionField: tt.fields.CollectionField, + Value: tt.fields.Value, + Include: tt.fields.Include, + fieldMarker: tt.fields.fieldMarker, + } + got := rm.getFieldMarker(tt.args.markers) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestResourceMarker_Process(t *testing.T) { + t.Parallel() + + fieldOne := "field.process.test" + fieldTwo := "field.two.process.test" + fieldMissing := "field.missing" + include := true + + testMarkers := &MarkerCollection{ + CollectionFieldMarkers: []*CollectionFieldMarker{ + { + Name: fieldOne, + Type: FieldString, + }, + }, + FieldMarkers: []*FieldMarker{ + { + Name: fieldOne, + Type: FieldString, + forCollection: true, + }, + { + Name: fieldOne, + Type: FieldString, + forCollection: false, + }, + { + Name: fieldTwo, + Type: FieldString, + forCollection: true, + }, + }, + } + + type fields struct { + Field *string + CollectionField *string + Value interface{} + Include *bool + includeCode string + fieldMarker FieldMarkerProcessor + } + + type args struct { + markers *MarkerCollection + } + + tests := []struct { + name string + fields fields + args args + wantErr bool + want *ResourceMarker + }{ + { + name: "ensure valid marker returns no errors during processing", + fields: fields{ + Field: &fieldOne, + Value: "this.is.super.valid", + Include: &include, + }, + args: args{ + markers: testMarkers, + }, + wantErr: false, + }, + { + name: "ensure missing marker returns error setting field marker", + fields: fields{ + Field: &fieldOne, + Value: []string{"thisisinvalid"}, + Include: &include, + }, + args: args{ + markers: testMarkers, + }, + wantErr: true, + }, + { + name: "ensure invalid marker returns error on validation", + fields: fields{}, + args: args{ + markers: testMarkers, + }, + wantErr: true, + }, + { + name: "ensure missing marker returns error setting field marker", + fields: fields{ + Field: &fieldMissing, + Value: "testValue", + Include: &include, + }, + args: args{ + markers: testMarkers, + }, + wantErr: true, + }, + { + name: "ensure missing marker returns error setting source code", + fields: fields{ + Field: &fieldOne, + Value: []string{"thisisinvalid"}, + Include: &include, + }, + args: args{ + markers: testMarkers, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rm := &ResourceMarker{ + Field: tt.fields.Field, + CollectionField: tt.fields.CollectionField, + Value: tt.fields.Value, + Include: tt.fields.Include, + includeCode: tt.fields.includeCode, + fieldMarker: tt.fields.fieldMarker, + } + if err := rm.Process(tt.args.markers); (err != nil) != tt.wantErr { + t.Errorf("ResourceMarker.Process() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.want != nil { + assert.Equal(t, tt.want, rm) + } + }) + } +} + +func TestResourceMarker_setSourceCode(t *testing.T) { + t.Parallel() + + includeTrue := true + includeFalse := false + testSourceCodeField := "test.nested.field" + + testCollectionMarker := &CollectionFieldMarker{ + Name: testSourceCodeField, + Type: FieldString, + } + + testFieldMarker := &FieldMarker{ + Name: testSourceCodeField, + Type: FieldInt, + } + + testInvalidMarker := &FieldMarker{ + Name: testSourceCodeField, + Type: FieldUnknownType, + } + + type fields struct { + Field *string + CollectionField *string + Value interface{} + Include *bool + includeCode string + fieldMarker FieldMarkerProcessor + } + + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "ensure valid field marker produces no error on include", + fields: fields{ + fieldMarker: testFieldMarker, + Include: &includeTrue, + Value: 1, + }, + wantErr: false, + }, + { + name: "ensure valid field marker produces no error on exclude", + fields: fields{ + fieldMarker: testFieldMarker, + Include: &includeFalse, + Value: 0, + }, + wantErr: false, + }, + { + name: "ensure valid collection marker produces no error on include", + fields: fields{ + fieldMarker: testCollectionMarker, + Include: &includeTrue, + Value: "testInclude", + }, + wantErr: false, + }, + { + name: "ensure valid collection marker produces no error on exclude", + fields: fields{ + fieldMarker: testCollectionMarker, + Include: &includeFalse, + Value: "testExclude", + }, + wantErr: false, + }, + { + name: "ensure invalid marker with mismatched types produces error", + fields: fields{ + fieldMarker: testFieldMarker, + Include: &includeTrue, + Value: "testMismatch", + }, + wantErr: true, + }, + { + name: "ensure invalid marker with unknown field marker type produces error", + fields: fields{ + fieldMarker: testInvalidMarker, + Include: &includeTrue, + Value: 1, + }, + wantErr: true, + }, + { + name: "ensure invalid marker with unknown resource marker value type produces error", + fields: fields{ + fieldMarker: testFieldMarker, + Include: &includeTrue, + Value: []string{}, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rm := &ResourceMarker{ + Field: tt.fields.Field, + CollectionField: tt.fields.CollectionField, + Value: tt.fields.Value, + Include: tt.fields.Include, + includeCode: tt.fields.includeCode, + fieldMarker: tt.fields.fieldMarker, + } + if err := rm.setSourceCode(); (err != nil) != tt.wantErr { + t.Errorf("ResourceMarker.setSourceCode() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/workload/v1/markers_internal_test.go b/internal/workload/v1/markers_internal_test.go deleted file mode 100644 index 80be4b8..0000000 --- a/internal/workload/v1/markers_internal_test.go +++ /dev/null @@ -1,450 +0,0 @@ -// Copyright 2021 VMware, Inc. -// SPDX-License-Identifier: MIT - -package v1 - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_getResourceDefinitionVar(t *testing.T) { - t.Parallel() - - type args struct { - path string - forCollectionMarker bool - } - - tests := []struct { - name string - args args - want string - }{ - { - name: "child resource with collection field should refer to collection", - args: args{ - path: "test.path", - forCollectionMarker: true, - }, - want: "collection.Spec.Test.Path", - }, - { - name: "child resource with non-collection field should refer to parent", - args: args{ - path: "test.path", - forCollectionMarker: false, - }, - want: "parent.Spec.Test.Path", - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - if got := getResourceDefinitionVar(tt.args.path, tt.args.forCollectionMarker); got != tt.want { - t.Errorf("getResourceDefinitionVar() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestResourceMarker_setSourceCodeVar(t *testing.T) { - t.Parallel() - - testPath := "test.set.source.code.var" - - type fields struct { - Field *string - CollectionField *string - sourceCodeVar string - } - - tests := []struct { - name string - fields fields - want *ResourceMarker - }{ - { - name: "resource marker referencing non-collection field", - fields: fields{ - Field: &testPath, - }, - want: &ResourceMarker{ - Field: &testPath, - sourceCodeVar: "parent.Spec.Test.Set.Source.Code.Var", - }, - }, - { - name: "resource marker referencing collection field", - fields: fields{ - CollectionField: &testPath, - }, - want: &ResourceMarker{ - CollectionField: &testPath, - sourceCodeVar: "collection.Spec.Test.Set.Source.Code.Var", - }, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - rm := &ResourceMarker{ - Field: tt.fields.Field, - CollectionField: tt.fields.CollectionField, - sourceCodeVar: tt.fields.sourceCodeVar, - } - rm.setSourceCodeVar() - assert.Equal(t, tt.want, rm) - }) - } -} - -func TestResourceMarker_hasField(t *testing.T) { - t.Parallel() - - testPath := "test.has.field" - testEmpty := "" - - type fields struct { - Field *string - CollectionField *string - } - - tests := []struct { - name string - fields fields - want bool - }{ - { - name: "resource marker with field returns true", - fields: fields{ - Field: &testPath, - }, - want: true, - }, - { - name: "resource marker with collection field returns true", - fields: fields{ - CollectionField: &testPath, - }, - want: true, - }, - { - name: "resource marker with empty field and collection field returns false", - fields: fields{ - Field: &testEmpty, - CollectionField: &testEmpty, - }, - want: false, - }, - { - name: "resource marker without field or collection field returns false", - fields: fields{}, - want: false, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - rm := &ResourceMarker{ - Field: tt.fields.Field, - CollectionField: tt.fields.CollectionField, - } - if got := rm.hasField(); got != tt.want { - t.Errorf("ResourceMarker.hasField() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestResourceMarker_hasValue(t *testing.T) { - t.Parallel() - - testValue := "test.has.value" - - type fields struct { - Value interface{} - } - - tests := []struct { - name string - fields fields - want bool - }{ - { - name: "resource marker with nil value returns false", - fields: fields{ - Value: nil, - }, - want: false, - }, - { - name: "resource marker with value returns true", - fields: fields{ - Value: &testValue, - }, - want: true, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - rm := &ResourceMarker{ - Value: tt.fields.Value, - } - if got := rm.hasValue(); got != tt.want { - t.Errorf("ResourceMarker.hasValue() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestResourceMarker_validate(t *testing.T) { - t.Parallel() - - testField := "test.validate" - testValue := "testValue" - testInclude := true - - type fields struct { - Field *string - CollectionField *string - Value interface{} - Include *bool - sourceCodeVar string - sourceCodeValue string - fieldMarker interface{} - } - - tests := []struct { - name string - fields fields - wantErr bool - }{ - { - name: "nil include value produces error", - fields: fields{ - Field: &testField, - Value: &testValue, - fieldMarker: "", - }, - wantErr: true, - }, - { - name: "missing field produces error", - fields: fields{ - Value: &testValue, - Include: &testInclude, - fieldMarker: "", - }, - wantErr: true, - }, - { - name: "missing value produces error", - fields: fields{ - Field: &testField, - Include: &testInclude, - fieldMarker: "", - }, - wantErr: true, - }, - { - name: "missing field marker produces error", - fields: fields{ - Field: &testField, - Value: &testValue, - Include: &testInclude, - }, - wantErr: true, - }, - { - name: "valid resource marker produces no error", - fields: fields{ - Field: &testField, - Value: &testValue, - Include: &testInclude, - fieldMarker: "", - }, - wantErr: false, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - rm := &ResourceMarker{ - Field: tt.fields.Field, - CollectionField: tt.fields.CollectionField, - Value: tt.fields.Value, - Include: tt.fields.Include, - sourceCodeVar: tt.fields.sourceCodeVar, - sourceCodeValue: tt.fields.sourceCodeValue, - fieldMarker: tt.fields.fieldMarker, - } - if err := rm.validate(); (err != nil) != tt.wantErr { - t.Errorf("ResourceMarker.validate() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestResourceMarker_associateFieldMarker(t *testing.T) { - t.Parallel() - - testMissing := "missing" - collectionFieldOne := "collectionFieldOne" - collectionFieldTwo := "collectionFieldTwo" - fieldOne := "fieldOne" - fieldTwo := "fieldTwo" - - testMarkers := &markerCollection{ - collectionFieldMarkers: []*CollectionFieldMarker{ - { - Name: collectionFieldOne, - Type: FieldString, - }, - { - Name: collectionFieldTwo, - Type: FieldString, - }, - }, - fieldMarkers: []*FieldMarker{ - { - Name: fieldOne, - Type: FieldString, - }, - { - Name: fieldTwo, - Type: FieldString, - }, - }, - } - - type fields struct { - Field *string - CollectionField *string - Value interface{} - Include *bool - sourceCodeVar string - sourceCodeValue string - fieldMarker interface{} - } - - type args struct { - markers *markerCollection - } - - tests := []struct { - name string - fields fields - args args - want *ResourceMarker - }{ - { - name: "resource marker with non-collection field first", - args: args{ - markers: testMarkers, - }, - fields: fields{ - Field: &fieldOne, - }, - want: &ResourceMarker{ - Field: &fieldOne, - fieldMarker: &FieldMarker{ - Name: fieldOne, - Type: FieldString, - }, - }, - }, - { - name: "resource marker with non-collection field second", - args: args{ - markers: testMarkers, - }, - fields: fields{ - Field: &fieldTwo, - }, - want: &ResourceMarker{ - Field: &fieldTwo, - fieldMarker: &FieldMarker{ - Name: fieldTwo, - Type: FieldString, - }, - }, - }, - { - name: "resource marker with collection field first", - args: args{ - markers: testMarkers, - }, - fields: fields{ - CollectionField: &collectionFieldOne, - }, - want: &ResourceMarker{ - CollectionField: &collectionFieldOne, - fieldMarker: &CollectionFieldMarker{ - Name: collectionFieldOne, - Type: FieldString, - }, - }, - }, - { - name: "resource marker with collection field second", - args: args{ - markers: testMarkers, - }, - fields: fields{ - CollectionField: &collectionFieldTwo, - }, - want: &ResourceMarker{ - CollectionField: &collectionFieldTwo, - fieldMarker: &CollectionFieldMarker{ - Name: collectionFieldTwo, - Type: FieldString, - }, - }, - }, - { - name: "resource marker with no related fields", - args: args{ - markers: testMarkers, - }, - fields: fields{ - CollectionField: &testMissing, - Field: &testMissing, - }, - want: &ResourceMarker{ - CollectionField: &testMissing, - Field: &testMissing, - }, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - rm := &ResourceMarker{ - Field: tt.fields.Field, - CollectionField: tt.fields.CollectionField, - Value: tt.fields.Value, - Include: tt.fields.Include, - sourceCodeVar: tt.fields.sourceCodeVar, - sourceCodeValue: tt.fields.sourceCodeValue, - fieldMarker: tt.fields.fieldMarker, - } - rm.associateFieldMarker(tt.args.markers) - assert.Equal(t, tt.want, rm) - }) - } -} diff --git a/internal/workload/v1/resource.go b/internal/workload/v1/resource.go index 810dcca..0964275 100644 --- a/internal/workload/v1/resource.go +++ b/internal/workload/v1/resource.go @@ -4,6 +4,7 @@ package v1 import ( + "errors" "fmt" "os" "path/filepath" @@ -12,6 +13,12 @@ import ( "gopkg.in/yaml.v3" "github.com/vmware-tanzu-labs/operator-builder/internal/utils" + "github.com/vmware-tanzu-labs/operator-builder/internal/workload/v1/markers" +) + +var ( + ErrChildResourceResourceMarkerInspect = errors.New("error inspecting resource markers for child resource") + ErrChildResourceResourceMarkerProcess = errors.New("error processing resource markers for child resource") ) // SourceFile represents a golang source code file that contains one or more @@ -57,8 +64,8 @@ func (r *Resource) loadContent(isCollection bool) error { if isCollection { // replace all instances of collection markers and collection field markers with regular field markers // as a collection marker on a collection is simply a field marker to itself - content := strings.ReplaceAll(string(manifestContent), collectionFieldMarker, fieldMarker) - content = strings.ReplaceAll(content, resourceMarkerCollectionFieldName, resourceMarkerFieldName) + content := strings.ReplaceAll(string(manifestContent), markers.CollectionFieldMarkerPrefix, markers.FieldMarkerPrefix) + content = strings.ReplaceAll(content, markers.ResourceMarkerCollectionFieldName, markers.ResourceMarkerFieldName) r.Content = []byte(content) } else { @@ -171,21 +178,11 @@ func expandResources(path string, resources []*Resource) ([]*Resource, error) { return expandedResources, nil } -const ( - includeCode = `if %s != %s { - return []client.Object{}, nil - }` - - excludeCode = `if %s == %s { - return []client.Object{}, nil - }` -) - -func (cr *ChildResource) processResourceMarkers(markers *markerCollection) error { - // obtain the marker results from the input yaml - _, markerResults, err := inspectMarkersForYAML([]byte(cr.StaticContent), ResourceMarkerType) +func (cr *ChildResource) processResourceMarkers(markerCollection *markers.MarkerCollection) error { + // obtain the marker results from the child resource input yaml + _, markerResults, err := markers.InspectForYAML([]byte(cr.StaticContent), markers.ResourceMarkerType) if err != nil { - return err + return fmt.Errorf("%w; %s", err, ErrChildResourceResourceMarkerInspect) } // ensure we have the expected number of resource markers @@ -197,38 +194,24 @@ func (cr *ChildResource) processResourceMarkers(markers *markerCollection) error return nil } - filtered := filterResourceMarkers(markerResults) - - var resourceMarker *ResourceMarker - //nolint: godox // depends on https://github.com/vmware-tanzu-labs/operator-builder/issues/271 // TODO: we need to ensure only one marker is found and return an error if we find more than one. // this becomes difficult as the results are returned as yaml nodes. for now, we just focus on the // first result and all others are ignored but we should notify the user. - // if len(filtered) == 1 { - marker := filtered[0] - - // associate the marker with a field marker - marker.associateFieldMarker(markers) + result := markerResults[0] - if marker.fieldMarker != nil { - resourceMarker = marker - } else { - return fmt.Errorf("%w; %v", ErrAssociateResourceMarker, marker) + // process the marker + marker, ok := result.Object.(markers.ResourceMarker) + if !ok { + return ErrChildResourceResourceMarkerProcess } - // } else { - // return fmt.Errorf("%w, found %d; markers: %v", ErrNumberResourceMarkers, len(filtered), filtered[1].Value) - // } - // process the marker and set the code snippet - if err := resourceMarker.process(); err != nil { - return err + if err := marker.Process(markerCollection); err != nil { + return fmt.Errorf("%w; %s", err, ErrChildResourceResourceMarkerProcess) } - if *resourceMarker.Include { - cr.IncludeCode = fmt.Sprintf(includeCode, resourceMarker.sourceCodeVar, resourceMarker.sourceCodeValue) - } else { - cr.IncludeCode = fmt.Sprintf(excludeCode, resourceMarker.sourceCodeVar, resourceMarker.sourceCodeValue) + if marker.GetIncludeCode() != "" { + cr.IncludeCode = marker.GetIncludeCode() } return nil diff --git a/internal/workload/v1/standalone.go b/internal/workload/v1/standalone.go index 9db943d..a8837a3 100644 --- a/internal/workload/v1/standalone.go +++ b/internal/workload/v1/standalone.go @@ -10,6 +10,7 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "github.com/vmware-tanzu-labs/operator-builder/internal/utils" + "github.com/vmware-tanzu-labs/operator-builder/internal/workload/v1/markers" ) var ErrNoComponentsOnStandalone = errors.New("cannot set component workloads on a component workload - only on collections") @@ -145,7 +146,7 @@ func (s *StandaloneWorkload) SetRBAC() { } func (s *StandaloneWorkload) SetResources(workloadPath string) error { - err := s.Spec.processManifests(FieldMarkerType) + err := s.Spec.processManifests(markers.FieldMarkerType) if err != nil { return err } diff --git a/internal/workload/v1/workload.go b/internal/workload/v1/workload.go index bdb18f4..8def939 100644 --- a/internal/workload/v1/workload.go +++ b/internal/workload/v1/workload.go @@ -16,6 +16,7 @@ import ( "k8s.io/client-go/kubernetes/scheme" "github.com/vmware-tanzu-labs/operator-builder/internal/markers/inspect" + "github.com/vmware-tanzu-labs/operator-builder/internal/workload/v1/markers" ) // WorkloadAPISpec sample fields which may be used in things like testing or @@ -45,20 +46,20 @@ type WorkloadShared struct { // WorkloadSpec contains information required to generate source code. type WorkloadSpec struct { - Resources []*Resource `json:"resources" yaml:"resources"` - FieldMarkers []*FieldMarker `json:",omitempty" yaml:",omitempty" validate:"omitempty"` - CollectionFieldMarkers []*CollectionFieldMarker `json:",omitempty" yaml:",omitempty" validate:"omitempty"` - ForCollection bool `json:",omitempty" yaml:",omitempty" validate:"omitempty"` - Collection *WorkloadCollection `json:",omitempty" yaml:",omitempty" validate:"omitempty"` - APISpecFields *APIFields `json:",omitempty" yaml:",omitempty" validate:"omitempty"` - SourceFiles *[]SourceFile `json:",omitempty" yaml:",omitempty" validate:"omitempty"` - RBACRules *RBACRules `json:",omitempty" yaml:",omitempty" validate:"omitempty"` + Resources []*Resource `json:"resources" yaml:"resources"` + FieldMarkers []*markers.FieldMarker `json:",omitempty" yaml:",omitempty" validate:"omitempty"` + CollectionFieldMarkers []*markers.CollectionFieldMarker `json:",omitempty" yaml:",omitempty" validate:"omitempty"` + ForCollection bool `json:",omitempty" yaml:",omitempty" validate:"omitempty"` + Collection *WorkloadCollection `json:",omitempty" yaml:",omitempty" validate:"omitempty"` + APISpecFields *APIFields `json:",omitempty" yaml:",omitempty" validate:"omitempty"` + SourceFiles *[]SourceFile `json:",omitempty" yaml:",omitempty" validate:"omitempty"` + RBACRules *RBACRules `json:",omitempty" yaml:",omitempty" validate:"omitempty"` } func (ws *WorkloadSpec) init() { ws.APISpecFields = &APIFields{ Name: "Spec", - Type: FieldStruct, + Type: markers.FieldStruct, Tags: fmt.Sprintf("`json: %q`", "spec"), Sample: "spec:", } @@ -94,8 +95,8 @@ func (ws *WorkloadSpec) appendCollectionRef() { // append to children collectionField := &APIFields{ Name: "Collection", - Type: FieldStruct, - Tags: fmt.Sprintf("`json:%q`", "collection,omitempty"), + Type: markers.FieldStruct, + Tags: fmt.Sprintf("`json:%q`", "collection"), Sample: "#collection:", StructName: "CollectionSpec", Markers: []string{ @@ -109,11 +110,10 @@ func (ws *WorkloadSpec) appendCollectionRef() { Comments: nil, Children: []*APIFields{ { - Name: "Name", - Type: FieldString, - Tags: fmt.Sprintf("`json:%q`", "name,omitempty"), - Sample: fmt.Sprintf("#name: %q", strings.ToLower(ws.Collection.GetAPIKind())+"-sample"), - Comments: nil, + Name: "Name", + Type: markers.FieldString, + Tags: fmt.Sprintf("`json:%q`", "name"), + Sample: fmt.Sprintf("#name: %q", strings.ToLower(ws.Collection.GetAPIKind())+"-sample"), Markers: []string{ "+kubebuilder:validation:Required", "Required if specifying collection. The name of the collection", @@ -121,11 +121,10 @@ func (ws *WorkloadSpec) appendCollectionRef() { }, }, { - Name: "Namespace", - Type: FieldString, - Tags: fmt.Sprintf("`json:%q`", "namespace,omitempty"), - Sample: fmt.Sprintf("#namespace: %q", sampleNamespace), - Comments: nil, + Name: "Namespace", + Type: markers.FieldString, + Tags: fmt.Sprintf("`json:%q`", "namespace"), + Sample: fmt.Sprintf("#namespace: %q", sampleNamespace), Markers: []string{ "+kubebuilder:validation:Optional", "(Default: \"\") The namespace where the collection exists. Required only if", @@ -148,7 +147,7 @@ func NewSampleAPISpec() *WorkloadAPISpec { } } -func (ws *WorkloadSpec) processManifests(markerTypes ...MarkerType) error { +func (ws *WorkloadSpec) processManifests(markerTypes ...markers.MarkerType) error { ws.init() for _, manifestFile := range ws.Resources { @@ -221,8 +220,8 @@ func (ws *WorkloadSpec) processManifests(markerTypes ...MarkerType) error { return nil } -func (ws *WorkloadSpec) processMarkers(manifestFile *Resource, markerTypes ...MarkerType) error { - nodes, markerResults, err := inspectMarkersForYAML(manifestFile.Content, markerTypes...) +func (ws *WorkloadSpec) processMarkers(manifestFile *Resource, markerTypes ...markers.MarkerType) error { + nodes, markerResults, err := markers.InspectForYAML(manifestFile.Content, markerTypes...) if err != nil { return formatProcessError(manifestFile.FileName, err) } @@ -250,7 +249,8 @@ func (ws *WorkloadSpec) processMarkers(manifestFile *Resource, markerTypes ...Ma // where there should be collection markers - they will result in // code that won't compile. We will convert collection markers to // field markers for the sake of UX. - if containsMarkerType(markerTypes, FieldMarkerType) && containsMarkerType(markerTypes, CollectionMarkerType) { + if markers.ContainsMarkerType(markerTypes, markers.FieldMarkerType) && + markers.ContainsMarkerType(markerTypes, markers.CollectionMarkerType) { // find & replace collection markers with field markers manifestFile.Content = []byte(strings.ReplaceAll(string(manifestFile.Content), "!!var collection", "!!var parent")) manifestFile.Content = []byte(strings.ReplaceAll(string(manifestFile.Content), "!!start collection", "!!start parent")) @@ -259,10 +259,10 @@ func (ws *WorkloadSpec) processMarkers(manifestFile *Resource, markerTypes ...Ma return nil } -func (ws *WorkloadSpec) processResourceMarkers(markers *markerCollection) error { +func (ws *WorkloadSpec) processResourceMarkers(markerCollection *markers.MarkerCollection) error { for _, sourceFile := range *ws.SourceFiles { for i := range sourceFile.Children { - if err := sourceFile.Children[i].processResourceMarkers(markers); err != nil { + if err := sourceFile.Children[i].processResourceMarkers(markerCollection); err != nil { return err } } @@ -277,64 +277,47 @@ func (ws *WorkloadSpec) processMarkerResults(markerResults []*inspect.YAMLResult var sampleVal interface{} - switch r := markerResult.Object.(type) { - case FieldMarker: - comments := []string{} + // convert to interface + var marker markers.FieldMarkerProcessor - if r.Description != nil { - comments = append(comments, strings.Split(*r.Description, "\n")...) - } - - if r.Default != nil { - defaultFound = true - sampleVal = r.Default - } else { - sampleVal = r.originalValue - } - - if err := ws.APISpecFields.AddField( - r.Name, - r.Type, - comments, - sampleVal, - defaultFound, - ); err != nil { - return err - } - - r.forCollection = ws.ForCollection - ws.FieldMarkers = append(ws.FieldMarkers, &r) - - case CollectionFieldMarker: - comments := []string{} - - if r.Description != nil { - comments = append(comments, strings.Split(*r.Description, "\n")...) - } + switch t := markerResult.Object.(type) { + case *markers.FieldMarker: + marker = t + ws.FieldMarkers = append(ws.FieldMarkers, t) + case *markers.CollectionFieldMarker: + marker = t + ws.CollectionFieldMarkers = append(ws.CollectionFieldMarkers, t) + default: + continue + } - if r.Default != nil { - defaultFound = true - sampleVal = r.Default - } else { - sampleVal = r.originalValue - } + // set the comments based on the description field of a field marker + comments := []string{} - if err := ws.APISpecFields.AddField( - r.Name, - r.Type, - comments, - sampleVal, - defaultFound, - ); err != nil { - return err - } + if marker.GetDescription() != "" { + comments = append(comments, strings.Split(marker.GetDescription(), "\n")...) + } - r.forCollection = ws.ForCollection - ws.CollectionFieldMarkers = append(ws.CollectionFieldMarkers, &r) + // set the sample value based on if a default was specified in the marker or not + if marker.GetDefault() != nil { + defaultFound = true + sampleVal = marker.GetDefault() + } else { + sampleVal = marker.GetOriginalValue() + } - default: - continue + // add the field to the api specification + if err := ws.APISpecFields.AddField( + marker.GetName(), + marker.GetFieldType(), + comments, + sampleVal, + defaultFound, + ); err != nil { + return err } + + marker.SetForCollection(ws.ForCollection) } return nil diff --git a/test/cases/platform/.workloadConfig/ingress/contour-resources/config-map.yaml b/test/cases/platform/.workloadConfig/ingress/contour-resources/config-map.yaml index 25794cb..204f0be 100644 --- a/test/cases/platform/.workloadConfig/ingress/contour-resources/config-map.yaml +++ b/test/cases/platform/.workloadConfig/ingress/contour-resources/config-map.yaml @@ -22,5 +22,7 @@ kind: ConfigMap metadata: name: contour-config-test-parse-comment namespace: ingress-system # +operator-builder:field:name=namespace,default=ingress-system,type=string + labels: + provider: "aws" # +operator-builder:collection:field:name=provider,type=string,default="aws" data: this: "serves no purpose other than to test comment spaces for resource markers on collection fields"