diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index d39b800..3e47325 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,13 +1,13 @@ ack_generate_info: - build_date: "2022-04-06T19:12:56Z" + build_date: "2022-04-06T21:44:24Z" build_hash: fc5620cf5fde243ee5124324d26bb7e952049bf2 go_version: go1.17.6 - version: v0.18.3 -api_directory_checksum: c2f6d6769a7f620358ba97b8b9c3ef8c80934448 + version: v0.18.3-dirty +api_directory_checksum: f7e376daef64d0c299eb1338df3bd097290b334a api_version: v1alpha1 aws_sdk_go_version: v1.42.29 generator_config_info: - file_checksum: 2a0a05a38b0ccfb3d749097e815c4533ba9ec868 + file_checksum: 7ce497a1a103652e30a5d7c843463da1c834fd3d original_file_name: generator.yaml last_modification: reason: API generation diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index 43be752..8583d5c 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -1,6 +1,7 @@ ignore: - resource_names: - - Snapshot + field_paths: + - DescribeSnapshotsInput.ClusterName + - DescribeSnapshotsInput.Source resources: Cluster: exceptions: @@ -182,6 +183,50 @@ resources: - path: Status.Status in: - active + Snapshot: + exceptions: + errors: + 404: + code: SnapshotNotFoundFault + terminal_codes: + - InvalidParameterCombinationException + - SnapshotAlreadyExistsFault + - SnapshotQuotaExceededFault + - TagQuotaPerResourceExceeded + - InvalidParameterValueException + - InvalidParameter + fields: + SourceSnapshotName: + from: + operation: CopySnapshot + path: SourceSnapshotName + ClusterName: + is_primary_key: false + from: + operation: CreateSnapshot + path: ClusterName + hooks: + sdk_create_pre_build_request: + template_path: hooks/snapshot/sdk_create_pre_build_request.go.tpl + sdk_create_post_set_output: + template_path: hooks/snapshot/sdk_create_post_set_output.go.tpl + sdk_read_many_post_set_output: + template_path: hooks/snapshot/sdk_read_many_post_set_output.go.tpl + sdk_delete_pre_build_request: + template_path: hooks/snapshot/sdk_delete_pre_build_request.go.tpl + sdk_delete_post_request: + template_path: hooks/snapshot/sdk_delete_post_request.go.tpl + renames: + operations: + CreateSnapshot: + input_fields: + SnapshotName: Name + DeleteSnapshot: + input_fields: + SnapshotName: Name + DescribeSnapshots: + input_fields: + SnapshotName: Name ParameterGroup: exceptions: errors: diff --git a/apis/v1alpha1/snapshot.go b/apis/v1alpha1/snapshot.go new file mode 100644 index 0000000..d9ab730 --- /dev/null +++ b/apis/v1alpha1/snapshot.go @@ -0,0 +1,88 @@ +// 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" +) + +// SnapshotSpec defines the desired state of Snapshot. +// +// Represents a copy of an entire cluster as of the time when the snapshot was +// taken. +type SnapshotSpec struct { + // The snapshot is created from this cluster. + ClusterName *string `json:"clusterName,omitempty"` + // The ID of the KMS key used to encrypt the snapshot. + KMSKeyID *string `json:"kmsKeyID,omitempty"` + // A name for the snapshot being created. + // +kubebuilder:validation:Required + Name *string `json:"name"` + // The name of an existing snapshot from which to make a copy. + SourceSnapshotName *string `json:"sourceSnapshotName,omitempty"` + // A list of tags to be added to this resource. A tag is a key-value pair. A + // tag key must be accompanied by a tag value, although null is accepted. + Tags []*Tag `json:"tags,omitempty"` +} + +// SnapshotStatus defines the observed state of Snapshot +type SnapshotStatus 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"` + // The configuration of the cluster from which the snapshot was taken + // +kubebuilder:validation:Optional + ClusterConfiguration *ClusterConfiguration `json:"clusterConfiguration,omitempty"` + // Indicates whether the snapshot is from an automatic backup (automated) or + // was created manually (manual). + // +kubebuilder:validation:Optional + Source *string `json:"source,omitempty"` + // The status of the snapshot. Valid values: creating | available | restoring + // | copying | deleting. + // +kubebuilder:validation:Optional + Status *string `json:"status,omitempty"` +} + +// Snapshot is the Schema for the Snapshots API +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type Snapshot struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec SnapshotSpec `json:"spec,omitempty"` + Status SnapshotStatus `json:"status,omitempty"` +} + +// SnapshotList contains a list of Snapshot +// +kubebuilder:object:root=true +type SnapshotList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Snapshot `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Snapshot{}, &SnapshotList{}) +} diff --git a/apis/v1alpha1/types.go b/apis/v1alpha1/types.go index 6c374c0..5f5f2cc 100644 --- a/apis/v1alpha1/types.go +++ b/apis/v1alpha1/types.go @@ -74,19 +74,20 @@ type AvailabilityZone struct { // A list of cluster configuration options. type ClusterConfiguration struct { - Description *string `json:"description,omitempty"` - EngineVersion *string `json:"engineVersion,omitempty"` - MaintenanceWindow *string `json:"maintenanceWindow,omitempty"` - Name *string `json:"name,omitempty"` - NodeType *string `json:"nodeType,omitempty"` - NumShards *int64 `json:"numShards,omitempty"` - ParameterGroupName *string `json:"parameterGroupName,omitempty"` - Port *int64 `json:"port,omitempty"` - SnapshotRetentionLimit *int64 `json:"snapshotRetentionLimit,omitempty"` - SnapshotWindow *string `json:"snapshotWindow,omitempty"` - SubnetGroupName *string `json:"subnetGroupName,omitempty"` - TopicARN *string `json:"topicARN,omitempty"` - VPCID *string `json:"vpcID,omitempty"` + Description *string `json:"description,omitempty"` + EngineVersion *string `json:"engineVersion,omitempty"` + MaintenanceWindow *string `json:"maintenanceWindow,omitempty"` + Name *string `json:"name,omitempty"` + NodeType *string `json:"nodeType,omitempty"` + NumShards *int64 `json:"numShards,omitempty"` + ParameterGroupName *string `json:"parameterGroupName,omitempty"` + Port *int64 `json:"port,omitempty"` + Shards []*ShardDetail `json:"shards,omitempty"` + SnapshotRetentionLimit *int64 `json:"snapshotRetentionLimit,omitempty"` + SnapshotWindow *string `json:"snapshotWindow,omitempty"` + SubnetGroupName *string `json:"subnetGroupName,omitempty"` + TopicARN *string `json:"topicARN,omitempty"` + VPCID *string `json:"vpcID,omitempty"` } // A list of updates being applied to the cluster @@ -261,9 +262,12 @@ type ShardConfigurationRequest struct { // Provides details of a shard in a snapshot type ShardDetail struct { - Name *string `json:"name,omitempty"` - Size *string `json:"size,omitempty"` - SnapshotCreationTime *metav1.Time `json:"snapshotCreationTime,omitempty"` + // Shard configuration options. Each shard configuration has the following: + // Slots and ReplicaCount. + Configuration *ShardConfiguration `json:"configuration,omitempty"` + Name *string `json:"name,omitempty"` + Size *string `json:"size,omitempty"` + SnapshotCreationTime *metav1.Time `json:"snapshotCreationTime,omitempty"` } // Represents the progress of an online resharding operation. @@ -273,12 +277,14 @@ type SlotMigration struct { // Represents a copy of an entire cluster as of the time when the snapshot was // taken. -type Snapshot struct { - ARN *string `json:"arn,omitempty"` - KMSKeyID *string `json:"kmsKeyID,omitempty"` - Name *string `json:"name,omitempty"` - Source *string `json:"source,omitempty"` - Status *string `json:"status,omitempty"` +type Snapshot_SDK struct { + ARN *string `json:"arn,omitempty"` + // A list of cluster configuration options. + ClusterConfiguration *ClusterConfiguration `json:"clusterConfiguration,omitempty"` + KMSKeyID *string `json:"kmsKeyID,omitempty"` + Name *string `json:"name,omitempty"` + Source *string `json:"source,omitempty"` + Status *string `json:"status,omitempty"` } // Represents the subnet associated with a cluster. This parameter refers to diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 308a005..fdf8256 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -448,6 +448,17 @@ func (in *ClusterConfiguration) DeepCopyInto(out *ClusterConfiguration) { *out = new(int64) **out = **in } + if in.Shards != nil { + in, out := &in.Shards, &out.Shards + *out = make([]*ShardDetail, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(ShardDetail) + (*in).DeepCopyInto(*out) + } + } + } if in.SnapshotRetentionLimit != nil { in, out := &in.SnapshotRetentionLimit, &out.SnapshotRetentionLimit *out = new(int64) @@ -1622,6 +1633,11 @@ func (in *ShardConfigurationRequest) DeepCopy() *ShardConfigurationRequest { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ShardDetail) DeepCopyInto(out *ShardDetail) { *out = *in + if in.Configuration != nil { + in, out := &in.Configuration, &out.Configuration + *out = new(ShardConfiguration) + (*in).DeepCopyInto(*out) + } if in.Name != nil { in, out := &in.Name, &out.Name *out = new(string) @@ -1670,12 +1686,168 @@ func (in *SlotMigration) DeepCopy() *SlotMigration { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Snapshot) DeepCopyInto(out *Snapshot) { + *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 Snapshot. +func (in *Snapshot) DeepCopy() *Snapshot { + if in == nil { + return nil + } + out := new(Snapshot) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Snapshot) 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 *SnapshotList) DeepCopyInto(out *SnapshotList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Snapshot, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotList. +func (in *SnapshotList) DeepCopy() *SnapshotList { + if in == nil { + return nil + } + out := new(SnapshotList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SnapshotList) 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 *SnapshotSpec) DeepCopyInto(out *SnapshotSpec) { + *out = *in + if in.ClusterName != nil { + in, out := &in.ClusterName, &out.ClusterName + *out = new(string) + **out = **in + } + if in.KMSKeyID != nil { + in, out := &in.KMSKeyID, &out.KMSKeyID + *out = new(string) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.SourceSnapshotName != nil { + in, out := &in.SourceSnapshotName, &out.SourceSnapshotName + *out = new(string) + **out = **in + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make([]*Tag, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Tag) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotSpec. +func (in *SnapshotSpec) DeepCopy() *SnapshotSpec { + if in == nil { + return nil + } + out := new(SnapshotSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SnapshotStatus) DeepCopyInto(out *SnapshotStatus) { + *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.ClusterConfiguration != nil { + in, out := &in.ClusterConfiguration, &out.ClusterConfiguration + *out = new(ClusterConfiguration) + (*in).DeepCopyInto(*out) + } + if in.Source != nil { + in, out := &in.Source, &out.Source + *out = new(string) + **out = **in + } + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotStatus. +func (in *SnapshotStatus) DeepCopy() *SnapshotStatus { + if in == nil { + return nil + } + out := new(SnapshotStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Snapshot_SDK) DeepCopyInto(out *Snapshot_SDK) { *out = *in if in.ARN != nil { in, out := &in.ARN, &out.ARN *out = new(string) **out = **in } + if in.ClusterConfiguration != nil { + in, out := &in.ClusterConfiguration, &out.ClusterConfiguration + *out = new(ClusterConfiguration) + (*in).DeepCopyInto(*out) + } if in.KMSKeyID != nil { in, out := &in.KMSKeyID, &out.KMSKeyID *out = new(string) @@ -1698,12 +1870,12 @@ func (in *Snapshot) DeepCopyInto(out *Snapshot) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Snapshot. -func (in *Snapshot) DeepCopy() *Snapshot { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Snapshot_SDK. +func (in *Snapshot_SDK) DeepCopy() *Snapshot_SDK { if in == nil { return nil } - out := new(Snapshot) + out := new(Snapshot_SDK) in.DeepCopyInto(out) return out } diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 713eeb3..cdf90f2 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -36,6 +36,7 @@ import ( _ "github.com/aws-controllers-k8s/memorydb-controller/pkg/resource/acl" _ "github.com/aws-controllers-k8s/memorydb-controller/pkg/resource/cluster" _ "github.com/aws-controllers-k8s/memorydb-controller/pkg/resource/parameter_group" + _ "github.com/aws-controllers-k8s/memorydb-controller/pkg/resource/snapshot" _ "github.com/aws-controllers-k8s/memorydb-controller/pkg/resource/subnet_group" _ "github.com/aws-controllers-k8s/memorydb-controller/pkg/resource/user" ) diff --git a/config/crd/bases/memorydb.services.k8s.aws_snapshots.yaml b/config/crd/bases/memorydb.services.k8s.aws_snapshots.yaml new file mode 100644 index 0000000..b5fea54 --- /dev/null +++ b/config/crd/bases/memorydb.services.k8s.aws_snapshots.yaml @@ -0,0 +1,214 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: snapshots.memorydb.services.k8s.aws +spec: + group: memorydb.services.k8s.aws + names: + kind: Snapshot + listKind: SnapshotList + plural: snapshots + singular: snapshot + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Snapshot is the Schema for the Snapshots 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: "SnapshotSpec defines the desired state of Snapshot. \n Represents + a copy of an entire cluster as of the time when the snapshot was taken." + properties: + clusterName: + description: The snapshot is created from this cluster. + type: string + kmsKeyID: + description: The ID of the KMS key used to encrypt the snapshot. + type: string + name: + description: A name for the snapshot being created. + type: string + sourceSnapshotName: + description: The name of an existing snapshot from which to make a + copy. + type: string + tags: + description: A list of tags to be added to this resource. A tag is + a key-value pair. A tag key must be accompanied by a tag value, + although null is accepted. + items: + description: A tag that can be added to an MemoryDB resource. Tags + are composed of a Key/Value pair. You can use tags to categorize + and track all your MemoryDB resources. When you add or remove + tags on clusters, those actions will be replicated to all nodes + in the cluster. A tag with a null Value is permitted. For more + information, see Tagging your MemoryDB resources (https://docs.aws.amazon.com/MemoryDB/latest/devguide/tagging-resources.html) + properties: + key: + type: string + value: + type: string + type: object + type: array + required: + - name + type: object + status: + description: SnapshotStatus defines the observed state of Snapshot + 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 + clusterConfiguration: + description: The configuration of the cluster from which the snapshot + was taken + properties: + description: + type: string + engineVersion: + type: string + maintenanceWindow: + type: string + name: + type: string + nodeType: + type: string + numShards: + format: int64 + type: integer + parameterGroupName: + type: string + port: + format: int64 + type: integer + shards: + items: + description: Provides details of a shard in a snapshot + properties: + configuration: + description: 'Shard configuration options. Each shard configuration + has the following: Slots and ReplicaCount.' + properties: + replicaCount: + format: int64 + type: integer + slots: + type: string + type: object + name: + type: string + size: + type: string + snapshotCreationTime: + format: date-time + type: string + type: object + type: array + snapshotRetentionLimit: + format: int64 + type: integer + snapshotWindow: + type: string + subnetGroupName: + type: string + topicARN: + type: string + vpcID: + type: string + 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 + source: + description: Indicates whether the snapshot is from an automatic backup + (automated) or was created manually (manual). + type: string + status: + description: 'The status of the snapshot. Valid values: creating | + available | restoring | copying | deleting.' + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/common/bases/services.k8s.aws_adoptedresources.yaml b/config/crd/common/bases/services.k8s.aws_adoptedresources.yaml index f764dbb..baee855 100644 --- a/config/crd/common/bases/services.k8s.aws_adoptedresources.yaml +++ b/config/crd/common/bases/services.k8s.aws_adoptedresources.yaml @@ -57,8 +57,9 @@ spec: type: string type: object kubernetes: - description: ResourceWithMetadata provides the values necessary to - create a Kubernetes resource and override any of its metadata values. + description: TargetKubernetesResource provides all the values necessary + to identify a given ACK type and override any metadata values when + creating a resource of that type. properties: group: type: string diff --git a/config/crd/common/kustomization.yaml b/config/crd/common/kustomization.yaml index 96349f6..d92ca2e 100644 --- a/config/crd/common/kustomization.yaml +++ b/config/crd/common/kustomization.yaml @@ -1,7 +1,6 @@ -# Code generated in runtime. DO NOT EDIT. +# This file is NOT auto-generated apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - bases/services.k8s.aws_adoptedresources.yaml - - bases/services.k8s.aws_fieldexports.yaml diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 4873663..6be7ced 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,5 +6,6 @@ resources: - bases/memorydb.services.k8s.aws_acls.yaml - bases/memorydb.services.k8s.aws_clusters.yaml - bases/memorydb.services.k8s.aws_parametergroups.yaml + - bases/memorydb.services.k8s.aws_snapshots.yaml - bases/memorydb.services.k8s.aws_subnetgroups.yaml - bases/memorydb.services.k8s.aws_users.yaml diff --git a/config/rbac/cluster-role-controller.yaml b/config/rbac/cluster-role-controller.yaml index e4f10a1..feca94e 100644 --- a/config/rbac/cluster-role-controller.yaml +++ b/config/rbac/cluster-role-controller.yaml @@ -92,6 +92,26 @@ rules: - get - patch - update +- apiGroups: + - memorydb.services.k8s.aws + resources: + - snapshots + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - memorydb.services.k8s.aws + resources: + - snapshots/status + verbs: + - get + - patch + - update - apiGroups: - memorydb.services.k8s.aws resources: diff --git a/config/rbac/role-reader.yaml b/config/rbac/role-reader.yaml index adf8ae4..33aa332 100644 --- a/config/rbac/role-reader.yaml +++ b/config/rbac/role-reader.yaml @@ -12,6 +12,7 @@ rules: - acls - clusters - parametergroups + - snapshots - subnetgroups - users verbs: diff --git a/config/rbac/role-writer.yaml b/config/rbac/role-writer.yaml index 0831ab5..8353432 100644 --- a/config/rbac/role-writer.yaml +++ b/config/rbac/role-writer.yaml @@ -12,6 +12,7 @@ rules: - acls - clusters - parametergroups + - snapshots - subnetgroups - users verbs: @@ -28,6 +29,7 @@ rules: - acls - clusters - parametergroups + - snapshots - subnetgroups - users verbs: diff --git a/generator.yaml b/generator.yaml index 43be752..8583d5c 100644 --- a/generator.yaml +++ b/generator.yaml @@ -1,6 +1,7 @@ ignore: - resource_names: - - Snapshot + field_paths: + - DescribeSnapshotsInput.ClusterName + - DescribeSnapshotsInput.Source resources: Cluster: exceptions: @@ -182,6 +183,50 @@ resources: - path: Status.Status in: - active + Snapshot: + exceptions: + errors: + 404: + code: SnapshotNotFoundFault + terminal_codes: + - InvalidParameterCombinationException + - SnapshotAlreadyExistsFault + - SnapshotQuotaExceededFault + - TagQuotaPerResourceExceeded + - InvalidParameterValueException + - InvalidParameter + fields: + SourceSnapshotName: + from: + operation: CopySnapshot + path: SourceSnapshotName + ClusterName: + is_primary_key: false + from: + operation: CreateSnapshot + path: ClusterName + hooks: + sdk_create_pre_build_request: + template_path: hooks/snapshot/sdk_create_pre_build_request.go.tpl + sdk_create_post_set_output: + template_path: hooks/snapshot/sdk_create_post_set_output.go.tpl + sdk_read_many_post_set_output: + template_path: hooks/snapshot/sdk_read_many_post_set_output.go.tpl + sdk_delete_pre_build_request: + template_path: hooks/snapshot/sdk_delete_pre_build_request.go.tpl + sdk_delete_post_request: + template_path: hooks/snapshot/sdk_delete_post_request.go.tpl + renames: + operations: + CreateSnapshot: + input_fields: + SnapshotName: Name + DeleteSnapshot: + input_fields: + SnapshotName: Name + DescribeSnapshots: + input_fields: + SnapshotName: Name ParameterGroup: exceptions: errors: diff --git a/helm/crds/memorydb.services.k8s.aws_snapshots.yaml b/helm/crds/memorydb.services.k8s.aws_snapshots.yaml new file mode 100644 index 0000000..b5fea54 --- /dev/null +++ b/helm/crds/memorydb.services.k8s.aws_snapshots.yaml @@ -0,0 +1,214 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: snapshots.memorydb.services.k8s.aws +spec: + group: memorydb.services.k8s.aws + names: + kind: Snapshot + listKind: SnapshotList + plural: snapshots + singular: snapshot + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Snapshot is the Schema for the Snapshots 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: "SnapshotSpec defines the desired state of Snapshot. \n Represents + a copy of an entire cluster as of the time when the snapshot was taken." + properties: + clusterName: + description: The snapshot is created from this cluster. + type: string + kmsKeyID: + description: The ID of the KMS key used to encrypt the snapshot. + type: string + name: + description: A name for the snapshot being created. + type: string + sourceSnapshotName: + description: The name of an existing snapshot from which to make a + copy. + type: string + tags: + description: A list of tags to be added to this resource. A tag is + a key-value pair. A tag key must be accompanied by a tag value, + although null is accepted. + items: + description: A tag that can be added to an MemoryDB resource. Tags + are composed of a Key/Value pair. You can use tags to categorize + and track all your MemoryDB resources. When you add or remove + tags on clusters, those actions will be replicated to all nodes + in the cluster. A tag with a null Value is permitted. For more + information, see Tagging your MemoryDB resources (https://docs.aws.amazon.com/MemoryDB/latest/devguide/tagging-resources.html) + properties: + key: + type: string + value: + type: string + type: object + type: array + required: + - name + type: object + status: + description: SnapshotStatus defines the observed state of Snapshot + 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 + clusterConfiguration: + description: The configuration of the cluster from which the snapshot + was taken + properties: + description: + type: string + engineVersion: + type: string + maintenanceWindow: + type: string + name: + type: string + nodeType: + type: string + numShards: + format: int64 + type: integer + parameterGroupName: + type: string + port: + format: int64 + type: integer + shards: + items: + description: Provides details of a shard in a snapshot + properties: + configuration: + description: 'Shard configuration options. Each shard configuration + has the following: Slots and ReplicaCount.' + properties: + replicaCount: + format: int64 + type: integer + slots: + type: string + type: object + name: + type: string + size: + type: string + snapshotCreationTime: + format: date-time + type: string + type: object + type: array + snapshotRetentionLimit: + format: int64 + type: integer + snapshotWindow: + type: string + subnetGroupName: + type: string + topicARN: + type: string + vpcID: + type: string + 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 + source: + description: Indicates whether the snapshot is from an automatic backup + (automated) or was created manually (manual). + type: string + status: + description: 'The status of the snapshot. Valid values: creating | + available | restoring | copying | deleting.' + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/helm/crds/services.k8s.aws_adoptedresources.yaml b/helm/crds/services.k8s.aws_adoptedresources.yaml index f764dbb..baee855 100644 --- a/helm/crds/services.k8s.aws_adoptedresources.yaml +++ b/helm/crds/services.k8s.aws_adoptedresources.yaml @@ -57,8 +57,9 @@ spec: type: string type: object kubernetes: - description: ResourceWithMetadata provides the values necessary to - create a Kubernetes resource and override any of its metadata values. + description: TargetKubernetesResource provides all the values necessary + to identify a given ACK type and override any metadata values when + creating a resource of that type. properties: group: type: string diff --git a/helm/templates/cluster-role-controller.yaml b/helm/templates/cluster-role-controller.yaml index bcec803..9a12eb0 100644 --- a/helm/templates/cluster-role-controller.yaml +++ b/helm/templates/cluster-role-controller.yaml @@ -98,6 +98,26 @@ rules: - get - patch - update +- apiGroups: + - memorydb.services.k8s.aws + resources: + - snapshots + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - memorydb.services.k8s.aws + resources: + - snapshots/status + verbs: + - get + - patch + - update - apiGroups: - memorydb.services.k8s.aws resources: diff --git a/helm/templates/role-reader.yaml b/helm/templates/role-reader.yaml index 47d6bfe..4136b2f 100644 --- a/helm/templates/role-reader.yaml +++ b/helm/templates/role-reader.yaml @@ -12,6 +12,7 @@ rules: - acls - clusters - parametergroups + - snapshots - subnetgroups - users verbs: diff --git a/helm/templates/role-writer.yaml b/helm/templates/role-writer.yaml index 4a3e799..3e257f9 100644 --- a/helm/templates/role-writer.yaml +++ b/helm/templates/role-writer.yaml @@ -15,6 +15,8 @@ rules: - parametergroups + - snapshots + - subnetgroups - users @@ -33,6 +35,7 @@ rules: - acls - clusters - parametergroups + - snapshots - subnetgroups - users verbs: diff --git a/pkg/resource/snapshot/copy_snapshot.go b/pkg/resource/snapshot/copy_snapshot.go new file mode 100644 index 0000000..8bd3d3c --- /dev/null +++ b/pkg/resource/snapshot/copy_snapshot.go @@ -0,0 +1,179 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package snapshot + +import ( + "context" + "errors" + svcapitypes "github.com/aws-controllers-k8s/memorydb-controller/apis/v1alpha1" + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" + svcsdk "github.com/aws/aws-sdk-go/service/memorydb" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (rm *resourceManager) customTryCopySnapshot( + ctx context.Context, + r *resource, +) (*resource, error) { + if r.ko.Spec.SourceSnapshotName == nil { + return nil, nil + } + if r.ko.Spec.ClusterName != nil { + return nil, ackerr.NewTerminalError(errors.New("Cannot specify ClusterName when SourceSnapshotName is specified")) + } + + input, err := rm.newCopySnapshotPayload(r) + if err != nil { + return nil, err + } + + resp, respErr := rm.sdkapi.CopySnapshot(input) + + rm.metrics.RecordAPICall("CREATE", "CopySnapshot", respErr) + if respErr != nil { + return nil, respErr + } + // Merge in the information we read from the API call above to the copy of + // the original Kubernetes object we passed to the function + ko := r.ko.DeepCopy() + + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + if resp.Snapshot.ARN != nil { + arn := ackv1alpha1.AWSResourceName(*resp.Snapshot.ARN) + ko.Status.ACKResourceMetadata.ARN = &arn + } + + if resp.Snapshot.KmsKeyId != nil { + ko.Spec.KMSKeyID = resp.Snapshot.KmsKeyId + } else { + ko.Spec.KMSKeyID = nil + } + + if resp.Snapshot.ClusterConfiguration != nil { + f1 := &svcapitypes.ClusterConfiguration{} + if resp.Snapshot.ClusterConfiguration.Description != nil { + f1.Description = resp.Snapshot.ClusterConfiguration.Description + } + if resp.Snapshot.ClusterConfiguration.EngineVersion != nil { + f1.EngineVersion = resp.Snapshot.ClusterConfiguration.EngineVersion + } + if resp.Snapshot.ClusterConfiguration.MaintenanceWindow != nil { + f1.MaintenanceWindow = resp.Snapshot.ClusterConfiguration.MaintenanceWindow + } + if resp.Snapshot.ClusterConfiguration.Name != nil { + f1.Name = resp.Snapshot.ClusterConfiguration.Name + } + if resp.Snapshot.ClusterConfiguration.NodeType != nil { + f1.NodeType = resp.Snapshot.ClusterConfiguration.NodeType + } + if resp.Snapshot.ClusterConfiguration.NumShards != nil { + f1.NumShards = resp.Snapshot.ClusterConfiguration.NumShards + } + if resp.Snapshot.ClusterConfiguration.ParameterGroupName != nil { + f1.ParameterGroupName = resp.Snapshot.ClusterConfiguration.ParameterGroupName + } + if resp.Snapshot.ClusterConfiguration.Port != nil { + f1.Port = resp.Snapshot.ClusterConfiguration.Port + } + if resp.Snapshot.ClusterConfiguration.Shards != nil { + f1f8 := []*svcapitypes.ShardDetail{} + for _, f1f8iter := range resp.Snapshot.ClusterConfiguration.Shards { + f1f8elem := &svcapitypes.ShardDetail{} + if f1f8iter.Configuration != nil { + f1f8elemf0 := &svcapitypes.ShardConfiguration{} + if f1f8iter.Configuration.ReplicaCount != nil { + f1f8elemf0.ReplicaCount = f1f8iter.Configuration.ReplicaCount + } + if f1f8iter.Configuration.Slots != nil { + f1f8elemf0.Slots = f1f8iter.Configuration.Slots + } + f1f8elem.Configuration = f1f8elemf0 + } + if f1f8iter.Name != nil { + f1f8elem.Name = f1f8iter.Name + } + if f1f8iter.Size != nil { + f1f8elem.Size = f1f8iter.Size + } + if f1f8iter.SnapshotCreationTime != nil { + f1f8elem.SnapshotCreationTime = &metav1.Time{*f1f8iter.SnapshotCreationTime} + } + f1f8 = append(f1f8, f1f8elem) + } + f1.Shards = f1f8 + } + if resp.Snapshot.ClusterConfiguration.SnapshotRetentionLimit != nil { + f1.SnapshotRetentionLimit = resp.Snapshot.ClusterConfiguration.SnapshotRetentionLimit + } + if resp.Snapshot.ClusterConfiguration.SnapshotWindow != nil { + f1.SnapshotWindow = resp.Snapshot.ClusterConfiguration.SnapshotWindow + } + if resp.Snapshot.ClusterConfiguration.SubnetGroupName != nil { + f1.SubnetGroupName = resp.Snapshot.ClusterConfiguration.SubnetGroupName + } + if resp.Snapshot.ClusterConfiguration.TopicArn != nil { + f1.TopicARN = resp.Snapshot.ClusterConfiguration.TopicArn + } + if resp.Snapshot.ClusterConfiguration.VpcId != nil { + f1.VPCID = resp.Snapshot.ClusterConfiguration.VpcId + } + ko.Status.ClusterConfiguration = f1 + } else { + ko.Status.ClusterConfiguration = nil + } + if resp.Snapshot.KmsKeyId != nil { + ko.Spec.KMSKeyID = resp.Snapshot.KmsKeyId + } else { + ko.Spec.KMSKeyID = nil + } + if resp.Snapshot.Source != nil { + ko.Status.Source = resp.Snapshot.Source + } else { + ko.Status.Source = nil + } + if resp.Snapshot.Status != nil { + ko.Status.Status = resp.Snapshot.Status + } else { + ko.Status.Status = nil + } + + rm.setStatusDefaults(ko) + // custom set output from response + rm.customCopySnapshotSetOutput(resp, ko) + return &resource{ko}, nil +} + +// newCopySnapshotPayload returns an SDK-specific struct for the HTTP request +// payload of the CopySnapshot API call +func (rm *resourceManager) newCopySnapshotPayload( + r *resource, +) (*svcsdk.CopySnapshotInput, error) { + res := &svcsdk.CopySnapshotInput{} + + if r.ko.Spec.SourceSnapshotName != nil { + res.SetSourceSnapshotName(*r.ko.Spec.SourceSnapshotName) + } + if r.ko.Spec.KMSKeyID != nil { + res.SetKmsKeyId(*r.ko.Spec.KMSKeyID) + } + + if r.ko.Spec.Name != nil { + res.SetTargetSnapshotName(*r.ko.Spec.Name) + } + + return res, nil +} diff --git a/pkg/resource/snapshot/custom_set_output.go b/pkg/resource/snapshot/custom_set_output.go new file mode 100644 index 0000000..777a8d2 --- /dev/null +++ b/pkg/resource/snapshot/custom_set_output.go @@ -0,0 +1,85 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package snapshot + +import ( + svcapitypes "github.com/aws-controllers-k8s/memorydb-controller/apis/v1alpha1" + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + "github.com/aws/aws-sdk-go/service/memorydb" + corev1 "k8s.io/api/core/v1" +) + +func (rm *resourceManager) customDescribeSnapshotSetOutput( + resp *memorydb.DescribeSnapshotsOutput, + ko *svcapitypes.Snapshot, +) (*svcapitypes.Snapshot, error) { + if len(resp.Snapshots) == 0 { + return ko, nil + } + elem := resp.Snapshots[0] + rm.customSetOutput(elem, ko) + return ko, nil +} + +func (rm *resourceManager) customCreateSnapshotSetOutput( + resp *memorydb.CreateSnapshotOutput, + ko *svcapitypes.Snapshot, +) (*svcapitypes.Snapshot, error) { + rm.customSetOutput(resp.Snapshot, ko) + return ko, nil +} + +func (rm *resourceManager) customCopySnapshotSetOutput( + resp *memorydb.CopySnapshotOutput, + ko *svcapitypes.Snapshot, +) *svcapitypes.Snapshot { + rm.customSetOutput(resp.Snapshot, ko) + return ko +} + +func (rm *resourceManager) customSetOutput( + respSnapshot *memorydb.Snapshot, + ko *svcapitypes.Snapshot, +) { + if ko.Status.Conditions == nil { + ko.Status.Conditions = []*ackv1alpha1.Condition{} + } + snapshotStatus := respSnapshot.Status + syncConditionStatus := corev1.ConditionUnknown + if snapshotStatus != nil { + if *snapshotStatus == "available" || + *snapshotStatus == "failed" { + syncConditionStatus = corev1.ConditionTrue + } else { + // resource in "creating", "restoring","exporting" + syncConditionStatus = corev1.ConditionFalse + } + } + var resourceSyncedCondition *ackv1alpha1.Condition = nil + for _, condition := range ko.Status.Conditions { + if condition.Type == ackv1alpha1.ConditionTypeResourceSynced { + resourceSyncedCondition = condition + break + } + } + if resourceSyncedCondition == nil { + resourceSyncedCondition = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeResourceSynced, + Status: syncConditionStatus, + } + ko.Status.Conditions = append(ko.Status.Conditions, resourceSyncedCondition) + } else { + resourceSyncedCondition.Status = syncConditionStatus + } +} diff --git a/pkg/resource/snapshot/delta.go b/pkg/resource/snapshot/delta.go new file mode 100644 index 0000000..1ecbdfe --- /dev/null +++ b/pkg/resource/snapshot/delta.go @@ -0,0 +1,77 @@ +// 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 snapshot + +import ( + "bytes" + "reflect" + + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" +) + +// Hack to avoid import errors during build... +var ( + _ = &bytes.Buffer{} + _ = &reflect.Method{} +) + +// newResourceDelta returns a new `ackcompare.Delta` used to compare two +// resources +func newResourceDelta( + a *resource, + b *resource, +) *ackcompare.Delta { + delta := ackcompare.NewDelta() + if (a == nil && b != nil) || + (a != nil && b == nil) { + delta.Add("", a, b) + return delta + } + + if ackcompare.HasNilDifference(a.ko.Spec.ClusterName, b.ko.Spec.ClusterName) { + delta.Add("Spec.ClusterName", a.ko.Spec.ClusterName, b.ko.Spec.ClusterName) + } else if a.ko.Spec.ClusterName != nil && b.ko.Spec.ClusterName != nil { + if *a.ko.Spec.ClusterName != *b.ko.Spec.ClusterName { + delta.Add("Spec.ClusterName", a.ko.Spec.ClusterName, b.ko.Spec.ClusterName) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.KMSKeyID, b.ko.Spec.KMSKeyID) { + delta.Add("Spec.KMSKeyID", a.ko.Spec.KMSKeyID, b.ko.Spec.KMSKeyID) + } else if a.ko.Spec.KMSKeyID != nil && b.ko.Spec.KMSKeyID != nil { + if *a.ko.Spec.KMSKeyID != *b.ko.Spec.KMSKeyID { + delta.Add("Spec.KMSKeyID", a.ko.Spec.KMSKeyID, b.ko.Spec.KMSKeyID) + } + } + 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) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.SourceSnapshotName, b.ko.Spec.SourceSnapshotName) { + delta.Add("Spec.SourceSnapshotName", a.ko.Spec.SourceSnapshotName, b.ko.Spec.SourceSnapshotName) + } else if a.ko.Spec.SourceSnapshotName != nil && b.ko.Spec.SourceSnapshotName != nil { + if *a.ko.Spec.SourceSnapshotName != *b.ko.Spec.SourceSnapshotName { + delta.Add("Spec.SourceSnapshotName", a.ko.Spec.SourceSnapshotName, b.ko.Spec.SourceSnapshotName) + } + } + if !reflect.DeepEqual(a.ko.Spec.Tags, b.ko.Spec.Tags) { + delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags) + } + + return delta +} diff --git a/pkg/resource/snapshot/descriptor.go b/pkg/resource/snapshot/descriptor.go new file mode 100644 index 0000000..86f38df --- /dev/null +++ b/pkg/resource/snapshot/descriptor.go @@ -0,0 +1,154 @@ +// 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 snapshot + +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" + rtclient "sigs.k8s.io/controller-runtime/pkg/client" + k8sctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + svcapitypes "github.com/aws-controllers-k8s/memorydb-controller/apis/v1alpha1" +) + +const ( + finalizerString = "finalizers.memorydb.services.k8s.aws/Snapshot" +) + +var ( + GroupVersionResource = svcapitypes.GroupVersion.WithResource("snapshots") + GroupKind = metav1.GroupKind{ + Group: "memorydb.services.k8s.aws", + Kind: "Snapshot", + } +) + +// resourceDescriptor implements the +// `aws-service-operator-k8s/pkg/types.AWSResourceDescriptor` interface +type resourceDescriptor struct { +} + +// GroupKind returns a Kubernetes metav1.GroupKind struct that describes the +// API Group and Kind of CRs described by the descriptor +func (d *resourceDescriptor) GroupKind() *metav1.GroupKind { + return &GroupKind +} + +// EmptyRuntimeObject returns an empty object prototype that may be used in +// apimachinery and k8s client operations +func (d *resourceDescriptor) EmptyRuntimeObject() rtclient.Object { + return &svcapitypes.Snapshot{} +} + +// 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.Snapshot), + } +} + +// 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/snapshot/identifiers.go b/pkg/resource/snapshot/identifiers.go new file mode 100644 index 0000000..0f66796 --- /dev/null +++ b/pkg/resource/snapshot/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 snapshot + +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/snapshot/manager.go b/pkg/resource/snapshot/manager.go new file mode 100644 index 0000000..40db198 --- /dev/null +++ b/pkg/resource/snapshot/manager.go @@ -0,0 +1,322 @@ +// 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 snapshot + +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" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + ackutil "github.com/aws-controllers-k8s/runtime/pkg/util" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + + svcsdk "github.com/aws/aws-sdk-go/service/memorydb" + svcsdkapi "github.com/aws/aws-sdk-go/service/memorydb/memorydbiface" +) + +var ( + _ = ackutil.InStrings +) + +// +kubebuilder:rbac:groups=memorydb.services.k8s.aws,resources=snapshots,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=memorydb.services.k8s.aws,resources=snapshots/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.MemoryDBAPI +} + +// 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 { + 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 { + 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:memorydb:%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 +} + +// 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/snapshot/manager_factory.go b/pkg/resource/snapshot/manager_factory.go new file mode 100644 index 0000000..c939888 --- /dev/null +++ b/pkg/resource/snapshot/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 snapshot + +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/memorydb-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/snapshot/references.go b/pkg/resource/snapshot/references.go new file mode 100644 index 0000000..6b32f49 --- /dev/null +++ b/pkg/resource/snapshot/references.go @@ -0,0 +1,52 @@ +// 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 snapshot + +import ( + "context" + "sigs.k8s.io/controller-runtime/pkg/client" + + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" + + svcapitypes "github.com/aws-controllers-k8s/memorydb-controller/apis/v1alpha1" +) + +// 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 target field(s). +// It returns an AWSResource with resolved reference(s), and an error if the +// passed AWSResource's reference field(s) cannot be resolved. +// This method also adds/updates the ConditionTypeReferencesResolved for the +// AWSResource. +func (rm *resourceManager) ResolveReferences( + ctx context.Context, + apiReader client.Reader, + res acktypes.AWSResource, +) (acktypes.AWSResource, error) { + return res, nil +} + +// validateReferenceFields validates the reference field and corresponding +// identifier field. +func validateReferenceFields(ko *svcapitypes.Snapshot) error { + return nil +} + +// hasNonNilReferences returns true if resource contains a reference to another +// resource +func hasNonNilReferences(ko *svcapitypes.Snapshot) bool { + return false +} diff --git a/pkg/resource/snapshot/resource.go b/pkg/resource/snapshot/resource.go new file mode 100644 index 0000000..9c05eee --- /dev/null +++ b/pkg/resource/snapshot/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 snapshot + +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/memorydb-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.Snapshot +} + +// 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 timestemp +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/snapshot/sdk.go b/pkg/resource/snapshot/sdk.go new file mode 100644 index 0000000..201ef64 --- /dev/null +++ b/pkg/resource/snapshot/sdk.go @@ -0,0 +1,593 @@ +// 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 snapshot + +import ( + "context" + "errors" + "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" + 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/memorydb" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + svcapitypes "github.com/aws-controllers-k8s/memorydb-controller/apis/v1alpha1" +) + +// Hack to avoid import errors during build... +var ( + _ = &metav1.Time{} + _ = strings.ToLower("") + _ = &aws.JSONValue{} + _ = &svcsdk.MemoryDB{} + _ = &svcapitypes.Snapshot{} + _ = ackv1alpha1.AWSAccountID("") + _ = &ackerr.NotFound + _ = &ackcondition.NotManagedMessage + _ = &reflect.Value{} +) + +// 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 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.DescribeSnapshotsOutput + resp, err = rm.sdkapi.DescribeSnapshotsWithContext(ctx, input) + rm.metrics.RecordAPICall("READ_MANY", "DescribeSnapshots", err) + if err != nil { + if awsErr, ok := ackerr.AWSError(err); ok && awsErr.Code() == "SnapshotNotFoundFault" { + 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() + + found := false + for _, elem := range resp.Snapshots { + 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.ClusterConfiguration != nil { + f1 := &svcapitypes.ClusterConfiguration{} + if elem.ClusterConfiguration.Description != nil { + f1.Description = elem.ClusterConfiguration.Description + } + if elem.ClusterConfiguration.EngineVersion != nil { + f1.EngineVersion = elem.ClusterConfiguration.EngineVersion + } + if elem.ClusterConfiguration.MaintenanceWindow != nil { + f1.MaintenanceWindow = elem.ClusterConfiguration.MaintenanceWindow + } + if elem.ClusterConfiguration.Name != nil { + f1.Name = elem.ClusterConfiguration.Name + } + if elem.ClusterConfiguration.NodeType != nil { + f1.NodeType = elem.ClusterConfiguration.NodeType + } + if elem.ClusterConfiguration.NumShards != nil { + f1.NumShards = elem.ClusterConfiguration.NumShards + } + if elem.ClusterConfiguration.ParameterGroupName != nil { + f1.ParameterGroupName = elem.ClusterConfiguration.ParameterGroupName + } + if elem.ClusterConfiguration.Port != nil { + f1.Port = elem.ClusterConfiguration.Port + } + if elem.ClusterConfiguration.Shards != nil { + f1f8 := []*svcapitypes.ShardDetail{} + for _, f1f8iter := range elem.ClusterConfiguration.Shards { + f1f8elem := &svcapitypes.ShardDetail{} + if f1f8iter.Configuration != nil { + f1f8elemf0 := &svcapitypes.ShardConfiguration{} + if f1f8iter.Configuration.ReplicaCount != nil { + f1f8elemf0.ReplicaCount = f1f8iter.Configuration.ReplicaCount + } + if f1f8iter.Configuration.Slots != nil { + f1f8elemf0.Slots = f1f8iter.Configuration.Slots + } + f1f8elem.Configuration = f1f8elemf0 + } + if f1f8iter.Name != nil { + f1f8elem.Name = f1f8iter.Name + } + if f1f8iter.Size != nil { + f1f8elem.Size = f1f8iter.Size + } + if f1f8iter.SnapshotCreationTime != nil { + f1f8elem.SnapshotCreationTime = &metav1.Time{*f1f8iter.SnapshotCreationTime} + } + f1f8 = append(f1f8, f1f8elem) + } + f1.Shards = f1f8 + } + if elem.ClusterConfiguration.SnapshotRetentionLimit != nil { + f1.SnapshotRetentionLimit = elem.ClusterConfiguration.SnapshotRetentionLimit + } + if elem.ClusterConfiguration.SnapshotWindow != nil { + f1.SnapshotWindow = elem.ClusterConfiguration.SnapshotWindow + } + if elem.ClusterConfiguration.SubnetGroupName != nil { + f1.SubnetGroupName = elem.ClusterConfiguration.SubnetGroupName + } + if elem.ClusterConfiguration.TopicArn != nil { + f1.TopicARN = elem.ClusterConfiguration.TopicArn + } + if elem.ClusterConfiguration.VpcId != nil { + f1.VPCID = elem.ClusterConfiguration.VpcId + } + ko.Status.ClusterConfiguration = f1 + } else { + ko.Status.ClusterConfiguration = nil + } + if elem.KmsKeyId != nil { + ko.Spec.KMSKeyID = elem.KmsKeyId + } else { + ko.Spec.KMSKeyID = nil + } + if elem.Name != nil { + ko.Spec.Name = elem.Name + } else { + ko.Spec.Name = nil + } + if elem.Source != nil { + ko.Status.Source = elem.Source + } else { + ko.Status.Source = nil + } + if elem.Status != nil { + ko.Status.Status = elem.Status + } else { + ko.Status.Status = nil + } + found = true + break + } + if !found { + return nil, ackerr.NotFound + } + + rm.setStatusDefaults(ko) + // custom set output from response + ko, err = rm.customDescribeSnapshotSetOutput(resp, ko) + if err != nil { + return nil, err + } + 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 r.ko.Spec.Name == nil + +} + +// 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.DescribeSnapshotsInput, error) { + res := &svcsdk.DescribeSnapshotsInput{} + + if r.ko.Spec.Name != nil { + res.SetSnapshotName(*r.ko.Spec.Name) + } + + 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 exit(err) + created, err = rm.customTryCopySnapshot(ctx, desired) + if created != nil || err != nil { + return created, err + } + input, err := rm.newCreateRequestPayload(ctx, desired) + if err != nil { + return nil, err + } + + var resp *svcsdk.CreateSnapshotOutput + _ = resp + resp, err = rm.sdkapi.CreateSnapshotWithContext(ctx, input) + rm.metrics.RecordAPICall("CREATE", "CreateSnapshot", err) + if err != nil { + return nil, err + } + // Merge in the information we read from the API call above to the copy of + // the original Kubernetes object we passed to the function + ko := desired.ko.DeepCopy() + + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + if resp.Snapshot.ARN != nil { + arn := ackv1alpha1.AWSResourceName(*resp.Snapshot.ARN) + ko.Status.ACKResourceMetadata.ARN = &arn + } + if resp.Snapshot.ClusterConfiguration != nil { + f1 := &svcapitypes.ClusterConfiguration{} + if resp.Snapshot.ClusterConfiguration.Description != nil { + f1.Description = resp.Snapshot.ClusterConfiguration.Description + } + if resp.Snapshot.ClusterConfiguration.EngineVersion != nil { + f1.EngineVersion = resp.Snapshot.ClusterConfiguration.EngineVersion + } + if resp.Snapshot.ClusterConfiguration.MaintenanceWindow != nil { + f1.MaintenanceWindow = resp.Snapshot.ClusterConfiguration.MaintenanceWindow + } + if resp.Snapshot.ClusterConfiguration.Name != nil { + f1.Name = resp.Snapshot.ClusterConfiguration.Name + } + if resp.Snapshot.ClusterConfiguration.NodeType != nil { + f1.NodeType = resp.Snapshot.ClusterConfiguration.NodeType + } + if resp.Snapshot.ClusterConfiguration.NumShards != nil { + f1.NumShards = resp.Snapshot.ClusterConfiguration.NumShards + } + if resp.Snapshot.ClusterConfiguration.ParameterGroupName != nil { + f1.ParameterGroupName = resp.Snapshot.ClusterConfiguration.ParameterGroupName + } + if resp.Snapshot.ClusterConfiguration.Port != nil { + f1.Port = resp.Snapshot.ClusterConfiguration.Port + } + if resp.Snapshot.ClusterConfiguration.Shards != nil { + f1f8 := []*svcapitypes.ShardDetail{} + for _, f1f8iter := range resp.Snapshot.ClusterConfiguration.Shards { + f1f8elem := &svcapitypes.ShardDetail{} + if f1f8iter.Configuration != nil { + f1f8elemf0 := &svcapitypes.ShardConfiguration{} + if f1f8iter.Configuration.ReplicaCount != nil { + f1f8elemf0.ReplicaCount = f1f8iter.Configuration.ReplicaCount + } + if f1f8iter.Configuration.Slots != nil { + f1f8elemf0.Slots = f1f8iter.Configuration.Slots + } + f1f8elem.Configuration = f1f8elemf0 + } + if f1f8iter.Name != nil { + f1f8elem.Name = f1f8iter.Name + } + if f1f8iter.Size != nil { + f1f8elem.Size = f1f8iter.Size + } + if f1f8iter.SnapshotCreationTime != nil { + f1f8elem.SnapshotCreationTime = &metav1.Time{*f1f8iter.SnapshotCreationTime} + } + f1f8 = append(f1f8, f1f8elem) + } + f1.Shards = f1f8 + } + if resp.Snapshot.ClusterConfiguration.SnapshotRetentionLimit != nil { + f1.SnapshotRetentionLimit = resp.Snapshot.ClusterConfiguration.SnapshotRetentionLimit + } + if resp.Snapshot.ClusterConfiguration.SnapshotWindow != nil { + f1.SnapshotWindow = resp.Snapshot.ClusterConfiguration.SnapshotWindow + } + if resp.Snapshot.ClusterConfiguration.SubnetGroupName != nil { + f1.SubnetGroupName = resp.Snapshot.ClusterConfiguration.SubnetGroupName + } + if resp.Snapshot.ClusterConfiguration.TopicArn != nil { + f1.TopicARN = resp.Snapshot.ClusterConfiguration.TopicArn + } + if resp.Snapshot.ClusterConfiguration.VpcId != nil { + f1.VPCID = resp.Snapshot.ClusterConfiguration.VpcId + } + ko.Status.ClusterConfiguration = f1 + } else { + ko.Status.ClusterConfiguration = nil + } + if resp.Snapshot.KmsKeyId != nil { + ko.Spec.KMSKeyID = resp.Snapshot.KmsKeyId + } else { + ko.Spec.KMSKeyID = nil + } + if resp.Snapshot.Name != nil { + ko.Spec.Name = resp.Snapshot.Name + } else { + ko.Spec.Name = nil + } + if resp.Snapshot.Source != nil { + ko.Status.Source = resp.Snapshot.Source + } else { + ko.Status.Source = nil + } + if resp.Snapshot.Status != nil { + ko.Status.Status = resp.Snapshot.Status + } else { + ko.Status.Status = nil + } + + rm.setStatusDefaults(ko) + // custom set output from response + ko, err = rm.customCreateSnapshotSetOutput(resp, ko) + if err != nil { + return nil, err + } + 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.CreateSnapshotInput, error) { + res := &svcsdk.CreateSnapshotInput{} + + if r.ko.Spec.ClusterName != nil { + res.SetClusterName(*r.ko.Spec.ClusterName) + } + if r.ko.Spec.KMSKeyID != nil { + res.SetKmsKeyId(*r.ko.Spec.KMSKeyID) + } + if r.ko.Spec.Name != nil { + res.SetSnapshotName(*r.ko.Spec.Name) + } + if r.ko.Spec.Tags != nil { + f3 := []*svcsdk.Tag{} + for _, f3iter := range r.ko.Spec.Tags { + f3elem := &svcsdk.Tag{} + if f3iter.Key != nil { + f3elem.SetKey(*f3iter.Key) + } + if f3iter.Value != nil { + f3elem.SetValue(*f3iter.Value) + } + f3 = append(f3, f3elem) + } + res.SetTags(f3) + } + + 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) { + // TODO(jaypipes): Figure this out... + return nil, ackerr.NotImplemented +} + +// sdkDelete deletes the supplied resource in the backend AWS service API +func (rm *resourceManager) sdkDelete( + ctx context.Context, + r *resource, +) (latest *resource, err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.sdkDelete") + defer exit(err) + if isDeleting(r) { + // Setting resource synced condition to false will trigger a requeue of + // the resource. + ackcondition.SetSynced( + r, + corev1.ConditionFalse, + &condMsgCurrentlyDeleting, + nil, + ) + // Need to return a requeue error here, otherwise: + // - reconciler.deleteResource() marks the resource unmanaged + // - reconciler.HandleReconcileError() does not update status for unmanaged resource + // - reconciler.handleRequeues() is not invoked for delete code path. + // TODO: return err as nil when reconciler is updated. + return r, requeueWaitWhileDeleting + } + input, err := rm.newDeleteRequestPayload(r) + if err != nil { + return nil, err + } + var resp *svcsdk.DeleteSnapshotOutput + _ = resp + resp, err = rm.sdkapi.DeleteSnapshotWithContext(ctx, input) + rm.metrics.RecordAPICall("DELETE", "DeleteSnapshot", err) + if err == nil { + rp, _ := rm.setSnapshotOutput(r, resp.Snapshot) + // Setting resource synced condition to false will trigger a requeue of + // the resource. + ackcondition.SetSynced( + rp, + corev1.ConditionFalse, + &condMsgCurrentlyDeleting, + nil, + ) + // Need to return a requeue error here, otherwise: + // - reconciler.deleteResource() marks the resource unmanaged + // - reconciler.HandleReconcileError() does not update status for unmanaged resource + // - reconciler.handleRequeues() is not invoked for delete code path. + // TODO: return err as nil when reconciler is updated. + return rp, requeueWaitWhileDeleting + } + 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.DeleteSnapshotInput, error) { + res := &svcsdk.DeleteSnapshotInput{} + + if r.ko.Spec.Name != nil { + res.SetSnapshotName(*r.ko.Spec.Name) + } + + return res, nil +} + +// setStatusDefaults sets default properties into supplied custom resource +func (rm *resourceManager) setStatusDefaults( + ko *svcapitypes.Snapshot, +) { + 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 "InvalidParameterCombinationException", + "SnapshotAlreadyExistsFault", + "SnapshotQuotaExceededFault", + "TagQuotaPerResourceExceeded", + "InvalidParameterValueException", + "InvalidParameter": + return true + default: + return false + } +} diff --git a/pkg/resource/snapshot/utility.go b/pkg/resource/snapshot/utility.go new file mode 100644 index 0000000..0549542 --- /dev/null +++ b/pkg/resource/snapshot/utility.go @@ -0,0 +1,147 @@ +package snapshot + +import ( + "errors" + svcapitypes "github.com/aws-controllers-k8s/memorydb-controller/apis/v1alpha1" + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" + svcsdk "github.com/aws/aws-sdk-go/service/memorydb" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + condMsgCurrentlyDeleting string = "snapshot currently being deleted" + deleteStatus string = "deleting" +) + +var ( + requeueWaitWhileDeleting = ackrequeue.NeededAfter( + errors.New("delete is in progress"), + ackrequeue.DefaultRequeueAfterDuration, + ) +) + +// isDeleting returns true if supplied snapshot resource state is 'deleting' +func isDeleting(r *resource) bool { + if r == nil || r.ko.Status.Status == nil { + return false + } + status := *r.ko.Status.Status + return status == deleteStatus +} + +func (rm *resourceManager) setSnapshotOutput( + r *resource, + obj *svcsdk.Snapshot, +) (*resource, error) { + if obj == nil || + r == nil || + r.ko == nil { + return nil, nil + } + resp := &svcsdk.DeleteSnapshotOutput{Snapshot: obj} + + // Merge in the information we read from the API call above to the copy of + // the original Kubernetes object we passed to the function + ko := r.ko.DeepCopy() + + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + if resp.Snapshot.ARN != nil { + arn := ackv1alpha1.AWSResourceName(*resp.Snapshot.ARN) + ko.Status.ACKResourceMetadata.ARN = &arn + } + if resp.Snapshot.ClusterConfiguration != nil { + f1 := &svcapitypes.ClusterConfiguration{} + if resp.Snapshot.ClusterConfiguration.Description != nil { + f1.Description = resp.Snapshot.ClusterConfiguration.Description + } + if resp.Snapshot.ClusterConfiguration.EngineVersion != nil { + f1.EngineVersion = resp.Snapshot.ClusterConfiguration.EngineVersion + } + if resp.Snapshot.ClusterConfiguration.MaintenanceWindow != nil { + f1.MaintenanceWindow = resp.Snapshot.ClusterConfiguration.MaintenanceWindow + } + if resp.Snapshot.ClusterConfiguration.Name != nil { + f1.Name = resp.Snapshot.ClusterConfiguration.Name + } + if resp.Snapshot.ClusterConfiguration.NodeType != nil { + f1.NodeType = resp.Snapshot.ClusterConfiguration.NodeType + } + if resp.Snapshot.ClusterConfiguration.NumShards != nil { + f1.NumShards = resp.Snapshot.ClusterConfiguration.NumShards + } + if resp.Snapshot.ClusterConfiguration.ParameterGroupName != nil { + f1.ParameterGroupName = resp.Snapshot.ClusterConfiguration.ParameterGroupName + } + if resp.Snapshot.ClusterConfiguration.Port != nil { + f1.Port = resp.Snapshot.ClusterConfiguration.Port + } + if resp.Snapshot.ClusterConfiguration.Shards != nil { + f1f8 := []*svcapitypes.ShardDetail{} + for _, f1f8iter := range resp.Snapshot.ClusterConfiguration.Shards { + f1f8elem := &svcapitypes.ShardDetail{} + if f1f8iter.Configuration != nil { + f1f8elemf0 := &svcapitypes.ShardConfiguration{} + if f1f8iter.Configuration.ReplicaCount != nil { + f1f8elemf0.ReplicaCount = f1f8iter.Configuration.ReplicaCount + } + if f1f8iter.Configuration.Slots != nil { + f1f8elemf0.Slots = f1f8iter.Configuration.Slots + } + f1f8elem.Configuration = f1f8elemf0 + } + if f1f8iter.Name != nil { + f1f8elem.Name = f1f8iter.Name + } + if f1f8iter.Size != nil { + f1f8elem.Size = f1f8iter.Size + } + if f1f8iter.SnapshotCreationTime != nil { + f1f8elem.SnapshotCreationTime = &metav1.Time{*f1f8iter.SnapshotCreationTime} + } + f1f8 = append(f1f8, f1f8elem) + } + f1.Shards = f1f8 + } + if resp.Snapshot.ClusterConfiguration.SnapshotRetentionLimit != nil { + f1.SnapshotRetentionLimit = resp.Snapshot.ClusterConfiguration.SnapshotRetentionLimit + } + if resp.Snapshot.ClusterConfiguration.SnapshotWindow != nil { + f1.SnapshotWindow = resp.Snapshot.ClusterConfiguration.SnapshotWindow + } + if resp.Snapshot.ClusterConfiguration.SubnetGroupName != nil { + f1.SubnetGroupName = resp.Snapshot.ClusterConfiguration.SubnetGroupName + } + if resp.Snapshot.ClusterConfiguration.TopicArn != nil { + f1.TopicARN = resp.Snapshot.ClusterConfiguration.TopicArn + } + if resp.Snapshot.ClusterConfiguration.VpcId != nil { + f1.VPCID = resp.Snapshot.ClusterConfiguration.VpcId + } + ko.Status.ClusterConfiguration = f1 + } else { + ko.Status.ClusterConfiguration = nil + } + if resp.Snapshot.KmsKeyId != nil { + ko.Spec.KMSKeyID = resp.Snapshot.KmsKeyId + } else { + ko.Spec.KMSKeyID = nil + } + if resp.Snapshot.Source != nil { + ko.Status.Source = resp.Snapshot.Source + } else { + ko.Status.Source = nil + } + if resp.Snapshot.Status != nil { + ko.Status.Status = resp.Snapshot.Status + } else { + ko.Status.Status = nil + } + + rm.setStatusDefaults(ko) + // custom set output from response + rm.customSetOutput(obj, ko) + return &resource{ko}, nil +} diff --git a/templates/hooks/snapshot/sdk_create_post_set_output.go.tpl b/templates/hooks/snapshot/sdk_create_post_set_output.go.tpl new file mode 100644 index 0000000..f068b25 --- /dev/null +++ b/templates/hooks/snapshot/sdk_create_post_set_output.go.tpl @@ -0,0 +1,5 @@ + // custom set output from response + ko, err = rm.customCreateSnapshotSetOutput(resp, ko) + if err != nil { + return nil, err + } \ No newline at end of file diff --git a/templates/hooks/snapshot/sdk_create_pre_build_request.go.tpl b/templates/hooks/snapshot/sdk_create_pre_build_request.go.tpl new file mode 100644 index 0000000..0896643 --- /dev/null +++ b/templates/hooks/snapshot/sdk_create_pre_build_request.go.tpl @@ -0,0 +1,4 @@ + created, err = rm.customTryCopySnapshot(ctx, desired) + if created != nil || err != nil { + return created, err + } \ No newline at end of file diff --git a/templates/hooks/snapshot/sdk_delete_post_request.go.tpl b/templates/hooks/snapshot/sdk_delete_post_request.go.tpl new file mode 100644 index 0000000..e103d0f --- /dev/null +++ b/templates/hooks/snapshot/sdk_delete_post_request.go.tpl @@ -0,0 +1,17 @@ + if err == nil { + rp, _ := rm.setSnapshotOutput(r, resp.Snapshot) + // Setting resource synced condition to false will trigger a requeue of + // the resource. + ackcondition.SetSynced( + rp, + corev1.ConditionFalse, + &condMsgCurrentlyDeleting, + nil, + ) + // Need to return a requeue error here, otherwise: + // - reconciler.deleteResource() marks the resource unmanaged + // - reconciler.HandleReconcileError() does not update status for unmanaged resource + // - reconciler.handleRequeues() is not invoked for delete code path. + // TODO: return err as nil when reconciler is updated. + return rp, requeueWaitWhileDeleting + } \ No newline at end of file diff --git a/templates/hooks/snapshot/sdk_delete_pre_build_request.go.tpl b/templates/hooks/snapshot/sdk_delete_pre_build_request.go.tpl new file mode 100644 index 0000000..04757e9 --- /dev/null +++ b/templates/hooks/snapshot/sdk_delete_pre_build_request.go.tpl @@ -0,0 +1,16 @@ + if isDeleting(r) { + // Setting resource synced condition to false will trigger a requeue of + // the resource. + ackcondition.SetSynced( + r, + corev1.ConditionFalse, + &condMsgCurrentlyDeleting, + nil, + ) + // Need to return a requeue error here, otherwise: + // - reconciler.deleteResource() marks the resource unmanaged + // - reconciler.HandleReconcileError() does not update status for unmanaged resource + // - reconciler.handleRequeues() is not invoked for delete code path. + // TODO: return err as nil when reconciler is updated. + return r, requeueWaitWhileDeleting + } \ No newline at end of file diff --git a/templates/hooks/snapshot/sdk_read_many_post_set_output.go.tpl b/templates/hooks/snapshot/sdk_read_many_post_set_output.go.tpl new file mode 100644 index 0000000..c6d81fa --- /dev/null +++ b/templates/hooks/snapshot/sdk_read_many_post_set_output.go.tpl @@ -0,0 +1,5 @@ + // custom set output from response + ko, err = rm.customDescribeSnapshotSetOutput(resp, ko) + if err != nil { + return nil, err + } \ No newline at end of file diff --git a/test/e2e/bootstrap_resources.py b/test/e2e/bootstrap_resources.py index cdde248..941dbeb 100644 --- a/test/e2e/bootstrap_resources.py +++ b/test/e2e/bootstrap_resources.py @@ -22,6 +22,7 @@ from e2e.bootstrappable.subnets import Subnets from e2e.bootstrappable.secrets import Secret from e2e.bootstrappable.topic import Topic +from e2e.bootstrappable.cluster import Cluster @dataclass @@ -32,6 +33,8 @@ class BootstrapResources(Resources): Topic1: Topic Topic2: Topic KMSKey: KMS + Cluster1: Cluster + Cluster2: Cluster _bootstrap_resources = None diff --git a/test/e2e/bootstrappable/cluster.py b/test/e2e/bootstrappable/cluster.py new file mode 100644 index 0000000..d956301 --- /dev/null +++ b/test/e2e/bootstrappable/cluster.py @@ -0,0 +1,59 @@ +# 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. + +from dataclasses import dataclass + +import boto3 +import time +from acktest.bootstrapping import Bootstrappable + + +@dataclass +class Cluster(Bootstrappable): + # Output + clusterName : str + + def create_cluster(self): + mdb = boto3.client("memorydb") + mdb.create_cluster(ClusterName=self.clusterName, + Description='Cluster for Ack snapshot resource testing', + NodeType='db.r6g.large', ACLName='open-access', NumShards=1, NumReplicasPerShard=0) + timeout = time.time() + 30*60 # 30 minutes from now + available_status = "Available" + while True: + clusters = mdb.describe_clusters(ClusterName=self.clusterName) + cluster = clusters['Clusters'][0] + if cluster.get("Status").casefold() == available_status.casefold(): + return True + if time.time() > timeout: + break + time.sleep(60) + raise ValueError('cluster not created within expected time') + + def delete_cluster(self): + mdb = boto3.client("memorydb") + mdb.delete_cluster(ClusterName=self.clusterName) + + def bootstrap(self): + """Find supported subnets. + """ + super().bootstrap() + # Try to create the subnet using all the available subnets + self.create_cluster() + + def cleanup(self): + """Nothing to do here as we did not create new subnets. + """ + super().cleanup() + + self.delete_cluster() \ No newline at end of file diff --git a/test/e2e/bootstrappable/snapshots.py b/test/e2e/bootstrappable/snapshots.py new file mode 100644 index 0000000..958a769 --- /dev/null +++ b/test/e2e/bootstrappable/snapshots.py @@ -0,0 +1,60 @@ +# 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. + +from dataclasses import dataclass + +import boto3 +import time +from acktest.bootstrapping import Bootstrappable +from acktest.resources import random_suffix_name + + +@dataclass +class Snapshots(Bootstrappable): + # Output + snapshotClusterName: str + + def create_cluster(self): + mdb = boto3.client("memorydb") + mdb.create_cluster(ClusterName=self.snapshotClusterName, + Description='Cluster for Ack snapshot resource testing', + NodeType='db.r6g.large', ACLName='open-access', NumShards=1, NumReplicasPerShard=0) + timeout = time.time() + 30*60 # 30 minutes from now + available_status = "Available" + while True: + clusters = mdb.describe_clusters(ClusterName=self.snapshotClusterName) + cluster = clusters['Clusters'][0] + if cluster.get("Status").casefold() == available_status.casefold(): + return True + if time.time() > timeout: + break + time.sleep(60) + raise ValueError('cluster not created within expected time') + + def delete_cluster(self): + mdb = boto3.client("memorydb") + mdb.delete_cluster(ClusterName=self.snapshotClusterName) + + def bootstrap(self): + """Find supported subnets. + """ + super().bootstrap() + # Try to create the subnet using all the available subnets + self.create_cluster() + + def cleanup(self): + """Nothing to do here as we did not create new subnets. + """ + super().cleanup() + + self.delete_cluster() \ No newline at end of file diff --git a/test/e2e/declarative_test_fwk/model.py b/test/e2e/declarative_test_fwk/model.py index 9940ede..d34e0aa 100644 --- a/test/e2e/declarative_test_fwk/model.py +++ b/test/e2e/declarative_test_fwk/model.py @@ -154,7 +154,6 @@ class ScenarioDict(TypedDict, total=False): resource: ResourceDict steps: List[StepDict] - class Scenario: """ Represents a declarative test scenario with steps diff --git a/test/e2e/replacement_values.py b/test/e2e/replacement_values.py index 45a9a5f..8085a20 100644 --- a/test/e2e/replacement_values.py +++ b/test/e2e/replacement_values.py @@ -22,5 +22,7 @@ "SUBNET2": get_bootstrap_resources().Subnets.subnets[1], "TOPIC1": get_bootstrap_resources().Topic1.topic_arn, "TOPIC2": get_bootstrap_resources().Topic2.topic_arn, - "KMSKEY": get_bootstrap_resources().KMSKey.key + "KMSKEY": get_bootstrap_resources().KMSKey.key, + "SNAPSHOT_CLUSTER_NAME1": get_bootstrap_resources().Cluster1.clusterName, + "SNAPSHOT_CLUSTER_NAME2": get_bootstrap_resources().Cluster2.clusterName } diff --git a/test/e2e/scenarios/Snapshot/snapshot_copy.yaml b/test/e2e/scenarios/Snapshot/snapshot_copy.yaml new file mode 100644 index 0000000..0d15296 --- /dev/null +++ b/test/e2e/scenarios/Snapshot/snapshot_copy.yaml @@ -0,0 +1,50 @@ +id: "SNAPSHOT_COPY" +description: "In this test we copy snapshot from another snapshot" +steps: + - id: "CREATE_INITIAL_SNAPSHOT" + description: "Create Initial Snapshot" + create: + apiVersion: $CRD_GROUP/$CRD_VERSION + kind: Snapshot + metadata: + name: snapshot$RANDOM_SUFFIX + spec: + description: "Create ACK snapshot" + clusterName: $SNAPSHOT_CLUSTER_NAME2 + name: snapshot$RANDOM_SUFFIX + wait: + status: + conditions: + ACK.ResourceSynced: + status: "True" + timeout: 1800 + - id: "COPY_SNAPSHOT" + description: "Ack Copy Snapshot" + create: + apiVersion: $CRD_GROUP/$CRD_VERSION + kind: Snapshot + metadata: + name: snapshotcopy$RANDOM_SUFFIX + spec: + sourceSnapshotName: snapshot$RANDOM_SUFFIX + name: snapshotcopy$RANDOM_SUFFIX + wait: + status: + conditions: + ACK.ResourceSynced: + status: "True" + timeout: 1800 + - id: "DELETE_COPY_SNAPSHOT" + description: "Ack Delete Copy Snapshot" + delete: + apiVersion: $CRD_GROUP/$CRD_VERSION + kind: Snapshot + metadata: + name: snapshotcopy$RANDOM_SUFFIX + - id: "DELETE_INITIAL_SNAPSHOT" + description: "Ack Delete Initial Snapshot" + delete: + apiVersion: $CRD_GROUP/$CRD_VERSION + kind: Snapshot + metadata: + name: snapshot$RANDOM_SUFFIX \ No newline at end of file diff --git a/test/e2e/scenarios/Snapshot/snapshot_create.yaml b/test/e2e/scenarios/Snapshot/snapshot_create.yaml new file mode 100644 index 0000000..684c030 --- /dev/null +++ b/test/e2e/scenarios/Snapshot/snapshot_create.yaml @@ -0,0 +1,27 @@ +id: "SNAPSHOT_CREATE" +description: "In this test we create Snapshot" +steps: + - id: "CREATE_SNAPSHOT" + description: "ACK Snapshot" + create: + apiVersion: $CRD_GROUP/$CRD_VERSION + kind: Snapshot + metadata: + name: snapshot$RANDOM_SUFFIX + spec: + description: "Create ACK snapshot" + clusterName: $SNAPSHOT_CLUSTER_NAME1 + name: snapshot$RANDOM_SUFFIX + wait: + status: + conditions: + ACK.ResourceSynced: + status: "True" + timeout: 1800 + - id: "DELETE_SNAPSHOT" + description: "Delete snapshot" + delete: + apiVersion: $CRD_GROUP/$CRD_VERSION + kind: Snapshot + metadata: + name: snapshot$RANDOM_SUFFIX diff --git a/test/e2e/scenarios/Snapshot/snapshot_create_terminal_condition.yaml b/test/e2e/scenarios/Snapshot/snapshot_create_terminal_condition.yaml new file mode 100644 index 0000000..633859c --- /dev/null +++ b/test/e2e/scenarios/Snapshot/snapshot_create_terminal_condition.yaml @@ -0,0 +1,25 @@ +id: "SNAPSHOT_CREATE_TERMINAL_CONDITION" +description: "In this test we try to create snapshot without specifying cluster" +steps: + - id: "SNAPSHOT_INITIAL_CREATE" + description: "Create snapshot with no clustername " + create: + apiVersion: $CRD_GROUP/$CRD_VERSION + kind: Snapshot + metadata: + name: snapshot$RANDOM_SUFFIX + spec: + description: "Create ACK snapshot" + name: snapshot$RANDOM_SUFFIX + wait: 120 + expect: + status: + conditions: + ACK.Terminal: "True" + - id: "DELETE_SNAPSHOT" + description: "Delete snapshot" + delete: + apiVersion: $CRD_GROUP/$CRD_VERSION + kind: Snapshot + metadata: + name: snapshot$RANDOM_SUFFIX \ No newline at end of file diff --git a/test/e2e/service_bootstrap.py b/test/e2e/service_bootstrap.py index 834c6e1..03a111e 100755 --- a/test/e2e/service_bootstrap.py +++ b/test/e2e/service_bootstrap.py @@ -23,6 +23,7 @@ from e2e import bootstrap_directory from e2e.bootstrap_resources import BootstrapResources from e2e.bootstrappable.topic import Topic +from e2e.bootstrappable.cluster import Cluster def service_bootstrap() -> Resources: @@ -33,7 +34,9 @@ def service_bootstrap() -> Resources: Subnets=Subnets(), Topic1=Topic(), Topic2=Topic(), - KMSKey=KMS()) + KMSKey=KMS(), + Cluster1=Cluster(random_suffix_name("cluster", 10)), + Cluster2=Cluster(random_suffix_name("cluster", 10))) try: resources.bootstrap() diff --git a/test/e2e/tests/test_scenarios.py b/test/e2e/tests/test_scenarios.py index e46b2fc..5a4989b 100644 --- a/test/e2e/tests/test_scenarios.py +++ b/test/e2e/tests/test_scenarios.py @@ -25,11 +25,10 @@ import logging from e2e import service_marker, scenarios_directory, resource_directory, CRD_VERSION, CRD_GROUP, SERVICE_NAME - -from acktest.k8s import resource as k8s - +from e2e.bootstrap_resources import get_bootstrap_resources from e2e.replacement_values import REPLACEMENT_VALUES +from acktest.k8s import resource as k8s @helper.register_resource_helper(resource_kind="ParameterGroup", resource_plural="ParameterGroups") class ParameterGroupHelper(helper.ResourceHelper): @@ -41,6 +40,15 @@ class ParameterGroupHelper(helper.ResourceHelper): def wait_for_delete(self, reference: k8s.CustomResourceReference): logging.debug(f"ParameterGroup - wait_for_delete()") +@helper.register_resource_helper(resource_kind="Snapshot", resource_plural="Snapshots") +class SnapshotHelper(helper.ResourceHelper): + """ + Helper for snapshot scenarios. + Overrides methods as required for custom resources. + """ + + def wait_for_delete(self, reference: k8s.CustomResourceReference): + logging.debug(f"Snapshot - wait_for_delete()") @helper.register_resource_helper(resource_kind="User", resource_plural="Users") class UserHelper(helper.ResourceHelper):