diff --git a/.gitignore b/.gitignore index ddd09c8280673..901c2922699cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.tsbuildinfo +.cdk.staging .vscode # VSCode extension diff --git a/packages/@aws-cdk/assets/lib/asset.ts b/packages/@aws-cdk/assets/lib/asset.ts index 5132ca86d47df..f725e857cd84c 100644 --- a/packages/@aws-cdk/assets/lib/asset.ts +++ b/packages/@aws-cdk/assets/lib/asset.ts @@ -158,7 +158,7 @@ export class Asset extends cdk.Construct { * * @see https://github.com/awslabs/aws-cdk/issues/1432 * - * @param resource The CloudFormation resource which is using this asset. + * @param resource The CloudFormation resource which is using this asset [disable-awslint:ref-via-interface] * @param resourceProperty The property name where this asset is referenced * (e.g. "Code" for AWS::Lambda::Function) */ diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index f93edcf74ab89..20a9ab49b97cc 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -144,7 +144,10 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc return this.children[pathPart]; } - public trackChild(pathPart: string, resource: Resource) { + /** + * @internal + */ + public _trackChild(pathPart: string, resource: Resource) { this.children[pathPart] = resource; } @@ -194,7 +197,7 @@ export class Resource extends ResourceBase { this.parentResource = props.parent; if (props.parent instanceof ResourceBase) { - props.parent.trackChild(props.pathPart, this); + props.parent._trackChild(props.pathPart, this); } const resourceProps: CfnResourceProps = { diff --git a/packages/@aws-cdk/aws-apigateway/lib/stage.ts b/packages/@aws-cdk/aws-apigateway/lib/stage.ts index c6cc7e3ac5bf0..8825bb866bbba 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stage.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stage.ts @@ -69,7 +69,7 @@ export interface StageOptions extends MethodDeploymentOptions { export interface StageProps extends StageOptions { /** - * The deployment that this stage points to. + * The deployment that this stage points to [disable-awslint:ref-via-interface]. */ readonly deployment: Deployment; } diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index abb5538710d8b..dea8199f3a8a8 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -86,9 +86,7 @@ }, "awslint": { "exclude": [ - "resource-attribute:@aws-cdk/aws-apigateway.IRestApi.restApiRootResourceId", - "from-method:@aws-cdk/aws-apigateway.Resource", - "construct-base-is-private:@aws-cdk/aws-apigateway.ResourceBase" + "from-method:@aws-cdk/aws-apigateway.Resource" ] } } diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts index 12cb04bde5d92..f14695a236043 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts @@ -1,9 +1,16 @@ import iam = require('@aws-cdk/aws-iam'); -import { Construct, Resource } from '@aws-cdk/cdk'; +import { Construct, IResource, Resource } from '@aws-cdk/cdk'; import { CfnScalableTarget } from './applicationautoscaling.generated'; import { BasicStepScalingPolicyProps, StepScalingPolicy } from './step-scaling-policy'; import { BasicTargetTrackingScalingPolicyProps, TargetTrackingScalingPolicy } from './target-tracking-scaling-policy'; +export interface IScalableTarget extends IResource { + /** + * @attribute + */ + readonly scalableTargetId: string; +} + /** * Properties for a scalable target */ @@ -61,7 +68,15 @@ export interface ScalableTargetProps { /** * Define a scalable target */ -export class ScalableTarget extends Resource { +export class ScalableTarget extends Resource implements IScalableTarget { + + public static fromScalableTargetId(scope: Construct, id: string, scalableTargetId: string): IScalableTarget { + class Import extends Resource implements IScalableTarget { + public readonly scalableTargetId = scalableTargetId; + } + return new Import(scope, id); + } + /** * ID of the Scalable Target * diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-action.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-action.ts index 5499ddcda4b41..d889522658977 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-action.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-action.ts @@ -1,7 +1,7 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import cdk = require('@aws-cdk/cdk'); import { CfnScalingPolicy } from './applicationautoscaling.generated'; -import { ScalableTarget } from './scalable-target'; +import { IScalableTarget } from './scalable-target'; /** * Properties for a scaling policy @@ -10,7 +10,7 @@ export interface StepScalingActionProps { /** * The scalable target */ - readonly scalingTarget: ScalableTarget; + readonly scalingTarget: IScalableTarget; /** * A name for the scaling policy diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts index 5cbe98b95e0d2..55180dc345aec 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts @@ -1,7 +1,7 @@ import { findAlarmThresholds, normalizeIntervals } from '@aws-cdk/aws-autoscaling-common'; import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import cdk = require('@aws-cdk/cdk'); -import { ScalableTarget } from './scalable-target'; +import { IScalableTarget } from './scalable-target'; import { AdjustmentType, MetricAggregationType, StepScalingAction } from './step-scaling-action'; export interface BasicStepScalingPolicyProps { @@ -52,7 +52,7 @@ export interface StepScalingPolicyProps extends BasicStepScalingPolicyProps { /** * The scaling target */ - readonly scalingTarget: ScalableTarget; + readonly scalingTarget: IScalableTarget; } /** diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/target-tracking-scaling-policy.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/target-tracking-scaling-policy.ts index bea6f15f0d7a9..0026bd6b833fb 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/target-tracking-scaling-policy.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/target-tracking-scaling-policy.ts @@ -1,7 +1,7 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import cdk = require('@aws-cdk/cdk'); import { CfnScalingPolicy } from './applicationautoscaling.generated'; -import { ScalableTarget } from './scalable-target'; +import { IScalableTarget } from './scalable-target'; /** * Base interface for target tracking props @@ -95,7 +95,7 @@ export interface TargetTrackingScalingPolicyProps extends BasicTargetTrackingSca /* * The scalable target */ - readonly scalingTarget: ScalableTarget; + readonly scalingTarget: IScalableTarget; } export class TargetTrackingScalingPolicy extends cdk.Construct { diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts index da76fc22b2611..051c81f7e229f 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts @@ -1,7 +1,7 @@ import route53 = require('@aws-cdk/aws-route53'); import s3 = require('@aws-cdk/aws-s3'); import cdk = require('@aws-cdk/cdk'); -import { CfnCloudFrontOriginAccessIdentity, CfnDistribution } from './cloudfront.generated'; +import { CfnDistribution } from './cloudfront.generated'; export enum HttpVersion { HTTP1_1 = "http1.1", @@ -237,12 +237,12 @@ export interface S3OriginConfig { /** * The source bucket to serve content from */ - readonly s3BucketSource: s3.IBucket, + readonly s3BucketSource: s3.IBucket; /** - * The optional origin identity cloudfront will use when calling your s3 bucket. + * The optional ID of the origin identity cloudfront will use when calling your s3 bucket. */ - readonly originAccessIdentity?: CfnCloudFrontOriginAccessIdentity + readonly originAccessIdentityId?: string; } /** @@ -570,8 +570,8 @@ export class CloudFrontWebDistribution extends cdk.Construct implements route53. : originConfig.customOriginSource!.domainName, originPath: originConfig.originPath, originCustomHeaders: originHeaders.length > 0 ? originHeaders : undefined, - s3OriginConfig: originConfig.s3OriginSource && originConfig.s3OriginSource.originAccessIdentity - ? { originAccessIdentity: `origin-access-identity/cloudfront/${originConfig.s3OriginSource.originAccessIdentity.ref}` } + s3OriginConfig: originConfig.s3OriginSource && originConfig.s3OriginSource.originAccessIdentityId + ? { originAccessIdentity: `origin-access-identity/cloudfront/${originConfig.s3OriginSource.originAccessIdentityId}` } : originConfig.s3OriginSource ? { } : undefined, diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index a09e2d2dde5d0..2f5929fc853d3 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -1,9 +1,21 @@ -import { Construct, Resource, Token } from '@aws-cdk/cdk'; +import { Construct, IResource, Resource, Token } from '@aws-cdk/cdk'; import { CfnAlarm } from './cloudwatch.generated'; import { HorizontalAnnotation } from './graph'; import { Dimension, Metric, MetricAlarmProps, Statistic, Unit } from './metric'; import { parseStatistic } from './util.statistic'; +export interface IAlarm extends IResource { + /** + * @attribute + */ + readonly alarmArn: string; + + /** + * @attribute + */ + readonly alarmName: string; +} + /** * Properties for Alarms */ @@ -62,7 +74,16 @@ export enum TreatMissingData { /** * An alarm on a CloudWatch metric */ -export class Alarm extends Resource { +export class Alarm extends Resource implements IAlarm { + + public static fromAlarmArn(scope: Construct, id: string, alarmArn: string): IAlarm { + class Import extends Resource implements IAlarm { + public readonly alarmArn = alarmArn; + public readonly alarmName = scope.node.stack.parseArn(alarmArn, ':').resourceName!; + } + return new Import(scope, id); + } + /** * ARN of this alarm * diff --git a/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts index 05a4535907833..df869a854abcb 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts @@ -71,7 +71,7 @@ export interface LambdaDeploymentGroupProps { * @default [] * @see https://docs.aws.amazon.com/codedeploy/latest/userguide/monitoring-create-alarms.html */ - readonly alarms?: cloudwatch.Alarm[]; + readonly alarms?: cloudwatch.IAlarm[]; /** * The service Role of this Deployment Group. @@ -83,6 +83,8 @@ export interface LambdaDeploymentGroupProps { /** * Lambda Alias to shift traffic. Updating the version * of the alias will trigger a CodeDeploy deployment. + * + * [disable-awslint:ref-via-interface] since we need to modify the alias CFN resource update policy */ readonly alias: lambda.Alias; @@ -134,7 +136,7 @@ export class LambdaDeploymentGroup extends cdk.Resource implements ILambdaDeploy public readonly deploymentGroupArn: string; public readonly role: iam.IRole; - private readonly alarms: cloudwatch.Alarm[]; + private readonly alarms: cloudwatch.IAlarm[]; private preHook?: lambda.IFunction; private postHook?: lambda.IFunction; @@ -188,7 +190,7 @@ export class LambdaDeploymentGroup extends cdk.Resource implements ILambdaDeploy * * @param alarm the alarm to associate with this Deployment Group */ - public addAlarm(alarm: cloudwatch.Alarm): void { + public addAlarm(alarm: cloudwatch.IAlarm): void { this.alarms.push(alarm); } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts index 9250f9d16560f..e90aa98ef4b13 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts @@ -13,7 +13,7 @@ import { IServerDeploymentConfig, ServerDeploymentConfig } from './deployment-co export interface IServerDeploymentGroup extends cdk.IResource { readonly application: IServerApplication; - readonly role?: iam.Role; + readonly role?: iam.IRole; /** * @attribute */ @@ -67,7 +67,7 @@ export interface ServerDeploymentGroupAttributes { */ abstract class ServerDeploymentGroupBase extends cdk.Resource implements IServerDeploymentGroup { public abstract readonly application: IServerApplication; - public abstract readonly role?: iam.Role; + public abstract readonly role?: iam.IRole; public abstract readonly deploymentGroupName: string; public abstract readonly deploymentGroupArn: string; public readonly deploymentConfig: IServerDeploymentConfig; @@ -150,7 +150,7 @@ export interface ServerDeploymentGroupProps { * The service Role of this Deployment Group. * If you don't provide one, a new Role will be created. */ - readonly role?: iam.Role; + readonly role?: iam.IRole; /** * The physical, human-readable name of the CodeDeploy Deployment Group. @@ -169,7 +169,11 @@ export interface ServerDeploymentGroupProps { /** * The auto-scaling groups belonging to this Deployment Group. * - * Auto-scaling groups can also be added after the Deployment Group is created using the {@link #addAutoScalingGroup} method. + * Auto-scaling groups can also be added after the Deployment Group is created + * using the {@link #addAutoScalingGroup} method. + * + * [disable-awslint:ref-via-interface] is needed because we update userdata + * for ASGs to install the codedeploy agent. * * @default [] */ @@ -217,7 +221,7 @@ export interface ServerDeploymentGroupProps { * @default [] * @see https://docs.aws.amazon.com/codedeploy/latest/userguide/monitoring-create-alarms.html */ - readonly alarms?: cloudwatch.Alarm[]; + readonly alarms?: cloudwatch.IAlarm[]; /** * Whether to continue a deployment even if fetching the alarm status from CloudWatch failed. @@ -254,14 +258,14 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { } public readonly application: IServerApplication; - public readonly role?: iam.Role; + public readonly role?: iam.IRole; public readonly deploymentGroupArn: string; public readonly deploymentGroupName: string; private readonly _autoScalingGroups: autoscaling.AutoScalingGroup[]; private readonly installAgent: boolean; private readonly codeDeployBucket: s3.IBucket; - private readonly alarms: cloudwatch.Alarm[]; + private readonly alarms: cloudwatch.IAlarm[]; constructor(scope: cdk.Construct, id: string, props: ServerDeploymentGroupProps = {}) { super(scope, id, props.deploymentConfig); @@ -321,7 +325,9 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { /** * Adds an additional auto-scaling group to this Deployment Group. * - * @param asg the auto-scaling group to add to this Deployment Group + * @param asg the auto-scaling group to add to this Deployment Group. + * [disable-awslint:ref-via-interface] is needed in order to install the code + * deploy agent by updating the ASGs user data. */ public addAutoScalingGroup(asg: autoscaling.AutoScalingGroup): void { this._autoScalingGroups.push(asg); @@ -333,7 +339,7 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { * * @param alarm the alarm to associate with this Deployment Group */ - public addAlarm(alarm: cloudwatch.Alarm): void { + public addAlarm(alarm: cloudwatch.IAlarm): void { this.alarms.push(alarm); } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/utils.ts b/packages/@aws-cdk/aws-codedeploy/lib/utils.ts index 9eca2c51327ff..717b3cfd1b679 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/utils.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/utils.ts @@ -15,7 +15,7 @@ export function arnForDeploymentConfig(name: string): string { return `arn:${Aws.partition}:codedeploy:${Aws.region}:${Aws.accountId}:deploymentconfig:${name}`; } -export function renderAlarmConfiguration(alarms: cloudwatch.Alarm[], ignorePollAlarmFailure?: boolean): +export function renderAlarmConfiguration(alarms: cloudwatch.IAlarm[], ignorePollAlarmFailure?: boolean): CfnDeploymentGroup.AlarmConfigurationProperty | undefined { return alarms.length === 0 ? undefined @@ -32,7 +32,7 @@ enum AutoRollbackEvent { DeploymentStopOnRequest = 'DEPLOYMENT_STOP_ON_REQUEST' } -export function renderAutoRollbackConfiguration(alarms: cloudwatch.Alarm[], autoRollbackConfig: AutoRollbackConfig = {}): +export function renderAutoRollbackConfiguration(alarms: cloudwatch.IAlarm[], autoRollbackConfig: AutoRollbackConfig = {}): CfnDeploymentGroup.AutoRollbackConfigurationProperty | undefined { const events = new Array(); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts index 85d7eab484dd1..1bba1fd738244 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts @@ -692,10 +692,10 @@ export = { ] })); - test.equal(pipeline.crossRegionScaffoldStacks[pipelineRegion], undefined); - test.equal(pipeline.crossRegionScaffoldStacks['us-west-1'], undefined); + test.equal(pipeline.crossRegionScaffolding[pipelineRegion], undefined); + test.equal(pipeline.crossRegionScaffolding['us-west-1'], undefined); - const usEast1ScaffoldStack = pipeline.crossRegionScaffoldStacks['us-east-1']; + const usEast1ScaffoldStack = pipeline.crossRegionScaffolding['us-east-1']; test.notEqual(usEast1ScaffoldStack, undefined); test.equal(usEast1ScaffoldStack.env.region, 'us-east-1'); test.equal(usEast1ScaffoldStack.env.account, pipelineAccount); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/cross-region-scaffold-stack.ts b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-scaffold-stack.ts index 7b7ffdf084d81..a8fcddd49e791 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/cross-region-scaffold-stack.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-scaffold-stack.ts @@ -1,6 +1,7 @@ import s3 = require('@aws-cdk/aws-s3'); import cdk = require('@aws-cdk/cdk'); import crypto = require('crypto'); +import { CrossRegionScaffolding } from './pipeline'; /** * Construction properties for {@link CrossRegionScaffoldStack}. @@ -22,14 +23,15 @@ export interface CrossRegionScaffoldStackProps { /** * A Stack containing resources required for the cross-region CodePipeline functionality to work. */ -export class CrossRegionScaffoldStack extends cdk.Stack { +export class CrossRegionScaffoldStack extends CrossRegionScaffolding { /** * The name of the S3 Bucket used for replicating the Pipeline's artifacts into the region. */ public readonly replicationBucketName: string; - constructor(scope: cdk.App, props: CrossRegionScaffoldStackProps) { - super(scope, generateStackName(props), { + constructor(scope: cdk.Construct, id: string, props: CrossRegionScaffoldStackProps) { + super(scope, id, { + stackName: generateStackName(props), env: { region: props.region, account: props.account, diff --git a/packages/@aws-cdk/aws-codepipeline/lib/index.ts b/packages/@aws-cdk/aws-codepipeline/lib/index.ts index 5976da7d21b5e..c62d8dda6b33f 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/index.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/index.ts @@ -1,6 +1,5 @@ export * from './action'; export * from './artifact'; -export * from './cross-region-scaffold-stack'; export * from './pipeline'; // AWS::CodePipeline CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 5805597bc5a31..7f9af258c0dda 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -2,10 +2,9 @@ import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import s3 = require('@aws-cdk/aws-s3'); -import { Construct, RemovalPolicy, Resource, Token } from '@aws-cdk/cdk'; +import { Construct, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/cdk'; import { Action, IPipeline, IStage } from "./action"; import { CfnPipeline } from './codepipeline.generated'; -import { CrossRegionScaffoldStack } from './cross-region-scaffold-stack'; import { Stage } from './stage'; import { validateName, validateSourceAction } from "./validation"; @@ -342,7 +341,7 @@ export class Pipeline extends PipelineBase { * Returns all of the {@link CrossRegionScaffoldStack}s that were generated automatically * when dealing with Actions that reside in a different region than the Pipeline itself. */ - public get crossRegionScaffoldStacks(): { [region: string]: CrossRegionScaffoldStack } { + public get crossRegionScaffolding(): { [region: string]: CrossRegionScaffolding } { const ret: { [region: string]: CrossRegionScaffoldStack } = {}; Object.keys(this._crossRegionScaffoldStacks).forEach((key) => { ret[key] = this._crossRegionScaffoldStacks[key]; @@ -400,7 +399,7 @@ export class Pipeline extends PipelineBase { if (!app) { throw new Error(`Pipeline stack which uses cross region actions must be part of an application`); } - const crossRegionScaffoldStack = new CrossRegionScaffoldStack(app, { + const crossRegionScaffoldStack = new CrossRegionScaffoldStack(this, `cross-region-stack-${pipelineAccount}:${region}`, { region, account: pipelineAccount, }); @@ -588,3 +587,16 @@ export class Pipeline extends PipelineBase { return this.stages.map(stage => stage.render()); } } + +/** + * A Stack containing resources required for the cross-region CodePipeline functionality to work. + */ +export abstract class CrossRegionScaffolding extends Stack { + /** + * The name of the S3 Bucket used for replicating the Pipeline's artifacts into the region. + */ + public abstract readonly replicationBucketName: string; +} + +// cyclic dependency +import { CrossRegionScaffoldStack } from './cross-region-scaffold-stack'; diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 4b669d5a9c00c..0d88c5ba6161d 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -790,7 +790,7 @@ export class VpcNetwork extends VpcNetworkBase { }); (this.publicSubnets as VpcPublicSubnet[]).forEach(publicSubnet => { - publicSubnet.addDefaultIGWRouteEntry(igw, att); + publicSubnet.addDefaultInternetRoute(igw.ref, att); }); // if gateways are needed create them @@ -803,7 +803,7 @@ export class VpcNetwork extends VpcNetworkBase { // round robin the available NatGW since one is not in your AZ ngwId = ngwArray[i % ngwArray.length]; } - privateSubnet.addDefaultNatRouteEntry(ngwId); + privateSubnet.addDefaultNatRoute(ngwId); }); } @@ -1049,10 +1049,17 @@ export interface VpcSubnetProps { readonly mapPublicIpOnLaunch?: boolean; } +const IS_VPC_SUBNET = Symbol.for('@aws-cdk/aws-ec2.VpcSubnet'); + /** * Represents a new VPC subnet resource */ export class VpcSubnet extends cdk.Construct implements IVpcSubnet { + + public static isVpcSubnet(o: any): o is VpcSubnet { + return IS_VPC_SUBNET in o; + } + public static import(scope: cdk.Construct, id: string, props: VpcSubnetImportProps): IVpcSubnet { return new ImportedVpcSubnet(scope, id, props); } @@ -1081,6 +1088,9 @@ export class VpcSubnet extends cdk.Construct implements IVpcSubnet { constructor(scope: cdk.Construct, id: string, props: VpcSubnetProps) { super(scope, id); + + Object.defineProperty(this, IS_VPC_SUBNET, { value: true }); + this.node.apply(new cdk.Tag(NAME_TAG, this.node.path)); this.availabilityZone = props.availabilityZone; @@ -1114,31 +1124,36 @@ export class VpcSubnet extends cdk.Construct implements IVpcSubnet { return this.internetDependencies; } - protected addDefaultRouteToNAT(natGatewayId: string) { + /** + * Create a default route that points to a passed IGW, with a dependency + * on the IGW's attachment to the VPC. + * + * @param gatewayId the logical ID (ref) of the gateway attached to your VPC + * @param gatewayAttachment the gateway attachment construct to be added as a dependency + */ + public addDefaultInternetRoute(gatewayId: string, gatewayAttachment: cdk.IDependable) { const route = new CfnRoute(this, `DefaultRoute`, { routeTableId: this.routeTableId!, destinationCidrBlock: '0.0.0.0/0', - natGatewayId + gatewayId }); + route.node.addDependency(gatewayAttachment); + + // Since the 'route' depends on the gateway attachment, just + // depending on the route is enough. this.internetDependencies.add(route); } /** - * Create a default route that points to a passed IGW, with a dependency - * on the IGW's attachment to the VPC. + * Adds an entry to this subnets route table that points to the passed NATGatwayId + * @param natGatewayId The ID of the NAT gateway */ - protected addDefaultRouteToIGW( - gateway: CfnInternetGateway, - gatewayAttachment: CfnVPCGatewayAttachment) { + public addDefaultNatRoute(natGatewayId: string) { const route = new CfnRoute(this, `DefaultRoute`, { routeTableId: this.routeTableId!, destinationCidrBlock: '0.0.0.0/0', - gatewayId: gateway.ref + natGatewayId }); - route.node.addDependency(gatewayAttachment); - - // Since the 'route' depends on the gateway attachment, just - // depending on the route is enough. this.internetDependencies.add(route); } } @@ -1157,16 +1172,6 @@ export class VpcPublicSubnet extends VpcSubnet { super(scope, id, props); } - /** - * Create a default route that points to a passed IGW, with a dependency - * on the IGW's attachment to the VPC. - */ - public addDefaultIGWRouteEntry( - gateway: CfnInternetGateway, - gatewayAttachment: CfnVPCGatewayAttachment) { - this.addDefaultRouteToIGW(gateway, gatewayAttachment); - } - /** * Creates a new managed NAT gateway attached to this public subnet. * Also adds the EIP for the managed NAT. @@ -1196,13 +1201,6 @@ export class VpcPrivateSubnet extends VpcSubnet { constructor(scope: cdk.Construct, id: string, props: VpcPrivateSubnetProps) { super(scope, id, props); } - - /** - * Adds an entry to this subnets route table that points to the passed NATGatwayId - */ - public addDefaultNatRouteEntry(natGatewayId: string) { - this.addDefaultRouteToNAT(natGatewayId); - } } function ifUndefined(value: T | undefined, defaultValue: T): T { diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 19066aaafcea7..1c0f652e9b630 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -4,12 +4,22 @@ import ec2 = require('@aws-cdk/aws-ec2'); import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); import iam = require('@aws-cdk/aws-iam'); import cloudmap = require('@aws-cdk/aws-servicediscovery'); +import { IResource, Resource } from '@aws-cdk/cdk'; import cdk = require('@aws-cdk/cdk'); import { NetworkMode, TaskDefinition } from '../base/task-definition'; import { ICluster } from '../cluster'; import { CfnService } from '../ecs.generated'; import { ScalableTaskCount } from './scalable-task-count'; +export interface IService extends IResource { + /** + * ARN of this service + * + * @attribute + */ + readonly serviceArn: string; +} + /** * Basic service properties */ @@ -82,8 +92,8 @@ export interface BaseServiceProps { /** * Base class for Ecs and Fargate services */ -export abstract class BaseService extends cdk.Construct - implements elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget { +export abstract class BaseService extends Resource + implements IService, elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget { /** * Manage allowed network traffic for this service @@ -97,6 +107,8 @@ export abstract class BaseService extends cdk.Construct /** * Name of this service + * + * @attribute */ public readonly serviceName: string; diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index 719da8487ce6f..09fa5d7d28479 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -1,9 +1,37 @@ import iam = require('@aws-cdk/aws-iam'); -import { Construct, Resource, Token } from '@aws-cdk/cdk'; +import { Construct, IResource, Resource, Token } from '@aws-cdk/cdk'; import { ContainerDefinition, ContainerDefinitionOptions } from '../container-definition'; import { CfnTaskDefinition } from '../ecs.generated'; import { PlacementConstraint } from '../placement'; +export interface ITaskDefinition extends IResource { + /** + * ARN of this task definition + * @attribute + */ + readonly taskDefinitionArn: string; + + /** + * Execution role for this task definition + */ + readonly executionRole?: iam.IRole; + + /** + * What launch types this task definition should be compatible with. + */ + readonly compatibility: Compatibility; + + /** + * Return true if the task definition can be run on an EC2 cluster + */ + readonly isEc2Compatible: boolean; + + /** + * Return true if the task definition can be run on a Fargate cluster + */ + readonly isFargateCompatible: boolean; +} + /** * Properties common to all Task definitions */ @@ -95,10 +123,47 @@ export interface TaskDefinitionProps extends CommonTaskDefinitionProps { readonly memoryMiB?: string; } +abstract class TaskDefinitionBase extends Resource implements ITaskDefinition { + + public abstract readonly compatibility: Compatibility; + public abstract readonly taskDefinitionArn: string; + public abstract readonly executionRole?: iam.IRole; + + /** + * Return true if the task definition can be run on an EC2 cluster + */ + public get isEc2Compatible(): boolean { + return isEc2Compatible(this.compatibility); + } + + /** + * Return true if the task definition can be run on a Fargate cluster + */ + public get isFargateCompatible(): boolean { + return isFargateCompatible(this.compatibility); + } +} + /** * Base class for Ecs and Fargate task definitions */ -export class TaskDefinition extends Resource { +export class TaskDefinition extends TaskDefinitionBase { + + /** + * Imports a task definition by ARN. + * + * The task will have a compatibility of EC2+Fargate. + */ + public static fromTaskDefinitionArn(scope: Construct, id: string, taskDefinitionArn: string): ITaskDefinition { + class Import extends TaskDefinitionBase { + public readonly taskDefinitionArn = taskDefinitionArn; + public readonly compatibility = Compatibility.Ec2AndFargate; + public readonly executionRole?: iam.IRole = undefined; + } + + return new Import(scope, id); + } + /** * The family name of this task definition */ @@ -132,14 +197,7 @@ export class TaskDefinition extends Resource { /** * What launching modes this task is compatible with */ - public compatibility: Compatibility; - - /** - * Execution role for this task definition - * - * May not exist, will be created as needed. - */ - public executionRole?: iam.IRole; + public readonly compatibility: Compatibility; /** * All containers @@ -156,6 +214,8 @@ export class TaskDefinition extends Resource { */ private readonly placementConstraints = new Array(); + private _executionRole?: iam.IRole; + constructor(scope: Construct, id: string, props: TaskDefinitionProps) { super(scope, id); @@ -180,7 +240,7 @@ export class TaskDefinition extends Resource { throw new Error(`Fargate-compatible tasks require both CPU (${props.cpu}) and memory (${props.memoryMiB}) specifications`); } - this.executionRole = props.executionRole; + this._executionRole = props.executionRole; this.taskRole = props.taskRole || new iam.Role(this, 'TaskRole', { assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), @@ -210,6 +270,10 @@ export class TaskDefinition extends Resource { this.taskDefinitionArn = taskDef.taskDefinitionArn; } + public get executionRole(): iam.IRole | undefined { + return this._executionRole; + } + /** * Add a policy statement to the Task Role */ @@ -273,26 +337,12 @@ export class TaskDefinition extends Resource { * Create the execution role if it doesn't exist */ public obtainExecutionRole(): iam.IRole { - if (!this.executionRole) { - this.executionRole = new iam.Role(this, 'ExecutionRole', { + if (!this._executionRole) { + this._executionRole = new iam.Role(this, 'ExecutionRole', { assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), }); } - return this.executionRole; - } - - /** - * Return true if the task definition can be run on an EC2 cluster - */ - public get isEc2Compatible(): boolean { - return isEc2Compatible(this.compatibility); - } - - /** - * Return true if the task definition can be run on a Fargate cluster - */ - public get isFargateCompatible(): boolean { - return isFargateCompatible(this.compatibility); + return this._executionRole; } /** @@ -449,6 +499,8 @@ export enum Compatibility { export interface ITaskDefinitionExtension { /** * Apply the extension to the given TaskDefinition + * + * @param taskDefinition [disable-awslint:ref-via-interface] */ extend(taskDefinition: TaskDefinition): void; } diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 91ff061a34ae6..7e37bb6a3b699 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -131,6 +131,9 @@ export class Cluster extends Resource implements ICluster { /** * Add compute capacity to this ECS cluster in the form of an AutoScalingGroup + * @param autoScalingGroup the ASG to add to this cluster. + * [disable-awslint:ref-via-interface] is needed in order to install the ECS + * agent by updating the ASGs user data. */ public addAutoScalingGroup(autoScalingGroup: autoscaling.AutoScalingGroup, options: AddAutoScalingGroupCapacityOptions = {}) { this._hasEc2Capacity = true; diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index fb6f1c581135c..1f0f4a98f4894 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -181,6 +181,8 @@ export interface ContainerDefinitionOptions { export interface ContainerDefinitionProps extends ContainerDefinitionOptions { /** * The task this container definition belongs to. + * + * [disable-awslint:ref-via-interface] */ readonly taskDefinition: TaskDefinition; } diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-event-rule-target.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-event-rule-target.ts index fc203ebbb5ebd..33f2df17307f7 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-event-rule-target.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-event-rule-target.ts @@ -1,7 +1,7 @@ import events = require ('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { TaskDefinition } from '../base/task-definition'; +import { Compatibility, ITaskDefinition } from '../base/task-definition'; import { ICluster } from '../cluster'; /** @@ -16,7 +16,7 @@ export interface Ec2EventRuleTargetProps { /** * Task Definition of the task that should be started */ - readonly taskDefinition: TaskDefinition; + readonly taskDefinition: ITaskDefinition; /** * How many tasks should be started when this event is triggered @@ -31,13 +31,13 @@ export interface Ec2EventRuleTargetProps { */ export class Ec2EventRuleTarget extends cdk.Construct implements events.IEventRuleTarget { private readonly cluster: ICluster; - private readonly taskDefinition: TaskDefinition; + private readonly taskDefinition: ITaskDefinition; private readonly taskCount: number; constructor(scope: cdk.Construct, id: string, props: Ec2EventRuleTargetProps) { super(scope, id); - if (!props.taskDefinition.isEc2Compatible) { + if (props.taskDefinition.compatibility === Compatibility.Fargate) { throw new Error('Supplied TaskDefinition is not configured for compatibility with EC2'); } @@ -75,9 +75,11 @@ export class Ec2EventRuleTarget extends cdk.Construct implements events.IEventRu * they can reuse the same role. */ public get eventsRole(): iam.IRole { - let role = this.taskDefinition.node.tryFindChild('EventsRole') as iam.IRole; + const stack = this.node.stack; + const id = `${this.taskDefinition.node.uniqueId}-EventsRole`; + let role = stack.node.tryFindChild(id) as iam.IRole; if (role === undefined) { - role = new iam.Role(this.taskDefinition, 'EventsRole', { + role = new iam.Role(stack, id, { assumedBy: new iam.ServicePrincipal('events.amazonaws.com') }); } diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index d4e9cf33d8b6f..43337f9215256 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -1,8 +1,8 @@ import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import elb = require('@aws-cdk/aws-elasticloadbalancing'); -import cdk = require('@aws-cdk/cdk'); -import { BaseService, BaseServiceProps } from '../base/base-service'; +import { Construct, Resource, Token } from '@aws-cdk/cdk'; +import { BaseService, BaseServiceProps, IService } from '../base/base-service'; import { NetworkMode, TaskDefinition } from '../base/task-definition'; import { CfnService } from '../ecs.generated'; import { BinPackResource, PlacementConstraint, PlacementStrategy } from '../placement'; @@ -13,6 +13,8 @@ import { BinPackResource, PlacementConstraint, PlacementStrategy } from '../plac export interface Ec2ServiceProps extends BaseServiceProps { /** * Task Definition used for running tasks in the service + * + * [disable-awslint:ref-via-interface] */ readonly taskDefinition: TaskDefinition; @@ -59,10 +61,24 @@ export interface Ec2ServiceProps extends BaseServiceProps { readonly daemon?: boolean; } +export interface IEc2Service extends IService { + +} + /** * Start a service on an ECS cluster + * + * @resource AWS::ECS::Service */ -export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { +export class Ec2Service extends BaseService implements IEc2Service, elb.ILoadBalancerTarget { + + public static fromEc2ServiceArn(scope: Construct, id: string, ec2ServiceArn: string): IEc2Service { + class Import extends Resource implements IEc2Service { + public readonly serviceArn = ec2ServiceArn; + } + return new Import(scope, id); + } + /** * Name of the cluster */ @@ -72,7 +88,7 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { private readonly strategies: CfnService.PlacementStrategyProperty[]; private readonly daemon: boolean; - constructor(scope: cdk.Construct, id: string, props: Ec2ServiceProps) { + constructor(scope: Construct, id: string, props: Ec2ServiceProps) { if (props.daemon && props.desiredCount !== undefined) { throw new Error('Daemon mode launches one task on every instance. Don\'t supply desiredCount.'); } @@ -100,8 +116,8 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { cluster: props.cluster.clusterName, taskDefinition: props.taskDefinition.taskDefinitionArn, launchType: 'EC2', - placementConstraints: new cdk.Token(() => this.constraints.length > 0 ? this.constraints : undefined), - placementStrategies: new cdk.Token(() => this.strategies.length > 0 ? this.strategies : undefined), + placementConstraints: new Token(() => this.constraints.length > 0 ? this.constraints : undefined), + placementStrategies: new Token(() => this.strategies.length > 0 ? this.strategies : undefined), schedulingStrategy: props.daemon ? 'DAEMON' : 'REPLICA', }, props.cluster.clusterName, props.taskDefinition); @@ -300,4 +316,4 @@ export class BuiltInAttributes { * Either 'linux' or 'windows'. */ public static readonly OsType = 'attribute:ecs.os-type'; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts index 75a4c62583d07..5c773a0cba82e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts @@ -1,5 +1,5 @@ -import cdk = require('@aws-cdk/cdk'); -import { CommonTaskDefinitionProps, Compatibility, NetworkMode, TaskDefinition } from '../base/task-definition'; +import { Construct, Resource } from '@aws-cdk/cdk'; +import { CommonTaskDefinitionProps, Compatibility, ITaskDefinition, NetworkMode, TaskDefinition } from '../base/task-definition'; import { PlacementConstraint } from '../placement'; /** @@ -25,17 +25,32 @@ export interface Ec2TaskDefinitionProps extends CommonTaskDefinitionProps { readonly placementConstraints?: PlacementConstraint[]; } +export interface IEc2TaskDefinition extends ITaskDefinition { + +} + /** * Define Tasks to run on an ECS cluster * * @resource AWS::ECS::TaskDefinition */ -export class Ec2TaskDefinition extends TaskDefinition { - constructor(scope: cdk.Construct, id: string, props: Ec2TaskDefinitionProps = {}) { +export class Ec2TaskDefinition extends TaskDefinition implements IEc2TaskDefinition { + + public static fromEc2TaskDefinitionArn(scope: Construct, id: string, ec2TaskDefinitionArn: string): IEc2TaskDefinition { + class Import extends Resource implements IEc2TaskDefinition { + public readonly taskDefinitionArn = ec2TaskDefinitionArn; + public readonly compatibility = Compatibility.Ec2; + public readonly isEc2Compatible = true; + public readonly isFargateCompatible = false; + } + return new Import(scope, id); + } + + constructor(scope: Construct, id: string, props: Ec2TaskDefinitionProps = {}) { super(scope, id, { ...props, compatibility: Compatibility.Ec2, placementConstraints: props.placementConstraints, }); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index a6b396d1b19df..6e49554d39909 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -1,6 +1,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); -import { BaseService, BaseServiceProps } from '../base/base-service'; +import { Construct, Resource } from '@aws-cdk/cdk'; +import { BaseService, BaseServiceProps, IService } from '../base/base-service'; import { TaskDefinition } from '../base/task-definition'; /** @@ -9,6 +10,8 @@ import { TaskDefinition } from '../base/task-definition'; export interface FargateServiceProps extends BaseServiceProps { /** * Task Definition used for running tasks in the service + * + * [disable-awslint:ref-via-interface] */ readonly taskDefinition: TaskDefinition; @@ -44,10 +47,24 @@ export interface FargateServiceProps extends BaseServiceProps { readonly platformVersion?: FargatePlatformVersion; } +export interface IFargateService extends IService { + +} + /** * Start a service on an ECS cluster + * + * @resource AWS::ECS::Service */ -export class FargateService extends BaseService { +export class FargateService extends BaseService implements IFargateService { + + public static fromFargateServiceArn(scope: Construct, id: string, fargateServiceArn: string): IFargateService { + class Import extends Resource implements IFargateService { + public readonly serviceArn = fargateServiceArn; + } + return new Import(scope, id); + } + constructor(scope: cdk.Construct, id: string, props: FargateServiceProps) { if (!props.taskDefinition.isFargateCompatible) { throw new Error('Supplied TaskDefinition is not configured for compatibility with Fargate'); diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index 49731e6d26ec5..dcfff8539da7b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -1,5 +1,5 @@ -import cdk = require('@aws-cdk/cdk'); -import { CommonTaskDefinitionProps, Compatibility, NetworkMode, TaskDefinition } from '../base/task-definition'; +import { Construct, Resource } from '@aws-cdk/cdk'; +import { CommonTaskDefinitionProps, Compatibility, ITaskDefinition, NetworkMode, TaskDefinition } from '../base/task-definition'; /** * Properties to define a Fargate Task @@ -39,11 +39,27 @@ export interface FargateTaskDefinitionProps extends CommonTaskDefinitionProps { readonly memoryMiB?: string; } +export interface IFargateTaskDefinition extends ITaskDefinition { + +} + /** * A definition for Tasks on a Fargate cluster * @resource AWS::ECS::TaskDefinition */ -export class FargateTaskDefinition extends TaskDefinition { +export class FargateTaskDefinition extends TaskDefinition implements IFargateTaskDefinition { + + public static fromFargateTaskDefinitionArn(scope: Construct, id: string, fargateTaskDefinitionArn: string): IFargateTaskDefinition { + class Import extends Resource implements IFargateTaskDefinition { + public readonly taskDefinitionArn = fargateTaskDefinitionArn; + public readonly compatibility = Compatibility.Fargate; + public readonly isEc2Compatible = false; + public readonly isFargateCompatible = true; + } + + return new Import(scope, id); + } + /** * The configured network mode */ @@ -52,7 +68,7 @@ export class FargateTaskDefinition extends TaskDefinition { // we need to explicitly write the type here, as type deduction for enums won't lead to // the import being generated in the .d.ts file. - constructor(scope: cdk.Construct, id: string, props: FargateTaskDefinitionProps = {}) { + constructor(scope: Construct, id: string, props: FargateTaskDefinitionProps = {}) { super(scope, id, { ...props, cpu: props.cpu || '256', diff --git a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts index 6ea1fa21d6c38..4a20c1287ebbe 100644 --- a/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts +++ b/packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts @@ -109,7 +109,7 @@ export interface LoadBalancedFargateServiceAppletProps extends cdk.StackProps { * load balancer, ECS cluster, VPC, and (optionally) Route53 alias record. */ export class LoadBalancedFargateServiceApplet extends cdk.Stack { - constructor(scope: cdk.App, id: string, props: LoadBalancedFargateServiceAppletProps) { + constructor(scope: cdk.Construct, id: string, props: LoadBalancedFargateServiceAppletProps) { super(scope, id, props); const vpc = new VpcNetwork(this, 'MyVpc', { maxAZs: 2 }); diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index b2ee60994b16e..532d1f041f6d4 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -113,12 +113,5 @@ }, "engines": { "node": ">= 8.10.0" - }, - "awslint": { - "exclude": [ - "resource-attribute:@aws-cdk/aws-ecs.ICluster.clusterArn", - "construct-ctor:@aws-cdk/aws-ecs.BaseService.", - "construct-ctor:@aws-cdk/aws-ecs.LoadBalancedFargateServiceApplet..params[0]" - ] } } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.expected.json index ed575becc83d6..9fd2edb69d81b 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.expected.json @@ -845,76 +845,6 @@ ] } }, - "TaskDefEventsRoleFB3B67B8": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "events.", - { - "Ref": "AWS::URLSuffix" - } - ] - ] - } - } - } - ], - "Version": "2012-10-17" - } - } - }, - "TaskDefEventsRoleDefaultPolicyA124E85B": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "ecs:RunTask", - "Condition": { - "ArnEquals": { - "ecs:cluster": { - "Fn::GetAtt": [ - "EcsCluster97242B84", - "Arn" - ] - } - } - }, - "Effect": "Allow", - "Resource": { - "Ref": "TaskDef54694570" - } - }, - { - "Action": "iam:PassRole", - "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "TaskDefExecutionRoleB4775C97", - "Arn" - ] - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "TaskDefEventsRoleDefaultPolicyA124E85B", - "Roles": [ - { - "Ref": "TaskDefEventsRoleFB3B67B8" - } - ] - } - }, "EventImageAdoptRepositoryDFAAC242": { "Type": "Custom::ECRAdoptedRepository", "Properties": { @@ -1127,13 +1057,83 @@ }, "RoleArn": { "Fn::GetAtt": [ - "TaskDefEventsRoleFB3B67B8", + "awsecsintegecsTaskDef8DD0C801EventsRoleC617AC5B", "Arn" ] } } ] } + }, + "awsecsintegecsTaskDef8DD0C801EventsRoleC617AC5B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "events.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "awsecsintegecsTaskDef8DD0C801EventsRoleDefaultPolicy2DFC09DA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ecs:RunTask", + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": { + "Ref": "TaskDef54694570" + } + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TaskDefExecutionRoleB4775C97", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "awsecsintegecsTaskDef8DD0C801EventsRoleDefaultPolicy2DFC09DA", + "Roles": [ + { + "Ref": "awsecsintegecsTaskDef8DD0C801EventsRoleC617AC5B" + } + ] + } } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-event-rule-target.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-event-rule-target.ts index f706a7a497735..055d1b80fc890 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-event-rule-target.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-event-rule-target.ts @@ -51,7 +51,7 @@ export = { InputTransformer: { InputTemplate: "{\"argument\":\"hello\"}" }, - RoleArn: { "Fn::GetAtt": ["TaskDefEventsRoleFB3B67B8", "Arn"] } + RoleArn: { "Fn::GetAtt": ["TaskDefEventsRole7BD19E45", "Arn"] } } ] })); diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 8f2c1da02cf1c..eee8de971dacc 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -1,5 +1,6 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); import ec2 = require('@aws-cdk/aws-ec2'); +import { VpcSubnet } from '@aws-cdk/aws-ec2'; import iam = require('@aws-cdk/aws-iam'); import { CfnOutput, Construct, IResource, Resource, Tag } from '@aws-cdk/cdk'; import { EksOptimizedAmi, nodeTypeForInstanceType } from './ami'; @@ -317,6 +318,7 @@ export class Cluster extends ClusterBase { * the right AMI and the `maxPods` number based on your instance type. * * @see https://docs.aws.amazon.com/eks/latest/userguide/launch-workers.html + * @param autoScalingGroup [disable-awslint:ref-via-interface] */ public addAutoScalingGroup(autoScalingGroup: autoscaling.AutoScalingGroup, options: AddAutoScalingGroupOptions) { // self rules @@ -364,7 +366,7 @@ export class Cluster extends ClusterBase { */ private tagSubnets() { for (const subnet of this.vpc.privateSubnets) { - if (!isRealSubnetConstruct(subnet)) { + if (!VpcSubnet.isVpcSubnet(subnet)) { // Just give up, all of them will be the same. this.node.addWarning('Could not auto-tag private subnets with "kubernetes.io/role/internal-elb=1", please remember to do this manually'); return; @@ -375,10 +377,6 @@ export class Cluster extends ClusterBase { } } -function isRealSubnetConstruct(subnet: ec2.IVpcSubnet): subnet is ec2.VpcSubnet { - return (subnet as any).addDefaultRouteToIGW !== undefined; -} - /** * Options for adding worker nodes */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts index 515f37e4b3a4b..72ad94c36a209 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts @@ -109,6 +109,7 @@ export interface HealthCheck { export interface ILoadBalancerTarget extends IConnectable { /** * Attach load-balanced target to a classic ELB + * @param loadBalancer [disable-awslint:ref-via-interface] The load balancer to attach the target to */ attachToClassicLB(loadBalancer: LoadBalancer): void; } diff --git a/packages/@aws-cdk/aws-iam/lib/group.ts b/packages/@aws-cdk/aws-iam/lib/group.ts index 228321690e3c5..96a3e8404b7a8 100644 --- a/packages/@aws-cdk/aws-iam/lib/group.ts +++ b/packages/@aws-cdk/aws-iam/lib/group.ts @@ -4,9 +4,21 @@ import { IIdentity } from './identity-base'; import { Policy } from './policy'; import { ArnPrincipal, PolicyStatement, PrincipalPolicyFragment } from './policy-document'; import { IPrincipal } from './principals'; -import { User } from './user'; +import { IUser } from './user'; import { AttachedPolicies, undefinedIfEmpty } from './util'; +export interface IGroup extends IIdentity { + /** + * @attribute + */ + readonly groupName: string; + + /** + * @attribute + */ + readonly groupArn: string; +} + export interface GroupProps { /** * A name for the IAM group. For valid values, see the GroupName parameter @@ -36,50 +48,18 @@ export interface GroupProps { readonly path?: string; } -export class Group extends Resource implements IIdentity { +abstract class GroupBase extends Resource implements IGroup { + public abstract readonly groupName: string; + public abstract readonly groupArn: string; + public readonly grantPrincipal: IPrincipal = this; public readonly assumeRoleAction: string = 'sts:AssumeRole'; - /** - * The runtime name of this group. - * @attribute - */ - public readonly groupName: string; - - /** - * The ARN of this group. - * @attribute - */ - public readonly groupArn: string; - - public readonly policyFragment: PrincipalPolicyFragment; - - private readonly managedPolicies: string[]; private readonly attachedPolicies = new AttachedPolicies(); private defaultPolicy?: Policy; - constructor(scope: Construct, id: string, props: GroupProps = {}) { - super(scope, id); - - this.managedPolicies = props.managedPolicyArns || []; - - const group = new CfnGroup(this, 'Resource', { - groupName: props.groupName, - managedPolicyArns: undefinedIfEmpty(() => this.managedPolicies), - path: props.path, - }); - - this.groupName = group.groupName; - this.groupArn = group.groupArn; - this.policyFragment = new ArnPrincipal(this.groupArn).policyFragment; - } - - /** - * Attaches a managed policy to this group. - * @param arn The ARN of the managed policy to attach. - */ - public attachManagedPolicy(arn: string) { - this.managedPolicies.push(arn); + public get policyFragment(): PrincipalPolicyFragment { + return new ArnPrincipal(this.groupArn).policyFragment; } /** @@ -91,10 +71,14 @@ export class Group extends Resource implements IIdentity { policy.attachToGroup(this); } + public attachManagedPolicy(_arn: string) { + // drop + } + /** * Adds a user to this group. */ - public addUser(user: User) { + public addUser(user: IUser) { user.addToGroup(this); } @@ -111,3 +95,48 @@ export class Group extends Resource implements IIdentity { return true; } } + +export class Group extends GroupBase { + + /** + * Imports a group from ARN + * @param groupArn (e.g. `arn:aws:iam::account-id:group/group-name`) + */ + public static fromGroupArn(scope: Construct, id: string, groupArn: string): IGroup { + const groupName = scope.node.stack.parseArn(groupArn).resourceName!; + class Import extends GroupBase { + public groupName = groupName; + public groupArn = groupArn; + } + + return new Import(scope, id); + } + + public readonly groupName: string; + public readonly groupArn: string; + + private readonly managedPolicies: string[]; + + constructor(scope: Construct, id: string, props: GroupProps = {}) { + super(scope, id); + + this.managedPolicies = props.managedPolicyArns || []; + + const group = new CfnGroup(this, 'Resource', { + groupName: props.groupName, + managedPolicyArns: undefinedIfEmpty(() => this.managedPolicies), + path: props.path, + }); + + this.groupName = group.groupName; + this.groupArn = group.groupArn; + } + + /** + * Attaches a managed policy to this group. + * @param arn The ARN of the managed policy to attach. + */ + public attachManagedPolicy(arn: string) { + this.managedPolicies.push(arn); + } +} diff --git a/packages/@aws-cdk/aws-iam/lib/identity-base.ts b/packages/@aws-cdk/aws-iam/lib/identity-base.ts index 026064f033cec..48dd488b86fac 100644 --- a/packages/@aws-cdk/aws-iam/lib/identity-base.ts +++ b/packages/@aws-cdk/aws-iam/lib/identity-base.ts @@ -9,7 +9,7 @@ export interface IIdentity extends IPrincipal, IResource { /** * Attaches an inline policy to this principal. * This is the same as calling `policy.addToXxx(principal)`. - * @param policy The policy resource to attach to this principal. + * @param policy The policy resource to attach to this principal [disable-awslint:ref-via-interface] */ attachInlinePolicy(policy: Policy): void; diff --git a/packages/@aws-cdk/aws-iam/lib/policy.ts b/packages/@aws-cdk/aws-iam/lib/policy.ts index 1f5059b4e1a78..415cc4ded027b 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy.ts @@ -1,11 +1,18 @@ -import { Construct, Resource, Token } from '@aws-cdk/cdk'; -import { Group } from './group'; +import { Construct, IResource, Resource, Token } from '@aws-cdk/cdk'; +import { IGroup } from './group'; import { CfnPolicy } from './iam.generated'; import { PolicyDocument, PolicyStatement } from './policy-document'; import { IRole } from './role'; -import { User } from './user'; +import { IUser } from './user'; import { generatePolicyName, undefinedIfEmpty } from './util'; +export interface IPolicy extends IResource { + /** + * @attribute + */ + readonly policyName: string; +} + export interface PolicyProps { /** * The name of the policy. If you specify multiple policies for an entity, @@ -21,7 +28,7 @@ export interface PolicyProps { * Users to attach this policy to. * You can also use `attachToUser(user)` to attach this policy to a user. */ - readonly users?: User[]; + readonly users?: IUser[]; /** * Roles to attach this policy to. @@ -33,7 +40,7 @@ export interface PolicyProps { * Groups to attach this policy to. * You can also use `attachToGroup(group)` to attach this policy to a group. */ - readonly groups?: Group[]; + readonly groups?: IGroup[]; /** * Initial set of permissions to add to this policy document. @@ -48,7 +55,16 @@ export interface PolicyProps { * Policies](http://docs.aws.amazon.com/IAM/latest/UserGuide/policies_overview.html) * in the IAM User Guide guide. */ -export class Policy extends Resource { +export class Policy extends Resource implements IPolicy { + + public static fromPolicyName(scope: Construct, id: string, policyName: string): IPolicy { + class Import extends Resource implements IPolicy { + public readonly policyName = policyName; + } + + return new Import(scope, id); + } + /** * The policy document. */ @@ -62,8 +78,8 @@ export class Policy extends Resource { public readonly policyName: string; private readonly roles = new Array(); - private readonly users = new Array(); - private readonly groups = new Array(); + private readonly users = new Array(); + private readonly groups = new Array(); constructor(scope: Construct, id: string, props: PolicyProps = {}) { super(scope, id); @@ -108,7 +124,7 @@ export class Policy extends Resource { /** * Attaches this policy to a user. */ - public attachToUser(user: User) { + public attachToUser(user: IUser) { if (this.users.find(u => u === user)) { return; } this.users.push(user); user.attachInlinePolicy(this); @@ -126,7 +142,7 @@ export class Policy extends Resource { /** * Attaches this policy to a group. */ - public attachToGroup(group: Group) { + public attachToGroup(group: IGroup) { if (this.groups.find(g => g === group)) { return; } this.groups.push(group); group.attachInlinePolicy(this); diff --git a/packages/@aws-cdk/aws-iam/lib/user.ts b/packages/@aws-cdk/aws-iam/lib/user.ts index 08851a6b2f836..f0e1d9ac9f6eb 100644 --- a/packages/@aws-cdk/aws-iam/lib/user.ts +++ b/packages/@aws-cdk/aws-iam/lib/user.ts @@ -1,5 +1,5 @@ import { Construct, Resource, SecretValue } from '@aws-cdk/cdk'; -import { Group } from './group'; +import { IGroup } from './group'; import { CfnUser } from './iam.generated'; import { IIdentity } from './identity-base'; import { Policy } from './policy'; @@ -8,12 +8,17 @@ import { ArnPrincipal, PrincipalPolicyFragment } from './policy-document'; import { IPrincipal } from './principals'; import { AttachedPolicies, undefinedIfEmpty } from './util'; +export interface IUser extends IIdentity { + readonly userName: string; + addToGroup(group: IGroup): void; +} + export interface UserProps { /** * Groups to add this user to. You can also use `addToGroup` to add this * user to a group. */ - readonly groups?: Group[]; + readonly groups?: IGroup[]; /** * A list of ARNs for managed policies attacherd to this user. @@ -114,7 +119,7 @@ export class User extends Resource implements IIdentity { /** * Adds this user to a group. */ - public addToGroup(group: Group) { + public addToGroup(group: IGroup) { this.groups.push(group.groupName); } diff --git a/packages/@aws-cdk/aws-iam/lib/util.ts b/packages/@aws-cdk/aws-iam/lib/util.ts index 8fc2a504f9557..930ecd15b915f 100644 --- a/packages/@aws-cdk/aws-iam/lib/util.ts +++ b/packages/@aws-cdk/aws-iam/lib/util.ts @@ -1,5 +1,5 @@ import { Token } from '@aws-cdk/cdk'; -import { Policy } from './policy'; +import { IPolicy } from './policy'; const MAX_POLICY_NAME_LEN = 128; @@ -24,7 +24,7 @@ export function generatePolicyName(logicalId: string) { * Helper class that maintains the set of attached policies for a principal. */ export class AttachedPolicies { - private policies = new Array(); + private policies = new Array(); /** * Adds a policy to the list of attached policies. @@ -32,7 +32,7 @@ export class AttachedPolicies { * If this policy is already, attached, returns false. * If there is another policy attached with the same name, throws an exception. */ - public attach(policy: Policy) { + public attach(policy: IPolicy) { if (this.policies.find(p => p === policy)) { return; // already attached } diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/api.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/api.ts index 870ef7055f63d..c1945bb0220c9 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/api.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/api.ts @@ -8,11 +8,12 @@ export class ApiEventSource implements lambda.IEventSource { } } - public bind(target: lambda.FunctionBase): void { - const id = 'ApiEventSourceA7A86A4F'; - let api = target.node.tryFindChild(id) as apigw.RestApi; + public bind(target: lambda.IFunction): void { + const id = `${target.node.uniqueId}:ApiEventSourceA7A86A4F`; + const stack = target.node.stack; + let api = stack.node.tryFindChild(id) as apigw.RestApi; if (!api) { - api = new apigw.RestApi(target, id, { + api = new apigw.RestApi(stack, id, { defaultIntegration: new apigw.LambdaIntegration(target), }); } diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts index 156ceea800db7..0c96fd5f13441 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts @@ -29,9 +29,8 @@ export class DynamoEventSource implements lambda.IEventSource { } } - public bind(target: lambda.FunctionBase) { - new lambda.EventSourceMapping(target, `DynamoDBEventSource:${this.table.node.uniqueId}`, { - target, + public bind(target: lambda.IFunction) { + target.addEventSourceMapping(`DynamoDBEventSource:${this.table.node.uniqueId}`, { batchSize: this.props.batchSize || 100, eventSourceArn: this.table.tableStreamArn, startingPosition: this.props.startingPosition diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/kinesis.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/kinesis.ts index 5893935ab375b..93102e456432e 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/kinesis.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/kinesis.ts @@ -29,9 +29,8 @@ export class KinesisEventSource implements lambda.IEventSource { } } - public bind(target: lambda.FunctionBase) { - new lambda.EventSourceMapping(target, `KinesisEventSource:${this.stream.node.uniqueId}`, { - target, + public bind(target: lambda.IFunction) { + target.addEventSourceMapping(`KinesisEventSource:${this.stream.node.uniqueId}`, { batchSize: this.props.batchSize || 100, startingPosition: this.props.startingPosition, eventSourceArn: this.stream.streamArn, diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/s3.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/s3.ts index be8a2253caf75..3b669ad26a0f2 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/s3.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/s3.ts @@ -24,7 +24,7 @@ export class S3EventSource implements lambda.IEventSource { } - public bind(target: lambda.FunctionBase) { + public bind(target: lambda.IFunction) { const filters = this.props.filters || []; for (const event of this.props.events) { this.bucket.onEvent(event, target, ...filters); diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/sns.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/sns.ts index f4f690f00dd7d..d2c40c1b4b47b 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/sns.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/sns.ts @@ -8,7 +8,7 @@ export class SnsEventSource implements lambda.IEventSource { constructor(readonly topic: sns.ITopic) { } - public bind(target: lambda.FunctionBase) { + public bind(target: lambda.IFunction) { this.topic.subscribeLambda(target); } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/sqs.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/sqs.ts index d372116fe49f2..2752fdd370ff4 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/sqs.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/sqs.ts @@ -24,9 +24,8 @@ export class SqsEventSource implements lambda.IEventSource { } } - public bind(target: lambda.FunctionBase) { - new lambda.EventSourceMapping(target, `SqsEventSource:${this.queue.node.uniqueId}`, { - target, + public bind(target: lambda.IFunction) { + target.addEventSourceMapping(`SqsEventSource:${this.queue.node.uniqueId}`, { batchSize: this.props.batchSize, eventSourceArn: this.queue.queueArn, }); diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.api.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/test.api.ts index 0076d043b54ad..44d00f76d9daa 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/test.api.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/test.api.ts @@ -20,12 +20,12 @@ export = { // THEN expect(stack).to(haveResource('AWS::ApiGateway::Resource', { PathPart: "foo", - ParentId: { "Fn::GetAtt": [ "MyFuncApiEventSourceA7A86A4FF4B434AC", "RootResourceId" ] } + ParentId: { "Fn::GetAtt": [ "MyFuncApiEventSourceA7A86A4FFB3F557C", "RootResourceId" ] } })); expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: 'GET', - ResourceId: { Ref: "MyFuncApiEventSourceA7A86A4Ffoo73254F28" }, + ResourceId: { Ref: "MyFuncApiEventSourceA7A86A4FfooCA6F87E4" }, })); test.done(); @@ -47,22 +47,22 @@ export = { // THEN expect(stack).to(haveResource('AWS::ApiGateway::Resource', { PathPart: "foo", - ParentId: { "Fn::GetAtt": [ "MyFuncApiEventSourceA7A86A4FF4B434AC", "RootResourceId" ] } + ParentId: { "Fn::GetAtt": [ "MyFuncApiEventSourceA7A86A4FFB3F557C", "RootResourceId" ] } })); expect(stack).to(haveResource('AWS::ApiGateway::Resource', { PathPart: "bar", - ParentId: { "Fn::GetAtt": [ "MyFuncApiEventSourceA7A86A4FF4B434AC", "RootResourceId" ] } + ParentId: { "Fn::GetAtt": [ "MyFuncApiEventSourceA7A86A4FFB3F557C", "RootResourceId" ] } })); expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: 'GET', - ResourceId: { Ref: "MyFuncApiEventSourceA7A86A4Ffoo73254F28" }, + ResourceId: { Ref: "MyFuncApiEventSourceA7A86A4FfooCA6F87E4" }, })); expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: 'POST', - ResourceId: { Ref: "MyFuncApiEventSourceA7A86A4FbarFF0EF497" }, + ResourceId: { Ref: "MyFuncApiEventSourceA7A86A4FbarDFB0F21B" }, })); test.done(); @@ -85,22 +85,22 @@ export = { // THEN expect(stack).to(haveResource('AWS::ApiGateway::Resource', { PathPart: "foo", - ParentId: { "Fn::GetAtt": [ "MyFuncApiEventSourceA7A86A4FF4B434AC", "RootResourceId" ] } + ParentId: { "Fn::GetAtt": [ "MyFuncApiEventSourceA7A86A4FFB3F557C", "RootResourceId" ] } })); expect(stack).to(haveResource('AWS::ApiGateway::Resource', { PathPart: "bar", - ParentId: { Ref: "MyFuncApiEventSourceA7A86A4Ffoo73254F28" } + ParentId: { Ref: "MyFuncApiEventSourceA7A86A4FfooCA6F87E4" } })); expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: 'GET', - ResourceId: { Ref: "MyFuncApiEventSourceA7A86A4Ffoo73254F28" }, + ResourceId: { Ref: "MyFuncApiEventSourceA7A86A4FfooCA6F87E4" }, })); expect(stack).to(haveResource('AWS::ApiGateway::Method', { HttpMethod: 'POST', - ResourceId: { Ref: "MyFuncApiEventSourceA7A86A4Ffoobarzoo34214ADE" }, + ResourceId: { Ref: "MyFuncApiEventSourceA7A86A4Ffoobar028FFFDE" }, })); test.done(); diff --git a/packages/@aws-cdk/aws-lambda/lib/alias.ts b/packages/@aws-cdk/aws-lambda/lib/alias.ts index 0105a9052381d..4c4512a093ea8 100644 --- a/packages/@aws-cdk/aws-lambda/lib/alias.ts +++ b/packages/@aws-cdk/aws-lambda/lib/alias.ts @@ -1,7 +1,7 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import { CfnOutput, Construct } from '@aws-cdk/cdk'; import { FunctionAttributes, FunctionBase, IFunction } from './function-base'; -import { Version } from './lambda-version'; +import { IVersion } from './lambda-version'; import { CfnAlias } from './lambda.generated'; /** @@ -20,7 +20,7 @@ export interface AliasProps { * * Use lambda.addVersion() to obtain a new lambda version to refer to. */ - readonly version: Version; + readonly version: IVersion; /** * Name of this alias @@ -176,7 +176,7 @@ export interface VersionWeight { /** * The version to route traffic to */ - readonly version: Version; + readonly version: IVersion; /** * How much weight to assign to this version (0..1) diff --git a/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts b/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts index 6b13dc66eb88b..073900ab07eec 100644 --- a/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts +++ b/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts @@ -3,18 +3,13 @@ import { Resource } from '@aws-cdk/cdk'; import { IFunction } from './function-base'; import { CfnEventSourceMapping } from './lambda.generated'; -export interface EventSourceMappingProps { +export interface EventSourceMappingOptions { /** * The Amazon Resource Name (ARN) of the event source. Any record added to * this stream can invoke the Lambda function. */ readonly eventSourceArn: string; - /** - * The target AWS Lambda function. - */ - readonly target: IFunction; - /** * The largest number of records that AWS Lambda will retrieve from your event * source at the time of invoking your function. Your function receives an @@ -43,6 +38,13 @@ export interface EventSourceMappingProps { readonly startingPosition?: StartingPosition } +export interface EventSourceMappingProps extends EventSourceMappingOptions { + /** + * The target AWS Lambda function. + */ + readonly target: IFunction; +} + /** * Defines a Lambda EventSourceMapping resource. * diff --git a/packages/@aws-cdk/aws-lambda/lib/event-source.ts b/packages/@aws-cdk/aws-lambda/lib/event-source.ts index 1eec70b08c809..d4e6340c33dfa 100644 --- a/packages/@aws-cdk/aws-lambda/lib/event-source.ts +++ b/packages/@aws-cdk/aws-lambda/lib/event-source.ts @@ -1,4 +1,4 @@ -import { FunctionBase } from './function-base'; +import { IFunction } from './function-base'; /** * An abstract class which represents an AWS Lambda event source. @@ -9,5 +9,5 @@ export interface IEventSource { * function. * @param target That lambda function to bind to. */ - bind(target: FunctionBase): void; + bind(target: IFunction): void; } diff --git a/packages/@aws-cdk/aws-lambda/lib/function-base.ts b/packages/@aws-cdk/aws-lambda/lib/function-base.ts index fac1cef7be9e7..0f5c818260697 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-base.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-base.ts @@ -6,6 +6,7 @@ import s3n = require('@aws-cdk/aws-s3-notifications'); import cdk = require('@aws-cdk/cdk'); import { IResource, Resource } from '@aws-cdk/cdk'; import { IEventSource } from './event-source'; +import { EventSourceMapping, EventSourceMappingOptions } from './event-source-mapping'; import { CfnPermission } from './lambda.generated'; import { Permission } from './permission'; @@ -43,6 +44,13 @@ export interface IFunction extends IResource, logs.ILogSubscriptionDestination, */ readonly isBoundToVpc: boolean; + /** + * Adds an event source that maps to this AWS Lambda function. + * @param id construct ID + * @param options mapping options + */ + addEventSourceMapping(id: string, options: EventSourceMappingOptions): EventSourceMapping; + /** * Adds a permission to the Lambda resource policy. * @param id The id ƒor the permission construct @@ -217,6 +225,13 @@ export abstract class FunctionBase extends Resource implements IFunction { return !!this._connections; } + public addEventSourceMapping(id: string, options: EventSourceMappingOptions): EventSourceMapping { + return new EventSourceMapping(this, id, { + target: this, + ...options + }); + } + /** * Grant the given identity permissions to invoke this Lambda */ diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts index 83499a1f72c7b..800062141a052 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts @@ -1,7 +1,20 @@ -import { Construct, Resource } from '@aws-cdk/cdk'; +import { Construct, IResource, Resource } from '@aws-cdk/cdk'; import { IFunction } from './function-base'; import { CfnVersion } from './lambda.generated'; +export interface IVersion extends IResource { + /** + * The most recently deployed version of this function. + * @attribute + */ + readonly version: string; + + /** + * The underlying AWS Lambda function. + */ + readonly lambda: IFunction; +} + /** * Properties for a new Lambda version */ @@ -28,6 +41,18 @@ export interface VersionProps { readonly lambda: IFunction; } +export interface VersionAttributes { + /** + * The version. + */ + readonly version: string; + + /** + * The lambda function. + */ + readonly lambda: IFunction; +} + /** * A single newly-deployed version of a Lambda function. * @@ -44,16 +69,17 @@ export interface VersionProps { * the right deployment, specify the `codeSha256` property while * creating the `Version. */ -export class Version extends Resource { - /** - * The most recently deployed version of this function. - * @attribute - */ - public readonly version: string; +export class Version extends Resource implements IVersion { - /** - * Lambda object this version is associated with - */ + public static fromVersionAttributes(scope: Construct, id: string, attrs: VersionAttributes): IVersion { + class Import extends Resource implements IVersion { + public readonly version = attrs.version; + public readonly lambda = attrs.lambda; + } + return new Import(scope, id); + } + + public readonly version: string; public readonly lambda: IFunction; constructor(scope: Construct, id: string, props: VersionProps) { diff --git a/packages/@aws-cdk/aws-lambda/lib/layers.ts b/packages/@aws-cdk/aws-lambda/lib/layers.ts index 6dac11b8d8676..553471d4c6688 100644 --- a/packages/@aws-cdk/aws-lambda/lib/layers.ts +++ b/packages/@aws-cdk/aws-lambda/lib/layers.ts @@ -54,15 +54,17 @@ export interface ILayerVersion extends IResource { export(): LayerVersionAttributes; /** - * Grants usage of this layer to specific entities. Usage within the same account where the layer is defined is always - * allowed and does not require calling this method. Note that the principal that creates the Lambda function using - * the layer (for example, a CloudFormation changeset execution role) also needs to have the - * ``lambda:GetLayerVersion`` permission on the layer version. + * Add permission for this layer version to specific entities. Usage within + * the same account where the layer is defined is always allowed and does not + * require calling this method. Note that the principal that creates the + * Lambda function using the layer (for example, a CloudFormation changeset + * execution role) also needs to have the ``lambda:GetLayerVersion`` + * permission on the layer version. * * @param id the ID of the grant in the construct tree. - * @param grantee the identification of the grantee. + * @param permission the identification of the grantee. */ - grantUsage(id: string, grantee: LayerVersionUsageGrantee): ILayerVersion + addPermission(id: string, permission: LayerVersionPermission): void; } /** @@ -72,18 +74,17 @@ abstract class LayerVersionBase extends Resource implements ILayerVersion { public abstract readonly layerVersionArn: string; public abstract readonly compatibleRuntimes?: Runtime[]; - public grantUsage(id: string, grantee: LayerVersionUsageGrantee): ILayerVersion { - if (grantee.organizationId != null && grantee.accountId !== '*') { - throw new Error(`OrganizationId can only be specified if AwsAccountId is '*', but it is ${grantee.accountId}`); + public addPermission(id: string, permission: LayerVersionPermission) { + if (permission.organizationId != null && permission.accountId !== '*') { + throw new Error(`OrganizationId can only be specified if AwsAccountId is '*', but it is ${permission.accountId}`); } new CfnLayerVersionPermission(this, id, { action: 'lambda:GetLayerVersion', layerVersionArn: this.layerVersionArn, - principal: grantee.accountId, - organizationId: grantee.organizationId, + principal: permission.accountId, + organizationId: permission.organizationId, }); - return this; } public export(): LayerVersionAttributes { @@ -97,7 +98,7 @@ abstract class LayerVersionBase extends Resource implements ILayerVersion { /** * Identification of an account (or organization) that is allowed to access a Lambda Layer Version. */ -export interface LayerVersionUsageGrantee { +export interface LayerVersionPermission { /** * The AWS Account id of the account that is authorized to use a Lambda Layer Version. The wild-card ``'*'`` can be * used to grant access to "any" account (or any account in an organization when ``organizationId`` is specified). @@ -231,9 +232,8 @@ export class SingletonLayerVersion extends Construct implements ILayerVersion { }; } - public grantUsage(id: string, grantee: LayerVersionUsageGrantee): ILayerVersion { - this.layerVersion.grantUsage(id, grantee); - return this; + public addPermission(id: string, grantee: LayerVersionPermission) { + this.layerVersion.addPermission(id, grantee); } private ensureLayerVersion(props: SingletonLayerVersionProps): ILayerVersion { diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 8e3de5ecd2da2..9ab3e71f08f47 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -104,15 +104,5 @@ }, "engines": { "node": ">= 8.10.0" - }, - "awslint": { - "exclude": [ - "construct-base-is-private:@aws-cdk/aws-lambda.FunctionBase", - "grant-result:@aws-cdk/aws-lambda.LayerVersion.grantUsage", - "from-method:@aws-cdk/aws-lambda.Alias", - "from-attributes:fromAliasAttributes", - "from-method:@aws-cdk/aws-lambda.SingletonFunction", - "from-method:@aws-cdk/aws-lambda.Version" - ] } } diff --git a/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.ts b/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.ts index de01eb0c1d5f0..3d79d80a89d95 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.ts +++ b/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.ts @@ -18,7 +18,7 @@ const layer = new lambda.LayerVersion(stack, 'MyLayer', { }); // To grant usage by other AWS accounts -layer.grantUsage('remote-account-grant', { accountId: awsAccountId }); +layer.addPermission('remote-account-grant', { accountId: awsAccountId }); // To grant usage to all accounts in some AWS Ogranization // layer.grantUsage({ accountId: '*', organizationId }); diff --git a/packages/@aws-cdk/aws-lambda/test/test.layers.ts b/packages/@aws-cdk/aws-lambda/test/test.layers.ts index 11cebcb8bc634..244c58095ace8 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.layers.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.layers.ts @@ -40,8 +40,8 @@ export = testCase({ }); // WHEN - layer.grantUsage('GrantUsage-123456789012', { accountId: '123456789012' }); - layer.grantUsage('GrantUsage-o-123456', { accountId: '*', organizationId: 'o-123456' }); + layer.addPermission('GrantUsage-123456789012', { accountId: '123456789012' }); + layer.addPermission('GrantUsage-o-123456', { accountId: '*', organizationId: 'o-123456' }); // THEN expect(stack).to(haveResource('AWS::Lambda::LayerVersionPermission', { diff --git a/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts b/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts index b14448e2c7afd..7c03a3766c166 100644 --- a/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts +++ b/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts @@ -17,7 +17,7 @@ export interface CrossAccountDestinationProps { * * The role must be assumable by 'logs.{REGION}.amazonaws.com'. */ - readonly role: iam.Role; + readonly role: iam.IRole; /** * The log destination target's ARN diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts index c83b41982dd43..921b37a84fdc8 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts @@ -161,7 +161,7 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone { * @param delegate the zone being delegated to. * @param opts options for creating the DNS record, if any. */ - public addDelegation(delegate: PublicHostedZone, opts: ZoneDelegationOptions = {}): void { + public addDelegation(delegate: IPublicHostedZone, opts: ZoneDelegationOptions = {}): void { new ZoneDelegationRecord(this, `${this.zoneName} -> ${delegate.zoneName}`, { zone: this, delegatedZoneName: delegate.zoneName, diff --git a/packages/@aws-cdk/aws-s3/package.json b/packages/@aws-cdk/aws-s3/package.json index 7a9f3c5d02cb6..c9d6f9d2c8f24 100644 --- a/packages/@aws-cdk/aws-s3/package.json +++ b/packages/@aws-cdk/aws-s3/package.json @@ -81,14 +81,5 @@ }, "engines": { "node": ">= 8.10.0" - }, - "awslint": { - "exclude": [ - "resource-attribute:@aws-cdk/aws-s3.IBucket.bucketDomainName", - "resource-attribute:@aws-cdk/aws-s3.IBucket.bucketDualStackDomainName", - "resource-attribute:@aws-cdk/aws-s3.IBucket.bucketRegionalDomainName", - "resource-attribute:@aws-cdk/aws-s3.IBucket.bucketWebsiteUrl", - "resource-interface:@aws-cdk/aws-s3.IBucketPolicy" - ] } } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 1eee9ad06408e..fb53a145dbf1c 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -27,7 +27,7 @@ export interface StateMachineProps { * * @default A role is automatically created */ - readonly role?: iam.Role; + readonly role?: iam.IRole; /** * Maximum run time for this state machine @@ -55,7 +55,7 @@ export class StateMachine extends Resource implements IStateMachine, events.IEve /** * Execution role of this state machine */ - public readonly role: iam.Role; + public readonly role: iam.IRole; /** * The name of the state machine diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index 54a3d00e113b1..dd524d03fdbac 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -242,4 +242,4 @@ export class Task extends State implements INextable { } return this.metric(prefix + suffix, props); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/cdk/lib/stack.ts b/packages/@aws-cdk/cdk/lib/stack.ts index 6f7a66206c42f..d8392af9aec31 100644 --- a/packages/@aws-cdk/cdk/lib/stack.ts +++ b/packages/@aws-cdk/cdk/lib/stack.ts @@ -143,16 +143,16 @@ export class Stack extends Construct { Object.defineProperty(this, STACK_SYMBOL, { value: true }); - if (name && !Stack.VALID_STACK_NAME_REGEX.test(name)) { - throw new Error(`Stack name must match the regular expression: ${Stack.VALID_STACK_NAME_REGEX.toString()}, got '${name}'`); - } - this.configuredEnv = props.env || {}; this.env = this.parseEnvironment(props.env); this.logicalIds = new LogicalIDs(props && props.namingScheme ? props.namingScheme : new HashedAddressingScheme()); this.name = props.stackName !== undefined ? props.stackName : this.calculateStackName(); this.autoDeploy = props && props.autoDeploy === false ? false : true; + + if (!Stack.VALID_STACK_NAME_REGEX.test(this.name)) { + throw new Error(`Stack name must match the regular expression: ${Stack.VALID_STACK_NAME_REGEX.toString()}, got '${name}'`); + } } /** diff --git a/packages/@aws-cdk/cdk/test/test.stack.ts b/packages/@aws-cdk/cdk/test/test.stack.ts index 4a7791a0778a4..8d7e37f85b135 100644 --- a/packages/@aws-cdk/cdk/test/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/test.stack.ts @@ -396,6 +396,33 @@ export = { test.done(); }, + + 'stack construct id does not go through stack name validation if there is an explicit stack name'(test: Test) { + // GIVEN + const app = new App(); + + // WHEN + const stack = new Stack(app, 'invalid as : stack name, but thats fine', { + stackName: 'valid-stack-name' + }); + + // THEN + const session = app.run(); + test.deepEqual(stack.name, 'valid-stack-name'); + test.ok('valid-stack-name' in (session.manifest.artifacts || {})); + test.done(); + }, + + 'stack validation is performed on explicit stack name'(test: Test) { + // GIVEN + const app = new App(); + + // THEN + test.throws(() => new Stack(app, 'boom', { stackName: 'invalid:stack:name' }), + /Stack name must match the regular expression/); + + test.done(); + } }; class StackWithPostProcessor extends Stack { diff --git a/packages/decdk/test/__snapshots__/synth.test.js.snap b/packages/decdk/test/__snapshots__/synth.test.js.snap index a89d53a44d62d..155f265f0271e 100644 --- a/packages/decdk/test/__snapshots__/synth.test.js.snap +++ b/packages/decdk/test/__snapshots__/synth.test.js.snap @@ -919,14 +919,14 @@ Object { exports[`lambda-events.json: lambda-events 1`] = ` Object { "Outputs": Object { - "HelloWorldFunctionApiEventSourceA7A86A4FEndpointF99496B3": Object { + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FEndpointC6202FF3": Object { "Value": Object { "Fn::Join": Array [ "", Array [ "https://", Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FF396C351", + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4F12449C5F", }, ".execute-api.", Object { @@ -938,7 +938,7 @@ Object { }, "/", Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FDeploymentStageprod064E2683", + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FDeploymentStageprod6A86C016", }, "/", ], @@ -957,188 +957,6 @@ Object { }, }, "Resources": Object { - "HelloWorldFunctionApiEventSourceA7A86A4FAccount79978FBC": Object { - "DependsOn": Array [ - "HelloWorldFunctionApiEventSourceA7A86A4FF396C351", - ], - "Properties": Object { - "CloudWatchRoleArn": Object { - "Fn::GetAtt": Array [ - "HelloWorldFunctionApiEventSourceA7A86A4FCloudWatchRole26206BB7", - "Arn", - ], - }, - }, - "Type": "AWS::ApiGateway::Account", - }, - "HelloWorldFunctionApiEventSourceA7A86A4FCloudWatchRole26206BB7": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "Service": Object { - "Fn::Join": Array [ - "", - Array [ - "apigateway.", - Object { - "Ref": "AWS::URLSuffix", - }, - ], - ], - }, - }, - }, - ], - "Version": "2012-10-17", - }, - "ManagedPolicyArns": Array [ - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", - ], - ], - }, - ], - }, - "Type": "AWS::IAM::Role", - }, - "HelloWorldFunctionApiEventSourceA7A86A4FDeployment1834FFEC33de358c7cd6f808076d7fb5561d9930": Object { - "DependsOn": Array [ - "HelloWorldFunctionApiEventSourceA7A86A4FhelloGET79ECD95F", - "HelloWorldFunctionApiEventSourceA7A86A4FhelloPOST0D6D6818", - "HelloWorldFunctionApiEventSourceA7A86A4FhelloF1478520", - ], - "Properties": Object { - "Description": "Automatically created by the RestApi construct", - "RestApiId": Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FF396C351", - }, - }, - "Type": "AWS::ApiGateway::Deployment", - }, - "HelloWorldFunctionApiEventSourceA7A86A4FDeploymentStageprod064E2683": Object { - "Properties": Object { - "DeploymentId": Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FDeployment1834FFEC33de358c7cd6f808076d7fb5561d9930", - }, - "RestApiId": Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FF396C351", - }, - "StageName": "prod", - }, - "Type": "AWS::ApiGateway::Stage", - }, - "HelloWorldFunctionApiEventSourceA7A86A4FF396C351": Object { - "Properties": Object { - "Name": "ApiEventSourceA7A86A4F", - }, - "Type": "AWS::ApiGateway::RestApi", - }, - "HelloWorldFunctionApiEventSourceA7A86A4FhelloF1478520": Object { - "Properties": Object { - "ParentId": Object { - "Fn::GetAtt": Array [ - "HelloWorldFunctionApiEventSourceA7A86A4FF396C351", - "RootResourceId", - ], - }, - "PathPart": "hello", - "RestApiId": Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FF396C351", - }, - }, - "Type": "AWS::ApiGateway::Resource", - }, - "HelloWorldFunctionApiEventSourceA7A86A4FhelloGET79ECD95F": Object { - "Properties": Object { - "AuthorizationType": "NONE", - "HttpMethod": "GET", - "Integration": Object { - "IntegrationHttpMethod": "POST", - "Type": "AWS_PROXY", - "Uri": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":apigateway:", - Object { - "Ref": "AWS::Region", - }, - ":lambda:path/2015-03-31/functions/", - Object { - "Fn::GetAtt": Array [ - "HelloWorldFunctionB2AB6E79", - "Arn", - ], - }, - "/invocations", - ], - ], - }, - }, - "ResourceId": Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FhelloF1478520", - }, - "RestApiId": Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FF396C351", - }, - }, - "Type": "AWS::ApiGateway::Method", - }, - "HelloWorldFunctionApiEventSourceA7A86A4FhelloPOST0D6D6818": Object { - "Properties": Object { - "AuthorizationType": "NONE", - "HttpMethod": "POST", - "Integration": Object { - "IntegrationHttpMethod": "POST", - "Type": "AWS_PROXY", - "Uri": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":apigateway:", - Object { - "Ref": "AWS::Region", - }, - ":lambda:path/2015-03-31/functions/", - Object { - "Fn::GetAtt": Array [ - "HelloWorldFunctionB2AB6E79", - "Arn", - ], - }, - "/invocations", - ], - ], - }, - }, - "ResourceId": Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FhelloF1478520", - }, - "RestApiId": Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FF396C351", - }, - }, - "Type": "AWS::ApiGateway::Method", - }, "HelloWorldFunctionApiPermissionGEThello0F001828": Object { "Properties": Object { "Action": "lambda:InvokeFunction", @@ -1167,11 +985,11 @@ Object { }, ":", Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FF396C351", + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4F12449C5F", }, "/", Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FDeploymentStageprod064E2683", + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FDeploymentStageprod6A86C016", }, "/GET/hello", ], @@ -1208,11 +1026,11 @@ Object { }, ":", Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FF396C351", + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4F12449C5F", }, "/", Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FDeploymentStageprod064E2683", + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FDeploymentStageprod6A86C016", }, "/POST/hello", ], @@ -1249,7 +1067,7 @@ Object { }, ":", Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FF396C351", + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4F12449C5F", }, "/test-invoke-stage/GET/hello", ], @@ -1286,7 +1104,7 @@ Object { }, ":", Object { - "Ref": "HelloWorldFunctionApiEventSourceA7A86A4FF396C351", + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4F12449C5F", }, "/test-invoke-stage/POST/hello", ], @@ -1505,6 +1323,188 @@ Object { }, "Type": "AWS::DynamoDB::Table", }, + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4F12449C5F": Object { + "Properties": Object { + "Name": "lambdaeventsHelloWorldFunctionAB27BB65:ApiEventSourceA7A86A4F", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FAccountF7734F1E": Object { + "DependsOn": Array [ + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4F12449C5F", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FCloudWatchRole495FDB9D", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FCloudWatchRole495FDB9D": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": Object { + "Fn::Join": Array [ + "", + Array [ + "apigateway.", + Object { + "Ref": "AWS::URLSuffix", + }, + ], + ], + }, + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FDeploymentFF3F4A1Aa119fa0e654d84489ef88606c21a73fb": Object { + "DependsOn": Array [ + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FhelloGET04FBC7F6", + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FhelloPOST53F177B1", + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FhelloC30B3CD9", + ], + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4F12449C5F", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FDeploymentStageprod6A86C016": Object { + "Properties": Object { + "DeploymentId": Object { + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FDeploymentFF3F4A1Aa119fa0e654d84489ef88606c21a73fb", + }, + "RestApiId": Object { + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4F12449C5F", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FhelloC30B3CD9": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4F12449C5F", + "RootResourceId", + ], + }, + "PathPart": "hello", + "RestApiId": Object { + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4F12449C5F", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FhelloGET04FBC7F6": Object { + "Properties": Object { + "AuthorizationType": "NONE", + "HttpMethod": "GET", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "HelloWorldFunctionB2AB6E79", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FhelloC30B3CD9", + }, + "RestApiId": Object { + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4F12449C5F", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FhelloPOST53F177B1": Object { + "Properties": Object { + "AuthorizationType": "NONE", + "HttpMethod": "POST", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "HelloWorldFunctionB2AB6E79", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FhelloC30B3CD9", + }, + "RestApiId": Object { + "Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4F12449C5F", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, }, } `; diff --git a/tools/awslint/bin/awslint.ts b/tools/awslint/bin/awslint.ts index 9533216736233..68dc273e85327 100644 --- a/tools/awslint/bin/awslint.ts +++ b/tools/awslint/bin/awslint.ts @@ -5,9 +5,17 @@ import fs = require('fs-extra'); import reflect = require('jsii-reflect'); import path = require('path'); import yargs = require('yargs'); -import { AggregateLinter, attributesLinter, cfnResourceLinter, constructLinter, DiagnosticLevel, importsLinter, moduleLinter, resourceLinter } from '../lib'; - -const linter = new AggregateLinter(moduleLinter, constructLinter, cfnResourceLinter, resourceLinter, importsLinter, attributesLinter); +import { AggregateLinter, apiLinter, attributesLinter, cfnResourceLinter, constructLinter, DiagnosticLevel, importsLinter, moduleLinter, resourceLinter } from '../lib'; + +const linter = new AggregateLinter( + moduleLinter, + constructLinter, + cfnResourceLinter, + resourceLinter, + apiLinter, + importsLinter, + attributesLinter +); let stackTrace = false; diff --git a/tools/awslint/lib/linter.ts b/tools/awslint/lib/linter.ts index 1480c03774dde..fa84010818b81 100644 --- a/tools/awslint/lib/linter.ts +++ b/tools/awslint/lib/linter.ts @@ -1,5 +1,6 @@ import reflect = require('jsii-reflect'); import { PrimitiveType } from 'jsii-spec'; +import util = require('util'); export interface LinterOptions { /** @@ -85,17 +86,16 @@ export class Linter extends LinterBase { ctxs = [ ctxs ]; } - const results = new Array(); + const diag = new Array(); for (const ctx of ctxs) { for (const rule of Object.values(this._rules)) { - const evaluation = new Evaluation(ctx, rule, options); + const evaluation = new Evaluation(ctx, rule, diag, options); rule.eval(evaluation); - results.push(...evaluation.diagnostics); } } - return results; + return diag; } } @@ -105,18 +105,26 @@ export class Linter extends LinterBase { export class Evaluation { public readonly ctx: T; public readonly options: LinterOptions; - public diagnostics = new Array(); + private readonly curr: ConcreteRule; + private readonly diagnostics: Diagnostic[]; - constructor(ctx: T, rule: ConcreteRule, options: LinterOptions) { + constructor(ctx: T, rule: ConcreteRule, diagnostics: Diagnostic[], options: LinterOptions) { this.ctx = ctx; this.options = options; this.curr = rule; + this.diagnostics = diagnostics; } public assert(condition: any, scope: string, extra?: string): condition is true { + + // deduplicate: skip if this specific assertion ("rule:scope") was already examined + if (this.diagnostics.find(d => d.rule.code === this.curr.code && d.scope === scope)) { + return condition; + } + const include = this.shouldEvaluate(this.curr.code, scope); - const message = this.curr.message + (extra || ''); + const message = util.format(this.curr.message, extra || ''); let level: DiagnosticLevel; if (!include) { diff --git a/tools/awslint/lib/rules/api.ts b/tools/awslint/lib/rules/api.ts new file mode 100644 index 0000000000000..26d7ddcb02808 --- /dev/null +++ b/tools/awslint/lib/rules/api.ts @@ -0,0 +1,146 @@ +import reflect = require('jsii-reflect'); +import { Linter } from '../linter'; +import { CfnResourceReflection } from './cfn-resource'; +import { ConstructReflection } from './construct'; +import { ResourceReflection } from './resource'; + +const EXCLUDE_ANNOTATION_REF_VIA_INTERFACE = '[disable-awslint:ref-via-interface]'; + +// lint all constructs that are not L1 resources +export const apiLinter = new Linter(a => ConstructReflection + .findAllConstructs(a) + .filter(c => !CfnResourceReflection.isCfnResource(c.classType))); + +apiLinter.add({ + code: 'ref-via-interface', + message: 'API should use interface and not the concrete class (%s). ' + + `If this is intentional, add "${EXCLUDE_ANNOTATION_REF_VIA_INTERFACE}" to element's jsdoc`, + eval: e => { + const cls = e.ctx.classType; + const visited = new Set(); + + assertClass(cls); + + function assertClass(type: reflect.ClassType): void { + if (visited.has(type.fqn)) { return; } + visited.add(type.fqn); + + for (const method of type.allMethods) { + assertMethod(method); + } + + if (type.initializer) { + assertMethod(type.initializer); + } + } + + function assertDataType(type: reflect.InterfaceType): void { + for (const property of type.allProperties) { + assertProperty(property); + } + } + + function assertInterface(type: reflect.InterfaceType): void { + if (visited.has(type.fqn)) { return; } + visited.add(type.fqn); + + if (type.datatype) { + assertDataType(type); + } + + for (const method of type.allMethods) { + assertMethod(method); + } + } + + function assertProperty(property: reflect.Property) { + + if (property.protected) { + return; + } + + const site = property.overrides ? property.overrides : property.parentType; + assertType(property.type, property.docs, `${site.fqn}.${property.name}`); + } + + function assertMethod(method: reflect.Callable) { + + if (method.protected) { + return; + } + + const site = method.overrides ? method.overrides : method.parentType; + const scope = `${site.fqn}.${method.name}`; + + let firstMethod: reflect.Callable | undefined = site.isClassType() || site.isInterfaceType() + ? site.allMethods.find(m => m.name === method.name) + : undefined; + + if (!firstMethod) { + firstMethod = method; + } + + for (const param of firstMethod.parameters) { + assertType(param.type, param.docs, `${scope}.${param.name}`); + } + + // note that we do not require that return values will use an interface + } + + function assertType(type: reflect.TypeReference, docs: reflect.Docs, scope: string): void { + if (type.primitive) { + return; + } + + if (type.void) { + return; + } + + if (type.arrayOfType) { + return assertType(type.arrayOfType, docs, scope); + } + + if (type.mapOfType) { + return assertType(type.mapOfType, docs, scope); + } + + if (type.unionOfTypes) { + for (const t of type.unionOfTypes) { + assertType(t, docs, scope); + } + return; + } + + // interfaces are okay + if (type.type && type.type.isInterfaceType()) { + return assertInterface(type.type); + } + + // enums are okay + if (type.type && type.type.isEnumType()) { + return; + } + + // classes are okay as long as they are not resource constructs + if (type.type && type.type.isClassType()) { + if (!ResourceReflection.isResourceClass(type.type)) { + return; + } + + if (type.type.fqn === ConstructReflection.CONSTRUCT_FQN) { + return; + } + + // allow exclusion of this rule + if (docs.summary.includes(EXCLUDE_ANNOTATION_REF_VIA_INTERFACE) || docs.remarks.includes(EXCLUDE_ANNOTATION_REF_VIA_INTERFACE)) { + return; + } + + e.assert(false, scope, type.type.fqn); + return; + } + + throw new Error(`invalid type reference: ${type.toString()}`); + } + } +}); \ No newline at end of file diff --git a/tools/awslint/lib/rules/cfn-resource.ts b/tools/awslint/lib/rules/cfn-resource.ts index a5b0655ff6a7f..c0270b1e7c94d 100644 --- a/tools/awslint/lib/rules/cfn-resource.ts +++ b/tools/awslint/lib/rules/cfn-resource.ts @@ -39,31 +39,35 @@ export class CfnResourceReflection { * Returns all CFN resource classes within an assembly. */ public static findAll(assembly: reflect.Assembly) { - return assembly.classes.filter(c => { - if (!c.system.includesAssembly(CORE_MODULE)) { - return false; - } - - // skip CfnResource itself - if (c.fqn === CFN_RESOURCE_BASE_CLASS_FQN) { - return false; - } - - if (!ConstructReflection.isConstructClass(c)) { - return false; - } - - const cfnResourceClass = c.system.findFqn(CFN_RESOURCE_BASE_CLASS_FQN); - if (!c.extends(cfnResourceClass)) { - return false; - } - - if (!c.name.startsWith('Cfn')) { - return false; - } - - return true; - }).map(c => new CfnResourceReflection(c)); + return assembly.classes + .filter(c => this.isCfnResource(c)) + .map(c => new CfnResourceReflection(c)); + } + + public static isCfnResource(c: reflect.ClassType) { + if (!c.system.includesAssembly(CORE_MODULE)) { + return false; + } + + // skip CfnResource itself + if (c.fqn === CFN_RESOURCE_BASE_CLASS_FQN) { + return false; + } + + if (!ConstructReflection.isConstructClass(c)) { + return false; + } + + const cfnResourceClass = c.system.findFqn(CFN_RESOURCE_BASE_CLASS_FQN); + if (!c.extends(cfnResourceClass)) { + return false; + } + + if (!c.name.startsWith('Cfn')) { + return false; + } + + return true; } public readonly classType: reflect.ClassType; diff --git a/tools/awslint/lib/rules/construct.ts b/tools/awslint/lib/rules/construct.ts index 500705432c1de..fbb624d3c2a97 100644 --- a/tools/awslint/lib/rules/construct.ts +++ b/tools/awslint/lib/rules/construct.ts @@ -2,14 +2,15 @@ import reflect = require('jsii-reflect'); import { Linter, MethodSignatureParameterExpectation } from '../linter'; import { CORE_MODULE } from './common'; -const CONSTRUCT_FQN = `${CORE_MODULE}.Construct`; -const CONSTRUCT_INTERFACE_FQN = `${CORE_MODULE}.IConstruct`; - export const constructLinter = new Linter(assembly => assembly.classes .filter(t => ConstructReflection.isConstructClass(t)) .map(construct => new ConstructReflection(construct))); export class ConstructReflection { + + public static readonly CONSTRUCT_FQN = `${CORE_MODULE}.Construct`; + public static readonly CONSTRUCT_INTERFACE_FQN = `${CORE_MODULE}.IConstruct`; + /** * Determines if a class is a construct. */ @@ -26,7 +27,7 @@ export class ConstructReflection { return false; } - return c.extends(c.system.findFqn(CONSTRUCT_FQN)); + return c.extends(c.system.findFqn(this.CONSTRUCT_FQN)); } public static findAllConstructs(assembly: reflect.Assembly) { @@ -49,7 +50,7 @@ export class ConstructReflection { constructor(public readonly classType: reflect.ClassType) { this.fqn = classType.fqn; this.sys = classType.system; - this.ROOT_CLASS = this.sys.findClass(CONSTRUCT_FQN); + this.ROOT_CLASS = this.sys.findClass(ConstructReflection.CONSTRUCT_FQN); this.interfaceFqn = `${classType.assembly.name}.I${classType.name}`; this.propsFqn = `${classType.assembly.name}.${classType.name}Props`; this.interfaceType = this.tryFindInterface(); @@ -104,7 +105,7 @@ constructLinter.add({ expectedParams.push({ name: 'scope', - type: CONSTRUCT_FQN + type: ConstructReflection.CONSTRUCT_FQN }); expectedParams.push({ @@ -180,7 +181,7 @@ constructLinter.add({ message: 'construct interface must extend core.IConstruct', eval: e => { if (!e.ctx.interfaceType) { return; } - const interfaceBase = e.ctx.sys.findInterface(CONSTRUCT_INTERFACE_FQN); + const interfaceBase = e.ctx.sys.findInterface(ConstructReflection.CONSTRUCT_INTERFACE_FQN); e.assert(e.ctx.interfaceType.extends(interfaceBase), e.ctx.interfaceType.fqn); } }); @@ -188,9 +189,10 @@ constructLinter.add({ constructLinter.add({ code: 'construct-base-is-private', message: 'prefer that the construct base class is private', + warning: true, eval: e => { if (!e.ctx.interfaceType) { return; } const baseFqn = `${e.ctx.classType.fqn}Base`; e.assert(!e.ctx.sys.tryFindFqn(baseFqn), baseFqn); } -}); \ No newline at end of file +}); diff --git a/tools/awslint/lib/rules/index.ts b/tools/awslint/lib/rules/index.ts index 3afad529f9eca..5edde09ca4515 100644 --- a/tools/awslint/lib/rules/index.ts +++ b/tools/awslint/lib/rules/index.ts @@ -4,3 +4,4 @@ export * from './resource'; export * from './imports'; export * from './cfn-resource'; export * from './attributes'; +export * from './api'; \ No newline at end of file diff --git a/tools/awslint/lib/rules/resource.ts b/tools/awslint/lib/rules/resource.ts index 9682e21af81aa..3161626e8275a 100644 --- a/tools/awslint/lib/rules/resource.ts +++ b/tools/awslint/lib/rules/resource.ts @@ -23,6 +23,15 @@ export enum AttributeSite { } export class ResourceReflection { + + /** + * @returns true if `classType` represents an AWS resource (i.e. extends `cdk.Resource`). + */ + public static isResourceClass(classType: reflect.ClassType) { + const baseResource = classType.system.findClass(RESOURCE_BASE_CLASS_FQN); + return classType.extends(baseResource) || getDocTag(classType, 'resource'); + } + /** * @returns all resource constructs (everything that extends `cdk.Resource`) */ @@ -31,11 +40,9 @@ export class ResourceReflection { return []; // not part of the dep stack } - const baseResource = assembly.system.findClass(RESOURCE_BASE_CLASS_FQN); - return ConstructReflection .findAllConstructs(assembly) - .filter(c => c.classType.extends(baseResource) || getDocTag(c.classType, 'resource')) + .filter(c => ResourceReflection.isResourceClass(c.classType)) .map(c => new ResourceReflection(c)); } @@ -71,6 +78,10 @@ export class ResourceReflection { const result = new Array(); for (const p of this.construct.classType.allProperties) { + if (p.protected) { + continue; // skip any protected properties + } + // an attribute property is a property which starts with the type name // (e.g. "bucketXxx") and/or has an @attribute doc tag. const tag = getDocTag(p, 'attribute');