From c7fab5b29aca518fb6e1c8f2868d915885fedf04 Mon Sep 17 00:00:00 2001 From: Matt Christ <952712+mattchrist@users.noreply.github.com> Date: Thu, 19 Mar 2020 09:22:23 -0500 Subject: [PATCH] feat(eks): KubernetesPatch (#6753) feat(eks): KubernetesPatch (#6753) Exports KubernetesPatch from aws-eks module. Adds ability to specify a patch "type" for `kubectl patch` to use (fixes #6723) --- packages/@aws-cdk/aws-eks/README.md | 15 +++++ packages/@aws-cdk/aws-eks/lib/index.ts | 1 + packages/@aws-cdk/aws-eks/lib/k8s-patch.ts | 44 +++++++++++-- .../lib/kubectl-handler/patch/__init__.py | 3 +- .../test/integ.eks-cluster.expected.json | 38 ++++++------ .../@aws-cdk/aws-eks/test/test.k8s-patch.ts | 61 ++++++++++++++++++- 6 files changed, 135 insertions(+), 27 deletions(-) diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index 346642a2e55a9..7104dad4dd979 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -321,6 +321,21 @@ CDK. This means that if the resource is deleted from your code (or the stack is deleted), the next `cdk deploy` will issue a `kubectl delete` command and the Kubernetes resources will be deleted. +### Patching Kubernetes Resources + +The KubernetesPatch construct can be used to update existing kubernetes +resources. The following example can be used to patch the `hello-kubernetes` +deployment from the example above with 5 replicas. + +```ts +new KubernetesPatch(this, 'hello-kub-deployment-label', { + cluster, + resourceName: "deployment/hello-kubernetes", + applyPatch: { spec: { replicas: 5 } }, + restorePatch: { spec: { replicas: 3 } } +}) +``` + ### AWS IAM Mapping As described in the [Amazon EKS User Guide](https://docs.aws.amazon.com/en_us/eks/latest/userguide/add-user-role.html), diff --git a/packages/@aws-cdk/aws-eks/lib/index.ts b/packages/@aws-cdk/aws-eks/lib/index.ts index 17f9ac36bb92d..d3b55b29df12f 100644 --- a/packages/@aws-cdk/aws-eks/lib/index.ts +++ b/packages/@aws-cdk/aws-eks/lib/index.ts @@ -4,5 +4,6 @@ export * from './cluster'; export * from './eks.generated'; export * from './fargate-profile'; export * from './helm-chart'; +export * from './k8s-patch'; export * from './k8s-resource'; export * from './fargate-cluster'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/lib/k8s-patch.ts b/packages/@aws-cdk/aws-eks/lib/k8s-patch.ts index 812a4f73b043c..d0c108a786b40 100644 --- a/packages/@aws-cdk/aws-eks/lib/k8s-patch.ts +++ b/packages/@aws-cdk/aws-eks/lib/k8s-patch.ts @@ -3,21 +3,25 @@ import { Construct, Stack } from "@aws-cdk/core"; import { Cluster } from "./cluster"; import { KubectlProvider } from "./kubectl-provider"; -export interface CoreDnsComputeTypeProps { +/** + * Properties for KubernetesPatch + */ +export interface KubernetesPatchProps { /** * The cluster to apply the patch to. + * [disable-awslint:ref-via-interface] */ readonly cluster: Cluster; /** * The JSON object to pass to `kubectl patch` when the resource is created/updated. */ - readonly applyPatch: any; + readonly applyPatch: { [key: string]: any }; /** * The JSON object to pass to `kubectl patch` when the resource is removed. */ - readonly restorePatch: any; + readonly restorePatch: { [key: string]: any }; /** * The full name of the resource to patch (e.g. `deployment/coredns`). @@ -30,14 +34,41 @@ export interface CoreDnsComputeTypeProps { * @default "default" */ readonly resourceNamespace?: string; + + /** + * The patch type to pass to `kubectl patch`. + * The default type used by `kubectl patch` is "strategic". + * + * @default PatchType.STRATEGIC + */ + readonly patchType?: PatchType; +} + +/** + * Values for `kubectl patch` --type argument + */ +export enum PatchType { + /** + * JSON Patch, RFC 6902 + */ + JSON = "json", + /** + * JSON Merge patch + */ + MERGE = "merge", + /** + * Strategic merge patch + */ + STRATEGIC = "strategic" } /** * A CloudFormation resource which applies/restores a JSON patch into a * Kubernetes resource. + * @see https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/ */ export class KubernetesPatch extends Construct { - constructor(scope: Construct, id: string, props: CoreDnsComputeTypeProps) { + constructor(scope: Construct, id: string, props: KubernetesPatchProps) { super(scope, id); const stack = Stack.of(this); @@ -52,8 +83,9 @@ export class KubernetesPatch extends Construct { ApplyPatchJson: stack.toJsonString(props.applyPatch), RestorePatchJson: stack.toJsonString(props.restorePatch), ClusterName: props.cluster.clusterName, - RoleArn: props.cluster._getKubectlCreationRoleArn(provider.role) + RoleArn: props.cluster._getKubectlCreationRoleArn(provider.role), + PatchType: props.patchType ?? PatchType.STRATEGIC } }); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/patch/__init__.py b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/patch/__init__.py index b427b71f6342b..d6211b9348e1e 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/patch/__init__.py +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/patch/__init__.py @@ -33,6 +33,7 @@ def patch_handler(event, context): resource_namespace = props['ResourceNamespace'] apply_patch_json = props['ApplyPatchJson'] restore_patch_json = props['RestorePatchJson'] + patch_type = props['PatchType'] patch_json = None if request_type == 'Create' or request_type == 'Update': @@ -42,7 +43,7 @@ def patch_handler(event, context): else: raise Exception("invalid request type %s" % request_type) - kubectl([ 'patch', resource_name, '-n', resource_namespace, '-p', patch_json ]) + kubectl([ 'patch', resource_name, '-n', resource_namespace, '-p', patch_json, '--type', patch_type ]) def kubectl(args): diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index 4e0fd0d7605a5..de1aa505baf0b 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -1756,7 +1756,7 @@ }, "/", { - "Ref": "AssetParameters6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859S3BucketBA51B749" + "Ref": "AssetParametersb2a83bc01824acea756ffd355b4f53ae58aacffc1525dc8c75796ebb74cd8f87S3BucketB4483E96" }, "/", { @@ -1766,7 +1766,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859S3VersionKey723A87EA" + "Ref": "AssetParametersb2a83bc01824acea756ffd355b4f53ae58aacffc1525dc8c75796ebb74cd8f87S3VersionKey0402E731" } ] } @@ -1779,7 +1779,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859S3VersionKey723A87EA" + "Ref": "AssetParametersb2a83bc01824acea756ffd355b4f53ae58aacffc1525dc8c75796ebb74cd8f87S3VersionKey0402E731" } ] } @@ -1789,11 +1789,11 @@ ] }, "Parameters": { - "referencetoawscdkeksclustertestAssetParameters809b8ac7e88704d37fac32bbd5cfa56be7ea4d3e9ddb682d216c4b6868cd8fa2S3Bucket5C1311C2Ref": { - "Ref": "AssetParameters809b8ac7e88704d37fac32bbd5cfa56be7ea4d3e9ddb682d216c4b6868cd8fa2S3Bucket8A1A4BE8" + "referencetoawscdkeksclustertestAssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3Bucket6A8A7186Ref": { + "Ref": "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3Bucket0C3A00C2" }, - "referencetoawscdkeksclustertestAssetParameters809b8ac7e88704d37fac32bbd5cfa56be7ea4d3e9ddb682d216c4b6868cd8fa2S3VersionKey33922910Ref": { - "Ref": "AssetParameters809b8ac7e88704d37fac32bbd5cfa56be7ea4d3e9ddb682d216c4b6868cd8fa2S3VersionKeyB580A234" + "referencetoawscdkeksclustertestAssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3VersionKeyA18C5C39Ref": { + "Ref": "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3VersionKeyBED95764" }, "referencetoawscdkeksclustertestAssetParameters6c3e21f76e4ba0bc4b901f71bfa9c1eaf7929edcfd9a1591690d12b024100044S3Bucket24E1CF9DRef": { "Ref": "AssetParameters6c3e21f76e4ba0bc4b901f71bfa9c1eaf7929edcfd9a1591690d12b024100044S3Bucket75CDEB48" @@ -1901,29 +1901,29 @@ "Type": "String", "Description": "Artifact hash for asset \"6c3e21f76e4ba0bc4b901f71bfa9c1eaf7929edcfd9a1591690d12b024100044\"" }, - "AssetParameters809b8ac7e88704d37fac32bbd5cfa56be7ea4d3e9ddb682d216c4b6868cd8fa2S3Bucket8A1A4BE8": { + "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3Bucket0C3A00C2": { "Type": "String", - "Description": "S3 bucket for asset \"809b8ac7e88704d37fac32bbd5cfa56be7ea4d3e9ddb682d216c4b6868cd8fa2\"" + "Description": "S3 bucket for asset \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" }, - "AssetParameters809b8ac7e88704d37fac32bbd5cfa56be7ea4d3e9ddb682d216c4b6868cd8fa2S3VersionKeyB580A234": { + "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3VersionKeyBED95764": { "Type": "String", - "Description": "S3 key for asset version \"809b8ac7e88704d37fac32bbd5cfa56be7ea4d3e9ddb682d216c4b6868cd8fa2\"" + "Description": "S3 key for asset version \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" }, - "AssetParameters809b8ac7e88704d37fac32bbd5cfa56be7ea4d3e9ddb682d216c4b6868cd8fa2ArtifactHash5CE7C76A": { + "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbArtifactHashBF08C2D7": { "Type": "String", - "Description": "Artifact hash for asset \"809b8ac7e88704d37fac32bbd5cfa56be7ea4d3e9ddb682d216c4b6868cd8fa2\"" + "Description": "Artifact hash for asset \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" }, - "AssetParameters6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859S3BucketBA51B749": { + "AssetParametersb2a83bc01824acea756ffd355b4f53ae58aacffc1525dc8c75796ebb74cd8f87S3BucketB4483E96": { "Type": "String", - "Description": "S3 bucket for asset \"6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859\"" + "Description": "S3 bucket for asset \"b2a83bc01824acea756ffd355b4f53ae58aacffc1525dc8c75796ebb74cd8f87\"" }, - "AssetParameters6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859S3VersionKey723A87EA": { + "AssetParametersb2a83bc01824acea756ffd355b4f53ae58aacffc1525dc8c75796ebb74cd8f87S3VersionKey0402E731": { "Type": "String", - "Description": "S3 key for asset version \"6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859\"" + "Description": "S3 key for asset version \"b2a83bc01824acea756ffd355b4f53ae58aacffc1525dc8c75796ebb74cd8f87\"" }, - "AssetParameters6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859ArtifactHash22D2ECF0": { + "AssetParametersb2a83bc01824acea756ffd355b4f53ae58aacffc1525dc8c75796ebb74cd8f87ArtifactHash1FD4BC6F": { "Type": "String", - "Description": "Artifact hash for asset \"6a008e167065eeab066c7f96e7f3c21c2636476b93c075681fba2953ae54a859\"" + "Description": "Artifact hash for asset \"b2a83bc01824acea756ffd355b4f53ae58aacffc1525dc8c75796ebb74cd8f87\"" }, "AssetParameters6348c4414dfcbc19ed407c51ecc75d12faf4ee3219e972437d4ceed53e5b79a0S3BucketEF51ACE0": { "Type": "String", diff --git a/packages/@aws-cdk/aws-eks/test/test.k8s-patch.ts b/packages/@aws-cdk/aws-eks/test/test.k8s-patch.ts index 9aa05c2f94c0d..4a22aa4b72b6c 100644 --- a/packages/@aws-cdk/aws-eks/test/test.k8s-patch.ts +++ b/packages/@aws-cdk/aws-eks/test/test.k8s-patch.ts @@ -2,7 +2,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import { Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as eks from '../lib'; -import { KubernetesPatch } from '../lib/k8s-patch'; +import { KubernetesPatch, PatchType } from '../lib/k8s-patch'; export = { 'applies a patch to k8s'(test: Test) { @@ -41,5 +41,64 @@ export = { } })); test.done(); + }, + 'defaults to "strategic" patch type if no patchType is specified'(test: Test) { + // GIVEN + const stack = new Stack(); + const cluster = new eks.Cluster(stack, 'MyCluster'); + + // WHEN + new KubernetesPatch(stack, 'MyPatch', { + cluster, + applyPatch: { patch: { to: 'apply' } }, + restorePatch: { restore: { patch: 123 }}, + resourceName: 'myResourceName', + }); + expect(stack).to(haveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + PatchType: "strategic" + })); + test.done(); + }, + 'uses specified to patch type if specified'(test: Test) { + // GIVEN + const stack = new Stack(); + const cluster = new eks.Cluster(stack, 'MyCluster'); + + // WHEN + new KubernetesPatch(stack, 'jsonPatch', { + cluster, + applyPatch: { patch: { to: 'apply' } }, + restorePatch: { restore: { patch: 123 }}, + resourceName: 'jsonPatchResource', + patchType: PatchType.JSON + }); + new KubernetesPatch(stack, 'mergePatch', { + cluster, + applyPatch: { patch: { to: 'apply' } }, + restorePatch: { restore: { patch: 123 }}, + resourceName: 'mergePatchResource', + patchType: PatchType.MERGE + }); + new KubernetesPatch(stack, 'strategicPatch', { + cluster, + applyPatch: { patch: { to: 'apply' } }, + restorePatch: { restore: { patch: 123 }}, + resourceName: 'strategicPatchResource', + patchType: PatchType.STRATEGIC + }); + + expect(stack).to(haveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + ResourceName: "jsonPatchResource", + PatchType: "json" + })); + expect(stack).to(haveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + ResourceName: "mergePatchResource", + PatchType: "merge" + })); + expect(stack).to(haveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + ResourceName: "strategicPatchResource", + PatchType: "strategic" + })); + test.done(); } }; \ No newline at end of file