diff --git a/API.md b/API.md index d1ccf795..3db42e36 100644 --- a/API.md +++ b/API.md @@ -878,6 +878,7 @@ Convert an object, potentially containing tokens, to a JSON string. | isConstruct | Checks if `x` is a construct. | | isStack | Return whether the given object is a Stack. | | of | Looks up the first stack scope in which `construct` is defined. | +| createDefaultPermissionsBoundary | *No description.* | --- @@ -937,6 +938,32 @@ The construct to start the search from. --- +##### `createDefaultPermissionsBoundary` + +```typescript +import { BaseStack } from 'aws-ddk-core' + +BaseStack.createDefaultPermissionsBoundary(scope: Construct, id: string, props: PermissionsBoundaryProps) +``` + +###### `scope`Required + +- *Type:* constructs.Construct + +--- + +###### `id`Required + +- *Type:* string + +--- + +###### `props`Required + +- *Type:* PermissionsBoundaryProps + +--- + #### Properties | **Name** | **Type** | **Description** | @@ -1869,6 +1896,7 @@ public synth(): CICDPipelineStack | isConstruct | Checks if `x` is a construct. | | isStack | Return whether the given object is a Stack. | | of | Looks up the first stack scope in which `construct` is defined. | +| createDefaultPermissionsBoundary | *No description.* | --- @@ -1928,6 +1956,32 @@ The construct to start the search from. --- +##### `createDefaultPermissionsBoundary` + +```typescript +import { CICDPipelineStack } from 'aws-ddk-core' + +CICDPipelineStack.createDefaultPermissionsBoundary(scope: Construct, id: string, props: PermissionsBoundaryProps) +``` + +###### `scope`Required + +- *Type:* constructs.Construct + +--- + +###### `id`Required + +- *Type:* string + +--- + +###### `props`Required + +- *Type:* PermissionsBoundaryProps + +--- + #### Properties | **Name** | **Type** | **Description** | @@ -6430,12 +6484,26 @@ const dataBrewTransformStageProps: DataBrewTransformStageProps = { ... } | stateMachineInput | {[ key: string ]: any} | *No description.* | | stateMachineName | string | *No description.* | | createJob | boolean | *No description.* | +| databaseOutputs | aws-cdk-lib.aws_databrew.CfnJob.DatabaseOutputProperty[] | *No description.* | +| dataCatalogOutputs | aws-cdk-lib.aws_databrew.CfnJob.DataCatalogOutputProperty[] | *No description.* | | datasetName | string | *No description.* | +| encryptionKeyArn | string | *No description.* | +| encryptionMode | string | *No description.* | | jobName | string | *No description.* | | jobRoleArn | string | *No description.* | +| jobSample | aws-cdk-lib.aws_databrew.CfnJob.JobSampleProperty | *No description.* | | jobType | string | *No description.* | +| logSubscription | string | *No description.* | +| maxCapacity | number | *No description.* | +| maxRetries | number | *No description.* | +| outputLocation | aws-cdk-lib.aws_databrew.CfnJob.OutputLocationProperty | *No description.* | | outputs | aws-cdk-lib.aws_databrew.CfnJob.OutputProperty[] | *No description.* | +| profileConfiguration | aws-cdk-lib.aws_databrew.CfnJob.ProfileConfigurationProperty | *No description.* | +| projectName | string | *No description.* | | recipe | aws-cdk-lib.aws_databrew.CfnJob.RecipeProperty | *No description.* | +| tags | aws-cdk-lib.CfnTag[] | *No description.* | +| timeout | number | *No description.* | +| validationConfigurations | aws-cdk-lib.aws_databrew.CfnJob.ValidationConfigurationProperty[] | *No description.* | --- @@ -6529,6 +6597,26 @@ public readonly createJob: boolean; --- +##### `databaseOutputs`Optional + +```typescript +public readonly databaseOutputs: DatabaseOutputProperty[]; +``` + +- *Type:* aws-cdk-lib.aws_databrew.CfnJob.DatabaseOutputProperty[] + +--- + +##### `dataCatalogOutputs`Optional + +```typescript +public readonly dataCatalogOutputs: DataCatalogOutputProperty[]; +``` + +- *Type:* aws-cdk-lib.aws_databrew.CfnJob.DataCatalogOutputProperty[] + +--- + ##### `datasetName`Optional ```typescript @@ -6539,6 +6627,26 @@ public readonly datasetName: string; --- +##### `encryptionKeyArn`Optional + +```typescript +public readonly encryptionKeyArn: string; +``` + +- *Type:* string + +--- + +##### `encryptionMode`Optional + +```typescript +public readonly encryptionMode: string; +``` + +- *Type:* string + +--- + ##### `jobName`Optional ```typescript @@ -6559,6 +6667,16 @@ public readonly jobRoleArn: string; --- +##### `jobSample`Optional + +```typescript +public readonly jobSample: JobSampleProperty; +``` + +- *Type:* aws-cdk-lib.aws_databrew.CfnJob.JobSampleProperty + +--- + ##### `jobType`Optional ```typescript @@ -6569,6 +6687,46 @@ public readonly jobType: string; --- +##### `logSubscription`Optional + +```typescript +public readonly logSubscription: string; +``` + +- *Type:* string + +--- + +##### `maxCapacity`Optional + +```typescript +public readonly maxCapacity: number; +``` + +- *Type:* number + +--- + +##### `maxRetries`Optional + +```typescript +public readonly maxRetries: number; +``` + +- *Type:* number + +--- + +##### `outputLocation`Optional + +```typescript +public readonly outputLocation: OutputLocationProperty; +``` + +- *Type:* aws-cdk-lib.aws_databrew.CfnJob.OutputLocationProperty + +--- + ##### `outputs`Optional ```typescript @@ -6579,6 +6737,26 @@ public readonly outputs: OutputProperty[]; --- +##### `profileConfiguration`Optional + +```typescript +public readonly profileConfiguration: ProfileConfigurationProperty; +``` + +- *Type:* aws-cdk-lib.aws_databrew.CfnJob.ProfileConfigurationProperty + +--- + +##### `projectName`Optional + +```typescript +public readonly projectName: string; +``` + +- *Type:* string + +--- + ##### `recipe`Optional ```typescript @@ -6589,6 +6767,36 @@ public readonly recipe: RecipeProperty; --- +##### `tags`Optional + +```typescript +public readonly tags: CfnTag[]; +``` + +- *Type:* aws-cdk-lib.CfnTag[] + +--- + +##### `timeout`Optional + +```typescript +public readonly timeout: number; +``` + +- *Type:* number + +--- + +##### `validationConfigurations`Optional + +```typescript +public readonly validationConfigurations: ValidationConfigurationProperty[]; +``` + +- *Type:* aws-cdk-lib.aws_databrew.CfnJob.ValidationConfigurationProperty[] + +--- + ### DataPipelineProps #### Initializer @@ -6678,6 +6886,89 @@ public readonly alarmsEnabled: boolean; --- +### DeliveryStreamProps + +#### Initializer + +```typescript +import { DeliveryStreamProps } from 'aws-ddk-core' + +const deliveryStreamProps: DeliveryStreamProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| deliveryStreamName | string | *No description.* | +| destinations | @aws-cdk/aws-kinesisfirehose-alpha.IDestination[] | *No description.* | +| encryption | @aws-cdk/aws-kinesisfirehose-alpha.StreamEncryption | *No description.* | +| encryptionKey | aws-cdk-lib.aws_kms.IKey | *No description.* | +| role | aws-cdk-lib.aws_iam.IRole | *No description.* | +| sourceStream | aws-cdk-lib.aws_kinesis.IStream | *No description.* | + +--- + +##### `deliveryStreamName`Optional + +```typescript +public readonly deliveryStreamName: string; +``` + +- *Type:* string + +--- + +##### `destinations`Optional + +```typescript +public readonly destinations: IDestination[]; +``` + +- *Type:* @aws-cdk/aws-kinesisfirehose-alpha.IDestination[] + +--- + +##### `encryption`Optional + +```typescript +public readonly encryption: StreamEncryption; +``` + +- *Type:* @aws-cdk/aws-kinesisfirehose-alpha.StreamEncryption + +--- + +##### `encryptionKey`Optional + +```typescript +public readonly encryptionKey: IKey; +``` + +- *Type:* aws-cdk-lib.aws_kms.IKey + +--- + +##### `role`Optional + +```typescript +public readonly role: IRole; +``` + +- *Type:* aws-cdk-lib.aws_iam.IRole + +--- + +##### `sourceStream`Optional + +```typescript +public readonly sourceStream: IStream; +``` + +- *Type:* aws-cdk-lib.aws_kinesis.IStream + +--- + ### EventStageProps #### Initializer @@ -6735,10 +7026,12 @@ const firehoseToS3StageProps: FirehoseToS3StageProps = { ... } | name | string | *No description.* | | alarmsEnabled | boolean | *No description.* | | dataOutputPrefix | string | *No description.* | +| dataStream | aws-cdk-lib.aws_kinesis.Stream | *No description.* | | dataStreamEnabled | boolean | *No description.* | | deliveryStreamDataFreshnessErrorsAlarmThreshold | number | *No description.* | | deliveryStreamDataFreshnessErrorsEvaluationPeriods | number | *No description.* | -| firehoseDeliveryStreamProps | @aws-cdk/aws-kinesisfirehose-alpha.DeliveryStreamProps | *No description.* | +| firehoseDeliveryStream | @aws-cdk/aws-kinesisfirehose-alpha.DeliveryStream | *No description.* | +| firehoseDeliveryStreamProps | DeliveryStreamProps | *No description.* | | kinesisFirehoseDestinationsS3BucketProps | @aws-cdk/aws-kinesisfirehose-destinations-alpha.S3BucketProps | *No description.* | | s3Bucket | aws-cdk-lib.aws_s3.IBucket | *No description.* | | s3BucketProps | aws-cdk-lib.aws_s3.BucketProps | *No description.* | @@ -6785,6 +7078,16 @@ public readonly dataOutputPrefix: string; --- +##### `dataStream`Optional + +```typescript +public readonly dataStream: Stream; +``` + +- *Type:* aws-cdk-lib.aws_kinesis.Stream + +--- + ##### `dataStreamEnabled`Optional ```typescript @@ -6815,13 +7118,23 @@ public readonly deliveryStreamDataFreshnessErrorsEvaluationPeriods: number; --- +##### `firehoseDeliveryStream`Optional + +```typescript +public readonly firehoseDeliveryStream: DeliveryStream; +``` + +- *Type:* @aws-cdk/aws-kinesisfirehose-alpha.DeliveryStream + +--- + ##### `firehoseDeliveryStreamProps`Optional ```typescript public readonly firehoseDeliveryStreamProps: DeliveryStreamProps; ``` -- *Type:* @aws-cdk/aws-kinesisfirehose-alpha.DeliveryStreamProps +- *Type:* DeliveryStreamProps --- @@ -7021,6 +7334,45 @@ public readonly rolePolicyStatements: PolicyStatement[]; --- +### GetTagsProps + +#### Initializer + +```typescript +import { GetTagsProps } from 'aws-ddk-core' + +const getTagsProps: GetTagsProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| configPath | string | *No description.* | +| environmentId | string | *No description.* | + +--- + +##### `configPath`Required + +```typescript +public readonly configPath: string; +``` + +- *Type:* string + +--- + +##### `environmentId`Optional + +```typescript +public readonly environmentId: string; +``` + +- *Type:* string + +--- + ### GlueTransformStageProps #### Initializer @@ -7046,12 +7398,15 @@ const glueTransformStageProps: GlueTransformStageProps = { ... } | crawlerAllowFailure | boolean | *No description.* | | crawlerName | string | *No description.* | | crawlerProps | aws-cdk-lib.aws_glue.CfnCrawlerProps | *No description.* | +| crawlerRole | string | *No description.* | +| databaseName | string | *No description.* | | jobName | string | *No description.* | | jobProps | @aws-cdk/aws-glue-alpha.JobProps | *No description.* | | jobRunArgs | {[ key: string ]: any} | *No description.* | | stateMachineRetryBackoffRate | number | *No description.* | | stateMachineRetryInterval | aws-cdk-lib.Duration | *No description.* | | stateMachineRetryMaxAttempts | number | *No description.* | +| targets | aws-cdk-lib.aws_glue.CfnCrawler.TargetsProperty | *No description.* | --- @@ -7165,6 +7520,26 @@ public readonly crawlerProps: CfnCrawlerProps; --- +##### `crawlerRole`Optional + +```typescript +public readonly crawlerRole: string; +``` + +- *Type:* string + +--- + +##### `databaseName`Optional + +```typescript +public readonly databaseName: string; +``` + +- *Type:* string + +--- + ##### `jobName`Optional ```typescript @@ -7225,6 +7600,16 @@ public readonly stateMachineRetryMaxAttempts: number; --- +##### `targets`Optional + +```typescript +public readonly targets: TargetsProperty; +``` + +- *Type:* aws-cdk-lib.aws_glue.CfnCrawler.TargetsProperty + +--- + ### PermissionsBoundaryProps #### Initializer @@ -9137,6 +9522,7 @@ public tagConstruct(scope: Construct, tags: {[ key: string ]: string}): void | **Name** | **Description** | | --- | --- | | getEnvConfig | *No description.* | +| getTags | *No description.* | --- @@ -9154,6 +9540,20 @@ Configurator.getEnvConfig(props: GetEnvConfigProps) --- +##### `getTags` + +```typescript +import { Configurator } from 'aws-ddk-core' + +Configurator.getTags(props: GetTagsProps) +``` + +###### `props`Required + +- *Type:* GetTagsProps + +--- + #### Properties | **Name** | **Type** | **Description** | diff --git a/src/base/stack.ts b/src/base/stack.ts index 5d9461b4..2008eeb7 100644 --- a/src/base/stack.ts +++ b/src/base/stack.ts @@ -9,7 +9,86 @@ export interface BaseStackProps extends cdk.StackProps { readonly config?: string | object; } +export interface PermissionsBoundaryProps { + readonly environmentId?: string; + readonly prefix?: string; + readonly qualifier?: string; +} + export class BaseStack extends cdk.Stack { + public static createDefaultPermissionsBoundary( + scope: Construct, + id: string, + props: PermissionsBoundaryProps, + ): iam.IManagedPolicy { + const prefix = props.prefix ?? "ddk"; + const environmentId = props.environmentId ?? "dev"; + const qualifier = props.environmentId ?? "hnb659fds"; + + const policyStatements = [ + new iam.PolicyStatement({ + effect: iam.Effect.DENY, + actions: ["s3:PutAccountPublicAccessBlock"], + resources: ["*"], + }), + new iam.PolicyStatement({ + effect: iam.Effect.DENY, + actions: [ + "iam:CreatePolicyVersion", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:SetDefaultPolicyVersion", + ], + resources: [ + `arn:${cdk.Stack.of(scope).partition}:iam::${ + cdk.Stack.of(scope).account + }:policy/${prefix}-${environmentId}-${qualifier}-permissions-boundary-${cdk.Stack.of(scope).account}-${ + cdk.Stack.of(scope).region + }`, + ], + }), + new iam.PolicyStatement({ + effect: iam.Effect.DENY, + actions: ["iam:DeleteRolePermissionsBoundary"], + resources: [`arn:${cdk.Stack.of(scope).partition}:iam::${cdk.Stack.of(scope).account}:role/*`], + conditions: { + "ForAnyValue:StringEquals": { + "iam:PermissionsBoundary": `arn:${cdk.Stack.of(scope).partition}:iam::${ + cdk.Stack.of(scope).account + }:policy/${prefix}-${environmentId}-${qualifier}-permissions-boundary-${cdk.Stack.of(scope).account}-${ + cdk.Stack.of(scope).region + }`, + }, + }, + }), + new iam.PolicyStatement({ + effect: iam.Effect.DENY, + actions: ["iam:PutRolePermissionsBoundary"], + resources: [`arn:${cdk.Stack.of(scope).partition}:iam::${cdk.Stack.of(scope).account}:role/*`], + conditions: { + "ForAnyValue:StringNotEquals": { + "iam:PermissionsBoundary": `arn:${cdk.Stack.of(scope).partition}:iam::${ + cdk.Stack.of(scope).account + }:policy/${prefix}-${environmentId}-${qualifier}-permissions-boundary-${cdk.Stack.of(scope).account}-${ + cdk.Stack.of(scope).region + }`, + }, + }, + }), + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ["*"], + resources: ["*"], + }), + ]; + return new iam.ManagedPolicy(scope, id, { + statements: policyStatements, + managedPolicyName: `${prefix}-${environmentId}-${qualifier}-permissions-boundary-${cdk.Stack.of(scope).account}-${ + cdk.Stack.of(scope).region + }`, + description: "AWS-DDK: Deny dangerous actions that could escalate privilege or cause security incident", + }); + } readonly terminationProtection?: boolean | undefined; constructor(scope: Construct, id: string, props: BaseStackProps) { @@ -27,79 +106,3 @@ export class BaseStack extends cdk.Stack { } } } - -export interface PermissionsBoundaryProps { - readonly environmentId?: string; - readonly prefix?: string; - readonly qualifier?: string; -} - -export function createDefaultPermissionsBoundary(scope: Construct, id: string, props: PermissionsBoundaryProps) { - const prefix = props.prefix ?? "ddk"; - const environmentId = props.environmentId ?? "dev"; - const qualifier = props.environmentId ?? "hnb659fds"; - - const policyStatements = [ - new iam.PolicyStatement({ - effect: iam.Effect.DENY, - actions: ["s3:PutAccountPublicAccessBlock"], - resources: ["*"], - }), - new iam.PolicyStatement({ - effect: iam.Effect.DENY, - actions: [ - "iam:CreatePolicyVersion", - "iam:DeletePolicy", - "iam:DeletePolicyVersion", - "iam:SetDefaultPolicyVersion", - ], - resources: [ - `arn:${cdk.Stack.of(scope).partition}:iam::${ - cdk.Stack.of(scope).account - }:policy/${prefix}-${environmentId}-${qualifier}-permissions-boundary-${cdk.Stack.of(scope).account}-${ - cdk.Stack.of(scope).region - }`, - ], - }), - new iam.PolicyStatement({ - effect: iam.Effect.DENY, - actions: ["iam:DeleteRolePermissionsBoundary"], - resources: [`arn:${cdk.Stack.of(scope).partition}:iam::${cdk.Stack.of(scope).account}:role/*`], - conditions: { - "ForAnyValue:StringEquals": { - "iam:PermissionsBoundary": `arn:${cdk.Stack.of(scope).partition}:iam::${ - cdk.Stack.of(scope).account - }:policy/${prefix}-${environmentId}-${qualifier}-permissions-boundary-${cdk.Stack.of(scope).account}-${ - cdk.Stack.of(scope).region - }`, - }, - }, - }), - new iam.PolicyStatement({ - effect: iam.Effect.DENY, - actions: ["iam:PutRolePermissionsBoundary"], - resources: [`arn:${cdk.Stack.of(scope).partition}:iam::${cdk.Stack.of(scope).account}:role/*`], - conditions: { - "ForAnyValue:StringNotEquals": { - "iam:PermissionsBoundary": `arn:${cdk.Stack.of(scope).partition}:iam::${ - cdk.Stack.of(scope).account - }:policy/${prefix}-${environmentId}-${qualifier}-permissions-boundary-${cdk.Stack.of(scope).account}-${ - cdk.Stack.of(scope).region - }`, - }, - }, - }), - new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: ["*"], - resources: ["*"], - }), - ]; - return new iam.ManagedPolicy(scope, id, { - statements: policyStatements, - managedPolicyName: `${prefix}-${environmentId}-${qualifier}-permissions-boundary-${cdk.Stack.of(scope).account}-${ - cdk.Stack.of(scope).region - }`, - description: "AWS-DDK: Deny dangerous actions that could escalate privilege or cause security incident", - }); -} diff --git a/src/config/configurator.ts b/src/config/configurator.ts index a3f0faff..29509602 100644 --- a/src/config/configurator.ts +++ b/src/config/configurator.ts @@ -129,11 +129,26 @@ export interface GetEnvConfigProps { readonly environmentId: string; } +export interface GetTagsProps { + readonly configPath: string; + readonly environmentId?: string; +} + export class Configurator { public static getEnvConfig(props: GetEnvConfigProps): any { const config = getConfig({ config: props.configPath }); return config.environments ? config.environments[props.environmentId] : undefined; } + public static getTags(props: GetTagsProps): any { + const config = getConfig({ config: props.configPath }); + return props.environmentId + ? config.environments + ? config.environments[props.environmentId].tags + : {} + : config.tags + ? config.tags + : {}; + } public readonly config: any; public readonly environmentId?: string; constructor(scope: constructs.Construct, config: string | object, environmentId?: string) { diff --git a/src/core/glue-factory.ts b/src/core/glue-factory.ts index b3d5e1fe..0b74ac89 100644 --- a/src/core/glue-factory.ts +++ b/src/core/glue-factory.ts @@ -5,15 +5,18 @@ import { overrideProps } from "./utils"; export class GlueFactory { public static job(scope: Construct, id: string, props: glue.JobProps) { + const securityConfiguration = !props.securityConfiguration + ? new glue.SecurityConfiguration(scope, `${id}-security-configuration`, { + s3Encryption: { + mode: glue.S3EncryptionMode.S3_MANAGED, + }, + }) + : undefined; const defaultProps: Partial = { maxConcurrentRuns: 1, maxRetries: 1, timeout: cdk.Duration.hours(10), - securityConfiguration: new glue.SecurityConfiguration(scope, `${id}-security-configuration`, { - s3Encryption: { - mode: glue.S3EncryptionMode.S3_MANAGED, - }, - }), + securityConfiguration: securityConfiguration, }; const mergedProps = overrideProps(defaultProps, props); diff --git a/src/pipelines/pipelines.ts b/src/pipelines/pipelines.ts index 1eb4e21f..4825511f 100644 --- a/src/pipelines/pipelines.ts +++ b/src/pipelines/pipelines.ts @@ -48,7 +48,7 @@ export class DataPipeline extends Construct { const skipRule = props.skipRule ?? false; if (props.overrideRule) { - this.addRule({ overrideRule: props.overrideRule, schedule: props.schedule }); + this.addRule({ overrideRule: props.overrideRule, schedule: props.schedule, ruleName: props.ruleName }); } else if (this.previousStage && skipRule === false) { if (stage.targets === undefined) { throw new Error( @@ -61,6 +61,7 @@ export class DataPipeline extends Construct { eventPattern: this.previousStage?.eventPattern, eventTargets: stage.targets, schedule: props.schedule, + ruleName: props.ruleName, }); } diff --git a/src/pipelines/stage.ts b/src/pipelines/stage.ts index 118aa959..79d9e2ec 100644 --- a/src/pipelines/stage.ts +++ b/src/pipelines/stage.ts @@ -53,7 +53,7 @@ export abstract class DataStage extends Stage { new cloudwatch.Alarm(this, id, { metric: props.metric, comparisonOperator: props.comparisonOperator ?? cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, - threshold: props.threshold ?? 5, + threshold: props.threshold ?? 1, evaluationPeriods: props.evaluationPeriods ?? 1, }), ); diff --git a/src/stages/databrew-transform.ts b/src/stages/databrew-transform.ts index ae1194e1..efb4788e 100644 --- a/src/stages/databrew-transform.ts +++ b/src/stages/databrew-transform.ts @@ -1,3 +1,4 @@ +import * as cdk from "aws-cdk-lib"; import * as databrew from "aws-cdk-lib/aws-databrew"; import * as events from "aws-cdk-lib/aws-events"; import * as iam from "aws-cdk-lib/aws-iam"; @@ -12,9 +13,23 @@ export interface DataBrewTransformStageProps extends StateMachineStageProps { readonly jobRoleArn?: string; readonly jobType?: string; readonly datasetName?: string; - readonly recipe?: databrew.CfnJob.RecipeProperty; - readonly outputs?: databrew.CfnJob.OutputProperty[]; readonly createJob?: boolean; + readonly dataCatalogOutputs?: databrew.CfnJob.DataCatalogOutputProperty[]; + readonly databaseOutputs?: databrew.CfnJob.DatabaseOutputProperty[]; + readonly encryptionKeyArn?: string; + readonly encryptionMode?: string; + readonly jobSample?: databrew.CfnJob.JobSampleProperty; + readonly logSubscription?: string; + readonly maxCapacity?: number; + readonly maxRetries?: number; + readonly outputLocation?: databrew.CfnJob.OutputLocationProperty; + readonly outputs?: databrew.CfnJob.OutputProperty[]; + readonly profileConfiguration?: databrew.CfnJob.ProfileConfigurationProperty; + readonly projectName?: string; + readonly recipe?: databrew.CfnJob.RecipeProperty; + readonly tags?: cdk.CfnTag[]; + readonly timeout?: number; + readonly validationConfigurations?: databrew.CfnJob.ValidationConfigurationProperty[]; } export class DataBrewTransformStage extends StateMachineStage { @@ -28,21 +43,34 @@ export class DataBrewTransformStage extends StateMachineStage { constructor(scope: Construct, id: string, props: DataBrewTransformStageProps) { super(scope, id, props); - const { jobName, jobRoleArn, jobType, datasetName, recipe, outputs, createJob } = props; - this.jobName = jobName ? jobName : `${id}-job`; - this.createJob = jobName && !createJob ? false : true; + this.jobName = props.jobName ? props.jobName : `${id}-job`; + this.createJob = props.jobName && !props.createJob ? false : true; if (this.createJob) { - if (!jobType) { + if (!props.jobType) { throw new Error("if 'jobType' is a required property when creating a new DataBrew job"); } this.job = new databrew.CfnJob(this, "DataBrew Job", { name: this.jobName, - roleArn: jobRoleArn ? jobRoleArn : this.createDefaultDataBrewJobRole().roleArn, - type: jobType, - datasetName: datasetName, - recipe: recipe, - outputs: outputs, + roleArn: props.jobRoleArn ? props.jobRoleArn : this.createDefaultDataBrewJobRole().roleArn, + type: props.jobType, + datasetName: props.datasetName, + dataCatalogOutputs: props.dataCatalogOutputs, + databaseOutputs: props.databaseOutputs, + encryptionKeyArn: props.encryptionKeyArn, + encryptionMode: props.encryptionMode, + jobSample: props.jobSample, + logSubscription: props.logSubscription, + maxCapacity: props.maxCapacity, + maxRetries: props.maxRetries, + outputLocation: props.outputLocation, + outputs: props.outputs, + profileConfiguration: props.profileConfiguration, + projectName: props.projectName, + recipe: props.recipe, + tags: props.tags, + timeout: props.timeout, + validationConfigurations: props.validationConfigurations, }); } const startJobRun = new tasks.GlueDataBrewStartJobRun(this, "Start DataBrew Job", { diff --git a/src/stages/glue-transform.ts b/src/stages/glue-transform.ts index 8ae4d34e..3703694d 100644 --- a/src/stages/glue-transform.ts +++ b/src/stages/glue-transform.ts @@ -13,6 +13,9 @@ export interface GlueTransformStageProps extends StateMachineStageProps { readonly jobProps?: glue_alpha.JobProps; readonly jobRunArgs?: { [key: string]: any }; readonly crawlerName?: string; + readonly crawlerRole?: string; + readonly databaseName?: string; + readonly targets?: glue.CfnCrawler.TargetsProperty; readonly crawlerProps?: glue.CfnCrawlerProps; readonly crawlerAllowFailure?: boolean; readonly stateMachineRetryMaxAttempts?: number; @@ -93,11 +96,22 @@ export class GlueTransformStage extends StateMachineStage { } private getCrawler(props: GlueTransformStageProps): glue.CfnCrawler { - if (!props.crawlerProps) { - throw TypeError("'crawlerName' or 'crawlerProps' must be set to instantiate this stage"); + const role = props.crawlerRole ?? props.crawlerProps?.role; + if (!role) { + throw TypeError("Crawler Role must be set either by 'crawlerRole' or 'crawlerProps.role"); } - const crawler = new glue.CfnCrawler(this, "Crawler", props.crawlerProps); + const targets = props.targets ?? props.crawlerProps?.targets; + if (!targets) { + throw TypeError("Crawler Targets must be set either by 'targets' or 'crawlerProps.targets"); + } + + const crawler = new glue.CfnCrawler(this, "Crawler", { + role: role, + databaseName: props.databaseName, + targets: targets, + ...props.crawlerProps, + }); return crawler; } } diff --git a/src/stages/kinesis-s3.ts b/src/stages/kinesis-s3.ts index a4d7b7d3..7bf27dba 100644 --- a/src/stages/kinesis-s3.ts +++ b/src/stages/kinesis-s3.ts @@ -2,21 +2,33 @@ import * as firehose from "@aws-cdk/aws-kinesisfirehose-alpha"; import * as destinations from "@aws-cdk/aws-kinesisfirehose-destinations-alpha"; import * as cdk from "aws-cdk-lib"; import * as events from "aws-cdk-lib/aws-events"; +import * as iam from "aws-cdk-lib/aws-iam"; import * as kinesis from "aws-cdk-lib/aws-kinesis"; +import * as kms from "aws-cdk-lib/aws-kms"; import * as s3 from "aws-cdk-lib/aws-s3"; import { Construct } from "constructs"; import { overrideProps } from "../core"; import { S3Factory } from "../core/s3-factory"; import { DataStage, DataStageProps } from "../pipelines/stage"; +export interface DeliveryStreamProps { + readonly destinations?: firehose.IDestination[]; + readonly deliveryStreamName?: string; + readonly encryption?: firehose.StreamEncryption; + readonly encryptionKey?: kms.IKey; + readonly role?: iam.IRole; + readonly sourceStream?: kinesis.IStream; +} + export interface FirehoseToS3StageProps extends DataStageProps { readonly s3Bucket?: s3.IBucket; readonly s3BucketProps?: s3.BucketProps; - readonly firehoseDeliveryStreamProps?: firehose.DeliveryStreamProps; + readonly firehoseDeliveryStream?: firehose.DeliveryStream; + readonly firehoseDeliveryStreamProps?: DeliveryStreamProps; readonly kinesisFirehoseDestinationsS3BucketProps?: destinations.S3BucketProps; - readonly dataOutputPrefix?: string; readonly dataStreamEnabled?: boolean; + readonly dataStream?: kinesis.Stream; readonly deliveryStreamDataFreshnessErrorsAlarmThreshold?: number; readonly deliveryStreamDataFreshnessErrorsEvaluationPeriods?: number; } @@ -43,9 +55,10 @@ export class FirehoseToS3Stage extends DataStage { throw TypeError("'s3Bucket' or 's3BucketProps' must be set to instantiate this stage"); } - const dataStreamEnabled = props.dataStreamEnabled ?? false; - if (dataStreamEnabled == true) { + if (props.dataStreamEnabled == true && !props.dataStream) { this.dataStream = new kinesis.Stream(this, "Data Stream", {}); + } else if (props.dataStreamEnabled != false && props.dataStream) { + this.dataStream = props.dataStream; } const destinationsBucketProps = overrideProps( @@ -59,11 +72,13 @@ export class FirehoseToS3Stage extends DataStage { dataOutputPrefix: props.dataOutputPrefix, }, ); - this.deliveryStream = new firehose.DeliveryStream(this, "Delivery Stream", { - destinations: [new destinations.S3Bucket(this.bucket, destinationsBucketProps)], - sourceStream: this.dataStream, - ...props.firehoseDeliveryStreamProps, - }); + this.deliveryStream = props.firehoseDeliveryStream + ? props.firehoseDeliveryStream + : new firehose.DeliveryStream(this, "Delivery Stream", { + destinations: [new destinations.S3Bucket(this.bucket, destinationsBucketProps)], + sourceStream: this.dataStream, + ...props.firehoseDeliveryStreamProps, + }); const dataOutputPrefix: string = destinationsBucketProps.dataOutputPrefix; this.addAlarm("Data Freshness Errors", { diff --git a/src/stages/sqs-lambda.ts b/src/stages/sqs-lambda.ts index 7f3dfbdf..2d750109 100644 --- a/src/stages/sqs-lambda.ts +++ b/src/stages/sqs-lambda.ts @@ -48,18 +48,14 @@ export class SqsToLambdaStage extends DataStage { const functionProps: SqsToLambdaStageFunctionProps = props.lambdaFunctionProps; this.function = new lambda.Function(this, "Process Function", { - code: functionProps.code, - runtime: functionProps.runtime, - handler: functionProps.handler, timeout: functionProps.timeout ?? cdk.Duration.seconds(120), memorySize: functionProps.memorySize ?? 256, - layers: functionProps.layers, - role: functionProps.role, environment: { EVENT_SOURCE: eventSource, EVENT_DETAIL_TYPE: eventDetailType, ...(functionProps.environment ?? {}), }, + ...functionProps, }); } else { throw TypeError("'lambdaFunction' or 'lambdaFunctionProps' must be set to instantiate this stage"); @@ -92,6 +88,7 @@ export class SqsToLambdaStage extends DataStage { } : undefined, fifo: props.sqsQueueProps?.fifo ? props.sqsQueueProps?.fifo : undefined, + ...props.sqsQueueProps, }); } diff --git a/test/appflow-ingestion-stage.test.ts b/test/appflow-ingestion-stage.test.ts index 5daaf5fd..610e05bd 100644 --- a/test/appflow-ingestion-stage.test.ts +++ b/test/appflow-ingestion-stage.test.ts @@ -34,7 +34,7 @@ test("AppFlow Ingestion stage creates State Machine, Lambda & Alarm", () => { Namespace: "AWS/States", Period: 300, Statistic: "Sum", - Threshold: 5, + Threshold: 1, }); }); diff --git a/test/base-stack.test.ts b/test/base-stack.test.ts index 334d3b37..94b1bd24 100644 --- a/test/base-stack.test.ts +++ b/test/base-stack.test.ts @@ -4,7 +4,7 @@ import * as cdk from "aws-cdk-lib"; import { Template } from "aws-cdk-lib/assertions"; import * as lambda from "aws-cdk-lib/aws-lambda"; -import { BaseStack, createDefaultPermissionsBoundary, SqsToLambdaStage } from "../src"; +import { BaseStack, SqsToLambdaStage } from "../src"; test("Base Stack No Bootstrap Config", () => { const sampleConfig = { @@ -310,7 +310,11 @@ test("Base Stack Permissions Boundary", () => { test("Test Permissions Boundary Creation and Usage in BaseStack", () => { const app = new cdk.App(); const bootstrapStack = new cdk.Stack(app, "my-bootstrap-stack"); - const permissionsBoundary = createDefaultPermissionsBoundary(bootstrapStack, "DDK Default Permissions Boundary", {}); + const permissionsBoundary = BaseStack.createDefaultPermissionsBoundary( + bootstrapStack, + "DDK Default Permissions Boundary", + {}, + ); const stack = new BaseStack(app, "my-stack", { environmentId: "dev", permissionsBoundaryArn: permissionsBoundary.managedPolicyArn, @@ -337,7 +341,7 @@ test("Test Permissions Boundary Creation and Usage in BaseStack", () => { test("Permissions Boundary Creation Full", () => { const app = new cdk.App(); const bootstrapStack = new cdk.Stack(app, "my-bootstrap-stack"); - createDefaultPermissionsBoundary(bootstrapStack, "DDK Default Permissions Boundary", { + BaseStack.createDefaultPermissionsBoundary(bootstrapStack, "DDK Default Permissions Boundary", { prefix: "custom", environmentId: "stage", qualifier: "abcdefgh", diff --git a/test/config.test.ts b/test/config.test.ts index 866c169a..e1b32778 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -467,3 +467,12 @@ test("Get Env Config Static Method", () => { assert(config === expectedDevConfig); assert(nullConfig === undefined); }); + +test("Get Tags", () => { + const devTags = Configurator.getTags({ configPath: "./test/test-config.yaml", environmentId: "dev" }); + const prodTags = Configurator.getTags({ configPath: "./test/test-config.yaml", environmentId: "prod" }); + const globalTags = Configurator.getTags({ configPath: "./test/test-config.yaml" }); + assert(devTags.CostCenter === "2014"); + assert(prodTags.CostCenter === "2015"); + assert(globalTags["global:foo"] === "bar"); +}); diff --git a/test/data-pipelines.test.ts b/test/data-pipelines.test.ts index 360fdd06..4359540d 100644 --- a/test/data-pipelines.test.ts +++ b/test/data-pipelines.test.ts @@ -181,3 +181,46 @@ test("DataPipeline with skip rule", () => { const template = Template.fromStack(stack); template.resourceCountIs("AWS::Events::Rule", 0); }); + +test("DataPipeline Rule Name", () => { + const stack = new cdk.Stack(); + + const bucket = new s3.Bucket(stack, "Bucket"); + + const firehoseToS3Stage = new FirehoseToS3Stage(stack, "Firehose To S3 Stage", { s3Bucket: bucket }); + + const sqsToLambdaStage = new SqsToLambdaStage(stack, "SQS To Lambda Stage 2", { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(path.join(__dirname, "/../src/")), + handler: "commons.handlers.lambda_handler", + memorySize: 512, + runtime: lambda.Runtime.PYTHON_3_9, + layers: [ + lambda.LayerVersion.fromLayerVersionArn(stack, "Layer", "arn:aws:lambda:us-east-1:222222222222:layer:dummy:1"), + ], + }, + }); + + const pipeline = new DataPipeline(stack, "Pipeline", {}); + + pipeline.addStage({ stage: firehoseToS3Stage }).addStage({ stage: sqsToLambdaStage, ruleName: "foobar" }); + + const template = Template.fromStack(stack); + + template.resourceCountIs("AWS::Events::Rule", 1); + template.hasResourceProperties("AWS::Events::Rule", { + State: "ENABLED", + Name: "foobar", + EventPattern: Match.objectLike({ + "detail-type": firehoseToS3Stage.eventPattern?.detailType, + source: firehoseToS3Stage.eventPattern?.source, + }), + Targets: Match.arrayEquals([ + Match.objectLike({ + Arn: { + "Fn::GetAtt": [stack.resolve((sqsToLambdaStage.queue.node.defaultChild as cdk.CfnElement).logicalId), "Arn"], + }, + }), + ]), + }); +}); diff --git a/test/databrew-transform-stage.test.ts b/test/databrew-transform-stage.test.ts index cb892af6..5eae078b 100644 --- a/test/databrew-transform-stage.test.ts +++ b/test/databrew-transform-stage.test.ts @@ -24,7 +24,7 @@ test("DataBrew Transfrom stage creates State Machine & Alarm", () => { Namespace: "AWS/States", Period: 300, Statistic: "Sum", - Threshold: 5, + Threshold: 1, }); }); @@ -108,3 +108,38 @@ test("DataBrew Transform must have 'jobType' if 'createJob' is enabled or no exi }); }).toThrowError("if 'jobType' is a required property when creating a new DataBrew job"); }); + +test("DataBrew Transfrom stage additional properties", () => { + const stack = new cdk.Stack(); + + new DataBrewTransformStage(stack, "databrew-transform", { + jobName: "dummy-job", + jobType: "PROFILE", + createJob: true, + maxCapacity: 2, + maxRetries: 2, + encryptionMode: "SSE-S3", + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties("AWS::DataBrew::Job", { + Name: "dummy-job", + Type: "PROFILE", + MaxCapacity: 2, + MaxRetries: 2, + }); + template.hasResourceProperties("AWS::StepFunctions::StateMachine", { + DefinitionString: { + "Fn::Join": ["", Match.arrayWith([Match.stringLikeRegexp("Start DataBrew Job")])], + }, + }); + template.hasResourceProperties("AWS::CloudWatch::Alarm", { + ComparisonOperator: "GreaterThanThreshold", + EvaluationPeriods: 1, + MetricName: "ExecutionsFailed", + Namespace: "AWS/States", + Period: 300, + Statistic: "Sum", + Threshold: 1, + }); +}); diff --git a/test/glue-transform-stage.test.ts b/test/glue-transform-stage.test.ts index bcd17687..a92be247 100644 --- a/test/glue-transform-stage.test.ts +++ b/test/glue-transform-stage.test.ts @@ -75,6 +75,31 @@ test("GlueTransformStage stage creates Glue Crawler", () => { }); }); +test("GlueTransformStage stage creates Glue Crawler 2", () => { + const stack = new cdk.Stack(); + + new GlueTransformStage(stack, "glue-transform", { + jobName: "myJob", + crawlerRole: "role", + targets: { + s3Targets: [ + { + path: "s3://my-bucket/crawl-path", + }, + ], + }, + }); + + const template = Template.fromStack(stack); + template.resourceCountIs("AWS::Glue::Job", 0); + template.hasResourceProperties("AWS::Glue::Crawler", { + Role: "role", + Targets: { + S3Targets: [{ Path: "s3://my-bucket/crawl-path" }], + }, + }); +}); + test("GlueTranformStage must have 'jobName' or 'jobProps' set", () => { const stack = new cdk.Stack(); expect(() => { @@ -82,13 +107,23 @@ test("GlueTranformStage must have 'jobName' or 'jobProps' set", () => { }).toThrowError("'jobName' or 'jobProps' must be set to instantiate this stage"); }); -test("GlueTranformStage must have 'crawlerName' or 'crawlerProps' set", () => { +test("GlueTranformStage must set crawler role ", () => { + const stack = new cdk.Stack(); + expect(() => { + new GlueTransformStage(stack, "Stage", { + jobName: "myJob", + }); + }).toThrowError("Crawler Role must be set either by 'crawlerRole' or 'crawlerProps.role"); +}); + +test("GlueTranformStage must set crawler targets", () => { const stack = new cdk.Stack(); expect(() => { new GlueTransformStage(stack, "Stage", { jobName: "myJob", + crawlerRole: "arn:aws:iam::role/dummy-role", }); - }).toThrowError("'crawlerName' or 'crawlerProps' must be set to instantiate this stage"); + }).toThrowError("Crawler Targets must be set either by 'targets' or 'crawlerProps.targets"); }); test("GlueTransformStage retry settings", () => { diff --git a/test/kinesis-s3-stage.test.ts b/test/kinesis-s3-stage.test.ts index 8d6a3394..c10b2363 100644 --- a/test/kinesis-s3-stage.test.ts +++ b/test/kinesis-s3-stage.test.ts @@ -1,6 +1,9 @@ +import * as firehose from "@aws-cdk/aws-kinesisfirehose-alpha"; +import * as destinations from "@aws-cdk/aws-kinesisfirehose-destinations-alpha"; import * as cdk from "aws-cdk-lib"; import { Size } from "aws-cdk-lib"; import { Match, Template } from "aws-cdk-lib/assertions"; +import * as kinesis from "aws-cdk-lib/aws-kinesis"; import { Bucket } from "aws-cdk-lib/aws-s3"; import { FirehoseToS3Stage } from "../src"; @@ -51,7 +54,7 @@ test("FirehoseToS3Stage creates Firehose DeliveryStream and S3 Bucket", () => { template.hasResourceProperties("AWS::CloudWatch::Alarm", { MetricName: "DeliveryToS3.DataFreshness", - Threshold: 5, + Threshold: 1, EvaluationPeriods: 1, Dimensions: Match.arrayWith([ Match.objectLike({ @@ -95,6 +98,48 @@ test("FirehoseToS3Stage uses S3 Bucket and creates Kinesis DataStream", () => { }); }); +test("FirehoseToS3Stage uses existing S3 Bucket and Kinesis DataStream", () => { + const stack = new cdk.Stack(); + + const stage = new FirehoseToS3Stage(stack, "Stage", { + s3Bucket: new Bucket(stack, "Bucket"), + dataStream: new kinesis.Stream(stack, "Stream"), + deliveryStreamDataFreshnessErrorsAlarmThreshold: 10, + }); + + const template = Template.fromStack(stack); + + template.resourceCountIs("AWS::S3::Bucket", 1); + template.resourceCountIs("AWS::Kinesis::Stream", 1); + + template.hasResourceProperties("AWS::KinesisFirehose::DeliveryStream", { + KinesisStreamSourceConfiguration: Match.objectLike({ + KinesisStreamARN: Match.objectLike({ + "Fn::GetAtt": Match.arrayWith([ + stack.resolve((stage.dataStream?.node.defaultChild as cdk.CfnElement).logicalId), + "Arn", + ]), + }), + }), + }); +}); + +test("FirehoseToS3Stage uses existing S3 Bucket and Firehose DeliveryStream", () => { + const stack = new cdk.Stack(); + const s3Bucket = new Bucket(stack, "Bucket"); + new FirehoseToS3Stage(stack, "Stage", { + s3Bucket: s3Bucket, + firehoseDeliveryStream: new firehose.DeliveryStream(stack, "DeliveryStream", { + destinations: [new destinations.S3Bucket(s3Bucket, {})], + }), + }); + + const template = Template.fromStack(stack); + + template.resourceCountIs("AWS::S3::Bucket", 1); + template.resourceCountIs("AWS::KinesisFirehose::DeliveryStream", 1); +}); + test("FirehoseToS3Stage uses dataOutputPrefix in Firehose DeliveryStream", () => { const stack = new cdk.Stack(); const bucket = new Bucket(stack, "Bucket"); diff --git a/test/sqs-lambda-stage.test.ts b/test/sqs-lambda-stage.test.ts index aa3c38d1..3993f6e9 100644 --- a/test/sqs-lambda-stage.test.ts +++ b/test/sqs-lambda-stage.test.ts @@ -205,6 +205,7 @@ test("SQSToLambdaStage additional properties", () => { new SqsToLambdaStage(stack, "Stage", { lambdaFunctionProps: { + functionName: "dummy-function", code: lambda.Code.fromAsset(path.join(__dirname, "/../src/")), handler: "commons.handlers.lambda_handler", runtime: lambda.Runtime.PYTHON_3_8, @@ -213,6 +214,7 @@ test("SQSToLambdaStage additional properties", () => { maxReceiveCount: 2, dlqEnabled: true, sqsQueueProps: { + queueName: "dummy-queue.fifo", fifo: true, visibilityTimeout: cdk.Duration.minutes(5), }, @@ -222,9 +224,11 @@ test("SQSToLambdaStage additional properties", () => { template.hasResourceProperties("AWS::Lambda::Function", { Runtime: "python3.8", + FunctionName: "dummy-function", }); template.resourceCountIs("AWS::SQS::Queue", 2); template.hasResourceProperties("AWS::SQS::Queue", { VisibilityTimeout: 300, + QueueName: "dummy-queue.fifo", }); });