Skip to content

Commit 7e624d9

Browse files
feat(eks): Allow helm pull from OCI repositories (#18547)
The feature allows lambda to install charts from OCI repositories. This also adds login capabilities when the AWS registry is used. Fixes - #18001 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent f262ebb commit 7e624d9

File tree

4 files changed

+155
-3
lines changed

4 files changed

+155
-3
lines changed

packages/@aws-cdk/aws-eks/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,24 @@ cluster.addHelmChart('test-chart', {
11441144
});
11451145
```
11461146

1147+
### OCI Charts
1148+
1149+
OCI charts are also supported.
1150+
Also replace the `${VARS}` with appropriate values.
1151+
1152+
```ts
1153+
declare const cluster: eks.Cluster;
1154+
// option 1: use a construct
1155+
new eks.HelmChart(this, 'MyOCIChart', {
1156+
cluster,
1157+
chart: 'some-chart',
1158+
repository: 'oci://${ACCOUNT_ID}.dkr.ecr.${ACCOUNT_REGION}.amazonaws.com/${REPO_NAME}',
1159+
namespace: 'oci',
1160+
version: '0.0.1'
1161+
});
1162+
1163+
```
1164+
11471165
Helm charts are implemented as CloudFormation resources in CDK.
11481166
This means that if the chart is deleted from your code (or the stack is
11491167
deleted), the next `cdk deploy` will issue a `helm uninstall` command and the

packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import json
22
import logging
33
import os
4+
import re
45
import subprocess
56
import shutil
7+
import tempfile
68
import zipfile
79
from urllib.parse import urlparse, unquote
810

@@ -78,13 +80,71 @@ def helm_handler(event, context):
7880
# future work: support versions from s3 assets
7981
chart = get_chart_asset_from_url(chart_asset_url)
8082

83+
if repository.startswith('oci://'):
84+
assert(repository is not None)
85+
tmpdir = tempfile.TemporaryDirectory()
86+
chart_dir = get_chart_from_oci(tmpdir.name, release, repository, version)
87+
chart = chart_dir
88+
8189
helm('upgrade', release, chart, repository, values_file, namespace, version, wait, timeout, create_namespace)
8290
elif request_type == "Delete":
8391
try:
8492
helm('uninstall', release, namespace=namespace, timeout=timeout)
8593
except Exception as e:
8694
logger.info("delete error: %s" % e)
8795

96+
97+
def get_oci_cmd(repository, version):
98+
99+
cmnd = []
100+
pattern = '\d+.dkr.ecr.[a-z]+-[a-z]+-\d.amazonaws.com'
101+
102+
registry = repository.rsplit('/', 1)[0].replace('oci://', '')
103+
104+
if re.fullmatch(pattern, registry) is not None:
105+
region = registry.replace('.amazonaws.com', '').split('.')[-1]
106+
cmnd = [
107+
f"aws ecr get-login-password --region {region} | " \
108+
f"helm registry login --username AWS --password-stdin {registry}; helm pull {repository} --version {version} --untar"
109+
]
110+
else:
111+
logger.info("Non AWS OCI repository found")
112+
cmnd = ['HELM_EXPERIMENTAL_OCI=1', 'helm', 'pull', repository, '--version', version, '--untar']
113+
114+
return cmnd
115+
116+
117+
def get_chart_from_oci(tmpdir, release, repository = None, version = None):
118+
119+
cmnd = get_oci_cmd(repository, version)
120+
121+
maxAttempts = 3
122+
retry = maxAttempts
123+
while retry > 0:
124+
try:
125+
logger.info(cmnd)
126+
env = get_env_with_oci_flag()
127+
output = subprocess.check_output(cmnd, stderr=subprocess.STDOUT, cwd=tmpdir, env=env, shell=True)
128+
logger.info(output)
129+
130+
return os.path.join(tmpdir, release)
131+
except subprocess.CalledProcessError as exc:
132+
output = exc.output
133+
if b'Broken pipe' in output:
134+
retry = retry - 1
135+
logger.info("Broken pipe, retries left: %s" % retry)
136+
else:
137+
raise Exception(output)
138+
raise Exception(f'Operation failed after {maxAttempts} attempts: {output}')
139+
140+
141+
def get_env_with_oci_flag():
142+
env = os.environ.copy()
143+
env['HELM_EXPERIMENTAL_OCI'] = '1'
144+
145+
return env
146+
147+
88148
def helm(verb, release, chart = None, repo = None, file = None, namespace = None, version = None, wait = False, timeout = None, create_namespace = None):
89149
import subprocess
90150

@@ -113,7 +173,8 @@ def helm(verb, release, chart = None, repo = None, file = None, namespace = None
113173
retry = maxAttempts
114174
while retry > 0:
115175
try:
116-
output = subprocess.check_output(cmnd, stderr=subprocess.STDOUT, cwd=outdir)
176+
env = get_env_with_oci_flag()
177+
output = subprocess.check_output(cmnd, stderr=subprocess.STDOUT, cwd=outdir, env=env)
117178
logger.info(output)
118179
return
119180
except subprocess.CalledProcessError as exc:

packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ export class KubectlProvider extends NestedStack implements IKubectlProvider {
168168
resources: [cluster.clusterArn],
169169
}));
170170

171+
// For OCI helm chart authorization.
172+
this.handlerRole.addManagedPolicy(
173+
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly'),
174+
);
175+
171176
// allow this handler to assume the kubectl role
172177
cluster.kubectlRole.grant(this.handlerRole, 'sts:AssumeRole');
173178

packages/@aws-cdk/aws-eks/test/cluster.test.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2107,7 +2107,41 @@ describe('cluster', () => {
21072107
],
21082108
});
21092109

2110-
2110+
Template.fromStack(providerStack).hasResourceProperties('AWS::IAM::Role', {
2111+
AssumeRolePolicyDocument: {
2112+
Statement: [
2113+
{
2114+
Action: 'sts:AssumeRole',
2115+
Effect: 'Allow',
2116+
Principal: { Service: 'lambda.amazonaws.com' },
2117+
},
2118+
],
2119+
Version: '2012-10-17',
2120+
},
2121+
ManagedPolicyArns: [
2122+
{
2123+
'Fn::Join': ['', [
2124+
'arn:',
2125+
{ Ref: 'AWS::Partition' },
2126+
':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
2127+
]],
2128+
},
2129+
{
2130+
'Fn::Join': ['', [
2131+
'arn:',
2132+
{ Ref: 'AWS::Partition' },
2133+
':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole',
2134+
]],
2135+
},
2136+
{
2137+
'Fn::Join': ['', [
2138+
'arn:',
2139+
{ Ref: 'AWS::Partition' },
2140+
':iam::aws:policy/AmazonEC2ContainerRegistryReadOnly',
2141+
]],
2142+
},
2143+
],
2144+
});
21112145
});
21122146

21132147
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', () => {
22742308
},
22752309
});
22762310

2311+
Template.fromStack(providerStack).hasResourceProperties('AWS::IAM::Role', {
2312+
AssumeRolePolicyDocument: {
2313+
Statement: [
2314+
{
2315+
Action: 'sts:AssumeRole',
2316+
Effect: 'Allow',
2317+
Principal: { Service: 'lambda.amazonaws.com' },
2318+
},
2319+
],
2320+
Version: '2012-10-17',
2321+
},
2322+
ManagedPolicyArns: [
2323+
{
2324+
'Fn::Join': ['', [
2325+
'arn:',
2326+
{ Ref: 'AWS::Partition' },
2327+
':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
2328+
]],
2329+
},
2330+
{
2331+
'Fn::Join': ['', [
2332+
'arn:',
2333+
{ Ref: 'AWS::Partition' },
2334+
':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole',
2335+
]],
2336+
},
2337+
{
2338+
'Fn::Join': ['', [
2339+
'arn:',
2340+
{ Ref: 'AWS::Partition' },
2341+
':iam::aws:policy/AmazonEC2ContainerRegistryReadOnly',
2342+
]],
2343+
},
2344+
],
2345+
});
22772346
});
2278-
22792347
});
22802348

22812349
test('kubectl provider passes security group to provider', () => {

0 commit comments

Comments
 (0)