Skip to content

Commit d3fc8c0

Browse files
authored
feat(eks): install helm chart from asset (#17217)
Adding on to the work @plumdog started on #13496 and @pradoz in #15899. Implemented the @iliapolo's [suggested changes](https://github.com/aws/aws-cdk/pull/15899/files#r683431181) Related to #9273 ### Use Case To be able to use private helm charts without needing a private chart repository. ### Proposed Solution [Allow helm charts](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-eks.HelmChart.html) to be an asset by introducing the property `chartAsset`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent ef8ab72 commit d3fc8c0

File tree

8 files changed

+319
-46
lines changed

8 files changed

+319
-46
lines changed

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

+15
Original file line numberDiff line numberDiff line change
@@ -1113,6 +1113,21 @@ are being passed down (such as `repo`, `values`, `version`, `namespace`, `wait`,
11131113
This means that if the chart is added to CDK with the same release name, it will try to update
11141114
the chart in the cluster.
11151115

1116+
Additionally, the `chartAsset` property can be an `aws-s3-assets.Asset`. This allows the use of local, private helm charts.
1117+
1118+
```ts
1119+
import * as s3Assets from '@aws-cdk/aws-s3-assets';
1120+
1121+
declare const cluster: eks.Cluster;
1122+
const chartAsset = new s3Assets.Asset(this, 'ChartAsset', {
1123+
path: '/path/to/asset'
1124+
});
1125+
1126+
cluster.addHelmChart('test-chart', {
1127+
chartAsset: chartAsset,
1128+
});
1129+
```
1130+
11161131
Helm charts are implemented as CloudFormation resources in CDK.
11171132
This means that if the chart is deleted from your code (or the stack is
11181133
deleted), the next `cdk deploy` will issue a `helm uninstall` command and the

packages/@aws-cdk/aws-eks/lib/helm-chart.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Asset } from '@aws-cdk/aws-s3-assets';
12
import { CustomResource, Duration, Names, Stack } from '@aws-cdk/core';
23
import { Construct } from 'constructs';
34
import { ICluster } from './cluster';
@@ -14,8 +15,11 @@ import { Construct as CoreConstruct } from '@aws-cdk/core';
1415
export interface HelmChartOptions {
1516
/**
1617
* The name of the chart.
18+
* Either this or `chartAsset` must be specified.
19+
*
20+
* @default - No chart name. Implies `chartAsset` is used.
1721
*/
18-
readonly chart: string;
22+
readonly chart?: string;
1923

2024
/**
2125
* The name of the release.
@@ -35,6 +39,14 @@ export interface HelmChartOptions {
3539
*/
3640
readonly repository?: string;
3741

42+
/**
43+
* The chart in the form of an asset.
44+
* Either this or `chart` must be specified.
45+
*
46+
* @default - No chart asset. Implies `chart` is used.
47+
*/
48+
readonly chartAsset?: Asset;
49+
3850
/**
3951
* The Kubernetes namespace scope of the requests.
4052
* @default default
@@ -102,11 +114,23 @@ export class HelmChart extends CoreConstruct {
102114
throw new Error('Helm chart timeout cannot be higher than 15 minutes.');
103115
}
104116

117+
if (!props.chart && !props.chartAsset) {
118+
throw new Error("Either 'chart' or 'chartAsset' must be specified to install a helm chart");
119+
}
120+
121+
if (props.chartAsset && (props.repository || props.version)) {
122+
throw new Error(
123+
"Neither 'repository' nor 'version' can be used when configuring 'chartAsset'",
124+
);
125+
}
126+
105127
// default not to wait
106128
const wait = props.wait ?? false;
107129
// default to create new namespace
108130
const createNamespace = props.createNamespace ?? true;
109131

132+
props.chartAsset?.grantRead(provider.handlerRole);
133+
110134
new CustomResource(this, 'Resource', {
111135
serviceToken: provider.serviceToken,
112136
resourceType: HelmChart.RESOURCE_TYPE,
@@ -115,6 +139,7 @@ export class HelmChart extends CoreConstruct {
115139
RoleArn: provider.roleArn, // TODO: bake into the provider's environment
116140
Release: props.release ?? Names.uniqueId(this).slice(-53).toLowerCase(), // Helm has a 53 character limit for the name
117141
Chart: props.chart,
142+
ChartAssetURL: props.chartAsset?.s3ObjectUrl,
118143
Version: props.version,
119144
Wait: wait || undefined, // props are stringified so we encode “false” as undefined
120145
Timeout: timeout ? `${timeout.toString()}s` : undefined, // Helm v3 expects duration instead of integer

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

+37-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
import logging
33
import os
44
import subprocess
5+
import shutil
6+
import zipfile
7+
from urllib.parse import urlparse, unquote
58

69
logger = logging.getLogger()
710
logger.setLevel(logging.INFO)
@@ -12,6 +15,16 @@
1215
outdir = os.environ.get('TEST_OUTDIR', '/tmp')
1316
kubeconfig = os.path.join(outdir, 'kubeconfig')
1417

18+
def get_chart_asset_from_url(chart_asset_url):
19+
chart_zip = os.path.join(outdir, 'chart.zip')
20+
shutil.rmtree(chart_zip, ignore_errors=True)
21+
subprocess.check_call(['aws', 's3', 'cp', chart_asset_url, chart_zip])
22+
chart_dir = os.path.join(outdir, 'chart')
23+
shutil.rmtree(chart_dir, ignore_errors=True)
24+
os.mkdir(chart_dir)
25+
with zipfile.ZipFile(chart_zip, 'r') as zip_ref:
26+
zip_ref.extractall(chart_dir)
27+
return chart_dir
1528

1629
def helm_handler(event, context):
1730
logger.info(json.dumps(event))
@@ -20,17 +33,18 @@ def helm_handler(event, context):
2033
props = event['ResourceProperties']
2134

2235
# resource properties
23-
cluster_name = props['ClusterName']
24-
role_arn = props['RoleArn']
25-
release = props['Release']
26-
chart = props['Chart']
27-
version = props.get('Version', None)
28-
wait = props.get('Wait', False)
29-
timeout = props.get('Timeout', None)
30-
namespace = props.get('Namespace', None)
36+
cluster_name = props['ClusterName']
37+
role_arn = props['RoleArn']
38+
release = props['Release']
39+
chart = props.get('Chart', None)
40+
chart_asset_url = props.get('ChartAssetURL', None)
41+
version = props.get('Version', None)
42+
wait = props.get('Wait', False)
43+
timeout = props.get('Timeout', None)
44+
namespace = props.get('Namespace', None)
3145
create_namespace = props.get('CreateNamespace', None)
32-
repository = props.get('Repository', None)
33-
values_text = props.get('Values', None)
46+
repository = props.get('Repository', None)
47+
values_text = props.get('Values', None)
3448

3549
# "log in" to the cluster
3650
subprocess.check_call([ 'aws', 'eks', 'update-kubeconfig',
@@ -51,6 +65,19 @@ def helm_handler(event, context):
5165
f.write(json.dumps(values, indent=2))
5266

5367
if request_type == 'Create' or request_type == 'Update':
68+
# Ensure chart or chart_asset_url are set
69+
if chart == None and chart_asset_url == None:
70+
raise RuntimeError(f'chart or chartAsset must be specified')
71+
72+
if chart_asset_url != None:
73+
assert(chart==None)
74+
assert(repository==None)
75+
assert(version==None)
76+
if not chart_asset_url.startswith('s3://'):
77+
raise RuntimeError(f'ChartAssetURL must point to as s3 location but is {chart_asset_url}')
78+
# future work: support versions from s3 assets
79+
chart = get_chart_asset_from_url(chart_asset_url)
80+
5481
helm('upgrade', release, chart, repository, values_file, namespace, version, wait, timeout, create_namespace)
5582
elif request_type == "Delete":
5683
try:

packages/@aws-cdk/aws-eks/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"dependencies": {
9898
"@aws-cdk/aws-autoscaling": "0.0.0",
9999
"@aws-cdk/aws-ec2": "0.0.0",
100+
"@aws-cdk/aws-s3-assets": "0.0.0",
100101
"@aws-cdk/aws-iam": "0.0.0",
101102
"@aws-cdk/aws-kms": "0.0.0",
102103
"@aws-cdk/aws-lambda": "0.0.0",
@@ -116,6 +117,7 @@
116117
"peerDependencies": {
117118
"@aws-cdk/aws-autoscaling": "0.0.0",
118119
"@aws-cdk/aws-ec2": "0.0.0",
120+
"@aws-cdk/aws-s3-assets": "0.0.0",
119121
"@aws-cdk/aws-iam": "0.0.0",
120122
"@aws-cdk/aws-kms": "0.0.0",
121123
"@aws-cdk/aws-lambda": "0.0.0",

0 commit comments

Comments
 (0)