From 821ff04f5e0ce9466d0ae349d00edc5c52909fc0 Mon Sep 17 00:00:00 2001 From: Jack Kleeman Date: Wed, 14 Feb 2024 14:31:16 +0000 Subject: [PATCH] Ack addon must support inline policies for EKS The EKS ack controller doesn't have a recommended managed policy, only an inline one, and the currently provided managed policy in blueprints doesn't seem to give any eks permissions, nor does any other managed policy seem to be appropriate. The best option is to support inline policies. There may be other ack controllers with similar issues, but EKS is the one I'm aware of. Signed-off-by: Jack Kleeman --- lib/addons/ack/index.ts | 257 ++++++++++++++++-------------- lib/addons/ack/serviceMappings.ts | 16 +- 2 files changed, 149 insertions(+), 124 deletions(-) diff --git a/lib/addons/ack/index.ts b/lib/addons/ack/index.ts index e8c6d929d..65dbf8e73 100644 --- a/lib/addons/ack/index.ts +++ b/lib/addons/ack/index.ts @@ -1,122 +1,135 @@ -import { ManagedPolicy } from 'aws-cdk-lib/aws-iam'; -import { Construct } from 'constructs'; -import merge from "ts-deepmerge"; -import { ClusterInfo, Values } from "../../spi"; -import "reflect-metadata"; -import { createNamespace, setPath, supportsX86 } from "../../utils"; -import { HelmAddOn, HelmAddOnProps, HelmAddOnUserProps } from "../helm-addon"; -import { AckServiceName, serviceMappings } from './serviceMappings'; - -export * from "./serviceMappings"; - -/** - * User provided option for the Helm Chart - */ -export interface AckAddOnProps extends HelmAddOnUserProps { - /** - * Required identified, must be unique within the parent stack scope. - */ - id?: string; - /** - * Default Service Name - * @default iam - */ - serviceName: AckServiceName; - /** - * Managed IAM Policy of the ack controller - * @default IAMFullAccess - */ - managedPolicyName?: string; - /** - * To Create Namespace using CDK. This should be done only for the first time. - */ - createNamespace?: boolean; - /** - * To create Service Account - */ - saName?: string; -} - -/** - * Default props to be used when creating the Helm chart - */ -const defaultProps: AckAddOnProps = { - namespace: "ack-system", - values: {}, - createNamespace: true, - serviceName: AckServiceName.IAM, - id: "iam-ack" -}; - -/** - * Main class to instantiate the Helm chart - */ -@Reflect.metadata("ordered", true) -@supportsX86 -export class AckAddOn extends HelmAddOn { - - readonly options: AckAddOnProps; - readonly id? : string; - - constructor(props?: AckAddOnProps) { - super(populateDefaults(defaultProps, props) as HelmAddOnProps); - this.options = this.props as AckAddOnProps; - this.id = this.options.id; - } - - - deploy(clusterInfo: ClusterInfo): Promise { - const cluster = clusterInfo.cluster; - - const sa = cluster.addServiceAccount(`${this.options.chart}-sa`, { - namespace: this.options.namespace, - name: this.options.saName, - }); - - let values: Values = populateValues(this.options,cluster.stack.region); - values = merge(values, this.props.values ?? {}); - - if(this.options.createNamespace == true){ - // Let CDK Create the Namespace - const namespace = createNamespace(this.options.namespace! , cluster); - sa.node.addDependency(namespace); - } - - sa.role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName(this.options.managedPolicyName!)); - const chart = this.addHelmChart(clusterInfo, values); - chart.node.addDependency(sa); - return Promise.resolve(chart); - } -} - -/** - * populateValues populates the appropriate values used to customize the Helm chart - * @param helmOptions User provided values to customize the chart - */ -function populateValues(helmOptions: AckAddOnProps, awsRegion: string): Values { - const values = helmOptions.values ?? {}; - setPath(values, "aws.region", awsRegion); - setPath(values,"serviceAccount.create", false); - setPath(values,"serviceAccount.name", helmOptions.saName); - return values; -} - -/** - * populate parameters passed or the default values from service Mappings. - */ -function populateDefaults(defaultProps: AckAddOnProps, props?: AckAddOnProps): AckAddOnProps { - let tempProps : Partial = {...props ?? {}}; // since props may be empty - tempProps.id = tempProps.id ?? defaultProps.id; - tempProps.serviceName = tempProps.serviceName ?? defaultProps.serviceName; - tempProps.name = tempProps.name ?? serviceMappings[tempProps.serviceName!]!.chart; - tempProps.namespace = tempProps.namespace ?? defaultProps.namespace; - tempProps.chart = tempProps.chart ?? serviceMappings[tempProps.serviceName!]?.chart; - tempProps.version = tempProps.version ?? serviceMappings[tempProps.serviceName!]?.version; - const repositoryUrl = "oci://public.ecr.aws/aws-controllers-k8s"; - tempProps.release = tempProps.release ?? tempProps.chart; - tempProps.repository = tempProps.repository ?? `${repositoryUrl}/${tempProps.name}`; - tempProps.managedPolicyName = tempProps.managedPolicyName ?? serviceMappings[tempProps.serviceName!]?.managedPolicyName; - tempProps.createNamespace = tempProps.createNamespace ?? defaultProps.createNamespace; - tempProps.saName = tempProps.saName ?? `${tempProps.chart}-sa`; - return tempProps as AckAddOnProps; -} \ No newline at end of file +import { ManagedPolicy, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; +import merge from "ts-deepmerge"; +import { ClusterInfo, Values } from "../../spi"; +import "reflect-metadata"; +import { createNamespace, setPath, supportsX86 } from "../../utils"; +import { HelmAddOn, HelmAddOnProps, HelmAddOnUserProps } from "../helm-addon"; +import { AckServiceName, serviceMappings } from './serviceMappings'; + +export * from "./serviceMappings"; + +/** + * User provided option for the Helm Chart + */ +export interface AckAddOnProps extends HelmAddOnUserProps { + /** + * Required identified, must be unique within the parent stack scope. + */ + id?: string; + /** + * Default Service Name + * @default iam + */ + serviceName: AckServiceName; + /** + * Managed IAM Policy of the ack controller + * @default IAMFullAccess + */ + managedPolicyName?: string; + /** + * Inline IAM Policy for the ack controller + * @default undefined + */ + inlinePolicyStatements?: PolicyStatement[]; + /** + * To Create Namespace using CDK. This should be done only for the first time. + */ + createNamespace?: boolean; + /** + * To create Service Account + */ + saName?: string; +} + +/** + * Default props to be used when creating the Helm chart + */ +const defaultProps: AckAddOnProps = { + namespace: "ack-system", + values: {}, + createNamespace: true, + serviceName: AckServiceName.IAM, + id: "iam-ack" +}; + +/** + * Main class to instantiate the Helm chart + */ +@Reflect.metadata("ordered", true) +@supportsX86 +export class AckAddOn extends HelmAddOn { + + readonly options: AckAddOnProps; + readonly id? : string; + + constructor(props?: AckAddOnProps) { + super(populateDefaults(defaultProps, props) as HelmAddOnProps); + this.options = this.props as AckAddOnProps; + this.id = this.options.id; + } + + + deploy(clusterInfo: ClusterInfo): Promise { + const cluster = clusterInfo.cluster; + + const sa = cluster.addServiceAccount(`${this.options.chart}-sa`, { + namespace: this.options.namespace, + name: this.options.saName, + }); + + let values: Values = populateValues(this.options,cluster.stack.region); + values = merge(values, this.props.values ?? {}); + + if(this.options.createNamespace == true){ + // Let CDK Create the Namespace + const namespace = createNamespace(this.options.namespace! , cluster); + sa.node.addDependency(namespace); + } + + if (this.options.managedPolicyName) { + sa.role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName(this.options.managedPolicyName!)); + } + if (this.options.inlinePolicyStatements && this.options.inlinePolicyStatements.length > 0) { + sa.role.attachInlinePolicy(new Policy(cluster.stack, "inline-policy", { + statements: this.options.inlinePolicyStatements + })); + } + const chart = this.addHelmChart(clusterInfo, values); + chart.node.addDependency(sa); + return Promise.resolve(chart); + } +} + +/** + * populateValues populates the appropriate values used to customize the Helm chart + * @param helmOptions User provided values to customize the chart + */ +function populateValues(helmOptions: AckAddOnProps, awsRegion: string): Values { + const values = helmOptions.values ?? {}; + setPath(values, "aws.region", awsRegion); + setPath(values,"serviceAccount.create", false); + setPath(values,"serviceAccount.name", helmOptions.saName); + return values; +} + +/** + * populate parameters passed or the default values from service Mappings. + */ +function populateDefaults(defaultProps: AckAddOnProps, props?: AckAddOnProps): AckAddOnProps { + let tempProps : Partial = {...props ?? {}}; // since props may be empty + tempProps.id = tempProps.id ?? defaultProps.id; + tempProps.serviceName = tempProps.serviceName ?? defaultProps.serviceName; + tempProps.name = tempProps.name ?? serviceMappings[tempProps.serviceName!]!.chart; + tempProps.namespace = tempProps.namespace ?? defaultProps.namespace; + tempProps.chart = tempProps.chart ?? serviceMappings[tempProps.serviceName!]?.chart; + tempProps.version = tempProps.version ?? serviceMappings[tempProps.serviceName!]?.version; + const repositoryUrl = "oci://public.ecr.aws/aws-controllers-k8s"; + tempProps.release = tempProps.release ?? tempProps.chart; + tempProps.repository = tempProps.repository ?? `${repositoryUrl}/${tempProps.name}`; + tempProps.managedPolicyName = tempProps.managedPolicyName ?? serviceMappings[tempProps.serviceName!]?.managedPolicyName; + tempProps.inlinePolicyStatements = tempProps.inlinePolicyStatements ?? serviceMappings[tempProps.serviceName!]?.inlinePolicyStatements; + tempProps.createNamespace = tempProps.createNamespace ?? defaultProps.createNamespace; + tempProps.saName = tempProps.saName ?? `${tempProps.chart}-sa`; + return tempProps as AckAddOnProps; +} diff --git a/lib/addons/ack/serviceMappings.ts b/lib/addons/ack/serviceMappings.ts index 9d49190c3..45c94f727 100644 --- a/lib/addons/ack/serviceMappings.ts +++ b/lib/addons/ack/serviceMappings.ts @@ -1,10 +1,13 @@ +import {PolicyStatement} from "aws-cdk-lib/aws-iam"; + /** * Chart Mapping for fields such as chart, version, managed IAM policy. */ export interface AckChartMapping { chart: string, version: string, - managedPolicyName: string + managedPolicyName?: string + inlinePolicyStatements?: PolicyStatement[] } /** @@ -125,7 +128,16 @@ export const serviceMappings : {[key in AckServiceName]?: AckChartMapping } = { [AckServiceName.EKS]: { chart: "eks-chart", version: "1.0.5", - managedPolicyName: "AmazonEKSClusterPolicy" + managedPolicyName: "AmazonEKSClusterPolicy", + inlinePolicyStatements: [PolicyStatement.fromJson({ + "Effect": "Allow", + "Action": [ + "eks:*", + "iam:GetRole", + "iam:PassRole" + ], + "Resource": "*" + })] }, [AckServiceName.APPLICATIONAUTOSCALING]: { chart: "applicationautoscaling-chart",