diff --git a/apis/v1alpha1/types.go b/apis/v1alpha1/types.go index b940aa96..af4c7b17 100644 --- a/apis/v1alpha1/types.go +++ b/apis/v1alpha1/types.go @@ -30,7 +30,8 @@ var ( // Indicates whether the user requires a password to authenticate. type Authentication struct { - PasswordCount *int64 `json:"passwordCount,omitempty"` + PasswordCount *int64 `json:"passwordCount,omitempty"` + Type *string `json:"type_,omitempty"` } // Describes an Availability Zone in which the cluster is launched. @@ -255,6 +256,12 @@ type Event struct { SourceIdentifier *string `json:"sourceIdentifier,omitempty"` } +// Used to streamline results of a search based on the property being filtered. +type Filter struct { + Name *string `json:"name,omitempty"` + Values []*string `json:"values,omitempty"` +} + // Indicates the slot configuration and global identifier for a slice group. type GlobalNodeGroup struct { GlobalNodeGroupID *string `json:"globalNodeGroupID,omitempty"` @@ -622,17 +629,9 @@ type UpdateAction struct { UpdateActionStatusModifiedDate *metav1.Time `json:"updateActionStatusModifiedDate,omitempty"` } -type User struct { - ARN *string `json:"arn,omitempty"` - AccessString *string `json:"accessString,omitempty"` - Status *string `json:"status,omitempty"` - UserGroupIDs []*string `json:"userGroupIDs,omitempty"` - UserID *string `json:"userID,omitempty"` - UserName *string `json:"userName,omitempty"` -} - type UserGroup struct { ARN *string `json:"arn,omitempty"` + Engine *string `json:"engine,omitempty"` Status *string `json:"status,omitempty"` UserGroupID *string `json:"userGroupID,omitempty"` } @@ -642,3 +641,15 @@ type UserGroupsUpdateStatus struct { UserGroupIDsToAdd []*string `json:"userGroupIDsToAdd,omitempty"` UserGroupIDsToRemove []*string `json:"userGroupIDsToRemove,omitempty"` } + +type User_SDK struct { + ARN *string `json:"arn,omitempty"` + AccessString *string `json:"accessString,omitempty"` + // Indicates whether the user requires a password to authenticate. + Authentication *Authentication `json:"authentication,omitempty"` + Engine *string `json:"engine,omitempty"` + Status *string `json:"status,omitempty"` + UserGroupIDs []*string `json:"userGroupIDs,omitempty"` + UserID *string `json:"userID,omitempty"` + UserName *string `json:"userName,omitempty"` +} diff --git a/apis/v1alpha1/user.go b/apis/v1alpha1/user.go new file mode 100644 index 00000000..380e8733 --- /dev/null +++ b/apis/v1alpha1/user.go @@ -0,0 +1,84 @@ +// 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" +) + +// UserSpec defines the desired state of User +type UserSpec struct { + // Access permissions string used for this user. + // +kubebuilder:validation:Required + AccessString *string `json:"accessString"` + // The current supported value is Redis. + // +kubebuilder:validation:Required + Engine *string `json:"engine"` + // Indicates a password is not required for this user. + NoPasswordRequired *bool `json:"noPasswordRequired,omitempty"` + // The ID of the user. + // +kubebuilder:validation:Required + UserID *string `json:"userID"` + // The username of the user. + // +kubebuilder:validation:Required + UserName *string `json:"userName"` +} + +// UserStatus defines the observed state of User +type UserStatus 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 + 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 + Conditions []*ackv1alpha1.Condition `json:"conditions"` + // Denotes whether the user requires a password to authenticate. + Authentication *Authentication `json:"authentication,omitempty"` + // Access permissions string used for this user. + ExpandedAccessString *string `json:"expandedAccessString,omitempty"` + // Access permissions string used for this user. + LastRequestedAccessString *string `json:"lastRequestedAccessString,omitempty"` + // Indicates the user status. Can be "active", "modifying" or "deleting". + Status *string `json:"status,omitempty"` + // Returns a list of the user group IDs the user belongs to. + UserGroupIDs []*string `json:"userGroupIDs,omitempty"` +} + +// User is the Schema for the Users API +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type User struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec UserSpec `json:"spec,omitempty"` + Status UserStatus `json:"status,omitempty"` +} + +// UserList contains a list of User +// +kubebuilder:object:root=true +type UserList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []User `json:"items"` +} + +func init() { + SchemeBuilder.Register(&User{}, &UserList{}) +} diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 5cdb52e0..9a1f6842 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -32,6 +32,11 @@ func (in *Authentication) DeepCopyInto(out *Authentication) { *out = new(int64) **out = **in } + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication. @@ -1037,6 +1042,37 @@ func (in *Event) DeepCopy() *Event { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Filter) DeepCopyInto(out *Filter) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = make([]*string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(string) + **out = **in + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Filter. +func (in *Filter) DeepCopy() *Filter { + if in == nil { + return nil + } + out := new(Filter) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GlobalNodeGroup) DeepCopyInto(out *GlobalNodeGroup) { *out = *in @@ -3181,14 +3217,41 @@ func (in *UpdateAction) DeepCopy() *UpdateAction { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *User) DeepCopyInto(out *User) { + *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 User. +func (in *User) DeepCopy() *User { + if in == nil { + return nil + } + out := new(User) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *User) 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 *UserGroup) DeepCopyInto(out *UserGroup) { *out = *in if in.ARN != nil { in, out := &in.ARN, &out.ARN *out = new(string) **out = **in } - if in.AccessString != nil { - in, out := &in.AccessString, &out.AccessString + if in.Engine != nil { + in, out := &in.Engine, &out.Engine *out = new(string) **out = **in } @@ -3197,8 +3260,28 @@ func (in *User) DeepCopyInto(out *User) { *out = new(string) **out = **in } - if in.UserGroupIDs != nil { - in, out := &in.UserGroupIDs, &out.UserGroupIDs + if in.UserGroupID != nil { + in, out := &in.UserGroupID, &out.UserGroupID + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserGroup. +func (in *UserGroup) DeepCopy() *UserGroup { + if in == nil { + return nil + } + out := new(UserGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserGroupsUpdateStatus) DeepCopyInto(out *UserGroupsUpdateStatus) { + *out = *in + if in.UserGroupIDsToAdd != nil { + in, out := &in.UserGroupIDsToAdd, &out.UserGroupIDsToAdd *out = make([]*string, len(*in)) for i := range *in { if (*in)[i] != nil { @@ -3208,63 +3291,142 @@ func (in *User) DeepCopyInto(out *User) { } } } - if in.UserID != nil { - in, out := &in.UserID, &out.UserID - *out = new(string) - **out = **in + if in.UserGroupIDsToRemove != nil { + in, out := &in.UserGroupIDsToRemove, &out.UserGroupIDsToRemove + *out = make([]*string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(string) + **out = **in + } + } } - if in.UserName != nil { - in, out := &in.UserName, &out.UserName - *out = new(string) - **out = **in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserGroupsUpdateStatus. +func (in *UserGroupsUpdateStatus) DeepCopy() *UserGroupsUpdateStatus { + if in == nil { + return nil } + out := new(UserGroupsUpdateStatus) + in.DeepCopyInto(out) + return out } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new User. -func (in *User) DeepCopy() *User { +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserList) DeepCopyInto(out *UserList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]User, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserList. +func (in *UserList) DeepCopy() *UserList { if in == nil { return nil } - out := new(User) + out := new(UserList) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *UserList) 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 *UserGroup) DeepCopyInto(out *UserGroup) { +func (in *UserSpec) DeepCopyInto(out *UserSpec) { *out = *in - if in.ARN != nil { - in, out := &in.ARN, &out.ARN + if in.AccessString != nil { + in, out := &in.AccessString, &out.AccessString *out = new(string) **out = **in } - if in.Status != nil { - in, out := &in.Status, &out.Status + if in.Engine != nil { + in, out := &in.Engine, &out.Engine *out = new(string) **out = **in } - if in.UserGroupID != nil { - in, out := &in.UserGroupID, &out.UserGroupID + if in.NoPasswordRequired != nil { + in, out := &in.NoPasswordRequired, &out.NoPasswordRequired + *out = new(bool) + **out = **in + } + if in.UserID != nil { + in, out := &in.UserID, &out.UserID + *out = new(string) + **out = **in + } + if in.UserName != nil { + in, out := &in.UserName, &out.UserName *out = new(string) **out = **in } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserGroup. -func (in *UserGroup) DeepCopy() *UserGroup { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserSpec. +func (in *UserSpec) DeepCopy() *UserSpec { if in == nil { return nil } - out := new(UserGroup) + out := new(UserSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *UserGroupsUpdateStatus) DeepCopyInto(out *UserGroupsUpdateStatus) { +func (in *UserStatus) DeepCopyInto(out *UserStatus) { *out = *in - if in.UserGroupIDsToAdd != nil { - in, out := &in.UserGroupIDsToAdd, &out.UserGroupIDsToAdd + 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.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(Authentication) + (*in).DeepCopyInto(*out) + } + if in.ExpandedAccessString != nil { + in, out := &in.ExpandedAccessString, &out.ExpandedAccessString + *out = new(string) + **out = **in + } + if in.LastRequestedAccessString != nil { + in, out := &in.LastRequestedAccessString, &out.LastRequestedAccessString + *out = new(string) + **out = **in + } + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = new(string) + **out = **in + } + if in.UserGroupIDs != nil { + in, out := &in.UserGroupIDs, &out.UserGroupIDs *out = make([]*string, len(*in)) for i := range *in { if (*in)[i] != nil { @@ -3274,8 +3436,48 @@ func (in *UserGroupsUpdateStatus) DeepCopyInto(out *UserGroupsUpdateStatus) { } } } - if in.UserGroupIDsToRemove != nil { - in, out := &in.UserGroupIDsToRemove, &out.UserGroupIDsToRemove +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserStatus. +func (in *UserStatus) DeepCopy() *UserStatus { + if in == nil { + return nil + } + out := new(UserStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *User_SDK) DeepCopyInto(out *User_SDK) { + *out = *in + if in.ARN != nil { + in, out := &in.ARN, &out.ARN + *out = new(string) + **out = **in + } + if in.AccessString != nil { + in, out := &in.AccessString, &out.AccessString + *out = new(string) + **out = **in + } + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(Authentication) + (*in).DeepCopyInto(*out) + } + if in.Engine != nil { + in, out := &in.Engine, &out.Engine + *out = new(string) + **out = **in + } + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = new(string) + **out = **in + } + if in.UserGroupIDs != nil { + in, out := &in.UserGroupIDs, &out.UserGroupIDs *out = make([]*string, len(*in)) for i := range *in { if (*in)[i] != nil { @@ -3285,14 +3487,24 @@ func (in *UserGroupsUpdateStatus) DeepCopyInto(out *UserGroupsUpdateStatus) { } } } + if in.UserID != nil { + in, out := &in.UserID, &out.UserID + *out = new(string) + **out = **in + } + if in.UserName != nil { + in, out := &in.UserName, &out.UserName + *out = new(string) + **out = **in + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserGroupsUpdateStatus. -func (in *UserGroupsUpdateStatus) DeepCopy() *UserGroupsUpdateStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new User_SDK. +func (in *User_SDK) DeepCopy() *User_SDK { if in == nil { return nil } - out := new(UserGroupsUpdateStatus) + out := new(User_SDK) in.DeepCopyInto(out) return out } diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 34bfbd47..2db18aad 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -34,6 +34,7 @@ import ( _ "github.com/aws-controllers-k8s/elasticache-controller/pkg/resource/cache_subnet_group" _ "github.com/aws-controllers-k8s/elasticache-controller/pkg/resource/replication_group" _ "github.com/aws-controllers-k8s/elasticache-controller/pkg/resource/snapshot" + _ "github.com/aws-controllers-k8s/elasticache-controller/pkg/resource/user" ) var ( diff --git a/config/crd/bases/elasticache.services.k8s.aws_users.yaml b/config/crd/bases/elasticache.services.k8s.aws_users.yaml new file mode 100644 index 00000000..4d2fdac2 --- /dev/null +++ b/config/crd/bases/elasticache.services.k8s.aws_users.yaml @@ -0,0 +1,158 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: users.elasticache.services.k8s.aws +spec: + group: elasticache.services.k8s.aws + names: + kind: User + listKind: UserList + plural: users + singular: user + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: User is the Schema for the Users 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: UserSpec defines the desired state of User + properties: + accessString: + description: Access permissions string used for this user. + type: string + engine: + description: The current supported value is Redis. + type: string + noPasswordRequired: + description: Indicates a password is not required for this user. + type: boolean + userID: + description: The ID of the user. + type: string + userName: + description: The username of the user. + type: string + required: + - accessString + - engine + - userID + - userName + type: object + status: + description: UserStatus defines the observed state of User + 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 + required: + - ownerAccountID + type: object + authentication: + description: Denotes whether the user requires a password to authenticate. + properties: + passwordCount: + format: int64 + type: integer + type_: + 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 + expandedAccessString: + description: Access permissions string used for this user. + type: string + lastRequestedAccessString: + description: Access permissions string used for this user. + type: string + status: + description: Indicates the user status. Can be "active", "modifying" + or "deleting". + type: string + userGroupIDs: + description: Returns a list of the user group IDs the user belongs + to. + items: + type: string + type: array + required: + - ackResourceMetadata + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 3e0b9bfb..bce428cd 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -7,3 +7,4 @@ resources: - bases/elasticache.services.k8s.aws_cachesubnetgroups.yaml - bases/elasticache.services.k8s.aws_replicationgroups.yaml - bases/elasticache.services.k8s.aws_snapshots.yaml + - bases/elasticache.services.k8s.aws_users.yaml diff --git a/config/rbac/cluster-role-controller.yaml b/config/rbac/cluster-role-controller.yaml index ff2b437f..72a2c10c 100644 --- a/config/rbac/cluster-role-controller.yaml +++ b/config/rbac/cluster-role-controller.yaml @@ -102,6 +102,26 @@ rules: - get - patch - update +- apiGroups: + - elasticache.services.k8s.aws + resources: + - users + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - elasticache.services.k8s.aws + resources: + - users/status + verbs: + - get + - patch + - update - apiGroups: - services.k8s.aws resources: diff --git a/config/rbac/role-reader.yaml b/config/rbac/role-reader.yaml index af518033..036ce82f 100644 --- a/config/rbac/role-reader.yaml +++ b/config/rbac/role-reader.yaml @@ -13,6 +13,7 @@ rules: - cachesubnetgroups - replicationgroups - snapshots + - users verbs: - get - list diff --git a/config/rbac/role-writer.yaml b/config/rbac/role-writer.yaml index 2b197d37..e43c5a94 100644 --- a/config/rbac/role-writer.yaml +++ b/config/rbac/role-writer.yaml @@ -13,6 +13,7 @@ rules: - cachesubnetgroups - replicationgroups - snapshots + - users verbs: - create - delete @@ -28,6 +29,7 @@ rules: - cachesubnetgroups - replicationgroups - snapshots + - users verbs: - get - patch diff --git a/generator.yaml b/generator.yaml index 5aeb9063..fbe80d5a 100644 --- a/generator.yaml +++ b/generator.yaml @@ -100,6 +100,39 @@ resources: path: Events update_operation: custom_method_name: customUpdateCacheParameterGroup + User: + exceptions: + terminal_codes: + - UserAlreadyExists + - UserQuotaExceeded + - DuplicateUserName + - InvalidParameterValue + - InvalidParameterCombination + - InvalidUserState + - UserNotFound + - DefaultUserAssociatedToUserGroup + fields: + LastRequestedAccessString: + is_read_only: true + from: + operation: CreateUser + path: AccessString + ExpandedAccessString: + is_read_only: true + from: + operation: CreateUser + path: AccessString + hooks: + sdk_read_many_post_set_output: + code: "rm.setSyncedCondition(resp.Users[0].Status, &resource{ko})" + sdk_create_post_set_output: + code: "rm.setSyncedCondition(resp.Status, &resource{ko})" + sdk_update_post_build_request: + code: "rm.populateUpdatePayload(input, desired, delta)" + sdk_update_post_set_output: + code: "rm.setSyncedCondition(resp.Status, &resource{ko})" + delta_post_compare: + code: "filterDelta(delta, a, b)" operations: DescribeCacheSubnetGroups: set_output_custom_method_name: CustomDescribeCacheSubnetGroupsSetOutput @@ -121,17 +154,26 @@ operations: set_output_custom_method_name: CustomCreateCacheParameterGroupSetOutput DescribeCacheParameterGroups: set_output_custom_method_name: CustomDescribeCacheParameterGroupsSetOutput + CreateUser: + set_output_custom_method_name: CustomCreateUserSetOutput + ModifyUser: + custom_implementation: CustomModifyUser + set_output_custom_method_name: CustomModifyUserSetOutput ignore: resource_names: - GlobalReplicationGroup - CacheCluster - CacheSecurityGroup - - User - UserGroup field_paths: - DescribeSnapshotsInput.CacheClusterId - DescribeSnapshotsInput.ReplicationGroupId - DescribeSnapshotsInput.SnapshotSource + - DescribeUsersInput.Engine + - CreateUserInput.Passwords #TODO: remove this once we have support for k8s secrets within slices + - ModifyUserInput.AccessString + - ModifyUserInput.NoPasswordRequired + - ModifyUserInput.Passwords - ModifyReplicationGroupInput.SecurityGroupIds - ModifyReplicationGroupInput.EngineVersion - CreateReplicationGroupInput.GlobalReplicationGroupId diff --git a/pkg/resource/user/custom_set_output.go b/pkg/resource/user/custom_set_output.go new file mode 100644 index 00000000..8846f03b --- /dev/null +++ b/pkg/resource/user/custom_set_output.go @@ -0,0 +1,58 @@ +// 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 user + +import ( + "context" + + svcapitypes "github.com/aws-controllers-k8s/elasticache-controller/apis/v1alpha1" + svcsdk "github.com/aws/aws-sdk-go/service/elasticache" +) + +// set the custom Status fields upon creation +func (rm *resourceManager) CustomCreateUserSetOutput( + ctx context.Context, + r *resource, + resp *svcsdk.CreateUserOutput, + ko *svcapitypes.User, +) (*svcapitypes.User, error) { + return rm.CustomSetOutput(r, resp.AccessString, ko) +} + +// precondition: successful ModifyUserWithContext call +// By updating 'latest' Status fields, these changes should be applied to 'desired' +// upon patching +func (rm *resourceManager) CustomModifyUserSetOutput( + ctx context.Context, + r *resource, + resp *svcsdk.ModifyUserOutput, + ko *svcapitypes.User, +) (*svcapitypes.User, error) { + return rm.CustomSetOutput(r, resp.AccessString, ko) +} + +func (rm *resourceManager) CustomSetOutput( + r *resource, + responseAccessString *string, + ko *svcapitypes.User, +) (*svcapitypes.User, error) { + + lastRequested := *r.ko.Spec.AccessString + ko.Status.LastRequestedAccessString = &lastRequested + + expandedAccessStringValue := *responseAccessString + ko.Status.ExpandedAccessString = &expandedAccessStringValue + + return ko, nil +} diff --git a/pkg/resource/user/custom_update.go b/pkg/resource/user/custom_update.go new file mode 100644 index 00000000..7b5cefa5 --- /dev/null +++ b/pkg/resource/user/custom_update.go @@ -0,0 +1,41 @@ +// 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 user + +import ( + "context" + "github.com/pkg/errors" + + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + "github.com/aws-controllers-k8s/runtime/pkg/requeue" +) + +// currently this function's only purpose is to requeue if the resource is currently unavailable +func (rm *resourceManager) CustomModifyUser( + ctx context.Context, + desired *resource, + latest *resource, + delta *ackcompare.Delta, +) (*resource, error) { + + // requeue if necessary + latestStatus := latest.ko.Status.Status + if latestStatus == nil || *latestStatus != "active" { + return nil, requeue.NeededAfter( + errors.New("User cannot be modified as its status is not 'active'."), + requeue.DefaultRequeueAfterDuration) + } + + return nil, nil +} diff --git a/pkg/resource/user/delta.go b/pkg/resource/user/delta.go new file mode 100644 index 00000000..dfe04807 --- /dev/null +++ b/pkg/resource/user/delta.go @@ -0,0 +1,73 @@ +// 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 user + +import ( + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" +) + +// 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.AccessString, b.ko.Spec.AccessString) { + delta.Add("Spec.AccessString", a.ko.Spec.AccessString, b.ko.Spec.AccessString) + } else if a.ko.Spec.AccessString != nil && b.ko.Spec.AccessString != nil { + if *a.ko.Spec.AccessString != *b.ko.Spec.AccessString { + delta.Add("Spec.AccessString", a.ko.Spec.AccessString, b.ko.Spec.AccessString) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.Engine, b.ko.Spec.Engine) { + delta.Add("Spec.Engine", a.ko.Spec.Engine, b.ko.Spec.Engine) + } else if a.ko.Spec.Engine != nil && b.ko.Spec.Engine != nil { + if *a.ko.Spec.Engine != *b.ko.Spec.Engine { + delta.Add("Spec.Engine", a.ko.Spec.Engine, b.ko.Spec.Engine) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.NoPasswordRequired, b.ko.Spec.NoPasswordRequired) { + delta.Add("Spec.NoPasswordRequired", a.ko.Spec.NoPasswordRequired, b.ko.Spec.NoPasswordRequired) + } else if a.ko.Spec.NoPasswordRequired != nil && b.ko.Spec.NoPasswordRequired != nil { + if *a.ko.Spec.NoPasswordRequired != *b.ko.Spec.NoPasswordRequired { + delta.Add("Spec.NoPasswordRequired", a.ko.Spec.NoPasswordRequired, b.ko.Spec.NoPasswordRequired) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.UserID, b.ko.Spec.UserID) { + delta.Add("Spec.UserID", a.ko.Spec.UserID, b.ko.Spec.UserID) + } else if a.ko.Spec.UserID != nil && b.ko.Spec.UserID != nil { + if *a.ko.Spec.UserID != *b.ko.Spec.UserID { + delta.Add("Spec.UserID", a.ko.Spec.UserID, b.ko.Spec.UserID) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.UserName, b.ko.Spec.UserName) { + delta.Add("Spec.UserName", a.ko.Spec.UserName, b.ko.Spec.UserName) + } else if a.ko.Spec.UserName != nil && b.ko.Spec.UserName != nil { + if *a.ko.Spec.UserName != *b.ko.Spec.UserName { + delta.Add("Spec.UserName", a.ko.Spec.UserName, b.ko.Spec.UserName) + } + } + + filterDelta(delta, a, b) + return delta +} diff --git a/pkg/resource/user/delta_util.go b/pkg/resource/user/delta_util.go new file mode 100644 index 00000000..b981597f --- /dev/null +++ b/pkg/resource/user/delta_util.go @@ -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. + +package user + +import ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + +// remove differences which are not meaningful (i.e. ones that don't warrant a call to rm.Update) +func filterDelta( + delta *ackcompare.Delta, + desired *resource, + latest *resource, +) { + // the returned AccessString can be different than the specified one; as long as the last requested AccessString + // matches the currently desired one, remove this difference from the delta + //TODO: revert this call to Spec.AccessString once we have a new implementation of it + if delta.DifferentAt("AccessString") { + if *desired.ko.Spec.AccessString == *desired.ko.Status.LastRequestedAccessString { + + //TODO: revert the call to Spec.AccessString once removeFromDelta implementation changes + removeFromDelta(delta, "AccessString") + } + } +} + +// remove the Difference corresponding to the given subject from the delta struct +//TODO: ideally this would have a common implementation in compare/delta.go +func removeFromDelta( + delta *ackcompare.Delta, + subject string, +) { + // copy slice + differences := delta.Differences + + // identify index of Difference to remove + //TODO: change once we get a Path.Equals or similar method + var i *int = nil + for j, diff := range differences { + if diff.Path.Contains(subject) { + i = &j + break + } + } + + // if found, create a new slice and replace the original + if i != nil { + differences = append(differences[:*i], differences[*i+1:]...) + delta.Differences = differences + } +} diff --git a/pkg/resource/user/descriptor.go b/pkg/resource/user/descriptor.go new file mode 100644 index 00000000..cb38dc7b --- /dev/null +++ b/pkg/resource/user/descriptor.go @@ -0,0 +1,163 @@ +// 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 user + +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" + k8sapirt "k8s.io/apimachinery/pkg/runtime" + k8sctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + svcapitypes "github.com/aws-controllers-k8s/elasticache-controller/apis/v1alpha1" +) + +const ( + finalizerString = "finalizers.elasticache.services.k8s.aws/User" +) + +var ( + resourceGK = metav1.GroupKind{ + Group: "elasticache.services.k8s.aws", + Kind: "User", + } +) + +// 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 &resourceGK +} + +// EmptyRuntimeObject returns an empty object prototype that may be used in +// apimachinery and k8s client operations +func (d *resourceDescriptor) EmptyRuntimeObject() k8sapirt.Object { + return &svcapitypes.User{} +} + +// ResourceFromRuntimeObject returns an AWSResource that has been initialized +// with the supplied runtime.Object +func (d *resourceDescriptor) ResourceFromRuntimeObject( + obj k8sapirt.Object, +) acktypes.AWSResource { + return &resource{ + ko: obj.(*svcapitypes.User), + } +} + +// 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)) +} + +// UpdateCRStatus accepts an AWSResource object and changes the Status +// sub-object of the AWSResource's Kubernetes custom resource (CR) and +// returns whether any changes were made +func (d *resourceDescriptor) UpdateCRStatus( + res acktypes.AWSResource, +) (bool, error) { + updated := true + return updated, nil +} + +// 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.RuntimeMetaObject() + 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 acktypes.RuntimeMetaObject, 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.RuntimeMetaObject() + 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.RuntimeMetaObject() + 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.RuntimeMetaObject() + if obj == nil { + // Should not happen. If it does, there is a bug in the code + panic("nil RuntimeMetaObject 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/user/identifiers.go b/pkg/resource/user/identifiers.go new file mode 100644 index 00000000..be1538e2 --- /dev/null +++ b/pkg/resource/user/identifiers.go @@ -0,0 +1,46 @@ +// 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 user + +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 +} diff --git a/pkg/resource/user/manager.go b/pkg/resource/user/manager.go new file mode 100644 index 00000000..9e4f5e85 --- /dev/null +++ b/pkg/resource/user/manager.go @@ -0,0 +1,222 @@ +// 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 user + +import ( + "context" + "fmt" + ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" + corev1 "k8s.io/api/core/v1" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + 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" + + svcsdk "github.com/aws/aws-sdk-go/service/elasticache" + svcsdkapi "github.com/aws/aws-sdk-go/service/elasticache/elasticacheiface" +) + +// +kubebuilder:rbac:groups=elasticache.services.k8s.aws,resources=users,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=elasticache.services.k8s.aws,resources=users/status,verbs=get;update;patch + +// 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.ElastiCacheAPI +} + +// 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 { + 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. +func (rm *resourceManager) Delete( + ctx context.Context, + res 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") + } + return rm.sdkDelete(ctx, r) +} + +// 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:elasticache:%s:%s:%s", + rm.awsRegion, + rm.awsAccountID, + name, + ) +} + +// 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) { + r1, updated := rm.updateConditions(r, 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) { + r1, updated := rm.updateConditions(r, nil) + if !updated { + return r, nil + } + return r1, nil +} diff --git a/pkg/resource/user/manager_factory.go b/pkg/resource/user/manager_factory.go new file mode 100644 index 00000000..b96caef6 --- /dev/null +++ b/pkg/resource/user/manager_factory.go @@ -0,0 +1,90 @@ +// 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 user + +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/elasticache-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 +} + +func newResourceManagerFactory() *resourceManagerFactory { + return &resourceManagerFactory{ + rmCache: map[string]*resourceManager{}, + } +} + +func init() { + svcresource.RegisterManagerFactory(newResourceManagerFactory()) +} diff --git a/pkg/resource/user/post_build_request.go b/pkg/resource/user/post_build_request.go new file mode 100644 index 00000000..6e3f12a4 --- /dev/null +++ b/pkg/resource/user/post_build_request.go @@ -0,0 +1,41 @@ +// 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 user + +import ( + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + svcsdk "github.com/aws/aws-sdk-go/service/elasticache" +) + +// TODO: this should be generated in the future. In general, it doesn't seem like a good idea to add every non-nil +// Spec field in desired.Spec to the payload (i.e. what we do when building most inputs), unless there is +// actually a difference in the Spec field between desired and latest +func (rm *resourceManager) populateUpdatePayload( + input *svcsdk.ModifyUserInput, + r *resource, + delta *ackcompare.Delta, +) { + //TODO: change all calls to DifferentAt to include full path once the implementation is clarified + + if delta.DifferentAt("AccessString") && r.ko.Spec.AccessString != nil { + input.AccessString = r.ko.Spec.AccessString + } + + if delta.DifferentAt("NoPasswordRequired") && r.ko.Spec.NoPasswordRequired != nil { + input.NoPasswordRequired = r.ko.Spec.NoPasswordRequired + } + + //TODO: add the passwords field here once we have secrets support for it + +} diff --git a/pkg/resource/user/post_set_output.go b/pkg/resource/user/post_set_output.go new file mode 100644 index 00000000..8c1d576e --- /dev/null +++ b/pkg/resource/user/post_set_output.go @@ -0,0 +1,63 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package user + +import ( + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + corev1 "k8s.io/api/core/v1" +) + +/* + This file contains functions to update the state of the resource where the generated code or the set_output + functions are insufficient +*/ + +// set the ResourceSynced condition based on the User's Status. r is a wrapper around the User resource which will +// eventually be returned as "latest" +func (rm *resourceManager) setSyncedCondition( + status *string, + r *resource, +) { + // determine whether the resource can be considered synced + syncedStatus := corev1.ConditionUnknown + if status != nil { + if *status == "active" { + syncedStatus = corev1.ConditionTrue + } else { + syncedStatus = corev1.ConditionFalse + } + + } + + // TODO: add utility function in a common repo to do the below as it's done at least once per resource + + // set existing condition to the above status (or create a new condition with this status) + ko := r.ko + 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: syncedStatus, + } + ko.Status.Conditions = append(ko.Status.Conditions, resourceSyncedCondition) + } else { + resourceSyncedCondition.Status = syncedStatus + } +} diff --git a/pkg/resource/user/resource.go b/pkg/resource/user/resource.go new file mode 100644 index 00000000..9a98f15d --- /dev/null +++ b/pkg/resource/user/resource.go @@ -0,0 +1,90 @@ +// 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 user + +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" + k8srt "k8s.io/apimachinery/pkg/runtime" + + svcapitypes "github.com/aws-controllers-k8s/elasticache-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.User +} + +// 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() k8srt.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 +} + +// RuntimeMetaObject returns an object that implements both the Kubernetes +// apimachinery/runtime.Object and the Kubernetes +// apimachinery/apis/meta/v1.Object interfaces +func (r *resource) RuntimeMetaObject() acktypes.RuntimeMetaObject { + return r.ko +} + +// Conditions returns the ACK Conditions collection for the AWSResource +func (r *resource) Conditions() []*ackv1alpha1.Condition { + return r.ko.Status.Conditions +} + +// SetObjectMeta sets the ObjectMeta field for the resource +func (r *resource) SetObjectMeta(meta metav1.ObjectMeta) { + r.ko.ObjectMeta = meta +} + +// 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 == nil { + return ackerrors.MissingNameIdentifier + } + r.ko.Spec.UserID = identifier.NameOrID + return nil +} diff --git a/pkg/resource/user/sdk.go b/pkg/resource/user/sdk.go new file mode 100644 index 00000000..8c8b63cc --- /dev/null +++ b/pkg/resource/user/sdk.go @@ -0,0 +1,471 @@ +// 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 user + +import ( + "context" + "strings" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" + "github.com/aws/aws-sdk-go/aws" + svcsdk "github.com/aws/aws-sdk-go/service/elasticache" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + svcapitypes "github.com/aws-controllers-k8s/elasticache-controller/apis/v1alpha1" +) + +// Hack to avoid import errors during build... +var ( + _ = &metav1.Time{} + _ = strings.ToLower("") + _ = &aws.JSONValue{} + _ = &svcsdk.ElastiCache{} + _ = &svcapitypes.User{} + _ = ackv1alpha1.AWSAccountID("") + _ = &ackerr.NotFound +) + +// sdkFind returns SDK-specific information about a supplied resource +func (rm *resourceManager) sdkFind( + ctx context.Context, + r *resource, +) (*resource, error) { + input, err := rm.newListRequestPayload(r) + if err != nil { + return nil, err + } + + resp, respErr := rm.sdkapi.DescribeUsersWithContext(ctx, input) + rm.metrics.RecordAPICall("READ_MANY", "DescribeUsers", respErr) + if respErr != nil { + if awsErr, ok := ackerr.AWSError(respErr); ok && awsErr.Code() == "UserNotFound" { + return nil, ackerr.NotFound + } + 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() + + found := false + for _, elem := range resp.Users { + 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.AccessString != nil { + ko.Spec.AccessString = elem.AccessString + } else { + ko.Spec.AccessString = nil + } + if elem.Authentication != nil { + f2 := &svcapitypes.Authentication{} + if elem.Authentication.PasswordCount != nil { + f2.PasswordCount = elem.Authentication.PasswordCount + } + if elem.Authentication.Type != nil { + f2.Type = elem.Authentication.Type + } + ko.Status.Authentication = f2 + } else { + ko.Status.Authentication = nil + } + if elem.Engine != nil { + ko.Spec.Engine = elem.Engine + } else { + ko.Spec.Engine = nil + } + if elem.Status != nil { + ko.Status.Status = elem.Status + } else { + ko.Status.Status = nil + } + if elem.UserGroupIds != nil { + f5 := []*string{} + for _, f5iter := range elem.UserGroupIds { + var f5elem string + f5elem = *f5iter + f5 = append(f5, &f5elem) + } + ko.Status.UserGroupIDs = f5 + } else { + ko.Status.UserGroupIDs = nil + } + if elem.UserId != nil { + ko.Spec.UserID = elem.UserId + } else { + ko.Spec.UserID = nil + } + if elem.UserName != nil { + ko.Spec.UserName = elem.UserName + } else { + ko.Spec.UserName = nil + } + found = true + break + } + if !found { + return nil, ackerr.NotFound + } + + rm.setStatusDefaults(ko) + + rm.setSyncedCondition(resp.Users[0].Status, &resource{ko}) + return &resource{ko}, 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.DescribeUsersInput, error) { + res := &svcsdk.DescribeUsersInput{} + + if r.ko.Spec.UserID != nil { + res.SetUserId(*r.ko.Spec.UserID) + } + + return res, nil +} + +// sdkCreate creates the supplied resource in the backend AWS service API and +// returns a new resource with any fields in the Status field filled in +func (rm *resourceManager) sdkCreate( + ctx context.Context, + r *resource, +) (*resource, error) { + input, err := rm.newCreateRequestPayload(ctx, r) + if err != nil { + return nil, err + } + + resp, respErr := rm.sdkapi.CreateUserWithContext(ctx, input) + rm.metrics.RecordAPICall("CREATE", "CreateUser", 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.ARN != nil { + arn := ackv1alpha1.AWSResourceName(*resp.ARN) + ko.Status.ACKResourceMetadata.ARN = &arn + } + if resp.Authentication != nil { + f2 := &svcapitypes.Authentication{} + if resp.Authentication.PasswordCount != nil { + f2.PasswordCount = resp.Authentication.PasswordCount + } + if resp.Authentication.Type != nil { + f2.Type = resp.Authentication.Type + } + ko.Status.Authentication = f2 + } else { + ko.Status.Authentication = nil + } + if resp.Status != nil { + ko.Status.Status = resp.Status + } else { + ko.Status.Status = nil + } + if resp.UserGroupIds != nil { + f5 := []*string{} + for _, f5iter := range resp.UserGroupIds { + var f5elem string + f5elem = *f5iter + f5 = append(f5, &f5elem) + } + ko.Status.UserGroupIDs = f5 + } else { + ko.Status.UserGroupIDs = nil + } + + rm.setStatusDefaults(ko) + + // custom set output from response + ko, err = rm.CustomCreateUserSetOutput(ctx, r, resp, ko) + if err != nil { + return nil, err + } + + rm.setSyncedCondition(resp.Status, &resource{ko}) + 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.CreateUserInput, error) { + res := &svcsdk.CreateUserInput{} + + if r.ko.Spec.AccessString != nil { + res.SetAccessString(*r.ko.Spec.AccessString) + } + if r.ko.Spec.Engine != nil { + res.SetEngine(*r.ko.Spec.Engine) + } + if r.ko.Spec.NoPasswordRequired != nil { + res.SetNoPasswordRequired(*r.ko.Spec.NoPasswordRequired) + } + if r.ko.Spec.UserID != nil { + res.SetUserId(*r.ko.Spec.UserID) + } + if r.ko.Spec.UserName != nil { + res.SetUserName(*r.ko.Spec.UserName) + } + + 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) { + + customResp, customRespErr := rm.CustomModifyUser(ctx, desired, latest, delta) + if customResp != nil || customRespErr != nil { + return customResp, customRespErr + } + + input, err := rm.newUpdateRequestPayload(ctx, desired) + if err != nil { + return nil, err + } + rm.populateUpdatePayload(input, desired, delta) + + resp, respErr := rm.sdkapi.ModifyUserWithContext(ctx, input) + rm.metrics.RecordAPICall("UPDATE", "ModifyUser", 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 := desired.ko.DeepCopy() + + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + if resp.ARN != nil { + arn := ackv1alpha1.AWSResourceName(*resp.ARN) + ko.Status.ACKResourceMetadata.ARN = &arn + } + if resp.Authentication != nil { + f2 := &svcapitypes.Authentication{} + if resp.Authentication.PasswordCount != nil { + f2.PasswordCount = resp.Authentication.PasswordCount + } + if resp.Authentication.Type != nil { + f2.Type = resp.Authentication.Type + } + ko.Status.Authentication = f2 + } else { + ko.Status.Authentication = nil + } + if resp.Status != nil { + ko.Status.Status = resp.Status + } else { + ko.Status.Status = nil + } + if resp.UserGroupIds != nil { + f5 := []*string{} + for _, f5iter := range resp.UserGroupIds { + var f5elem string + f5elem = *f5iter + f5 = append(f5, &f5elem) + } + ko.Status.UserGroupIDs = f5 + } else { + ko.Status.UserGroupIDs = nil + } + + rm.setStatusDefaults(ko) + + // custom set output from response + ko, err = rm.CustomModifyUserSetOutput(ctx, desired, resp, ko) + if err != nil { + return nil, err + } + + rm.setSyncedCondition(resp.Status, &resource{ko}) + return &resource{ko}, nil +} + +// newUpdateRequestPayload returns an SDK-specific struct for the HTTP request +// payload of the Update API call for the resource +func (rm *resourceManager) newUpdateRequestPayload( + ctx context.Context, + r *resource, +) (*svcsdk.ModifyUserInput, error) { + res := &svcsdk.ModifyUserInput{} + + if r.ko.Spec.UserID != nil { + res.SetUserId(*r.ko.Spec.UserID) + } + + return res, nil +} + +// sdkDelete deletes the supplied resource in the backend AWS service API +func (rm *resourceManager) sdkDelete( + ctx context.Context, + r *resource, +) error { + + input, err := rm.newDeleteRequestPayload(r) + if err != nil { + return err + } + _, respErr := rm.sdkapi.DeleteUserWithContext(ctx, input) + rm.metrics.RecordAPICall("DELETE", "DeleteUser", respErr) + return respErr +} + +// 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.DeleteUserInput, error) { + res := &svcsdk.DeleteUserInput{} + + if r.ko.Spec.UserID != nil { + res.SetUserId(*r.ko.Spec.UserID) + } + + return res, nil +} + +// setStatusDefaults sets default properties into supplied custom resource +func (rm *resourceManager) setStatusDefaults( + ko *svcapitypes.User, +) { + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + 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, + err error, +) (*resource, bool) { + ko := r.ko.DeepCopy() + rm.setStatusDefaults(ko) + + // Terminal condition + var terminalCondition *ackv1alpha1.Condition = nil + var recoverableCondition *ackv1alpha1.Condition = nil + for _, condition := range ko.Status.Conditions { + if condition.Type == ackv1alpha1.ConditionTypeTerminal { + terminalCondition = condition + } + if condition.Type == ackv1alpha1.ConditionTypeRecoverable { + recoverableCondition = condition + } + } + + if rm.terminalAWSError(err) { + if terminalCondition == nil { + terminalCondition = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeTerminal, + } + ko.Status.Conditions = append(ko.Status.Conditions, terminalCondition) + } + terminalCondition.Status = corev1.ConditionTrue + awsErr, _ := ackerr.AWSError(err) + errorMessage := awsErr.Message() + 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.Message() + } + recoverableCondition.Message = &errorMessage + } else if recoverableCondition != nil { + recoverableCondition.Status = corev1.ConditionFalse + recoverableCondition.Message = nil + } + } + if terminalCondition != nil || recoverableCondition != 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 "UserAlreadyExists", + "UserQuotaExceeded", + "DuplicateUserName", + "InvalidParameterValue", + "InvalidParameterCombination", + "InvalidUserState", + "UserNotFound", + "DefaultUserAssociatedToUserGroup": + return true + default: + return false + } +} diff --git a/test/e2e/resources/user.yaml b/test/e2e/resources/user.yaml new file mode 100644 index 00000000..9104b5c3 --- /dev/null +++ b/test/e2e/resources/user.yaml @@ -0,0 +1,10 @@ +apiVersion: elasticache.services.k8s.aws/v1alpha1 +kind: User +metadata: + name: $USER_ID +spec: + accessString: $ACCESS_STRING + engine: redis + noPasswordRequired: true + userID: $USER_ID + userName: $USER_ID \ No newline at end of file diff --git a/test/e2e/tests/test_user.py b/test/e2e/tests/test_user.py new file mode 100644 index 00000000..1ff5fe86 --- /dev/null +++ b/test/e2e/tests/test_user.py @@ -0,0 +1,89 @@ +# 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. + +"""CRUD tests for the Elasticache User resource +""" + +import boto3 +import botocore +import pytest + +from acktest.resources import random_suffix_name +from acktest.k8s import resource as k8s + +from time import sleep +from e2e import service_marker, CRD_GROUP, CRD_VERSION, load_elasticache_resource + +RESOURCE_PLURAL = "users" +DEFAULT_WAIT_SECS = 90 + + +@pytest.fixture(scope="module") +def elasticache_client(): + return boto3.client("elasticache") + + +# set up input parameters for User +@pytest.fixture(scope="module") +def input_dict(): + resource_name = random_suffix_name("test-user", 32) + input_dict = { + "USER_ID": resource_name, + "ACCESS_STRING": "on ~app::* -@all +@read" + } + return input_dict + + +@pytest.fixture(scope="module") +def user(input_dict, elasticache_client): + + # inject parameters into yaml; create User in cluster + user = load_elasticache_resource("user", additional_replacements=input_dict) + reference = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, input_dict["USER_ID"], namespace="default") + _ = k8s.create_custom_resource(reference, user) + resource = k8s.wait_resource_consumed_by_controller(reference) + assert resource is not None + yield (reference, resource) + + # teardown: delete in k8s, assert user does not exist in AWS + k8s.delete_custom_resource(reference) + sleep(DEFAULT_WAIT_SECS) + with pytest.raises(botocore.exceptions.ClientError, match="UserNotFound"): + _ = elasticache_client.describe_users(UserId=input_dict["USER_ID"]) + + +@service_marker +class TestUser: + + # TODO: add more scenarios once the passwords field is enabled + + # CRUD test for User; "create" and "delete" operations implicit in "user" fixture + def test_CRUD(self, user, input_dict): + (reference, resource) = user + assert k8s.get_resource_exists(reference) + + assert k8s.wait_on_condition(reference, "ACK.ResourceSynced", "True", wait_periods=5) + resource = k8s.get_resource(reference) + assert resource["status"]["lastRequestedAccessString"] == input_dict["ACCESS_STRING"] + + new_access_string = "on ~app::* -@all +@read +@write" + user_patch = {"spec": {"accessString": new_access_string}} + _ = k8s.patch_custom_resource(reference, user_patch) + sleep(DEFAULT_WAIT_SECS) + + assert k8s.wait_on_condition(reference, "ACK.ResourceSynced", "True", wait_periods=5) + resource = k8s.get_resource(reference) + assert resource["status"]["lastRequestedAccessString"] == new_access_string + + #TODO: add terminal condition checks