diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index 759548a7..ab7f96d2 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,13 +1,13 @@ ack_generate_info: - build_date: "2024-02-24T00:04:37Z" - build_hash: f429bd95efc1c7286e7cc973dc174a30490a5521 - go_version: go1.22.0 - version: v0.30.0-3-gf429bd9 + build_date: "2024-03-06T08:51:16Z" + build_hash: c2165b65565ab3a094cfb474c4396f4d14c7ef1e + go_version: go1.21.6 + version: v0.31.0 api_directory_checksum: 731faf4c5d6d6f5140b4e0786127df447f773217 api_version: v1alpha1 aws_sdk_go_version: v1.50.15 generator_config_info: - file_checksum: 0d728ab3662c7e538aff6727f087b54c5969fdcf + file_checksum: 43c8931df528f38bf2c179722e7878f4615d5584 original_file_name: generator.yaml last_modification: reason: API generation diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index 5ba794e6..692d6335 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -22,11 +22,22 @@ operations: StopPipelineExecution: operation_type: Delete resource_name: PipelineExecution + AddTags: + operation_type: Update + resource_name: Model resources: Model: hooks: delta_pre_compare: code: customSetDefaults(a, b) + sdk_read_one_pre_set_output: + template_path: model/sdk_read_one_pre_set_output.go.tpl + sdk_read_one_post_set_output: + template_path: model/sdk_read_one_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: model/sdk_update_pre_build_request.go.tpl + sdk_update_post_build_request: + template_path: model/sdk_update_post_build_request.go.tpl exceptions: errors: 404: @@ -37,9 +48,9 @@ resources: - InvalidParameterValue - MissingParameter fields: - Tags: - compare: - is_ignored: true + # Tags: + # compare: + # is_ignored: true EnableNetworkIsolation: late_initialize: min_backoff_seconds: 5 diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 951b2ee2..42921b1f 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -30,6 +30,7 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrlrt "sigs.k8s.io/controller-runtime" ctrlrtcache "sigs.k8s.io/controller-runtime/pkg/cache" + ctrlrthealthz "sigs.k8s.io/controller-runtime/pkg/healthz" ctrlrtmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" ctrlrtwebhook "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -137,6 +138,7 @@ func main() { LeaderElection: ackCfg.EnableLeaderElection, LeaderElectionID: "ack-" + awsServiceAPIGroup, LeaderElectionNamespace: ackCfg.LeaderElectionNamespace, + }) if err != nil { setupLog.Error( @@ -187,6 +189,21 @@ func main() { os.Exit(1) } + if err = mgr.AddHealthzCheck("health", ctrlrthealthz.Ping); err != nil { + setupLog.Error( + err, "unable to set up health check", + "aws.service", awsServiceAlias, + ) + os.Exit(1) + } + if err = mgr.AddReadyzCheck("check", ctrlrthealthz.Ping); err != nil { + setupLog.Error( + err, "unable to set up ready check", + "aws.service", awsServiceAlias, + ) + os.Exit(1) + } + setupLog.Info( "starting manager", "aws.service", awsServiceAlias, diff --git a/config/controller/deployment.yaml b/config/controller/deployment.yaml index 3964b564..6f4d4081 100644 --- a/config/controller/deployment.yaml +++ b/config/controller/deployment.yaml @@ -79,6 +79,18 @@ spec: capabilities: drop: - ALL + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 securityContext: seccompProfile: type: RuntimeDefault diff --git a/config/crd/common/bases/services.k8s.aws_adoptedresources.yaml b/config/crd/common/bases/services.k8s.aws_adoptedresources.yaml index 65eff735..7dca541d 100644 --- a/config/crd/common/bases/services.k8s.aws_adoptedresources.yaml +++ b/config/crd/common/bases/services.k8s.aws_adoptedresources.yaml @@ -3,7 +3,8 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null name: adoptedresources.services.k8s.aws spec: group: services.k8s.aws @@ -20,19 +21,14 @@ spec: description: AdoptedResource is the schema for the AdoptedResource API. properties: apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -46,149 +42,126 @@ spec: additionalKeys: additionalProperties: type: string - description: |- - AdditionalKeys represents any additional arbitrary identifiers used when - describing the target resource. + description: AdditionalKeys represents any additional arbitrary + identifiers used when describing the target resource. type: object arn: - description: |- - ARN is the AWS Resource Name for the resource. It is a globally - unique identifier. + description: ARN is the AWS Resource Name for the resource. It + is a globally unique identifier. type: string nameOrID: - description: |- - NameOrId is a user-supplied string identifier for the resource. It may - or may not be globally unique, depending on the type of resource. + description: NameOrId is a user-supplied string identifier for + the resource. It may or may not be globally unique, depending + on the type of resource. type: string type: object kubernetes: - description: |- - ResourceWithMetadata provides the values necessary to create a - Kubernetes resource and override any of its metadata values. + description: ResourceWithMetadata provides the values necessary to + create a Kubernetes resource and override any of its metadata values. properties: group: type: string kind: type: string metadata: - description: |- - ObjectMeta is metadata that all persisted resources must have, which includes all objects - users must create. - It is not possible to use `metav1.ObjectMeta` inside spec, as the controller-gen - automatically converts this to an arbitrary string-string map. - https://github.com/kubernetes-sigs/controller-tools/issues/385 - - - Active discussion about inclusion of this field in the spec is happening in this PR: - https://github.com/kubernetes-sigs/controller-tools/pull/395 - - - Until this is allowed, or if it never is, we will produce a subset of the object meta - that contains only the fields which the user is allowed to modify in the metadata. + description: "ObjectMeta is metadata that all persisted resources + must have, which includes all objects users must create. It + is not possible to use `metav1.ObjectMeta` inside spec, as the + controller-gen automatically converts this to an arbitrary string-string + map. https://github.com/kubernetes-sigs/controller-tools/issues/385 + \n Active discussion about inclusion of this field in the spec + is happening in this PR: https://github.com/kubernetes-sigs/controller-tools/pull/395 + \n Until this is allowed, or if it never is, we will produce + a subset of the object meta that contains only the fields which + the user is allowed to modify in the metadata." properties: annotations: additionalProperties: type: string - description: |- - Annotations is an unstructured key value map stored with a resource that may be - set by external tools to store and retrieve arbitrary metadata. They are not - queryable and should be preserved when modifying objects. - More info: http://kubernetes.io/docs/user-guide/annotations + description: 'Annotations is an unstructured key value map + stored with a resource that may be set by external tools + to store and retrieve arbitrary metadata. They are not queryable + and should be preserved when modifying objects. More info: + http://kubernetes.io/docs/user-guide/annotations' type: object generateName: - description: |- - GenerateName is an optional prefix, used by the server, to generate a unique - name ONLY IF the Name field has not been provided. - If this field is used, the name returned to the client will be different - than the name passed. This value will also be combined with a unique suffix. - The provided value has the same validation rules as the Name field, - and may be truncated by the length of the suffix required to make the value - unique on the server. - - - If this field is specified and the generated name exists, the server will - NOT return a 409 - instead, it will either return 201 Created or 500 with Reason - ServerTimeout indicating a unique name could not be found in the time allotted, and the client - should retry (optionally after the time indicated in the Retry-After header). - - - Applied only if Name is not specified. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency + description: "GenerateName is an optional prefix, used by + the server, to generate a unique name ONLY IF the Name field + has not been provided. If this field is used, the name returned + to the client will be different than the name passed. This + value will also be combined with a unique suffix. The provided + value has the same validation rules as the Name field, and + may be truncated by the length of the suffix required to + make the value unique on the server. \n If this field is + specified and the generated name exists, the server will + NOT return a 409 - instead, it will either return 201 Created + or 500 with Reason ServerTimeout indicating a unique name + could not be found in the time allotted, and the client + should retry (optionally after the time indicated in the + Retry-After header). \n Applied only if Name is not specified. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency" type: string labels: additionalProperties: type: string - description: |- - Map of string keys and values that can be used to organize and categorize - (scope and select) objects. May match selectors of replication controllers - and services. - More info: http://kubernetes.io/docs/user-guide/labels + description: 'Map of string keys and values that can be used + to organize and categorize (scope and select) objects. May + match selectors of replication controllers and services. + More info: http://kubernetes.io/docs/user-guide/labels' type: object name: - description: |- - Name must be unique within a namespace. Is required when creating resources, although - some resources may allow a client to request the generation of an appropriate name - automatically. Name is primarily intended for creation idempotence and configuration - definition. - Cannot be updated. - More info: http://kubernetes.io/docs/user-guide/identifiers#names + description: 'Name must be unique within a namespace. Is required + when creating resources, although some resources may allow + a client to request the generation of an appropriate name + automatically. Name is primarily intended for creation idempotence + and configuration definition. Cannot be updated. More info: + http://kubernetes.io/docs/user-guide/identifiers#names' type: string namespace: - description: |- - Namespace defines the space within each name must be unique. An empty namespace is - equivalent to the "default" namespace, but "default" is the canonical representation. - Not all objects are required to be scoped to a namespace - the value of this field for - those objects will be empty. - - - Must be a DNS_LABEL. - Cannot be updated. - More info: http://kubernetes.io/docs/user-guide/namespaces + description: "Namespace defines the space within each name + must be unique. An empty namespace is equivalent to the + \"default\" namespace, but \"default\" is the canonical + representation. Not all objects are required to be scoped + to a namespace - the value of this field for those objects + will be empty. \n Must be a DNS_LABEL. Cannot be updated. + More info: http://kubernetes.io/docs/user-guide/namespaces" type: string ownerReferences: - description: |- - List of objects depended by this object. If ALL objects in the list have - been deleted, this object will be garbage collected. If this object is managed by a controller, - then an entry in this list will point to this controller, with the controller field set to true. - There cannot be more than one managing controller. + description: List of objects depended by this object. If ALL + objects in the list have been deleted, this object will + be garbage collected. If this object is managed by a controller, + then an entry in this list will point to this controller, + with the controller field set to true. There cannot be more + than one managing controller. items: - description: |- - OwnerReference contains enough information to let you identify an owning - object. An owning object must be in the same namespace as the dependent, or - be cluster-scoped, so there is no namespace field. + description: OwnerReference contains enough information + to let you identify an owning object. An owning object + must be in the same namespace as the dependent, or be + cluster-scoped, so there is no namespace field. properties: apiVersion: description: API version of the referent. type: string blockOwnerDeletion: - description: |- - If true, AND if the owner has the "foregroundDeletion" finalizer, then - the owner cannot be deleted from the key-value store until this - reference is removed. - See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion - for how the garbage collector interacts with this field and enforces the foreground deletion. - Defaults to false. - To set this field, a user needs "delete" permission of the owner, - otherwise 422 (Unprocessable Entity) will be returned. + description: If true, AND if the owner has the "foregroundDeletion" + finalizer, then the owner cannot be deleted from the + key-value store until this reference is removed. Defaults + to false. To set this field, a user needs "delete" + permission of the owner, otherwise 422 (Unprocessable + Entity) will be returned. type: boolean controller: description: If true, this reference points to the managing controller. type: boolean kind: - description: |- - Kind of the referent. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names + description: 'Name of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#names' type: string uid: - description: |- - UID of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids + description: 'UID of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#uids' type: string required: - apiVersion @@ -212,14 +185,13 @@ spec: AdoptedResource. properties: conditions: - description: |- - A collection of `ackv1alpha1.Condition` objects that describe the various - terminal states of the adopted resource CR and its target custom resource + description: A collection of `ackv1alpha1.Condition` objects that + describe the various terminal states of the adopted resource CR + and its target custom resource items: - description: |- - Condition is the common struct used by all CRDs managed by ACK service - controllers to indicate terminal states of the CR and its backend AWS - service API resource + description: Condition is the common struct used by all CRDs managed + by ACK service controllers to indicate terminal states of the + CR and its backend AWS service API resource properties: lastTransitionTime: description: Last time the condition transitioned from one status diff --git a/config/crd/common/bases/services.k8s.aws_fieldexports.yaml b/config/crd/common/bases/services.k8s.aws_fieldexports.yaml index 4d3a8f1d..4a7ab61b 100644 --- a/config/crd/common/bases/services.k8s.aws_fieldexports.yaml +++ b/config/crd/common/bases/services.k8s.aws_fieldexports.yaml @@ -3,7 +3,8 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null name: fieldexports.services.k8s.aws spec: group: services.k8s.aws @@ -20,19 +21,14 @@ spec: description: FieldExport is the schema for the FieldExport API. properties: apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -40,17 +36,15 @@ spec: description: FieldExportSpec defines the desired state of the FieldExport. properties: from: - description: |- - ResourceFieldSelector provides the values necessary to identify an individual - field on an individual K8s resource. + description: ResourceFieldSelector provides the values necessary to + identify an individual field on an individual K8s resource. properties: path: type: string resource: - description: |- - NamespacedResource provides all the values necessary to identify an ACK - resource of a given type (within the same namespace as the custom resource - containing this type). + description: NamespacedResource provides all the values necessary + to identify an ACK resource of a given type (within the same + namespace as the custom resource containing this type). properties: group: type: string @@ -68,18 +62,16 @@ spec: - resource type: object to: - description: |- - FieldExportTarget provides the values necessary to identify the - output path for a field export. + description: FieldExportTarget provides the values necessary to identify + the output path for a field export. properties: key: description: Key overrides the default value (`.`) for the FieldExport target type: string kind: - description: |- - FieldExportOutputType represents all types that can be produced by a field - export operation + description: FieldExportOutputType represents all types that can + be produced by a field export operation enum: - configmap - secret @@ -102,14 +94,12 @@ spec: description: FieldExportStatus defines the observed status of the FieldExport. properties: conditions: - description: |- - A collection of `ackv1alpha1.Condition` objects that describe the various - recoverable states of the field CR + description: A collection of `ackv1alpha1.Condition` objects that + describe the various recoverable states of the field CR items: - description: |- - Condition is the common struct used by all CRDs managed by ACK service - controllers to indicate terminal states of the CR and its backend AWS - service API resource + description: Condition is the common struct used by all CRDs managed + by ACK service controllers to indicate terminal states of the + CR and its backend AWS service API resource properties: lastTransitionTime: description: Last time the condition transitioned from one status diff --git a/generator.yaml b/generator.yaml index 5ba794e6..692d6335 100644 --- a/generator.yaml +++ b/generator.yaml @@ -22,11 +22,22 @@ operations: StopPipelineExecution: operation_type: Delete resource_name: PipelineExecution + AddTags: + operation_type: Update + resource_name: Model resources: Model: hooks: delta_pre_compare: code: customSetDefaults(a, b) + sdk_read_one_pre_set_output: + template_path: model/sdk_read_one_pre_set_output.go.tpl + sdk_read_one_post_set_output: + template_path: model/sdk_read_one_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: model/sdk_update_pre_build_request.go.tpl + sdk_update_post_build_request: + template_path: model/sdk_update_post_build_request.go.tpl exceptions: errors: 404: @@ -37,9 +48,9 @@ resources: - InvalidParameterValue - MissingParameter fields: - Tags: - compare: - is_ignored: true + # Tags: + # compare: + # is_ignored: true EnableNetworkIsolation: late_initialize: min_backoff_seconds: 5 diff --git a/helm/crds/services.k8s.aws_adoptedresources.yaml b/helm/crds/services.k8s.aws_adoptedresources.yaml index 65eff735..272119ee 100644 --- a/helm/crds/services.k8s.aws_adoptedresources.yaml +++ b/helm/crds/services.k8s.aws_adoptedresources.yaml @@ -183,12 +183,12 @@ spec: name: description: |- Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names + More info: http://kubernetes.io/docs/user-guide/identifiers#names type: string uid: description: |- UID of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids + More info: http://kubernetes.io/docs/user-guide/identifiers#uids type: string required: - apiVersion diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 3598fdc3..3ccf7bd1 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -132,6 +132,18 @@ spec: capabilities: drop: - ALL + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 securityContext: seccompProfile: type: RuntimeDefault diff --git a/pkg/resource/model/delta.go b/pkg/resource/model/delta.go index e9ad5243..12491f52 100644 --- a/pkg/resource/model/delta.go +++ b/pkg/resource/model/delta.go @@ -209,6 +209,9 @@ func newResourceDelta( } } } + if !ackcompare.MapStringStringEqual(ToACKTags(a.ko.Spec.Tags), ToACKTags(b.ko.Spec.Tags)) { + delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags) + } if ackcompare.HasNilDifference(a.ko.Spec.VPCConfig, b.ko.Spec.VPCConfig) { delta.Add("Spec.VPCConfig", a.ko.Spec.VPCConfig, b.ko.Spec.VPCConfig) } else if a.ko.Spec.VPCConfig != nil && b.ko.Spec.VPCConfig != nil { diff --git a/pkg/resource/model/hooks.go b/pkg/resource/model/hooks.go new file mode 100644 index 00000000..07e1639d --- /dev/null +++ b/pkg/resource/model/hooks.go @@ -0,0 +1,120 @@ +// 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 ( + "context" + + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" + svcapitypes "github.com/aws-controllers-k8s/sagemaker-controller/apis/v1alpha1" + svcsdk "github.com/aws/aws-sdk-go/service/sagemaker" +) + +// deleteTags is used to keep tags in sync by calling Create and Delete API's +func (rm *resourceManager) deleteTags( + ctx context.Context, + desired *resource, + latest *resource, +) (resp *svcsdk.DeleteTagsOutput, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.syncTags") + defer func(err error) { + exit(err) + }(err) + + resourceId := (*string)(latest.ko.Status.ACKResourceMetadata.ARN) + + toDelete := computeTagsDelta( + desired.ko.Spec.Tags, latest.ko.Spec.Tags, + ) + + if len(toDelete) > 0 { + rlog.Debug("removing tags from model resource", "tags", toDelete) + + keys := make([]*string, len(toDelete)) + for i, raw_key := range toDelete { + keys[i] = raw_key.Key + } + resp, err = rm.sdkapi.DeleteTagsWithContext( + ctx, + &svcsdk.DeleteTagsInput{ + ResourceArn: resourceId, + TagKeys: keys, + }, + ) + rm.metrics.RecordAPICall("UPDATE", "DeleteTags", err) + if err != nil { + return nil, err + } + + return resp, nil + + } + + return nil, nil +} + +// sdkTags converts *svcapitypes.Tag array to a *svcsdk.Tag array +func (rm *resourceManager) sdkTags( + tags []*svcapitypes.Tag, +) (sdktags []*svcsdk.Tag) { + + for _, i := range tags { + sdktag := rm.newTag(*i) + sdktags = append(sdktags, sdktag) + } + + return sdktags +} + +func (rm *resourceManager) newTag( + c svcapitypes.Tag, +) *svcsdk.Tag { + res := &svcsdk.Tag{} + if c.Key != nil { + res.SetKey(*c.Key) + } + if c.Value != nil { + res.SetValue(*c.Value) + } + + return res +} + +// computeTagsDelta returns tags to be added and removed from the resource +func computeTagsDelta( + desired []*svcapitypes.Tag, + latest []*svcapitypes.Tag, +) (toDelete []*svcapitypes.Tag) { + + desiredTags := map[string]string{} + for _, tag := range desired { + desiredTags[*tag.Key] = *tag.Value + } + + latestTags := map[string]string{} + for _, tag := range latest { + latestTags[*tag.Key] = *tag.Value + } + + for _, tag := range latest { + _, ok := desiredTags[*tag.Key] + if !ok { + toDelete = append(toDelete, tag) + } + } + + return toDelete + +} diff --git a/pkg/resource/model/sdk.go b/pkg/resource/model/sdk.go index 3aa1b281..cd88d69f 100644 --- a/pkg/resource/model/sdk.go +++ b/pkg/resource/model/sdk.go @@ -90,6 +90,13 @@ func (rm *resourceManager) sdkFind( // the original Kubernetes object we passed to the function ko := r.ko.DeepCopy() + // This is require if only spec.primarycontainer.modeldataurl is in use and not the ko.spec.primarycontainer.ModelDataSource + // in this case, during find "ko.spec.primarycontainer.ModelDataSource" gets updated as well , which creates a new k8s generation + + if ko.Spec.PrimaryContainer.ModelDataSource == nil && resp.PrimaryContainer.ModelDataSource != nil { + resp.PrimaryContainer.ModelDataSource = nil + } + if resp.Containers != nil { f0 := []*svcapitypes.ContainerDefinition{} for _, f0iter := range resp.Containers { @@ -307,6 +314,25 @@ func (rm *resourceManager) sdkFind( } rm.setStatusDefaults(ko) + var resp_tags *svcsdk.ListTagsOutput + + resp_tags, err = rm.sdkapi.ListTagsWithContext(ctx, &svcsdk.ListTagsInput{ResourceArn: resp.ModelArn}) + rm.metrics.RecordAPICall("READ_ONE", "DescribeTags", err) + + if resp_tags != nil { + f6 := []*svcapitypes.Tag{} + for _, f6iter := range resp_tags.Tags { + f6elem := &svcapitypes.Tag{} + if f6iter.Key != nil { + f6elem.Key = f6iter.Key + } + if f6iter.Value != nil { + f6elem.Value = f6iter.Value + } + f6 = append(f6, f6elem) + } + ko.Spec.Tags = f6 + } return &resource{ko}, nil } @@ -601,8 +627,92 @@ func (rm *resourceManager) sdkUpdate( desired *resource, latest *resource, delta *ackcompare.Delta, -) (*resource, error) { - return nil, ackerr.NewTerminalError(ackerr.NotImplemented) +) (updated *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.sdkUpdate") + defer func() { + exit(err) + }() + // checking if any delta is found other than tags + if len(delta.Differences) > 0 { + if delta.DifferentExcept("Spec.Tags") { + for _, parts := range delta.Differences { + if !parts.Path.Contains("Tags") { + return nil, fmt.Errorf("cannot update the following fields: %s , Allowed field to change: Spec.Tags", parts.Path) + } + } + } + } + // this to handle delete/remove tags + _, err = rm.deleteTags(ctx, desired, latest) + if err != nil { + return nil, err + } + input, err := rm.newUpdateRequestPayload(ctx, desired, delta) + if err != nil { + return nil, err + } + if desired.ko.Status.ACKResourceMetadata.ARN != nil { + input.SetResourceArn(string(*desired.ko.Status.ACKResourceMetadata.ARN)) + } + + var resp *svcsdk.AddTagsOutput + _ = resp + resp, err = rm.sdkapi.AddTagsWithContext(ctx, input) + rm.metrics.RecordAPICall("UPDATE", "AddTags", err) + if err != nil { + return nil, err + } + // Merge in the information we read from the API call above to the copy of + // the original Kubernetes object we passed to the function + ko := desired.ko.DeepCopy() + + if resp.Tags != nil { + f0 := []*svcapitypes.Tag{} + for _, f0iter := range resp.Tags { + f0elem := &svcapitypes.Tag{} + if f0iter.Key != nil { + f0elem.Key = f0iter.Key + } + if f0iter.Value != nil { + f0elem.Value = f0iter.Value + } + f0 = append(f0, f0elem) + } + ko.Spec.Tags = f0 + } else { + ko.Spec.Tags = nil + } + + rm.setStatusDefaults(ko) + return &resource{ko}, nil +} + +// newUpdateRequestPayload returns an SDK-specific struct for the HTTP request +// payload of the Update API call for the resource +func (rm *resourceManager) newUpdateRequestPayload( + ctx context.Context, + r *resource, + delta *ackcompare.Delta, +) (*svcsdk.AddTagsInput, error) { + res := &svcsdk.AddTagsInput{} + + if r.ko.Spec.Tags != nil { + f1 := []*svcsdk.Tag{} + for _, f1iter := range r.ko.Spec.Tags { + f1elem := &svcsdk.Tag{} + if f1iter.Key != nil { + f1elem.SetKey(*f1iter.Key) + } + if f1iter.Value != nil { + f1elem.SetValue(*f1iter.Value) + } + f1 = append(f1, f1elem) + } + res.SetTags(f1) + } + + return res, nil } // sdkDelete deletes the supplied resource in the backend AWS service API diff --git a/templates/model/sdk_read_one_post_set_output.go.tpl b/templates/model/sdk_read_one_post_set_output.go.tpl new file mode 100644 index 00000000..f5d117fb --- /dev/null +++ b/templates/model/sdk_read_one_post_set_output.go.tpl @@ -0,0 +1,19 @@ +var resp_tags *svcsdk.ListTagsOutput + +resp_tags, err = rm.sdkapi.ListTagsWithContext(ctx,&svcsdk.ListTagsInput{ResourceArn: resp.ModelArn}) +rm.metrics.RecordAPICall("READ_ONE", "DescribeTags", err) + +if resp_tags != nil { + f6 := []*svcapitypes.Tag{} + for _, f6iter := range resp_tags.Tags { + f6elem := &svcapitypes.Tag{} + if f6iter.Key != nil { + f6elem.Key = f6iter.Key + } + if f6iter.Value != nil { + f6elem.Value = f6iter.Value + } + f6 = append(f6, f6elem) + } + ko.Spec.Tags = f6 + } \ No newline at end of file diff --git a/templates/model/sdk_read_one_pre_set_output.go.tpl b/templates/model/sdk_read_one_pre_set_output.go.tpl new file mode 100644 index 00000000..9b8a4940 --- /dev/null +++ b/templates/model/sdk_read_one_pre_set_output.go.tpl @@ -0,0 +1,7 @@ + +// This is require if only spec.primarycontainer.modeldataurl is in use and not the ko.spec.primarycontainer.ModelDataSource +// in this case, during find "ko.spec.primarycontainer.ModelDataSource" gets updated as well , which creates a new k8s generation + +if ko.Spec.PrimaryContainer.ModelDataSource == nil && resp.PrimaryContainer.ModelDataSource != nil { + resp.PrimaryContainer.ModelDataSource = nil +} \ No newline at end of file diff --git a/templates/model/sdk_update_post_build_request.go.tpl b/templates/model/sdk_update_post_build_request.go.tpl new file mode 100644 index 00000000..1df1587a --- /dev/null +++ b/templates/model/sdk_update_post_build_request.go.tpl @@ -0,0 +1,3 @@ +if desired.ko.Status.ACKResourceMetadata.ARN != nil { + input.SetResourceArn(string(*desired.ko.Status.ACKResourceMetadata.ARN)) +} \ No newline at end of file diff --git a/templates/model/sdk_update_pre_build_request.go.tpl b/templates/model/sdk_update_pre_build_request.go.tpl new file mode 100644 index 00000000..2fcc810d --- /dev/null +++ b/templates/model/sdk_update_pre_build_request.go.tpl @@ -0,0 +1,15 @@ +// checking if any delta is found other than tags +if len(delta.Differences) > 0 { + if delta.DifferentExcept("Spec.Tags") { + for _, parts := range delta.Differences { + if !parts.Path.Contains("Tags") { + return nil, fmt.Errorf("cannot update the following fields: %s , Allowed field to change: Spec.Tags", parts.Path) + } + } + } +} +// this to handle delete/remove tags +_ , err = rm.deleteTags(ctx,desired,latest) +if err != nil { + return nil, err +} \ No newline at end of file diff --git a/test/e2e/tests/test_model.py b/test/e2e/tests/test_model.py index 02fda114..b4fc71bd 100644 --- a/test/e2e/tests/test_model.py +++ b/test/e2e/tests/test_model.py @@ -70,6 +70,27 @@ def test_create_model(self, xgboost_model): resource_tags = resource["spec"].get("tags", None) assert_tags_in_sync(model_arn, resource_tags) + # Add new tags + resource_tags.append({'key': 'newtagkey', 'value': 'newtagvalue'}) + + updates = { + "spec": {"tags": resource_tags}, + } + + k8s.patch_custom_resource(reference,updates) + time.sleep(cfg.TAG_DELAY_SLEEP) + assert_tags_in_sync(model_arn, resource_tags) + + + # Remove latest added tag + resource_tags = [i for i in resource_tags if not i["key"] == "newtagkey"] + updates = { + "spec": {"tags": resource_tags}, + } + k8s.patch_custom_resource(reference,updates) + time.sleep(cfg.TAG_DELAY_SLEEP) + assert_tags_in_sync(model_arn, resource_tags) + # Delete the k8s resource. assert delete_custom_resource( reference, cfg.DELETE_WAIT_PERIOD, cfg.DELETE_WAIT_LENGTH