diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index 668c70fd96d98..001a2cbb3022a 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -1144,6 +1144,24 @@ cluster.addHelmChart('test-chart', { }); ``` +### OCI Charts + +OCI charts are also supported. +Also replace the `${VARS}` with appropriate values. + +```ts +declare const cluster: eks.Cluster; +// option 1: use a construct +new eks.HelmChart(this, 'MyOCIChart', { + cluster, + chart: 'some-chart', + repository: 'oci://${ACCOUNT_ID}.dkr.ecr.${ACCOUNT_REGION}.amazonaws.com/${REPO_NAME}', + namespace: 'oci', + version: '0.0.1' +}); + +``` + Helm charts are implemented as CloudFormation resources in CDK. This means that if the chart is deleted from your code (or the stack is deleted), the next `cdk deploy` will issue a `helm uninstall` command and the diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py index f83fc204b73e3..7d51e26d7d09b 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py @@ -1,8 +1,10 @@ import json import logging import os +import re import subprocess import shutil +import tempfile import zipfile from urllib.parse import urlparse, unquote @@ -78,6 +80,12 @@ def helm_handler(event, context): # future work: support versions from s3 assets chart = get_chart_asset_from_url(chart_asset_url) + if repository.startswith('oci://'): + assert(repository is not None) + tmpdir = tempfile.TemporaryDirectory() + chart_dir = get_chart_from_oci(tmpdir.name, release, repository, version) + chart = chart_dir + helm('upgrade', release, chart, repository, values_file, namespace, version, wait, timeout, create_namespace) elif request_type == "Delete": try: @@ -85,6 +93,58 @@ def helm_handler(event, context): except Exception as e: logger.info("delete error: %s" % e) + +def get_oci_cmd(repository, version): + + cmnd = [] + pattern = '\d+.dkr.ecr.[a-z]+-[a-z]+-\d.amazonaws.com' + + registry = repository.rsplit('/', 1)[0].replace('oci://', '') + + if re.fullmatch(pattern, registry) is not None: + region = registry.replace('.amazonaws.com', '').split('.')[-1] + cmnd = [ + f"aws ecr get-login-password --region {region} | " \ + f"helm registry login --username AWS --password-stdin {registry}; helm pull {repository} --version {version} --untar" + ] + else: + logger.info("Non AWS OCI repository found") + cmnd = ['HELM_EXPERIMENTAL_OCI=1', 'helm', 'pull', repository, '--version', version, '--untar'] + + return cmnd + + +def get_chart_from_oci(tmpdir, release, repository = None, version = None): + + cmnd = get_oci_cmd(repository, version) + + maxAttempts = 3 + retry = maxAttempts + while retry > 0: + try: + logger.info(cmnd) + env = get_env_with_oci_flag() + output = subprocess.check_output(cmnd, stderr=subprocess.STDOUT, cwd=tmpdir, env=env, shell=True) + logger.info(output) + + return os.path.join(tmpdir, release) + except subprocess.CalledProcessError as exc: + output = exc.output + if b'Broken pipe' in output: + retry = retry - 1 + logger.info("Broken pipe, retries left: %s" % retry) + else: + raise Exception(output) + raise Exception(f'Operation failed after {maxAttempts} attempts: {output}') + + +def get_env_with_oci_flag(): + env = os.environ.copy() + env['HELM_EXPERIMENTAL_OCI'] = '1' + + return env + + def helm(verb, release, chart = None, repo = None, file = None, namespace = None, version = None, wait = False, timeout = None, create_namespace = None): import subprocess @@ -113,7 +173,8 @@ def helm(verb, release, chart = None, repo = None, file = None, namespace = None retry = maxAttempts while retry > 0: try: - output = subprocess.check_output(cmnd, stderr=subprocess.STDOUT, cwd=outdir) + env = get_env_with_oci_flag() + output = subprocess.check_output(cmnd, stderr=subprocess.STDOUT, cwd=outdir, env=env) logger.info(output) return except subprocess.CalledProcessError as exc: diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts index 161ef77cfe3f3..eef07598abf27 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts @@ -168,6 +168,11 @@ export class KubectlProvider extends NestedStack implements IKubectlProvider { resources: [cluster.clusterArn], })); + // For OCI helm chart authorization. + this.handlerRole.addManagedPolicy( + iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly'), + ); + // allow this handler to assume the kubectl role cluster.kubectlRole.grant(this.handlerRole, 'sts:AssumeRole'); diff --git a/packages/@aws-cdk/aws-eks/test/cluster.test.ts b/packages/@aws-cdk/aws-eks/test/cluster.test.ts index 77556af1a0da4..5e579c4f2a247 100644 --- a/packages/@aws-cdk/aws-eks/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-eks/test/cluster.test.ts @@ -2107,7 +2107,41 @@ describe('cluster', () => { ], }); - + Template.fromStack(providerStack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { Service: 'lambda.amazonaws.com' }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + ]], + }, + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', + ]], + }, + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/AmazonEC2ContainerRegistryReadOnly', + ]], + }, + ], + }); }); test('coreDnsComputeType will patch the coreDNS configuration to use a "fargate" compute type and restore to "ec2" upon removal', () => { @@ -2274,8 +2308,42 @@ describe('cluster', () => { }, }); + Template.fromStack(providerStack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { Service: 'lambda.amazonaws.com' }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + ]], + }, + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', + ]], + }, + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/AmazonEC2ContainerRegistryReadOnly', + ]], + }, + ], + }); }); - }); test('kubectl provider passes security group to provider', () => {