diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index e89181f1..d80788ee 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,14 +1,14 @@ ack_generate_info: - build_date: "2021-07-01T00:05:05Z" + build_date: "2021-07-01T01:00:26Z" build_hash: 9fec5f08628d5dcc3af38d517ea014930b1ae39d go_version: go1.15.2 darwin/amd64 version: v0.3.1 -api_directory_checksum: 9b3b2aba266495c0066bcda32c685f5392eaa4c3 +api_directory_checksum: a0e397840365dfef9f86900d211b96e5040cf6a9 api_version: v1alpha1 aws_sdk_go_version: "" generator_config_info: - file_checksum: 4c9de6e616b479328ab112b111a77382551cee21 + file_checksum: fa5597fbe4affa148d62ff2079b8d12ac1e07098 original_file_name: generator.yaml last_modification: reason: API generation - timestamp: 2021-07-01 00:06:30.140979 +0000 UTC \ No newline at end of file + timestamp: 2021-07-01 01:00:35.012299 +0000 UTC \ No newline at end of file diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index 6b1804bf..ae39c9d8 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -129,6 +129,10 @@ resources: from: operation: CreateUser path: AccessString + Passwords: + is_secret: true + compare: + is_ignored: true hooks: sdk_read_many_post_set_output: code: "rm.setSyncedCondition(resp.Users[0].Status, &resource{ko})" @@ -193,7 +197,6 @@ ignore: - 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 diff --git a/apis/v1alpha1/user.go b/apis/v1alpha1/user.go index 2c1b6586..f876ffa3 100644 --- a/apis/v1alpha1/user.go +++ b/apis/v1alpha1/user.go @@ -32,6 +32,9 @@ type UserSpec struct { Engine *string `json:"engine"` // Indicates a password is not required for this user. NoPasswordRequired *bool `json:"noPasswordRequired,omitempty"` + // Passwords used for this user. You can create up to two passwords for each + // user. + Passwords []*ackv1alpha1.SecretKeyReference `json:"passwords,omitempty"` // The ID of the user. // +kubebuilder:validation:Required UserID *string `json:"userID"` diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index b9ef943e..037c72f9 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -3820,6 +3820,17 @@ func (in *UserSpec) DeepCopyInto(out *UserSpec) { *out = new(bool) **out = **in } + if in.Passwords != nil { + in, out := &in.Passwords, &out.Passwords + *out = make([]*corev1alpha1.SecretKeyReference, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(corev1alpha1.SecretKeyReference) + **out = **in + } + } + } if in.UserID != nil { in, out := &in.UserID, &out.UserID *out = new(string) diff --git a/config/crd/bases/elasticache.services.k8s.aws_users.yaml b/config/crd/bases/elasticache.services.k8s.aws_users.yaml index b2fd193c..15e5b3fe 100644 --- a/config/crd/bases/elasticache.services.k8s.aws_users.yaml +++ b/config/crd/bases/elasticache.services.k8s.aws_users.yaml @@ -44,6 +44,28 @@ spec: noPasswordRequired: description: Indicates a password is not required for this user. type: boolean + passwords: + description: Passwords used for this user. You can create up to two + passwords for each user. + items: + description: SecretKeyReference combines a k8s corev1.SecretReference + with a specific key within the referred-to Secret + properties: + key: + description: Key is the key within the secret + type: string + name: + description: Name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: Namespace defines the space within which the secret + name must be unique. + type: string + required: + - key + type: object + type: array userID: description: The ID of the user. type: string diff --git a/generator.yaml b/generator.yaml index 99abef7f..5aedf654 100644 --- a/generator.yaml +++ b/generator.yaml @@ -136,6 +136,10 @@ resources: from: operation: CreateUser path: AccessString + Passwords: + is_secret: true + compare: + is_ignored: true hooks: sdk_read_many_post_set_output: code: "rm.setSyncedCondition(resp.Users[0].Status, &resource{ko})" @@ -200,7 +204,6 @@ ignore: - 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 diff --git a/pkg/resource/user/post_build_request.go b/pkg/resource/user/post_build_request.go index 40e2c4e3..487328cb 100644 --- a/pkg/resource/user/post_build_request.go +++ b/pkg/resource/user/post_build_request.go @@ -34,6 +34,6 @@ func (rm *resourceManager) populateUpdatePayload( input.NoPasswordRequired = r.ko.Spec.NoPasswordRequired } - //TODO: add the passwords field here once we have secrets support for it + //TODO: add update for passwords field once we have framework-level support } diff --git a/pkg/resource/user/sdk.go b/pkg/resource/user/sdk.go index c4b29d7b..c7894d07 100644 --- a/pkg/resource/user/sdk.go +++ b/pkg/resource/user/sdk.go @@ -240,6 +240,23 @@ func (rm *resourceManager) newCreateRequestPayload( if r.ko.Spec.NoPasswordRequired != nil { res.SetNoPasswordRequired(*r.ko.Spec.NoPasswordRequired) } + if r.ko.Spec.Passwords != nil { + f3 := []*string{} + for _, f3iter := range r.ko.Spec.Passwords { + var f3elem string + if f3iter != nil { + tmpSecret, err := rm.rr.SecretValueFromReference(ctx, f3iter) + if err != nil { + return nil, err + } + if tmpSecret != "" { + f3elem = tmpSecret + } + } + f3 = append(f3, &f3elem) + } + res.SetPasswords(f3) + } if r.ko.Spec.UserID != nil { res.SetUserId(*r.ko.Spec.UserID) } diff --git a/test/e2e/resources/user.yaml b/test/e2e/resources/user_nopass.yaml similarity index 100% rename from test/e2e/resources/user.yaml rename to test/e2e/resources/user_nopass.yaml diff --git a/test/e2e/resources/user_password.yaml b/test/e2e/resources/user_password.yaml new file mode 100644 index 00000000..6afa297d --- /dev/null +++ b/test/e2e/resources/user_password.yaml @@ -0,0 +1,16 @@ +apiVersion: elasticache.services.k8s.aws/v1alpha1 +kind: User +metadata: + name: $USER_ID +spec: + accessString: $ACCESS_STRING + engine: redis + passwords: + - namespace: default + name: $NAME1 + key: $KEY1 + - namespace: default + name: $NAME2 + key: $KEY2 + userID: $USER_ID + userName: $USER_ID \ No newline at end of file diff --git a/test/e2e/tests/test_replicationgroup.py b/test/e2e/tests/test_replicationgroup.py index 623d5340..735dd57c 100644 --- a/test/e2e/tests/test_replicationgroup.py +++ b/test/e2e/tests/test_replicationgroup.py @@ -87,33 +87,35 @@ def rg_input_coverage(bootstrap_resources, make_rg_name, make_replication_group, sleep(DEFAULT_WAIT_SECS) rg_deletion_waiter.wait(ReplicationGroupId=input_dict["RG_ID"]) #throws exception if wait fails - @pytest.fixture(scope="module") -def first_secret(): - k8s.create_opaque_secret("default", "first", "secret1", "securetoken123456") - yield - k8s.delete_secret("default", "first") - +def secrets(): + secrets = { + "NAME1": random_suffix_name("first", 32), + "NAME2": random_suffix_name("second", 32), + "KEY1": "secret1", + "KEY2": "secret2" + } + k8s.create_opaque_secret("default", secrets['NAME1'], secrets['KEY1'], random_suffix_name("token", 32)) + k8s.create_opaque_secret("default", secrets['NAME2'], secrets['KEY2'], random_suffix_name("token", 32)) + yield secrets -@pytest.fixture(scope="module") -def second_secret(): - k8s.create_opaque_secret("default", "second", "secret2", "newsecuretoken123456") - yield - k8s.delete_secret("default", "second") + # teardown + k8s.delete_secret("default", secrets['NAME1']) + k8s.delete_secret("default", secrets['NAME2']) @pytest.fixture(scope="module") -def rg_auth_token(make_rg_name, make_replication_group, rg_deletion_waiter, first_secret, second_secret): +def rg_auth_token(make_rg_name, make_replication_group, rg_deletion_waiter, secrets): input_dict = { "RG_ID": make_rg_name("rg-auth-token"), - "NAME": "first", - "KEY": "secret1" + "NAME": secrets['NAME1'], + "KEY": secrets['KEY1'] } (reference, resource) = make_replication_group("replicationgroup_authtoken", input_dict, input_dict["RG_ID"]) yield (reference, resource) k8s.delete_custom_resource(reference) sleep(DEFAULT_WAIT_SECS) - rg_deletion_waiter.wait(ReplicationGroupId=input_dict["RG_ID"]) #throws exception if wait fails + rg_deletion_waiter.wait(ReplicationGroupId=input_dict["RG_ID"]) # throws exception if wait fails @pytest.fixture(scope="module") @@ -230,22 +232,13 @@ def test_rg_cmd_update(self, rg_cmd_update_input, rg_cmd_update): assert cc is not None assert cc['EngineVersion'] == desired_engine_version - # TODO: remove annotation once https://github.com/aws-controllers-k8s/community/issues/745 is resolved - @pytest.mark.blocked - def test_rg_auth_token(self, rg_auth_token): + def test_rg_auth_token(self, rg_auth_token, secrets): (reference, _) = rg_auth_token assert k8s.wait_on_condition(reference, "ACK.ResourceSynced", "True", wait_periods=30) - update_dict = { - "RG_ID": reference.name, - "NAME": "second", - "KEY": "secret2" - } - - updated_spec = load_elasticache_resource( - "replicationgroup_authtoken", additional_replacements=update_dict) - - k8s.patch_custom_resource(reference, updated_spec) + patch = {"spec": {"authToken": {"name": secrets['NAME2'], "key": secrets['KEY2']}}} + k8s.patch_custom_resource(reference, patch) + sleep(DEFAULT_WAIT_SECS) assert k8s.wait_on_condition(reference, "ACK.ResourceSynced", "True", wait_periods=30) def test_rg_deletion(self, rg_deletion_input, rg_deletion, rg_deletion_waiter): diff --git a/test/e2e/tests/test_user.py b/test/e2e/tests/test_user.py index 1ff5fe86..c6e89f5e 100644 --- a/test/e2e/tests/test_user.py +++ b/test/e2e/tests/test_user.py @@ -35,22 +35,20 @@ def elasticache_client(): # 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, +def user_nopass_input(): + return { + "USER_ID": random_suffix_name("user-nopass", 32), "ACCESS_STRING": "on ~app::* -@all +@read" } - return input_dict @pytest.fixture(scope="module") -def user(input_dict, elasticache_client): +def user_nopass(user_nopass_input, elasticache_client): # inject parameters into yaml; create User in cluster - user = load_elasticache_resource("user", additional_replacements=input_dict) + user = load_elasticache_resource("user_nopass", additional_replacements=user_nopass_input) reference = k8s.CustomResourceReference( - CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, input_dict["USER_ID"], namespace="default") + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, user_nopass_input["USER_ID"], namespace="default") _ = k8s.create_custom_resource(reference, user) resource = k8s.wait_resource_consumed_by_controller(reference) assert resource is not None @@ -60,22 +58,67 @@ def user(input_dict, elasticache_client): 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"]) + _ = elasticache_client.describe_users(UserId=user_nopass_input["USER_ID"]) + + +# create secrets for below user password test +@pytest.fixture(scope="module") +def secrets(): + secrets = { + "NAME1": random_suffix_name("first", 32), + "NAME2": random_suffix_name("second", 32), + "KEY1": "secret1", + "KEY2": "secret2" + } + k8s.create_opaque_secret("default", secrets['NAME1'], secrets['KEY1'], random_suffix_name("password", 32)) + k8s.create_opaque_secret("default", secrets['NAME2'], secrets['KEY2'], random_suffix_name("password", 32)) + yield secrets + + # teardown + k8s.delete_secret("default", secrets['NAME1']) + k8s.delete_secret("default", secrets['NAME2']) + + +# input for test case with Passwords field +@pytest.fixture(scope="module") +def user_password_input(secrets): + inputs = { + "USER_ID": random_suffix_name("user-password", 32), + "ACCESS_STRING": "on ~app::* -@all +@read", + } + return {**secrets, **inputs} + + +@pytest.fixture(scope="module") +def user_password(user_password_input, elasticache_client): + + # inject parameters into yaml; create User in cluster + user = load_elasticache_resource("user_password", additional_replacements=user_password_input) + reference = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, user_password_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) + with pytest.raises(botocore.exceptions.ClientError, match="UserNotFound"): + _ = elasticache_client.describe_users(UserId=user_password_input["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 + def test_user_nopass(self, user_nopass, user_nopass_input): + (reference, resource) = user_nopass 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"] + assert resource["status"]["lastRequestedAccessString"] == user_nopass_input["ACCESS_STRING"] new_access_string = "on ~app::* -@all +@read +@write" user_patch = {"spec": {"accessString": new_access_string}} @@ -87,3 +130,14 @@ def test_CRUD(self, user, input_dict): assert resource["status"]["lastRequestedAccessString"] == new_access_string #TODO: add terminal condition checks + + # test creation with Passwords specified (as k8s secrets) + def test_user_password(self, user_password, user_password_input): + (reference, resource) = user_password + 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"]["authentication"] is not None + assert resource["status"]["authentication"]["type_"] == "password" + assert resource["status"]["authentication"]["passwordCount"] == 2