diff --git a/go.mod b/go.mod index a1ca520a..6eb29dbf 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/aws-controllers-k8s/code-generator go 1.17 require ( - github.com/aws-controllers-k8s/runtime v0.18.4 + github.com/aws-controllers-k8s/runtime v0.19.0 github.com/aws/aws-sdk-go v1.42.0 github.com/dlclark/regexp2 v1.4.0 // pin to v0.1.1 due to release problem with v0.1.2 diff --git a/go.sum b/go.sum index 81e36cd7..c6ddcc48 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws-controllers-k8s/runtime v0.18.4 h1:iwLYNwhbuiWZrHPoulGj75oT+alE91wCNkF1FUELiAw= -github.com/aws-controllers-k8s/runtime v0.18.4/go.mod h1:oA8ML1/LL3chPn26P6SzBNu1CUI2nekB+PTqykNs0qU= +github.com/aws-controllers-k8s/runtime v0.19.0 h1:+O5a6jBSBAd8XTNMrVCIYu4G+ZUPZe/G5eopVFO18Dc= +github.com/aws-controllers-k8s/runtime v0.19.0/go.mod h1:oA8ML1/LL3chPn26P6SzBNu1CUI2nekB+PTqykNs0qU= github.com/aws/aws-sdk-go v1.42.0 h1:BMZws0t8NAhHFsfnT3B40IwD13jVDG5KerlRksctVIw= github.com/aws/aws-sdk-go v1.42.0/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= diff --git a/pkg/config/resource.go b/pkg/config/resource.go index 2373d5e8..510f43ba 100644 --- a/pkg/config/resource.go +++ b/pkg/config/resource.go @@ -96,6 +96,28 @@ type ResourceConfig struct { // IsARNPrimaryKey determines whether the CRD uses the ARN as the primary // identifier in the ReadOne operations. IsARNPrimaryKey bool `json:"is_arn_primary_key"` + // TagConfig contains instructions for the code generator to generate + // custom code for ensuring tags + TagConfig *TagConfig `json:"tags,omitempty"` +} + +// TagConfig instructs the code generator on how to generate functions that +// ensure that controller tags are added to the AWS Resource +type TagConfig struct { + // Ignore is a boolean that indicates whether ensuring controller tags + // should be ignored for a resource. For AWS resources which do not + // support tagging, this should be set to True + Ignore bool `json:"ignore,omitempty"` + // Path represents the field path for the member which contains the tags + Path *string `json:"path,omitempty"` + // KeyMemberName is the name of field which represents AWS tag key inside tag + // struct. This is only used for tag fields with shape as list of struct, + // where the struct represents a single tag. + KeyMemberName *string `json:"key_name,omitempty"` + // ValueMemberName is the name of field which represents AWS tag value inside + // tag struct. This is only used for tag fields with shape as list of struct, + // where the struct represents a single tag. + ValueMemberName *string `json:"value_name,omitempty"` } // SyncedConfig instructs the code generator on how to generate functions that checks @@ -625,3 +647,14 @@ func (c *Config) GetListOpMatchFieldNames( } return rConfig.ListOperation.MatchFields } + +// TagsAreIgnored returns whether ensuring controller tags should be ignored +// for a resource or not. +func (c *Config) TagsAreIgnored(resName string) bool { + if rConfig, found := c.Resources[resName]; found { + if tagConfig := rConfig.TagConfig; tagConfig != nil { + return tagConfig.Ignore + } + } + return false +} diff --git a/pkg/generate/ack/controller.go b/pkg/generate/ack/controller.go index 0371fe0b..a06488f7 100644 --- a/pkg/generate/ack/controller.go +++ b/pkg/generate/ack/controller.go @@ -169,6 +169,12 @@ var ( "CheckNilReferencesPath": func(f *ackmodel.Field, sourceVarName string) string { return code.CheckNilReferencesPath(f, sourceVarName) }, + "GoCodeInitializeNestedStructField": func(r *ackmodel.CRD, + sourceVarName string, f *ackmodel.Field, apiPkgImportName string, + indentLevel int) string { + return code.InitializeNestedStructField(r, sourceVarName, f, + apiPkgImportName, indentLevel) + }, } ) @@ -220,9 +226,14 @@ func Controller( "references.go.tpl", "resource.go.tpl", "sdk.go.tpl", + "tags.go.tpl", } for _, crd := range crds { for _, target := range targets { + // skip adding "tags.go.tpl" file if tagging is ignored for a crd + if target == "tags.go.tpl" && crd.Config().TagsAreIgnored(crd.Names.Original) { + continue + } outPath := filepath.Join("pkg/resource", crd.Names.Snake, strings.TrimSuffix(target, ".tpl")) tplPath := filepath.Join("pkg/resource", target) crdVars := &templateCRDVars{ diff --git a/pkg/generate/ack/hook.go b/pkg/generate/ack/hook.go index d2bd406f..a6736b13 100644 --- a/pkg/generate/ack/hook.go +++ b/pkg/generate/ack/hook.go @@ -63,6 +63,12 @@ code paths: * late_initialize_post_read_one * references_pre_resolve * references_post_resolve +* ensure_tags +* convert_tags +* convert_tags_pre_to_ack_tags +* convert_tags_post_to_ack_tags +* convert_tags_pre_from_ack_tags +* convert_tags_post_from_ack_tags The "pre_build_request" hooks are called BEFORE the call to construct the Input shape that is used in the API operation and therefore BEFORE @@ -119,6 +125,24 @@ method The "references_post_resolve" hooks are called AFTER resolving the references for all Reference fields inside AWSResourceManager.ResolveReferences() method + +The "ensure_tags" hooks provide the complete custom implementation for +AWSResourceManager.EnsureTags() method + +The "convert_tags" hooks provide the complete custom implementation for +"ToACKTags" and "FromACKTags" methods. + +The "convert_tags_pre_to_ack_tags" are called before converting the K8s +resource tags into ACK tags + +The "convert_tags_post_to_ack_tags" are called after converting the K8s +resource tags into ACK tags + +The "convert_tags_pre_from_ack_tags" are called before converting the ACK +tags into K8s resource tags + +The "convert_tags_post_from_ack_tags" are called after converting the ACK +tags into K8s resource tags */ // ResourceHookCode returns a string with custom callback code for a resource diff --git a/pkg/generate/ack/runtime_test.go b/pkg/generate/ack/runtime_test.go index b9d7a50f..e23f60bc 100644 --- a/pkg/generate/ack/runtime_test.go +++ b/pkg/generate/ack/runtime_test.go @@ -134,6 +134,14 @@ func (frm *fakeRM) IsSynced(context.Context, acktypes.AWSResource) (bool, error) return true, nil } +func (frm *fakeRM) EnsureTags( + context.Context, + acktypes.AWSResource, + acktypes.ServiceControllerMetadata, +) error { + return nil +} + // This test is mostly just a hack to introduce a Go module dependency between // the ACK runtime library and the code generator. The code generator doesn't // actually depend on Go code in the ACK runtime, but it *produces* templated diff --git a/pkg/generate/code/initialize_field.go b/pkg/generate/code/initialize_field.go new file mode 100644 index 00000000..f634f68f --- /dev/null +++ b/pkg/generate/code/initialize_field.go @@ -0,0 +1,107 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package code + +import ( + "fmt" + "strings" + + "github.com/aws-controllers-k8s/code-generator/pkg/fieldpath" + + "github.com/aws-controllers-k8s/code-generator/pkg/model" +) + +// InitializeNestedStructField returns the go code for initializing a nested +// struct field. Currently this method only supports the struct shape for +// nested elements. +// +// TODO(vijtrip2): Refactor the code out of set_resource.go for generating +// constructors and reuse here. This method is currently being used for handling +// nested Tagging fields. +// +// Example: generated code for "Logging.LoggingEnabled.TargetBucket" field +// inside "s3" "bucket" crd looks like: +// +// ``` +// r.ko.Spec.Logging = &svcapitypes.BucketLoggingStatus{} +// r.ko.Spec.Logging.LoggingEnabled = &svcapitypes.LoggingEnabled{} +// ``` +func InitializeNestedStructField( + r *model.CRD, + sourceVarName string, + field *model.Field, + // apiPkgAlias contains the imported package alias where the type definition + // for nested structs is present. + // ex: svcapitypes "github.com/aws-controllers-k8s/s3-controller/apis/v1alpha1" + apiPkgAlias string, + // Number of levels of indentation to use + indentLevel int, +) string { + out := "" + indent := strings.Repeat("\t", indentLevel) + fieldPath := field.Path + if fieldPath != "" { + fp := fieldpath.FromString(fieldPath) + if fp.Size() > 1 { + // replace the front field name with front field shape name inside + // the field path to construct the fieldShapePath + front := fp.Front() + frontField := r.Fields[front] + if frontField == nil { + panic(fmt.Sprintf("unable to find the field with name %s"+ + " for fieldpath %s", front, fieldPath)) + } + if frontField.ShapeRef == nil { + panic(fmt.Sprintf("nil ShapeRef for field %s", front)) + } + fieldShapePath := strings.Replace(fieldPath, front, + frontField.ShapeRef.ShapeName, 1) + fsp := fieldpath.FromString(fieldShapePath) + var index int + // Build the prefix to access elements in field path. + // Use the front of fieldpath to determine whether the field is + // a spec field or status field. + elemAccessPrefix := sourceVarName + if _, found := r.SpecFields[front]; found { + elemAccessPrefix = fmt.Sprintf("%s%s", elemAccessPrefix, + r.Config().PrefixConfig.SpecField) + } else { + elemAccessPrefix = fmt.Sprintf("%s%s", elemAccessPrefix, + r.Config().PrefixConfig.StatusField) + } + var importPath string + if apiPkgAlias != "" { + importPath = fmt.Sprintf("%s.", apiPkgAlias) + } + // traverse over the fieldShapePath and initialize every element + // except the last. + for index < fsp.Size()-1 { + elemName := fp.At(index) + elemShapeRef := fsp.ShapeRefAt(frontField.ShapeRef, index) + if elemShapeRef.Shape.Type != "structure" { + panic(fmt.Sprintf("only nested structures are supported."+ + " Shape type for %s is %s inside fieldpath %s", elemName, + elemShapeRef.Shape.Type, fieldPath)) + } + out += fmt.Sprintf("%s%s.%s = &%s%s{}\n", + indent, elemAccessPrefix, elemName, importPath, + elemShapeRef.GoTypeElem()) + elemAccessPrefix = fmt.Sprintf("%s.%s", elemAccessPrefix, + elemName) + index++ + } + } + } + return out +} diff --git a/pkg/generate/code/initialize_field_test.go b/pkg/generate/code/initialize_field_test.go new file mode 100644 index 00000000..f8667454 --- /dev/null +++ b/pkg/generate/code/initialize_field_test.go @@ -0,0 +1,30 @@ +package code_test + +import ( + "testing" + + "github.com/aws-controllers-k8s/code-generator/pkg/generate/code" + + "github.com/aws-controllers-k8s/code-generator/pkg/testutil" + "github.com/stretchr/testify/assert" +) + +func TestInitializeNestedStructField(t *testing.T) { + assert := assert.New(t) + + g := testutil.NewModelForServiceWithOptions(t, "s3", + &testutil.TestingModelOptions{GeneratorConfigFile: "generator-with-tags.yaml"}) + + crd := testutil.GetCRDByName(t, g, "Bucket") + assert.NotNil(crd) + + f := crd.Fields["Logging.LoggingEnabled.TargetBucket"] + + s := code.InitializeNestedStructField(crd, "r.ko", f, + "svcapitypes", 1) + expected := + ` r.ko.Spec.Logging = &svcapitypes.BucketLoggingStatus{} + r.ko.Spec.Logging.LoggingEnabled = &svcapitypes.LoggingEnabled{} +` + assert.Equal(expected, s) +} diff --git a/pkg/model/model_apigwv2_test.go b/pkg/model/model_apigwv2_test.go index 8255b98c..fc09d3cf 100644 --- a/pkg/model/model_apigwv2_test.go +++ b/pkg/model/model_apigwv2_test.go @@ -54,6 +54,16 @@ func TestAPIGatewayV2_Api(t *testing.T) { crd := getCRDByName("Api", crds) require.NotNil(crd) + assert.False(crd.Config().TagsAreIgnored(crd.Names.Original)) + tfName, err := crd.GetTagFieldName() + assert.Nil(err) + assert.Equal("Tags", tfName) + + tf, err := crd.GetTagField() + assert.NotNil(tf) + assert.Nil(err) + assert.Equal("Tags", tf.Names.Original) + assert.Equal("API", crd.Names.Camel) assert.Equal("api", crd.Names.CamelLower) assert.Equal("api", crd.Names.Snake) @@ -84,6 +94,17 @@ func TestAPIGatewayV2_Route(t *testing.T) { crd := getCRDByName("Route", crds) require.NotNil(crd) + assert.True(crd.Config().TagsAreIgnored(crd.Names.Original)) + tfName, err := crd.GetTagFieldName() + assert.NotNil(err) + assert.Empty(tfName) + + tf, err := crd.GetTagField() + assert.Nil(tf) + + assert.Empty(crd.GetTagKeyMemberName()) + assert.Empty(crd.GetTagValueMemberName()) + assert.Equal("Route", crd.Names.Camel) assert.Equal("route", crd.Names.CamelLower) assert.Equal("route", crd.Names.Snake) diff --git a/pkg/model/model_ecr_test.go b/pkg/model/model_ecr_test.go index 9c8fdde1..f702016d 100644 --- a/pkg/model/model_ecr_test.go +++ b/pkg/model/model_ecr_test.go @@ -34,6 +34,20 @@ func TestECRRepository(t *testing.T) { crd := getCRDByName("Repository", crds) require.NotNil(crd) + assert.False(crd.Config().TagsAreIgnored(crd.Names.Original)) + tfName, err := crd.GetTagFieldName() + assert.Nil(err) + assert.Equal("Tags", tfName) + + tf, err := crd.GetTagField() + assert.NotNil(tf) + assert.Nil(err) + assert.Equal("Tags", tf.Names.Original) + + tagKeyMemberName := crd.GetTagKeyMemberName() + tagValueMemberName := crd.GetTagValueMemberName() + assert.Equal("Key", tagKeyMemberName) + assert.Equal("Value", tagValueMemberName) // The ECR Repository API has just the C and R of the normal CRUD // operations: // diff --git a/pkg/model/tagging.go b/pkg/model/tagging.go new file mode 100644 index 00000000..701c1a6f --- /dev/null +++ b/pkg/model/tagging.go @@ -0,0 +1,121 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package model + +import ( + "fmt" + "strings" +) + +// GetTagFieldName returns the name of field containing AWS tags. The default +// name is "Tags". If no tag field is found inside the CRD, an error is returned +func (r *CRD) GetTagFieldName() (string, error) { + tagFieldPath := "Tags" // default tag field path + + // If there is an explicit tag field path mentioned inside generator config, + // retrieve it. + if resConfig := r.cfg.GetResourceConfig(r.Names.Original); resConfig != nil { + if tagConfig := resConfig.TagConfig; tagConfig != nil { + if tagConfig.Path != nil { + tagFieldPath = *tagConfig.Path + } + } + } + // verify that the tagFieldPath exists inside CRD + for fName, field := range r.Fields { + if strings.EqualFold(field.Path, tagFieldPath) { + return fName, nil + } + } + // If the tagFieldPath did not exist in CRD, return an error + return "", fmt.Errorf("tag field path %s does not exist inside %s"+ + " crd", tagFieldPath, r.Names.Original) +} + +// GetTagField return the model.Field representing the Tag field for CRD. If no +// such field is found an error is returned. +func (r *CRD) GetTagField() (*Field, error) { + if r.cfg.TagsAreIgnored(r.Names.Original) { + return nil, nil + } + tagFieldName, err := r.GetTagFieldName() + if err != nil { + return nil, err + } + tagField := r.Fields[tagFieldName] + return tagField, nil +} + +// GetTagKeyMemberName returns the member name which represents tag key. +// The default value is "Key". This is only applicable for tag fields with +// shape list of struct.If the tag field shape is not list of struct, an empty +// string is returned. +func (r *CRD) GetTagKeyMemberName() (keyMemberName string) { + tagField, _ := r.GetTagField() + if tagField == nil { + return keyMemberName + } + // TagKey member field will only be present when the tag field shape is + // list of struct + if isListOfStruct(tagField) { + keyMemberName = "Key" + if resConfig := r.Config().GetResourceConfig(r.Names.Original); resConfig != nil { + if tagsConfig := resConfig.TagConfig; tagsConfig != nil { + if tagsConfig.KeyMemberName != nil && *tagsConfig.KeyMemberName != "" { + keyMemberName = *tagsConfig.KeyMemberName + } + } + } + } + return keyMemberName +} + +// GetTagValueMemberName returns the member name which represents tag value. +// The default value is "Value". This is only applicable for tag fields with +// shape list of struct.If the tag field shape is not list of struct, an empty +// string is returned. +func (r *CRD) GetTagValueMemberName() (valueMemberName string) { + tagField, _ := r.GetTagField() + if tagField == nil { + return valueMemberName + } + // TagValue member field will only be present when the tag field shape is + // list of struct. + if isListOfStruct(tagField) { + valueMemberName = "Value" + if resConfig := r.Config().GetResourceConfig(r.Names.Original); resConfig != nil { + if tagsConfig := resConfig.TagConfig; tagsConfig != nil { + if tagsConfig.ValueMemberName != nil && *tagsConfig.ValueMemberName != "" { + valueMemberName = *tagsConfig.ValueMemberName + } + } + } + } + return valueMemberName +} + +// isListOfStruct method returns true is the shape of field is list of struct, +// false otherwise +func isListOfStruct(f *Field) bool { + if f.ShapeRef != nil && f.ShapeRef.Shape != nil { + if f.ShapeRef.Shape.Type == "list" { + if f.ShapeRef.Shape.MemberRef.Shape != nil { + if f.ShapeRef.Shape.MemberRef.Shape.Type == "structure" { + return true + } + } + } + } + return false +} diff --git a/pkg/testdata/models/apis/apigatewayv2/0000-00-00/generator.yaml b/pkg/testdata/models/apis/apigatewayv2/0000-00-00/generator.yaml index 625edd27..58cdbf80 100644 --- a/pkg/testdata/models/apis/apigatewayv2/0000-00-00/generator.yaml +++ b/pkg/testdata/models/apis/apigatewayv2/0000-00-00/generator.yaml @@ -19,6 +19,9 @@ resources: is_required: false update_operation: custom_method_name: customUpdateApi + Route: + tags: + ignore: True operations: CreateApi: custom_implementation: customCreateApi diff --git a/pkg/testdata/models/apis/s3/0000-00-00/generator-with-tags.yaml b/pkg/testdata/models/apis/s3/0000-00-00/generator-with-tags.yaml new file mode 100644 index 00000000..1e6100f9 --- /dev/null +++ b/pkg/testdata/models/apis/s3/0000-00-00/generator-with-tags.yaml @@ -0,0 +1,36 @@ +ignore: + resource_names: + - Object + - MultipartUpload + shape_names: + # These shapes are structs with no members... + - SSES3 +resources: + Bucket: + tags: + path: Tagging.TagSet + renames: + operations: + CreateBucket: + input_fields: + Bucket: Name + DeleteBucket: + input_fields: + Bucket: Name + list_operation: + match_fields: + - Name + fields: + Tagging: + from: + operation: PutBucketTagging + path: Tagging + ACL: + # This is to test the ackcompare field ignore functionality. This + # should NOT be in a production generator.yaml... + compare: + is_ignored: true + Logging: + from: + operation: PutBucketLogging + path: BucketLoggingStatus \ No newline at end of file diff --git a/templates/cmd/controller/main.go.tpl b/templates/cmd/controller/main.go.tpl index 47bc7e7a..d9ecf569 100644 --- a/templates/cmd/controller/main.go.tpl +++ b/templates/cmd/controller/main.go.tpl @@ -8,6 +8,7 @@ import ( ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" ackcfg "github.com/aws-controllers-k8s/runtime/pkg/config" ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" ackrtutil "github.com/aws-controllers-k8s/runtime/pkg/util" ackrtwebhook "github.com/aws-controllers-k8s/runtime/pkg/webhook" flag "github.com/spf13/pflag" @@ -103,7 +104,7 @@ func main() { ) sc := ackrt.NewServiceController( awsServiceAlias, awsServiceAPIGroup, awsServiceEndpointsID, - ackrt.VersionInfo{ + acktypes.VersionInfo{ version.GitCommit, version.GitVersion, version.BuildDate, diff --git a/templates/config/controller/deployment.yaml.tpl b/templates/config/controller/deployment.yaml.tpl index f0777bed..ea4515f7 100644 --- a/templates/config/controller/deployment.yaml.tpl +++ b/templates/config/controller/deployment.yaml.tpl @@ -66,7 +66,7 @@ spec: - name: ACK_LOG_LEVEL value: "info" - name: ACK_RESOURCE_TAGS - value: "services.k8s.aws/managed=true,services.k8s.aws/created=%UTCNOW%,services.k8s.aws/namespace=%KUBERNETES_NAMESPACE%" + value: "services.k8s.aws/controller-version=%CONTROLLER_SERVICE%-%CONTROLLER_VERSION%,services.k8s.aws/namespace=%K8S_NAMESPACE%" securityContext: allowPrivilegeEscalation: false privileged: false diff --git a/templates/helm/values.yaml.tpl b/templates/helm/values.yaml.tpl index 2ef2e389..90bb7d9c 100644 --- a/templates/helm/values.yaml.tpl +++ b/templates/helm/values.yaml.tpl @@ -64,9 +64,8 @@ installScope: cluster resourceTags: # Configures the ACK service controller to always set key/value pairs tags on # resources that it manages. - - services.k8s.aws/managed=true - - services.k8s.aws/created=%UTCNOW% - - services.k8s.aws/namespace=%KUBERNETES_NAMESPACE% + - services.k8s.aws/controller-version=%CONTROLLER_SERVICE%-%CONTROLLER_VERSION% + - services.k8s.aws/namespace=%K8S_NAMESPACE% serviceAccount: # Specifies whether a service account should be created diff --git a/templates/pkg/resource/manager.go.tpl b/templates/pkg/resource/manager.go.tpl index 2167d2f5..31403e8b 100644 --- a/templates/pkg/resource/manager.go.tpl +++ b/templates/pkg/resource/manager.go.tpl @@ -14,19 +14,25 @@ import ( ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" ackmetrics "github.com/aws-controllers-k8s/runtime/pkg/metrics" ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" + acktags "github.com/aws-controllers-k8s/runtime/pkg/tags" acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" ackutil "github.com/aws-controllers-k8s/runtime/pkg/util" "github.com/aws/aws-sdk-go/aws/session" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" - svcsdk "github.com/aws/aws-sdk-go/service/{{ .ServicePackageName }}" svcsdkapi "github.com/aws/aws-sdk-go/service/{{ .ServicePackageName }}/{{ .ServicePackageName }}iface" + + svcapitypes "github.com/aws-controllers-k8s/{{ .ServicePackageName }}-controller/apis/{{ .APIVersion }}" ) var ( _ = ackutil.InStrings + _ = acktags.NewTags() + _ = ackrt.MissingImageTagValue + _ = svcapitypes.{{ .CRD.Kind }}{} ) // +kubebuilder:rbac:groups={{ .APIGroup }},resources={{ ToLower .CRD.Plural }},verbs=get;list;watch;create;update;patch;delete @@ -252,6 +258,52 @@ func (rm *resourceManager) IsSynced(ctx context.Context, res acktypes.AWSResourc return true, nil } +// EnsureTags ensures that tags are present inside the AWSResource. +// If the AWSResource does not have any existing resource tags, the 'tags' +// field is initialized and the controller tags are added. +// If the AWSResource has existing resource tags, then controller tags are +// added to the existing resource tags without overriding them. +// If the AWSResource does not support tags, only then the controller tags +// will not be added to the AWSResource. +func (rm *resourceManager) EnsureTags( + ctx context.Context, + res acktypes.AWSResource, + md acktypes.ServiceControllerMetadata, +) error { +{{- if $hookCode := Hook .CRD "ensure_tags" }} +{{ $hookCode }} +{{ else }} +{{ $tagField := .CRD.GetTagField -}} +{{ if $tagField -}} +{{ $tagFieldShapeType := $tagField.ShapeRef.Shape.Type -}} +{{ $tagFieldGoType := $tagField.GoType -}} +{{ if eq "list" $tagFieldShapeType -}} +{{ $tagFieldGoType = (print "[]*svcapitypes." $tagField.GoTypeElem) -}} +{{ end -}} + r := rm.concreteResource(res) + if r.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's EnsureTags method received resource with nil CR object") + } + defaultTags := ackrt.GetDefaultTags(&rm.cfg, r.ko, md) + var existingTags {{ $tagFieldGoType }} +{{ $nilCheck := CheckNilFieldPath $tagField "r.ko.Spec" -}} +{{ if not (eq $nilCheck "") -}} + if {{ $nilCheck }} { + existingTags = nil + } else { + existingTags = r.ko.Spec.{{ $tagField.Path }} + } +{{ end -}} + resourceTags := ToACKTags(existingTags) + tags := acktags.Merge(resourceTags, defaultTags) +{{ GoCodeInitializeNestedStructField .CRD "r.ko" $tagField "svcapitypes" 1 -}} + r.ko.Spec.{{ $tagField.Path }} = FromACKTags(tags) +{{- end }} + return nil +{{- end }} +} + // newResourceManager returns a new struct implementing // acktypes.AWSResourceManager func newResourceManager( diff --git a/templates/pkg/resource/tags.go.tpl b/templates/pkg/resource/tags.go.tpl new file mode 100644 index 00000000..aca02885 --- /dev/null +++ b/templates/pkg/resource/tags.go.tpl @@ -0,0 +1,87 @@ +{{ template "boilerplate" }} + +package {{ .CRD.Names.Snake }} + +import( + acktags "github.com/aws-controllers-k8s/runtime/pkg/tags" + + svcapitypes "github.com/aws-controllers-k8s/{{ .ServicePackageName }}-controller/apis/{{ .APIVersion }}" +) + +var ( + _ = svcapitypes.{{ .CRD.Kind }}{} + _ = acktags.NewTags() +) + +{{- if $hookCode := Hook .CRD "convert_tags" }} +{{ $hookCode }} +{{ else -}} +{{- $tagField := .CRD.GetTagField }} +{{- if $tagField }} +{{- $tagFieldShapeType := $tagField.ShapeRef.Shape.Type }} +{{- $tagFieldGoType := $tagField.GoType }} +{{- $keyMemberName := .CRD.GetTagKeyMemberName }} +{{- $valueMemberName := .CRD.GetTagValueMemberName }} +{{- if eq "list" $tagFieldShapeType }} +{{- $tagFieldGoType = (print "[]*svcapitypes." $tagField.GoTypeElem) }} +{{- end }} +// ToACKTags converts the tags parameter into 'acktags.Tags' shape. +// This method helps in creating the hub(acktags.Tags) for merging +// default controller tags with existing resource tags. +func ToACKTags(tags {{ $tagFieldGoType }}) acktags.Tags { + result := acktags.NewTags() +{{- if $hookCode := Hook .CRD "pre_convert_to_ack_tags" }} +{{ $hookCode }} +{{ end }} + if tags == nil || len(tags) == 0 { + return result + } +{{ if eq "map" $tagFieldShapeType }} + for k, v := range tags { + if v == nil { + result[k] = "" + } else { + result[k] = *v + } + } +{{ else if eq "list" $tagFieldShapeType }} + for _, t := range tags { + if t.{{ $valueMemberName }} == nil { + result[*t.{{ $keyMemberName}}] = "" + } else { + result[*t.{{ $keyMemberName }}] = *t.{{ $valueMemberName }} + } + } +{{ end }} +{{- if $hookCode := Hook .CRD "post_convert_to_ack_tags" }} +{{ $hookCode }} +{{ end }} + return result +} + +// FromACKTags converts the tags parameter into {{ $tagFieldGoType }} shape. +// This method helps in setting the tags back inside AWSResource after merging +// default controller tags with existing resource tags. +func FromACKTags(tags acktags.Tags) {{ $tagFieldGoType }} { + result := {{ $tagFieldGoType }}{} +{{- if $hookCode := Hook .CRD "pre_convert_from_ack_tags" }} +{{ $hookCode }} +{{ end }} + for k, v := range tags { +{{- if eq "map" $tagFieldShapeType }} + vCopy := v + result[k] = &vCopy +{{- else if eq "list" $tagFieldShapeType }} + kCopy := k + vCopy := v + tag := svcapitypes.{{ $tagField.GoTypeElem }}{ {{ $keyMemberName }}: &kCopy, {{ $valueMemberName }} : &vCopy} + result = append(result, &tag) +{{- end }} + } +{{- if $hookCode := Hook .CRD "post_convert_from_ack_tags" }} +{{ $hookCode }} +{{ end }} + return result +} +{{ end }} +{{ end }} \ No newline at end of file