diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 39465a1231a5c..b4b208aa2f43c 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -291,6 +291,8 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu const publiclyAccessible = props.instanceProps.vpcSubnets && props.instanceProps.vpcSubnets.subnetType === ec2.SubnetType.Public; + const instanceParameterGroupName = props.instanceProps.parameterGroup && props.instanceProps.parameterGroup.parameterGroupName; + const instance = new CfnDBInstance(this, `Instance${instanceIndex}`, { // Link to cluster engine: props.engine, @@ -301,6 +303,7 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu publiclyAccessible, // This is already set on the Cluster. Unclear to me whether it should be repeated or not. Better yes. dbSubnetGroupName: subnetGroup.ref, + dbParameterGroupName: instanceParameterGroupName, }); // We must have a dependency on the NAT gateway provider here to create diff --git a/packages/@aws-cdk/aws-rds/lib/index.ts b/packages/@aws-cdk/aws-rds/lib/index.ts index 290e0153e4c74..242987ebf0d8a 100644 --- a/packages/@aws-cdk/aws-rds/lib/index.ts +++ b/packages/@aws-cdk/aws-rds/lib/index.ts @@ -5,6 +5,7 @@ export * from './props'; export * from './cluster-parameter-group'; export * from './rotation-single-user'; export * from './database-secret'; +export * from './instance-parameter-group'; // AWS::RDS CloudFormation Resources: export * from './rds.generated'; diff --git a/packages/@aws-cdk/aws-rds/lib/instance-parameter-group.ts b/packages/@aws-cdk/aws-rds/lib/instance-parameter-group.ts new file mode 100644 index 0000000000000..93fbcaa36341b --- /dev/null +++ b/packages/@aws-cdk/aws-rds/lib/instance-parameter-group.ts @@ -0,0 +1,134 @@ +import { CfnOutput, Construct, IConstruct, Token } from '@aws-cdk/cdk'; +import { Parameters } from './props'; +import { CfnDBParameterGroup } from './rds.generated'; + +/** + * A instance parameter group + */ +export interface IInstanceParameterGroup extends IConstruct { + /** + * Name of this parameter group + */ + readonly parameterGroupName: string; + + /** + * Export this parameter group + */ + export(): InstanceParameterGroupImportProps; +} + +/** + * Properties to reference a instance parameter group + */ +export interface InstanceParameterGroupImportProps { + readonly parameterGroupName: string; +} + +/** + * Properties for a instance parameter group + */ +export interface InstanceParameterGroupProps { + /** + * Database family of this parameter group + * + * @see https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_CreateDBParameterGroup.html#API_CreateDBParameterGroup_RequestParameters + * @example + * { family: 'MySQL5.5' } + */ + family: string; + + /** + * Description for this parameter group + */ + description: string; + + /** + * The parameters in this parameter group + */ + parameters: Parameters; +} + +/** + * Defina a instance parameter group + */ +export class InstanceParameterGroup extends Construct implements IInstanceParameterGroup { + /** + * Import a parameter group + */ + public static import(scope: Construct, id: string, props: InstanceParameterGroupImportProps): IInstanceParameterGroup { + return new ImportedInstanceParameterGroup(scope, id, props); + } + + public readonly parameterGroupName: string; + private readonly parameters: Parameters = {}; + + constructor(scope: Construct, id: string, props: InstanceParameterGroupProps) { + super(scope, id); + + const { description, family, parameters } = props; + + const resource = new CfnDBParameterGroup(this, 'Resource', { + description, + family, + parameters: new Token(() => this.parameters) + }); + + for (const [key, value] of Object.entries(parameters || {})) { + this.setParameter(key, value); + } + + this.parameterGroupName = resource.dbParameterGroupName; + } + + /** + * Export this parameter group + */ + public export(): InstanceParameterGroupImportProps { + return { + parameterGroupName: new CfnOutput(this, 'ParameterGroupName', { value: this.parameterGroupName }).makeImportValue().toString(), + }; + } + + /** + * Set a single parameter in this parameter group + */ + public setParameter(key: string, value: string | undefined) { + if (value === undefined && key in this.parameters) { + delete this.parameters[key]; + } + if (value !== undefined) { + this.parameters[key] = value; + } + } + + /** + * Remove a previously-set parameter from this parameter group + */ + public removeParameter(key: string) { + this.setParameter(key, undefined); + } + + /** + * Validate this construct + */ + protected validate(): string[] { + if (Object.keys(this.parameters).length === 0) { + return ['At least one parameter required, call setParameter().']; + } + return []; + } +} + +class ImportedInstanceParameterGroup extends Construct implements IInstanceParameterGroup { + public readonly parameterGroupName: string; + + constructor(scope: Construct, id: string, private readonly props: InstanceParameterGroupImportProps) { + super(scope, id); + + this.parameterGroupName = props.parameterGroupName; + } + + public export() { + return this.props; + } +} diff --git a/packages/@aws-cdk/aws-rds/lib/props.ts b/packages/@aws-cdk/aws-rds/lib/props.ts index 4816f249cc083..e132d6908744e 100644 --- a/packages/@aws-cdk/aws-rds/lib/props.ts +++ b/packages/@aws-cdk/aws-rds/lib/props.ts @@ -1,5 +1,6 @@ import ec2 = require('@aws-cdk/aws-ec2'); import kms = require('@aws-cdk/aws-kms'); +import { IInstanceParameterGroup } from './instance-parameter-group'; /** * The engine for the database cluster @@ -36,6 +37,13 @@ export interface InstanceProps { * Security group. If not specified a new one will be created. */ securityGroup?: ec2.ISecurityGroup; + + /** + * Additional parameters to pass to the database instance + * + * @default No parameter group + */ + parameterGroup?: IInstanceParameterGroup; } /** diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index cda81e806128f..82814f9b5425b 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -2,7 +2,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; -import { ClusterParameterGroup, DatabaseCluster, DatabaseClusterEngine } from '../lib'; +import { ClusterParameterGroup, DatabaseCluster, DatabaseClusterEngine, InstanceParameterGroup } from '../lib'; export = { 'check that instantiation works'(test: Test) { @@ -160,6 +160,44 @@ export = { test.done(); }, + 'cluster with instance parameter group'(test: Test) { + // GIVEN + const stack = testStack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + + // WHEN + const instanceParameterGroup = new InstanceParameterGroup(stack, 'Params', { + family: 'mysql', + description: 'bye', + parameters: {}, + }); + instanceParameterGroup.setParameter('param1', 'value1'); + instanceParameterGroup.setParameter('param2', 'value2'); + instanceParameterGroup.removeParameter('param2'); + new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.Aurora, + masterUser: { + username: 'admin', + password: 'tooshort', + }, + instances: 1, + instanceProps: { + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + parameterGroup: instanceParameterGroup, + vpc + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBInstance', { + DBParameterGroupName: { + Ref: 'ParamsA8366201', + }, + })); + + test.done(); + }, + 'import/export cluster parameter group'(test: Test) { // GIVEN const stack = testStack(); @@ -178,6 +216,25 @@ export = { test.done(); }, + 'import/export instance parameter group'(test: Test) { + // GIVEN + const stack = testStack(); + const group = new InstanceParameterGroup(stack, 'Params', { + family: 'hello', + description: 'desc', + parameters: {}, + }); + + // WHEN + const exported = group.export(); + const imported = InstanceParameterGroup.import(stack, 'ImportParams', exported); + + // THEN + test.deepEqual(stack.node.resolve(exported), { parameterGroupName: { 'Fn::ImportValue': 'Stack:ParamsParameterGroupNameA6B808D7' } }); + test.deepEqual(stack.node.resolve(imported.parameterGroupName), { 'Fn::ImportValue': 'Stack:ParamsParameterGroupNameA6B808D7' }); + test.done(); + }, + 'creates a secret when master credentials are not specified'(test: Test) { // GIVEN const stack = testStack(); @@ -233,7 +290,21 @@ export = { })); test.done(); - } + }, + + 'empty instance parameter group is not valid'(test: Test) { + const stack = testStack(); + new InstanceParameterGroup(stack, 'Params', { + family: 'hello', + description: 'desc', + parameters: {}, + }); + + const errorMessages = stack.node.validateTree().map(e => e.message); + test.deepEqual(errorMessages, ['At least one parameter required, call setParameter().']); + + test.done(); + }, }; function testStack() {