diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts index e6930b7844d32..bf505c5c526b7 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts @@ -162,6 +162,44 @@ export class ClusterResourceHandler extends ResourceHandler { return { EksUpdateId: updateResponse.update?.id }; } + if (updates.updateTags) { + console.log(`updating cluster tags to ${JSON.stringify(this.newProps.tags)}`); + + const resp = await this.eks.describeCluster({ + name: this.clusterName, + }); + + if (!resp.cluster?.arn) { + throw new Error(`Cannot obtain cluster ARN with cluster name ${this.clusterName}`); + } + + const { oldTags, newTags } = { + oldTags: this.oldProps.tags ?? {}, + newTags: this.newProps.tags ?? {}, + }; + const { oldKeys, newKeys } = { + oldKeys: Object.keys(oldTags), + newKeys: Object.keys(newTags), + }; + + const removeKeys = oldKeys.filter((v) => !newKeys.includes(v)); + if (removeKeys.length) { + await this.eks.untagResource({ + resourceArn: resp.cluster.arn, + tagKeys: removeKeys, + }); + } + + if (newKeys.length) { + await this.eks.tagResource({ + resourceArn: resp.cluster.arn, + tags: newTags, + }); + } + + return; + } + // no updates return; } @@ -309,6 +347,7 @@ interface UpdateMap { updateLogging: boolean; // logging updateEncryption: boolean; // encryption (cannot be updated) updateAccess: boolean; // resourcesVpcConfig.endpointPrivateAccess and endpointPublicAccess + updateTags: boolean; // tags } function analyzeUpdate(oldProps: Partial, newProps: aws.EKS.CreateClusterRequest): UpdateMap { @@ -336,6 +375,7 @@ function analyzeUpdate(oldProps: Partial, newProps updateVersion: newProps.version !== oldProps.version, updateEncryption: JSON.stringify(newEnc) !== JSON.stringify(oldEnc), updateLogging: JSON.stringify(newProps.logging) !== JSON.stringify(oldProps.logging), + updateTags: JSON.stringify(newProps.tags) !== JSON.stringify(oldProps.tags), }; } diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/common.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/common.ts index 21cf958df5a68..e2ca596edc463 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/common.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/common.ts @@ -84,4 +84,6 @@ export interface EksClient { createFargateProfile(request: aws.EKS.CreateFargateProfileRequest): Promise; describeFargateProfile(request: aws.EKS.DescribeFargateProfileRequest): Promise; deleteFargateProfile(request: aws.EKS.DeleteFargateProfileRequest): Promise; + untagResource(req: aws.EKS.UntagResourceRequest): Promise; + tagResource(req: aws.EKS.TagResourceRequest): Promise; } diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/index.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/index.ts index f4db036cc6e48..e1c75145ccc15 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/index.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/index.ts @@ -28,6 +28,8 @@ const defaultEksClient: EksClient = { createFargateProfile: req => getEksClient().createFargateProfile(req).promise(), deleteFargateProfile: req => getEksClient().deleteFargateProfile(req).promise(), describeFargateProfile: req => getEksClient().describeFargateProfile(req).promise(), + untagResource: req => getEksClient().untagResource(req).promise(), + tagResource: req => getEksClient().tagResource(req).promise(), configureAssumeRole: req => { console.log(JSON.stringify({ assumeRole: req }, undefined, 2)); const creds = new aws.ChainableTemporaryCredentials({ diff --git a/packages/@aws-cdk/aws-eks/test/cluster-resource-handler-mocks.ts b/packages/@aws-cdk/aws-eks/test/cluster-resource-handler-mocks.ts index 5081415b4d90a..4b2c2f17c1706 100644 --- a/packages/@aws-cdk/aws-eks/test/cluster-resource-handler-mocks.ts +++ b/packages/@aws-cdk/aws-eks/test/cluster-resource-handler-mocks.ts @@ -16,7 +16,9 @@ export let actualRequest: { createFargateProfile?: sdk.EKS.CreateFargateProfileRequest; describeFargateProfile?: sdk.EKS.DescribeFargateProfileRequest; deleteFargateProfile?: sdk.EKS.DeleteFargateProfileRequest; -} = { }; + untagResourceRequest?: sdk.EKS.UntagResourceRequest; + tagResourceRequest?: sdk.EKS.TagResourceRequest; +} = {}; /** * Responses can be simulated by assigning values here. @@ -27,11 +29,11 @@ export let simulateResponse: { describeUpdateResponseMockErrors?: sdk.EKS.ErrorDetails; deleteClusterErrorCode?: string; describeClusterExceptionCode?: string; -} = { }; +} = {}; export function reset() { - actualRequest = { }; - simulateResponse = { }; + actualRequest = {}; + simulateResponse = {}; } export const MOCK_UPDATE_STATUS_ID = 'MockEksUpdateStatusId'; @@ -124,18 +126,30 @@ export const client: EksClient = { createFargateProfile: async req => { actualRequest.createFargateProfile = req; - return { }; + return {}; }, describeFargateProfile: async req => { actualRequest.describeFargateProfile = req; - return { }; + return {}; }, deleteFargateProfile: async req => { actualRequest.deleteFargateProfile = req; - return { }; + return {}; }, + + untagResource: async req => { + actualRequest.untagResourceRequest = req; + return {}; + }, + + tagResource: async req => { + actualRequest.tagResourceRequest = req; + return {}; + }, + + }; export const MOCK_PROPS = { diff --git a/packages/@aws-cdk/aws-eks/test/cluster-resource-provider.test.ts b/packages/@aws-cdk/aws-eks/test/cluster-resource-provider.test.ts index f1ad11b562838..bb484a8022158 100644 --- a/packages/@aws-cdk/aws-eks/test/cluster-resource-provider.test.ts +++ b/packages/@aws-cdk/aws-eks/test/cluster-resource-provider.test.ts @@ -701,6 +701,202 @@ describe('cluster resource provider', () => { expect(resp).toEqual({ EksUpdateId: 'MockEksUpdateStatusId' }); }); }); + + describe('tag change', () => { + test('add tags', async () => { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + tags: { + new: 'one', + another: 'two', + }, + }, { + tags: undefined, + })); + + const resp = await handler.onEvent(); + expect(resp).toEqual(undefined); + expect(mocks.actualRequest.untagResourceRequest).toEqual(undefined); + expect(mocks.actualRequest.tagResourceRequest).toEqual({ + resourceArn: 'arn:cluster-arn', + tags: { + new: 'one', + another: 'two', + }, + }); + expect(mocks.actualRequest.createClusterRequest).toEqual(undefined); + }); + + test('remove all tags', async () => { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + tags: undefined, + }, { + tags: { + new: 'one', + another: 'two', + }, + })); + + const resp = await handler.onEvent(); + expect(resp).toEqual(undefined); + expect(mocks.actualRequest.untagResourceRequest).toEqual({ + resourceArn: 'arn:cluster-arn', + tagKeys: ['new', 'another'], + }); + expect(mocks.actualRequest.tagResourceRequest).toEqual(undefined); + expect(mocks.actualRequest.createClusterRequest).toEqual(undefined); + }); + + test('add tags to existing tags', async () => { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + tags: { + 'new': 'one', + 'another': 'two', + 'the-other': 'three', + 'latest': 'four', + }, + }, { + tags: { + new: 'one', + another: 'two', + }, + })); + + const resp = await handler.onEvent(); + expect(resp).toEqual(undefined); + expect(mocks.actualRequest.untagResourceRequest).toEqual(undefined); + expect(mocks.actualRequest.tagResourceRequest).toEqual({ + resourceArn: 'arn:cluster-arn', + tags: { + 'new': 'one', + 'another': 'two', + 'the-other': 'three', + 'latest': 'four', + }, + }); + expect(mocks.actualRequest.createClusterRequest).toEqual(undefined); + }); + + test('remove some tags', async () => { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + tags: { + 'the-other': 'three', + 'latest': 'four', + }, + }, { + tags: { + 'new': 'one', + 'another': 'two', + 'the-other': 'three', + 'latest': 'four', + }, + })); + + const resp = await handler.onEvent(); + expect(resp).toEqual(undefined); + expect(mocks.actualRequest.untagResourceRequest).toEqual({ + resourceArn: 'arn:cluster-arn', + tagKeys: ['new', 'another'], + }); + expect(mocks.actualRequest.tagResourceRequest).toEqual({ + resourceArn: 'arn:cluster-arn', + tags: { + 'the-other': 'three', + 'latest': 'four', + }, + }); + expect(mocks.actualRequest.createClusterRequest).toEqual(undefined); + }); + + test('update tag value', async () => { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + tags: { + new: 'three', + another: 'two', + }, + }, { + tags: { + new: 'one', + another: 'two', + }, + })); + + const resp = await handler.onEvent(); + expect(resp).toEqual(undefined); + expect(mocks.actualRequest.untagResourceRequest).toEqual(undefined); + expect(mocks.actualRequest.tagResourceRequest).toEqual({ + resourceArn: 'arn:cluster-arn', + tags: { + new: 'three', + another: 'two', + }, + }); + expect(mocks.actualRequest.createClusterRequest).toEqual(undefined); + }); + + test('update tag key', async () => { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + tags: { + new: 'one', + another: 'two', + }, + }, { + tags: { + old: 'one', + another: 'two', + }, + })); + + const resp = await handler.onEvent(); + expect(resp).toEqual(undefined); + expect(mocks.actualRequest.untagResourceRequest).toEqual({ + resourceArn: 'arn:cluster-arn', + tagKeys: ['old'], + }); + expect(mocks.actualRequest.tagResourceRequest).toEqual({ + resourceArn: 'arn:cluster-arn', + tags: { + new: 'one', + another: 'two', + }, + }); + expect(mocks.actualRequest.createClusterRequest).toEqual(undefined); + }); + + test('mixed', async () => { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + tags: { + another: 'five', + the_other: 'three', + latest: 'four', + keep: 'same', + }, + }, { + tags: { + 'new': 'one', + 'another': 'two', + 'the-other': 'three', + 'keep': 'same', + }, + })); + + const resp = await handler.onEvent(); + expect(resp).toEqual(undefined); + expect(mocks.actualRequest.untagResourceRequest).toEqual({ + resourceArn: 'arn:cluster-arn', + tagKeys: ['new', 'the-other'], + }); + expect(mocks.actualRequest.tagResourceRequest).toEqual({ + resourceArn: 'arn:cluster-arn', + tags: { + another: 'five', + the_other: 'three', + latest: 'four', + keep: 'same', + }, + }); + expect(mocks.actualRequest.createClusterRequest).toEqual(undefined); + }); + }); }); }); }); diff --git a/packages/@aws-cdk/aws-eks/test/fargate-resource-provider.test.ts b/packages/@aws-cdk/aws-eks/test/fargate-resource-provider.test.ts index 45dcb2c85457e..b4459894b7d83 100644 --- a/packages/@aws-cdk/aws-eks/test/fargate-resource-provider.test.ts +++ b/packages/@aws-cdk/aws-eks/test/fargate-resource-provider.test.ts @@ -244,7 +244,7 @@ describe('fargate resource provider', () => { }); }); -function newRequestMock(props: any = { }): any { +function newRequestMock(props: any = {}): any { return { RequestType: 'Create', ServiceToken: 'ServiceTokenMock', @@ -282,5 +282,7 @@ function newEksClientMock() { }), deleteFargateProfile: sinon.fake(), describeFargateProfile: sinon.fake.throws('not implemented'), + untagResource: sinon.fake.throws('not implemented'), + tagResource: sinon.fake.throws('not implemented'), }; -} \ No newline at end of file +}