From f45df148c02858aba78868202bc247458e48192e Mon Sep 17 00:00:00 2001 From: ack-bot <82905295+ack-bot@users.noreply.github.com> Date: Fri, 3 Oct 2025 19:08:24 +0000 Subject: [PATCH] Add JobTemplate to emrcontainers --- apis/v1alpha1/ack-generate-metadata.yaml | 12 +- apis/v1alpha1/generator.yaml | 67 ++- apis/v1alpha1/job_template.go | 95 ++++ apis/v1alpha1/types.go | 39 +- apis/v1alpha1/zz_generated.deepcopy.go | 244 ++++++++- cmd/controller/main.go | 1 + config/controller/kustomization.yaml | 2 +- ...mrcontainers.services.k8s.aws_jobruns.yaml | 2 +- ...tainers.services.k8s.aws_jobtemplates.yaml | 202 +++++++ ...ners.services.k8s.aws_virtualclusters.yaml | 2 +- config/crd/kustomization.yaml | 1 + config/rbac/cluster-role-controller.yaml | 2 + config/rbac/role-reader.yaml | 1 + config/rbac/role-writer.yaml | 2 + generator.yaml | 67 ++- go.mod | 1 + go.sum | 3 + helm/Chart.yaml | 4 +- ...mrcontainers.services.k8s.aws_jobruns.yaml | 2 +- ...tainers.services.k8s.aws_jobtemplates.yaml | 202 +++++++ ...ners.services.k8s.aws_virtualclusters.yaml | 2 +- .../services.k8s.aws_adoptedresources.yaml | 2 +- helm/crds/services.k8s.aws_fieldexports.yaml | 2 +- helm/templates/NOTES.txt | 2 +- helm/templates/_helpers.tpl | 2 + helm/templates/role-reader.yaml | 1 + helm/templates/role-writer.yaml | 2 + helm/values.yaml | 3 +- pkg/resource/job_template/delta.go | 135 +++++ pkg/resource/job_template/descriptor.go | 155 ++++++ pkg/resource/job_template/hooks.go | 66 +++ pkg/resource/job_template/identifiers.go | 55 ++ pkg/resource/job_template/manager.go | 404 ++++++++++++++ pkg/resource/job_template/manager_factory.go | 100 ++++ pkg/resource/job_template/references.go | 57 ++ pkg/resource/job_template/resource.go | 113 ++++ pkg/resource/job_template/sdk.go | 498 ++++++++++++++++++ pkg/resource/job_template/tags.go | 108 ++++ pkg/tags/sync.go | 121 +++++ pkg/tags/sync_test.go | 151 ++++++ .../sdk_create_post_set_output.go.tpl | 5 + .../sdk_read_one_post_set_output.go.tpl | 4 + .../sdk_update_pre_build_request.go.tpl | 15 + 43 files changed, 2902 insertions(+), 52 deletions(-) create mode 100644 apis/v1alpha1/job_template.go create mode 100644 config/crd/bases/emrcontainers.services.k8s.aws_jobtemplates.yaml create mode 100644 helm/crds/emrcontainers.services.k8s.aws_jobtemplates.yaml create mode 100644 pkg/resource/job_template/delta.go create mode 100644 pkg/resource/job_template/descriptor.go create mode 100644 pkg/resource/job_template/hooks.go create mode 100644 pkg/resource/job_template/identifiers.go create mode 100644 pkg/resource/job_template/manager.go create mode 100644 pkg/resource/job_template/manager_factory.go create mode 100644 pkg/resource/job_template/references.go create mode 100644 pkg/resource/job_template/resource.go create mode 100644 pkg/resource/job_template/sdk.go create mode 100644 pkg/resource/job_template/tags.go create mode 100644 pkg/tags/sync.go create mode 100644 pkg/tags/sync_test.go create mode 100644 templates/hooks/job_template/sdk_create_post_set_output.go.tpl create mode 100644 templates/hooks/job_template/sdk_read_one_post_set_output.go.tpl create mode 100644 templates/hooks/job_template/sdk_update_pre_build_request.go.tpl diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index df7ea1a..c479eed 100644 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,13 +1,13 @@ ack_generate_info: - build_date: "2025-09-19T17:30:39Z" - build_hash: 6b4211163dcc34776b01da9a18217bac0f4103fd - go_version: go1.24.6 - version: v0.52.0 -api_directory_checksum: b3aac8d5f9f5b91ae438c9ca3fe4e905658bde01 + build_date: "2025-10-03T19:06:42Z" + build_hash: 37562000612658e62686882f1b4b924049d1e38c + go_version: go1.24.5 + version: v0.52.0-5-g3756200 +api_directory_checksum: 0975eb09cef44c3cf5f21df36b22c53f78cbfaff api_version: v1alpha1 aws_sdk_go_version: v1.32.6 generator_config_info: - file_checksum: 9949dc8a85d6a7a97564896c261a787854640a16 + file_checksum: 3df05ab1615817faedbbe89eeec7130cea903a7e original_file_name: generator.yaml last_modification: reason: API generation diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index 5b8ffa1..4485c2f 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -9,12 +9,14 @@ ignore: - StartJobRunInput.RetryPolicyConfiguration - StartJobRunInput.JobTemplateParameters - CreateVirtualClusterInput.SecurityConfigurationId + - Configuration.Configurations + - JobTemplateData.ConfigurationOverrides.ApplicationConfiguration.Configurations operations: null resource_names: # - VirtualCluster # - JobRun - ManagedEndpoint - - JobTemplate + # - JobTemplate # Removed from ignore list - SecurityConfiguration sdk_names: model_name: emr-containers @@ -29,6 +31,8 @@ operations: resource_name: JobRun DescribeJobRun: output_wrapper_field_path: JobRun + GetJobTemplate: + output_wrapper_field_path: "" prefix_config: {} resources: VirtualCluster: @@ -90,3 +94,64 @@ resources: exceptions: terminal_codes: - ValidationException + JobTemplate: + fields: + # Primary identifiers + ID: + is_primary_key: true + Name: + is_required: true + ARN: + is_read_only: true + # Computed fields + CreatedAt: + is_read_only: true + CreatedBy: + is_read_only: true + # Immutable fields + ID: + is_immutable: true + CreatedAt: + is_immutable: true + CreatedBy: + is_immutable: true + # Handle ConfigurationOverrides similar to JobRun + JobTemplateData.ConfigurationOverrides: + type: "string" + compare: + is_ignored: true + exceptions: + errors: + 404: + code: ResourceNotFoundException + terminal_codes: + - ValidationException + - ResourceNotFoundException + hooks: + sdk_create_post_set_output: + template_path: hooks/job_template/sdk_create_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: hooks/job_template/sdk_update_pre_build_request.go.tpl + sdk_read_one_post_set_output: + template_path: hooks/job_template/sdk_read_one_post_set_output.go.tpl + renames: + operations: + CreateJobTemplate: + output_fields: + id: ID + GetJobTemplate: + input_fields: + id: ID + output_fields: + id: ID + arn: ARN + DeleteJobTemplate: + input_fields: + id: ID + output_fields: + id: ID + UpdateJobTemplate: + input_fields: + id: ID + output_fields: + id: ID \ No newline at end of file diff --git a/apis/v1alpha1/job_template.go b/apis/v1alpha1/job_template.go new file mode 100644 index 0000000..edb1a2b --- /dev/null +++ b/apis/v1alpha1/job_template.go @@ -0,0 +1,95 @@ +// 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. + +// Code generated by ack-generate. DO NOT EDIT. + +package v1alpha1 + +import ( + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// JobTemplateSpec defines the desired state of JobTemplate. +// +// This entity describes a job template. Job template stores values of StartJobRun +// API request in a template and can be used to start a job run. Job template +// allows two use cases: avoid repeating recurring StartJobRun API request values, +// enforcing certain values in StartJobRun API request. +type JobTemplateSpec struct { + + // The client token of the job template. + // + // Regex Pattern: `\S` + // +kubebuilder:validation:Required + ClientToken *string `json:"clientToken"` + // The job template data which holds values of StartJobRun API request. + // +kubebuilder:validation:Required + JobTemplateData *JobTemplateData `json:"jobTemplateData"` + // The KMS key ARN used to encrypt the job template. + // + // Regex Pattern: `^(arn:(aws[a-zA-Z0-9-]*):kms:.+:(\d{12})?:key\/[(0-9a-zA-Z)-?]+|\$\{[a-zA-Z]\w*\})$` + KMSKeyARN *string `json:"kmsKeyARN,omitempty"` + // The specified name of the job template. + // + // Regex Pattern: `^[\.\-_/#A-Za-z0-9]+$` + // +kubebuilder:validation:Required + Name *string `json:"name"` + // The tags that are associated with the job template. + Tags map[string]*string `json:"tags,omitempty"` +} + +// JobTemplateStatus defines the observed state of JobTemplate +type JobTemplateStatus struct { + // All CRs managed by ACK have a common `Status.ACKResourceMetadata` member + // that is used to contain resource sync state, account ownership, + // constructed ARN for the resource + // +kubebuilder:validation:Optional + ACKResourceMetadata *ackv1alpha1.ResourceMetadata `json:"ackResourceMetadata"` + // All CRs managed by ACK have a common `Status.Conditions` member that + // contains a collection of `ackv1alpha1.Condition` objects that describe + // the various terminal states of the CR and its backend AWS service API + // resource + // +kubebuilder:validation:Optional + Conditions []*ackv1alpha1.Condition `json:"conditions"` + // This output displays the date and time when the job template was created. + // +kubebuilder:validation:Optional + CreatedAt *metav1.Time `json:"createdAt,omitempty"` + // This output display the created job template ID. + // + // Regex Pattern: `^[0-9a-z]+$` + // +kubebuilder:validation:Optional + ID *string `json:"id,omitempty"` +} + +// JobTemplate is the Schema for the JobTemplates API +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type JobTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec JobTemplateSpec `json:"spec,omitempty"` + Status JobTemplateStatus `json:"status,omitempty"` +} + +// JobTemplateList contains a list of JobTemplate +// +kubebuilder:object:root=true +type JobTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []JobTemplate `json:"items"` +} + +func init() { + SchemeBuilder.Register(&JobTemplate{}, &JobTemplateList{}) +} diff --git a/apis/v1alpha1/types.go b/apis/v1alpha1/types.go index a5c16b8..1cfb021 100644 --- a/apis/v1alpha1/types.go +++ b/apis/v1alpha1/types.go @@ -111,25 +111,35 @@ type JobRun_SDK struct { VirtualClusterID *string `json:"virtualClusterID,omitempty"` } -// This entity describes a job template. Job template stores values of StartJobRun -// API request in a template and can be used to start a job run. Job template -// allows two use cases: avoid repeating recurring StartJobRun API request values, -// enforcing certain values in StartJobRun API request. -type JobTemplate struct { - CreatedAt *metav1.Time `json:"createdAt,omitempty"` - CreatedBy *string `json:"createdBy,omitempty"` - ID *string `json:"id,omitempty"` - Name *string `json:"name,omitempty"` - Tags map[string]*string `json:"tags,omitempty"` -} - // The values of StartJobRun API requests used in job runs started using the // job template. type JobTemplateData struct { + ConfigurationOverrides *string `json:"configurationOverrides,omitempty"` + ExecutionRoleARN *string `json:"executionRoleARN,omitempty"` // Specify the driver that the job runs on. Exactly one of the two available // job drivers is required, either sparkSqlJobDriver or sparkSubmitJobDriver. - JobDriver *JobDriver `json:"jobDriver,omitempty"` - JobTags map[string]*string `json:"jobTags,omitempty"` + JobDriver *JobDriver `json:"jobDriver,omitempty"` + JobTags map[string]*string `json:"jobTags,omitempty"` + ParameterConfiguration map[string]*TemplateParameterConfiguration `json:"parameterConfiguration,omitempty"` + ReleaseLabel *string `json:"releaseLabel,omitempty"` +} + +// This entity describes a job template. Job template stores values of StartJobRun +// API request in a template and can be used to start a job run. Job template +// allows two use cases: avoid repeating recurring StartJobRun API request values, +// enforcing certain values in StartJobRun API request. +type JobTemplate_SDK struct { + ARN *string `json:"arn,omitempty"` + CreatedAt *metav1.Time `json:"createdAt,omitempty"` + CreatedBy *string `json:"createdBy,omitempty"` + DECRyptionError *string `json:"dECRyptionError,omitempty"` + ID *string `json:"id,omitempty"` + // The values of StartJobRun API requests used in job runs started using the + // job template. + JobTemplateData *JobTemplateData `json:"jobTemplateData,omitempty"` + KMSKeyARN *string `json:"kmsKeyARN,omitempty"` + Name *string `json:"name,omitempty"` + Tags map[string]*string `json:"tags,omitempty"` } // Lake Formation related configuration inputs for the security configuration. @@ -188,6 +198,7 @@ type SparkSubmitJobDriver struct { // The configuration of a job template parameter. type TemplateParameterConfiguration struct { DefaultValue *string `json:"defaultValue,omitempty"` + Type *string `json:"type_,omitempty"` } // This entity describes a virtual cluster. A virtual cluster is a Kubernetes diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 2d32c67..00bb5e9 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -507,17 +507,144 @@ func (in *JobRun_SDK) DeepCopy() *JobRun_SDK { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JobTemplate) DeepCopyInto(out *JobTemplate) { *out = *in - if in.CreatedAt != nil { - in, out := &in.CreatedAt, &out.CreatedAt - *out = (*in).DeepCopy() + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobTemplate. +func (in *JobTemplate) DeepCopy() *JobTemplate { + if in == nil { + return nil } - if in.CreatedBy != nil { - in, out := &in.CreatedBy, &out.CreatedBy + out := new(JobTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *JobTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JobTemplateData) DeepCopyInto(out *JobTemplateData) { + *out = *in + if in.ConfigurationOverrides != nil { + in, out := &in.ConfigurationOverrides, &out.ConfigurationOverrides *out = new(string) **out = **in } - if in.ID != nil { - in, out := &in.ID, &out.ID + if in.ExecutionRoleARN != nil { + in, out := &in.ExecutionRoleARN, &out.ExecutionRoleARN + *out = new(string) + **out = **in + } + if in.JobDriver != nil { + in, out := &in.JobDriver, &out.JobDriver + *out = new(JobDriver) + (*in).DeepCopyInto(*out) + } + if in.JobTags != nil { + in, out := &in.JobTags, &out.JobTags + *out = make(map[string]*string, len(*in)) + for key, val := range *in { + var outVal *string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = new(string) + **out = **in + } + (*out)[key] = outVal + } + } + if in.ParameterConfiguration != nil { + in, out := &in.ParameterConfiguration, &out.ParameterConfiguration + *out = make(map[string]*TemplateParameterConfiguration, len(*in)) + for key, val := range *in { + var outVal *TemplateParameterConfiguration + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = new(TemplateParameterConfiguration) + (*in).DeepCopyInto(*out) + } + (*out)[key] = outVal + } + } + if in.ReleaseLabel != nil { + in, out := &in.ReleaseLabel, &out.ReleaseLabel + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobTemplateData. +func (in *JobTemplateData) DeepCopy() *JobTemplateData { + if in == nil { + return nil + } + out := new(JobTemplateData) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JobTemplateList) DeepCopyInto(out *JobTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]JobTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobTemplateList. +func (in *JobTemplateList) DeepCopy() *JobTemplateList { + if in == nil { + return nil + } + out := new(JobTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *JobTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JobTemplateSpec) DeepCopyInto(out *JobTemplateSpec) { + *out = *in + if in.ClientToken != nil { + in, out := &in.ClientToken, &out.ClientToken + *out = new(string) + **out = **in + } + if in.JobTemplateData != nil { + in, out := &in.JobTemplateData, &out.JobTemplateData + *out = new(JobTemplateData) + (*in).DeepCopyInto(*out) + } + if in.KMSKeyARN != nil { + in, out := &in.KMSKeyARN, &out.KMSKeyARN *out = new(string) **out = **in } @@ -544,26 +671,100 @@ func (in *JobTemplate) DeepCopyInto(out *JobTemplate) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobTemplate. -func (in *JobTemplate) DeepCopy() *JobTemplate { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobTemplateSpec. +func (in *JobTemplateSpec) DeepCopy() *JobTemplateSpec { if in == nil { return nil } - out := new(JobTemplate) + out := new(JobTemplateSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *JobTemplateData) DeepCopyInto(out *JobTemplateData) { +func (in *JobTemplateStatus) DeepCopyInto(out *JobTemplateStatus) { *out = *in - if in.JobDriver != nil { - in, out := &in.JobDriver, &out.JobDriver - *out = new(JobDriver) + if in.ACKResourceMetadata != nil { + in, out := &in.ACKResourceMetadata, &out.ACKResourceMetadata + *out = new(corev1alpha1.ResourceMetadata) (*in).DeepCopyInto(*out) } - if in.JobTags != nil { - in, out := &in.JobTags, &out.JobTags + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]*corev1alpha1.Condition, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(corev1alpha1.Condition) + (*in).DeepCopyInto(*out) + } + } + } + if in.CreatedAt != nil { + in, out := &in.CreatedAt, &out.CreatedAt + *out = (*in).DeepCopy() + } + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobTemplateStatus. +func (in *JobTemplateStatus) DeepCopy() *JobTemplateStatus { + if in == nil { + return nil + } + out := new(JobTemplateStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JobTemplate_SDK) DeepCopyInto(out *JobTemplate_SDK) { + *out = *in + if in.ARN != nil { + in, out := &in.ARN, &out.ARN + *out = new(string) + **out = **in + } + if in.CreatedAt != nil { + in, out := &in.CreatedAt, &out.CreatedAt + *out = (*in).DeepCopy() + } + if in.CreatedBy != nil { + in, out := &in.CreatedBy, &out.CreatedBy + *out = new(string) + **out = **in + } + if in.DECRyptionError != nil { + in, out := &in.DECRyptionError, &out.DECRyptionError + *out = new(string) + **out = **in + } + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } + if in.JobTemplateData != nil { + in, out := &in.JobTemplateData, &out.JobTemplateData + *out = new(JobTemplateData) + (*in).DeepCopyInto(*out) + } + if in.KMSKeyARN != nil { + in, out := &in.KMSKeyARN, &out.KMSKeyARN + *out = new(string) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags *out = make(map[string]*string, len(*in)) for key, val := range *in { var outVal *string @@ -580,12 +781,12 @@ func (in *JobTemplateData) DeepCopyInto(out *JobTemplateData) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobTemplateData. -func (in *JobTemplateData) DeepCopy() *JobTemplateData { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobTemplate_SDK. +func (in *JobTemplate_SDK) DeepCopy() *JobTemplate_SDK { if in == nil { return nil } - out := new(JobTemplateData) + out := new(JobTemplate_SDK) in.DeepCopyInto(out) return out } @@ -809,6 +1010,11 @@ func (in *TemplateParameterConfiguration) DeepCopyInto(out *TemplateParameterCon *out = new(string) **out = **in } + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateParameterConfiguration. diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 8dc6cad..24732bb 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -40,6 +40,7 @@ import ( svcresource "github.com/aws-controllers-k8s/emrcontainers-controller/pkg/resource" _ "github.com/aws-controllers-k8s/emrcontainers-controller/pkg/resource/job_run" + _ "github.com/aws-controllers-k8s/emrcontainers-controller/pkg/resource/job_template" _ "github.com/aws-controllers-k8s/emrcontainers-controller/pkg/resource/virtual_cluster" "github.com/aws-controllers-k8s/emrcontainers-controller/pkg/version" diff --git a/config/controller/kustomization.yaml b/config/controller/kustomization.yaml index f14ce13..d980950 100644 --- a/config/controller/kustomization.yaml +++ b/config/controller/kustomization.yaml @@ -6,4 +6,4 @@ kind: Kustomization images: - name: controller newName: public.ecr.aws/aws-controllers-k8s/emrcontainers-controller - newTag: 1.1.1 + newTag: 0.0.0-non-release-version diff --git a/config/crd/bases/emrcontainers.services.k8s.aws_jobruns.yaml b/config/crd/bases/emrcontainers.services.k8s.aws_jobruns.yaml index d712486..32c306a 100644 --- a/config/crd/bases/emrcontainers.services.k8s.aws_jobruns.yaml +++ b/config/crd/bases/emrcontainers.services.k8s.aws_jobruns.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.2 + controller-gen.kubebuilder.io/version: v0.19.0 name: jobruns.emrcontainers.services.k8s.aws spec: group: emrcontainers.services.k8s.aws diff --git a/config/crd/bases/emrcontainers.services.k8s.aws_jobtemplates.yaml b/config/crd/bases/emrcontainers.services.k8s.aws_jobtemplates.yaml new file mode 100644 index 0000000..fcf65e3 --- /dev/null +++ b/config/crd/bases/emrcontainers.services.k8s.aws_jobtemplates.yaml @@ -0,0 +1,202 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: jobtemplates.emrcontainers.services.k8s.aws +spec: + group: emrcontainers.services.k8s.aws + names: + kind: JobTemplate + listKind: JobTemplateList + plural: jobtemplates + singular: jobtemplate + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: JobTemplate is the Schema for the JobTemplates 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 + 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 + type: string + metadata: + type: object + spec: + description: |- + JobTemplateSpec defines the desired state of JobTemplate. + + This entity describes a job template. Job template stores values of StartJobRun + API request in a template and can be used to start a job run. Job template + allows two use cases: avoid repeating recurring StartJobRun API request values, + enforcing certain values in StartJobRun API request. + properties: + clientToken: + description: |- + The client token of the job template. + + Regex Pattern: `\S` + type: string + jobTemplateData: + description: The job template data which holds values of StartJobRun + API request. + properties: + configurationOverrides: + type: string + executionRoleARN: + type: string + jobDriver: + description: |- + Specify the driver that the job runs on. Exactly one of the two available + job drivers is required, either sparkSqlJobDriver or sparkSubmitJobDriver. + properties: + sparkSubmitJobDriver: + description: The information about job driver for Spark submit. + properties: + entryPoint: + type: string + entryPointArguments: + items: + type: string + type: array + sparkSubmitParameters: + type: string + type: object + type: object + jobTags: + additionalProperties: + type: string + type: object + parameterConfiguration: + additionalProperties: + description: The configuration of a job template parameter. + properties: + defaultValue: + type: string + type_: + type: string + type: object + type: object + releaseLabel: + type: string + type: object + kmsKeyARN: + description: |- + The KMS key ARN used to encrypt the job template. + + Regex Pattern: `^(arn:(aws[a-zA-Z0-9-]*):kms:.+:(\d{12})?:key\/[(0-9a-zA-Z)-?]+|\$\{[a-zA-Z]\w*\})$` + type: string + name: + description: |- + The specified name of the job template. + + Regex Pattern: `^[\.\-_/#A-Za-z0-9]+$` + type: string + tags: + additionalProperties: + type: string + description: The tags that are associated with the job template. + type: object + required: + - clientToken + - jobTemplateData + - name + type: object + status: + description: JobTemplateStatus defines the observed state of JobTemplate + properties: + ackResourceMetadata: + description: |- + All CRs managed by ACK have a common `Status.ACKResourceMetadata` member + that is used to contain resource sync state, account ownership, + constructed ARN for the resource + properties: + arn: + description: |- + ARN is the Amazon Resource Name for the resource. This is a + globally-unique identifier and is set only by the ACK service controller + once the controller has orchestrated the creation of the resource OR + when it has verified that an "adopted" resource (a resource where the + ARN annotation was set by the Kubernetes user on the CR) exists and + matches the supplied CR's Spec field values. + https://github.com/aws/aws-controllers-k8s/issues/270 + type: string + ownerAccountID: + description: |- + OwnerAccountID is the AWS Account ID of the account that owns the + backend AWS service API resource. + type: string + region: + description: Region is the AWS region in which the resource exists + or will exist. + type: string + required: + - ownerAccountID + - region + type: object + conditions: + description: |- + All CRs managed by ACK have a common `Status.Conditions` member that + contains a collection of `ackv1alpha1.Condition` objects that describe + the various terminal states of the CR and its backend AWS service API + 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 + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type is the type of the Condition + type: string + required: + - status + - type + type: object + type: array + createdAt: + description: This output displays the date and time when the job template + was created. + format: date-time + type: string + id: + description: |- + This output display the created job template ID. + + Regex Pattern: `^[0-9a-z]+$` + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/emrcontainers.services.k8s.aws_virtualclusters.yaml b/config/crd/bases/emrcontainers.services.k8s.aws_virtualclusters.yaml index 24b6ef5..c64e68b 100644 --- a/config/crd/bases/emrcontainers.services.k8s.aws_virtualclusters.yaml +++ b/config/crd/bases/emrcontainers.services.k8s.aws_virtualclusters.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.2 + controller-gen.kubebuilder.io/version: v0.19.0 name: virtualclusters.emrcontainers.services.k8s.aws spec: group: emrcontainers.services.k8s.aws diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 91c7363..0d1df9e 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,4 +3,5 @@ kind: Kustomization resources: - common - bases/emrcontainers.services.k8s.aws_jobruns.yaml + - bases/emrcontainers.services.k8s.aws_jobtemplates.yaml - bases/emrcontainers.services.k8s.aws_virtualclusters.yaml diff --git a/config/rbac/cluster-role-controller.yaml b/config/rbac/cluster-role-controller.yaml index 92211ac..9ac33e9 100644 --- a/config/rbac/cluster-role-controller.yaml +++ b/config/rbac/cluster-role-controller.yaml @@ -26,6 +26,7 @@ rules: - emrcontainers.services.k8s.aws resources: - jobruns + - jobtemplates - virtualclusters verbs: - create @@ -39,6 +40,7 @@ rules: - emrcontainers.services.k8s.aws resources: - jobruns/status + - jobtemplates/status - virtualclusters/status verbs: - get diff --git a/config/rbac/role-reader.yaml b/config/rbac/role-reader.yaml index 861715c..e087d57 100644 --- a/config/rbac/role-reader.yaml +++ b/config/rbac/role-reader.yaml @@ -10,6 +10,7 @@ rules: - emrcontainers.services.k8s.aws resources: - jobruns + - jobtemplates - virtualclusters verbs: - get diff --git a/config/rbac/role-writer.yaml b/config/rbac/role-writer.yaml index 4acaa65..d01332e 100644 --- a/config/rbac/role-writer.yaml +++ b/config/rbac/role-writer.yaml @@ -10,6 +10,7 @@ rules: - emrcontainers.services.k8s.aws resources: - jobruns + - jobtemplates - virtualclusters verbs: - create @@ -23,6 +24,7 @@ rules: - emrcontainers.services.k8s.aws resources: - jobruns + - jobtemplates - virtualclusters verbs: - get diff --git a/generator.yaml b/generator.yaml index 5b8ffa1..4485c2f 100644 --- a/generator.yaml +++ b/generator.yaml @@ -9,12 +9,14 @@ ignore: - StartJobRunInput.RetryPolicyConfiguration - StartJobRunInput.JobTemplateParameters - CreateVirtualClusterInput.SecurityConfigurationId + - Configuration.Configurations + - JobTemplateData.ConfigurationOverrides.ApplicationConfiguration.Configurations operations: null resource_names: # - VirtualCluster # - JobRun - ManagedEndpoint - - JobTemplate + # - JobTemplate # Removed from ignore list - SecurityConfiguration sdk_names: model_name: emr-containers @@ -29,6 +31,8 @@ operations: resource_name: JobRun DescribeJobRun: output_wrapper_field_path: JobRun + GetJobTemplate: + output_wrapper_field_path: "" prefix_config: {} resources: VirtualCluster: @@ -90,3 +94,64 @@ resources: exceptions: terminal_codes: - ValidationException + JobTemplate: + fields: + # Primary identifiers + ID: + is_primary_key: true + Name: + is_required: true + ARN: + is_read_only: true + # Computed fields + CreatedAt: + is_read_only: true + CreatedBy: + is_read_only: true + # Immutable fields + ID: + is_immutable: true + CreatedAt: + is_immutable: true + CreatedBy: + is_immutable: true + # Handle ConfigurationOverrides similar to JobRun + JobTemplateData.ConfigurationOverrides: + type: "string" + compare: + is_ignored: true + exceptions: + errors: + 404: + code: ResourceNotFoundException + terminal_codes: + - ValidationException + - ResourceNotFoundException + hooks: + sdk_create_post_set_output: + template_path: hooks/job_template/sdk_create_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: hooks/job_template/sdk_update_pre_build_request.go.tpl + sdk_read_one_post_set_output: + template_path: hooks/job_template/sdk_read_one_post_set_output.go.tpl + renames: + operations: + CreateJobTemplate: + output_fields: + id: ID + GetJobTemplate: + input_fields: + id: ID + output_fields: + id: ID + arn: ARN + DeleteJobTemplate: + input_fields: + id: ID + output_fields: + id: ID + UpdateJobTemplate: + input_fields: + id: ID + output_fields: + id: ID \ No newline at end of file diff --git a/go.mod b/go.mod index 27aa0d4..30d0e5b 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/itchyny/gojq v0.12.6 // indirect github.com/itchyny/timefmt-go v0.1.3 // indirect github.com/jaypipes/envutil v1.0.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect diff --git a/go.sum b/go.sum index bbab68a..41f7392 100644 --- a/go.sum +++ b/go.sum @@ -94,6 +94,8 @@ github.com/jaypipes/envutil v1.0.0 h1:u6Vwy9HwruFihoZrL0bxDLCa/YNadGVwKyPElNmZWo github.com/jaypipes/envutil v1.0.0/go.mod h1:vgIRDly+xgBq0eeZRcflOHMMobMwgC6MkMbxo/Nw65M= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -218,6 +220,7 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helm/Chart.yaml b/helm/Chart.yaml index e7caf10..fabfb22 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 name: emrcontainers-chart description: A Helm chart for the ACK service controller for Amazon EMR on EKS (EMRContainers) -version: 1.1.1 -appVersion: 1.1.1 +version: 0.0.0-non-release-version +appVersion: 0.0.0-non-release-version home: https://github.com/aws-controllers-k8s/emrcontainers-controller icon: https://raw.githubusercontent.com/aws/eks-charts/master/docs/logo/aws.png sources: diff --git a/helm/crds/emrcontainers.services.k8s.aws_jobruns.yaml b/helm/crds/emrcontainers.services.k8s.aws_jobruns.yaml index d712486..32c306a 100644 --- a/helm/crds/emrcontainers.services.k8s.aws_jobruns.yaml +++ b/helm/crds/emrcontainers.services.k8s.aws_jobruns.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.2 + controller-gen.kubebuilder.io/version: v0.19.0 name: jobruns.emrcontainers.services.k8s.aws spec: group: emrcontainers.services.k8s.aws diff --git a/helm/crds/emrcontainers.services.k8s.aws_jobtemplates.yaml b/helm/crds/emrcontainers.services.k8s.aws_jobtemplates.yaml new file mode 100644 index 0000000..fcf65e3 --- /dev/null +++ b/helm/crds/emrcontainers.services.k8s.aws_jobtemplates.yaml @@ -0,0 +1,202 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: jobtemplates.emrcontainers.services.k8s.aws +spec: + group: emrcontainers.services.k8s.aws + names: + kind: JobTemplate + listKind: JobTemplateList + plural: jobtemplates + singular: jobtemplate + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: JobTemplate is the Schema for the JobTemplates 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 + 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 + type: string + metadata: + type: object + spec: + description: |- + JobTemplateSpec defines the desired state of JobTemplate. + + This entity describes a job template. Job template stores values of StartJobRun + API request in a template and can be used to start a job run. Job template + allows two use cases: avoid repeating recurring StartJobRun API request values, + enforcing certain values in StartJobRun API request. + properties: + clientToken: + description: |- + The client token of the job template. + + Regex Pattern: `\S` + type: string + jobTemplateData: + description: The job template data which holds values of StartJobRun + API request. + properties: + configurationOverrides: + type: string + executionRoleARN: + type: string + jobDriver: + description: |- + Specify the driver that the job runs on. Exactly one of the two available + job drivers is required, either sparkSqlJobDriver or sparkSubmitJobDriver. + properties: + sparkSubmitJobDriver: + description: The information about job driver for Spark submit. + properties: + entryPoint: + type: string + entryPointArguments: + items: + type: string + type: array + sparkSubmitParameters: + type: string + type: object + type: object + jobTags: + additionalProperties: + type: string + type: object + parameterConfiguration: + additionalProperties: + description: The configuration of a job template parameter. + properties: + defaultValue: + type: string + type_: + type: string + type: object + type: object + releaseLabel: + type: string + type: object + kmsKeyARN: + description: |- + The KMS key ARN used to encrypt the job template. + + Regex Pattern: `^(arn:(aws[a-zA-Z0-9-]*):kms:.+:(\d{12})?:key\/[(0-9a-zA-Z)-?]+|\$\{[a-zA-Z]\w*\})$` + type: string + name: + description: |- + The specified name of the job template. + + Regex Pattern: `^[\.\-_/#A-Za-z0-9]+$` + type: string + tags: + additionalProperties: + type: string + description: The tags that are associated with the job template. + type: object + required: + - clientToken + - jobTemplateData + - name + type: object + status: + description: JobTemplateStatus defines the observed state of JobTemplate + properties: + ackResourceMetadata: + description: |- + All CRs managed by ACK have a common `Status.ACKResourceMetadata` member + that is used to contain resource sync state, account ownership, + constructed ARN for the resource + properties: + arn: + description: |- + ARN is the Amazon Resource Name for the resource. This is a + globally-unique identifier and is set only by the ACK service controller + once the controller has orchestrated the creation of the resource OR + when it has verified that an "adopted" resource (a resource where the + ARN annotation was set by the Kubernetes user on the CR) exists and + matches the supplied CR's Spec field values. + https://github.com/aws/aws-controllers-k8s/issues/270 + type: string + ownerAccountID: + description: |- + OwnerAccountID is the AWS Account ID of the account that owns the + backend AWS service API resource. + type: string + region: + description: Region is the AWS region in which the resource exists + or will exist. + type: string + required: + - ownerAccountID + - region + type: object + conditions: + description: |- + All CRs managed by ACK have a common `Status.Conditions` member that + contains a collection of `ackv1alpha1.Condition` objects that describe + the various terminal states of the CR and its backend AWS service API + 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 + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type is the type of the Condition + type: string + required: + - status + - type + type: object + type: array + createdAt: + description: This output displays the date and time when the job template + was created. + format: date-time + type: string + id: + description: |- + This output display the created job template ID. + + Regex Pattern: `^[0-9a-z]+$` + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/crds/emrcontainers.services.k8s.aws_virtualclusters.yaml b/helm/crds/emrcontainers.services.k8s.aws_virtualclusters.yaml index 24b6ef5..c64e68b 100644 --- a/helm/crds/emrcontainers.services.k8s.aws_virtualclusters.yaml +++ b/helm/crds/emrcontainers.services.k8s.aws_virtualclusters.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.2 + controller-gen.kubebuilder.io/version: v0.19.0 name: virtualclusters.emrcontainers.services.k8s.aws spec: group: emrcontainers.services.k8s.aws diff --git a/helm/crds/services.k8s.aws_adoptedresources.yaml b/helm/crds/services.k8s.aws_adoptedresources.yaml index b7be322..d6cdd10 100644 --- a/helm/crds/services.k8s.aws_adoptedresources.yaml +++ b/helm/crds/services.k8s.aws_adoptedresources.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.2 + controller-gen.kubebuilder.io/version: v0.19.0 name: adoptedresources.services.k8s.aws spec: group: services.k8s.aws diff --git a/helm/crds/services.k8s.aws_fieldexports.yaml b/helm/crds/services.k8s.aws_fieldexports.yaml index 49b4f38..6e2c61e 100644 --- a/helm/crds/services.k8s.aws_fieldexports.yaml +++ b/helm/crds/services.k8s.aws_fieldexports.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.2 + controller-gen.kubebuilder.io/version: v0.19.0 name: fieldexports.services.k8s.aws spec: group: services.k8s.aws diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt index b5334c0..6681234 100644 --- a/helm/templates/NOTES.txt +++ b/helm/templates/NOTES.txt @@ -1,5 +1,5 @@ {{ .Chart.Name }} has been installed. -This chart deploys "public.ecr.aws/aws-controllers-k8s/emrcontainers-controller:1.1.1". +This chart deploys "public.ecr.aws/aws-controllers-k8s/emrcontainers-controller:0.0.0-non-release-version". Check its status by running: kubectl --namespace {{ .Release.Namespace }} get pods -l "app.kubernetes.io/instance={{ .Release.Name }}" diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 19c2d75..46ba787 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -73,6 +73,7 @@ rules: - emrcontainers.services.k8s.aws resources: - jobruns + - jobtemplates - virtualclusters verbs: - create @@ -86,6 +87,7 @@ rules: - emrcontainers.services.k8s.aws resources: - jobruns/status + - jobtemplates/status - virtualclusters/status verbs: - get diff --git a/helm/templates/role-reader.yaml b/helm/templates/role-reader.yaml index 6952f5e..d1ca12f 100644 --- a/helm/templates/role-reader.yaml +++ b/helm/templates/role-reader.yaml @@ -17,6 +17,7 @@ rules: - emrcontainers.services.k8s.aws resources: - jobruns + - jobtemplates - virtualclusters verbs: - get diff --git a/helm/templates/role-writer.yaml b/helm/templates/role-writer.yaml index 173ed0e..a229ea6 100644 --- a/helm/templates/role-writer.yaml +++ b/helm/templates/role-writer.yaml @@ -17,6 +17,7 @@ rules: - emrcontainers.services.k8s.aws resources: - jobruns + - jobtemplates - virtualclusters verbs: - create @@ -30,6 +31,7 @@ rules: - emrcontainers.services.k8s.aws resources: - jobruns + - jobtemplates - virtualclusters verbs: - get diff --git a/helm/values.yaml b/helm/values.yaml index 2ba665c..3023522 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -4,7 +4,7 @@ image: repository: public.ecr.aws/aws-controllers-k8s/emrcontainers-controller - tag: 1.1.1 + tag: 0.0.0-non-release-version pullPolicy: IfNotPresent pullSecrets: [] @@ -146,6 +146,7 @@ reconcile: # If specified, only the listed resource kinds will be reconciled. resources: - JobRun + - JobTemplate - VirtualCluster serviceAccount: diff --git a/pkg/resource/job_template/delta.go b/pkg/resource/job_template/delta.go new file mode 100644 index 0000000..8038040 --- /dev/null +++ b/pkg/resource/job_template/delta.go @@ -0,0 +1,135 @@ +// 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. + +// Code generated by ack-generate. DO NOT EDIT. + +package job_template + +import ( + "bytes" + "reflect" + + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + acktags "github.com/aws-controllers-k8s/runtime/pkg/tags" +) + +// Hack to avoid import errors during build... +var ( + _ = &bytes.Buffer{} + _ = &reflect.Method{} + _ = &acktags.Tags{} +) + +// newResourceDelta returns a new `ackcompare.Delta` used to compare two +// resources +func newResourceDelta( + a *resource, + b *resource, +) *ackcompare.Delta { + delta := ackcompare.NewDelta() + if (a == nil && b != nil) || + (a != nil && b == nil) { + delta.Add("", a, b) + return delta + } + + if ackcompare.HasNilDifference(a.ko.Spec.ClientToken, b.ko.Spec.ClientToken) { + delta.Add("Spec.ClientToken", a.ko.Spec.ClientToken, b.ko.Spec.ClientToken) + } else if a.ko.Spec.ClientToken != nil && b.ko.Spec.ClientToken != nil { + if *a.ko.Spec.ClientToken != *b.ko.Spec.ClientToken { + delta.Add("Spec.ClientToken", a.ko.Spec.ClientToken, b.ko.Spec.ClientToken) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.JobTemplateData, b.ko.Spec.JobTemplateData) { + delta.Add("Spec.JobTemplateData", a.ko.Spec.JobTemplateData, b.ko.Spec.JobTemplateData) + } else if a.ko.Spec.JobTemplateData != nil && b.ko.Spec.JobTemplateData != nil { + if ackcompare.HasNilDifference(a.ko.Spec.JobTemplateData.ExecutionRoleARN, b.ko.Spec.JobTemplateData.ExecutionRoleARN) { + delta.Add("Spec.JobTemplateData.ExecutionRoleARN", a.ko.Spec.JobTemplateData.ExecutionRoleARN, b.ko.Spec.JobTemplateData.ExecutionRoleARN) + } else if a.ko.Spec.JobTemplateData.ExecutionRoleARN != nil && b.ko.Spec.JobTemplateData.ExecutionRoleARN != nil { + if *a.ko.Spec.JobTemplateData.ExecutionRoleARN != *b.ko.Spec.JobTemplateData.ExecutionRoleARN { + delta.Add("Spec.JobTemplateData.ExecutionRoleARN", a.ko.Spec.JobTemplateData.ExecutionRoleARN, b.ko.Spec.JobTemplateData.ExecutionRoleARN) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.JobTemplateData.JobDriver, b.ko.Spec.JobTemplateData.JobDriver) { + delta.Add("Spec.JobTemplateData.JobDriver", a.ko.Spec.JobTemplateData.JobDriver, b.ko.Spec.JobTemplateData.JobDriver) + } else if a.ko.Spec.JobTemplateData.JobDriver != nil && b.ko.Spec.JobTemplateData.JobDriver != nil { + if ackcompare.HasNilDifference(a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver, b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver) { + delta.Add("Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver", a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver, b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver) + } else if a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver != nil && b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver != nil { + if ackcompare.HasNilDifference(a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint, b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint) { + delta.Add("Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint", a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint, b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint) + } else if a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint != nil && b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint != nil { + if *a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint != *b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint { + delta.Add("Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint", a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint, b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint) + } + } + if len(a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments) != len(b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments) { + delta.Add("Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments", a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments, b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments) + } else if len(a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments) > 0 { + if !ackcompare.SliceStringPEqual(a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments, b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments) { + delta.Add("Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments", a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments, b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters, b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters) { + delta.Add("Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters", a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters, b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters) + } else if a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters != nil && b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters != nil { + if *a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters != *b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters { + delta.Add("Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters", a.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters, b.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters) + } + } + } + } + if len(a.ko.Spec.JobTemplateData.JobTags) != len(b.ko.Spec.JobTemplateData.JobTags) { + delta.Add("Spec.JobTemplateData.JobTags", a.ko.Spec.JobTemplateData.JobTags, b.ko.Spec.JobTemplateData.JobTags) + } else if len(a.ko.Spec.JobTemplateData.JobTags) > 0 { + if !ackcompare.MapStringStringPEqual(a.ko.Spec.JobTemplateData.JobTags, b.ko.Spec.JobTemplateData.JobTags) { + delta.Add("Spec.JobTemplateData.JobTags", a.ko.Spec.JobTemplateData.JobTags, b.ko.Spec.JobTemplateData.JobTags) + } + } + if len(a.ko.Spec.JobTemplateData.ParameterConfiguration) != len(b.ko.Spec.JobTemplateData.ParameterConfiguration) { + delta.Add("Spec.JobTemplateData.ParameterConfiguration", a.ko.Spec.JobTemplateData.ParameterConfiguration, b.ko.Spec.JobTemplateData.ParameterConfiguration) + } else if len(a.ko.Spec.JobTemplateData.ParameterConfiguration) > 0 { + if !reflect.DeepEqual(a.ko.Spec.JobTemplateData.ParameterConfiguration, b.ko.Spec.JobTemplateData.ParameterConfiguration) { + delta.Add("Spec.JobTemplateData.ParameterConfiguration", a.ko.Spec.JobTemplateData.ParameterConfiguration, b.ko.Spec.JobTemplateData.ParameterConfiguration) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.JobTemplateData.ReleaseLabel, b.ko.Spec.JobTemplateData.ReleaseLabel) { + delta.Add("Spec.JobTemplateData.ReleaseLabel", a.ko.Spec.JobTemplateData.ReleaseLabel, b.ko.Spec.JobTemplateData.ReleaseLabel) + } else if a.ko.Spec.JobTemplateData.ReleaseLabel != nil && b.ko.Spec.JobTemplateData.ReleaseLabel != nil { + if *a.ko.Spec.JobTemplateData.ReleaseLabel != *b.ko.Spec.JobTemplateData.ReleaseLabel { + delta.Add("Spec.JobTemplateData.ReleaseLabel", a.ko.Spec.JobTemplateData.ReleaseLabel, b.ko.Spec.JobTemplateData.ReleaseLabel) + } + } + } + if ackcompare.HasNilDifference(a.ko.Spec.KMSKeyARN, b.ko.Spec.KMSKeyARN) { + delta.Add("Spec.KMSKeyARN", a.ko.Spec.KMSKeyARN, b.ko.Spec.KMSKeyARN) + } else if a.ko.Spec.KMSKeyARN != nil && b.ko.Spec.KMSKeyARN != nil { + if *a.ko.Spec.KMSKeyARN != *b.ko.Spec.KMSKeyARN { + delta.Add("Spec.KMSKeyARN", a.ko.Spec.KMSKeyARN, b.ko.Spec.KMSKeyARN) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.Name, b.ko.Spec.Name) { + delta.Add("Spec.Name", a.ko.Spec.Name, b.ko.Spec.Name) + } else if a.ko.Spec.Name != nil && b.ko.Spec.Name != nil { + if *a.ko.Spec.Name != *b.ko.Spec.Name { + delta.Add("Spec.Name", a.ko.Spec.Name, b.ko.Spec.Name) + } + } + desiredACKTags, _ := convertToOrderedACKTags(a.ko.Spec.Tags) + latestACKTags, _ := convertToOrderedACKTags(b.ko.Spec.Tags) + if !ackcompare.MapStringStringEqual(desiredACKTags, latestACKTags) { + delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags) + } + + return delta +} diff --git a/pkg/resource/job_template/descriptor.go b/pkg/resource/job_template/descriptor.go new file mode 100644 index 0000000..11b7cd9 --- /dev/null +++ b/pkg/resource/job_template/descriptor.go @@ -0,0 +1,155 @@ +// 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. + +// Code generated by ack-generate. DO NOT EDIT. + +package job_template + +import ( + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + rtclient "sigs.k8s.io/controller-runtime/pkg/client" + k8sctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + svcapitypes "github.com/aws-controllers-k8s/emrcontainers-controller/apis/v1alpha1" +) + +const ( + FinalizerString = "finalizers.emrcontainers.services.k8s.aws/JobTemplate" +) + +var ( + GroupVersionResource = svcapitypes.GroupVersion.WithResource("jobtemplates") + GroupKind = metav1.GroupKind{ + Group: "emrcontainers.services.k8s.aws", + Kind: "JobTemplate", + } +) + +// resourceDescriptor implements the +// `aws-service-operator-k8s/pkg/types.AWSResourceDescriptor` interface +type resourceDescriptor struct { +} + +// GroupVersionKind returns a Kubernetes schema.GroupVersionKind struct that +// describes the API Group, Version and Kind of CRs described by the descriptor +func (d *resourceDescriptor) GroupVersionKind() schema.GroupVersionKind { + return svcapitypes.GroupVersion.WithKind(GroupKind.Kind) +} + +// EmptyRuntimeObject returns an empty object prototype that may be used in +// apimachinery and k8s client operations +func (d *resourceDescriptor) EmptyRuntimeObject() rtclient.Object { + return &svcapitypes.JobTemplate{} +} + +// ResourceFromRuntimeObject returns an AWSResource that has been initialized +// with the supplied runtime.Object +func (d *resourceDescriptor) ResourceFromRuntimeObject( + obj rtclient.Object, +) acktypes.AWSResource { + return &resource{ + ko: obj.(*svcapitypes.JobTemplate), + } +} + +// Delta returns an `ackcompare.Delta` object containing the difference between +// one `AWSResource` and another. +func (d *resourceDescriptor) Delta(a, b acktypes.AWSResource) *ackcompare.Delta { + return newResourceDelta(a.(*resource), b.(*resource)) +} + +// IsManaged returns true if the supplied AWSResource is under the management +// of an ACK service controller. What this means in practice is that the +// underlying custom resource (CR) in the AWSResource has had a +// resource-specific finalizer associated with it. +func (d *resourceDescriptor) IsManaged( + res acktypes.AWSResource, +) bool { + obj := res.RuntimeObject() + if obj == nil { + // Should not happen. If it does, there is a bug in the code + panic("nil RuntimeMetaObject in AWSResource") + } + // Remove use of custom code once + // https://github.com/kubernetes-sigs/controller-runtime/issues/994 is + // fixed. This should be able to be: + // + // return k8sctrlutil.ContainsFinalizer(obj, FinalizerString) + return containsFinalizer(obj, FinalizerString) +} + +// Remove once https://github.com/kubernetes-sigs/controller-runtime/issues/994 +// is fixed. +func containsFinalizer(obj rtclient.Object, finalizer string) bool { + f := obj.GetFinalizers() + for _, e := range f { + if e == finalizer { + return true + } + } + return false +} + +// MarkManaged places the supplied resource under the management of ACK. What +// this typically means is that the resource manager will decorate the +// underlying custom resource (CR) with a finalizer that indicates ACK is +// managing the resource and the underlying CR may not be deleted until ACK is +// finished cleaning up any backend AWS service resources associated with the +// CR. +func (d *resourceDescriptor) MarkManaged( + res acktypes.AWSResource, +) { + obj := res.RuntimeObject() + if obj == nil { + // Should not happen. If it does, there is a bug in the code + panic("nil RuntimeMetaObject in AWSResource") + } + k8sctrlutil.AddFinalizer(obj, FinalizerString) +} + +// MarkUnmanaged removes the supplied resource from management by ACK. What +// this typically means is that the resource manager will remove a finalizer +// underlying custom resource (CR) that indicates ACK is managing the resource. +// This will allow the Kubernetes API server to delete the underlying CR. +func (d *resourceDescriptor) MarkUnmanaged( + res acktypes.AWSResource, +) { + obj := res.RuntimeObject() + if obj == nil { + // Should not happen. If it does, there is a bug in the code + panic("nil RuntimeMetaObject in AWSResource") + } + k8sctrlutil.RemoveFinalizer(obj, FinalizerString) +} + +// MarkAdopted places descriptors on the custom resource that indicate the +// resource was not created from within ACK. +func (d *resourceDescriptor) MarkAdopted( + res acktypes.AWSResource, +) { + obj := res.RuntimeObject() + if obj == nil { + // Should not happen. If it does, there is a bug in the code + panic("nil RuntimeObject in AWSResource") + } + curr := obj.GetAnnotations() + if curr == nil { + curr = make(map[string]string) + } + curr[ackv1alpha1.AnnotationAdopted] = "true" + obj.SetAnnotations(curr) +} diff --git a/pkg/resource/job_template/hooks.go b/pkg/resource/job_template/hooks.go new file mode 100644 index 0000000..6049600 --- /dev/null +++ b/pkg/resource/job_template/hooks.go @@ -0,0 +1,66 @@ +// 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 job_template + +import ( + "context" + + "github.com/aws-controllers-k8s/emrcontainers-controller/pkg/tags" +) + +// getTags retrieves the tags for a given JobTemplate resource +func (rm *resourceManager) getTags( + ctx context.Context, + resourceARN string, +) map[string]*string { + tags, err := tags.GetTags(ctx, resourceARN, rm.sdkapi) + if err != nil { + return nil + } + return tags +} + +// syncTags synchronizes the tags between the spec and the AWS resource +func (rm *resourceManager) syncTags( + ctx context.Context, + latest *resource, + desired *resource, +) error { + if latest.ko.Status.ACKResourceMetadata == nil || latest.ko.Status.ACKResourceMetadata.ARN == nil { + return nil + } + resourceARN := string(*latest.ko.Status.ACKResourceMetadata.ARN) + + var latestTags map[string]*string + if latest.ko.Spec.Tags != nil { + latestTags = latest.ko.Spec.Tags + } else { + latestTags = map[string]*string{} + } + + var desiredTags map[string]*string + if desired.ko.Spec.Tags != nil { + desiredTags = desired.ko.Spec.Tags + } else { + desiredTags = map[string]*string{} + } + + return tags.SyncTags( + ctx, + resourceARN, + desiredTags, + latestTags, + rm.sdkapi, + ) +} diff --git a/pkg/resource/job_template/identifiers.go b/pkg/resource/job_template/identifiers.go new file mode 100644 index 0000000..0d3d671 --- /dev/null +++ b/pkg/resource/job_template/identifiers.go @@ -0,0 +1,55 @@ +// 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. + +// Code generated by ack-generate. DO NOT EDIT. + +package job_template + +import ( + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" +) + +// resourceIdentifiers implements the +// `aws-service-operator-k8s/pkg/types.AWSResourceIdentifiers` interface +type resourceIdentifiers struct { + meta *ackv1alpha1.ResourceMetadata +} + +// ARN returns the AWS Resource Name for the backend AWS resource. If nil, +// this means the resource has not yet been created in the backend AWS +// service. +func (ri *resourceIdentifiers) ARN() *ackv1alpha1.AWSResourceName { + if ri.meta != nil { + return ri.meta.ARN + } + return nil +} + +// OwnerAccountID returns the AWS account identifier in which the +// backend AWS resource resides, or nil if this information is not known +// for the resource +func (ri *resourceIdentifiers) OwnerAccountID() *ackv1alpha1.AWSAccountID { + if ri.meta != nil { + return ri.meta.OwnerAccountID + } + return nil +} + +// Region returns the AWS region in which the resource exists, or +// nil if this information is not known. +func (ri *resourceIdentifiers) Region() *ackv1alpha1.AWSRegion { + if ri.meta != nil { + return ri.meta.Region + } + return nil +} diff --git a/pkg/resource/job_template/manager.go b/pkg/resource/job_template/manager.go new file mode 100644 index 0000000..81960cb --- /dev/null +++ b/pkg/resource/job_template/manager.go @@ -0,0 +1,404 @@ +// 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. + +// Code generated by ack-generate. DO NOT EDIT. + +package job_template + +import ( + "context" + "fmt" + "time" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition" + ackcfg "github.com/aws-controllers-k8s/runtime/pkg/config" + 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-v2/aws" + svcsdk "github.com/aws/aws-sdk-go-v2/service/emrcontainers" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + + svcapitypes "github.com/aws-controllers-k8s/emrcontainers-controller/apis/v1alpha1" +) + +var ( + _ = ackutil.InStrings + _ = acktags.NewTags() + _ = ackrt.MissingImageTagValue + _ = svcapitypes.JobTemplate{} +) + +// +kubebuilder:rbac:groups=emrcontainers.services.k8s.aws,resources=jobtemplates,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=emrcontainers.services.k8s.aws,resources=jobtemplates/status,verbs=get;update;patch + +var lateInitializeFieldNames = []string{} + +// resourceManager is responsible for providing a consistent way to perform +// CRUD operations in a backend AWS service API for Book custom resources. +type resourceManager struct { + // cfg is a copy of the ackcfg.Config object passed on start of the service + // controller + cfg ackcfg.Config + // clientcfg is a copy of the client configuration passed on start of the + // service controller + clientcfg aws.Config + // log refers to the logr.Logger object handling logging for the service + // controller + log logr.Logger + // metrics contains a collection of Prometheus metric objects that the + // service controller and its reconcilers track + metrics *ackmetrics.Metrics + // rr is the Reconciler which can be used for various utility + // functions such as querying for Secret values given a SecretReference + rr acktypes.Reconciler + // awsAccountID is the AWS account identifier that contains the resources + // managed by this resource manager + awsAccountID ackv1alpha1.AWSAccountID + // The AWS Region that this resource manager targets + awsRegion ackv1alpha1.AWSRegion + // sdk is a pointer to the AWS service API client exposed by the + // aws-sdk-go-v2/services/{alias} package. + sdkapi *svcsdk.Client +} + +// concreteResource returns a pointer to a resource from the supplied +// generic AWSResource interface +func (rm *resourceManager) concreteResource( + res acktypes.AWSResource, +) *resource { + // cast the generic interface into a pointer type specific to the concrete + // implementing resource type managed by this resource manager + return res.(*resource) +} + +// ReadOne returns the currently-observed state of the supplied AWSResource in +// the backend AWS service API. +func (rm *resourceManager) ReadOne( + ctx context.Context, + res acktypes.AWSResource, +) (acktypes.AWSResource, error) { + r := rm.concreteResource(res) + if r.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's ReadOne() method received resource with nil CR object") + } + observed, err := rm.sdkFind(ctx, r) + mirrorAWSTags(r, observed) + if err != nil { + if observed != nil { + return rm.onError(observed, err) + } + return rm.onError(r, err) + } + return rm.onSuccess(observed) +} + +// Create attempts to create the supplied AWSResource in the backend AWS +// service API, returning an AWSResource representing the newly-created +// resource +func (rm *resourceManager) Create( + ctx context.Context, + res acktypes.AWSResource, +) (acktypes.AWSResource, error) { + r := rm.concreteResource(res) + if r.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's Create() method received resource with nil CR object") + } + created, err := rm.sdkCreate(ctx, r) + if err != nil { + if created != nil { + return rm.onError(created, err) + } + return rm.onError(r, err) + } + return rm.onSuccess(created) +} + +// Update attempts to mutate the supplied desired AWSResource in the backend AWS +// service API, returning an AWSResource representing the newly-mutated +// resource. +// Note for specialized logic implementers can check to see how the latest +// observed resource differs from the supplied desired state. The +// higher-level reonciler determines whether or not the desired differs +// from the latest observed and decides whether to call the resource +// manager's Update method +func (rm *resourceManager) Update( + ctx context.Context, + resDesired acktypes.AWSResource, + resLatest acktypes.AWSResource, + delta *ackcompare.Delta, +) (acktypes.AWSResource, error) { + desired := rm.concreteResource(resDesired) + latest := rm.concreteResource(resLatest) + if desired.ko == nil || latest.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's Update() method received resource with nil CR object") + } + updated, err := rm.sdkUpdate(ctx, desired, latest, delta) + if err != nil { + if updated != nil { + return rm.onError(updated, err) + } + return rm.onError(latest, err) + } + return rm.onSuccess(updated) +} + +// Delete attempts to destroy the supplied AWSResource in the backend AWS +// service API, returning an AWSResource representing the +// resource being deleted (if delete is asynchronous and takes time) +func (rm *resourceManager) Delete( + ctx context.Context, + res acktypes.AWSResource, +) (acktypes.AWSResource, error) { + r := rm.concreteResource(res) + if r.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's Update() method received resource with nil CR object") + } + observed, err := rm.sdkDelete(ctx, r) + if err != nil { + if observed != nil { + return rm.onError(observed, err) + } + return rm.onError(r, err) + } + + return rm.onSuccess(observed) +} + +// ARNFromName returns an AWS Resource Name from a given string name. This +// is useful for constructing ARNs for APIs that require ARNs in their +// GetAttributes operations but all we have (for new CRs at least) is a +// name for the resource +func (rm *resourceManager) ARNFromName(name string) string { + return fmt.Sprintf( + "arn:aws:emrcontainers:%s:%s:%s", + rm.awsRegion, + rm.awsAccountID, + name, + ) +} + +// LateInitialize returns an acktypes.AWSResource after setting the late initialized +// fields from the readOne call. This method will initialize the optional fields +// which were not provided by the k8s user but were defaulted by the AWS service. +// If there are no such fields to be initialized, the returned object is similar to +// object passed in the parameter. +func (rm *resourceManager) LateInitialize( + ctx context.Context, + latest acktypes.AWSResource, +) (acktypes.AWSResource, error) { + rlog := ackrtlog.FromContext(ctx) + // If there are no fields to late initialize, do nothing + if len(lateInitializeFieldNames) == 0 { + rlog.Debug("no late initialization required.") + return latest, nil + } + latestCopy := latest.DeepCopy() + lateInitConditionReason := "" + lateInitConditionMessage := "" + observed, err := rm.ReadOne(ctx, latestCopy) + if err != nil { + lateInitConditionMessage = "Unable to complete Read operation required for late initialization" + lateInitConditionReason = "Late Initialization Failure" + ackcondition.SetLateInitialized(latestCopy, corev1.ConditionFalse, &lateInitConditionMessage, &lateInitConditionReason) + ackcondition.SetSynced(latestCopy, corev1.ConditionFalse, nil, nil) + return latestCopy, err + } + lateInitializedRes := rm.lateInitializeFromReadOneOutput(observed, latestCopy) + incompleteInitialization := rm.incompleteLateInitialization(lateInitializedRes) + if incompleteInitialization { + // Add the condition with LateInitialized=False + lateInitConditionMessage = "Late initialization did not complete, requeuing with delay of 5 seconds" + lateInitConditionReason = "Delayed Late Initialization" + ackcondition.SetLateInitialized(lateInitializedRes, corev1.ConditionFalse, &lateInitConditionMessage, &lateInitConditionReason) + ackcondition.SetSynced(lateInitializedRes, corev1.ConditionFalse, nil, nil) + return lateInitializedRes, ackrequeue.NeededAfter(nil, time.Duration(5)*time.Second) + } + // Set LateInitialized condition to True + lateInitConditionMessage = "Late initialization successful" + lateInitConditionReason = "Late initialization successful" + ackcondition.SetLateInitialized(lateInitializedRes, corev1.ConditionTrue, &lateInitConditionMessage, &lateInitConditionReason) + return lateInitializedRes, nil +} + +// incompleteLateInitialization return true if there are fields which were supposed to be +// late initialized but are not. If all the fields are late initialized, false is returned +func (rm *resourceManager) incompleteLateInitialization( + res acktypes.AWSResource, +) bool { + return false +} + +// lateInitializeFromReadOneOutput late initializes the 'latest' resource from the 'observed' +// resource and returns 'latest' resource +func (rm *resourceManager) lateInitializeFromReadOneOutput( + observed acktypes.AWSResource, + latest acktypes.AWSResource, +) acktypes.AWSResource { + return latest +} + +// IsSynced returns true if the resource is synced. +func (rm *resourceManager) IsSynced(ctx context.Context, res acktypes.AWSResource) (bool, error) { + r := rm.concreteResource(res) + if r.ko == nil { + // Should never happen... if it does, it's buggy code. + panic("resource manager's IsSynced() method received resource with nil CR object") + } + + 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 { + 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 map[string]*string + existingTags = r.ko.Spec.Tags + resourceTags, keyOrder := convertToOrderedACKTags(existingTags) + tags := acktags.Merge(resourceTags, defaultTags) + r.ko.Spec.Tags = fromACKTags(tags, keyOrder) + return nil +} + +// FilterAWSTags ignores tags that have keys that start with "aws:" +// is needed to ensure the controller does not attempt to remove +// tags set by AWS. This function needs to be called after each Read +// operation. +// Eg. resources created with cloudformation have tags that cannot be +// removed by an ACK controller +func (rm *resourceManager) FilterSystemTags(res acktypes.AWSResource) { + r := rm.concreteResource(res) + if r == nil || r.ko == nil { + return + } + var existingTags map[string]*string + existingTags = r.ko.Spec.Tags + resourceTags, tagKeyOrder := convertToOrderedACKTags(existingTags) + ignoreSystemTags(resourceTags) + r.ko.Spec.Tags = fromACKTags(resourceTags, tagKeyOrder) +} + +// mirrorAWSTags ensures that AWS tags are included in the desired resource +// if they are present in the latest resource. This will ensure that the +// aws tags are not present in a diff. The logic of the controller will +// ensure these tags aren't patched to the resource in the cluster, and +// will only be present to make sure we don't try to remove these tags. +// +// Although there are a lot of similarities between this function and +// EnsureTags, they are very much different. +// While EnsureTags tries to make sure the resource contains the controller +// tags, mirrowAWSTags tries to make sure tags injected by AWS are mirrored +// from the latest resoruce to the desired resource. +func mirrorAWSTags(a *resource, b *resource) { + if a == nil || a.ko == nil || b == nil || b.ko == nil { + return + } + var existingLatestTags map[string]*string + var existingDesiredTags map[string]*string + existingDesiredTags = a.ko.Spec.Tags + existingLatestTags = b.ko.Spec.Tags + desiredTags, desiredTagKeyOrder := convertToOrderedACKTags(existingDesiredTags) + latestTags, _ := convertToOrderedACKTags(existingLatestTags) + syncAWSTags(desiredTags, latestTags) + a.ko.Spec.Tags = fromACKTags(desiredTags, desiredTagKeyOrder) +} + +// newResourceManager returns a new struct implementing +// acktypes.AWSResourceManager +// This is for AWS-SDK-GO-V2 - Created newResourceManager With AWS sdk-Go-ClientV2 +func newResourceManager( + cfg ackcfg.Config, + clientcfg aws.Config, + log logr.Logger, + metrics *ackmetrics.Metrics, + rr acktypes.Reconciler, + id ackv1alpha1.AWSAccountID, + region ackv1alpha1.AWSRegion, +) (*resourceManager, error) { + return &resourceManager{ + cfg: cfg, + clientcfg: clientcfg, + log: log, + metrics: metrics, + rr: rr, + awsAccountID: id, + awsRegion: region, + sdkapi: svcsdk.NewFromConfig(clientcfg), + }, nil +} + +// onError updates resource conditions and returns updated resource +// it returns nil if no condition is updated. +func (rm *resourceManager) onError( + r *resource, + err error, +) (acktypes.AWSResource, error) { + if r == nil { + return nil, err + } + r1, updated := rm.updateConditions(r, false, err) + if !updated { + return r, err + } + for _, condition := range r1.Conditions() { + if condition.Type == ackv1alpha1.ConditionTypeTerminal && + condition.Status == corev1.ConditionTrue { + // resource is in Terminal condition + // return Terminal error + return r1, ackerr.Terminal + } + } + return r1, err +} + +// onSuccess updates resource conditions and returns updated resource +// it returns the supplied resource if no condition is updated. +func (rm *resourceManager) onSuccess( + r *resource, +) (acktypes.AWSResource, error) { + if r == nil { + return nil, nil + } + r1, updated := rm.updateConditions(r, true, nil) + if !updated { + return r, nil + } + return r1, nil +} diff --git a/pkg/resource/job_template/manager_factory.go b/pkg/resource/job_template/manager_factory.go new file mode 100644 index 0000000..b56179f --- /dev/null +++ b/pkg/resource/job_template/manager_factory.go @@ -0,0 +1,100 @@ +// 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. + +// Code generated by ack-generate. DO NOT EDIT. + +package job_template + +import ( + "fmt" + "sync" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackcfg "github.com/aws-controllers-k8s/runtime/pkg/config" + ackmetrics "github.com/aws-controllers-k8s/runtime/pkg/metrics" + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/go-logr/logr" + + svcresource "github.com/aws-controllers-k8s/emrcontainers-controller/pkg/resource" +) + +// resourceManagerFactory produces resourceManager objects. It implements the +// `types.AWSResourceManagerFactory` interface. +type resourceManagerFactory struct { + sync.RWMutex + // rmCache contains resource managers for a particular AWS account ID + rmCache map[string]*resourceManager +} + +// ResourcePrototype returns an AWSResource that resource managers produced by +// this factory will handle +func (f *resourceManagerFactory) ResourceDescriptor() acktypes.AWSResourceDescriptor { + return &resourceDescriptor{} +} + +// ManagerFor returns a resource manager object that can manage resources for a +// supplied AWS account +func (f *resourceManagerFactory) ManagerFor( + cfg ackcfg.Config, + clientcfg aws.Config, + log logr.Logger, + metrics *ackmetrics.Metrics, + rr acktypes.Reconciler, + id ackv1alpha1.AWSAccountID, + region ackv1alpha1.AWSRegion, + roleARN ackv1alpha1.AWSResourceName, +) (acktypes.AWSResourceManager, error) { + // We use the account ID, region, and role ARN to uniquely identify a + // resource manager. This helps us to avoid creating multiple resource + // managers for the same account/region/roleARN combination. + rmId := fmt.Sprintf("%s/%s/%s", id, region, roleARN) + f.RLock() + rm, found := f.rmCache[rmId] + f.RUnlock() + + if found { + return rm, nil + } + + f.Lock() + defer f.Unlock() + + rm, err := newResourceManager(cfg, clientcfg, log, metrics, rr, id, region) + if err != nil { + return nil, err + } + f.rmCache[rmId] = rm + return rm, nil +} + +// IsAdoptable returns true if the resource is able to be adopted +func (f *resourceManagerFactory) IsAdoptable() bool { + return true +} + +// RequeueOnSuccessSeconds returns true if the resource should be requeued after specified seconds +// Default is false which means resource will not be requeued after success. +func (f *resourceManagerFactory) RequeueOnSuccessSeconds() int { + return 0 +} + +func newResourceManagerFactory() *resourceManagerFactory { + return &resourceManagerFactory{ + rmCache: map[string]*resourceManager{}, + } +} + +func init() { + svcresource.RegisterManagerFactory(newResourceManagerFactory()) +} diff --git a/pkg/resource/job_template/references.go b/pkg/resource/job_template/references.go new file mode 100644 index 0000000..0781524 --- /dev/null +++ b/pkg/resource/job_template/references.go @@ -0,0 +1,57 @@ +// 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. + +// Code generated by ack-generate. DO NOT EDIT. + +package job_template + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" + + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + + svcapitypes "github.com/aws-controllers-k8s/emrcontainers-controller/apis/v1alpha1" +) + +// ClearResolvedReferences removes any reference values that were made +// concrete in the spec. It returns a copy of the input AWSResource which +// contains the original *Ref values, but none of their respective concrete +// values. +func (rm *resourceManager) ClearResolvedReferences(res acktypes.AWSResource) acktypes.AWSResource { + ko := rm.concreteResource(res).ko.DeepCopy() + + return &resource{ko} +} + +// ResolveReferences finds if there are any Reference field(s) present +// inside AWSResource passed in the parameter and attempts to resolve those +// reference field(s) into their respective target field(s). It returns a +// copy of the input AWSResource with resolved reference(s), a boolean which +// is set to true if the resource contains any references (regardless of if +// they are resolved successfully) and an error if the passed AWSResource's +// reference field(s) could not be resolved. +func (rm *resourceManager) ResolveReferences( + ctx context.Context, + apiReader client.Reader, + res acktypes.AWSResource, +) (acktypes.AWSResource, bool, error) { + return res, false, nil +} + +// validateReferenceFields validates the reference field and corresponding +// identifier field. +func validateReferenceFields(ko *svcapitypes.JobTemplate) error { + return nil +} diff --git a/pkg/resource/job_template/resource.go b/pkg/resource/job_template/resource.go new file mode 100644 index 0000000..59e7e3d --- /dev/null +++ b/pkg/resource/job_template/resource.go @@ -0,0 +1,113 @@ +// 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. + +// Code generated by ack-generate. DO NOT EDIT. + +package job_template + +import ( + "fmt" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackerrors "github.com/aws-controllers-k8s/runtime/pkg/errors" + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + rtclient "sigs.k8s.io/controller-runtime/pkg/client" + + svcapitypes "github.com/aws-controllers-k8s/emrcontainers-controller/apis/v1alpha1" +) + +// Hack to avoid import errors during build... +var ( + _ = &ackerrors.MissingNameIdentifier +) + +// resource implements the `aws-controller-k8s/runtime/pkg/types.AWSResource` +// interface +type resource struct { + // The Kubernetes-native CR representing the resource + ko *svcapitypes.JobTemplate +} + +// Identifiers returns an AWSResourceIdentifiers object containing various +// identifying information, including the AWS account ID that owns the +// resource, the resource's AWS Resource Name (ARN) +func (r *resource) Identifiers() acktypes.AWSResourceIdentifiers { + return &resourceIdentifiers{r.ko.Status.ACKResourceMetadata} +} + +// IsBeingDeleted returns true if the Kubernetes resource has a non-zero +// deletion timestamp +func (r *resource) IsBeingDeleted() bool { + return !r.ko.DeletionTimestamp.IsZero() +} + +// RuntimeObject returns the Kubernetes apimachinery/runtime representation of +// the AWSResource +func (r *resource) RuntimeObject() rtclient.Object { + return r.ko +} + +// MetaObject returns the Kubernetes apimachinery/apis/meta/v1.Object +// representation of the AWSResource +func (r *resource) MetaObject() metav1.Object { + return r.ko.GetObjectMeta() +} + +// Conditions returns the ACK Conditions collection for the AWSResource +func (r *resource) Conditions() []*ackv1alpha1.Condition { + return r.ko.Status.Conditions +} + +// ReplaceConditions sets the Conditions status field for the resource +func (r *resource) ReplaceConditions(conditions []*ackv1alpha1.Condition) { + r.ko.Status.Conditions = conditions +} + +// SetObjectMeta sets the ObjectMeta field for the resource +func (r *resource) SetObjectMeta(meta metav1.ObjectMeta) { + r.ko.ObjectMeta = meta +} + +// SetStatus will set the Status field for the resource +func (r *resource) SetStatus(desired acktypes.AWSResource) { + r.ko.Status = desired.(*resource).ko.Status +} + +// SetIdentifiers sets the Spec or Status field that is referenced as the unique +// resource identifier +func (r *resource) SetIdentifiers(identifier *ackv1alpha1.AWSIdentifiers) error { + if identifier.NameOrID == "" { + return ackerrors.MissingNameIdentifier + } + r.ko.Status.ID = &identifier.NameOrID + + return nil +} + +// PopulateResourceFromAnnotation populates the fields passed from adoption annotation +func (r *resource) PopulateResourceFromAnnotation(fields map[string]string) error { + f0, ok := fields["id"] + if !ok { + return ackerrors.NewTerminalError(fmt.Errorf("required field missing: id")) + } + r.ko.Status.ID = &f0 + + return nil +} + +// DeepCopy will return a copy of the resource +func (r *resource) DeepCopy() acktypes.AWSResource { + koCopy := r.ko.DeepCopy() + return &resource{koCopy} +} diff --git a/pkg/resource/job_template/sdk.go b/pkg/resource/job_template/sdk.go new file mode 100644 index 0000000..c3bc390 --- /dev/null +++ b/pkg/resource/job_template/sdk.go @@ -0,0 +1,498 @@ +// 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. + +// Code generated by ack-generate. DO NOT EDIT. + +package job_template + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition" + ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" + ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" + "github.com/aws/aws-sdk-go-v2/aws" + svcsdk "github.com/aws/aws-sdk-go-v2/service/emrcontainers" + svcsdktypes "github.com/aws/aws-sdk-go-v2/service/emrcontainers/types" + smithy "github.com/aws/smithy-go" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + svcapitypes "github.com/aws-controllers-k8s/emrcontainers-controller/apis/v1alpha1" +) + +// Hack to avoid import errors during build... +var ( + _ = &metav1.Time{} + _ = strings.ToLower("") + _ = &svcsdk.Client{} + _ = &svcapitypes.JobTemplate{} + _ = ackv1alpha1.AWSAccountID("") + _ = &ackerr.NotFound + _ = &ackcondition.NotManagedMessage + _ = &reflect.Value{} + _ = fmt.Sprintf("") + _ = &ackrequeue.NoRequeue{} + _ = &aws.Config{} +) + +// sdkFind returns SDK-specific information about a supplied resource +func (rm *resourceManager) sdkFind( + ctx context.Context, + r *resource, +) (latest *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.sdkFind") + defer func() { + exit(err) + }() + // If any required fields in the input shape are missing, AWS resource is + // not created yet. Return NotFound here to indicate to callers that the + // resource isn't yet created. + if rm.requiredFieldsMissingFromReadOneInput(r) { + return nil, ackerr.NotFound + } + + input, err := rm.newDescribeRequestPayload(r) + if err != nil { + return nil, err + } + + var resp *svcsdk.DescribeJobTemplateOutput + resp, err = rm.sdkapi.DescribeJobTemplate(ctx, input) + rm.metrics.RecordAPICall("READ_ONE", "DescribeJobTemplate", err) + if err != nil { + var awsErr smithy.APIError + if errors.As(err, &awsErr) && awsErr.ErrorCode() == "ResourceNotFoundException" { + return nil, ackerr.NotFound + } + 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 := r.ko.DeepCopy() + + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + if resp.JobTemplate.Arn != nil { + arn := ackv1alpha1.AWSResourceName(*resp.JobTemplate.Arn) + ko.Status.ACKResourceMetadata.ARN = &arn + } + if resp.JobTemplate.CreatedAt != nil { + ko.Status.CreatedAt = &metav1.Time{*resp.JobTemplate.CreatedAt} + } else { + ko.Status.CreatedAt = nil + } + if resp.JobTemplate.Id != nil { + ko.Status.ID = resp.JobTemplate.Id + } else { + ko.Status.ID = nil + } + if resp.JobTemplate.JobTemplateData != nil { + f5 := &svcapitypes.JobTemplateData{} + if resp.JobTemplate.JobTemplateData.ConfigurationOverrides != nil { + f5.ConfigurationOverrides = resp.JobTemplate.JobTemplateData.ConfigurationOverrides + } + if resp.JobTemplate.JobTemplateData.ExecutionRoleArn != nil { + f5.ExecutionRoleARN = resp.JobTemplate.JobTemplateData.ExecutionRoleArn + } + if resp.JobTemplate.JobTemplateData.JobDriver != nil { + f5f2 := &svcapitypes.JobDriver{} + if resp.JobTemplate.JobTemplateData.JobDriver.SparkSubmitJobDriver != nil { + f5f2f0 := &svcapitypes.SparkSubmitJobDriver{} + if resp.JobTemplate.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint != nil { + f5f2f0.EntryPoint = resp.JobTemplate.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint + } + if resp.JobTemplate.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments != nil { + f5f2f0.EntryPointArguments = aws.StringSlice(resp.JobTemplate.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments) + } + if resp.JobTemplate.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters != nil { + f5f2f0.SparkSubmitParameters = resp.JobTemplate.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters + } + f5f2.SparkSubmitJobDriver = f5f2f0 + } + f5.JobDriver = f5f2 + } + if resp.JobTemplate.JobTemplateData.JobTags != nil { + f5.JobTags = aws.StringMap(resp.JobTemplate.JobTemplateData.JobTags) + } + if resp.JobTemplate.JobTemplateData.ParameterConfiguration != nil { + f5f4 := map[string]*svcapitypes.TemplateParameterConfiguration{} + for f5f4key, f5f4valiter := range resp.JobTemplate.JobTemplateData.ParameterConfiguration { + f5f4val := &svcapitypes.TemplateParameterConfiguration{} + if f5f4valiter.DefaultValue != nil { + f5f4val.DefaultValue = f5f4valiter.DefaultValue + } + if f5f4valiter.Type != "" { + f5f4val.Type = aws.String(string(f5f4valiter.Type)) + } + f5f4[f5f4key] = f5f4val + } + f5.ParameterConfiguration = f5f4 + } + if resp.JobTemplate.JobTemplateData.ReleaseLabel != nil { + f5.ReleaseLabel = resp.JobTemplate.JobTemplateData.ReleaseLabel + } + ko.Spec.JobTemplateData = f5 + } else { + ko.Spec.JobTemplateData = nil + } + if resp.JobTemplate.KmsKeyArn != nil { + ko.Spec.KMSKeyARN = resp.JobTemplate.KmsKeyArn + } else { + ko.Spec.KMSKeyARN = nil + } + if resp.JobTemplate.Name != nil { + ko.Spec.Name = resp.JobTemplate.Name + } else { + ko.Spec.Name = nil + } + if resp.JobTemplate.Tags != nil { + ko.Spec.Tags = aws.StringMap(resp.JobTemplate.Tags) + } else { + ko.Spec.Tags = nil + } + + rm.setStatusDefaults(ko) + // Retrieve and set the tags for the JobTemplate resource + if ko.Status.ACKResourceMetadata != nil && ko.Status.ACKResourceMetadata.ARN != nil { + ko.Spec.Tags = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) + } + return &resource{ko}, nil +} + +// requiredFieldsMissingFromReadOneInput returns true if there are any fields +// for the ReadOne Input shape that are required but not present in the +// resource's Spec or Status +func (rm *resourceManager) requiredFieldsMissingFromReadOneInput( + r *resource, +) bool { + return r.ko.Status.ID == nil + +} + +// newDescribeRequestPayload returns SDK-specific struct for the HTTP request +// payload of the Describe API call for the resource +func (rm *resourceManager) newDescribeRequestPayload( + r *resource, +) (*svcsdk.DescribeJobTemplateInput, error) { + res := &svcsdk.DescribeJobTemplateInput{} + + if r.ko.Status.ID != nil { + res.Id = r.ko.Status.ID + } + + return res, nil +} + +// sdkCreate creates the supplied resource in the backend AWS service API and +// returns a copy of the resource with resource fields (in both Spec and +// Status) filled in with values from the CREATE API operation's Output shape. +func (rm *resourceManager) sdkCreate( + ctx context.Context, + desired *resource, +) (created *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.sdkCreate") + defer func() { + exit(err) + }() + input, err := rm.newCreateRequestPayload(ctx, desired) + if err != nil { + return nil, err + } + + var resp *svcsdk.CreateJobTemplateOutput + _ = resp + resp, err = rm.sdkapi.CreateJobTemplate(ctx, input) + rm.metrics.RecordAPICall("CREATE", "CreateJobTemplate", 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 ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + if resp.Arn != nil { + arn := ackv1alpha1.AWSResourceName(*resp.Arn) + ko.Status.ACKResourceMetadata.ARN = &arn + } + if resp.CreatedAt != nil { + ko.Status.CreatedAt = &metav1.Time{*resp.CreatedAt} + } else { + ko.Status.CreatedAt = nil + } + if resp.Id != nil { + ko.Status.ID = resp.Id + } else { + ko.Status.ID = nil + } + if resp.Name != nil { + ko.Spec.Name = resp.Name + } else { + ko.Spec.Name = nil + } + + rm.setStatusDefaults(ko) + // Add tags to the JobTemplate resource after creation + if ko.Spec.Tags != nil { + // Mark the resource as not synced to trigger a requeue and apply tags + rm.setStatusSynced(ctx, &resource{ko}, nil, nil, false) + } + return &resource{ko}, nil +} + +// newCreateRequestPayload returns an SDK-specific struct for the HTTP request +// payload of the Create API call for the resource +func (rm *resourceManager) newCreateRequestPayload( + ctx context.Context, + r *resource, +) (*svcsdk.CreateJobTemplateInput, error) { + res := &svcsdk.CreateJobTemplateInput{} + + if r.ko.Spec.ClientToken != nil { + res.ClientToken = r.ko.Spec.ClientToken + } + if r.ko.Spec.JobTemplateData != nil { + f1 := &svcsdktypes.JobTemplateData{} + if r.ko.Spec.JobTemplateData.ConfigurationOverrides != nil { + f1.ConfigurationOverrides = r.ko.Spec.JobTemplateData.ConfigurationOverrides + } + if r.ko.Spec.JobTemplateData.ExecutionRoleARN != nil { + f1.ExecutionRoleArn = r.ko.Spec.JobTemplateData.ExecutionRoleARN + } + if r.ko.Spec.JobTemplateData.JobDriver != nil { + f1f2 := &svcsdktypes.JobDriver{} + if r.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver != nil { + f1f2f0 := &svcsdktypes.SparkSubmitJobDriver{} + if r.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint != nil { + f1f2f0.EntryPoint = r.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPoint + } + if r.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments != nil { + f1f2f0.EntryPointArguments = aws.ToStringSlice(r.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.EntryPointArguments) + } + if r.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters != nil { + f1f2f0.SparkSubmitParameters = r.ko.Spec.JobTemplateData.JobDriver.SparkSubmitJobDriver.SparkSubmitParameters + } + f1f2.SparkSubmitJobDriver = f1f2f0 + } + f1.JobDriver = f1f2 + } + if r.ko.Spec.JobTemplateData.JobTags != nil { + f1.JobTags = aws.ToStringMap(r.ko.Spec.JobTemplateData.JobTags) + } + if r.ko.Spec.JobTemplateData.ParameterConfiguration != nil { + f1f4 := map[string]svcsdktypes.TemplateParameterConfiguration{} + for f1f4key, f1f4valiter := range r.ko.Spec.JobTemplateData.ParameterConfiguration { + f1f4val := &svcsdktypes.TemplateParameterConfiguration{} + if f1f4valiter.DefaultValue != nil { + f1f4val.DefaultValue = f1f4valiter.DefaultValue + } + if f1f4valiter.Type != nil { + f1f4val.Type = svcsdktypes.TemplateParameterDataType(*f1f4valiter.Type) + } + f1f4[f1f4key] = *f1f4val + } + f1.ParameterConfiguration = f1f4 + } + if r.ko.Spec.JobTemplateData.ReleaseLabel != nil { + f1.ReleaseLabel = r.ko.Spec.JobTemplateData.ReleaseLabel + } + res.JobTemplateData = f1 + } + if r.ko.Spec.KMSKeyARN != nil { + res.KmsKeyArn = r.ko.Spec.KMSKeyARN + } + if r.ko.Spec.Name != nil { + res.Name = r.ko.Spec.Name + } + if r.ko.Spec.Tags != nil { + res.Tags = aws.ToStringMap(r.ko.Spec.Tags) + } + + return res, nil +} + +// sdkUpdate patches the supplied resource in the backend AWS service API and +// returns a new resource with updated fields. +func (rm *resourceManager) sdkUpdate( + ctx context.Context, + desired *resource, + latest *resource, + delta *ackcompare.Delta, +) (*resource, error) { + return nil, ackerr.NewTerminalError(ackerr.NotImplemented) +} + +// sdkDelete deletes the supplied resource in the backend AWS service API +func (rm *resourceManager) sdkDelete( + ctx context.Context, + r *resource, +) (latest *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.sdkDelete") + defer func() { + exit(err) + }() + input, err := rm.newDeleteRequestPayload(r) + if err != nil { + return nil, err + } + var resp *svcsdk.DeleteJobTemplateOutput + _ = resp + resp, err = rm.sdkapi.DeleteJobTemplate(ctx, input) + rm.metrics.RecordAPICall("DELETE", "DeleteJobTemplate", err) + return nil, err +} + +// newDeleteRequestPayload returns an SDK-specific struct for the HTTP request +// payload of the Delete API call for the resource +func (rm *resourceManager) newDeleteRequestPayload( + r *resource, +) (*svcsdk.DeleteJobTemplateInput, error) { + res := &svcsdk.DeleteJobTemplateInput{} + + if r.ko.Status.ID != nil { + res.Id = r.ko.Status.ID + } + + return res, nil +} + +// setStatusDefaults sets default properties into supplied custom resource +func (rm *resourceManager) setStatusDefaults( + ko *svcapitypes.JobTemplate, +) { + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + if ko.Status.ACKResourceMetadata.Region == nil { + ko.Status.ACKResourceMetadata.Region = &rm.awsRegion + } + if ko.Status.ACKResourceMetadata.OwnerAccountID == nil { + ko.Status.ACKResourceMetadata.OwnerAccountID = &rm.awsAccountID + } + if ko.Status.Conditions == nil { + ko.Status.Conditions = []*ackv1alpha1.Condition{} + } +} + +// updateConditions returns updated resource, true; if conditions were updated +// else it returns nil, false +func (rm *resourceManager) updateConditions( + r *resource, + onSuccess bool, + err error, +) (*resource, bool) { + ko := r.ko.DeepCopy() + rm.setStatusDefaults(ko) + + // Terminal condition + var terminalCondition *ackv1alpha1.Condition = nil + var recoverableCondition *ackv1alpha1.Condition = nil + var syncCondition *ackv1alpha1.Condition = nil + for _, condition := range ko.Status.Conditions { + if condition.Type == ackv1alpha1.ConditionTypeTerminal { + terminalCondition = condition + } + if condition.Type == ackv1alpha1.ConditionTypeRecoverable { + recoverableCondition = condition + } + if condition.Type == ackv1alpha1.ConditionTypeResourceSynced { + syncCondition = condition + } + } + var termError *ackerr.TerminalError + if rm.terminalAWSError(err) || err == ackerr.SecretTypeNotSupported || err == ackerr.SecretNotFound || errors.As(err, &termError) { + if terminalCondition == nil { + terminalCondition = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeTerminal, + } + ko.Status.Conditions = append(ko.Status.Conditions, terminalCondition) + } + var errorMessage = "" + if err == ackerr.SecretTypeNotSupported || err == ackerr.SecretNotFound || errors.As(err, &termError) { + errorMessage = err.Error() + } else { + awsErr, _ := ackerr.AWSError(err) + errorMessage = awsErr.Error() + } + terminalCondition.Status = corev1.ConditionTrue + terminalCondition.Message = &errorMessage + } else { + // Clear the terminal condition if no longer present + if terminalCondition != nil { + terminalCondition.Status = corev1.ConditionFalse + terminalCondition.Message = nil + } + // Handling Recoverable Conditions + if err != nil { + if recoverableCondition == nil { + // Add a new Condition containing a non-terminal error + recoverableCondition = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeRecoverable, + } + ko.Status.Conditions = append(ko.Status.Conditions, recoverableCondition) + } + recoverableCondition.Status = corev1.ConditionTrue + awsErr, _ := ackerr.AWSError(err) + errorMessage := err.Error() + if awsErr != nil { + errorMessage = awsErr.Error() + } + recoverableCondition.Message = &errorMessage + } else if recoverableCondition != nil { + recoverableCondition.Status = corev1.ConditionFalse + recoverableCondition.Message = nil + } + } + // Required to avoid the "declared but not used" error in the default case + _ = syncCondition + if terminalCondition != nil || recoverableCondition != nil || syncCondition != nil { + return &resource{ko}, true // updated + } + return nil, false // not updated +} + +// terminalAWSError returns awserr, true; if the supplied error is an aws Error type +// and if the exception indicates that it is a Terminal exception +// 'Terminal' exception are specified in generator configuration +func (rm *resourceManager) terminalAWSError(err error) bool { + if err == nil { + return false + } + + var terminalErr smithy.APIError + if !errors.As(err, &terminalErr) { + return false + } + switch terminalErr.ErrorCode() { + case "ValidationException", + "ResourceNotFoundException": + return true + default: + return false + } +} diff --git a/pkg/resource/job_template/tags.go b/pkg/resource/job_template/tags.go new file mode 100644 index 0000000..e1dfa87 --- /dev/null +++ b/pkg/resource/job_template/tags.go @@ -0,0 +1,108 @@ +// 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. + +// Code generated by ack-generate. DO NOT EDIT. + +package job_template + +import ( + "slices" + "strings" + + acktags "github.com/aws-controllers-k8s/runtime/pkg/tags" + + svcapitypes "github.com/aws-controllers-k8s/emrcontainers-controller/apis/v1alpha1" +) + +var ( + _ = svcapitypes.JobTemplate{} + _ = acktags.NewTags() + ACKSystemTags = []string{"services.k8s.aws/namespace", "services.k8s.aws/controller-version"} +) + +// convertToOrderedACKTags 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. It also returns a slice +// of keys maintaining the original key Order when the tags are a list +func convertToOrderedACKTags(tags map[string]*string) (acktags.Tags, []string) { + result := acktags.NewTags() + keyOrder := []string{} + + if len(tags) == 0 { + return result, keyOrder + } + for k, v := range tags { + if v == nil { + result[k] = "" + } else { + result[k] = *v + } + } + + return result, keyOrder +} + +// fromACKTags converts the tags parameter into map[string]*string shape. +// This method helps in setting the tags back inside AWSResource after merging +// default controller tags with existing resource tags. When a list, +// it maintains the order from original +func fromACKTags(tags acktags.Tags, keyOrder []string) map[string]*string { + result := map[string]*string{} + + _ = keyOrder + for k, v := range tags { + result[k] = &v + } + + return result +} + +// ignoreSystemTags ignores tags that have keys that start with "aws:" +// and ACKSystemTags, to avoid patching them to the resourceSpec. +// Eg. resources created with cloudformation have tags that cannot be +// removed by an ACK controller +func ignoreSystemTags(tags acktags.Tags) { + for k := range tags { + if strings.HasPrefix(k, "aws:") || + slices.Contains(ACKSystemTags, k) { + delete(tags, k) + } + } +} + +// syncAWSTags ensures AWS-managed tags (prefixed with "aws:") from the latest resource state +// are preserved in the desired state. This prevents the controller from attempting to +// modify AWS-managed tags, which would result in an error. +// +// AWS-managed tags are automatically added by AWS services (e.g., CloudFormation, Service Catalog) +// and cannot be modified or deleted through normal tag operations. Common examples include: +// - aws:cloudformation:stack-name +// - aws:servicecatalog:productArn +// +// Parameters: +// - a: The target Tags map to be updated (typically desired state) +// - b: The source Tags map containing AWS-managed tags (typically latest state) +// +// Example: +// +// latest := Tags{"aws:cloudformation:stack-name": "my-stack", "environment": "prod"} +// desired := Tags{"environment": "dev"} +// SyncAWSTags(desired, latest) +// desired now contains {"aws:cloudformation:stack-name": "my-stack", "environment": "dev"} +func syncAWSTags(a acktags.Tags, b acktags.Tags) { + for k := range b { + if strings.HasPrefix(k, "aws:") { + a[k] = b[k] + } + } +} diff --git a/pkg/tags/sync.go b/pkg/tags/sync.go new file mode 100644 index 0000000..b70ed5b --- /dev/null +++ b/pkg/tags/sync.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 tags + +import ( + "context" + + svcsdk "github.com/aws/aws-sdk-go-v2/service/emrcontainers" +) + +// GetTags returns the tags for a given resource ARN +func GetTags( + ctx context.Context, + resourceARN string, + sdkapi *svcsdk.Client, +) (map[string]*string, error) { + input := &svcsdk.ListTagsForResourceInput{ + ResourceArn: &resourceARN, + } + resp, err := sdkapi.ListTagsForResource(ctx, input) + if err != nil { + return nil, err + } + + // Convert to map[string]*string for compatibility with ACK + tags := make(map[string]*string) + for k, v := range resp.Tags { + value := v // Create a copy to avoid issues with the loop variable + tags[k] = &value + } + + return tags, nil +} + +// SyncTags synchronizes the tags between the spec and the AWS resource +func SyncTags( + ctx context.Context, + resourceARN string, + desired map[string]*string, + latest map[string]*string, + sdkapi *svcsdk.Client, +) error { + // Check if tags are the same + if mapsEqual(desired, latest) { + return nil + } + + // Determine which tags to add/update + toAdd := make(map[string]string) + for k, v := range desired { + if v != nil { + latestVal, exists := latest[k] + if !exists || *v != *latestVal { + toAdd[k] = *v + } + } + } + + // Determine which tags to remove + toRemove := []string{} + for k := range latest { + if _, exists := desired[k]; !exists { + toRemove = append(toRemove, k) + } + } + + if len(toAdd) > 0 { + _, err := sdkapi.TagResource( + ctx, + &svcsdk.TagResourceInput{ + ResourceArn: &resourceARN, + Tags: toAdd, + }, + ) + if err != nil { + return err + } + } + + if len(toRemove) > 0 { + _, err := sdkapi.UntagResource( + ctx, + &svcsdk.UntagResourceInput{ + ResourceArn: &resourceARN, + TagKeys: toRemove, + }, + ) + if err != nil { + return err + } + } + + return nil +} + +// mapsEqual compares two string pointer maps for equality +func mapsEqual(a, b map[string]*string) bool { + if len(a) != len(b) { + return false + } + + for k, v1 := range a { + v2, ok := b[k] + if !ok || v1 == nil || v2 == nil || *v1 != *v2 { + return false + } + } + + return true +} diff --git a/pkg/tags/sync_test.go b/pkg/tags/sync_test.go new file mode 100644 index 0000000..720dfcc --- /dev/null +++ b/pkg/tags/sync_test.go @@ -0,0 +1,151 @@ +// 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 tags + +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go/aws" + svcsdk "github.com/aws/aws-sdk-go/service/emrcontainers" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type mockSDKAPI struct { + mock.Mock +} + +func (m *mockSDKAPI) ListTagsForResourceWithContext(ctx context.Context, input *svcsdk.ListTagsForResourceInput, opts ...interface{}) (*svcsdk.ListTagsForResourceOutput, error) { + args := m.Called(ctx, input) + return args.Get(0).(*svcsdk.ListTagsForResourceOutput), args.Error(1) +} + +func (m *mockSDKAPI) TagResourceWithContext(ctx context.Context, input *svcsdk.TagResourceInput, opts ...interface{}) (*svcsdk.TagResourceOutput, error) { + args := m.Called(ctx, input) + return args.Get(0).(*svcsdk.TagResourceOutput), args.Error(1) +} + +func (m *mockSDKAPI) UntagResourceWithContext(ctx context.Context, input *svcsdk.UntagResourceInput, opts ...interface{}) (*svcsdk.UntagResourceOutput, error) { + args := m.Called(ctx, input) + return args.Get(0).(*svcsdk.UntagResourceOutput), args.Error(1) +} + +func TestGetTags(t *testing.T) { + ctx := context.Background() + resourceARN := "arn:aws:emr-containers:us-west-2:123456789012:jobtemplate/test-job-template" + + mockAPI := &mockSDKAPI{} + mockAPI.On("ListTagsForResourceWithContext", ctx, &svcsdk.ListTagsForResourceInput{ + ResourceArn: aws.String(resourceARN), + }).Return(&svcsdk.ListTagsForResourceOutput{ + Tags: map[string]*string{ + "key1": aws.String("value1"), + "key2": aws.String("value2"), + }, + }, nil) + + tags, err := GetTags(ctx, resourceARN, mockAPI) + + assert.NoError(t, err) + assert.Len(t, tags, 2) + assert.Equal(t, "value1", aws.StringValue(tags["key1"])) + assert.Equal(t, "value2", aws.StringValue(tags["key2"])) + + mockAPI.AssertExpectations(t) +} + +func TestSyncTags(t *testing.T) { + ctx := context.Background() + resourceARN := "arn:aws:emr-containers:us-west-2:123456789012:jobtemplate/test-job-template" + + // Test adding and updating tags + mockAPI := &mockSDKAPI{} + mockAPI.On("TagResourceWithContext", ctx, mock.MatchedBy(func(input *svcsdk.TagResourceInput) bool { + return aws.StringValue(input.ResourceArn) == resourceARN && + aws.StringValue(input.Tags["key1"]) == "new-value1" && + aws.StringValue(input.Tags["key3"]) == "value3" + })).Return(&svcsdk.TagResourceOutput{}, nil) + + mockAPI.On("UntagResourceWithContext", ctx, mock.MatchedBy(func(input *svcsdk.UntagResourceInput) bool { + return aws.StringValue(input.ResourceArn) == resourceARN && + aws.StringValue(input.TagKeys[0]) == "key2" + })).Return(&svcsdk.UntagResourceOutput{}, nil) + + desired := map[string]*string{ + "key1": aws.String("new-value1"), + "key3": aws.String("value3"), + } + + latest := map[string]*string{ + "key1": aws.String("value1"), + "key2": aws.String("value2"), + } + + err := SyncTags(ctx, resourceARN, desired, latest, mockAPI) + + assert.NoError(t, err) + mockAPI.AssertExpectations(t) + + // Test with no changes + mockAPI = &mockSDKAPI{} + + desired = map[string]*string{ + "key1": aws.String("value1"), + "key2": aws.String("value2"), + } + + latest = map[string]*string{ + "key1": aws.String("value1"), + "key2": aws.String("value2"), + } + + err = SyncTags(ctx, resourceARN, desired, latest, mockAPI) + + assert.NoError(t, err) + mockAPI.AssertExpectations(t) +} + +func TestMapsEqual(t *testing.T) { + // Test equal maps + map1 := map[string]*string{ + "key1": aws.String("value1"), + "key2": aws.String("value2"), + } + map2 := map[string]*string{ + "key1": aws.String("value1"), + "key2": aws.String("value2"), + } + assert.True(t, mapsEqual(map1, map2)) + + // Test different values + map3 := map[string]*string{ + "key1": aws.String("different"), + "key2": aws.String("value2"), + } + assert.False(t, mapsEqual(map1, map3)) + + // Test different keys + map4 := map[string]*string{ + "key1": aws.String("value1"), + "key3": aws.String("value3"), + } + assert.False(t, mapsEqual(map1, map4)) + + // Test different lengths + map5 := map[string]*string{ + "key1": aws.String("value1"), + } + assert.False(t, mapsEqual(map1, map5)) +} diff --git a/templates/hooks/job_template/sdk_create_post_set_output.go.tpl b/templates/hooks/job_template/sdk_create_post_set_output.go.tpl new file mode 100644 index 0000000..ab77cb4 --- /dev/null +++ b/templates/hooks/job_template/sdk_create_post_set_output.go.tpl @@ -0,0 +1,5 @@ +// Add tags to the JobTemplate resource after creation +if ko.Spec.Tags != nil { + // Mark the resource as not synced to trigger a requeue and apply tags + rm.setStatusSynced(ctx, &resource{ko}, nil, nil, false) +} \ No newline at end of file diff --git a/templates/hooks/job_template/sdk_read_one_post_set_output.go.tpl b/templates/hooks/job_template/sdk_read_one_post_set_output.go.tpl new file mode 100644 index 0000000..9219c31 --- /dev/null +++ b/templates/hooks/job_template/sdk_read_one_post_set_output.go.tpl @@ -0,0 +1,4 @@ +// Retrieve and set the tags for the JobTemplate resource +if ko.Status.ACKResourceMetadata != nil && ko.Status.ACKResourceMetadata.ARN != nil { + ko.Spec.Tags = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) +} \ No newline at end of file diff --git a/templates/hooks/job_template/sdk_update_pre_build_request.go.tpl b/templates/hooks/job_template/sdk_update_pre_build_request.go.tpl new file mode 100644 index 0000000..301b794 --- /dev/null +++ b/templates/hooks/job_template/sdk_update_pre_build_request.go.tpl @@ -0,0 +1,15 @@ +// Sync tags if they've changed +if delta.DifferentAt("Spec.Tags") { + err := rm.syncTags( + ctx, + latest, + desired, + ) + if err != nil { + return nil, err + } +} +// If the only difference is in the tags, we don't need to make an update call +if !delta.DifferentExcept("Spec.Tags") { + return desired, nil +} \ No newline at end of file