diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index 8c03322..ef61702 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -457,8 +457,6 @@ ignore: - CacheCluster.LogDeliveryConfigurations - CacheCluster.PendingModifiedValues.ScaleConfig - PendingModifiedValues.LogDeliveryConfigurations - - CreateUserInput.AuthenticationMode - - ModifyUserInput.AuthenticationMode - CreateCacheSubnetGroupOutput.CacheSubnetGroup.SupportedNetworkTypes - CreateCacheSubnetGroupOutput.CacheSubnetGroup.Subnets.SupportedNetworkTypes - ModifyCacheSubnetGroupOutput.CacheSubnetGroup.Subnets.SupportedNetworkTypes diff --git a/apis/v1alpha1/types.go b/apis/v1alpha1/types.go index 69122b0..a3139c6 100644 --- a/apis/v1alpha1/types.go +++ b/apis/v1alpha1/types.go @@ -36,7 +36,9 @@ type Authentication struct { // Specifies the authentication mode to use. type AuthenticationMode struct { - Passwords []*string `json:"passwords,omitempty"` + // Specifies the authentication type. Possible options are IAM authentication, + // password and no password. + Type *string `json:"type,omitempty"` } // Describes an Availability Zone in which the cluster is launched. diff --git a/apis/v1alpha1/user.go b/apis/v1alpha1/user.go index c080bf8..3b7b99a 100644 --- a/apis/v1alpha1/user.go +++ b/apis/v1alpha1/user.go @@ -30,6 +30,9 @@ type UserSpec struct { // Regex Pattern: `\S` // +kubebuilder:validation:Required AccessString *string `json:"accessString"` + // Specifies the authentication mode to use. Includes the authentication type + // (e.g. iam, no-password-required, password) and, if applicable, passwords. + AuthenticationMode *AuthenticationMode `json:"authenticationMode,omitempty"` // The options are valkey or redis. // // Regex Pattern: `^[a-zA-Z]*$` diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index fd081a2..39e776e 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -52,16 +52,10 @@ func (in *Authentication) DeepCopy() *Authentication { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AuthenticationMode) DeepCopyInto(out *AuthenticationMode) { *out = *in - if in.Passwords != nil { - in, out := &in.Passwords, &out.Passwords - *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.Type != nil { + in, out := &in.Type, &out.Type + *out = new(string) + **out = **in } } @@ -5255,6 +5249,11 @@ func (in *UserSpec) DeepCopyInto(out *UserSpec) { *out = new(string) **out = **in } + if in.AuthenticationMode != nil { + in, out := &in.AuthenticationMode, &out.AuthenticationMode + *out = new(AuthenticationMode) + (*in).DeepCopyInto(*out) + } if in.Engine != nil { in, out := &in.Engine, &out.Engine *out = new(string) diff --git a/generator.yaml b/generator.yaml index 8c03322..ef61702 100644 --- a/generator.yaml +++ b/generator.yaml @@ -457,8 +457,6 @@ ignore: - CacheCluster.LogDeliveryConfigurations - CacheCluster.PendingModifiedValues.ScaleConfig - PendingModifiedValues.LogDeliveryConfigurations - - CreateUserInput.AuthenticationMode - - ModifyUserInput.AuthenticationMode - CreateCacheSubnetGroupOutput.CacheSubnetGroup.SupportedNetworkTypes - CreateCacheSubnetGroupOutput.CacheSubnetGroup.Subnets.SupportedNetworkTypes - ModifyCacheSubnetGroupOutput.CacheSubnetGroup.Subnets.SupportedNetworkTypes diff --git a/helm/crds/elasticache.services.k8s.aws_users.yaml b/helm/crds/elasticache.services.k8s.aws_users.yaml index 711cd9e..3040734 100644 --- a/helm/crds/elasticache.services.k8s.aws_users.yaml +++ b/helm/crds/elasticache.services.k8s.aws_users.yaml @@ -44,6 +44,18 @@ spec: Regex Pattern: `\S` type: string + authenticationMode: + description: |- + Specifies the authentication mode to use. Includes the authentication type + (e.g. iam, no-password-required, password). Passwords should be provided + via the spec.passwords field using SecretKeyReference. + properties: + type: + description: |- + Specifies the authentication type. Possible options are IAM authentication, + password and no password. + type: string + type: object engine: description: |- The options are valkey or redis. diff --git a/pkg/resource/user/hooks.go b/pkg/resource/user/hooks.go index 28e2b31..d5ce3e3 100644 --- a/pkg/resource/user/hooks.go +++ b/pkg/resource/user/hooks.go @@ -17,6 +17,7 @@ import ( "context" svcsdk "github.com/aws/aws-sdk-go-v2/service/elasticache" + svcsdktypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -95,6 +96,14 @@ func (rm *resourceManager) populateUpdatePayload( input.AccessString = r.ko.Spec.AccessString } + if delta.DifferentAt("Spec.AuthenticationMode") && r.ko.Spec.AuthenticationMode != nil { + authMode := &svcsdktypes.AuthenticationMode{} + if r.ko.Spec.AuthenticationMode.Type != nil { + authMode.Type = svcsdktypes.InputAuthenticationType(*r.ko.Spec.AuthenticationMode.Type) + } + input.AuthenticationMode = authMode + } + if delta.DifferentAt("Spec.NoPasswordRequired") && r.ko.Spec.NoPasswordRequired != nil { input.NoPasswordRequired = r.ko.Spec.NoPasswordRequired } diff --git a/pkg/resource/user/sdk.go b/pkg/resource/user/sdk.go index 70f9a74..2d4fa5b 100644 --- a/pkg/resource/user/sdk.go +++ b/pkg/resource/user/sdk.go @@ -287,6 +287,13 @@ func (rm *resourceManager) newCreateRequestPayload( if r.ko.Spec.AccessString != nil { res.AccessString = r.ko.Spec.AccessString } + if r.ko.Spec.AuthenticationMode != nil { + authMode := &svcsdktypes.AuthenticationMode{} + if r.ko.Spec.AuthenticationMode.Type != nil { + authMode.Type = svcsdktypes.InputAuthenticationType(*r.ko.Spec.AuthenticationMode.Type) + } + res.AuthenticationMode = authMode + } if r.ko.Spec.Engine != nil { res.Engine = r.ko.Spec.Engine } diff --git a/test/e2e/resources/user_iam.yaml b/test/e2e/resources/user_iam.yaml new file mode 100644 index 0000000..cd716ec --- /dev/null +++ b/test/e2e/resources/user_iam.yaml @@ -0,0 +1,11 @@ +apiVersion: elasticache.services.k8s.aws/v1alpha1 +kind: User +metadata: + name: $USER_ID +spec: + accessString: $ACCESS_STRING + authenticationMode: + type: iam + engine: valkey + userID: $USER_ID + userName: $USER_ID diff --git a/test/e2e/tests/test_user.py b/test/e2e/tests/test_user.py index b0f5a9e..e10e1cc 100644 --- a/test/e2e/tests/test_user.py +++ b/test/e2e/tests/test_user.py @@ -107,6 +107,32 @@ def user_password(user_password_input, elasticache_client): assert_user_deletion(user_password_input['USER_ID']) +@pytest.fixture(scope="module") +def user_iam_input(): + return { + "USER_ID": random_suffix_name("user-iam", 32), + "ACCESS_STRING": "on ~app::* -@all +@read" + } + + +@pytest.fixture(scope="module") +def user_iam(user_iam_input, elasticache_client): + + # inject parameters into yaml; create User in cluster + user = load_elasticache_resource("user_iam", additional_replacements=user_iam_input) + reference = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, user_iam_input["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) + assert_user_deletion(user_iam_input['USER_ID']) + + @service_marker class TestUser: @@ -128,6 +154,17 @@ def test_user_nopass(self, user_nopass, user_nopass_input): resource = k8s.get_resource(reference) assert resource["status"]["lastRequestedAccessString"] == new_access_string + # test creation with IAM authentication mode (valkey engine) + def test_user_iam(self, user_iam, user_iam_input): + (reference, resource) = user_iam + 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"] == user_iam_input["ACCESS_STRING"] + assert resource["status"]["authentication"] is not None + assert resource["status"]["authentication"]["type_"] == "iam" + # test creation with Passwords specified (as k8s secrets) def test_user_password(self, user_password, user_password_input): (reference, resource) = user_password