Skip to content

Commit

Permalink
CDK v2 support
Browse files Browse the repository at this point in the history
  • Loading branch information
plumdog committed Mar 4, 2022
1 parent bfc1ca8 commit cfb1ddc
Show file tree
Hide file tree
Showing 9 changed files with 14,126 additions and 4,390 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ It takes an object, where:
- `path`, required, an array of strings, pointing to a value in the structured sops data
- `encoding`, optional, `'string'` or `'json'`, control how to alter the value found from sops for storage in Secrets Manager

## CDK v1 and v2

CDK v1:
```typescript
import { SopsSecretsManager } from 'sops-secretsmanager-cdk';
// or
import { SopsSecretsManager } from 'sops-secretsmanager-cdk/cdkv1';
```

CDK v2:
```typescript
import { SopsSecretsManager } from 'sops-secretsmanager-cdk/cdkv2';
```

Note: `hackToForceNode12` has no effect with CDK v2.

## Implementation

Using the CDK's custom resource mini-framework, the sops secrets file
Expand Down
120 changes: 120 additions & 0 deletions cdkv1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as cfn from '@aws-cdk/aws-cloudformation';
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import * as lambda from '@aws-cdk/aws-lambda';
import * as s3Assets from '@aws-cdk/aws-s3-assets';
import * as secretsManager from '@aws-cdk/aws-secretsmanager';
import * as cdk from '@aws-cdk/core';
import * as customResource from '@aws-cdk/custom-resources';
import * as common from './common';
export * from './common';

export interface SopsSecretsManagerProps extends common.SopsSecretsManagerBaseProps {
readonly secret?: secretsManager.Secret | secretsManager.ISecret;
readonly asset?: s3Assets.Asset;
readonly kmsKey?: kms.IKey;
}

class SopsSecretsManagerProvider extends cdk.Construct {
public readonly provider: customResource.Provider;

public static getOrCreate(scope: cdk.Construct, forceNode12: boolean): customResource.Provider {
const stack = cdk.Stack.of(scope);
const id = common.providerId;
const x = (stack.node.tryFindChild(id) as SopsSecretsManagerProvider) || new SopsSecretsManagerProvider(stack, id, forceNode12);
return x.provider;
}

constructor(scope: cdk.Construct, id: string, forceNode12: boolean) {
super(scope, id);

const policyStatements: Array<iam.PolicyStatement> = [];
for (const statement of common.providerPolicyStatements) {
policyStatements.push(new iam.PolicyStatement(statement));
}

this.provider = new customResource.Provider(this, common.providerLogicalId, {
onEventHandler: new lambda.Function(this, common.providerFunctionLogicalId, {
code: lambda.Code.fromAsset(common.providerCodePath),
runtime: lambda.Runtime.NODEJS_12_X,
handler: common.providerHandler,
timeout: cdk.Duration.minutes(common.providerTimoutMinutes),
initialPolicy: policyStatements,
}),
});

if (forceNode12) {
// Find the provider lambda and hack away
// This section hacks the CDK's utility lambda to use Node 12,
// which uses Node 10 in cdk <1.94.0. This is no longer
// deployable as of July 30, 2021.
const lambdaFn = (this.provider.node.findChild('framework-onEvent') as unknown) as lambda.Function;
const cfnLambdaFn = lambdaFn.node.defaultChild as lambda.CfnFunction;
cfnLambdaFn.addPropertyOverride('Runtime', lambda.Runtime.NODEJS_12_X.toString());
}
}
}

export class SopsSecretsManager extends cdk.Construct {
public readonly secret: secretsManager.Secret | undefined;
public readonly secretArn: string;
public readonly asset: s3Assets.Asset;

constructor(scope: cdk.Construct, id: string, props: SopsSecretsManagerProps) {
super(scope, id);

if (props.secret && props.secretName) {
throw new Error('Cannot set both secret and secretName');
} else if (props.secret) {
this.secretArn = props.secret.secretArn;
this.secret = undefined;
} else if (props.secretName) {
this.secret = new secretsManager.Secret(this, 'Secret', {
secretName: props.secretName,
});
this.secretArn = this.secret.secretArn;
} else {
throw new Error('Must set one of secret or secretName');
}
this.asset = this.getAsset(props.asset, props.path);

if (props.wholeFile && props.mappings) {
throw new Error('Cannot set mappings and set wholeFile to true');
} else if (!props.wholeFile && !props.mappings) {
throw new Error('Must set mappings or set wholeFile to true');
}

new cfn.CustomResource(this, 'Resource', {
provider: SopsSecretsManagerProvider.getOrCreate(this, props.hackToForceNode12 ?? false),
resourceType: 'Custom::SopsSecretsManager',
properties: {
SecretArn: this.secretArn,
S3Bucket: this.asset.s3BucketName,
S3Path: this.asset.s3ObjectKey,
SourceHash: this.asset.sourceHash,
KMSKeyArn: props.kmsKey?.keyArn,
Mappings: JSON.stringify(props.mappings || {}),
WholeFile: props.wholeFile || false,
FileType: props.fileType,
},
});
}

public getAsset(asset?: s3Assets.Asset, secretFilePath?: string): s3Assets.Asset {
if (asset && secretFilePath) {
throw new Error('Cannot set both asset and path');
}

if (asset) {
return asset;
}

if (secretFilePath) {
return new s3Assets.Asset(this, 'SopsAsset', {
path: secretFilePath,
});
}

throw new Error('Must set one of asset or path');
}
}
112 changes: 112 additions & 0 deletions cdkv2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as iam from 'aws-cdk-lib/aws-iam';
import * as kms from 'aws-cdk-lib/aws-kms';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as s3Assets from 'aws-cdk-lib/aws-s3-assets';
import * as secretsManager from 'aws-cdk-lib/aws-secretsmanager';
import * as cdk from 'aws-cdk-lib';
import * as constructs from 'constructs';
import * as customResource from 'aws-cdk-lib/custom-resources';
import * as common from './common';
export * from './common';

export interface SopsSecretsManagerProps extends common.SopsSecretsManagerBaseProps {
readonly secret?: secretsManager.Secret | secretsManager.ISecret;
readonly asset?: s3Assets.Asset;
readonly kmsKey?: kms.IKey;
}

class SopsSecretsManagerProvider extends constructs.Construct {
public readonly provider: customResource.Provider;

public static getOrCreate(scope: constructs.Construct): customResource.Provider {
const stack = cdk.Stack.of(scope);
const id = common.providerId;
const x = (stack.node.tryFindChild(id) as SopsSecretsManagerProvider) || new SopsSecretsManagerProvider(stack, id);
return x.provider;
}

constructor(scope: constructs.Construct, id: string) {
super(scope, id);

const policyStatements: Array<iam.PolicyStatement> = [];
for (const statement of common.providerPolicyStatements) {
policyStatements.push(new iam.PolicyStatement(statement));
}

this.provider = new customResource.Provider(this, common.providerLogicalId, {
onEventHandler: new lambda.Function(this, common.providerFunctionLogicalId, {
code: lambda.Code.fromAsset(common.providerCodePath),
runtime: lambda.Runtime.NODEJS_12_X,
handler: common.providerHandler,
timeout: cdk.Duration.minutes(common.providerTimoutMinutes),
initialPolicy: policyStatements,
}),
});
}
}

export class SopsSecretsManager extends constructs.Construct {
public readonly secret: secretsManager.Secret | undefined;
public readonly secretArn: string;
public readonly asset: s3Assets.Asset;

constructor(scope: constructs.Construct, id: string, props: SopsSecretsManagerProps) {
super(scope, id);

if (props.secret && props.secretName) {
throw new Error('Cannot set both secret and secretName');
} else if (props.secret) {
this.secretArn = props.secret.secretArn;
this.secret = undefined;
} else if (props.secretName) {
this.secret = new secretsManager.Secret(this, 'Secret', {
secretName: props.secretName,
});
this.secretArn = this.secret.secretArn;
} else {
throw new Error('Must set one of secret or secretName');
}
this.asset = this.getAsset(props.asset, props.path);

if (props.wholeFile && props.mappings) {
throw new Error('Cannot set mappings and set wholeFile to true');
} else if (!props.wholeFile && !props.mappings) {
throw new Error('Must set mappings or set wholeFile to true');
}

const provider = SopsSecretsManagerProvider.getOrCreate(this);

new cdk.CustomResource(this, 'Resource', {
serviceToken: provider.serviceToken,
resourceType: 'Custom::SopsSecretsManager',
properties: {
SecretArn: this.secretArn,
S3Bucket: this.asset.s3BucketName,
S3Path: this.asset.s3ObjectKey,
SourceHash: this.asset.assetHash,
KMSKeyArn: props.kmsKey?.keyArn,
Mappings: JSON.stringify(props.mappings || {}),
WholeFile: props.wholeFile || false,
FileType: props.fileType,
},
});
}

public getAsset(asset?: s3Assets.Asset, secretFilePath?: string): s3Assets.Asset {
if (asset && secretFilePath) {
throw new Error('Cannot set both asset and path');
}

if (asset) {
return asset;
}

if (secretFilePath) {
return new s3Assets.Asset(this, 'SopsAsset', {
path: secretFilePath,
});
}

throw new Error('Must set one of asset or path');
}
}
49 changes: 49 additions & 0 deletions common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as path from 'path';

export type SopsSecretsManagerEncoding = 'string' | 'json';

export type SopsSecretsManagerFileType = 'yaml' | 'json';

export interface SopsSecretsManagerMapping {
path: Array<string>;
encoding?: SopsSecretsManagerEncoding;
}

export interface SopsSecretsManagerMappings {
[key: string]: SopsSecretsManagerMapping;
}

export interface SopsSecretsManagerBaseProps {
readonly secret?: unknown;
readonly secretName?: string;
readonly asset?: unknown;
readonly path?: string;
readonly kmsKey?: unknown;
readonly mappings?: SopsSecretsManagerMappings;
readonly wholeFile?: boolean;
readonly fileType?: SopsSecretsManagerFileType;
readonly hackToForceNode12?: boolean;
}

export const providerId = 'com.isotoma.cdk.custom-resources.sops-secrets-manager';
export const providerLogicalId = 'sops-secrets-manager-provider';
export const providerFunctionLogicalId = 'sops-secrets-manager-event';
export const providerCodePath = path.join(__dirname, 'provider');
export const providerHandler = 'index.onEvent';
export const providerTimoutMinutes = 5;

interface PolicyStatement {
resources: Array<string>;
actions: Array<string>;
};

export const providerPolicyStatements: Array<PolicyStatement> = [{
resources: ['*'],
actions: ['s3:GetObject*', 's3:GetBucket*', 's3:List*', 's3:DeleteObject*', 's3:PutObject*', 's3:Abort*'],
}, {
resources: ['*'],
actions: ['kms:*'],
}, {
resources: ['*'],
actions: ['secretsmanager:*'],
}];
Loading

0 comments on commit cfb1ddc

Please sign in to comment.