From 8e13474ecb0d34c2c30c8d6cf85451c07fea3eaf Mon Sep 17 00:00:00 2001 From: Guido van der Hart Date: Mon, 8 Jul 2024 11:09:27 +0200 Subject: [PATCH 1/2] Add CidrCollection support to route53 controller --- apis/v1alpha1/ack-generate-metadata.yaml | 2 +- apis/v1alpha1/cidr_collection.go | 80 +++ apis/v1alpha1/generator.yaml | 72 ++- apis/v1alpha1/types.go | 25 +- apis/v1alpha1/zz_generated.deepcopy.go | 233 ++++++++- cmd/controller/main.go | 1 + ...te53.services.k8s.aws_cidrcollections.yaml | 164 ++++++ config/crd/kustomization.yaml | 1 + config/rbac/cluster-role-controller.yaml | 20 + config/rbac/role-reader.yaml | 1 + config/rbac/role-writer.yaml | 2 + generator.yaml | 72 ++- ...te53.services.k8s.aws_cidrcollections.yaml | 164 ++++++ helm/templates/_helpers.tpl | 20 + helm/templates/role-reader.yaml | 1 + helm/templates/role-writer.yaml | 2 + pkg/resource/cidr_collection/delta.go | 63 +++ pkg/resource/cidr_collection/descriptor.go | 155 ++++++ pkg/resource/cidr_collection/hooks.go | 185 +++++++ pkg/resource/cidr_collection/identifiers.go | 55 ++ pkg/resource/cidr_collection/manager.go | 350 +++++++++++++ .../cidr_collection/manager_factory.go | 96 ++++ pkg/resource/cidr_collection/references.go | 56 +++ pkg/resource/cidr_collection/resource.go | 100 ++++ pkg/resource/cidr_collection/sdk.go | 475 ++++++++++++++++++ .../sdk_create_post_build_request.go.tpl | 7 + .../sdk_create_post_set_output.go.tpl | 6 + .../sdk_delete_post_build_request.go.tpl | 8 + .../sdk_read_many_pre_set_output.go.tpl | 77 +++ test/e2e/resources/cidr_collection.yaml | 15 + test/e2e/tests/helper.py | 29 ++ test/e2e/tests/test_cidr_collection.py | 93 ++++ 32 files changed, 2619 insertions(+), 11 deletions(-) create mode 100644 apis/v1alpha1/cidr_collection.go create mode 100644 config/crd/bases/route53.services.k8s.aws_cidrcollections.yaml create mode 100644 helm/crds/route53.services.k8s.aws_cidrcollections.yaml create mode 100644 pkg/resource/cidr_collection/delta.go create mode 100644 pkg/resource/cidr_collection/descriptor.go create mode 100644 pkg/resource/cidr_collection/hooks.go create mode 100644 pkg/resource/cidr_collection/identifiers.go create mode 100644 pkg/resource/cidr_collection/manager.go create mode 100644 pkg/resource/cidr_collection/manager_factory.go create mode 100644 pkg/resource/cidr_collection/references.go create mode 100644 pkg/resource/cidr_collection/resource.go create mode 100644 pkg/resource/cidr_collection/sdk.go create mode 100644 templates/hooks/cidr_collection/sdk_create_post_build_request.go.tpl create mode 100644 templates/hooks/cidr_collection/sdk_create_post_set_output.go.tpl create mode 100644 templates/hooks/cidr_collection/sdk_delete_post_build_request.go.tpl create mode 100644 templates/hooks/cidr_collection/sdk_read_many_pre_set_output.go.tpl create mode 100644 test/e2e/resources/cidr_collection.yaml create mode 100644 test/e2e/tests/test_cidr_collection.py diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index 2c01d87..510144c 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -7,7 +7,7 @@ api_directory_checksum: 78fb7fd24a85da24b8de6246cad67ff3fb6598f8 api_version: v1alpha1 aws_sdk_go_version: v1.49.0 generator_config_info: - file_checksum: 3a694e7177f90f7ba4c526b14e1ce2b1e9748446 + file_checksum: a841c574fab5ddf8a35ade3ba90e7f6713255d7e original_file_name: generator.yaml last_modification: reason: API generation diff --git a/apis/v1alpha1/cidr_collection.go b/apis/v1alpha1/cidr_collection.go new file mode 100644 index 0000000..a18692b --- /dev/null +++ b/apis/v1alpha1/cidr_collection.go @@ -0,0 +1,80 @@ +// 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" +) + +// CidrCollectionSpec defines the desired state of CidrCollection. +// +// A complex type that identifies a CIDR collection. +type CIDRCollectionSpec struct { + + // Information about changes to a CIDR collection. + // +kubebuilder:validation:Required + Locations []*CIDRCollectionChange `json:"locations"` + // A unique identifier for the account that can be used to reference the collection + // from other API calls. + // +kubebuilder:validation:Required + Name *string `json:"name"` +} + +// CIDRCollectionStatus defines the observed state of CIDRCollection +type CIDRCollectionStatus 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"` + // +kubebuilder:validation:Optional + CallerReference *string `json:"callerReference,omitempty"` + // A complex type that contains information about the CIDR collection. + // +kubebuilder:validation:Optional + Collection *CIDRCollection_SDK `json:"collection,omitempty"` + // A unique URL that represents the location for the CIDR collection. + // +kubebuilder:validation:Optional + Location *string `json:"location,omitempty"` +} + +// CIDRCollection is the Schema for the CIDRCollections API +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type CIDRCollection struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec CIDRCollectionSpec `json:"spec,omitempty"` + Status CIDRCollectionStatus `json:"status,omitempty"` +} + +// CIDRCollectionList contains a list of CIDRCollection +// +kubebuilder:object:root=true +type CIDRCollectionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []CIDRCollection `json:"items"` +} + +func init() { + SchemeBuilder.Register(&CIDRCollection{}, &CIDRCollectionList{}) +} diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index 1c4e386..1d693e9 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -3,9 +3,10 @@ ignore: - ChangeResourceRecordSetsOutput.ChangeInfo.Comment - CreateHostedZoneInput.CallerReference - CreateHealthCheckInput.CallerReference + - CreateCidrCollectionInput.CallerReference operations: null resource_names: - - CidrCollection + # - CidrCollection # - HealthCheck # - HostedZone - KeySigningKey @@ -15,7 +16,6 @@ ignore: - TrafficPolicyInstance - TrafficPolicyVersion - VPCAssociationAuthorization - - CidrCollection shape_names: null model_name: route53 operations: @@ -30,6 +30,26 @@ operations: - List resource_name: RecordSet + CreateCidrCollection: + operation_type: + - Create + resource_name: + CidrCollection + ChangeCidrCollection: + operation_type: + - Update + resource_name: + CidrCollection + DeleteCidrCollection: + operation_type: + - Delete + resource_name: + CidrCollection + ListCidrCollections: + operation_type: + - List + resource_name: + CidrCollection CreateHostedZone: output_wrapper_field_path: HostedZone GetHostedZone: @@ -151,6 +171,54 @@ resources: ignore: true update_operation: custom_method_name: customUpdateRecordSet + CidrCollection: + renames: + operations: + ChangeCidrCollection: + input_fields: + Changes: Locations + exceptions: + terminal_codes: + - InvalidInput + - CidrCollectionAlreadyExists + - CidrCollectionInUse + - CidrCollectionVersionMismatch + - NoSuchCidrCollection + - CidrBlockInUse + fields: + Name: + from: + operation: CreateCidrCollection + path: Name + is_required: true + is_primary_key: true + is_immutable: true + CollectionVersion: + is_required: false + Locations: + from: + operation: ChangeCidrCollection + path: Changes + is_required: true + CallerReference: + is_read_only: true + type: "*string" + hooks: + delta_pre_compare: + code: compareCustom(delta, a, b) + sdk_create_post_build_request: + template_path: hooks/cidr_collection/sdk_create_post_build_request.go.tpl + sdk_create_post_set_output: + template_path: hooks/cidr_collection/sdk_create_post_set_output.go.tpl + sdk_delete_post_build_request: + template_path: hooks/cidr_collection/sdk_delete_post_build_request.go.tpl + sdk_read_many_pre_set_output: + template_path: hooks/cidr_collection/sdk_read_many_pre_set_output.go.tpl + # This resource does not have tags + tags: + ignore: true + update_operation: + custom_method_name: customUpdateCidrCollection HostedZone: exceptions: terminal_codes: diff --git a/apis/v1alpha1/types.go b/apis/v1alpha1/types.go index 82c867e..ec3a1f9 100644 --- a/apis/v1alpha1/types.go +++ b/apis/v1alpha1/types.go @@ -50,9 +50,25 @@ type AliasTarget struct { HostedZoneID *string `json:"hostedZoneID,omitempty"` } +// A complex type that lists the CIDR blocks. +type CIDRBlockSummary struct { + CIDRBlock *string `json:"cidrBlock,omitempty"` + LocationName *string `json:"locationName,omitempty"` +} + +// A complex type that contains information about the CIDR collection change. +type CIDRCollectionChange struct { + Action *string `json:"action,omitempty"` + CIDRList []*string `json:"cidrList,omitempty"` + LocationName *string `json:"locationName,omitempty"` +} + // A complex type that identifies a CIDR collection. -type CIDRCollection struct { - ID *string `json:"id,omitempty"` +type CIDRCollection_SDK struct { + ARN *string `json:"arn,omitempty"` + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Version *int64 `json:"version,omitempty"` } // The object that is specified in resource record set object when you are linking @@ -102,7 +118,10 @@ type CloudWatchAlarmConfiguration struct { // A complex type that is an entry in an CidrCollection (https://docs.aws.amazon.com/Route53/latest/APIReference/API_CidrCollection.html) // array. type CollectionSummary struct { - ID *string `json:"id,omitempty"` + ARN *string `json:"arn,omitempty"` + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Version *int64 `json:"version,omitempty"` } // A complex type that lists the name servers in a delegation set, as well as diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index c939622..fc79536 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -80,15 +80,39 @@ func (in *AliasTarget) DeepCopy() *AliasTarget { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CIDRCollection) DeepCopyInto(out *CIDRCollection) { +func (in *CIDRBlockSummary) DeepCopyInto(out *CIDRBlockSummary) { *out = *in - if in.ID != nil { - in, out := &in.ID, &out.ID + if in.CIDRBlock != nil { + in, out := &in.CIDRBlock, &out.CIDRBlock + *out = new(string) + **out = **in + } + if in.LocationName != nil { + in, out := &in.LocationName, &out.LocationName *out = new(string) **out = **in } } +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CIDRBlockSummary. +func (in *CIDRBlockSummary) DeepCopy() *CIDRBlockSummary { + if in == nil { + return nil + } + out := new(CIDRBlockSummary) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CIDRCollection) DeepCopyInto(out *CIDRCollection) { + *out = *in + 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 CIDRCollection. func (in *CIDRCollection) DeepCopy() *CIDRCollection { if in == nil { @@ -99,6 +123,194 @@ func (in *CIDRCollection) DeepCopy() *CIDRCollection { return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CIDRCollection) 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 *CIDRCollectionChange) DeepCopyInto(out *CIDRCollectionChange) { + *out = *in + if in.Action != nil { + in, out := &in.Action, &out.Action + *out = new(string) + **out = **in + } + if in.CIDRList != nil { + in, out := &in.CIDRList, &out.CIDRList + *out = make([]*string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(string) + **out = **in + } + } + } + if in.LocationName != nil { + in, out := &in.LocationName, &out.LocationName + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CIDRCollectionChange. +func (in *CIDRCollectionChange) DeepCopy() *CIDRCollectionChange { + if in == nil { + return nil + } + out := new(CIDRCollectionChange) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CIDRCollectionList) DeepCopyInto(out *CIDRCollectionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CIDRCollection, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CIDRCollectionList. +func (in *CIDRCollectionList) DeepCopy() *CIDRCollectionList { + if in == nil { + return nil + } + out := new(CIDRCollectionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CIDRCollectionList) 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 *CIDRCollectionSpec) DeepCopyInto(out *CIDRCollectionSpec) { + *out = *in + if in.Locations != nil { + in, out := &in.Locations, &out.Locations + *out = make([]*CIDRCollectionChange, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(CIDRCollectionChange) + (*in).DeepCopyInto(*out) + } + } + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CIDRCollectionSpec. +func (in *CIDRCollectionSpec) DeepCopy() *CIDRCollectionSpec { + if in == nil { + return nil + } + out := new(CIDRCollectionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CIDRCollectionStatus) DeepCopyInto(out *CIDRCollectionStatus) { + *out = *in + if in.ACKResourceMetadata != nil { + in, out := &in.ACKResourceMetadata, &out.ACKResourceMetadata + *out = new(corev1alpha1.ResourceMetadata) + (*in).DeepCopyInto(*out) + } + 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.CallerReference != nil { + in, out := &in.CallerReference, &out.CallerReference + *out = new(string) + **out = **in + } + if in.Collection != nil { + in, out := &in.Collection, &out.Collection + *out = new(CIDRCollection_SDK) + (*in).DeepCopyInto(*out) + } + if in.Location != nil { + in, out := &in.Location, &out.Location + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CIDRCollectionStatus. +func (in *CIDRCollectionStatus) DeepCopy() *CIDRCollectionStatus { + if in == nil { + return nil + } + out := new(CIDRCollectionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CIDRCollection_SDK) DeepCopyInto(out *CIDRCollection_SDK) { + *out = *in + if in.ARN != nil { + in, out := &in.ARN, &out.ARN + *out = new(string) + **out = **in + } + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CIDRCollection_SDK. +func (in *CIDRCollection_SDK) DeepCopy() *CIDRCollection_SDK { + if in == nil { + return nil + } + out := new(CIDRCollection_SDK) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CIDRRoutingConfig) DeepCopyInto(out *CIDRRoutingConfig) { *out = *in @@ -273,11 +485,26 @@ func (in *CloudWatchAlarmConfiguration) DeepCopy() *CloudWatchAlarmConfiguration // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CollectionSummary) DeepCopyInto(out *CollectionSummary) { *out = *in + if in.ARN != nil { + in, out := &in.ARN, &out.ARN + *out = new(string) + **out = **in + } if in.ID != nil { in, out := &in.ID, &out.ID *out = new(string) **out = **in } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(int64) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CollectionSummary. diff --git a/cmd/controller/main.go b/cmd/controller/main.go index b0119b3..68b7378 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -39,6 +39,7 @@ import ( svcresource "github.com/aws-controllers-k8s/route53-controller/pkg/resource" svcsdk "github.com/aws/aws-sdk-go/service/route53" + _ "github.com/aws-controllers-k8s/route53-controller/pkg/resource/cidr_collection" _ "github.com/aws-controllers-k8s/route53-controller/pkg/resource/health_check" _ "github.com/aws-controllers-k8s/route53-controller/pkg/resource/hosted_zone" _ "github.com/aws-controllers-k8s/route53-controller/pkg/resource/record_set" diff --git a/config/crd/bases/route53.services.k8s.aws_cidrcollections.yaml b/config/crd/bases/route53.services.k8s.aws_cidrcollections.yaml new file mode 100644 index 0000000..4344189 --- /dev/null +++ b/config/crd/bases/route53.services.k8s.aws_cidrcollections.yaml @@ -0,0 +1,164 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: cidrcollections.route53.services.k8s.aws +spec: + group: route53.services.k8s.aws + names: + kind: CIDRCollection + listKind: CIDRCollectionList + plural: cidrcollections + singular: cidrcollection + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: CIDRCollection is the Schema for the CIDRCollections 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: |- + CidrCollectionSpec defines the desired state of CidrCollection. + + + A complex type that identifies a CIDR collection. + properties: + locations: + description: Information about changes to a CIDR collection. + items: + description: A complex type that contains information about the + CIDR collection change. + properties: + action: + type: string + cidrList: + items: + type: string + type: array + locationName: + type: string + type: object + type: array + name: + description: |- + A unique identifier for the account that can be used to reference the collection + from other API calls. + type: string + required: + - locations + - name + type: object + status: + description: CIDRCollectionStatus defines the observed state of CIDRCollection + 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. + TODO(vijat@): Find a better strategy for resources that do not have ARN in CreateOutputResponse + 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 + callerReference: + type: string + collection: + description: A complex type that contains information about the CIDR + collection. + properties: + arn: + type: string + id: + type: string + name: + type: string + version: + format: int64 + type: integer + 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 + location: + description: A unique URL that represents the location for the CIDR + collection. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 8116e33..5c18118 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -2,6 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - common + - bases/route53.services.k8s.aws_cidrcollections.yaml - bases/route53.services.k8s.aws_healthchecks.yaml - bases/route53.services.k8s.aws_hostedzones.yaml - bases/route53.services.k8s.aws_recordsets.yaml diff --git a/config/rbac/cluster-role-controller.yaml b/config/rbac/cluster-role-controller.yaml index bde8537..f369820 100644 --- a/config/rbac/cluster-role-controller.yaml +++ b/config/rbac/cluster-role-controller.yaml @@ -30,6 +30,26 @@ rules: - list - patch - watch +- apiGroups: + - route53.services.k8s.aws + resources: + - cidrcollections + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - route53.services.k8s.aws + resources: + - cidrcollections/status + verbs: + - get + - patch + - update - apiGroups: - route53.services.k8s.aws resources: diff --git a/config/rbac/role-reader.yaml b/config/rbac/role-reader.yaml index d1e233d..e958776 100644 --- a/config/rbac/role-reader.yaml +++ b/config/rbac/role-reader.yaml @@ -9,6 +9,7 @@ rules: - apiGroups: - route53.services.k8s.aws resources: + - cidrcollections - healthchecks - hostedzones - recordsets diff --git a/config/rbac/role-writer.yaml b/config/rbac/role-writer.yaml index 32951cb..3861847 100644 --- a/config/rbac/role-writer.yaml +++ b/config/rbac/role-writer.yaml @@ -9,6 +9,7 @@ rules: - apiGroups: - route53.services.k8s.aws resources: + - cidrcollections - healthchecks - hostedzones - recordsets @@ -23,6 +24,7 @@ rules: - apiGroups: - route53.services.k8s.aws resources: + - cidrcollections - healthchecks - hostedzones - recordsets diff --git a/generator.yaml b/generator.yaml index 1c4e386..1d693e9 100644 --- a/generator.yaml +++ b/generator.yaml @@ -3,9 +3,10 @@ ignore: - ChangeResourceRecordSetsOutput.ChangeInfo.Comment - CreateHostedZoneInput.CallerReference - CreateHealthCheckInput.CallerReference + - CreateCidrCollectionInput.CallerReference operations: null resource_names: - - CidrCollection + # - CidrCollection # - HealthCheck # - HostedZone - KeySigningKey @@ -15,7 +16,6 @@ ignore: - TrafficPolicyInstance - TrafficPolicyVersion - VPCAssociationAuthorization - - CidrCollection shape_names: null model_name: route53 operations: @@ -30,6 +30,26 @@ operations: - List resource_name: RecordSet + CreateCidrCollection: + operation_type: + - Create + resource_name: + CidrCollection + ChangeCidrCollection: + operation_type: + - Update + resource_name: + CidrCollection + DeleteCidrCollection: + operation_type: + - Delete + resource_name: + CidrCollection + ListCidrCollections: + operation_type: + - List + resource_name: + CidrCollection CreateHostedZone: output_wrapper_field_path: HostedZone GetHostedZone: @@ -151,6 +171,54 @@ resources: ignore: true update_operation: custom_method_name: customUpdateRecordSet + CidrCollection: + renames: + operations: + ChangeCidrCollection: + input_fields: + Changes: Locations + exceptions: + terminal_codes: + - InvalidInput + - CidrCollectionAlreadyExists + - CidrCollectionInUse + - CidrCollectionVersionMismatch + - NoSuchCidrCollection + - CidrBlockInUse + fields: + Name: + from: + operation: CreateCidrCollection + path: Name + is_required: true + is_primary_key: true + is_immutable: true + CollectionVersion: + is_required: false + Locations: + from: + operation: ChangeCidrCollection + path: Changes + is_required: true + CallerReference: + is_read_only: true + type: "*string" + hooks: + delta_pre_compare: + code: compareCustom(delta, a, b) + sdk_create_post_build_request: + template_path: hooks/cidr_collection/sdk_create_post_build_request.go.tpl + sdk_create_post_set_output: + template_path: hooks/cidr_collection/sdk_create_post_set_output.go.tpl + sdk_delete_post_build_request: + template_path: hooks/cidr_collection/sdk_delete_post_build_request.go.tpl + sdk_read_many_pre_set_output: + template_path: hooks/cidr_collection/sdk_read_many_pre_set_output.go.tpl + # This resource does not have tags + tags: + ignore: true + update_operation: + custom_method_name: customUpdateCidrCollection HostedZone: exceptions: terminal_codes: diff --git a/helm/crds/route53.services.k8s.aws_cidrcollections.yaml b/helm/crds/route53.services.k8s.aws_cidrcollections.yaml new file mode 100644 index 0000000..4344189 --- /dev/null +++ b/helm/crds/route53.services.k8s.aws_cidrcollections.yaml @@ -0,0 +1,164 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: cidrcollections.route53.services.k8s.aws +spec: + group: route53.services.k8s.aws + names: + kind: CIDRCollection + listKind: CIDRCollectionList + plural: cidrcollections + singular: cidrcollection + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: CIDRCollection is the Schema for the CIDRCollections 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: |- + CidrCollectionSpec defines the desired state of CidrCollection. + + + A complex type that identifies a CIDR collection. + properties: + locations: + description: Information about changes to a CIDR collection. + items: + description: A complex type that contains information about the + CIDR collection change. + properties: + action: + type: string + cidrList: + items: + type: string + type: array + locationName: + type: string + type: object + type: array + name: + description: |- + A unique identifier for the account that can be used to reference the collection + from other API calls. + type: string + required: + - locations + - name + type: object + status: + description: CIDRCollectionStatus defines the observed state of CIDRCollection + 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. + TODO(vijat@): Find a better strategy for resources that do not have ARN in CreateOutputResponse + 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 + callerReference: + type: string + collection: + description: A complex type that contains information about the CIDR + collection. + properties: + arn: + type: string + id: + type: string + name: + type: string + version: + format: int64 + type: integer + 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 + location: + description: A unique URL that represents the location for the CIDR + collection. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index a6f632a..d220910 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -77,6 +77,26 @@ rules: - list - patch - watch +- apiGroups: + - route53.services.k8s.aws + resources: + - cidrcollections + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - route53.services.k8s.aws + resources: + - cidrcollections/status + verbs: + - get + - patch + - update - apiGroups: - route53.services.k8s.aws resources: diff --git a/helm/templates/role-reader.yaml b/helm/templates/role-reader.yaml index af4cc87..e3d7a9e 100644 --- a/helm/templates/role-reader.yaml +++ b/helm/templates/role-reader.yaml @@ -9,6 +9,7 @@ rules: - apiGroups: - route53.services.k8s.aws resources: + - cidrcollections - healthchecks - hostedzones - recordsets diff --git a/helm/templates/role-writer.yaml b/helm/templates/role-writer.yaml index a222f6d..87ff0d3 100644 --- a/helm/templates/role-writer.yaml +++ b/helm/templates/role-writer.yaml @@ -9,6 +9,7 @@ rules: - apiGroups: - route53.services.k8s.aws resources: + - cidrcollections - healthchecks - hostedzones - recordsets @@ -23,6 +24,7 @@ rules: - apiGroups: - route53.services.k8s.aws resources: + - cidrcollections - healthchecks - hostedzones - recordsets diff --git a/pkg/resource/cidr_collection/delta.go b/pkg/resource/cidr_collection/delta.go new file mode 100644 index 0000000..59bc00c --- /dev/null +++ b/pkg/resource/cidr_collection/delta.go @@ -0,0 +1,63 @@ +// 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 cidr_collection + +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 + } + compareCustom(delta, a, b) + + if len(a.ko.Spec.Locations) != len(b.ko.Spec.Locations) { + delta.Add("Spec.Locations", a.ko.Spec.Locations, b.ko.Spec.Locations) + } else if len(a.ko.Spec.Locations) > 0 { + if !reflect.DeepEqual(a.ko.Spec.Locations, b.ko.Spec.Locations) { + delta.Add("Spec.Locations", a.ko.Spec.Locations, b.ko.Spec.Locations) + } + } + 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) + } + } + + return delta +} diff --git a/pkg/resource/cidr_collection/descriptor.go b/pkg/resource/cidr_collection/descriptor.go new file mode 100644 index 0000000..ebdcd70 --- /dev/null +++ b/pkg/resource/cidr_collection/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 cidr_collection + +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/route53-controller/apis/v1alpha1" +) + +const ( + finalizerString = "finalizers.route53.services.k8s.aws/CIDRCollection" +) + +var ( + GroupVersionResource = svcapitypes.GroupVersion.WithResource("cidrcollections") + GroupKind = metav1.GroupKind{ + Group: "route53.services.k8s.aws", + Kind: "CIDRCollection", + } +) + +// 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.CIDRCollection{} +} + +// 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.CIDRCollection), + } +} + +// 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/cidr_collection/hooks.go b/pkg/resource/cidr_collection/hooks.go new file mode 100644 index 0000000..2a52752 --- /dev/null +++ b/pkg/resource/cidr_collection/hooks.go @@ -0,0 +1,185 @@ +package cidr_collection + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + "time" + + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" + svcsdk "github.com/aws/aws-sdk-go/service/route53" +) + +// getCallerReference will generate a CallerReference for a given health check +// using the current timestamp, so that it produces a unique value +func getCallerReference() string { + return fmt.Sprintf("%d", time.Now().UnixMilli()) +} + +// oldLocations returns a slice of CidrLocation pointer objects +// to delete. +func (rm *resourceManager) oldLocations( + latest *resource, +) []*svcsdk.CidrCollectionChange { + if latest == nil { + return nil + } + + locations := latest.ko.Spec.Locations + var oldLocations []*svcsdk.CidrCollectionChange + // Add removed locations to Changes using the DELETE_IF_EXISTS action + if len(locations) > 0 { + action := "DELETE_IF_EXISTS" + for _, rr := range locations { + change := &svcsdk.CidrCollectionChange{} + change.SetLocationName(*rr.LocationName) + change.SetCidrList(rr.CIDRList) + change.SetAction(action) + oldLocations = append(oldLocations, change) + } + } + return oldLocations +} + +// newLocations returns a slice of CidrLocation pointer objects +// with values set by the resource's corresponding spec field. +func (rm *resourceManager) newLocations( + desired *resource, +) ([]*svcsdk.CidrCollectionChange, error) { + if desired == nil { + return nil, nil + } + + locations := desired.ko.Spec.Locations + + var newLocations []*svcsdk.CidrCollectionChange + // Add locations to Changes using the PUT action + if len(locations) > 0 { + action := "PUT" + for _, rr := range locations { + if rr.LocationName == nil { + return nil, errors.New("InvalidInput: locationName is required in cidrLocation") + } + if rr.CIDRList == nil { + return nil, errors.New("InvalidInput: cidrList is required in cidrLocation") + } + change := &svcsdk.CidrCollectionChange{} + change.SetLocationName(*rr.LocationName) + change.SetCidrList(rr.CIDRList) + change.SetAction(action) + newLocations = append(newLocations, change) + } + } + + return newLocations, nil +} + +// customUpdateRecordSet is the custom implementation for +// RecordSet resource's update operation. +func (rm *resourceManager) customUpdateCidrCollection( + ctx context.Context, + desired *resource, + latest *resource, + delta *ackcompare.Delta, +) (updated *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.customUpdateCidrCollection") + defer func() { + exit(err) + }() + + if delta != nil { + // Do not proceed with update if an immutable field was updated + if immutableFieldChanges := rm.getImmutableFieldChanges(delta); len(immutableFieldChanges) > 0 { + msg := fmt.Sprintf("Immutable Spec fields have been modified: %s", strings.Join(immutableFieldChanges, ",")) + return nil, ackerr.NewTerminalError(fmt.Errorf(msg)) + } + } + + // 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() + + input := &svcsdk.ChangeCidrCollectionInput{} + input.SetId(*desired.ko.Status.Collection.ID) + collectionVersion := *desired.ko.Status.Collection.Version + input.SetCollectionVersion(collectionVersion) + + newLocations, err := rm.newLocations(desired) + if err != nil { + return nil, err + } + oldLocations := rm.oldLocations(latest) + + // First remove old Locations as there is no api call for updating cidr Collection locations + if oldLocations != nil { + input.SetChanges(oldLocations) + _, err = rm.sdkapi.ChangeCidrCollectionWithContext(ctx, input) + rm.metrics.RecordAPICall("UPDATE", "ChangeCidrCollection", err) + if err != nil { + return nil, err + } + collectionVersion = collectionVersion + 1 + input.SetCollectionVersion(collectionVersion) + ko.Status.Collection.Version = &collectionVersion + } + + // Add all new Locations to the cidr Collection + if newLocations != nil { + input.SetChanges(newLocations) + _, err = rm.sdkapi.ChangeCidrCollectionWithContext(ctx, input) + rm.metrics.RecordAPICall("UPDATE", "ChangeCidrCollection", err) + if err != nil { + return nil, err + } + collectionVersion = collectionVersion + 1 + input.SetCollectionVersion(collectionVersion) + ko.Status.Collection.Version = &collectionVersion + } + + return &resource{ko}, nil +} + +// newListCidrLocationsRequestPayload returns SDK-specific struct for the HTTP request +// payload of the ListCidrBlocks API call for the resource +func (rm *resourceManager) newListCidrLocationsRequestPayload( + r *resource, +) (*svcsdk.ListCidrLocationsInput, error) { + res := &svcsdk.ListCidrLocationsInput{} + + return res, nil +} + +// newListCidrBlocksRequestPayload returns SDK-specific struct for the HTTP request +// payload of the ListCidrBlocks API call for the resource +func (rm *resourceManager) newListCidrBlocksRequestPayload( + r *resource, +) (*svcsdk.ListCidrBlocksInput, error) { + res := &svcsdk.ListCidrBlocksInput{} + + return res, nil +} + +// compareCustom is a custom comparison function for comparing Locations Slices +func compareCustom( + delta *ackcompare.Delta, + a *resource, + b *resource, +) { + if len(a.ko.Spec.Locations) != len(b.ko.Spec.Locations) { + delta.Add("Spec.Locations", a.ko.Spec.Locations, b.ko.Spec.Locations) + } else if len(a.ko.Spec.Locations) > 0 { + for index, elem := range a.ko.Spec.Locations { + if elem.LocationName != b.ko.Spec.Locations[index].LocationName { + delta.Add("Spec.Locations", a.ko.Spec.Locations, b.ko.Spec.Locations) + } + if !reflect.DeepEqual(elem.CIDRList, b.ko.Spec.Locations[index].CIDRList) { + delta.Add("Spec.Locations", a.ko.Spec.Locations, b.ko.Spec.Locations) + } + } + } +} diff --git a/pkg/resource/cidr_collection/identifiers.go b/pkg/resource/cidr_collection/identifiers.go new file mode 100644 index 0000000..99f30d8 --- /dev/null +++ b/pkg/resource/cidr_collection/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 cidr_collection + +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/cidr_collection/manager.go b/pkg/resource/cidr_collection/manager.go new file mode 100644 index 0000000..1631b0a --- /dev/null +++ b/pkg/resource/cidr_collection/manager.go @@ -0,0 +1,350 @@ +// 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 cidr_collection + +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/aws/session" + svcsdk "github.com/aws/aws-sdk-go/service/route53" + svcsdkapi "github.com/aws/aws-sdk-go/service/route53/route53iface" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + + svcapitypes "github.com/aws-controllers-k8s/route53-controller/apis/v1alpha1" +) + +var ( + _ = ackutil.InStrings + _ = acktags.NewTags() + _ = ackrt.MissingImageTagValue + _ = svcapitypes.CIDRCollection{} +) + +// +kubebuilder:rbac:groups=route53.services.k8s.aws,resources=cidrcollections,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=route53.services.k8s.aws,resources=cidrcollections/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 + // 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 + // sess is the AWS SDK Session object used to communicate with the backend + // AWS service API + sess *session.Session + // sdk is a pointer to the AWS service API interface exposed by the + // aws-sdk-go/services/{alias}/{alias}iface package. + sdkapi svcsdkapi.Route53API +} + +// 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) + 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:route53:%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 { + + return nil +} + +// newResourceManager returns a new struct implementing +// acktypes.AWSResourceManager +func newResourceManager( + cfg ackcfg.Config, + log logr.Logger, + metrics *ackmetrics.Metrics, + rr acktypes.Reconciler, + sess *session.Session, + id ackv1alpha1.AWSAccountID, + region ackv1alpha1.AWSRegion, +) (*resourceManager, error) { + return &resourceManager{ + cfg: cfg, + log: log, + metrics: metrics, + rr: rr, + awsAccountID: id, + awsRegion: region, + sess: sess, + sdkapi: svcsdk.New(sess), + }, 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/cidr_collection/manager_factory.go b/pkg/resource/cidr_collection/manager_factory.go new file mode 100644 index 0000000..22ceff9 --- /dev/null +++ b/pkg/resource/cidr_collection/manager_factory.go @@ -0,0 +1,96 @@ +// 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 cidr_collection + +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/aws/session" + "github.com/go-logr/logr" + + svcresource "github.com/aws-controllers-k8s/route53-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, + log logr.Logger, + metrics *ackmetrics.Metrics, + rr acktypes.Reconciler, + sess *session.Session, + id ackv1alpha1.AWSAccountID, + region ackv1alpha1.AWSRegion, +) (acktypes.AWSResourceManager, error) { + rmId := fmt.Sprintf("%s/%s", id, region) + f.RLock() + rm, found := f.rmCache[rmId] + f.RUnlock() + + if found { + return rm, nil + } + + f.Lock() + defer f.Unlock() + + rm, err := newResourceManager(cfg, log, metrics, rr, sess, 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/cidr_collection/references.go b/pkg/resource/cidr_collection/references.go new file mode 100644 index 0000000..9417934 --- /dev/null +++ b/pkg/resource/cidr_collection/references.go @@ -0,0 +1,56 @@ +// 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 cidr_collection + +import ( + "context" + "sigs.k8s.io/controller-runtime/pkg/client" + + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + + svcapitypes "github.com/aws-controllers-k8s/route53-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.CIDRCollection) error { + return nil +} diff --git a/pkg/resource/cidr_collection/resource.go b/pkg/resource/cidr_collection/resource.go new file mode 100644 index 0000000..6d363cc --- /dev/null +++ b/pkg/resource/cidr_collection/resource.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 cidr_collection + +import ( + 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/route53-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.CIDRCollection +} + +// 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.Spec.Name = &identifier.NameOrID + + 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/cidr_collection/sdk.go b/pkg/resource/cidr_collection/sdk.go new file mode 100644 index 0000000..e40c515 --- /dev/null +++ b/pkg/resource/cidr_collection/sdk.go @@ -0,0 +1,475 @@ +// 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 cidr_collection + +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/aws" + svcsdk "github.com/aws/aws-sdk-go/service/route53" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + svcapitypes "github.com/aws-controllers-k8s/route53-controller/apis/v1alpha1" +) + +// Hack to avoid import errors during build... +var ( + _ = &metav1.Time{} + _ = strings.ToLower("") + _ = &aws.JSONValue{} + _ = &svcsdk.Route53{} + _ = &svcapitypes.CIDRCollection{} + _ = ackv1alpha1.AWSAccountID("") + _ = &ackerr.NotFound + _ = &ackcondition.NotManagedMessage + _ = &reflect.Value{} + _ = fmt.Sprintf("") + _ = &ackrequeue.NoRequeue{} +) + +// 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.requiredFieldsMissingFromReadManyInput(r) { + return nil, ackerr.NotFound + } + + input, err := rm.newListRequestPayload(r) + if err != nil { + return nil, err + } + var resp *svcsdk.ListCidrCollectionsOutput + resp, err = rm.sdkapi.ListCidrCollectionsWithContext(ctx, input) + rm.metrics.RecordAPICall("READ_MANY", "ListCidrCollections", err) + if err != nil { + if awsErr, ok := ackerr.AWSError(err); ok && awsErr.Code() == "UNKNOWN" { + 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() + + // ListCidrCollections does not result in an exact match of relevant cidr locations as + // there is no filter option, so we need to filter here + // Furthermore we need different api calls to get the cidrLocations and cidrBlocks associated with the cidr Collection + var resourceId string + if ko.Status.Collection != nil { + if ko.Status.Collection.ID != nil { + resourceId = *ko.Status.Collection.ID + } + } + + var cidrCollections []*svcsdk.CollectionSummary + for _, elem := range resp.CidrCollections { + elemId := *elem.Id + if elemId == resourceId { + cidrCollections = append(cidrCollections, elem) + if ko.Status.Collection == nil { + ko.Status.Collection = &svcapitypes.CIDRCollection_SDK{} + } + ko.Status.Collection.ARN = elem.Arn + ko.Status.Collection.ID = elem.Id + ko.Status.Collection.Name = elem.Name + ko.Status.Collection.Version = elem.Version + } + } + + if len(cidrCollections) == 0 { + return nil, ackerr.NotFound + } + + inputListCidrLocations, err := rm.newListCidrLocationsRequestPayload(r) + if err != nil { + return nil, err + } + inputListCidrLocations.SetCollectionId(*ko.Status.Collection.ID) + respListCidrLocations, err := rm.sdkapi.ListCidrLocationsWithContext(ctx, inputListCidrLocations) + rm.metrics.RecordAPICall("READ_MANY", "ListCidrLocations", err) + if err != nil { + if awsErr, ok := ackerr.AWSError(err); ok && awsErr.Code() == "UNKNOWN" { + return nil, ackerr.NotFound + } + return nil, err + } + + var locations []*svcapitypes.CIDRCollectionChange + for _, elemCidrLocation := range respListCidrLocations.CidrLocations { + location := svcapitypes.CIDRCollectionChange{} + location.LocationName = elemCidrLocation.LocationName + + inputListCidrBlocks, err := rm.newListCidrBlocksRequestPayload(r) + if err != nil { + return nil, err + } + inputListCidrBlocks.SetCollectionId(*ko.Status.Collection.ID) + inputListCidrBlocks.SetLocationName(*elemCidrLocation.LocationName) + respListCidrBlocks, err := rm.sdkapi.ListCidrBlocksWithContext(ctx, inputListCidrBlocks) + rm.metrics.RecordAPICall("READ_MANY", "ListCidrBlocks", err) + if err != nil { + if awsErr, ok := ackerr.AWSError(err); ok && awsErr.Code() == "UNKNOWN" { + return nil, ackerr.NotFound + } + return nil, err + } + + var cidrList []*string + for _, elemcidrBlock := range respListCidrBlocks.CidrBlocks { + cidrList = append(cidrList, elemcidrBlock.CidrBlock) + } + location.CIDRList = cidrList + locations = append(locations, &location) + } + if ko.Spec.Locations == nil { + ko.Spec.Locations = []*svcapitypes.CIDRCollectionChange{} + } + ko.Spec.Locations = locations + + resp.CidrCollections = cidrCollections + + found := false + for _, elem := range resp.CidrCollections { + if elem.Arn != nil { + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + tmpARN := ackv1alpha1.AWSResourceName(*elem.Arn) + ko.Status.ACKResourceMetadata.ARN = &tmpARN + } + if elem.Name != nil { + ko.Spec.Name = elem.Name + } else { + ko.Spec.Name = nil + } + found = true + break + } + if !found { + return nil, ackerr.NotFound + } + + rm.setStatusDefaults(ko) + return &resource{ko}, nil +} + +// requiredFieldsMissingFromReadManyInput returns true if there are any fields +// for the ReadMany Input shape that are required but not present in the +// resource's Spec or Status +func (rm *resourceManager) requiredFieldsMissingFromReadManyInput( + r *resource, +) bool { + return false +} + +// newListRequestPayload returns SDK-specific struct for the HTTP request +// payload of the List API call for the resource +func (rm *resourceManager) newListRequestPayload( + r *resource, +) (*svcsdk.ListCidrCollectionsInput, error) { + res := &svcsdk.ListCidrCollectionsInput{} + + 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 + } + // You must use a unique CallerReference string every time you submit a + // CreateHealthCheck request. CallerReference can be any unique string, for + // example, a date/timestamp. + // TODO: Name is not sufficient, since a failed request cannot be retried. + // We might need to import the `time` package into `sdk.go` + callerReference := getCallerReference() + input.SetCallerReference(callerReference) + + var resp *svcsdk.CreateCidrCollectionOutput + _ = resp + resp, err = rm.sdkapi.CreateCidrCollectionWithContext(ctx, input) + rm.metrics.RecordAPICall("CREATE", "CreateCidrCollection", err) + if err != nil { + return nil, err + } + // Merge in the information we read from the API call above to the copy of + // the original Kubernetes object we passed to the function + ko := desired.ko.DeepCopy() + + if resp.Collection != nil { + f0 := &svcapitypes.CIDRCollection_SDK{} + if resp.Collection.Arn != nil { + f0.ARN = resp.Collection.Arn + } + if resp.Collection.Id != nil { + f0.ID = resp.Collection.Id + } + if resp.Collection.Name != nil { + f0.Name = resp.Collection.Name + } + if resp.Collection.Version != nil { + f0.Version = resp.Collection.Version + } + ko.Status.Collection = f0 + } else { + ko.Status.Collection = nil + } + if resp.Location != nil { + ko.Status.Location = resp.Location + } else { + ko.Status.Location = nil + } + + rm.setStatusDefaults(ko) + ko.Status.CallerReference = &callerReference + updatedResource, err := rm.customUpdateCidrCollection(ctx, &resource{ko}, nil, nil) + if err != nil { + return nil, err + } + ko = updatedResource.ko.DeepCopy() + + 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.CreateCidrCollectionInput, error) { + res := &svcsdk.CreateCidrCollectionInput{} + + if r.ko.Spec.Name != nil { + res.SetName(*r.ko.Spec.Name) + } + + 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 rm.customUpdateCidrCollection(ctx, desired, latest, delta) +} + +// 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 + } + ko := r.ko.DeepCopy() + ko.Spec.Locations = nil + desired := &resource{ko} + _, err = rm.customUpdateCidrCollection(ctx, desired, r, nil) + if err != nil { + return nil, err + } + input.SetId(*r.ko.Status.Collection.ID) + + var resp *svcsdk.DeleteCidrCollectionOutput + _ = resp + resp, err = rm.sdkapi.DeleteCidrCollectionWithContext(ctx, input) + rm.metrics.RecordAPICall("DELETE", "DeleteCidrCollection", 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.DeleteCidrCollectionInput, error) { + res := &svcsdk.DeleteCidrCollectionInput{} + + return res, nil +} + +// setStatusDefaults sets default properties into supplied custom resource +func (rm *resourceManager) setStatusDefaults( + ko *svcapitypes.CIDRCollection, +) { + 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 + } + awsErr, ok := ackerr.AWSError(err) + if !ok { + return false + } + switch awsErr.Code() { + case "InvalidInput", + "CidrCollectionAlreadyExists", + "CidrCollectionInUse", + "CidrCollectionVersionMismatch", + "NoSuchCidrCollection", + "CidrBlockInUse": + return true + default: + return false + } +} + +// getImmutableFieldChanges returns list of immutable fields from the +func (rm *resourceManager) getImmutableFieldChanges( + delta *ackcompare.Delta, +) []string { + var fields []string + if delta.DifferentAt("Spec.Name") { + fields = append(fields, "Name") + } + + return fields +} diff --git a/templates/hooks/cidr_collection/sdk_create_post_build_request.go.tpl b/templates/hooks/cidr_collection/sdk_create_post_build_request.go.tpl new file mode 100644 index 0000000..7920240 --- /dev/null +++ b/templates/hooks/cidr_collection/sdk_create_post_build_request.go.tpl @@ -0,0 +1,7 @@ + // You must use a unique CallerReference string every time you submit a + // CreateHealthCheck request. CallerReference can be any unique string, for + // example, a date/timestamp. + // TODO: Name is not sufficient, since a failed request cannot be retried. + // We might need to import the `time` package into `sdk.go` + callerReference := getCallerReference() + input.SetCallerReference(callerReference) diff --git a/templates/hooks/cidr_collection/sdk_create_post_set_output.go.tpl b/templates/hooks/cidr_collection/sdk_create_post_set_output.go.tpl new file mode 100644 index 0000000..e1cb844 --- /dev/null +++ b/templates/hooks/cidr_collection/sdk_create_post_set_output.go.tpl @@ -0,0 +1,6 @@ + ko.Status.CallerReference = &callerReference + updatedResource, err := rm.customUpdateCidrCollection(ctx, &resource{ko}, nil, nil) + if err != nil { + return nil, err + } + ko = updatedResource.ko.DeepCopy() diff --git a/templates/hooks/cidr_collection/sdk_delete_post_build_request.go.tpl b/templates/hooks/cidr_collection/sdk_delete_post_build_request.go.tpl new file mode 100644 index 0000000..c86c98b --- /dev/null +++ b/templates/hooks/cidr_collection/sdk_delete_post_build_request.go.tpl @@ -0,0 +1,8 @@ + ko := r.ko.DeepCopy() + ko.Spec.Locations = nil + desired := &resource{ko} + _, err = rm.customUpdateCidrCollection(ctx, desired, r, nil) + if err != nil { + return nil, err + } + input.SetId(*r.ko.Status.Collection.ID) diff --git a/templates/hooks/cidr_collection/sdk_read_many_pre_set_output.go.tpl b/templates/hooks/cidr_collection/sdk_read_many_pre_set_output.go.tpl new file mode 100644 index 0000000..c3d63e7 --- /dev/null +++ b/templates/hooks/cidr_collection/sdk_read_many_pre_set_output.go.tpl @@ -0,0 +1,77 @@ + + // ListCidrCollections does not result in an exact match of relevant cidr locations as + // there is no filter option, so we need to filter here + // Furthermore we need different api calls to get the cidrLocations and cidrBlocks associated with the cidr Collection + var resourceId string + if ko.Status.Collection != nil { + if ko.Status.Collection.ID != nil { + resourceId = *ko.Status.Collection.ID + } + } + + var cidrCollections []*svcsdk.CollectionSummary + for _, elem := range resp.CidrCollections { + elemId := *elem.Id + if elemId == resourceId { + cidrCollections = append(cidrCollections, elem) + if ko.Status.Collection == nil { + ko.Status.Collection = &svcapitypes.CIDRCollection_SDK{} + } + ko.Status.Collection.ARN = elem.Arn + ko.Status.Collection.ID = elem.Id + ko.Status.Collection.Name = elem.Name + ko.Status.Collection.Version = elem.Version + } + } + + if len(cidrCollections) == 0 { + return nil, ackerr.NotFound + } + + inputListCidrLocations, err := rm.newListCidrLocationsRequestPayload(r) + if err != nil { + return nil, err + } + inputListCidrLocations.SetCollectionId(*ko.Status.Collection.ID) + respListCidrLocations, err := rm.sdkapi.ListCidrLocationsWithContext(ctx, inputListCidrLocations) + rm.metrics.RecordAPICall("READ_MANY", "ListCidrLocations", err) + if err != nil { + if awsErr, ok := ackerr.AWSError(err); ok && awsErr.Code() == "UNKNOWN" { + return nil, ackerr.NotFound + } + return nil, err + } + + var locations []*svcapitypes.CIDRCollectionChange + for _, elemCidrLocation := range respListCidrLocations.CidrLocations { + location := svcapitypes.CIDRCollectionChange{} + location.LocationName = elemCidrLocation.LocationName + + inputListCidrBlocks, err := rm.newListCidrBlocksRequestPayload(r) + if err != nil { + return nil, err + } + inputListCidrBlocks.SetCollectionId(*ko.Status.Collection.ID) + inputListCidrBlocks.SetLocationName(*elemCidrLocation.LocationName) + respListCidrBlocks, err := rm.sdkapi.ListCidrBlocksWithContext(ctx, inputListCidrBlocks) + rm.metrics.RecordAPICall("READ_MANY", "ListCidrBlocks", err) + if err != nil { + if awsErr, ok := ackerr.AWSError(err); ok && awsErr.Code() == "UNKNOWN" { + return nil, ackerr.NotFound + } + return nil, err + } + + var cidrList []*string + for _, elemcidrBlock := range respListCidrBlocks.CidrBlocks { + cidrList = append(cidrList, elemcidrBlock.CidrBlock) + } + location.CIDRList = cidrList + locations = append(locations, &location) + } + if ko.Spec.Locations == nil { + ko.Spec.Locations = []*svcapitypes.CIDRCollectionChange{} + } + ko.Spec.Locations = locations + + resp.CidrCollections = cidrCollections diff --git a/test/e2e/resources/cidr_collection.yaml b/test/e2e/resources/cidr_collection.yaml new file mode 100644 index 0000000..7650e54 --- /dev/null +++ b/test/e2e/resources/cidr_collection.yaml @@ -0,0 +1,15 @@ +apiVersion: route53.services.k8s.aws/v1alpha1 +kind: CIDRCollection +metadata: + name: $CIDR_COLLECTION_NAME +spec: + name: $CIDR_COLLECTION_NAME + locations: + - locationName: location-1 + cidrList: + - 192.168.0.0/24 + - 192.168.1.0/24 + - locationName: location-2 + cidrList: + - 192.168.10.0/24 + - 192.168.11.0/24 diff --git a/test/e2e/tests/helper.py b/test/e2e/tests/helper.py index 746eebc..29f0ce2 100644 --- a/test/e2e/tests/helper.py +++ b/test/e2e/tests/helper.py @@ -31,6 +31,35 @@ def list_tags_for_resources(self, resource_id: str, resource_type: str): except self.route53_client.exceptions.ClientError as e: return None + def assert_cidr_collection(self, cr, exists=True): + res = None + found = False + cidr_collection_id = cr["status"].get("collection", None).get("id", None) + cidr_collection_name = cr["spec"].get("name", None) + cidr_collection_locations = cr["spec"].get("locations", None) + + if cidr_collection_id is None: + return + + try: + res = self.route53_client.list_cidr_collections() + res_cidr_collections = [cidr_collection for cidr_collection in res["CidrCollections"] if cidr_collection["Id"] == cidr_collection_id] + found = len(res_cidr_collections) == 1 + except self.route53_client.exceptions.ClientError: + pass + assert found is exists + if exists and cidr_collection_id and cidr_collection_name: + assert cidr_collection_name in str(res_cidr_collections) + + try: + res = self.route53_client.list_cidr_blocks(CollectionId=cidr_collection_id) + except self.route53_client.exceptions.ClientError: + pass + if exists and cidr_collection_locations is not None: + for location in cidr_collection_locations: + assert location["locationName"] in str(res) + assert location["cidrList"][0] in str(res) + def assert_hosted_zone(self, hosted_zone_id: str, exists=True): found = False try: diff --git a/test/e2e/tests/test_cidr_collection.py b/test/e2e/tests/test_cidr_collection.py new file mode 100644 index 0000000..7ba008c --- /dev/null +++ b/test/e2e/tests/test_cidr_collection.py @@ -0,0 +1,93 @@ +# 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. + +"""Integration tests for the Route53 RecordSet resource +""" + +import pytest +import time + +from acktest.k8s import resource as k8s +from acktest.resources import random_suffix_name +from e2e import service_marker, get_route53_resource, create_route53_resource, delete_route53_resource, patch_route53_resource +from e2e.replacement_values import REPLACEMENT_VALUES +from e2e.tests.helper import Route53Validator + +CREATE_WAIT_AFTER_SECONDS = 5 +UPDATE_WAIT_AFTER_SECONDS = 5 +DELETE_WAIT_AFTER_SECONDS = 10 + +@pytest.fixture +def cidr_collection(request): + cidr_collection_name = random_suffix_name("cidr-collection", 32) + + replacements = REPLACEMENT_VALUES.copy() + replacements["CIDR_COLLECTION_NAME"] = cidr_collection_name + + ref, cr = create_route53_resource( + "cidrcollections", + cidr_collection_name, + "cidr_collection", + replacements, + ) + + yield ref, cr + + delete_route53_resource(ref) + +def patch_cidr_collection(ref): + updates = { + "spec": { + "locations": [{ + "locationName": "location-new", + "cidrList": ["192.168.100.0/24"], + }] + } + } + patch_route53_resource(ref, updates) + return get_route53_resource(ref) + + +@service_marker +@pytest.mark.canary +class TestCidrCollection: + def test_crud(self, route53_client, cidr_collection): + ref, cr = cidr_collection + + cidr_collection_name = cr["spec"]["name"] + + assert cidr_collection_name + + time.sleep(CREATE_WAIT_AFTER_SECONDS) + + # Check cidr collection exists in AWS + route53_validator = Route53Validator(route53_client) + route53_validator.assert_cidr_collection(cr) + + # Update cidr collection resource + updated = patch_cidr_collection(ref) + assert updated["spec"]["locations"] != cr["spec"]["locations"] + + time.sleep(UPDATE_WAIT_AFTER_SECONDS) + + # Check cidr collection has been updated in AWS + route53_validator.assert_cidr_collection(updated) + + # Delete k8s resource + _, deleted = k8s.delete_custom_resource(ref) + assert deleted is True + + time.sleep(DELETE_WAIT_AFTER_SECONDS) + + # Check cidr collection no longer exists in AWS + route53_validator.assert_cidr_collection(cr, exists=False) From 2d9f8d3736f25dc0acf9976d035036128b5226cd Mon Sep 17 00:00:00 2001 From: Guido van der Hart Date: Fri, 23 Aug 2024 15:00:44 +0200 Subject: [PATCH 2/2] Add check to not update locations if nothing changed --- apis/v1alpha1/ack-generate-metadata.yaml | 8 ++++---- pkg/resource/cidr_collection/hooks.go | 5 +++++ pkg/resource/cidr_collection/manager_factory.go | 6 +++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index 510144c..b5f7507 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,13 +1,13 @@ ack_generate_info: - build_date: "2024-08-29T17:03:54Z" + build_date: "2024-09-10T12:17:23Z" build_hash: f8f98563404066ac3340db0a049d2e530e5c51cc - go_version: go1.22.5 + go_version: go1.23.1 version: v0.38.1 -api_directory_checksum: 78fb7fd24a85da24b8de6246cad67ff3fb6598f8 +api_directory_checksum: fcc2a039096955dfeb2eb1dbd94def23713aeb03 api_version: v1alpha1 aws_sdk_go_version: v1.49.0 generator_config_info: - file_checksum: a841c574fab5ddf8a35ade3ba90e7f6713255d7e + file_checksum: aaca78853a15611ad9b6b6abec9a4e8989b103a9 original_file_name: generator.yaml last_modification: reason: API generation diff --git a/pkg/resource/cidr_collection/hooks.go b/pkg/resource/cidr_collection/hooks.go index 2a52752..49b7b4b 100644 --- a/pkg/resource/cidr_collection/hooks.go +++ b/pkg/resource/cidr_collection/hooks.go @@ -115,6 +115,11 @@ func (rm *resourceManager) customUpdateCidrCollection( } oldLocations := rm.oldLocations(latest) + // Do not update Locations if nothing changed + if reflect.DeepEqual(newLocations, oldLocations) { + return &resource{ko}, nil + } + // First remove old Locations as there is no api call for updating cidr Collection locations if oldLocations != nil { input.SetChanges(oldLocations) diff --git a/pkg/resource/cidr_collection/manager_factory.go b/pkg/resource/cidr_collection/manager_factory.go index 22ceff9..f3642f7 100644 --- a/pkg/resource/cidr_collection/manager_factory.go +++ b/pkg/resource/cidr_collection/manager_factory.go @@ -53,8 +53,12 @@ func (f *resourceManagerFactory) ManagerFor( sess *session.Session, id ackv1alpha1.AWSAccountID, region ackv1alpha1.AWSRegion, + roleARN ackv1alpha1.AWSResourceName, ) (acktypes.AWSResourceManager, error) { - rmId := fmt.Sprintf("%s/%s", id, region) + // 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()