From 31a904f72d2c099621bf5cff247fede1dcc984d1 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Mon, 6 May 2019 21:31:36 +0300 Subject: [PATCH] refactor: convert "import" to "from" methods (#2456) Implement and apply the following awslint rules: - `awslint:from-method`: resources should have at least one static "from" method - `awslint:from-signature`: enforce method signature - `awslint:from-attributes`: a `fromAttributes` static method can be used to import from more than a single attribute - `awslint:from-attributes-struct`: `fromFooAttributes` should accept a `FooAttributes` struct as input - `awslint:no-static-import`: forbids a static `import` (deprecation helper rule) - `awslint:attribute-tag`: all resource attributes should have an "@attribute" doc tag - `awslint:attribute-readonly`: all attributes must be readonly properties Many resources now have an additional `fromFooArn` or `fromFooName` for importing from the intrinsic attribute. Misc: - Deprecate `Token.unresolved` in favor of `Token.isToken` (more idiomatic). - Added eager resolution of `Fn.select` and `Fn.split` in case they receive concrete values - Refactoring of awslint (decouple "resource" from "cfn-resource"). - Added `iam.Grant.drop` which allows "dropping" a grant on the floor for imported resources NOTE: many of the new methods do not have inline documentation. We will address this in a subsequent pass that will be focused on docs. Fixes #2450 Fixes #2428 Fixes #2424 Fixes #2429 Fixes #2425 Fixes #2422 Fixes #2423 Fixes #89 BREAKING CHANGE: all `Foo.import` static methods are now `Foo.fromFooAttributes` * All `FooImportProps` structs are now called `FooAttributes` * `stepfunctions.StateMachine.export` has been removed. * `ses.ReceiptRule.name` is now `ses.ReceiptRule.receiptRuleName` * `ses.ReceiptRuleSet.name` is now `ses.ReceiptRuleSet.receiptRuleSetName` * `secretsmanager.AttachedSecret` is now called `secretsmanager.SecretTargetAttachment` to match service semantics * `ecr.Repository.export` has been removed * `s3.Bucket.bucketUrl` is now called `s3.Bucket.bucketWebsiteUrl` * `lambda.Version.functionVersion` is now called `lambda.Version.version` * `ec2.SecurityGroup.groupName` is now `ec2.SecurityGroup.securityGroupName` * `cognito.UserPoolClient.clientId` is now `cognito.UserPoolClient.userPoolClientId` * `apigateway.IRestApiResource` is now `apigateway.IResource` * `apigateway.IResource.resourcePath` is now `apigateway.IResource.path` * `apigateway.IResource.resourceApi` is now `apigateway.IResource.restApi` --- .../test/test.pipeline-deploy-stack-action.ts | 2 +- packages/@aws-cdk/assets/lib/asset.ts | 4 +- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 1 + .../aws-apigateway/lib/integrations/lambda.ts | 2 +- .../@aws-cdk/aws-apigateway/lib/method.ts | 20 +- .../@aws-cdk/aws-apigateway/lib/resource.ts | 52 +- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 51 +- packages/@aws-cdk/aws-apigateway/lib/stage.ts | 3 + .../@aws-cdk/aws-apigateway/lib/vpc-link.ts | 1 + packages/@aws-cdk/aws-apigateway/package.json | 4 +- .../aws-apigateway/test/test.resource.ts | 8 +- .../aws-apigateway/test/test.restapi.ts | 21 +- .../lib/scalable-target.ts | 1 + .../aws-autoscaling/lib/auto-scaling-group.ts | 222 +-- .../aws-autoscaling/lib/lifecycle-hook.ts | 1 + .../test/test.auto-scaling-group.ts | 6 +- .../aws-certificatemanager/lib/certificate.ts | 37 +- .../lib/dns-validated-certificate.ts | 4 +- .../test/test.certificate.ts | 2 +- packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts | 4 + .../@aws-cdk/aws-codebuild/lib/project.ts | 97 +- .../@aws-cdk/aws-codecommit/lib/repository.ts | 91 +- .../lib/lambda/deployment-config.ts | 2 + .../lib/lambda/deployment-group.ts | 26 +- .../lib/server/deployment-config.ts | 34 +- .../lib/server/deployment-group.ts | 35 +- packages/@aws-cdk/aws-codedeploy/package.json | 3 +- .../test/server/test.deployment-config.ts | 4 +- .../test/server/test.deployment-group.ts | 2 +- .../test.cloudformation-pipeline-actions.ts | 4 +- .../test/test.pipeline.ts | 13 + .../@aws-cdk/aws-codepipeline/lib/action.ts | 4 + .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 103 +- .../aws-cognito/lib/user-pool-client.ts | 22 +- .../@aws-cdk/aws-cognito/lib/user-pool.ts | 70 +- packages/@aws-cdk/aws-cognito/package.json | 6 + packages/@aws-cdk/aws-dynamodb/lib/table.ts | 25 +- .../@aws-cdk/aws-ec2/lib/security-group.ts | 54 +- packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts | 2 +- .../@aws-cdk/aws-ec2/test/test.connections.ts | 2 +- packages/@aws-cdk/aws-ecr/lib/repository.ts | 182 +-- .../@aws-cdk/aws-ecr/test/test.repository.ts | 42 +- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 12 +- .../aws-ecs/lib/base/task-definition.ts | 1 + packages/@aws-cdk/aws-ecs/lib/cluster.ts | 18 +- .../aws-ecs/lib/ec2/ec2-task-definition.ts | 2 + .../lib/fargate/fargate-task-definition.ts | 1 + .../load-balanced-fargate-service-applet.ts | 2 +- .../aws-ecs/test/test.container-definition.ts | 4 +- .../@aws-cdk/aws-ecs/test/test.ecs-cluster.ts | 2 +- packages/@aws-cdk/aws-ecs/test/test.l3s.ts | 2 +- packages/@aws-cdk/aws-eks/lib/cluster.ts | 22 +- .../@aws-cdk/aws-eks/test/test.cluster.ts | 2 +- .../lib/load-balancer.ts | 22 + .../lib/alb/application-listener.ts | 2 +- .../lib/alb/application-load-balancer.ts | 2 +- packages/@aws-cdk/aws-events/lib/rule-ref.ts | 10 +- packages/@aws-cdk/aws-events/lib/rule.ts | 38 +- .../@aws-cdk/aws-events/test/test.rule.ts | 5 +- packages/@aws-cdk/aws-glue/lib/database.ts | 75 +- packages/@aws-cdk/aws-glue/lib/table.ts | 54 +- .../@aws-cdk/aws-glue/test/test.database.ts | 2 +- packages/@aws-cdk/aws-glue/test/test.table.ts | 14 +- packages/@aws-cdk/aws-iam/lib/grant.ts | 15 + packages/@aws-cdk/aws-iam/lib/group.ts | 3 + packages/@aws-cdk/aws-iam/lib/lazy-role.ts | 4 +- packages/@aws-cdk/aws-iam/lib/policy.ts | 2 + packages/@aws-cdk/aws-iam/lib/role.ts | 154 ++- packages/@aws-cdk/aws-iam/lib/user.ts | 2 + packages/@aws-cdk/aws-iam/test/test.grant.ts | 17 + packages/@aws-cdk/aws-iam/test/test.role.ts | 2 +- packages/@aws-cdk/aws-kinesis/lib/stream.ts | 62 +- .../@aws-cdk/aws-kinesis/test/test.stream.ts | 4 +- packages/@aws-cdk/aws-lambda/lib/alias.ts | 10 +- .../@aws-cdk/aws-lambda/lib/function-base.ts | 10 +- packages/@aws-cdk/aws-lambda/lib/function.ts | 76 +- .../@aws-cdk/aws-lambda/lib/lambda-version.ts | 5 +- packages/@aws-cdk/aws-lambda/lib/layers.ts | 51 +- .../aws-lambda/lib/singleton-lambda.ts | 6 +- packages/@aws-cdk/aws-lambda/package.json | 6 +- .../@aws-cdk/aws-lambda/test/test.alias.ts | 6 +- .../@aws-cdk/aws-lambda/test/test.lambda.ts | 6 +- .../aws-lambda/test/test.vpc-lambda.ts | 2 +- packages/@aws-cdk/aws-logs/lib/log-group.ts | 65 +- packages/@aws-cdk/aws-logs/lib/log-stream.ts | 52 +- .../@aws-cdk/aws-logs/test/test.loggroup.ts | 9 +- packages/@aws-cdk/aws-quickstarts/lib/rdgw.ts | 5 +- packages/@aws-cdk/aws-rds/lib/cluster.ts | 2 +- .../@aws-cdk/aws-rds/lib/database-secret.ts | 2 + .../aws-rds/lib/rotation-single-user.ts | 14 +- .../@aws-cdk/aws-rds/test/test.cluster.ts | 4 +- .../aws-route53/lib/hosted-zone-provider.ts | 6 +- .../aws-route53/lib/hosted-zone-ref.ts | 8 +- .../@aws-cdk/aws-route53/lib/hosted-zone.ts | 105 +- packages/@aws-cdk/aws-route53/package.json | 6 + .../test/test.hosted-zone-provider.ts | 6 +- .../@aws-cdk/aws-route53/test/test.route53.ts | 2 +- packages/@aws-cdk/aws-s3/lib/bucket.ts | 67 +- packages/@aws-cdk/aws-s3/lib/util.ts | 6 +- .../aws-s3/test/demo.import-export.ts | 6 +- .../aws-s3/test/integ.bucket.domain-name.ts | 2 +- .../test/integ.bucket.url.lit.expected.json | 21 +- .../aws-s3/test/integ.bucket.url.lit.ts | 2 +- packages/@aws-cdk/aws-s3/test/test.bucket.ts | 1220 +++++++++-------- .../aws-s3/test/test.notifications.ts | 4 +- .../@aws-cdk/aws-secretsmanager/lib/secret.ts | 66 +- .../@aws-cdk/aws-secretsmanager/package.json | 6 + .../test/example.app-with-secret.lit.ts | 2 +- .../aws-secretsmanager/test/test.secret.ts | 4 +- .../lib/cname-instance.ts | 1 + .../lib/http-namespace.ts | 23 +- .../aws-servicediscovery/lib/ip-instance.ts | 2 + .../lib/non-ip-instance.ts | 2 + .../lib/private-dns-namespace.ts | 21 +- .../lib/public-dns-namespace.ts | 20 +- .../aws-servicediscovery/lib/service.ts | 36 +- .../aws-servicediscovery/package.json | 21 +- .../@aws-cdk/aws-ses/lib/receipt-filter.ts | 2 +- .../@aws-cdk/aws-ses/lib/receipt-rule-set.ts | 51 +- packages/@aws-cdk/aws-ses/lib/receipt-rule.ts | 55 +- .../aws-ses/test/test.receipt-rule-set.ts | 4 +- .../aws-ses/test/test.receipt-rule.ts | 5 +- packages/@aws-cdk/aws-sns/lib/topic-base.ts | 12 +- packages/@aws-cdk/aws-sns/lib/topic.ts | 21 +- packages/@aws-cdk/aws-sns/test/test.sns.ts | 2 +- packages/@aws-cdk/aws-sqs/lib/queue-base.ts | 11 +- packages/@aws-cdk/aws-sqs/lib/queue.ts | 67 +- packages/@aws-cdk/aws-sqs/test/test.sqs.ts | 4 +- packages/@aws-cdk/aws-ssm/lib/parameter.ts | 87 +- packages/@aws-cdk/aws-ssm/package.json | 4 +- .../@aws-cdk/aws-ssm/test/test.parameter.ts | 49 + .../aws-stepfunctions/lib/activity.ts | 7 + .../aws-stepfunctions/lib/state-machine.ts | 48 +- packages/@aws-cdk/cdk/lib/fn.ts | 15 +- packages/@aws-cdk/cdk/lib/token.ts | 13 +- packages/@aws-cdk/cdk/test/test.fn.ts | 12 +- tools/awslint/bin/awslint.ts | 23 +- tools/awslint/lib/linter.ts | 61 +- tools/awslint/lib/rules/attributes.ts | 37 + tools/awslint/lib/rules/cfn-resource.ts | 115 ++ tools/awslint/lib/rules/construct.ts | 5 +- tools/awslint/lib/rules/imports.ts | 113 ++ tools/awslint/lib/rules/index.ts | 5 +- tools/awslint/lib/rules/module.ts | 4 +- tools/awslint/lib/rules/resource.ts | 235 ++-- tools/awslint/lib/rules/util.ts | 41 + tools/awslint/package.json | 1 + tslint.yaml | 6 +- 148 files changed, 2807 insertions(+), 2130 deletions(-) create mode 100644 packages/@aws-cdk/aws-iam/test/test.grant.ts create mode 100644 tools/awslint/lib/rules/attributes.ts create mode 100644 tools/awslint/lib/rules/cfn-resource.ts create mode 100644 tools/awslint/lib/rules/imports.ts create mode 100644 tools/awslint/lib/rules/util.ts diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts index eeee3d697538d..42b313a0da85d 100644 --- a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts @@ -327,7 +327,7 @@ function createSelfUpdatingStack(pipelineStack: cdk.Stack): SelfUpdatingPipeline }); // simple source - const bucket = s3.Bucket.import( pipeline, 'PatternBucket', { bucketArn: 'arn:aws:s3:::totally-fake-bucket' }); + const bucket = s3.Bucket.fromBucketArn( pipeline, 'PatternBucket', 'arn:aws:s3:::totally-fake-bucket'); const sourceOutput = new codepipeline.Artifact('SourceOutput'); const sourceAction = new cpactions.S3SourceAction({ actionName: 'S3Source', diff --git a/packages/@aws-cdk/assets/lib/asset.ts b/packages/@aws-cdk/assets/lib/asset.ts index df3ddd36c5dad..5132ca86d47df 100644 --- a/packages/@aws-cdk/assets/lib/asset.ts +++ b/packages/@aws-cdk/assets/lib/asset.ts @@ -122,9 +122,7 @@ export class Asset extends cdk.Construct { const s3Filename = cdk.Fn.select(1, cdk.Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.stringValue)).toString(); this.s3ObjectKey = `${this.s3Prefix}${s3Filename}`; - this.bucket = s3.Bucket.import(this, 'AssetBucket', { - bucketName: this.s3BucketName - }); + this.bucket = s3.Bucket.fromBucketName(this, 'AssetBucket', this.s3BucketName); // form the s3 URL of the object key this.s3Url = this.bucket.urlForObject(this.s3ObjectKey); diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 60a91b630cb3f..65bc57d2dc6e5 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -55,6 +55,7 @@ export interface DeploymentProps { * automatically for the `restApi.latestDeployment` deployment. */ export class Deployment extends Resource { + /** @attribute */ public readonly deploymentId: string; public readonly api: IRestApi; diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts index 26c2560c12d1e..235b147ed3503 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts @@ -55,7 +55,7 @@ export class LambdaIntegration extends AwsIntegration { super.bind(method); const principal = new iam.ServicePrincipal('apigateway.amazonaws.com'); - const desc = `${method.httpMethod}.${method.resource.resourcePath.replace(/\//g, '.')}`; + const desc = `${method.httpMethod}.${method.resource.path.replace(/\//g, '.')}`; this.handler.addPermission(`ApiPermission.${desc}`, { principal, diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index b724e88d733c9..c0772c38b6413 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -3,7 +3,7 @@ import { CfnMethod, CfnMethodProps } from './apigateway.generated'; import { ConnectionType, Integration } from './integration'; import { MockIntegration } from './integrations/mock'; import { MethodResponse } from './methodresponse'; -import { IRestApiResource } from './resource'; +import { IResource } from './resource'; import { RestApi } from './restapi'; import { validateHttpMethod } from './util'; @@ -66,7 +66,7 @@ export interface MethodProps { * The resource this method is associated with. For root resource methods, * specify the `RestApi` object. */ - readonly resource: IRestApiResource; + readonly resource: IResource; /** * The HTTP method ("GET", "POST", "PUT", ...) that clients use to call this method. @@ -85,16 +85,18 @@ export interface MethodProps { } export class Method extends Resource { + /** @attribute */ public readonly methodId: string; + public readonly httpMethod: string; - public readonly resource: IRestApiResource; + public readonly resource: IResource; public readonly restApi: RestApi; constructor(scope: Construct, id: string, props: MethodProps) { super(scope, id); this.resource = props.resource; - this.restApi = props.resource.resourceApi; + this.restApi = props.resource.restApi; this.httpMethod = props.httpMethod.toUpperCase(); validateHttpMethod(this.httpMethod); @@ -120,9 +122,9 @@ export class Method extends Resource { this.methodId = resource.ref; - props.resource.resourceApi._attachMethod(this); + props.resource.restApi._attachMethod(this); - const deployment = props.resource.resourceApi.latestDeployment; + const deployment = props.resource.restApi.latestDeployment; if (deployment) { deployment.node.addDependency(resource); deployment.addToLogicalId({ method: methodProps }); @@ -136,6 +138,8 @@ export class Method extends Resource { * * NOTE: {stage} will refer to the `restApi.deploymentStage`, which will * automatically set if auto-deploy is enabled. + * + * @attribute */ public get methodArn(): string { if (!this.restApi.deploymentStage) { @@ -145,7 +149,7 @@ export class Method extends Resource { } const stage = this.restApi.deploymentStage.stageName.toString(); - return this.restApi.executeApiArn(this.httpMethod, this.resource.resourcePath, stage); + return this.restApi.executeApiArn(this.httpMethod, this.resource.path, stage); } /** @@ -153,7 +157,7 @@ export class Method extends Resource { * This stage is used by the AWS Console UI when testing the method. */ public get testMethodArn(): string { - return this.restApi.executeApiArn(this.httpMethod, this.resource.resourcePath, 'test-invoke-stage'); + return this.restApi.executeApiArn(this.httpMethod, this.resource.path, 'test-invoke-stage'); } private renderIntegration(integration?: Integration): CfnMethod.IntegrationProperty { diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index afcf3faf7c16f..f93edcf74ab89 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -1,14 +1,14 @@ -import { Construct, IResource, Resource as ResourceConstruct } from '@aws-cdk/cdk'; +import { Construct, IResource as IResourceBase, Resource as ResourceConstruct } from '@aws-cdk/cdk'; import { CfnResource, CfnResourceProps } from './apigateway.generated'; import { Integration } from './integration'; import { Method, MethodOptions } from './method'; import { RestApi } from './restapi'; -export interface IRestApiResource extends IResource { +export interface IResource extends IResourceBase { /** * The parent of this resource or undefined for the root resource. */ - readonly parentResource?: IRestApiResource; + readonly parentResource?: IResource; /** * The rest API that this resource is part of. @@ -18,17 +18,18 @@ export interface IRestApiResource extends IResource { * hash to determine the ID of the deployment. This allows us to automatically update * the deployment when the model of the REST API changes. */ - readonly resourceApi: RestApi; + readonly restApi: RestApi; /** * The ID of the resource. + * @attribute */ readonly resourceId: string; /** * The full path of this resuorce. */ - readonly resourcePath: string; + readonly path: string; /** * An integration to use as a default for all methods created within this @@ -67,7 +68,7 @@ export interface IRestApiResource extends IResource { * @param pathPart The path part of the child resource * @returns the child resource or undefined if not found */ - getResource(pathPart: string): IRestApiResource | undefined; + getResource(pathPart: string): IResource | undefined; /** * Adds a greedy proxy resource ("{proxy+}") and an ANY method to this route. @@ -105,7 +106,7 @@ export interface ResourceProps extends ResourceOptions { * The parent resource of this resource. You can either pass another * `Resource` object or a `RestApi` object here. */ - readonly parent: IRestApiResource; + readonly parent: IResource; /** * A path name for the resource. @@ -113,11 +114,11 @@ export interface ResourceProps extends ResourceOptions { readonly pathPart: string; } -export abstract class ResourceBase extends ResourceConstruct implements IRestApiResource { - public abstract readonly parentResource?: IRestApiResource; - public abstract readonly resourceApi: RestApi; +export abstract class ResourceBase extends ResourceConstruct implements IResource { + public abstract readonly parentResource?: IResource; + public abstract readonly restApi: RestApi; public abstract readonly resourceId: string; - public abstract readonly resourcePath: string; + public abstract readonly path: string; public abstract readonly defaultIntegration?: Integration; public abstract readonly defaultMethodOptions?: MethodOptions; @@ -139,7 +140,7 @@ export abstract class ResourceBase extends ResourceConstruct implements IRestApi return new ProxyResource(this, '{proxy+}', { parent: this, ...options }); } - public getResource(pathPart: string): IRestApiResource | undefined { + public getResource(pathPart: string): IResource | undefined { return this.children[pathPart]; } @@ -153,8 +154,8 @@ export abstract class ResourceBase extends ResourceConstruct implements IRestApi } if (path.startsWith('/')) { - if (this.resourcePath !== '/') { - throw new Error(`Path may start with "/" only for the resource, but we are at: ${this.resourcePath}`); + if (this.path !== '/') { + throw new Error(`Path may start with "/" only for the resource, but we are at: ${this.path}`); } // trim trailing "/" @@ -177,10 +178,11 @@ export abstract class ResourceBase extends ResourceConstruct implements IRestApi } export class Resource extends ResourceBase { - public readonly parentResource?: IRestApiResource; - public readonly resourceApi: RestApi; + public readonly parentResource?: IResource; + public readonly restApi: RestApi; public readonly resourceId: string; - public readonly resourcePath: string; + public readonly path: string; + public readonly defaultIntegration?: Integration; public readonly defaultMethodOptions?: MethodOptions; @@ -196,21 +198,21 @@ export class Resource extends ResourceBase { } const resourceProps: CfnResourceProps = { - restApiId: props.parent.resourceApi.restApiId, + restApiId: props.parent.restApi.restApiId, parentId: props.parent.resourceId, pathPart: props.pathPart }; const resource = new CfnResource(this, 'Resource', resourceProps); this.resourceId = resource.resourceId; - this.resourceApi = props.parent.resourceApi; + this.restApi = props.parent.restApi; // render resource path (special case for root) - this.resourcePath = props.parent.resourcePath; - if (!this.resourcePath.endsWith('/')) { this.resourcePath += '/'; } - this.resourcePath += props.pathPart; + this.path = props.parent.path; + if (!this.path.endsWith('/')) { this.path += '/'; } + this.path += props.pathPart; - const deployment = props.parent.resourceApi.latestDeployment; + const deployment = props.parent.restApi.latestDeployment; if (deployment) { deployment.node.addDependency(resource); deployment.addToLogicalId({ resource: resourceProps }); @@ -231,7 +233,7 @@ export interface ProxyResourceProps extends ResourceOptions { * The parent resource of this resource. You can either pass another * `Resource` object or a `RestApi` object here. */ - readonly parent: IRestApiResource; + readonly parent: IResource; /** * Adds an "ANY" method to this resource. If set to `false`, you will have to explicitly @@ -270,7 +272,7 @@ export class ProxyResource extends Resource { public addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method { // In case this proxy is mounted under the root, also add this method to // the root so that empty paths are proxied as well. - if (this.parentResource && this.parentResource.resourcePath === '/') { + if (this.parentResource && this.parentResource.path === '/') { this.parentResource.addMethod(httpMethod); } return super.addMethod(httpMethod, integration, options); diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index e62cca6120b57..7c4afb3c96999 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -1,13 +1,13 @@ import iam = require('@aws-cdk/aws-iam'); -import { CfnOutput, Construct, IResource, Resource } from '@aws-cdk/cdk'; +import { CfnOutput, Construct, IResource as IResourceBase, Resource } from '@aws-cdk/cdk'; import { CfnAccount, CfnRestApi } from './apigateway.generated'; import { Deployment } from './deployment'; import { Integration } from './integration'; import { Method, MethodOptions } from './method'; -import { IRestApiResource, ResourceBase, ResourceOptions } from './resource'; +import { IResource, ResourceBase, ResourceOptions } from './resource'; import { Stage, StageOptions } from './stage'; -export interface RestApiImportProps { +export interface RestApiAttributes { /** * The REST API ID of an existing REST API resource. */ @@ -19,22 +19,18 @@ export interface RestApiImportProps { readonly restApiRootResourceId?: string; } -export interface IRestApi extends IResource { +export interface IRestApi extends IResourceBase { /** * The ID of this API Gateway RestApi. + * @attribute */ readonly restApiId: string; - /** - * The resource ID of the root resource. - */ - readonly restApiRootResourceId: string; - /** * Exports a REST API resource from this stack. * @returns REST API props that can be imported to another stack. */ - export(): RestApiImportProps; + export(): RestApiAttributes; } export interface RestApiProps extends ResourceOptions { @@ -165,20 +161,11 @@ export interface RestApiProps extends ResourceOptions { * public endpoint. */ export class RestApi extends Resource implements IRestApi { - /** - * Imports an existing REST API resource. - * @param scope Parent construct - * @param id Construct ID - * @param props Imported rest API properties - */ - public static import(scope: Construct, id: string, props: RestApiImportProps): IRestApi { - class Import extends Construct implements IRestApi { - public restApiId = props.restApiId; - public get restApiRootResourceId() { - if (!props.restApiRootResourceId) { throw new Error(`Imported REST API does not have "restApiRootResourceId"`); } - return props.restApiRootResourceId; - } - public export() { return props; } + + public static fromRestApiId(scope: Construct, id: string, restApiId: string): IRestApi { + class Import extends Resource implements IRestApi { + public readonly restApiId = restApiId; + public export(): RestApiAttributes { return { restApiId }; } } return new Import(scope, id); @@ -191,6 +178,8 @@ export class RestApi extends Resource implements IRestApi { /** * The resource ID of the root resource. + * + * @attribute */ public readonly restApiRootResourceId: string; @@ -216,7 +205,7 @@ export class RestApi extends Resource implements IRestApi { * api.root.addResource('friends').addMethod('GET', getFriendsHandler); // "GET /friends" * */ - public readonly root: IRestApiResource; + public readonly root: IResource; private readonly methods = new Array(); @@ -252,7 +241,7 @@ export class RestApi extends Resource implements IRestApi { * Exports a REST API resource from this stack. * @returns REST API props that can be imported to another stack. */ - public export(): RestApiImportProps { + public export(): RestApiAttributes { return { restApiId: new CfnOutput(this, 'RestApiId', { value: this.restApiId }).makeImportValue().toString() }; @@ -402,10 +391,10 @@ export enum EndpointType { } class RootResource extends ResourceBase { - public readonly parentResource?: IRestApiResource; - public readonly resourceApi: RestApi; + public readonly parentResource?: IResource; + public readonly restApi: RestApi; public readonly resourceId: string; - public readonly resourcePath: string; + public readonly path: string; public readonly defaultIntegration?: Integration | undefined; public readonly defaultMethodOptions?: MethodOptions | undefined; @@ -415,8 +404,8 @@ class RootResource extends ResourceBase { this.parentResource = undefined; this.defaultIntegration = props.defaultIntegration; this.defaultMethodOptions = props.defaultMethodOptions; - this.resourceApi = api; + this.restApi = api; this.resourceId = resourceId; - this.resourcePath = '/'; + this.path = '/'; } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/stage.ts b/packages/@aws-cdk/aws-apigateway/lib/stage.ts index 2505d459c9dd1..c6cc7e3ac5bf0 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stage.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stage.ts @@ -133,6 +133,9 @@ export interface MethodDeploymentOptions { } export class Stage extends Resource { + /** + * @attribute + */ public readonly stageName: string; private readonly restApi: IRestApi; diff --git a/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts b/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts index 896558c729dcb..00a5fd3ded451 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts @@ -32,6 +32,7 @@ export interface VpcLinkProps { export class VpcLink extends Resource { /** * Physical ID of the VpcLink resource + * @attribute */ public readonly vpcLinkId: string; diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index fe729d03a4c23..8e31614317dcd 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -86,7 +86,9 @@ }, "awslint": { "exclude": [ - "resource-attribute:@aws-cdk/aws-apigateway.IRestApi.restApiRootResourceId" + "resource-attribute:@aws-cdk/aws-apigateway.IRestApi.restApiRootResourceId", + "from-method:@aws-cdk/aws-apigateway.Resource", + "construct-base-is-private:@aws-cdk/aws-apigateway.ResourceBase" ] } } diff --git a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts index b8b9312865725..627998d524f3c 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts @@ -257,7 +257,7 @@ export = { const resource = api.root.resourceForPath('/boom/trach'); // THEN - test.deepEqual(resource.resourcePath, '/boom/trach'); + test.deepEqual(resource.path, '/boom/trach'); test.done(); }, @@ -273,14 +273,14 @@ export = { // THEN const parent = api.root.getResource('boom'); test.ok(parent); - test.deepEqual(parent!.resourcePath, '/boom'); + test.deepEqual(parent!.path, '/boom'); test.same(trach.parentResource, parent); - test.deepEqual(trach.parentResource!.resourcePath, '/boom'); + test.deepEqual(trach.parentResource!.path, '/boom'); const bam2 = api.root.resourceForPath('/boom/bam'); test.same(bam1, bam2); - test.deepEqual(bam1.parentResource!.resourcePath, '/boom'); + test.deepEqual(bam1.parentResource!.path, '/boom'); test.done(); } diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index 50d9286cc6c21..858d7e31816ac 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -280,12 +280,12 @@ export = { const r2 = api.root.addResource('r2'); // THEN - test.deepEqual(api.root.resourcePath, '/'); - test.deepEqual(r1.resourcePath, '/r1'); - test.deepEqual(r11.resourcePath, '/r1/r1_1'); - test.deepEqual(r12.resourcePath, '/r1/r1_2'); - test.deepEqual(r121.resourcePath, '/r1/r1_2/r1_2_1'); - test.deepEqual(r2.resourcePath, '/r2'); + test.deepEqual(api.root.path, '/'); + test.deepEqual(r1.path, '/r1'); + test.deepEqual(r11.path, '/r1/r1_1'); + test.deepEqual(r12.path, '/r1/r1_2'); + test.deepEqual(r121.path, '/r1/r1_2/r1_2_1'); + test.deepEqual(r2.path, '/r2'); test.done(); }, @@ -333,10 +333,7 @@ export = { const stack = new cdk.Stack(); // WHEN - const imported = apigateway.RestApi.import(stack, 'imported-api', { - restApiId: 'api-rxt4498f' - }); - + const imported = apigateway.RestApi.fromRestApiId(stack, 'imported-api', 'api-rxt4498f'); const api = new apigateway.RestApi(stack, 'MyRestApi'); api.root.addMethod('GET'); @@ -493,9 +490,7 @@ export = { '"cloneFrom" can be used to clone an existing API'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const cloneFrom = apigateway.RestApi.import(stack, 'RestApi', { - restApiId: 'foobar' - }); + const cloneFrom = apigateway.RestApi.fromRestApiId(stack, 'RestApi', 'foobar'); // WHEN const api = new apigateway.RestApi(stack, 'api', { diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts index 17b9ba9b1749d..12cb04bde5d92 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts @@ -66,6 +66,7 @@ export class ScalableTarget extends Resource { * ID of the Scalable Target * * @example service/ecsStack-MyECSCluster-AB12CDE3F4GH/ecsStack-MyECSService-AB12CDE3F4GH|ecs:service:DesiredCount|ecs + * @attribute */ public readonly scalableTargetId: string; diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 45bffb959d9c5..8e88c3626c5ac 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -176,6 +176,111 @@ export interface AutoScalingGroupProps extends CommonAutoScalingGroupProps { readonly role?: iam.IRole; } +abstract class AutoScalingGroupBase extends Resource implements IAutoScalingGroup { + + public abstract autoScalingGroupName: string; + protected albTargetGroup?: elbv2.ApplicationTargetGroup; + + /** + * Send a message to either an SQS queue or SNS topic when instances launch or terminate + */ + public onLifecycleTransition(id: string, props: BasicLifecycleHookProps): LifecycleHook { + return new LifecycleHook(this, `LifecycleHook${id}`, { + autoScalingGroup: this, + ...props + }); + } + + /** + * Scale out or in based on time + */ + public scaleOnSchedule(id: string, props: BasicScheduledActionProps): ScheduledAction { + return new ScheduledAction(this, `ScheduledAction${id}`, { + autoScalingGroup: this, + ...props, + }); + } + + /** + * Scale out or in to achieve a target CPU utilization + */ + public scaleOnCpuUtilization(id: string, props: CpuUtilizationScalingProps): TargetTrackingScalingPolicy { + return new TargetTrackingScalingPolicy(this, `ScalingPolicy${id}`, { + autoScalingGroup: this, + predefinedMetric: PredefinedMetric.ASGAverageCPUUtilization, + targetValue: props.targetUtilizationPercent, + ...props + }); + } + + /** + * Scale out or in to achieve a target network ingress rate + */ + public scaleOnIncomingBytes(id: string, props: NetworkUtilizationScalingProps): TargetTrackingScalingPolicy { + return new TargetTrackingScalingPolicy(this, `ScalingPolicy${id}`, { + autoScalingGroup: this, + predefinedMetric: PredefinedMetric.ASGAverageNetworkIn, + targetValue: props.targetBytesPerSecond, + ...props + }); + } + + /** + * Scale out or in to achieve a target network egress rate + */ + public scaleOnOutgoingBytes(id: string, props: NetworkUtilizationScalingProps): TargetTrackingScalingPolicy { + return new TargetTrackingScalingPolicy(this, `ScalingPolicy${id}`, { + autoScalingGroup: this, + predefinedMetric: PredefinedMetric.ASGAverageNetworkOut, + targetValue: props.targetBytesPerSecond, + ...props + }); + } + + /** + * Scale out or in to achieve a target request handling rate + * + * The AutoScalingGroup must have been attached to an Application Load Balancer + * in order to be able to call this. + */ + public scaleOnRequestCount(id: string, props: RequestCountScalingProps): TargetTrackingScalingPolicy { + if (this.albTargetGroup === undefined) { + throw new Error('Attach the AutoScalingGroup to an Application Load Balancer before calling scaleOnRequestCount()'); + } + + const resourceLabel = `${this.albTargetGroup.firstLoadBalancerFullName}/${this.albTargetGroup.targetGroupFullName}`; + + const policy = new TargetTrackingScalingPolicy(this, `ScalingPolicy${id}`, { + autoScalingGroup: this, + predefinedMetric: PredefinedMetric.ALBRequestCountPerTarget, + targetValue: props.targetRequestsPerSecond, + resourceLabel, + ...props + }); + + policy.node.addDependency(this.albTargetGroup.loadBalancerAttached); + return policy; + } + + /** + * Scale out or in in order to keep a metric around a target value + */ + public scaleToTrackMetric(id: string, props: MetricTargetTrackingProps): TargetTrackingScalingPolicy { + return new TargetTrackingScalingPolicy(this, `ScalingPolicy${id}`, { + autoScalingGroup: this, + customMetric: props.metric, + ...props + }); + } + + /** + * Scale out or in, in response to a metric + */ + public scaleOnMetric(id: string, props: BasicStepScalingPolicyProps): StepScalingPolicy { + return new StepScalingPolicy(this, id, { ...props, autoScalingGroup: this }); + } +} + /** * A Fleet represents a managed set of EC2 instances * @@ -187,8 +292,20 @@ export interface AutoScalingGroupProps extends CommonAutoScalingGroupProps { * * The ASG spans all availability zones. */ -export class AutoScalingGroup extends Resource implements IAutoScalingGroup, elb.ILoadBalancerTarget, ec2.IConnectable, - elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget { +export class AutoScalingGroup extends AutoScalingGroupBase implements + elb.ILoadBalancerTarget, + ec2.IConnectable, + elbv2.IApplicationLoadBalancerTarget, + elbv2.INetworkLoadBalancerTarget { + + public static fromAutoScalingGroupName(scope: Construct, id: string, autoScalingGroupName: string): IAutoScalingGroup { + class Import extends AutoScalingGroupBase { + public autoScalingGroupName = autoScalingGroupName; + } + + return new Import(scope, id); + } + /** * The type of OS instances of this fleet are running. */ @@ -215,7 +332,6 @@ export class AutoScalingGroup extends Resource implements IAutoScalingGroup, elb private readonly securityGroups: ec2.ISecurityGroup[] = []; private readonly loadBalancerNames: string[] = []; private readonly targetGroupArns: string[] = []; - private albTargetGroup?: elbv2.ApplicationTargetGroup; constructor(scope: Construct, id: string, props: AutoScalingGroupProps) { super(scope, id); @@ -345,95 +461,6 @@ export class AutoScalingGroup extends Resource implements IAutoScalingGroup, elb scriptLines.forEach(scriptLine => this.userDataLines.push(scriptLine)); } - /** - * Scale out or in based on time - */ - public scaleOnSchedule(id: string, props: BasicScheduledActionProps): ScheduledAction { - return new ScheduledAction(this, `ScheduledAction${id}`, { - autoScalingGroup: this, - ...props, - }); - } - - /** - * Scale out or in to achieve a target CPU utilization - */ - public scaleOnCpuUtilization(id: string, props: CpuUtilizationScalingProps): TargetTrackingScalingPolicy { - return new TargetTrackingScalingPolicy(this, `ScalingPolicy${id}`, { - autoScalingGroup: this, - predefinedMetric: PredefinedMetric.ASGAverageCPUUtilization, - targetValue: props.targetUtilizationPercent, - ...props - }); - } - - /** - * Scale out or in to achieve a target network ingress rate - */ - public scaleOnIncomingBytes(id: string, props: NetworkUtilizationScalingProps): TargetTrackingScalingPolicy { - return new TargetTrackingScalingPolicy(this, `ScalingPolicy${id}`, { - autoScalingGroup: this, - predefinedMetric: PredefinedMetric.ASGAverageNetworkIn, - targetValue: props.targetBytesPerSecond, - ...props - }); - } - - /** - * Scale out or in to achieve a target network egress rate - */ - public scaleOnOutgoingBytes(id: string, props: NetworkUtilizationScalingProps): TargetTrackingScalingPolicy { - return new TargetTrackingScalingPolicy(this, `ScalingPolicy${id}`, { - autoScalingGroup: this, - predefinedMetric: PredefinedMetric.ASGAverageNetworkOut, - targetValue: props.targetBytesPerSecond, - ...props - }); - } - - /** - * Scale out or in to achieve a target request handling rate - * - * The AutoScalingGroup must have been attached to an Application Load Balancer - * in order to be able to call this. - */ - public scaleOnRequestCount(id: string, props: RequestCountScalingProps): TargetTrackingScalingPolicy { - if (this.albTargetGroup === undefined) { - throw new Error('Attach the AutoScalingGroup to an Application Load Balancer before calling scaleOnRequestCount()'); - } - - const resourceLabel = `${this.albTargetGroup.firstLoadBalancerFullName}/${this.albTargetGroup.targetGroupFullName}`; - - const policy = new TargetTrackingScalingPolicy(this, `ScalingPolicy${id}`, { - autoScalingGroup: this, - predefinedMetric: PredefinedMetric.ALBRequestCountPerTarget, - targetValue: props.targetRequestsPerSecond, - resourceLabel, - ...props - }); - - policy.node.addDependency(this.albTargetGroup.loadBalancerAttached); - return policy; - } - - /** - * Scale out or in in order to keep a metric around a target value - */ - public scaleToTrackMetric(id: string, props: MetricTargetTrackingProps): TargetTrackingScalingPolicy { - return new TargetTrackingScalingPolicy(this, `ScalingPolicy${id}`, { - autoScalingGroup: this, - customMetric: props.metric, - ...props - }); - } - - /** - * Scale out or in, in response to a metric - */ - public scaleOnMetric(id: string, props: BasicStepScalingPolicyProps): StepScalingPolicy { - return new StepScalingPolicy(this, id, { ...props, autoScalingGroup: this }); - } - /** * Adds a statement to the IAM role assumed by instances of this fleet. */ @@ -441,16 +468,6 @@ export class AutoScalingGroup extends Resource implements IAutoScalingGroup, elb this.role.addToPolicy(statement); } - /** - * Send a message to either an SQS queue or SNS topic when instances launch or terminate - */ - public onLifecycleTransition(id: string, props: BasicLifecycleHookProps): LifecycleHook { - return new LifecycleHook(this, `LifecycleHook${id}`, { - autoScalingGroup: this, - ...props - }); - } - /** * Apply CloudFormation update policies for the AutoScalingGroup */ @@ -668,6 +685,7 @@ function validatePercentage(x?: number): number | undefined { export interface IAutoScalingGroup extends IResource { /** * The name of the AutoScalingGroup + * @attribute */ readonly autoScalingGroupName: string; diff --git a/packages/@aws-cdk/aws-autoscaling/lib/lifecycle-hook.ts b/packages/@aws-cdk/aws-autoscaling/lib/lifecycle-hook.ts index e068a0375f865..aa94479d00314 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/lifecycle-hook.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/lifecycle-hook.ts @@ -72,6 +72,7 @@ export class LifecycleHook extends Resource implements api.ILifecycleHook { /** * The name of this lifecycle hook + * @attribute */ public readonly lifecycleHookName: string; diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts index f833fe4eec5b7..86885a37f1f60 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts @@ -504,7 +504,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); - const importedRole = iam.Role.import(stack, 'ImportedRole', { roleArn: 'arn:aws:iam::123456789012:role/HelloDude' }); + const importedRole = iam.Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::123456789012:role/HelloDude'); // WHEN const asg = new autoscaling.AutoScalingGroup(stack, 'MyASG', { @@ -534,9 +534,7 @@ function mockVpc(stack: cdk.Stack) { } function mockSecurityGroup(stack: cdk.Stack) { - return ec2.SecurityGroup.import(stack, 'MySG', { - securityGroupId: 'most-secure', - }); + return ec2.SecurityGroup.fromSecurityGroupId(stack, 'MySG', 'most-secure'); } function getTestStack(): cdk.Stack { diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts index 0ea1ba629cd74..9ca8aa1f301f4 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts @@ -5,19 +5,21 @@ import { apexDomain } from './util'; export interface ICertificate extends IResource { /** * The certificate's ARN + * + * @attribute */ readonly certificateArn: string; /** * Export this certificate from the stack */ - export(): CertificateImportProps; + export(): CertificateAttributes; } /** * Reference to an existing Certificate */ -export interface CertificateImportProps { +export interface CertificateAttributes { /** * The certificate's ARN */ @@ -70,11 +72,19 @@ export interface CertificateProps { * For every domain that you register. */ export class Certificate extends Resource implements ICertificate { + /** * Import a certificate */ - public static import(scope: Construct, id: string, props: CertificateImportProps): ICertificate { - return new ImportedCertificate(scope, id, props); + public static fromCertificateArn(scope: Construct, id: string, certificateArn: string): ICertificate { + class Import extends Resource implements ICertificate { + public certificateArn = certificateArn; + public export(): CertificateAttributes { + return { certificateArn }; + } + } + + return new Import(scope, id); } /** @@ -112,26 +122,9 @@ export class Certificate extends Resource implements ICertificate { /** * Export this certificate from the stack */ - public export(): CertificateImportProps { + public export(): CertificateAttributes { return { certificateArn: new CfnOutput(this, 'Arn', { value: this.certificateArn }).makeImportValue().toString() }; } } - -/** - * A Certificate that has been imported from another stack - */ -class ImportedCertificate extends Construct implements ICertificate { - public readonly certificateArn: string; - - constructor(scope: Construct, id: string, private readonly props: CertificateImportProps) { - super(scope, id); - - this.certificateArn = props.certificateArn; - } - - public export() { - return this.props; - } -} diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts index e303441480945..d4548ca669606 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts @@ -4,7 +4,7 @@ import lambda = require('@aws-cdk/aws-lambda'); import route53 = require('@aws-cdk/aws-route53'); import cdk = require('@aws-cdk/cdk'); import path = require('path'); -import { CertificateImportProps, CertificateProps, ICertificate } from './certificate'; +import { CertificateAttributes, CertificateProps, ICertificate } from './certificate'; export interface DnsValidatedCertificateProps extends CertificateProps { /** @@ -74,7 +74,7 @@ export class DnsValidatedCertificate extends cdk.Construct implements ICertifica /** * Export this certificate from the stack */ - public export(): CertificateImportProps { + public export(): CertificateAttributes { return { certificateArn: new cdk.CfnOutput(this, 'Arn', { value: this.certificateArn }).makeImportValue().toString() }; diff --git a/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts b/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts index b8237a6708cb8..153778195a6e6 100644 --- a/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/test/test.certificate.ts @@ -49,7 +49,7 @@ export = { domainName: 'hello.com', }).export(); - Certificate.import(stack, 'Imported', refProps); + Certificate.fromCertificateArn(stack, 'Imported', refProps.certificateArn); test.done(); } diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index 03160d23a5824..a09e2d2dde5d0 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -65,11 +65,15 @@ export enum TreatMissingData { export class Alarm extends Resource { /** * ARN of this alarm + * + * @attribute */ public readonly alarmArn: string; /** * Name of this alarm. + * + * @attribute */ public readonly alarmName: string; diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 2f6c7f016531e..17610638cd83e 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -17,10 +17,16 @@ const S3_BUCKET_ENV = 'SCRIPT_S3_BUCKET'; const S3_KEY_ENV = 'SCRIPT_S3_KEY'; export interface IProject extends IResource, iam.IGrantable { - /** The ARN of this Project. */ + /** + * The ARN of this Project. + * @attribute + */ readonly projectArn: string; - /** The human-visible name of this Project. */ + /** + * The human-visible name of this Project. + * @attribute + */ readonly projectName: string; /** The IAM service Role of this Project. Undefined for imported Projects. */ @@ -128,7 +134,7 @@ export interface IProject extends IResource, iam.IGrantable { /** * Export this Project. Allows referencing this Project in a different CDK Stack. */ - export(): ProjectImportProps; + export(): ProjectAttributes; } /** @@ -137,7 +143,7 @@ export interface IProject extends IResource, iam.IGrantable { * @see Project.import * @see Project.export */ -export interface ProjectImportProps { +export interface ProjectAttributes { /** * The human-readable name of the CodeBuild Project we're referencing. * The Project must be in the same account and region as the root Stack. @@ -167,7 +173,7 @@ abstract class ProjectBase extends Resource implements IProject { /** The IAM service Role of this Project. */ public abstract readonly role?: iam.IRole; - public abstract export(): ProjectImportProps; + public abstract export(): ProjectAttributes; /** * Defines a CloudWatch event rule triggered when the build project state @@ -346,30 +352,6 @@ abstract class ProjectBase extends Resource implements IProject { } } -class ImportedProject extends ProjectBase { - public readonly grantPrincipal: iam.IPrincipal; - public readonly projectArn: string; - public readonly projectName: string; - public readonly role?: iam.Role = undefined; - - constructor(scope: Construct, id: string, private readonly props: ProjectImportProps) { - super(scope, id); - - this.projectArn = this.node.stack.formatArn({ - service: 'codebuild', - resource: 'project', - resourceName: props.projectName, - }); - this.grantPrincipal = new iam.ImportedResourcePrincipal({ resource: this }); - - this.projectName = props.projectName; - } - - public export() { - return this.props; - } -} - export interface CommonProjectProps { /** * A description of the project. Use the description to identify the purpose @@ -533,6 +515,29 @@ export interface ProjectProps extends CommonProjectProps { * A representation of a CodeBuild Project. */ export class Project extends ProjectBase { + + public static fromProjectArn(scope: Construct, id: string, projectArn: string): IProject { + class Import extends ProjectBase { + public readonly grantPrincipal: iam.IPrincipal; + public readonly projectArn = projectArn; + public readonly projectName = scope.node.stack.parseArn(projectArn).resourceName!; + public readonly role?: iam.Role = undefined; + + constructor(s: Construct, i: string) { + super(s, i); + this.grantPrincipal = new iam.ImportedResourcePrincipal({ resource: this }); + } + + public export(): ProjectAttributes { + return { + projectName: this.projectName + }; + } + } + + return new Import(scope, id); + } + /** * Import a Project defined either outside the CDK, * or in a different CDK Stack @@ -545,11 +550,37 @@ export class Project extends ProjectBase { * * @param scope the parent Construct for this Construct * @param id the logical name of this Construct - * @param props the properties of the referenced Project + * @param projectName the name of the project to import * @returns a reference to the existing Project */ - public static import(scope: Construct, id: string, props: ProjectImportProps): IProject { - return new ImportedProject(scope, id, props); + public static fromProjectName(scope: Construct, id: string, projectName: string): IProject { + class Import extends ProjectBase { + public readonly grantPrincipal: iam.IPrincipal; + public readonly projectArn: string; + public readonly projectName: string; + public readonly role?: iam.Role = undefined; + + constructor(s: Construct, i: string) { + super(s, i); + + this.projectArn = this.node.stack.formatArn({ + service: 'codebuild', + resource: 'project', + resourceName: projectName, + }); + + this.grantPrincipal = new iam.ImportedResourcePrincipal({ resource: this }); + this.projectName = projectName; + } + + public export(): ProjectAttributes { + return { + projectName + }; + } + } + + return new Import(scope, id); } public readonly grantPrincipal: iam.IPrincipal; @@ -687,7 +718,7 @@ export class Project extends ProjectBase { /** * Export this Project. Allows referencing this Project in a different CDK Stack. */ - public export(): ProjectImportProps { + public export(): ProjectAttributes { return { projectName: new CfnOutput(this, 'ProjectName', { value: this.projectName }).makeImportValue().toString(), }; diff --git a/packages/@aws-cdk/aws-codecommit/lib/repository.ts b/packages/@aws-cdk/aws-codecommit/lib/repository.ts index d053931af119a..97542f36a0c93 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/repository.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/repository.ts @@ -3,16 +3,28 @@ import { CfnOutput, Construct, IResource, Resource } from '@aws-cdk/cdk'; import { CfnRepository } from './codecommit.generated'; export interface IRepository extends IResource { - /** The ARN of this Repository. */ + /** + * The ARN of this Repository. + * @attribute + */ readonly repositoryArn: string; - /** The human-visible name of this Repository. */ + /** + * The human-visible name of this Repository. + * @attribute + */ readonly repositoryName: string; - /** The HTTP clone URL */ + /** + * The HTTP clone URL + * @attribute + */ readonly repositoryCloneUrlHttp: string; - /** The SSH clone URL */ + /** + * The SSH clone URL + * @attribute + */ readonly repositoryCloneUrlSsh: string; /** @@ -72,13 +84,13 @@ export interface IRepository extends IResource { * * @see import */ - export(): RepositoryImportProps; + export(): RepositoryAttributes; } /** * Properties for the {@link Repository.import} method. */ -export interface RepositoryImportProps { +export interface RepositoryAttributes { /** * The name of an existing CodeCommit Repository that we are referencing. * Must be in the same account and region as the root Stack. @@ -108,7 +120,7 @@ abstract class RepositoryBase extends Resource implements IRepository { /** The SSH clone URL */ public abstract readonly repositoryCloneUrlSsh: string; - public abstract export(): RepositoryImportProps; + public abstract export(): RepositoryAttributes; /** * Defines a CloudWatch event rule which triggers for repository events. Use @@ -207,37 +219,6 @@ abstract class RepositoryBase extends Resource implements IRepository { } } -class ImportedRepository extends RepositoryBase { - public readonly repositoryArn: string; - public readonly repositoryName: string; - - constructor(scope: Construct, id: string, private readonly props: RepositoryImportProps) { - super(scope, id); - - this.repositoryArn = this.node.stack.formatArn({ - service: 'codecommit', - resource: props.repositoryName, - }); - this.repositoryName = props.repositoryName; - } - - public export() { - return this.props; - } - - public get repositoryCloneUrlHttp() { - return this.repositoryCloneUrl('https'); - } - - public get repositoryCloneUrlSsh() { - return this.repositoryCloneUrl('ssh'); - } - - private repositoryCloneUrl(protocol: 'https' | 'ssh'): string { - return `${protocol}://git-codecommit.${this.node.stack.region}.${this.node.stack.urlSuffix}/v1/repos/${this.repositoryName}`; - } -} - export interface RepositoryProps { /** * Name of the repository. This property is required for all repositories. @@ -255,17 +236,31 @@ export interface RepositoryProps { * Provides a CodeCommit Repository */ export class Repository extends RepositoryBase { + /** - * Import a Repository defined either outside the CDK, or in a different Stack - * (exported with the {@link export} method). - * - * @param scope the parent Construct for the Repository - * @param id the name of the Repository Construct - * @param props the properties used to identify the existing Repository - * @returns a reference to the existing Repository + * Imports a codecommit repository. + * @param repositoryArn (e.g. `arn:aws:codecommit:us-east-1:123456789012:MyDemoRepo`) */ - public static import(scope: Construct, id: string, props: RepositoryImportProps): IRepository { - return new ImportedRepository(scope, id, props); + public static fromRepositoryArn(scope: Construct, id: string, repositoryArn: string): IRepository { + const stack = scope.node.stack; + const repositoryName = stack.parseArn(repositoryArn).resource; + const makeCloneUrl = (protocol: 'https' | 'ssh') => + `${protocol}://git-codecommit.${stack.region}.${stack.urlSuffix}/v1/repos/${repositoryName}`; + + class Import extends RepositoryBase { + public readonly repositoryArn = repositoryArn; + public readonly repositoryName = repositoryName; + public readonly repositoryCloneUrlHttp = makeCloneUrl('https'); + public readonly repositoryCloneUrlSsh = makeCloneUrl('ssh'); + public export() { + return { + repositoryArn: this.repositoryArn, + repositoryName: this.repositoryName + }; + } + } + + return new Import(scope, id); } private readonly repository: CfnRepository; @@ -302,7 +297,7 @@ export class Repository extends RepositoryBase { * * @see import */ - public export(): RepositoryImportProps { + public export(): RepositoryAttributes { return { repositoryName: new CfnOutput(this, 'RepositoryName', { value: this.repositoryName }).makeImportValue().toString() }; diff --git a/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-config.ts index 704c4d764e1ab..218a1c740477b 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-config.ts @@ -35,6 +35,8 @@ export interface LambdaDeploymentConfigImportProps { * Note: This class currently stands as namespaced container of the default configurations * until CloudFormation supports custom Lambda Deployment Configs. Until then it is closed * (private constructor) and does not extend {@link cdk.Construct} + * + * @resource AWS::CodeDeploy::DeploymentConfig */ export class LambdaDeploymentConfig { public static readonly AllAtOnce = deploymentConfig('CodeDeployDefault.LambdaAllAtOnce'); 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 194c69cc9caf1..05a4535907833 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts @@ -12,7 +12,7 @@ import { ILambdaDeploymentConfig, LambdaDeploymentConfig } from './deployment-co /** * Interface for a Lambda deployment groups. */ -export interface ILambdaDeploymentGroup extends cdk.IConstruct { +export interface ILambdaDeploymentGroup extends cdk.IResource { /** * The reference to the CodeDeploy Lambda Application that this Deployment Group belongs to. */ @@ -20,18 +20,20 @@ export interface ILambdaDeploymentGroup extends cdk.IConstruct { /** * The physical name of the CodeDeploy Deployment Group. + * @attribute */ readonly deploymentGroupName: string; /** * The ARN of this Deployment Group. + * @attribute */ readonly deploymentGroupArn: string; /** * Export this Deployment Group for use in another stack or application. */ - export(): LambdaDeploymentGroupImportProps; + export(): LambdaDeploymentGroupAttributes; } /** @@ -107,18 +109,24 @@ export interface LambdaDeploymentGroupProps { readonly autoRollback?: AutoRollbackConfig; } -export class LambdaDeploymentGroup extends cdk.Construct implements ILambdaDeploymentGroup { +/** + * @resource AWS::CodeDeploy::DeploymentGroup + */ +export class LambdaDeploymentGroup extends cdk.Resource implements ILambdaDeploymentGroup { /** * Import an Lambda Deployment Group defined either outside the CDK, * or in a different CDK Stack and exported using the {@link #export} method. * * @param scope the parent Construct for this new Construct * @param id the logical ID of this new Construct - * @param props the properties of the referenced Deployment Group + * @param attrs the properties of the referenced Deployment Group * @returns a Construct representing a reference to an existing Deployment Group */ - public static import(scope: cdk.Construct, id: string, props: LambdaDeploymentGroupImportProps): ILambdaDeploymentGroup { - return new ImportedLambdaDeploymentGroup(scope, id, props); + public static fromLambdaDeploymentGroupAttributes( + scope: cdk.Construct, + id: string, + attrs: LambdaDeploymentGroupAttributes): ILambdaDeploymentGroup { + return new ImportedLambdaDeploymentGroup(scope, id, attrs); } public readonly application: ILambdaApplication; @@ -225,7 +233,7 @@ export class LambdaDeploymentGroup extends cdk.Construct implements ILambdaDeplo }); } - public export(): LambdaDeploymentGroupImportProps { + public export(): LambdaDeploymentGroupAttributes { return { application: this.application, deploymentGroupName: new cdk.CfnOutput(this, 'DeploymentGroupName', { @@ -241,7 +249,7 @@ export class LambdaDeploymentGroup extends cdk.Construct implements ILambdaDeplo * @see LambdaDeploymentGroup#import * @see ILambdaDeploymentGroup#export */ -export interface LambdaDeploymentGroupImportProps { +export interface LambdaDeploymentGroupAttributes { /** * The reference to the CodeDeploy Lambda Application * that this Deployment Group belongs to. @@ -260,7 +268,7 @@ class ImportedLambdaDeploymentGroup extends cdk.Construct implements ILambdaDepl public readonly deploymentGroupName: string; public readonly deploymentGroupArn: string; - constructor(scope: cdk.Construct, id: string, private readonly props: LambdaDeploymentGroupImportProps) { + constructor(scope: cdk.Construct, id: string, private readonly props: LambdaDeploymentGroupAttributes) { super(scope, id); this.application = props.application; this.deploymentGroupName = props.deploymentGroupName; diff --git a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-config.ts index f59b1761251b9..f9e490f4fde5d 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-config.ts @@ -10,9 +10,17 @@ import { arnForDeploymentConfig } from '../utils'; * instantiate the {@link ServerDeploymentConfig} Construct. */ export interface IServerDeploymentConfig { + /** + * @attribute + */ readonly deploymentConfigName: string; + + /** + * @attribute + */ readonly deploymentConfigArn: string; - export(): ServerDeploymentConfigImportProps; + + export(): ServerDeploymentConfigAttributes; } /** @@ -21,7 +29,7 @@ export interface IServerDeploymentConfig { * @see ServerDeploymentConfig#import * @see ServerDeploymentConfig#export */ -export interface ServerDeploymentConfigImportProps { +export interface ServerDeploymentConfigAttributes { /** * The physical, human-readable name of the custom CodeDeploy EC2/on-premise Deployment Configuration * that we are referencing. @@ -83,6 +91,8 @@ export interface ServerDeploymentConfigProps { /** * A custom Deployment Configuration for an EC2/on-premise Deployment Group. + * + * @resource AWS::CodeDeploy::DeploymentConfig */ export class ServerDeploymentConfig extends cdk.Resource implements IServerDeploymentConfig { public static readonly OneAtATime = deploymentConfig('CodeDeployDefault.OneAtATime'); @@ -93,13 +103,19 @@ export class ServerDeploymentConfig extends cdk.Resource implements IServerDeplo * Import a custom Deployment Configuration for an EC2/on-premise Deployment Group defined either outside the CDK, * or in a different CDK Stack and exported using the {@link #export} method. * - * @param _scope the parent Construct for this new Construct - * @param _id the logical ID of this new Construct - * @param props the properties of the referenced custom Deployment Configuration + * @param scope the parent Construct for this new Construct + * @param id the logical ID of this new Construct + * @param serverDeploymentConfigName the properties of the referenced custom Deployment Configuration * @returns a Construct representing a reference to an existing custom Deployment Configuration */ - public static import(_scope: cdk.Construct, _id: string, props: ServerDeploymentConfigImportProps): IServerDeploymentConfig { - return deploymentConfig(props.deploymentConfigName); + public static fromServerDeploymentConfigName( + scope: cdk.Construct, + id: string, + serverDeploymentConfigName: string): IServerDeploymentConfig { + + ignore(scope); + ignore(id); + return deploymentConfig(serverDeploymentConfigName); } public readonly deploymentConfigName: string; @@ -117,7 +133,7 @@ export class ServerDeploymentConfig extends cdk.Resource implements IServerDeplo this.deploymentConfigArn = arnForDeploymentConfig(this.deploymentConfigName); } - public export(): ServerDeploymentConfigImportProps { + public export(): ServerDeploymentConfigAttributes { return { deploymentConfigName: new cdk.CfnOutput(this, 'DeploymentConfigName', { value: this.deploymentConfigName, @@ -133,3 +149,5 @@ function deploymentConfig(name: string): IServerDeploymentConfig { export() { return { deploymentConfigName: name }; } }; } + +function ignore(_x: any) { return; } \ No newline at end of file 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 20006fbe7d9da..9250f9d16560f 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts @@ -11,14 +11,21 @@ import { arnForDeploymentGroup, renderAlarmConfiguration, renderAutoRollbackConf import { IServerApplication, ServerApplication } from './application'; import { IServerDeploymentConfig, ServerDeploymentConfig } from './deployment-config'; -export interface IServerDeploymentGroup extends cdk.IConstruct { +export interface IServerDeploymentGroup extends cdk.IResource { readonly application: IServerApplication; readonly role?: iam.Role; + /** + * @attribute + */ readonly deploymentGroupName: string; + + /** + * @attribute + */ readonly deploymentGroupArn: string; readonly deploymentConfig: IServerDeploymentConfig; readonly autoScalingGroups?: autoscaling.AutoScalingGroup[]; - export(): ServerDeploymentGroupImportProps; + export(): ServerDeploymentGroupAttributes; } /** @@ -27,7 +34,7 @@ export interface IServerDeploymentGroup extends cdk.IConstruct { * @see ServerDeploymentGroup#import * @see IServerDeploymentGroup#export */ -export interface ServerDeploymentGroupImportProps { +export interface ServerDeploymentGroupAttributes { /** * The reference to the CodeDeploy EC2/on-premise Application * that this Deployment Group belongs to. @@ -58,7 +65,7 @@ export interface ServerDeploymentGroupImportProps { * or one defined in a different CDK Stack, * use the {@link #import} method. */ -abstract class ServerDeploymentGroupBase extends cdk.Construct implements IServerDeploymentGroup { +abstract class ServerDeploymentGroupBase extends cdk.Resource implements IServerDeploymentGroup { public abstract readonly application: IServerApplication; public abstract readonly role?: iam.Role; public abstract readonly deploymentGroupName: string; @@ -71,7 +78,7 @@ abstract class ServerDeploymentGroupBase extends cdk.Construct implements IServe this.deploymentConfig = deploymentConfig || ServerDeploymentConfig.OneAtATime; } - public abstract export(): ServerDeploymentGroupImportProps; + public abstract export(): ServerDeploymentGroupAttributes; } class ImportedServerDeploymentGroup extends ServerDeploymentGroupBase { @@ -81,7 +88,7 @@ class ImportedServerDeploymentGroup extends ServerDeploymentGroupBase { public readonly deploymentGroupArn: string; public readonly autoScalingGroups?: autoscaling.AutoScalingGroup[] = undefined; - constructor(scope: cdk.Construct, id: string, private readonly props: ServerDeploymentGroupImportProps) { + constructor(scope: cdk.Construct, id: string, private readonly props: ServerDeploymentGroupAttributes) { super(scope, id, props.deploymentConfig); this.application = props.application; @@ -227,6 +234,7 @@ export interface ServerDeploymentGroupProps { /** * A CodeDeploy Deployment Group that deploys to EC2/on-premise instances. + * @resource AWS::CodeDeploy::DeploymentGroup */ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { /** @@ -235,11 +243,14 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { * * @param scope the parent Construct for this new Construct * @param id the logical ID of this new Construct - * @param props the properties of the referenced Deployment Group + * @param attrs the properties of the referenced Deployment Group * @returns a Construct representing a reference to an existing Deployment Group */ - public static import(scope: cdk.Construct, id: string, props: ServerDeploymentGroupImportProps): IServerDeploymentGroup { - return new ImportedServerDeploymentGroup(scope, id, props); + public static fromServerDeploymentGroupAttributes( + scope: cdk.Construct, + id: string, + attrs: ServerDeploymentGroupAttributes): IServerDeploymentGroup { + return new ImportedServerDeploymentGroup(scope, id, attrs); } public readonly application: IServerApplication; @@ -264,9 +275,7 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { this._autoScalingGroups = props.autoScalingGroups || []; this.installAgent = props.installAgent === undefined ? true : props.installAgent; - this.codeDeployBucket = s3.Bucket.import(this, 'CodeDeployBucket', { - bucketName: `aws-codedeploy-${this.node.stack.region}`, - }); + this.codeDeployBucket = s3.Bucket.fromBucketName(this, 'Bucket', `aws-codedeploy-${this.node.stack.region}`); for (const asg of this._autoScalingGroups) { this.addCodeDeployAgentInstallUserData(asg); } @@ -299,7 +308,7 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { this.deploymentGroupArn = arnForDeploymentGroup(this.application.applicationName, this.deploymentGroupName); } - public export(): ServerDeploymentGroupImportProps { + public export(): ServerDeploymentGroupAttributes { return { application: this.application, deploymentGroupName: new cdk.CfnOutput(this, 'DeploymentGroupName', { diff --git a/packages/@aws-cdk/aws-codedeploy/package.json b/packages/@aws-cdk/aws-codedeploy/package.json index 696f0c007a707..572c7053befe5 100644 --- a/packages/@aws-cdk/aws-codedeploy/package.json +++ b/packages/@aws-cdk/aws-codedeploy/package.json @@ -94,7 +94,8 @@ }, "awslint": { "exclude": [ - "construct-interface-extends-iconstruct:@aws-cdk/aws-codedeploy.IServerDeploymentConfig" + "construct-interface-extends-iconstruct:@aws-cdk/aws-codedeploy.IServerDeploymentConfig", + "resource-interface-extends-resource:@aws-cdk/aws-codedeploy.IServerDeploymentConfig" ] } } diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-config.ts index c891719ade539..84bef7cd3a377 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-config.ts @@ -44,9 +44,7 @@ export = { 'can be imported'(test: Test) { const stack = new cdk.Stack(); - const deploymentConfig = codedeploy.ServerDeploymentConfig.import(stack, 'MyDC', { - deploymentConfigName: 'MyDC', - }); + const deploymentConfig = codedeploy.ServerDeploymentConfig.fromServerDeploymentConfigName(stack, 'MyDC', 'MyDC'); test.notEqual(deploymentConfig, undefined); diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts index 61aae5044cf74..19f3fb6094a4c 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts @@ -34,7 +34,7 @@ export = { const application = codedeploy.ServerApplication.import(stack, 'MyApp', { applicationName: 'MyApp', }); - const deploymentGroup = codedeploy.ServerDeploymentGroup.import(stack, 'MyDG', { + const deploymentGroup = codedeploy.ServerDeploymentGroup.fromServerDeploymentGroupAttributes(stack, 'MyDG', { application, deploymentGroupName: 'MyDG', }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts index 22333b48b18db..f9fcedf104f00 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts @@ -369,9 +369,7 @@ export = { 'Action service role is passed to template'(test: Test) { const stack = new TestFixture(); - const importedRole = Role.import(stack, 'ImportedRole', { - roleArn: 'arn:aws:iam::000000000000:role/action-role' - }); + const importedRole = Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::000000000000:role/action-role'); const freshRole = new Role(stack, 'FreshRole', { assumedBy: new ServicePrincipal('magicservice') }); 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 00de8197a20cb..85d7eab484dd1 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts @@ -705,6 +705,19 @@ export = { test.done(); }, }, + + 'Pipeline.fromPipelineArn'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const pl = codepipeline.Pipeline.fromPipelineArn(stack, 'imported', 'arn:aws:codepipeline:us-east-1:123456789012:MyDemoPipeline'); + + // THEN + test.deepEqual(pl.pipelineArn, 'arn:aws:codepipeline:us-east-1:123456789012:MyDemoPipeline'); + test.deepEqual(pl.pipelineName, 'MyDemoPipeline'); + test.done(); + } }; function stageForTesting(stack: Stack): codepipeline.IStage { diff --git a/packages/@aws-cdk/aws-codepipeline/lib/action.ts b/packages/@aws-cdk/aws-codepipeline/lib/action.ts index 84f71a4cc8a1f..0d1e505d3f2cd 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/action.ts @@ -61,11 +61,15 @@ export interface ActionBind { export interface IPipeline extends IResource, events.IEventRuleTarget { /** * The name of the Pipeline. + * + * @attribute */ readonly pipelineName: string; /** * The ARN of the Pipeline. + * + * @attribute */ readonly pipelineArn: string; diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 8a46c51fbde88..5805597bc5a31 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -98,6 +98,46 @@ export interface PipelineProps { readonly stages?: StageProps[]; } +abstract class PipelineBase extends Resource implements IPipeline { + public abstract pipelineName: string; + public abstract pipelineArn: string; + private eventsRole?: iam.Role; + public abstract grantBucketRead(identity: iam.IGrantable): iam.Grant; + public abstract grantBucketReadWrite(identity: iam.IGrantable): iam.Grant; + + /** + * Allows the pipeline to be used as a CloudWatch event rule target. + * + * Usage: + * + * const pipeline = new Pipeline(this, 'MyPipeline'); + * const rule = new EventRule(this, 'MyRule', { schedule: 'rate(1 minute)' }); + * rule.addTarget(pipeline); + * + */ + public asEventRuleTarget(_ruleArn: string, _ruleId: string): events.EventRuleTargetProps { + // the first time the event rule target is retrieved, we define an IAM + // role assumable by the CloudWatch events service which is allowed to + // start the execution of this pipeline. no need to define more than one + // role per pipeline. + if (!this.eventsRole) { + this.eventsRole = new iam.Role(this, 'EventsRole', { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com') + }); + + this.eventsRole.addToPolicy(new iam.PolicyStatement() + .addResource(this.pipelineArn) + .addAction('codepipeline:StartPipelineExecution')); + } + + return { + id: this.node.id, + arn: this.pipelineArn, + roleArn: this.eventsRole.roleArn, + }; + } +} + /** * An AWS CodePipeline pipeline with its associated IAM role and S3 bucket. * @@ -117,7 +157,31 @@ export interface PipelineProps { * * // ... add more stages */ -export class Pipeline extends Resource implements IPipeline { +export class Pipeline extends PipelineBase { + + /** + * Import a pipeline into this app. + * @param scope the scope into which to import this pipeline + * @param pipelineArn The ARN of the pipeline (e.g. `arn:aws:codepipeline:us-east-1:123456789012:MyDemoPipeline`) + */ + public static fromPipelineArn(scope: Construct, id: string, pipelineArn: string): IPipeline { + + class Import extends PipelineBase { + public pipelineName = scope.node.stack.parseArn(pipelineArn).resource; + public pipelineArn = pipelineArn; + + public grantBucketRead(identity: iam.IGrantable): iam.Grant { + return iam.Grant.drop(identity, `grant read permissions to the artifacts bucket of ${pipelineArn}`); + } + + public grantBucketReadWrite(identity: iam.IGrantable): iam.Grant { + return iam.Grant.drop(identity, `grant read/write permissions to the artifacts bucket of ${pipelineArn}`); + } + } + + return new Import(scope, id); + } + /** * The IAM role AWS CodePipeline will use to perform actions or assume roles for actions with * a more specific IAM role. @@ -136,6 +200,8 @@ export class Pipeline extends Resource implements IPipeline { /** * The version of the pipeline + * + * @attribute */ public readonly pipelineVersion: string; @@ -145,7 +211,6 @@ export class Pipeline extends Resource implements IPipeline { public readonly artifactBucket: s3.IBucket; private readonly stages = new Array(); - private eventsRole?: iam.Role; private readonly pipelineResource: CfnPipeline; private readonly crossRegionReplicationBuckets: { [region: string]: string }; private readonly artifactStores: { [region: string]: any }; @@ -233,38 +298,6 @@ export class Pipeline extends Resource implements IPipeline { this.role.addToPolicy(statement); } - /** - * Allows the pipeline to be used as a CloudWatch event rule target. - * - * Usage: - * - * const pipeline = new Pipeline(this, 'MyPipeline'); - * const rule = new EventRule(this, 'MyRule', { schedule: 'rate(1 minute)' }); - * rule.addTarget(pipeline); - * - */ - public asEventRuleTarget(_ruleArn: string, _ruleId: string): events.EventRuleTargetProps { - // the first time the event rule target is retrieved, we define an IAM - // role assumable by the CloudWatch events service which is allowed to - // start the execution of this pipeline. no need to define more than one - // role per pipeline. - if (!this.eventsRole) { - this.eventsRole = new iam.Role(this, 'EventsRole', { - assumedBy: new iam.ServicePrincipal('events.amazonaws.com') - }); - - this.eventsRole.addToPolicy(new iam.PolicyStatement() - .addResource(this.pipelineArn) - .addAction('codepipeline:StartPipelineExecution')); - } - - return { - id: this.node.id, - arn: this.pipelineArn, - roleArn: this.eventsRole.roleArn, - }; - } - /** * Defines an event rule triggered by the "CodePipeline Pipeline Execution * State Change" event emitted from this pipeline. @@ -375,7 +408,7 @@ export class Pipeline extends Resource implements IPipeline { replicationBucketName = crossRegionScaffoldStack.replicationBucketName; } - const replicationBucket = s3.Bucket.import(this, 'CrossRegionCodePipelineReplicationBucket-' + region, { + const replicationBucket = s3.Bucket.fromBucketAttributes(this, 'CrossRegionCodePipelineReplicationBucket-' + region, { bucketName: replicationBucketName, }); replicationBucket.grantReadWrite(this.role); diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index b26046f64450e..c8ff0eda0e111 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -51,17 +51,33 @@ export interface UserPoolClientProps { * Define a UserPool App Client */ export class UserPoolClient extends Resource { - public readonly clientId: string; + /** + * @attribute + */ + public readonly userPoolClientId: string; + + /** + * @attribute + */ + public readonly userPoolClientName: string; + + /** + * @attribute + */ + public readonly userPoolClientClientSecret: string; constructor(scope: Construct, id: string, props: UserPoolClientProps) { super(scope, id); - const userPoolClient = new CfnUserPoolClient(this, 'Resource', { + const resource = new CfnUserPoolClient(this, 'Resource', { clientName: props.clientName, generateSecret: props.generateSecret, userPoolId: props.userPool.userPoolId, explicitAuthFlows: props.enabledAuthFlows }); - this.clientId = userPoolClient.userPoolClientId; + + this.userPoolClientId = resource.userPoolClientId; + this.userPoolClientClientSecret = resource.userPoolClientClientSecret; + this.userPoolClientName = resource.userPoolClientName; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 3df23242c7775..b1c039be4d4e9 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -234,7 +234,7 @@ export interface UserPoolProps { readonly lambdaTriggers?: UserPoolTriggers; } -export interface UserPoolImportProps { +export interface UserPoolAttributes { /** * The ID of an existing user pool */ @@ -259,21 +259,25 @@ export interface UserPoolImportProps { export interface IUserPool extends IResource { /** * The physical ID of this user pool resource + * @attribute */ readonly userPoolId: string; /** * The ARN of this user pool resource + * @attribute */ readonly userPoolArn: string; /** * The provider name of this user pool resource + * @attribute */ readonly userPoolProviderName: string; /** * The provider URL of this user pool resource + * @attribute */ readonly userPoolProviderUrl: string; @@ -281,7 +285,7 @@ export interface IUserPool extends IResource { * Exports a User Pool from this stack * @returns user pool props that can be imported into another stack */ - export(): UserPoolImportProps; + export(): UserPoolAttributes; } /** @@ -292,10 +296,24 @@ export class UserPool extends Resource implements IUserPool { * Import an existing user pool resource * @param scope Parent construct * @param id Construct ID - * @param props Imported user pool properties - */ - public static import(scope: Construct, id: string, props: UserPoolImportProps): IUserPool { - return new ImportedUserPool(scope, id, props); + * @param attrs Imported user pool properties + */ + public static fromUserPoolAttributes(scope: Construct, id: string, attrs: UserPoolAttributes): IUserPool { + /** + * Define a user pool which has been declared in another stack + */ + class Import extends Construct implements IUserPool { + public readonly userPoolId = attrs.userPoolId; + public readonly userPoolArn = attrs.userPoolArn; + public readonly userPoolProviderName = attrs.userPoolProviderName; + public readonly userPoolProviderUrl = attrs.userPoolProviderUrl; + + public export(): UserPoolAttributes { + return attrs; + } + } + + return new Import(scope, id); } /** @@ -475,7 +493,7 @@ export class UserPool extends Resource implements IUserPool { this.triggers = { ...this.triggers, verifyAuthChallengeResponse: fn.functionArn }; } - public export(): UserPoolImportProps { + public export(): UserPoolAttributes { return { userPoolId: new CfnOutput(this, 'UserPoolId', { value: this.userPoolId }).makeImportValue().toString(), userPoolArn: new CfnOutput(this, 'UserPoolArn', { value: this.userPoolArn }).makeImportValue().toString(), @@ -492,41 +510,3 @@ export class UserPool extends Resource implements IUserPool { }); } } - -/** - * Define a user pool which has been declared in another stack - */ -class ImportedUserPool extends Construct implements IUserPool { - /** - * The ID of an existing user pool - */ - public readonly userPoolId: string; - - /** - * The ARN of the imported user pool - */ - public readonly userPoolArn: string; - - /** - * The provider name of the imported user pool - */ - public readonly userPoolProviderName: string; - - /** - * The URL of the imported user pool - */ - public readonly userPoolProviderUrl: string; - - constructor(scope: Construct, id: string, private readonly props: UserPoolImportProps) { - super(scope, id); - - this.userPoolId = props.userPoolId; - this.userPoolArn = props.userPoolArn; - this.userPoolProviderName = props.userPoolProviderName; - this.userPoolProviderUrl = props.userPoolProviderUrl; - } - - public export(): UserPoolImportProps { - return this.props; - } -} diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index c96c0613007cc..85992d8969fb3 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -76,5 +76,11 @@ }, "engines": { "node": ">= 8.10.0" + }, + "awslint": { + "exclude": [ + "from-method:@aws-cdk/aws-cognito.UserPool", + "from-arn:UserPool.fromUserPoolArn" + ] } } diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index b370e3fa292cf..686f6c14e0ae5 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -190,8 +190,19 @@ export class Table extends Resource { }); } + /** + * @attribute + */ public readonly tableArn: string; + + /** + * @attribute + */ public readonly tableName: string; + + /** + * @attribute + */ public readonly tableStreamArn: string; private readonly table: CfnTable; @@ -623,14 +634,12 @@ export class Table extends Resource { */ private makeScalingRole(): iam.IRole { // Use a Service Linked Role. - return iam.Role.import(this, 'ScalingRole', { - roleArn: this.node.stack.formatArn({ - // https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-service-linked-roles.html - service: 'iam', - resource: 'role/aws-service-role/dynamodb.application-autoscaling.amazonaws.com', - resourceName: 'AWSServiceRoleForApplicationAutoScaling_DynamoDBTable' - }) - }); + // https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-service-linked-roles.html + return iam.Role.fromRoleArn(this, 'ScalingRole', this.node.stack.formatArn({ + service: 'iam', + resource: 'role/aws-service-role/dynamodb.application-autoscaling.amazonaws.com', + resourceName: 'AWSServiceRoleForApplicationAutoScaling_DynamoDBTable' + })); } /** diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index 56b962fa0a48d..c915026df65ae 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -9,14 +9,10 @@ const isSecurityGroupSymbol = Symbol.for('aws-cdk:isSecurityGroup'); export interface ISecurityGroup extends IResource, ISecurityGroupRule, IConnectable { /** * ID for the current security group + * @attribute */ readonly securityGroupId: string; - /** - * The ID of the VPC this security group is part of. - */ - readonly securityGroupVpcId: string; - /** * Add an ingress rule for the current security group * @@ -42,20 +38,14 @@ export interface ISecurityGroup extends IResource, ISecurityGroupRule, IConnecta /** * Export the security group */ - export(): SecurityGroupImportProps; + export(): SecurityGroupAttributes; } -export interface SecurityGroupImportProps { +export interface SecurityGroupAttributes { /** * ID of security group */ readonly securityGroupId: string; - - /** - * The VPC ID this security group is part of. If not provided, the `securityGroupVpcId` property - * will throw an exception. - */ - readonly securityGroupVpcId?: string; } /** @@ -70,7 +60,6 @@ abstract class SecurityGroupBase extends Resource implements ISecurityGroup { } public abstract readonly securityGroupId: string; - public abstract readonly securityGroupVpcId: string; public readonly canInlineRule = false; public readonly connections: Connections = new Connections({ securityGroups: [this] }); @@ -137,7 +126,7 @@ abstract class SecurityGroupBase extends Resource implements ISecurityGroup { /** * Export this SecurityGroup for use in a different Stack */ - public abstract export(): SecurityGroupImportProps; + public abstract export(): SecurityGroupAttributes; } /** @@ -254,20 +243,15 @@ export interface SecurityGroupProps { * the template). */ export class SecurityGroup extends SecurityGroupBase { + /** - * Import an existing SecurityGroup + * Import an existing security group into this app. */ - public static import(scope: Construct, id: string, props: SecurityGroupImportProps): ISecurityGroup { + public static fromSecurityGroupId(scope: Construct, id: string, securityGroupId: string): ISecurityGroup { class Import extends SecurityGroupBase { - public readonly securityGroupId = props.securityGroupId; - - public get securityGroupVpcId() { - if (!props.securityGroupVpcId) { throw new Error(`Imported security group did not specify 'securityGroupVpcId'`); } - return props.securityGroupVpcId; - } - - public export() { - return props; + public securityGroupId = securityGroupId; + public export(): SecurityGroupAttributes { + return { securityGroupId }; } } @@ -276,21 +260,22 @@ export class SecurityGroup extends SecurityGroupBase { /** * An attribute that represents the security group name. + * + * @attribute */ - public readonly groupName: string; - - /** - * An attribute that represents the physical VPC ID this security group is part of. - */ - public readonly vpcId: string; + public readonly securityGroupName: string; /** * The ID of the security group + * + * @attribute */ public readonly securityGroupId: string; /** * The VPC ID this security group is part of. + * + * @attribute */ public readonly securityGroupVpcId: string; @@ -317,8 +302,7 @@ export class SecurityGroup extends SecurityGroupBase { this.securityGroupId = this.securityGroup.securityGroupId; this.securityGroupVpcId = this.securityGroup.securityGroupVpcId; - this.groupName = this.securityGroup.securityGroupName; - this.vpcId = this.securityGroup.securityGroupVpcId; + this.securityGroupName = this.securityGroup.securityGroupName; this.addDefaultEgressRule(); } @@ -326,7 +310,7 @@ export class SecurityGroup extends SecurityGroupBase { /** * Export this SecurityGroup for use in a different Stack */ - public export(): SecurityGroupImportProps { + public export(): SecurityGroupAttributes { return { securityGroupId: new CfnOutput(this, 'SecurityGroupId', { value: this.securityGroupId }).makeImportValue().toString() }; diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index 416339ee1a4b9..c9ead17185aa7 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -470,7 +470,7 @@ class ImportedInterfaceVpcEndpoint extends cdk.Construct implements IInterfaceVp this.connections = new Connections({ defaultPortRange: new TcpPortFromAttribute(props.port), - securityGroups: [SecurityGroup.import(this, 'SecurityGroup', props)], + securityGroups: [SecurityGroup.fromSecurityGroupId(this, 'SecurityGroup', props.securityGroupId)], }); } diff --git a/packages/@aws-cdk/aws-ec2/test/test.connections.ts b/packages/@aws-cdk/aws-ec2/test/test.connections.ts index b6f9b260a8a1e..dca715d87ff44 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.connections.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.connections.ts @@ -37,7 +37,7 @@ export = { const sg1 = new SecurityGroup(stack, 'SomeSecurityGroup', { vpc, allowAllOutbound: false }); const somethingConnectable = new SomethingConnectable(new Connections({ securityGroups: [sg1] })); - const securityGroup = SecurityGroup.import(stack, 'ImportedSG', { securityGroupId: 'sg-12345' }); + const securityGroup = SecurityGroup.fromSecurityGroupId(stack, 'ImportedSG', 'sg-12345'); // WHEN somethingConnectable.connections.allowTo(securityGroup, new TcpAllPorts(), 'Connect there'); diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 6c6e59e386fdc..7f2e3d5efd531 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -1,6 +1,6 @@ import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); -import { CfnOutput, Construct, DeletionPolicy, IConstruct, IResource, Resource, Token } from '@aws-cdk/cdk'; +import { Construct, DeletionPolicy, IConstruct, IResource, Resource, Token } from '@aws-cdk/cdk'; import { CfnRepository } from './ecr.generated'; import { CountType, LifecycleRule, TagStatus } from './lifecycle'; @@ -10,11 +10,13 @@ import { CountType, LifecycleRule, TagStatus } from './lifecycle'; export interface IRepository extends IResource { /** * The name of the repository + * @attribute */ readonly repositoryName: string; /** * The ARN of the repository + * @attribute */ readonly repositoryArn: string; @@ -23,6 +25,7 @@ export interface IRepository extends IResource { * * ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY * + * @attribute */ readonly repositoryUri: string; @@ -63,61 +66,12 @@ export interface IRepository extends IResource { * @param imageTag Only trigger on the specific image tag */ onImagePushed(name: string, target?: events.IEventRuleTarget, imageTag?: string): events.EventRule; - - /** - * Export this repository from the stack - */ - export(): RepositoryImportProps; -} - -export interface RepositoryImportProps { - /** - * The ARN of the repository to import. - * - * At least one of `repositoryArn` or `repositoryName` is required. - * - * @default If you only have a repository name and the repository is in the same - * account/region as the current stack, you can set `repositoryName` instead - * and the ARN will be formatted with the current region and account. - */ - readonly repositoryArn?: string; - - /** - * The full name of the repository to import. - * - * This is only needed if the repository ARN is not a concrete string, in which - * case it is impossible to safely parse the ARN and extract full repository - * names from it if it includes multiple components (e.g. `foo/bar/myrepo`). - * - * If the repository is in the same region/account as the stack, it is sufficient - * to only specify the repository name. - */ - readonly repositoryName?: string; } /** * Base class for ECR repository. Reused between imported repositories and owned repositories. */ export abstract class RepositoryBase extends Resource implements IRepository { - /** - * Import a repository - */ - public static import(scope: Construct, id: string, props: RepositoryImportProps): IRepository { - return new ImportedRepository(scope, id, props); - } - - /** - * Returns an ECR ARN for a repository that resides in the same account/region - * as the current stack. - */ - public static arnForLocalRepository(repositoryName: string, scope: IConstruct): string { - return scope.node.stack.formatArn({ - service: 'ecr', - resource: 'repository', - resourceName: repositoryName - }); - } - /** * The name of the repository */ @@ -156,11 +110,6 @@ export abstract class RepositoryBase extends Resource implements IRepository { return `${parts.account}.dkr.ecr.${parts.region}.amazonaws.com/${this.repositoryName}${tagSuffix}`; } - /** - * Export this repository from the stack - */ - public abstract export(): RepositoryImportProps; - /** * Defines an AWS CloudWatch event rule that can trigger a target when an image is pushed to this * repository. @@ -229,50 +178,6 @@ export abstract class RepositoryBase extends Resource implements IRepository { } } -/** - * An already existing repository - */ -class ImportedRepository extends RepositoryBase { - public readonly repositoryName: string; - public readonly repositoryArn: string; - - constructor(scope: Construct, id: string, private readonly props: RepositoryImportProps) { - super(scope, id); - - if (props.repositoryArn) { - this.repositoryArn = props.repositoryArn; - } else { - if (!props.repositoryName) { - throw new Error('If "repositoryArn" is not specified, you must specify "repositoryName", ' + - 'which also implies that the repository resides in the same region/account as this stack'); - } - - this.repositoryArn = RepositoryBase.arnForLocalRepository(props.repositoryName, this); - } - - if (props.repositoryName) { - this.repositoryName = props.repositoryName; - } else { - // if repositoryArn is a token, the repository name is also required. this is because - // repository names can include "/" (e.g. foo/bar/myrepo) and it is impossible to - // parse the name from an ARN using CloudFormation's split/select. - if (Token.unresolved(this.repositoryArn)) { - throw new Error('repositoryArn is a late-bound value, and therefore repositoryName is required'); - } - - this.repositoryName = this.repositoryArn.split('/').slice(1).join('/'); - } - } - - public export(): RepositoryImportProps { - return this.props; - } - - public addToResourcePolicy(_statement: iam.PolicyStatement) { - // FIXME: Add annotation about policy we dropped on the floor - } -} - export interface RepositoryProps { /** * Name for this repository @@ -307,10 +212,79 @@ export interface RepositoryProps { readonly retain?: boolean; } +export interface RepositoryAttributes { + readonly repositoryName: string; + readonly repositoryArn: string; +} + /** * Define an ECR repository */ export class Repository extends RepositoryBase { + /** + * Import a repository + */ + public static fromRepositoryAttributes(scope: Construct, id: string, attrs: RepositoryAttributes): IRepository { + class Import extends RepositoryBase { + public readonly repositoryName = attrs.repositoryName; + public readonly repositoryArn = attrs.repositoryArn; + + public addToResourcePolicy(_statement: iam.PolicyStatement) { + // dropped + } + } + + return new Import(scope, id); + } + + public static fromRepositoryArn(scope: Construct, id: string, repositoryArn: string): IRepository { + + // if repositoryArn is a token, the repository name is also required. this is because + // repository names can include "/" (e.g. foo/bar/myrepo) and it is impossible to + // parse the name from an ARN using CloudFormation's split/select. + if (Token.unresolved(repositoryArn)) { + throw new Error('"repositoryArn" is a late-bound value, and therefore "repositoryName" is required. Use `fromRepositoryAttributes` instead'); + } + + const repositoryName = repositoryArn.split('/').slice(1).join('/'); + + class Import extends RepositoryBase { + public repositoryName = repositoryName; + public repositoryArn = repositoryArn; + + public addToResourcePolicy(_statement: iam.PolicyStatement): void { + // dropped + } + } + + return new Import(scope, id); + } + + public static fromRepositoryName(scope: Construct, id: string, repositoryName: string): IRepository { + class Import extends RepositoryBase { + public repositoryName = repositoryName; + public repositoryArn = Repository.arnForLocalRepository(repositoryName, scope); + + public addToResourcePolicy(_statement: iam.PolicyStatement): void { + // dropped + } + } + + return new Import(scope, id); + } + + /** + * Returns an ECR ARN for a repository that resides in the same account/region + * as the current stack. + */ + public static arnForLocalRepository(repositoryName: string, scope: IConstruct): string { + return scope.node.stack.formatArn({ + service: 'ecr', + resource: 'repository', + resourceName: repositoryName + }); + } + public readonly repositoryName: string; public readonly repositoryArn: string; private readonly lifecycleRules = new Array(); @@ -340,16 +314,6 @@ export class Repository extends RepositoryBase { this.repositoryArn = resource.repositoryArn; } - /** - * Export this repository from the stack - */ - public export(): RepositoryImportProps { - return { - repositoryArn: new CfnOutput(this, 'RepositoryArn', { value: this.repositoryArn }).makeImportValue().toString(), - repositoryName: new CfnOutput(this, 'RepositoryName', { value: this.repositoryName }).makeImportValue().toString() - }; - } - public addToResourcePolicy(statement: iam.PolicyStatement) { if (this.policyDocument === undefined) { this.policyDocument = new iam.PolicyDocument(); diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index d852192a89f33..e83eeefd26704 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -169,43 +169,16 @@ export = { test.done(); }, - 'export/import'(test: Test) { - // GIVEN - const stack1 = new cdk.Stack(); - const repo1 = new ecr.Repository(stack1, 'Repo'); - - const stack2 = new cdk.Stack(); - - // WHEN - const repo2 = ecr.Repository.import(stack2, 'Repo', repo1.export()); - - // THEN - test.deepEqual(repo2.node.resolve(repo2.repositoryArn), { - 'Fn::ImportValue': 'Stack:RepoRepositoryArn7F2901C9' - }); - - test.deepEqual(repo2.node.resolve(repo2.repositoryName), { - 'Fn::ImportValue': 'Stack:RepoRepositoryName58A7E467' - }); - - test.done(); - }, - 'import with concrete arn'(test: Test) { // GIVEN const stack = new cdk.Stack(); // WHEN - const repo2 = ecr.Repository.import(stack, 'Repo', { - repositoryArn: 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo' - }); - - const exportImport = repo2.export(); + const repo2 = ecr.Repository.fromRepositoryArn(stack, 'repo', 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo'); // THEN test.deepEqual(repo2.node.resolve(repo2.repositoryArn), 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo'); test.deepEqual(repo2.node.resolve(repo2.repositoryName), 'foo/bar/foo/fooo'); - test.deepEqual(repo2.node.resolve(exportImport), { repositoryArn: 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo' }); test.done(); }, @@ -215,9 +188,8 @@ export = { const stack = new cdk.Stack(); // WHEN/THEN - test.throws(() => ecr.Repository.import(stack, 'Repo', { - repositoryArn: cdk.Fn.getAtt('Boom', 'Boom').toString() - }), /repositoryArn is a late-bound value, and therefore repositoryName is required/); + test.throws(() => ecr.Repository.fromRepositoryArn(stack, 'arn', cdk.Fn.getAtt('Boom', 'Boom').toString()), + /\"repositoryArn\" is a late-bound value, and therefore \"repositoryName\" is required\. Use \`fromRepositoryAttributes\` instead/); test.done(); }, @@ -227,7 +199,7 @@ export = { const stack = new cdk.Stack(); // WHEN - const repo = ecr.Repository.import(stack, 'Repo', { + const repo = ecr.Repository.fromRepositoryAttributes(stack, 'Repo', { repositoryArn: cdk.Fn.getAtt('Boom', 'Arn').toString(), repositoryName: cdk.Fn.getAtt('Boom', 'Name').toString() }); @@ -243,9 +215,7 @@ export = { const stack = new cdk.Stack(); // WHEN - const repo = ecr.Repository.import(stack, 'Repo', { - repositoryName: 'my-repo' - }); + const repo = ecr.Repository.fromRepositoryName(stack, 'just-name', 'my-repo'); // THEN test.deepEqual(repo.node.resolve(repo.repositoryArn), { @@ -269,7 +239,7 @@ export = { const repoName = cdk.Fn.getAtt('Boom', 'Name').toString(); // WHEN - const repo = ecr.Repository.import(stack, 'Repo', { + const repo = ecr.Repository.fromRepositoryAttributes(stack, 'Repo', { repositoryArn: ecr.Repository.arnForLocalRepository(repoName, stack), repositoryName: repoName }); 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 32a6b80c8145a..19066aaafcea7 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -275,13 +275,11 @@ export abstract class BaseService extends cdk.Construct */ private makeAutoScalingRole(): iam.IRole { // Use a Service Linked Role. - return iam.Role.import(this, 'ScalingRole', { - roleArn: this.node.stack.formatArn({ - service: 'iam', - resource: 'role/aws-service-role/ecs.application-autoscaling.amazonaws.com', - resourceName: 'AWSServiceRoleForApplicationAutoScaling_ECSService', - }) - }); + return iam.Role.fromRoleArn(this, 'ScalingRole', this.node.stack.formatArn({ + service: 'iam', + resource: 'role/aws-service-role/ecs.application-autoscaling.amazonaws.com', + resourceName: 'AWSServiceRoleForApplicationAutoScaling_ECSService', + })); } /** 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 d66e119c15eb1..af54b1333b9f2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -106,6 +106,7 @@ export class TaskDefinition extends Resource { /** * ARN of this task definition + * @attribute */ public readonly taskDefinitionArn: string; diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 4266dde63e6ca..54a0d8d2ecf1b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -31,8 +31,8 @@ export class Cluster extends Resource implements ICluster { /** * Import an existing cluster */ - public static import(scope: Construct, id: string, props: ClusterImportProps): ICluster { - return new ImportedCluster(scope, id, props); + public static fromClusterAttributes(scope: Construct, id: string, attrs: ClusterAttributes): ICluster { + return new ImportedCluster(scope, id, attrs); } /** @@ -183,7 +183,7 @@ export class Cluster extends Resource implements ICluster { /** * Export the Cluster */ - public export(): ClusterImportProps { + public export(): ClusterAttributes { return { clusterName: new CfnOutput(this, 'ClusterName', { value: this.clusterName }).makeImportValue().toString(), clusterArn: this.clusterArn, @@ -271,11 +271,13 @@ export class EcsOptimizedAmi implements ec2.IMachineImageSource { export interface ICluster extends IResource { /** * Name of the cluster + * @attribute */ readonly clusterName: string; /** * The ARN of this cluster + * @attribute */ readonly clusterArn: string; @@ -302,13 +304,13 @@ export interface ICluster extends IResource { /** * Export the Cluster */ - export(): ClusterImportProps; + export(): ClusterAttributes; } /** * Properties to import an ECS cluster */ -export interface ClusterImportProps { +export interface ClusterAttributes { /** * Name of the cluster */ @@ -329,7 +331,7 @@ export interface ClusterImportProps { /** * Security group of the cluster instances */ - readonly securityGroups: ec2.SecurityGroupImportProps[]; + readonly securityGroups: ec2.SecurityGroupAttributes[]; /** * Whether the given cluster has EC2 capacity @@ -380,7 +382,7 @@ class ImportedCluster extends Construct implements ICluster { */ private _defaultNamespace?: cloudmap.INamespace; - constructor(scope: Construct, id: string, private readonly props: ClusterImportProps) { + constructor(scope: Construct, id: string, private readonly props: ClusterAttributes) { super(scope, id); this.clusterName = props.clusterName; this.vpc = ec2.VpcNetwork.import(this, "vpc", props.vpc); @@ -395,7 +397,7 @@ class ImportedCluster extends Construct implements ICluster { let i = 1; for (const sgProps of props.securityGroups) { - this.connections.addSecurityGroup(ec2.SecurityGroup.import(this, `SecurityGroup${i}`, sgProps)); + this.connections.addSecurityGroup(ec2.SecurityGroup.fromSecurityGroupId(this, `SecurityGroup${i}`, sgProps.securityGroupId)); i++; } } 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 8c1bc5135d4c4..fac05588e1e71 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 @@ -26,6 +26,8 @@ export interface Ec2TaskDefinitionProps extends CommonTaskDefinitionProps { /** * 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 = {}) { 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 8ff1efd560d0b..49731e6d26ec5 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 @@ -41,6 +41,7 @@ export interface FargateTaskDefinitionProps extends CommonTaskDefinitionProps { /** * A definition for Tasks on a Fargate cluster + * @resource AWS::ECS::TaskDefinition */ export class FargateTaskDefinition extends TaskDefinition { /** 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 9d638dab7cc33..6ea1fa21d6c38 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 @@ -121,7 +121,7 @@ export class LoadBalancedFargateServiceApplet extends cdk.Stack { } let certificate; if (props.certificate) { - certificate = Certificate.import(this, 'Cert', { certificateArn: props.certificate }); + certificate = Certificate.fromCertificateArn(this, 'Cert', props.certificate); } // Instantiate Fargate Service with just cluster and image diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index 983e908d8d7f3..e91b7fa95f574 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -474,9 +474,7 @@ export = { const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); const mySecretArn = 'arn:aws:secretsmanager:region:1234567890:secret:MyRepoSecret-6f8hj3'; - const repoCreds = secretsmanager.Secret.import(stack, 'MyRepoSecret', { - secretArn: mySecretArn, - }); + const repoCreds = secretsmanager.Secret.fromSecretArn(stack, 'MyRepoSecret', mySecretArn); // WHEN taskDefinition.addContainer('Container', { diff --git a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts index 3fd7b9aea9563..9cdec4cfa9dba 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts @@ -307,7 +307,7 @@ export = { const stack2 = new cdk.Stack(); // WHEN - const cluster2 = ecs.Cluster.import(stack2, 'Cluster', cluster1.export()); + const cluster2 = ecs.Cluster.fromClusterAttributes(stack2, 'Cluster', cluster1.export()); // THEN test.equal(cluster2.defaultNamespace!.type, cloudmap.NamespaceType.DnsPrivate); diff --git a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts index e36a5a2005f9f..bdea16f2e191b 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts @@ -198,7 +198,7 @@ export = { image: ecs.ContainerImage.fromRegistry('test'), domainName: 'api.example.com', domainZone: zone, - certificate: Certificate.import(stack, 'Cert', { certificateArn: 'helloworld' }) + certificate: Certificate.fromCertificateArn(stack, 'Cert', 'helloworld') }); // THEN - stack contains a load balancer and a service diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 6f428a94426f0..8f2c1da02cf1c 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -17,29 +17,33 @@ export interface ICluster extends IResource, ec2.IConnectable { /** * The physical name of the Cluster + * @attribute */ readonly clusterName: string; /** * The unique ARN assigned to the service by AWS * in the form of arn:aws:eks: + * @attribute */ readonly clusterArn: string; /** * The API Server endpoint URL + * @attribute */ readonly clusterEndpoint: string; /** * The certificate-authority-data for your cluster. + * @attribute */ readonly clusterCertificateAuthorityData: string; /** * Export cluster references to use in other stacks */ - export(): ClusterImportProps; + export(): ClusterAttributes; } /** @@ -56,7 +60,7 @@ abstract class ClusterBase extends Resource implements ICluster { /** * Export cluster references to use in other stacks */ - public export(): ClusterImportProps { + public export(): ClusterAttributes { return { vpc: this.vpc.export(), clusterName: this.makeOutput('ClusterNameExport', this.clusterName), @@ -72,7 +76,7 @@ abstract class ClusterBase extends Resource implements ICluster { } } -export interface ClusterImportProps { +export interface ClusterAttributes { /** * The VPC in which this Cluster was created */ @@ -99,7 +103,7 @@ export interface ClusterImportProps { */ readonly clusterCertificateAuthorityData: string; - readonly securityGroups: ec2.SecurityGroupImportProps[]; + readonly securityGroups: ec2.SecurityGroupAttributes[]; } /** @@ -169,10 +173,10 @@ export class Cluster extends ClusterBase { * * @param scope the construct scope, in most cases 'this' * @param id the id or name to import as - * @param props the cluster properties to use for importing information + * @param attrs the cluster properties to use for importing information */ - public static import(scope: Construct, id: string, props: ClusterImportProps): ICluster { - return new ImportedCluster(scope, id, props); + public static fromClusterAttributes(scope: Construct, id: string, attrs: ClusterAttributes): ICluster { + return new ImportedCluster(scope, id, attrs); } /** @@ -409,7 +413,7 @@ class ImportedCluster extends ClusterBase { public readonly clusterEndpoint: string; public readonly connections = new ec2.Connections(); - constructor(scope: Construct, id: string, props: ClusterImportProps) { + constructor(scope: Construct, id: string, props: ClusterAttributes) { super(scope, id); this.vpc = ec2.VpcNetwork.import(this, "VPC", props.vpc); @@ -420,7 +424,7 @@ class ImportedCluster extends ClusterBase { let i = 1; for (const sgProps of props.securityGroups) { - this.connections.addSecurityGroup(ec2.SecurityGroup.import(this, `SecurityGroup${i}`, sgProps)); + this.connections.addSecurityGroup(ec2.SecurityGroup.fromSecurityGroupId(this, `SecurityGroup${i}`, sgProps.securityGroupId)); i++; } } diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index 38f0d8924f547..79421b32b5d0b 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -114,7 +114,7 @@ export = { const cluster = new eks.Cluster(stack1, 'Cluster', { vpc }); // WHEN - const imported = eks.Cluster.import(stack2, 'Imported', cluster.export()); + const imported = eks.Cluster.fromClusterAttributes(stack2, 'Imported', cluster.export()); // THEN test.deepEqual(stack2.node.resolve(imported.clusterArn), { diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts index ef04c650c69ba..515f37e4b3a4b 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts @@ -271,22 +271,44 @@ export class LoadBalancer extends Resource implements IConnectable, codedeploy.I this.newTarget(target); } + /** + * @attribute + */ public get loadBalancerName() { return this.elb.ref; } + /** + * @attribute + */ + public get loadBalancerCanonicalHostedZoneNameId() { + return this.elb.loadBalancerCanonicalHostedZoneNameId; + } + + /** + * @attribute + */ public get loadBalancerCanonicalHostedZoneName() { return this.elb.loadBalancerCanonicalHostedZoneName; } + /** + * @attribute + */ public get loadBalancerDnsName() { return this.elb.loadBalancerDnsName; } + /** + * @attribute + */ public get loadBalancerSourceSecurityGroupGroupName() { return this.elb.loadBalancerSourceSecurityGroupGroupName; } + /** + * @attribute + */ public get loadBalancerSourceSecurityGroupOwnerAlias() { return this.elb.loadBalancerSourceSecurityGroupOwnerAlias; } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index c58b9a6f5a09a..799908d4b4906 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -366,7 +366,7 @@ class ImportedApplicationListener extends cdk.Construct implements IApplicationL const defaultPortRange = props.defaultPort !== undefined ? new ec2.TcpPortFromAttribute(props.defaultPort) : undefined; this.connections = new ec2.Connections({ - securityGroups: [ec2.SecurityGroup.import(this, 'SecurityGroup', { securityGroupId: props.securityGroupId })], + securityGroups: [ec2.SecurityGroup.fromSecurityGroupId(this, 'SecurityGroup', props.securityGroupId)], defaultPortRange, }); } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index f4be21d3b1b84..1c1d5b6b2755d 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -560,7 +560,7 @@ class ImportedApplicationLoadBalancer extends cdk.Construct implements IApplicat this.loadBalancerArn = props.loadBalancerArn; this.connections = new ec2.Connections({ - securityGroups: [ec2.SecurityGroup.import(this, 'SecurityGroup', { securityGroupId: props.securityGroupId })] + securityGroups: [ec2.SecurityGroup.fromSecurityGroupId(this, 'SecurityGroup', props.securityGroupId)] }); } diff --git a/packages/@aws-cdk/aws-events/lib/rule-ref.ts b/packages/@aws-cdk/aws-events/lib/rule-ref.ts index 1472cce90530b..7e9019f55dcbd 100644 --- a/packages/@aws-cdk/aws-events/lib/rule-ref.ts +++ b/packages/@aws-cdk/aws-events/lib/rule-ref.ts @@ -1,6 +1,6 @@ -import { IConstruct } from '@aws-cdk/cdk'; +import { IResource } from '@aws-cdk/cdk'; -export interface EventRuleImportProps { +export interface EventRuleAttributes { /** * The value of the event rule Amazon Resource Name (ARN), such as * arn:aws:events:us-east-2:123456789012:rule/example. @@ -8,15 +8,17 @@ export interface EventRuleImportProps { readonly eventRuleArn: string; } -export interface IEventRule extends IConstruct { +export interface IEventRule extends IResource { /** * The value of the event rule Amazon Resource Name (ARN), such as * arn:aws:events:us-east-2:123456789012:rule/example. + * + * @attribute */ readonly ruleArn: string; /** * Exports this rule resource from this stack and returns an import token. */ - export(): EventRuleImportProps; + export(): EventRuleAttributes; } diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index 7a8da09647a58..5bd65aaac7203 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -1,8 +1,8 @@ -import { CfnOutput, Construct, Token } from '@aws-cdk/cdk'; +import { CfnOutput, Construct, Resource, Token } from '@aws-cdk/cdk'; import { EventPattern } from './event-pattern'; import { CfnRule } from './events.generated'; import { TargetInputTemplate } from './input-options'; -import { EventRuleImportProps, IEventRule } from './rule-ref'; +import { EventRuleAttributes, IEventRule } from './rule-ref'; import { IEventRuleTarget } from './target'; import { mergeEventPattern } from './util'; @@ -62,13 +62,19 @@ export interface EventRuleProps { /** * Defines a CloudWatch Event Rule in this stack. + * + * @resource AWS::Events::Rule */ -export class EventRule extends Construct implements IEventRule { - /** - * Imports a rule by ARN into this stack. - */ - public static import(scope: Construct, id: string, props: EventRuleImportProps): IEventRule { - return new ImportedEventRule(scope, id, props); +export class EventRule extends Resource implements IEventRule { + + public static fromEventRuleArn(scope: Construct, id: string, eventRuleArn: string): IEventRule { + class Import extends Resource implements IEventRule { + public ruleArn = eventRuleArn; + public export(): EventRuleAttributes { + return { eventRuleArn }; + } + } + return new Import(scope, id); } public readonly ruleArn: string; @@ -102,7 +108,7 @@ export class EventRule extends Construct implements IEventRule { /** * Exports this rule resource from this stack and returns an import token. */ - public export(): EventRuleImportProps { + public export(): EventRuleAttributes { return { eventRuleArn: new CfnOutput(this, 'RuleArn', { value: this.ruleArn }).makeImportValue().toString() }; @@ -240,17 +246,3 @@ export class EventRule extends Construct implements IEventRule { return out; } } - -class ImportedEventRule extends Construct implements IEventRule { - public readonly ruleArn: string; - - constructor(scope: Construct, id: string, private readonly props: EventRuleImportProps) { - super(scope, id); - - this.ruleArn = props.eventRuleArn; - } - - public export() { - return this.props; - } -} diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index 54055036f1d8b..b15c1187d5ef1 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -341,10 +341,7 @@ export = { // WHEN const exportedRule = myRule.export(); - - const importedRule = EventRule.import(stack, 'ImportedRule', { - eventRuleArn: 'arn:of:rule' - }); + const importedRule = EventRule.fromEventRuleArn(stack, 'ImportedRule', 'arn:of:rule'); // THEN test.deepEqual(stack.node.resolve(exportedRule), { eventRuleArn: { 'Fn::ImportValue': 'Stack:MyRuleRuleArnDB13ADB1' } }); diff --git a/packages/@aws-cdk/aws-glue/lib/database.ts b/packages/@aws-cdk/aws-glue/lib/database.ts index 2370bdf784f69..8e1fb872128e0 100644 --- a/packages/@aws-cdk/aws-glue/lib/database.ts +++ b/packages/@aws-cdk/aws-glue/lib/database.ts @@ -15,11 +15,15 @@ export interface IDatabase extends IResource { /** * The ARN of the database. + * + * @attribute */ readonly databaseArn: string; /** * The name of the database. + * + * @attribute */ readonly databaseName: string; @@ -28,10 +32,10 @@ export interface IDatabase extends IResource { */ readonly locationUri: string; - export(): DatabaseImportProps; + export(): DatabaseAttributes; } -export interface DatabaseImportProps { +export interface DatabaseAttributes { readonly catalogArn: string; readonly catalogId: string; readonly databaseArn: string; @@ -57,15 +61,53 @@ export interface DatabaseProps { * A Glue database. */ export class Database extends Resource implements IDatabase { + + public static fromDatabaseArn(scope: Construct, id: string, databaseArn: string): IDatabase { + class Import extends Construct implements IDatabase { + public databaseArn = databaseArn; + public databaseName = scope.node.stack.parseArn(databaseArn).resourceName!; + public catalogArn = scope.node.stack.formatArn({ service: 'glue', resource: 'catalog' }); + public catalogId = scope.node.stack.accountId; + + public get locationUri(): string { + throw new Error(`glue.Database.fromDatabaseArn: no "locationUri"`); + } + + public export(): DatabaseAttributes { + return { + catalogArn: this.catalogArn, + catalogId: this.catalogId, + databaseName: this.databaseName, + databaseArn: this.databaseArn, + locationUri: this.locationUri, + }; + } + } + + return new Import(scope, id); + } + /** * Creates a Database construct that represents an external database. * * @param scope The scope creating construct (usually `this`). * @param id The construct's id. - * @param props A `DatabaseImportProps` object. Can be obtained from a call to `database.export()` or manually created. + * @param attrs A `DatabaseAttributes` object. Can be obtained from a call to `database.export()` or manually created. */ - public static import(scope: Construct, id: string, props: DatabaseImportProps): IDatabase { - return new ImportedDatabase(scope, id, props); + public static fromDatabaseAttributes(scope: Construct, id: string, attrs: DatabaseAttributes): IDatabase { + + class Import extends Construct implements IDatabase { + public readonly catalogArn = attrs.catalogArn; + public readonly catalogId = attrs.catalogId; + public readonly databaseArn = attrs.databaseArn; + public readonly databaseName = attrs.databaseName; + public readonly locationUri = attrs.locationUri; + public export() { + return attrs; + } + } + + return new Import(scope, id); } /** @@ -129,7 +171,7 @@ export class Database extends Resource implements IDatabase { /** * Exports this database from the stack. */ - public export(): DatabaseImportProps { + public export(): DatabaseAttributes { return { catalogArn: new CfnOutput(this, 'CatalogArn', { value: this.catalogArn }).makeImportValue().toString(), catalogId: new CfnOutput(this, 'CatalogId', { value: this.catalogId }).makeImportValue().toString(), @@ -139,24 +181,3 @@ export class Database extends Resource implements IDatabase { }; } } - -class ImportedDatabase extends Construct implements IDatabase { - public readonly catalogArn: string; - public readonly catalogId: string; - public readonly databaseArn: string; - public readonly databaseName: string; - public readonly locationUri: string; - - constructor(parent: Construct, name: string, private readonly props: DatabaseImportProps) { - super(parent, name); - this.catalogArn = props.catalogArn; - this.catalogId = props.catalogId; - this.databaseArn = props.databaseArn; - this.databaseName = props.databaseName; - this.locationUri = props.locationUri; - } - - public export() { - return this.props; - } -} diff --git a/packages/@aws-cdk/aws-glue/lib/table.ts b/packages/@aws-cdk/aws-glue/lib/table.ts index e7146df8f1967..07428424b06ed 100644 --- a/packages/@aws-cdk/aws-glue/lib/table.ts +++ b/packages/@aws-cdk/aws-glue/lib/table.ts @@ -1,17 +1,24 @@ import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import s3 = require('@aws-cdk/aws-s3'); -import { CfnOutput, Construct, IResource, Resource } from '@aws-cdk/cdk'; +import { CfnOutput, Construct, Fn, IResource, Resource } from '@aws-cdk/cdk'; import { DataFormat } from './data-format'; import { IDatabase } from './database'; import { CfnTable } from './glue.generated'; import { Column } from './schema'; export interface ITable extends IResource { + /** + * @attribute + */ readonly tableArn: string; + + /** + * @attribute + */ readonly tableName: string; - export(): TableImportProps; + export(): TableAttributes; } /** @@ -49,7 +56,7 @@ export enum TableEncryption { ClientSideKms = 'CSE-KMS' } -export interface TableImportProps { +export interface TableAttributes { readonly tableArn: string; readonly tableName: string; } @@ -143,15 +150,33 @@ export interface TableProps { * A Glue table. */ export class Table extends Resource implements ITable { + + public static fromTableArn(scope: Construct, id: string, tableArn: string): ITable { + const tableName = Fn.select(1, Fn.split('/', scope.node.stack.parseArn(tableArn).resourceName!)); + + return Table.fromTableAttributes(scope, id, { + tableArn, + tableName + }); + } + /** * Creates a Table construct that represents an external table. * * @param scope The scope creating construct (usually `this`). * @param id The construct's id. - * @param props A `TableImportProps` object. Can be obtained from a call to `table.export()` or manually created. + * @param attrs A `TableImportProps` object. Can be obtained from a call to `table.export()` or manually created. */ - public static import(scope: Construct, id: string, props: TableImportProps): ITable { - return new ImportedTable(scope, id, props); + public static fromTableAttributes(scope: Construct, id: string, attrs: TableAttributes): ITable { + class Import extends Construct implements ITable { + public readonly tableArn = attrs.tableArn; + public readonly tableName = attrs.tableName; + public export(): TableAttributes { + return attrs; + } + } + + return new Import(scope, id); } /** @@ -254,7 +279,7 @@ export class Table extends Resource implements ITable { this.tableArn = `${this.database.databaseArn}/${this.tableName}`; } - public export(): TableImportProps { + public export(): TableAttributes { return { tableName: new CfnOutput(this, 'TableName', { value: this.tableName }).makeImportValue().toString(), tableArn: new CfnOutput(this, 'TableArn', { value: this.tableArn }).makeImportValue().toString(), @@ -397,18 +422,3 @@ function renderColumns(columns?: Array) { }; }); } - -class ImportedTable extends Construct implements ITable { - public readonly tableArn: string; - public readonly tableName: string; - - constructor(scope: Construct, id: string, private readonly props: TableImportProps) { - super(scope, id); - this.tableArn = props.tableArn; - this.tableName = props.tableName; - } - - public export(): TableImportProps { - return this.props; - } -} diff --git a/packages/@aws-cdk/aws-glue/test/test.database.ts b/packages/@aws-cdk/aws-glue/test/test.database.ts index fb3e353d6fbfe..29dd8ab4faf7d 100644 --- a/packages/@aws-cdk/aws-glue/test/test.database.ts +++ b/packages/@aws-cdk/aws-glue/test/test.database.ts @@ -82,7 +82,7 @@ export = { databaseName: 'test_database' }); - glue.Database.import(new cdk.Stack(), 'Database', db.export()); + glue.Database.fromDatabaseAttributes(new cdk.Stack(), 'Database', db.export()); expect(stack).toMatch({ Resources: { diff --git a/packages/@aws-cdk/aws-glue/test/test.table.ts b/packages/@aws-cdk/aws-glue/test/test.table.ts index d9933f9a35bc1..4d62c517c71e0 100644 --- a/packages/@aws-cdk/aws-glue/test/test.table.ts +++ b/packages/@aws-cdk/aws-glue/test/test.table.ts @@ -4,7 +4,6 @@ import kms = require('@aws-cdk/aws-kms'); import s3 = require('@aws-cdk/aws-s3'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; - import glue = require('../lib'); export = { @@ -1488,6 +1487,19 @@ export = { })); test.done(); } + }, + + 'Table.fromTableArn'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const table = glue.Table.fromTableArn(stack, 'boom', 'arn:aws:glue:us-east-1:123456789012:table/db1/tbl1'); + + // THEN + test.deepEqual(table.tableArn, 'arn:aws:glue:us-east-1:123456789012:table/db1/tbl1'); + test.deepEqual(table.tableName, 'tbl1'); + test.done(); } }; diff --git a/packages/@aws-cdk/aws-iam/lib/grant.ts b/packages/@aws-cdk/aws-iam/lib/grant.ts index f9eaad56795b7..c025e4e45a1b4 100644 --- a/packages/@aws-cdk/aws-iam/lib/grant.ts +++ b/packages/@aws-cdk/aws-iam/lib/grant.ts @@ -157,6 +157,21 @@ export class Grant { return new Grant({ principalStatement: statement, resourceStatement: result.resourceStatement, options }); } + /** + * Returns a "no-op" `Grant` object which represents a "dropped grant". + * + * This can be used for e.g. imported resources where you may not be able to modify + * the resource's policy or some underlying policy which you don't know about. + * + * @param grantee The intended grantee + * @param _intent The user's intent (will be ignored at the moment) + */ + public static drop(grantee: IGrantable, _intent: string): Grant { + return new Grant({ + options: { grantee, actions: [], resourceArns: [] } + }); + } + /** * The statement that was added to the principal's policy * diff --git a/packages/@aws-cdk/aws-iam/lib/group.ts b/packages/@aws-cdk/aws-iam/lib/group.ts index 671879b54da9d..228321690e3c5 100644 --- a/packages/@aws-cdk/aws-iam/lib/group.ts +++ b/packages/@aws-cdk/aws-iam/lib/group.ts @@ -39,13 +39,16 @@ export interface GroupProps { export class Group extends Resource implements IIdentity { 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; diff --git a/packages/@aws-cdk/aws-iam/lib/lazy-role.ts b/packages/@aws-cdk/aws-iam/lib/lazy-role.ts index 2af11a3acb1f0..a759385c43396 100644 --- a/packages/@aws-cdk/aws-iam/lib/lazy-role.ts +++ b/packages/@aws-cdk/aws-iam/lib/lazy-role.ts @@ -3,7 +3,7 @@ import { Grant } from './grant'; import { Policy } from './policy'; import { PolicyStatement, PrincipalPolicyFragment } from './policy-document'; import { IPrincipal } from './principals'; -import { IRole, Role, RoleImportProps, RoleProps } from './role'; +import { IRole, Role, RoleAttributes, RoleProps } from './role'; // tslint:disable-next-line:no-empty-interface export interface LazyRoleProps extends RoleProps { @@ -31,7 +31,7 @@ export class LazyRole extends cdk.Construct implements IRole { super(scope, id); } - public export(): RoleImportProps { + public export(): RoleAttributes { return this.instantiate().export(); } diff --git a/packages/@aws-cdk/aws-iam/lib/policy.ts b/packages/@aws-cdk/aws-iam/lib/policy.ts index bf433dfb3418e..1f5059b4e1a78 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy.ts @@ -56,6 +56,8 @@ export class Policy extends Resource { /** * The name of this policy. + * + * @attribute */ public readonly policyName: string; diff --git a/packages/@aws-cdk/aws-iam/lib/role.ts b/packages/@aws-cdk/aws-iam/lib/role.ts index cfbbc8f10877d..f1a0ed7644858 100644 --- a/packages/@aws-cdk/aws-iam/lib/role.ts +++ b/packages/@aws-cdk/aws-iam/lib/role.ts @@ -94,11 +94,81 @@ export interface RoleProps { * the specified AWS service principal defined in `serviceAssumeRole`. */ export class Role extends Resource implements IRole { + + /** + * Imports an external role by ARN + * @param scope construct scope + * @param id construct id + * @param roleArn the ARN of the role to import + */ + public static fromRoleArn(scope: Construct, id: string, roleArn: string): IRole { + return Role.fromRoleAttributes(scope, id, { roleArn }); + } + /** * Import a role that already exists */ - public static import(scope: Construct, id: string, props: RoleImportProps): IRole { - return new ImportedRole(scope, id, props); + public static fromRoleAttributes(scope: Construct, id: string, attrs: RoleAttributes): IRole { + + /** + * A role that already exists + */ + class Import extends Construct implements IRole { + public readonly grantPrincipal: IPrincipal = this; + public readonly assumeRoleAction: string = 'sts:AssumeRole'; + public readonly policyFragment = new ArnPrincipal(attrs.roleArn).policyFragment; + public readonly roleArn = attrs.roleArn; + public readonly roleName = scope.node.stack.parseArn(attrs.roleArn).resourceName!; + private readonly _roleId = attrs.roleId; + + public get roleId() { + if (!this._roleId) { + throw new Error(`No roleId specified for imported role`); + } + return this._roleId; + } + + public export(): RoleAttributes { + return { + roleArn: this.roleArn, + roleId: this._roleId + }; + } + + public addToPolicy(_statement: PolicyStatement): boolean { + // Statement will be added to resource instead + return false; + } + + public attachInlinePolicy(_policy: Policy): void { + // FIXME: Add warning that we're ignoring this + } + + public attachManagedPolicy(_arn: string): void { + // FIXME: Add warning that we're ignoring this + } + + /** + * Grant the actions defined in actions to the identity Principal on this resource. + */ + public grant(grantee: IPrincipal, ...actions: string[]): Grant { + return Grant.addToPrincipal({ + grantee, + actions, + resourceArns: [this.roleArn], + scope: this + }); + } + + /** + * Grant permissions to the given principal to pass this role. + */ + public grantPassRole(identity: IPrincipal): Grant { + return this.grant(identity, 'iam:PassRole'); + } + } + + return new Import(scope, id); } public readonly grantPrincipal: IPrincipal = this; @@ -170,7 +240,7 @@ export class Role extends Resource implements IRole { } } - public export(): RoleImportProps { + public export(): RoleAttributes { return { roleArn: new CfnOutput(this, 'RoleArn', { value: this.roleArn }).makeImportValue(), roleId: new CfnOutput(this, 'RoleId', { value: this.roleId }).makeImportValue() @@ -234,24 +304,30 @@ export class Role extends Resource implements IRole { export interface IRole extends IIdentity { /** * Returns the ARN of this role. + * + * @attribute */ readonly roleArn: string; /** * Returns the stable and unique string identifying the role. For example, * AIDAJQABLZS4A3QDU576Q. + * + * @attribute */ readonly roleId: string; /** * Returns the name of this role. + * + * @attribute */ readonly roleName: string; /** * Export this role to another stack. */ - export(): RoleImportProps; + export(): RoleAttributes; /** * Grant the actions defined in actions to the identity Principal on this resource. @@ -290,7 +366,7 @@ function validateMaxSessionDuration(duration?: number) { /** * Properties to import a Role */ -export interface RoleImportProps { +export interface RoleAttributes { /** * The role's ARN */ @@ -304,70 +380,4 @@ export interface RoleImportProps { * `role.roleId` will throw an exception. In most cases, role ID is not really needed. */ readonly roleId?: string; -} - -/** - * A role that already exists - */ -class ImportedRole extends Construct implements IRole { - public readonly grantPrincipal: IPrincipal = this; - public readonly assumeRoleAction: string = 'sts:AssumeRole'; - public readonly policyFragment: PrincipalPolicyFragment; - public readonly roleArn: string; - - private readonly _roleId?: string; - - constructor(scope: Construct, id: string, private readonly props: RoleImportProps) { - super(scope, id); - this.roleArn = props.roleArn; - this._roleId = props.roleId; - this.policyFragment = new ArnPrincipal(this.roleArn).policyFragment; - } - - public get roleId() { - if (!this._roleId) { - throw new Error(`No roleId specified for imported role`); - } - return this._roleId; - } - - public get roleName() { - return this.node.stack.parseArn(this.roleArn).resourceName!; - } - - public export() { - return this.props; - } - - public addToPolicy(_statement: PolicyStatement): boolean { - // Statement will be added to resource instead - return false; - } - - public attachInlinePolicy(_policy: Policy): void { - // FIXME: Add warning that we're ignoring this - } - - public attachManagedPolicy(_arn: string): void { - // FIXME: Add warning that we're ignoring this - } - - /** - * Grant the actions defined in actions to the identity Principal on this resource. - */ - public grant(grantee: IPrincipal, ...actions: string[]): Grant { - return Grant.addToPrincipal({ - grantee, - actions, - resourceArns: [this.roleArn], - scope: this - }); - } - - /** - * Grant permissions to the given principal to pass this role. - */ - public grantPassRole(identity: IPrincipal): Grant { - return this.grant(identity, 'iam:PassRole'); - } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/lib/user.ts b/packages/@aws-cdk/aws-iam/lib/user.ts index 10e8d4a8629b9..08851a6b2f836 100644 --- a/packages/@aws-cdk/aws-iam/lib/user.ts +++ b/packages/@aws-cdk/aws-iam/lib/user.ts @@ -74,11 +74,13 @@ export class User extends Resource implements IIdentity { /** * An attribute that represents the user name. + * @attribute */ public readonly userName: string; /** * An attribute that represents the user's ARN. + * @attribute */ public readonly userArn: string; diff --git a/packages/@aws-cdk/aws-iam/test/test.grant.ts b/packages/@aws-cdk/aws-iam/test/test.grant.ts new file mode 100644 index 0000000000000..942d12f5560d4 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/test.grant.ts @@ -0,0 +1,17 @@ +import { Stack } from '@aws-cdk/cdk'; +import { Test } from 'nodeunit'; +import { Grant } from '../lib'; +import iam = require('../lib'); + +export = { + 'Grant.drop() returns a no-op grant'(test: Test) { + const stack = new Stack(); + const user = new iam.User(stack, 'poo'); + const grant = Grant.drop(user, 'dropping me'); + + test.ok(!grant.success, 'grant should not be successul'); + test.deepEqual(grant.principalStatement, undefined); + test.deepEqual(grant.resourceStatement, undefined); + test.done(); + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/test.role.ts b/packages/@aws-cdk/aws-iam/test/test.role.ts index c8a7a7d266976..2e243a8a9b298 100644 --- a/packages/@aws-cdk/aws-iam/test/test.role.ts +++ b/packages/@aws-cdk/aws-iam/test/test.role.ts @@ -261,7 +261,7 @@ export = { // WHEN const exportedRole = myRole.export(); - const importedRole = Role.import(stack, 'ImportedRole', exportedRole); + const importedRole = Role.fromRoleAttributes(stack, 'ImportedRole', exportedRole); // THEN test.deepEqual(stack.node.resolve(exportedRole), { diff --git a/packages/@aws-cdk/aws-kinesis/lib/stream.ts b/packages/@aws-cdk/aws-kinesis/lib/stream.ts index 737b7051ab451..d7155f5f49f29 100644 --- a/packages/@aws-cdk/aws-kinesis/lib/stream.ts +++ b/packages/@aws-cdk/aws-kinesis/lib/stream.ts @@ -7,11 +7,15 @@ import { CfnStream } from './kinesis.generated'; export interface IStream extends IResource, logs.ILogSubscriptionDestination { /** * The ARN of the stream. + * + * @attribute */ readonly streamArn: string; /** * The name of the stream + * + * @attribute */ readonly streamName: string; @@ -23,7 +27,7 @@ export interface IStream extends IResource, logs.ILogSubscriptionDestination { /** * Exports this stream from the stack. */ - export(): StreamImportProps; + export(): StreamAttributes; /** * Grant read permissions for this stream and its contents to an IAM @@ -58,7 +62,7 @@ export interface IStream extends IResource, logs.ILogSubscriptionDestination { * `stream.export()`. Then, the consumer can use `Stream.import(this, ref)` and * get a `Stream`. */ -export interface StreamImportProps { +export interface StreamAttributes { /** * The ARN of the stream. */ @@ -108,7 +112,7 @@ abstract class StreamBase extends Resource implements IStream { */ private cloudWatchLogsRole?: iam.Role; - public abstract export(): StreamImportProps; + public abstract export(): StreamAttributes; /** * Grant write permissions for this stream and its contents to an IAM @@ -281,15 +285,33 @@ export interface StreamProps { * A Kinesis stream. Can be encrypted with a KMS key. */ export class Stream extends StreamBase { + + public static fromStreamArn(scope: Construct, id: string, streamArn: string): IStream { + return Stream.fromStreamAttributes(scope, id, { streamArn }); + } + /** * Creates a Stream construct that represents an external stream. * * @param scope The parent creating construct (usually `this`). * @param id The construct's name. - * @param props Stream import properties + * @param attrs Stream import properties */ - public static import(scope: Construct, id: string, props: StreamImportProps): IStream { - return new ImportedStream(scope, id, props); + public static fromStreamAttributes(scope: Construct, id: string, attrs: StreamAttributes): IStream { + const encryptionKey = attrs.encryptionKey + ? kms.EncryptionKey.import(scope, 'Key', attrs.encryptionKey) + : undefined; + + class Import extends StreamBase { + public readonly streamArn = attrs.streamArn; + public readonly streamName = scope.node.stack.parseArn(attrs.streamArn).resourceName!; + public readonly encryptionKey = encryptionKey; + public export() { + return attrs; + } + } + + return new Import(scope, id); } public readonly streamArn: string; @@ -325,7 +347,7 @@ export class Stream extends StreamBase { /** * Exports this stream from the stack. */ - public export(): StreamImportProps { + public export(): StreamAttributes { return { streamArn: new CfnOutput(this, 'StreamArn', { value: this.streamArn }).makeImportValue().toString(), encryptionKey: this.encryptionKey ? this.encryptionKey.export() : undefined, @@ -384,29 +406,3 @@ export enum StreamEncryption { */ Kms = 'KMS', } - -class ImportedStream extends StreamBase { - public readonly streamArn: string; - public readonly streamName: string; - public readonly encryptionKey?: kms.IEncryptionKey; - - constructor(scope: Construct, id: string, private readonly props: StreamImportProps) { - super(scope, id); - - this.streamArn = props.streamArn; - - // Get the name from the ARN - this.streamName = this.node.stack.parseArn(props.streamArn).resourceName!; - - if (props.encryptionKey) { - // TODO: import "scope" should be changed to "this" - this.encryptionKey = kms.EncryptionKey.import(scope, 'Key', props.encryptionKey); - } else { - this.encryptionKey = undefined; - } - } - - public export() { - return this.props; - } -} diff --git a/packages/@aws-cdk/aws-kinesis/test/test.stream.ts b/packages/@aws-cdk/aws-kinesis/test/test.stream.ts index 4612cf0f3c14f..eb0de8d1dc7f4 100644 --- a/packages/@aws-cdk/aws-kinesis/test/test.stream.ts +++ b/packages/@aws-cdk/aws-kinesis/test/test.stream.ts @@ -857,7 +857,7 @@ export = { const stackB = new cdk.Stack(); const user = new iam.User(stackB, 'UserWhoNeedsAccess'); - const theStreamFromStackAAsARefInStackB = Stream.import(stackB, 'RefToStreamFromStackA', refToStreamFromStackA); + const theStreamFromStackAAsARefInStackB = Stream.fromStreamAttributes(stackB, 'RefToStreamFromStackA', refToStreamFromStackA); theStreamFromStackAAsARefInStackB.grantRead(user); expect(stackA).toMatch({ @@ -931,7 +931,7 @@ export = { const stackB = new cdk.Stack(); const user = new iam.User(stackB, 'UserWhoNeedsAccess'); - const theStreamFromStackAAsARefInStackB = Stream.import(stackB, 'RefToStreamFromStackA', refToStreamFromStackA); + const theStreamFromStackAAsARefInStackB = Stream.fromStreamAttributes(stackB, 'RefToStreamFromStackA', refToStreamFromStackA); theStreamFromStackAAsARefInStackB.grantRead(user); expect(stackA).toMatch({ diff --git a/packages/@aws-cdk/aws-lambda/lib/alias.ts b/packages/@aws-cdk/aws-lambda/lib/alias.ts index e21b051675c1c..0105a9052381d 100644 --- a/packages/@aws-cdk/aws-lambda/lib/alias.ts +++ b/packages/@aws-cdk/aws-lambda/lib/alias.ts @@ -1,6 +1,6 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import { CfnOutput, Construct } from '@aws-cdk/cdk'; -import { FunctionBase, FunctionImportProps, IFunction } from './function-base'; +import { FunctionAttributes, FunctionBase, IFunction } from './function-base'; import { Version } from './lambda-version'; import { CfnAlias } from './lambda.generated'; @@ -53,6 +53,8 @@ export interface AliasProps { export class Alias extends FunctionBase { /** * Name of this alias. + * + * @attribute */ public readonly aliasName: string; /** @@ -88,7 +90,7 @@ export class Alias extends FunctionBase { name: props.aliasName, description: props.description, functionName: this.underlyingLambda.functionName, - functionVersion: props.version.functionVersion, + functionVersion: props.version.version, routingConfig: this.determineRoutingConfig(props) }); @@ -124,7 +126,7 @@ export class Alias extends FunctionBase { }); } - public export(): FunctionImportProps { + public export(): FunctionAttributes { return { functionArn: new CfnOutput(this, 'AliasArn', { value: this.functionArn }).makeImportValue().toString() }; @@ -143,7 +145,7 @@ export class Alias extends FunctionBase { return { additionalVersionWeights: props.additionalVersions.map(vw => { return { - functionVersion: vw.version.functionVersion, + functionVersion: vw.version.version, functionWeight: vw.weight }; }) diff --git a/packages/@aws-cdk/aws-lambda/lib/function-base.ts b/packages/@aws-cdk/aws-lambda/lib/function-base.ts index 5323614538ee6..12bb51a176a27 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-base.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-base.ts @@ -20,11 +20,15 @@ export interface IFunction extends IResource, logs.ILogSubscriptionDestination, /** * The name of the function. + * + * @attribute */ readonly functionName: string; /** * The ARN fo the function. + * + * @attribute */ readonly functionArn: string; @@ -82,7 +86,7 @@ export interface IFunction extends IResource, logs.ILogSubscriptionDestination, /** * Export this Function (without the role) */ - export(): FunctionImportProps; + export(): FunctionAttributes; addEventSource(source: IEventSource): void; } @@ -90,7 +94,7 @@ export interface IFunction extends IResource, logs.ILogSubscriptionDestination, /** * Represents a Lambda function defined outside of this stack. */ -export interface FunctionImportProps { +export interface FunctionAttributes { /** * The ARN of the Lambda function. * @@ -260,7 +264,7 @@ export abstract class FunctionBase extends Resource implements IFunction { /** * Export this Function (without the role) */ - public abstract export(): FunctionImportProps; + public abstract export(): FunctionAttributes; /** * Allows this Lambda to be used as a destination for bucket notifications. diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index 24cf1e3ceca6d..5f9b69c3938e2 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -6,7 +6,7 @@ import sqs = require('@aws-cdk/aws-sqs'); import { CfnOutput, Construct, Fn, Token } from '@aws-cdk/cdk'; import { Code } from './code'; import { IEventSource } from './event-source'; -import { FunctionBase, FunctionImportProps, IFunction } from './function-base'; +import { FunctionAttributes, FunctionBase, IFunction } from './function-base'; import { Version } from './lambda-version'; import { CfnFunction } from './lambda.generated'; import { ILayerVersion } from './layers'; @@ -223,6 +223,11 @@ export interface FunctionProps { * library. */ export class Function extends FunctionBase { + + public static fromFunctionArn(scope: Construct, id: string, functionArn: string): IFunction { + return Function.fromFunctionAttributes(scope, id, { functionArn }); + } + /** * Creates a Lambda function object which represents a function not defined * within this stack. @@ -231,11 +236,42 @@ export class Function extends FunctionBase { * * @param scope The parent construct * @param id The name of the lambda construct - * @param props A reference to a Lambda function. Can be created manually (see + * @param attrs A reference to a Lambda function. Can be created manually (see * example above) or obtained through a call to `lambda.export()`. */ - public static import(scope: Construct, id: string, props: FunctionImportProps): IFunction { - return new ImportedFunction(scope, id, props); + public static fromFunctionAttributes(scope: Construct, id: string, attrs: FunctionAttributes): IFunction { + const functionArn = attrs.functionArn; + const functionName = extractNameFromArn(attrs.functionArn); + const role = attrs.role; + + class Import extends FunctionBase { + public readonly functionName = functionName; + public readonly functionArn = functionArn; + public readonly role = role; + public readonly grantPrincipal: iam.IPrincipal; + + protected readonly canCreatePermissions = false; + + constructor(s: Construct, i: string) { + super(s, i); + + this.grantPrincipal = role || new iam.ImportedResourcePrincipal({ resource: this } ); + + if (attrs.securityGroupId) { + this._connections = new ec2.Connections({ + securityGroups: [ + ec2.SecurityGroup.fromSecurityGroupId(this, 'SecurityGroup', attrs.securityGroupId) + ] + }); + } + } + + public export() { + return attrs; + } + } + + return new Import(scope, id); } /** @@ -425,7 +461,7 @@ export class Function extends FunctionBase { /** * Export this Function (without the role) */ - public export(): FunctionImportProps { + public export(): FunctionAttributes { return { functionArn: new CfnOutput(this, 'FunctionArn', { value: this.functionArn }).makeImportValue().toString(), securityGroupId: this._connections && this._connections.securityGroups[0] @@ -605,36 +641,6 @@ export class Function extends FunctionBase { } } -class ImportedFunction extends FunctionBase { - public readonly grantPrincipal: iam.IPrincipal; - public readonly functionName: string; - public readonly functionArn: string; - public readonly role?: iam.IRole; - - protected readonly canCreatePermissions = false; - - constructor(scope: Construct, id: string, private readonly props: FunctionImportProps) { - super(scope, id); - - this.functionArn = props.functionArn; - this.functionName = extractNameFromArn(props.functionArn); - this.role = props.role; - this.grantPrincipal = this.role || new iam.ImportedResourcePrincipal({ resource: this } ); - - if (props.securityGroupId) { - this._connections = new ec2.Connections({ - securityGroups: [ - ec2.SecurityGroup.import(this, 'SecurityGroup', { securityGroupId: props.securityGroupId }) - ] - }); - } - } - - public export() { - return this.props; - } -} - /** * Given an opaque (token) ARN, returns a CloudFormation expression that extracts the function * name from the ARN. diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts index 8ecb1526680d6..83499a1f72c7b 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts @@ -47,8 +47,9 @@ export interface VersionProps { export class Version extends Resource { /** * The most recently deployed version of this function. + * @attribute */ - public readonly functionVersion: string; + public readonly version: string; /** * Lambda object this version is associated with @@ -64,7 +65,7 @@ export class Version extends Resource { functionName: props.lambda.functionName }); - this.functionVersion = version.version; + this.version = version.version; this.lambda = props.lambda; } } diff --git a/packages/@aws-cdk/aws-lambda/lib/layers.ts b/packages/@aws-cdk/aws-lambda/lib/layers.ts index 3c694736a937d..6dac11b8d8676 100644 --- a/packages/@aws-cdk/aws-lambda/lib/layers.ts +++ b/packages/@aws-cdk/aws-lambda/lib/layers.ts @@ -38,6 +38,7 @@ export interface LayerVersionProps { export interface ILayerVersion extends IResource { /** * The ARN of the Lambda Layer version that this Layer defines. + * @attribute */ readonly layerVersionArn: string; @@ -50,7 +51,7 @@ export interface ILayerVersion extends IResource { * Exports this layer for use in another Stack. The resulting object can be passed to the ``LayerVersion.import`` * function to obtain an ``ILayerVersion`` in the user stack. */ - export(): LayerVersionImportProps; + export(): LayerVersionAttributes; /** * Grants usage of this layer to specific entities. Usage within the same account where the layer is defined is always @@ -85,7 +86,7 @@ abstract class LayerVersionBase extends Resource implements ILayerVersion { return this; } - public export(): LayerVersionImportProps { + public export(): LayerVersionAttributes { return { layerVersionArn: new CfnOutput(this, 'LayerVersionArn', { value: this.layerVersionArn }).makeImportValue().toString(), compatibleRuntimes: this.compatibleRuntimes, @@ -114,7 +115,7 @@ export interface LayerVersionUsageGrantee { /** * Properties necessary to import a LayerVersion. */ -export interface LayerVersionImportProps { +export interface LayerVersionAttributes { /** * The ARN of the LayerVersion. */ @@ -130,15 +131,35 @@ export interface LayerVersionImportProps { * Defines a new Lambda Layer version. */ export class LayerVersion extends LayerVersionBase { + + /** + * Imports a layer version by ARN. Assumes it is compatible with all Lambda runtimes. + */ + public static fromLayerVersionArn(scope: Construct, id: string, layerVersionArn: string): ILayerVersion { + return LayerVersion.fromLayerVersionAttributes(scope, id, { + layerVersionArn, + compatibleRuntimes: Runtime.All + }); + } + /** * Imports a Layer that has been defined externally. * * @param scope the parent Construct that will use the imported layer. * @param id the id of the imported layer in the construct tree. - * @param props the properties of the imported layer. + * @param attrs the properties of the imported layer. */ - public static import(scope: Construct, id: string, props: LayerVersionImportProps): ILayerVersion { - return new ImportedLayerVersion(scope, id, props); + public static fromLayerVersionAttributes(scope: Construct, id: string, attrs: LayerVersionAttributes): ILayerVersion { + if (attrs.compatibleRuntimes && attrs.compatibleRuntimes.length === 0) { + throw new Error('Attempted to import a Lambda layer that supports no runtime!'); + } + + class Import extends LayerVersionBase { + public readonly layerVersionArn = attrs.layerVersionArn; + public readonly compatibleRuntimes = attrs.compatibleRuntimes; + } + + return new Import(scope, id); } public readonly layerVersionArn: string; @@ -168,22 +189,6 @@ export class LayerVersion extends LayerVersionBase { } } -class ImportedLayerVersion extends LayerVersionBase { - public readonly layerVersionArn: string; - public readonly compatibleRuntimes?: Runtime[]; - - public constructor(parent: Construct, id: string, props: LayerVersionImportProps) { - super(parent, id); - - if (props.compatibleRuntimes && props.compatibleRuntimes.length === 0) { - throw new Error('Attempted to import a Lambda layer that supports no runtime!'); - } - - this.layerVersionArn = props.layerVersionArn; - this.compatibleRuntimes = props.compatibleRuntimes; - } -} - /** * Properties of a Singleton Lambda Layer Version. */ @@ -219,7 +224,7 @@ export class SingletonLayerVersion extends Construct implements ILayerVersion { return this.layerVersion.compatibleRuntimes; } - public export(): LayerVersionImportProps { + public export(): LayerVersionAttributes { return { layerVersionArn: this.layerVersionArn, compatibleRuntimes: this.compatibleRuntimes, diff --git a/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts index fbecb0b0e997e..69929ef56d184 100644 --- a/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts @@ -1,7 +1,7 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Function as LambdaFunction, FunctionProps } from './function'; -import { FunctionBase, FunctionImportProps, IFunction } from './function-base'; +import { FunctionAttributes, FunctionBase, IFunction } from './function-base'; import { Permission } from './permission'; /** @@ -33,6 +33,8 @@ export interface SingletonFunctionProps extends FunctionProps { * * The lambda is identified using the value of 'uuid'. Run 'uuidgen' * for every SingletonLambda you create. + * + * @resource AWS::Lambda::Function */ export class SingletonFunction extends FunctionBase { public readonly grantPrincipal: iam.IPrincipal; @@ -55,7 +57,7 @@ export class SingletonFunction extends FunctionBase { this.canCreatePermissions = true; // Doesn't matter, addPermission is overriden anyway } - public export(): FunctionImportProps { + public export(): FunctionAttributes { return this.lambdaFunction.export(); } diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 9bfe81b8e6704..d4f3f3ee29f00 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -110,7 +110,11 @@ "awslint": { "exclude": [ "construct-base-is-private:@aws-cdk/aws-lambda.FunctionBase", - "grant-result:@aws-cdk/aws-lambda.LayerVersion.grantUsage" + "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/test.alias.ts b/packages/@aws-cdk/aws-lambda/test/test.alias.ts index 1e340495f691b..2ffeb46f6219c 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.alias.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.alias.ts @@ -31,7 +31,7 @@ export = { Type: "AWS::Lambda::Alias", Properties: { FunctionName: { Ref: "MyLambdaCCE802FB" }, - FunctionVersion: stack.node.resolve(version.functionVersion), + FunctionVersion: stack.node.resolve(version.version), Name: "prod" } } @@ -86,11 +86,11 @@ export = { }); expect(stack).to(haveResource('AWS::Lambda::Alias', { - FunctionVersion: stack.node.resolve(version1.functionVersion), + FunctionVersion: stack.node.resolve(version1.version), RoutingConfig: { AdditionalVersionWeights: [ { - FunctionVersion: stack.node.resolve(version2.functionVersion), + FunctionVersion: stack.node.resolve(version2.version), FunctionWeight: 0.1 } ] diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index dca4bb34d5d09..a591078948957 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -245,7 +245,7 @@ export = { // WHEN const props = fn.export(); - const imported = lambda.Function.import(stack2, 'Imported', props); + const imported = lambda.Function.fromFunctionAttributes(stack2, 'Imported', props); // Can call addPermission() but it won't do anything imported.addPermission('Hello', { @@ -1159,7 +1159,7 @@ export = { 'using an incompatible layer'(test: Test) { // GIVEN const stack = new cdk.Stack(undefined, 'TestStack'); - const layer = lambda.LayerVersion.import(stack, 'TestLayer', { + const layer = lambda.LayerVersion.fromLayerVersionAttributes(stack, 'TestLayer', { layerVersionArn: 'arn:aws:...', compatibleRuntimes: [lambda.Runtime.NodeJS810], }); @@ -1179,7 +1179,7 @@ export = { 'using more than 5 layers'(test: Test) { // GIVEN const stack = new cdk.Stack(undefined, 'TestStack'); - const layers = new Array(6).fill(lambda.LayerVersion.import(stack, 'TestLayer', { + const layers = new Array(6).fill(lambda.LayerVersion.fromLayerVersionAttributes(stack, 'TestLayer', { layerVersionArn: 'arn:aws:...', compatibleRuntimes: [lambda.Runtime.NodeJS810], })); diff --git a/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts index b069c11a8d2cc..fb92bbe244c39 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts @@ -81,7 +81,7 @@ export = { const somethingConnectable = new SomethingConnectable(new ec2.Connections({ securityGroups: [securityGroup] })); // WHEN - const importedLambda = lambda.Function.import(stack2, 'Lambda', this.lambda.export()); + const importedLambda = lambda.Function.fromFunctionAttributes(stack2, 'Lambda', this.lambda.export()); importedLambda.connections.allowTo(somethingConnectable, new ec2.TcpAllPorts(), 'Lambda can call connectable'); // THEN: SomeSecurityGroup accepts connections from Lambda diff --git a/packages/@aws-cdk/aws-logs/lib/log-group.ts b/packages/@aws-cdk/aws-logs/lib/log-group.ts index 9637167da345a..9aebfa8feaf3a 100644 --- a/packages/@aws-cdk/aws-logs/lib/log-group.ts +++ b/packages/@aws-cdk/aws-logs/lib/log-group.ts @@ -1,6 +1,6 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import iam = require('@aws-cdk/aws-iam'); -import { applyRemovalPolicy, CfnOutput, Construct, IResource, RemovalPolicy, Resource } from '@aws-cdk/cdk'; +import { applyRemovalPolicy, Construct, IResource, RemovalPolicy, Resource } from '@aws-cdk/cdk'; import { LogStream } from './log-stream'; import { CfnLogGroup } from './logs.generated'; import { MetricFilter } from './metric-filter'; @@ -10,11 +10,13 @@ import { ILogSubscriptionDestination, SubscriptionFilter } from './subscription- export interface ILogGroup extends IResource { /** * The ARN of this log group + * @attribute */ readonly logGroupArn: string; /** * The name of this log group + * @attribute */ readonly logGroupName: string; @@ -45,11 +47,6 @@ export interface ILogGroup extends IResource { */ newMetricFilter(scope: Construct, id: string, props: NewMetricFilterProps): MetricFilter; - /** - * Export this LogGroup - */ - export(): LogGroupImportProps; - /** * Extract a metric from structured log events in the LogGroup * @@ -77,13 +74,6 @@ export interface ILogGroup extends IResource { grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; } -/** - * Properties for importing a LogGroup - */ -export interface LogGroupImportProps { - readonly logGroupArn: string; -} - /** * An CloudWatch Log Group */ @@ -140,8 +130,6 @@ abstract class LogGroupBase extends Resource implements ILogGroup { }); } - public abstract export(): LogGroupImportProps; - /** * Extract a metric from structured log events in the LogGroup * @@ -320,8 +308,13 @@ export class LogGroup extends LogGroupBase { /** * Import an existing LogGroup */ - public static import(scope: Construct, id: string, props: LogGroupImportProps): ILogGroup { - return new ImportedLogGroup(scope, id, props); + public static fromLogGroupArn(scope: Construct, id: string, logGroupArn: string): ILogGroup { + class Import extends LogGroupBase { + public readonly logGroupArn = logGroupArn; + public readonly logGroupName = scope.node.stack.parseArn(logGroupArn, ':').resourceName!; + } + + return new Import(scope, id); } /** @@ -357,44 +350,6 @@ export class LogGroup extends LogGroupBase { this.logGroupArn = resource.logGroupArn; this.logGroupName = resource.logGroupName; } - - /** - * Export this LogGroup - */ - public export(): LogGroupImportProps { - return { - logGroupArn: new CfnOutput(this, 'LogGroupArn', { value: this.logGroupArn }).makeImportValue().toString() - }; - } -} - -/** - * An imported CloudWatch Log Group - */ -class ImportedLogGroup extends LogGroupBase { - /** - * The ARN of this log group - */ - public readonly logGroupArn: string; - - /** - * The name of this log group - */ - public readonly logGroupName: string; - - constructor(scope: Construct, id: string, private readonly props: LogGroupImportProps) { - super(scope, id); - - this.logGroupArn = props.logGroupArn; - this.logGroupName = this.node.stack.parseArn(props.logGroupArn, ':').resourceName!; - } - - /** - * Export this LogGroup - */ - public export() { - return this.props; - } } /** diff --git a/packages/@aws-cdk/aws-logs/lib/log-stream.ts b/packages/@aws-cdk/aws-logs/lib/log-stream.ts index aecf66b7fbc2c..687acb2787bf5 100644 --- a/packages/@aws-cdk/aws-logs/lib/log-stream.ts +++ b/packages/@aws-cdk/aws-logs/lib/log-stream.ts @@ -1,24 +1,13 @@ -import { CfnOutput, Construct, DeletionPolicy, IResource, Resource } from '@aws-cdk/cdk'; +import { Construct, DeletionPolicy, IResource, Resource } from '@aws-cdk/cdk'; import { ILogGroup } from './log-group'; import { CfnLogStream } from './logs.generated'; export interface ILogStream extends IResource { /** * The name of this log stream + * @attribute */ readonly logStreamName: string; - - /** - * Export this LogStream - */ - export(): LogStreamImportProps; -} - -/** - * Properties for importing a LogStream - */ -export interface LogStreamImportProps { - readonly logStreamName: string; } /** @@ -60,8 +49,12 @@ export class LogStream extends Resource implements ILogStream { /** * Import an existing LogGroup */ - public static import(scope: Construct, id: string, props: LogStreamImportProps): ILogStream { - return new ImportedLogStream(scope, id, props); + public static fromLogStreamName(scope: Construct, id: string, logStreamName: string): ILogStream { + class Import extends Construct implements ILogStream { + public readonly logStreamName = logStreamName; + } + + return new Import(scope, id); } /** @@ -83,33 +76,4 @@ export class LogStream extends Resource implements ILogStream { this.logStreamName = resource.logStreamName; } - - /** - * Export this LogStream - */ - public export(): LogStreamImportProps { - return { - logStreamName: new CfnOutput(this, 'LogStreamName', { value: this.logStreamName }).makeImportValue().toString() - }; - } -} - -/** - * An imported LogStream - */ -class ImportedLogStream extends Construct implements ILogStream { - /** - * The name of this log stream - */ - public readonly logStreamName: string; - - constructor(scope: Construct, id: string, private readonly props: LogStreamImportProps) { - super(scope, id); - - this.logStreamName = props.logStreamName; - } - - public export() { - return this.props; - } } diff --git a/packages/@aws-cdk/aws-logs/test/test.loggroup.ts b/packages/@aws-cdk/aws-logs/test/test.loggroup.ts index 418197a35839f..16d13706284d0 100644 --- a/packages/@aws-cdk/aws-logs/test/test.loggroup.ts +++ b/packages/@aws-cdk/aws-logs/test/test.loggroup.ts @@ -81,17 +81,16 @@ export = { 'export/import'(test: Test) { // GIVEN - const stack1 = new Stack(); - const lg = new LogGroup(stack1, 'LogGroup'); const stack2 = new Stack(); // WHEN - const imported = LogGroup.import(stack2, 'Import', lg.export()); + const imported = LogGroup.fromLogGroupArn(stack2, 'lg', 'arn:aws:logs:us-east-1:123456789012:log-group:my-log-group'); imported.newStream(stack2, 'MakeMeAStream'); // THEN - expect(stack2).to(haveResource('AWS::Logs::LogStream', {})); - + expect(stack2).to(haveResource('AWS::Logs::LogStream', { + LogGroupName: "my-log-group" + })); test.done(); }, diff --git a/packages/@aws-cdk/aws-quickstarts/lib/rdgw.ts b/packages/@aws-cdk/aws-quickstarts/lib/rdgw.ts index ae4912f7beb12..81ee0f8d36e81 100644 --- a/packages/@aws-cdk/aws-quickstarts/lib/rdgw.ts +++ b/packages/@aws-cdk/aws-quickstarts/lib/rdgw.ts @@ -47,9 +47,8 @@ export class RemoteDesktopGateway extends cdk.Construct implements ec2.IConnecta parameters: params }); - const securityGroup = ec2.SecurityGroup.import(this, 'SecurityGroup', { - securityGroupId: nestedStack.getAtt('Outputs.RemoteDesktopGatewaySGID').toString() - }); + const securityGroup = ec2.SecurityGroup.fromSecurityGroupId(this, 'SecurityGroup', + nestedStack.getAtt('Outputs.RemoteDesktopGatewaySGID').toString()); const defaultPortRange = new ec2.TcpPort(RemoteDesktopGateway.PORT); this.connections = new ec2.Connections({ securityGroups: [securityGroup], defaultPortRange }); diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 7a34ec936dc1a..0b68e29355e8d 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -437,7 +437,7 @@ class ImportedDatabaseCluster extends DatabaseClusterBase implements IDatabaseCl this.securityGroupId = props.securityGroupId; this.defaultPortRange = new ec2.TcpPortFromAttribute(props.port); this.connections = new ec2.Connections({ - securityGroups: [ec2.SecurityGroup.import(this, 'SecurityGroup', props)], + securityGroups: [ec2.SecurityGroup.fromSecurityGroupId(this, 'SecurityGroup', props.securityGroupId)], defaultPortRange: this.defaultPortRange }); this.clusterIdentifier = props.clusterIdentifier; diff --git a/packages/@aws-cdk/aws-rds/lib/database-secret.ts b/packages/@aws-cdk/aws-rds/lib/database-secret.ts index 875801c6695b5..23cdef86fd163 100644 --- a/packages/@aws-cdk/aws-rds/lib/database-secret.ts +++ b/packages/@aws-cdk/aws-rds/lib/database-secret.ts @@ -21,6 +21,8 @@ export interface DatabaseSecretProps { /** * A database secret. + * + * @resource AWS::SecretsManager::Secret */ export class DatabaseSecret extends secretsmanager.Secret { constructor(scope: cdk.Construct, id: string, props: DatabaseSecretProps) { diff --git a/packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts b/packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts index 357e9504a0fd2..6ff5dd4dd0e93 100644 --- a/packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts +++ b/packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts @@ -154,14 +154,12 @@ export class RotationSingleUser extends cdk.Construct { }); // Dummy import to reference this function in the rotation schedule - const rotationLambda = lambda.Function.import(this, 'RotationLambda', { - functionArn: this.node.stack.formatArn({ - service: 'lambda', - resource: 'function', - sep: ':', - resourceName: rotationFunctionName - }), - }); + const rotationLambda = lambda.Function.fromFunctionArn(this, 'RotationLambda', this.node.stack.formatArn({ + service: 'lambda', + resource: 'function', + sep: ':', + resourceName: rotationFunctionName + })); // Cannot use rotationLambda.addPermission because it currently does not // return a cdk.Construct and we need to add a dependency. diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index 124efb19cf9bb..de8557f34a865 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -106,9 +106,7 @@ export = { const vpc = ec2.VpcNetwork.importFromContext(stack, 'VPC', { vpcId: "VPC12345" }); - const sg = ec2.SecurityGroup.import(stack, 'SG', { - securityGroupId: "SecurityGroupId12345" - }); + const sg = ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', "SecurityGroupId12345"); // WHEN new DatabaseCluster(stack, 'Database', { diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone-provider.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone-provider.ts index 53f88d27d8b6a..0bf6f903d8523 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone-provider.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone-provider.ts @@ -1,7 +1,7 @@ import cdk = require('@aws-cdk/cdk'); import cxapi = require('@aws-cdk/cx-api'); import { HostedZone } from './hosted-zone'; -import { HostedZoneImportProps, IHostedZone } from './hosted-zone-ref'; +import { HostedZoneAttributes, IHostedZone } from './hosted-zone-ref'; /** * Zone properties for looking up the Hosted Zone @@ -41,12 +41,12 @@ export class HostedZoneProvider { * This method calls `findHostedZone` and returns the imported hosted zone */ public findAndImport(scope: cdk.Construct, id: string): IHostedZone { - return HostedZone.import(scope, id, this.findHostedZone()); + return HostedZone.fromHostedZoneAttributes(scope, id, this.findHostedZone()); } /** * Return the hosted zone meeting the filter */ - public findHostedZone(): HostedZoneImportProps { + public findHostedZone(): HostedZoneAttributes { const zone = this.provider.getValue(DEFAULT_HOSTED_ZONE) as HostedZoneContextResponse; // CDK handles the '.' at the end, so remove it here if (zone.Name.endsWith('.')) { diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone-ref.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone-ref.ts index db31b7f885f00..1b006a7f8e7a5 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone-ref.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone-ref.ts @@ -6,6 +6,8 @@ import { IResource } from '@aws-cdk/cdk'; export interface IHostedZone extends IResource { /** * ID of this hosted zone, such as "Z23ABC4XYZL05B" + * + * @attribute */ readonly hostedZoneId: string; @@ -19,19 +21,21 @@ export interface IHostedZone extends IResource { * ns1.example.com. * * This attribute will be undefined for private hosted zones or hosted zones imported from another stack. + * + * @attribute */ readonly hostedZoneNameServers?: string[]; /** * Export the hosted zone */ - export(): HostedZoneImportProps; + export(): HostedZoneAttributes; } /** * Reference to a hosted zone */ -export interface HostedZoneImportProps { +export interface HostedZoneAttributes { /** * Identifier of the hosted zone */ diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts index 58990e3b912cf..c83b41982dd43 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts @@ -1,6 +1,6 @@ import ec2 = require('@aws-cdk/aws-ec2'); import { CfnOutput, Construct, Resource, Token } from '@aws-cdk/cdk'; -import { HostedZoneImportProps, IHostedZone } from './hosted-zone-ref'; +import { HostedZoneAttributes, IHostedZone } from './hosted-zone-ref'; import { ZoneDelegationRecord } from './records'; import { CfnHostedZone } from './route53.generated'; import { validateZoneName } from './util'; @@ -43,11 +43,37 @@ export interface HostedZoneProps extends CommonHostedZoneProps { } export class HostedZone extends Resource implements IHostedZone { + + public static fromHostedZoneId(scope: Construct, id: string, hostedZoneId: string): IHostedZone { + class Import extends Construct implements IHostedZone { + public readonly hostedZoneId = hostedZoneId; + public get zoneName(): string { + throw new Error(`HostedZone.fromHostedZoneId doesn't support "zoneName"`); + } + public export(): HostedZoneAttributes { + return { + hostedZoneId: this.hostedZoneId, + zoneName: this.zoneName + }; + } + } + + return new Import(scope, id); + } + /** * Imports a hosted zone from another stack. */ - public static import(scope: Construct, id: string, props: HostedZoneImportProps): IHostedZone { - return new ImportedHostedZone(scope, id, props); + public static fromHostedZoneAttributes(scope: Construct, id: string, attrs: HostedZoneAttributes): IHostedZone { + class Import extends Construct implements IHostedZone { + public readonly hostedZoneId = attrs.hostedZoneId; + public readonly zoneName = attrs.zoneName; + public export() { + return attrs; + } + } + + return new Import(scope, id); } public readonly hostedZoneId: string; @@ -64,15 +90,15 @@ export class HostedZone extends Resource implements IHostedZone { validateZoneName(props.zoneName); - const hostedZone = new CfnHostedZone(this, 'Resource', { + const resource = new CfnHostedZone(this, 'Resource', { name: props.zoneName + '.', hostedZoneConfig: props.comment ? { comment: props.comment } : undefined, queryLoggingConfig: props.queryLogsLogGroupArn ? { cloudWatchLogsLogGroupArn: props.queryLogsLogGroupArn } : undefined, vpcs: new Token(() => this.vpcs.length === 0 ? undefined : this.vpcs) }); - this.hostedZoneId = hostedZone.ref; - this.hostedZoneNameServers = hostedZone.hostedZoneNameServers; + this.hostedZoneId = resource.ref; + this.hostedZoneNameServers = resource.hostedZoneNameServers; this.zoneName = props.zoneName; for (const vpc of props.vpcs || []) { @@ -80,7 +106,7 @@ export class HostedZone extends Resource implements IHostedZone { } } - public export(): HostedZoneImportProps { + public export(): HostedZoneAttributes { return { hostedZoneId: new CfnOutput(this, 'HostedZoneId', { value: this.hostedZoneId }).makeImportValue(), zoneName: this.zoneName, @@ -97,15 +123,30 @@ export class HostedZone extends Resource implements IHostedZone { } } -// tslint:disable-next-line:no-empty-interface -export interface PublicHostedZoneProps extends CommonHostedZoneProps { - -} +export interface PublicHostedZoneProps extends CommonHostedZoneProps { } +export interface IPublicHostedZone extends IHostedZone { } /** * Create a Route53 public hosted zone. + * + * @resource AWS::Route53::HostedZone */ -export class PublicHostedZone extends HostedZone { +export class PublicHostedZone extends HostedZone implements IPublicHostedZone { + + public static fromPublicHostedZoneId(scope: Construct, id: string, publicHostedZoneId: string): IPublicHostedZone { + class Import extends Resource implements IPublicHostedZone { + public readonly hostedZoneId = publicHostedZoneId; + public get zoneName(): string { throw new Error(`cannot retrieve "zoneName" from an an imported hosted zone`); } + public export(): HostedZoneAttributes { + return { + hostedZoneId: this.hostedZoneId, + zoneName: this.zoneName + }; + } + } + return new Import(scope, id); + } + constructor(scope: Construct, id: string, props: PublicHostedZoneProps) { super(scope, id, props); } @@ -160,35 +201,35 @@ export interface PrivateHostedZoneProps extends CommonHostedZoneProps { readonly vpc: ec2.IVpcNetwork; } +export interface IPrivateHostedZone extends IHostedZone {} + /** * Create a Route53 private hosted zone for use in one or more VPCs. * * Note that `enableDnsHostnames` and `enableDnsSupport` must have been enabled * for the VPC you're configuring for private hosted zones. + * + * @resource AWS::Route53::HostedZone */ -export class PrivateHostedZone extends HostedZone { +export class PrivateHostedZone extends HostedZone implements IPrivateHostedZone { + + public static fromPrivateHostedZoneId(scope: Construct, id: string, privateHostedZoneId: string): IPrivateHostedZone { + class Import extends Resource implements IPrivateHostedZone { + public readonly hostedZoneId = privateHostedZoneId; + public get zoneName(): string { throw new Error(`cannot retrieve "zoneName" from an an imported hosted zone`); } + public export(): HostedZoneAttributes { + return { + hostedZoneId: this.hostedZoneId, + zoneName: this.zoneName + }; + } + } + return new Import(scope, id); + } + constructor(scope: Construct, id: string, props: PrivateHostedZoneProps) { super(scope, id, props); this.addVpc(props.vpc); } } - -/** - * Imported hosted zone - */ -class ImportedHostedZone extends Construct implements IHostedZone { - public readonly hostedZoneId: string; - public readonly zoneName: string; - - constructor(scope: Construct, name: string, private readonly props: HostedZoneImportProps) { - super(scope, name); - - this.hostedZoneId = props.hostedZoneId; - this.zoneName = props.zoneName; - } - - public export() { - return this.props; - } -} diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 58fffdc9149ab..a14c151b80f0d 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -80,5 +80,11 @@ }, "engines": { "node": ">= 8.10.0" + }, + "awslint": { + "exclude": [ + "from-attributes:fromPrivateHostedZoneAttributes", + "from-attributes:fromPublicHostedZoneAttributes" + ] } } diff --git a/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts b/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts index e7dd3d2d09b06..2a2e8e8bc5348 100644 --- a/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts +++ b/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts @@ -1,6 +1,6 @@ import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; -import { HostedZone, HostedZoneImportProps, HostedZoneProvider } from '../lib'; +import { HostedZone, HostedZoneAttributes, HostedZoneProvider } from '../lib'; export = { 'Hosted Zone Provider': { @@ -24,12 +24,12 @@ export = { stack.node.setContext(key, fakeZone); - const cdkZoneProps: HostedZoneImportProps = { + const cdkZoneProps: HostedZoneAttributes = { hostedZoneId: fakeZone.Id, zoneName: 'example.com', }; - const cdkZone = HostedZone.import(stack, 'MyZone', cdkZoneProps); + const cdkZone = HostedZone.fromHostedZoneAttributes(stack, 'MyZone', cdkZoneProps); // WHEN const provider = new HostedZoneProvider(stack, filter); diff --git a/packages/@aws-cdk/aws-route53/test/test.route53.ts b/packages/@aws-cdk/aws-route53/test/test.route53.ts index 76481eda5b413..f79bf07177a04 100644 --- a/packages/@aws-cdk/aws-route53/test/test.route53.ts +++ b/packages/@aws-cdk/aws-route53/test/test.route53.ts @@ -78,7 +78,7 @@ export = { }); const zoneRef = zone.export(); - const importedZone = HostedZone.import(stack2, 'Imported', zoneRef); + const importedZone = HostedZone.fromHostedZoneAttributes(stack2, 'Imported', zoneRef); new TxtRecord(importedZone as any, 'Record', { zone: importedZone, recordName: 'lookHere', diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 2b12aba650a36..df3eaacf40ba6 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -14,26 +14,37 @@ import { parseBucketArn, parseBucketName } from './util'; export interface IBucket extends IResource { /** * The ARN of the bucket. + * @attribute */ readonly bucketArn: string; /** * The name of the bucket. + * @attribute */ readonly bucketName: string; + /** + * The URL of the static website. + * @attribute + */ + readonly bucketWebsiteUrl: string; + /** * The IPv4 DNS name of the specified bucket. + * @attribute */ readonly bucketDomainName: string; /** * The IPv6 DNS name of the specified bucket. + * @attribute */ readonly bucketDualStackDomainName: string; /** * The regional domain name of the specified bucket. + * @attribute */ readonly bucketRegionalDomainName: string; @@ -42,13 +53,6 @@ export interface IBucket extends IResource { */ readonly encryptionKey?: kms.IEncryptionKey; - /** - * The https:// URL of this bucket. - * @example https://s3.us-west-1.amazonaws.com/onlybucket - * Similar to calling `urlForObject` with no object key. - */ - readonly bucketUrl: string; - /** * The resource policy assoicated with this bucket. * @@ -60,7 +64,7 @@ export interface IBucket extends IResource { /** * Exports this bucket from the stack. */ - export(): BucketImportProps; + export(): BucketAttributes; /** * Adds a statement to the resource policy for a principal (i.e. @@ -187,7 +191,7 @@ export interface IBucket extends IResource { * `bucket.export()`. Then, the consumer can use `Bucket.import(this, ref)` and * get a `Bucket`. */ -export interface BucketImportProps { +export interface BucketAttributes { /** * The ARN of the bucket. At least one of bucketArn or bucketName must be * defined in order to initialize a bucket ref. @@ -256,6 +260,7 @@ abstract class BucketBase extends Resource implements IBucket { public abstract readonly bucketArn: string; public abstract readonly bucketName: string; public abstract readonly bucketDomainName: string; + public abstract readonly bucketWebsiteUrl: string; public abstract readonly bucketRegionalDomainName: string; public abstract readonly bucketDualStackDomainName: string; @@ -286,7 +291,7 @@ abstract class BucketBase extends Resource implements IBucket { /** * Exports this bucket from the stack. */ - public abstract export(): BucketImportProps; + public abstract export(): BucketAttributes; public onPutObject(name: string, target?: events.IEventRuleTarget, path?: string): events.EventRule { const eventRule = new events.EventRule(this, name, { @@ -332,15 +337,6 @@ abstract class BucketBase extends Resource implements IBucket { } } - /** - * The https:// URL of this bucket. - * @example https://s3.us-west-1.amazonaws.com/onlybucket - * Similar to calling `urlForObject` with no object key. - */ - public get bucketUrl() { - return this.urlForObject(); - } - /** * The https URL of an S3 object. For example: * @example https://s3.us-west-1.amazonaws.com/onlybucket @@ -652,26 +648,35 @@ export interface BucketProps { * BucketResource. */ export class Bucket extends BucketBase { + + public static fromBucketArn(scope: Construct, id: string, bucketArn: string): IBucket { + return Bucket.fromBucketAttributes(scope, id, { bucketArn }); + } + + public static fromBucketName(scope: Construct, id: string, bucketName: string): IBucket { + return Bucket.fromBucketAttributes(scope, id, { bucketName }); + } + /** * Creates a Bucket construct that represents an external bucket. * * @param scope The parent creating construct (usually `this`). * @param id The construct's name. - * @param props A `BucketAttributes` object. Can be obtained from a call to + * @param attrs A `BucketAttributes` object. Can be obtained from a call to * `bucket.export()` or manually created. */ - public static import(scope: Construct, id: string, props: BucketImportProps): IBucket { + public static fromBucketAttributes(scope: Construct, id: string, attrs: BucketAttributes): IBucket { const region = scope.node.stack.region; const urlSuffix = scope.node.stack.urlSuffix; - const bucketName = parseBucketName(scope, props); + const bucketName = parseBucketName(scope, attrs); if (!bucketName) { throw new Error('Bucket name is required'); } - const newUrlFormat = props.bucketWebsiteNewUrlFormat === undefined + const newUrlFormat = attrs.bucketWebsiteNewUrlFormat === undefined ? false - : props.bucketWebsiteNewUrlFormat; + : attrs.bucketWebsiteNewUrlFormat; const websiteUrl = newUrlFormat ? `${bucketName}.s3-website.${region}.${urlSuffix}` @@ -679,11 +684,11 @@ export class Bucket extends BucketBase { class Import extends BucketBase { public readonly bucketName = bucketName!; - public readonly bucketArn = parseBucketArn(scope, props); - public readonly bucketDomainName = props.bucketDomainName || `${bucketName}.s3.${urlSuffix}`; - public readonly bucketWebsiteUrl = props.bucketWebsiteUrl || websiteUrl; - public readonly bucketRegionalDomainName = props.bucketRegionalDomainName || `${bucketName}.s3.${region}.${urlSuffix}`; - public readonly bucketDualStackDomainName = props.bucketDualStackDomainName || `${bucketName}.s3.dualstack.${region}.${urlSuffix}`; + public readonly bucketArn = parseBucketArn(scope, attrs); + public readonly bucketDomainName = attrs.bucketDomainName || `${bucketName}.s3.${urlSuffix}`; + public readonly bucketWebsiteUrl = attrs.bucketWebsiteUrl || websiteUrl; + public readonly bucketRegionalDomainName = attrs.bucketRegionalDomainName || `${bucketName}.s3.${region}.${urlSuffix}`; + public readonly bucketDualStackDomainName = attrs.bucketDualStackDomainName || `${bucketName}.s3.dualstack.${region}.${urlSuffix}`; public readonly bucketWebsiteNewUrlFormat = newUrlFormat; public readonly encryptionKey?: kms.EncryptionKey; public policy?: BucketPolicy = undefined; @@ -694,7 +699,7 @@ export class Bucket extends BucketBase { * Exports this bucket from the stack. */ public export() { - return props; + return attrs; } } @@ -762,7 +767,7 @@ export class Bucket extends BucketBase { /** * Exports this bucket from the stack. */ - public export(): BucketImportProps { + public export(): BucketAttributes { return { bucketArn: new CfnOutput(this, 'BucketArn', { value: this.bucketArn }).makeImportValue().toString(), bucketName: new CfnOutput(this, 'BucketName', { value: this.bucketName }).makeImportValue().toString(), diff --git a/packages/@aws-cdk/aws-s3/lib/util.ts b/packages/@aws-cdk/aws-s3/lib/util.ts index 1af6468e8fc25..5b0674b3a8387 100644 --- a/packages/@aws-cdk/aws-s3/lib/util.ts +++ b/packages/@aws-cdk/aws-s3/lib/util.ts @@ -1,7 +1,7 @@ import cdk = require('@aws-cdk/cdk'); -import { BucketImportProps } from './bucket'; +import { BucketAttributes } from './bucket'; -export function parseBucketArn(construct: cdk.IConstruct, props: BucketImportProps): string { +export function parseBucketArn(construct: cdk.IConstruct, props: BucketAttributes): string { // if we have an explicit bucket ARN, use it. if (props.bucketArn) { @@ -22,7 +22,7 @@ export function parseBucketArn(construct: cdk.IConstruct, props: BucketImportPro throw new Error('Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed'); } -export function parseBucketName(construct: cdk.IConstruct, props: BucketImportProps): string | undefined { +export function parseBucketName(construct: cdk.IConstruct, props: BucketAttributes): string | undefined { // if we have an explicit bucket name, use it. if (props.bucketName) { diff --git a/packages/@aws-cdk/aws-s3/test/demo.import-export.ts b/packages/@aws-cdk/aws-s3/test/demo.import-export.ts index 07a2778e988dc..4cd40c719266f 100644 --- a/packages/@aws-cdk/aws-s3/test/demo.import-export.ts +++ b/packages/@aws-cdk/aws-s3/test/demo.import-export.ts @@ -7,7 +7,7 @@ import s3 = require('../lib'); // `Bucket.import`. class Producer extends cdk.Stack { - public readonly myBucketRef: s3.BucketImportProps; + public readonly myBucketRef: s3.BucketAttributes; constructor(scope: cdk.App, id: string) { super(scope, id); @@ -35,7 +35,7 @@ class ConsumerConstruct extends cdk.Construct { // this bucket and contents. interface ConsumerProps { - userBucketRef: s3.BucketImportProps; + userBucketRef: s3.BucketAttributes; } class Consumer extends cdk.Stack { @@ -43,7 +43,7 @@ class Consumer extends cdk.Stack { super(scope, id); const user = new iam.User(this, 'MyUser'); - const userBucket = s3.Bucket.import(this, 'ImportBucket', props.userBucketRef); + const userBucket = s3.Bucket.fromBucketAttributes(this, 'ImportBucket', props.userBucketRef); new ConsumerConstruct(this, 'SomeConstruct', { bucket: userBucket }); diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket.domain-name.ts b/packages/@aws-cdk/aws-s3/test/integ.bucket.domain-name.ts index f4c53d16039f4..73fa1f408e891 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket.domain-name.ts +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket.domain-name.ts @@ -9,7 +9,7 @@ class TestStack extends cdk.Stack { const bucket = new s3.Bucket(this, 'MyBucket', { removalPolicy: cdk.RemovalPolicy.Destroy }); - const bucket2 = s3.Bucket.import(this, "MyBucket2", { + const bucket2 = s3.Bucket.fromBucketAttributes(this, "MyBucket2", { bucketArn: "arn:aws:s3:::my-bucket-test" }); diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json index 6bf988af59b89..0ed62b177dc05 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json @@ -7,22 +7,9 @@ "Outputs": { "BucketURL": { "Value": { - "Fn::Join": [ - "", - [ - "https://s3.", - { - "Ref": "AWS::Region" - }, - ".", - { - "Ref": "AWS::URLSuffix" - }, - "/", - { - "Ref": "MyBucketF68F3FF0" - } - ] + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "WebsiteURL" ] } }, @@ -49,4 +36,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.ts b/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.ts index 7f5e0b961bfaa..32928a0083537 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.ts +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.ts @@ -10,7 +10,7 @@ class TestStack extends cdk.Stack { removalPolicy: cdk.RemovalPolicy.Destroy }); - new cdk.CfnOutput(this, 'BucketURL', { value: bucket.bucketUrl }); + new cdk.CfnOutput(this, 'BucketURL', { value: bucket.bucketWebsiteUrl }); new cdk.CfnOutput(this, 'ObjectURL', { value: bucket.urlForObject('myfolder/myfile.txt') }); /// !hide } diff --git a/packages/@aws-cdk/aws-s3/test/test.bucket.ts b/packages/@aws-cdk/aws-s3/test/test.bucket.ts index eb23be105fc90..20fb0cedd162d 100644 --- a/packages/@aws-cdk/aws-s3/test/test.bucket.ts +++ b/packages/@aws-cdk/aws-s3/test/test.bucket.ts @@ -2,9 +2,11 @@ import { expect, haveResource, SynthUtils } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import cdk = require('@aws-cdk/cdk'); +import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { EOL } from 'os'; import s3 = require('../lib'); +import { Bucket } from '../lib'; // to make it easy to copy & paste from output: // tslint:disable:object-literal-key-quotes @@ -18,8 +20,8 @@ export = { expect(stack).toMatch({ "Resources": { "MyBucketF68F3FF0": { - "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain", + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain", } } }); @@ -36,8 +38,8 @@ export = { expect(stack).toMatch({ "Resources": { "MyBucketF68F3FF0": { - "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain", + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain", } } }); @@ -54,19 +56,19 @@ export = { expect(stack).toMatch({ "Resources": { "MyBucketF68F3FF0": { - "Type": "AWS::S3::Bucket", - "Properties": { - "BucketEncryption": { - "ServerSideEncryptionConfiguration": [ - { - "ServerSideEncryptionByDefault": { - "SSEAlgorithm": "aws:kms" - } + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "aws:kms" + } + } + ] } - ] - } - }, - "DeletionPolicy": "Retain", + }, + "DeletionPolicy": "Retain", } } }); @@ -111,7 +113,7 @@ export = { test.throws(() => new s3.Bucket(stack, 'MyBucket', { bucketName: bucket - // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:only-arrow-functions }), function(err: Error) { return expectedErrors === err.message; }); @@ -243,73 +245,73 @@ export = { expect(stack).toMatch({ "Resources": { "MyKey6AB29FA6": { - "Type": "AWS::KMS::Key", - "Properties": { - "Description": "hello, world", - "KeyPolicy": { - "Statement": [ - { - "Action": [ - "kms:Create*", - "kms:Describe*", - "kms:Enable*", - "kms:List*", - "kms:Put*", - "kms:Update*", - "kms:Revoke*", - "kms:Disable*", - "kms:Get*", - "kms:Delete*", - "kms:ScheduleKeyDeletion", - "kms:CancelKeyDeletion" - ], - "Effect": "Allow", - "Principal": { - "AWS": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::", + "Type": "AWS::KMS::Key", + "Properties": { + "Description": "hello, world", + "KeyPolicy": { + "Statement": [ { - "Ref": "AWS::AccountId" - }, - ":root" - ] - ] - } - }, - "Resource": "*" + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" } - ], - "Version": "2012-10-17" - } - }, - "DeletionPolicy": "Retain" + }, + "DeletionPolicy": "Retain" }, "MyBucketF68F3FF0": { - "Type": "AWS::S3::Bucket", - "Properties": { - "BucketEncryption": { - "ServerSideEncryptionConfiguration": [ - { - "ServerSideEncryptionByDefault": { - "KMSMasterKeyID": { - "Fn::GetAtt": [ - "MyKey6AB29FA6", - "Arn" + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "MyKey6AB29FA6", + "Arn" + ] + }, + "SSEAlgorithm": "aws:kms" + } + } ] - }, - "SSEAlgorithm": "aws:kms" - } } - ] - } - }, - "DeletionPolicy": "Retain", + }, + "DeletionPolicy": "Retain", } } }); @@ -331,7 +333,7 @@ export = { "Status": "Enabled" } }, - "DeletionPolicy": "Retain", + "DeletionPolicy": "Retain", } } }); @@ -356,7 +358,7 @@ export = { "RestrictPublicBuckets": true, } }, - "DeletionPolicy": "Retain", + "DeletionPolicy": "Retain", } } }); @@ -379,7 +381,7 @@ export = { "IgnorePublicAcls": true, } }, - "DeletionPolicy": "Retain", + "DeletionPolicy": "Retain", } } }); @@ -401,7 +403,7 @@ export = { "RestrictPublicBuckets": true, } }, - "DeletionPolicy": "Retain", + "DeletionPolicy": "Retain", } } }); @@ -419,26 +421,26 @@ export = { expect(stack).toMatch({ "Resources": { "MyBucketF68F3FF0": { - "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain", + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain", }, "MyBucketPolicyE7FBAC7B": { - "Type": "AWS::S3::BucketPolicy", - "Properties": { - "Bucket": { - "Ref": "MyBucketF68F3FF0" - }, - "PolicyDocument": { - "Statement": [ - { - "Action": "bar", - "Effect": "Allow", - "Resource": "foo" + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "MyBucketF68F3FF0" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "bar", + "Effect": "Allow", + "Resource": "foo" + } + ], + "Version": "2012-10-17" } - ], - "Version": "2012-10-17" - } - }, + }, } } }); @@ -456,7 +458,7 @@ export = { test.deepEqual(bucket.node.resolve(x), { Action: 's3:ListBucket', Effect: 'Allow', - Resource: { 'Fn::GetAtt': [ 'MyBucketF68F3FF0', 'Arn' ] } + Resource: { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] } }); test.done(); @@ -475,7 +477,7 @@ export = { Resource: { 'Fn::Join': [ '', - [ { 'Fn::GetAtt': [ 'MyBucketF68F3FF0', 'Arn' ] }, '/hello/world' ] + [{ 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, '/hello/world'] ] } }); @@ -502,7 +504,7 @@ export = { 'Fn::Join': [ '', [ - { 'Fn::GetAtt': [ 'MyBucketF68F3FF0', 'Arn' ] }, + { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, '/home/', { Ref: 'MyTeam01DD6685' }, '/', @@ -564,7 +566,7 @@ export = { const stack = new cdk.Stack(); const bucketArn = 'arn:aws:s3:::my-bucket'; - const bucket = s3.Bucket.import(stack, 'ImportedBucket', { bucketArn }); + const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { bucketArn }); // this is a no-op since the bucket is external bucket.addToResourcePolicy(new iam.PolicyStatement().addResource('foo').addAction('bar')); @@ -587,7 +589,7 @@ export = { 'import can also be used to import arbitrary ARNs'(test: Test) { const stack = new cdk.Stack(); - const bucket = s3.Bucket.import(stack, 'ImportedBucket', { bucketArn: 'arn:aws:s3:::my-bucket' }); + const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { bucketArn: 'arn:aws:s3:::my-bucket' }); bucket.addToResourcePolicy(new iam.PolicyStatement().addAllResources().addAction('*')); // at this point we technically didn't create any resources in the consuming stack. @@ -602,30 +604,30 @@ export = { expect(stack).toMatch({ "Resources": { - "MyUserDC45028B": { - "Type": "AWS::IAM::User" - }, - "MyUserDefaultPolicy7B897426": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "s3:*", - "Effect": "Allow", - "Resource": "arn:aws:s3:::my-bucket/my/folder/my-bucket" - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "MyUserDefaultPolicy7B897426", - "Users": [ - { - "Ref": "MyUserDC45028B" - } - ] + "MyUserDC45028B": { + "Type": "AWS::IAM::User" }, - } + "MyUserDefaultPolicy7B897426": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Effect": "Allow", + "Resource": "arn:aws:s3:::my-bucket/my/folder/my-bucket" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyUserDefaultPolicy7B897426", + "Users": [ + { + "Ref": "MyUserDC45028B" + } + ] + }, + } } }); @@ -639,105 +641,105 @@ export = { expect(stack1).toMatch({ "Resources": { - "MyBucketF68F3FF0": { - "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain", - } - }, - "Outputs": { - "MyBucketBucketArnE260558C": { - "Value": { - "Fn::GetAtt": [ - "MyBucketF68F3FF0", - "Arn" - ] - }, - "Export": { - "Name": "S1:MyBucketBucketArnE260558C" + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain", } }, - "MyBucketBucketName8A027014": { - "Value": { - "Ref": "MyBucketF68F3FF0" + "Outputs": { + "MyBucketBucketArnE260558C": { + "Value": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "Export": { + "Name": "S1:MyBucketBucketArnE260558C" + } }, - "Export": { - "Name": "S1:MyBucketBucketName8A027014" - } - }, - "MyBucketDomainNameF76B9A7A": { - "Value": { - "Fn::GetAtt": [ - "MyBucketF68F3FF0", - "DomainName" - ] + "MyBucketBucketName8A027014": { + "Value": { + "Ref": "MyBucketF68F3FF0" + }, + "Export": { + "Name": "S1:MyBucketBucketName8A027014" + } }, - "Export": { - "Name": "S1:MyBucketDomainNameF76B9A7A" - } - }, - "MyBucketWebsiteURL9C222788": { - "Value": { - "Fn::GetAtt": [ - "MyBucketF68F3FF0", - "WebsiteURL" - ] + "MyBucketDomainNameF76B9A7A": { + "Value": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "DomainName" + ] + }, + "Export": { + "Name": "S1:MyBucketDomainNameF76B9A7A" + } }, - "Export": { - "Name": "S1:MyBucketWebsiteURL9C222788" + "MyBucketWebsiteURL9C222788": { + "Value": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "WebsiteURL" + ] + }, + "Export": { + "Name": "S1:MyBucketWebsiteURL9C222788" + } } } - } }); const stack2 = new cdk.Stack(undefined, 'S2'); - const importedBucket = s3.Bucket.import(stack2, 'ImportedBucket', bucketRef); + const importedBucket = s3.Bucket.fromBucketAttributes(stack2, 'ImportedBucket', bucketRef); const user = new iam.User(stack2, 'MyUser'); importedBucket.grantRead(user); expect(stack2).toMatch({ "Resources": { - "MyUserDC45028B": { - "Type": "AWS::IAM::User" - }, - "MyUserDefaultPolicy7B897426": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*" - ], - "Effect": "Allow", - "Resource": [ - { - "Fn::ImportValue": "S1:MyBucketBucketArnE260558C" + "MyUserDC45028B": { + "Type": "AWS::IAM::User" + }, + "MyUserDefaultPolicy7B897426": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::ImportValue": "S1:MyBucketBucketArnE260558C" + }, + { + "Fn::Join": [ + "", + [ + { "Fn::ImportValue": "S1:MyBucketBucketArnE260558C" }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" }, - { - "Fn::Join": [ - "", - [ - { "Fn::ImportValue": "S1:MyBucketBucketArnE260558C" }, - "/*" - ] - ] - } + "PolicyName": "MyUserDefaultPolicy7B897426", + "Users": [ + { + "Ref": "MyUserDC45028B" + } ] } - ], - "Version": "2012-10-17" - }, - "PolicyName": "MyUserDefaultPolicy7B897426", - "Users": [ - { - "Ref": "MyUserDC45028B" - } - ] } } - } }); test.done(); @@ -751,59 +753,59 @@ export = { bucket.grantRead(reader); expect(stack).toMatch({ "Resources": { - "ReaderF7BF189D": { - "Type": "AWS::IAM::User" - }, - "ReaderDefaultPolicy151F3818": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*" - ], - "Effect": "Allow", - "Resource": [ - { - "Fn::GetAtt": [ - "MyBucketF68F3FF0", - "Arn" - ] - }, - { - "Fn::Join": [ - "", - [ + "ReaderF7BF189D": { + "Type": "AWS::IAM::User" + }, + "ReaderDefaultPolicy151F3818": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ { - "Fn::GetAtt": [ - "MyBucketF68F3FF0", - "Arn" - ] - }, - "/*" - ] - ] - } + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ReaderDefaultPolicy151F3818", + "Users": [ + { + "Ref": "ReaderF7BF189D" + } ] } - ], - "Version": "2012-10-17" }, - "PolicyName": "ReaderDefaultPolicy151F3818", - "Users": [ - { - "Ref": "ReaderF7BF189D" - } - ] - } - }, - "MyBucketF68F3FF0": { - "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain" - }, + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain" + }, } }); test.done(); @@ -819,60 +821,60 @@ export = { expect(stack).toMatch({ "Resources": { "MyBucketF68F3FF0": { - "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain", + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain", }, "MyUserDC45028B": { - "Type": "AWS::IAM::User" + "Type": "AWS::IAM::User" }, "MyUserDefaultPolicy7B897426": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*", - "s3:DeleteObject*", - "s3:PutObject*", - "s3:Abort*" - ], - "Effect": "Allow", - "Resource": [ - { - "Fn::GetAtt": [ - "MyBucketF68F3FF0", - "Arn" - ] - }, - { - "Fn::Join": [ - "", - [ + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ { - "Fn::GetAtt": [ - "MyBucketF68F3FF0", - "Arn" + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } ] - }, - "/*" - ] - ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyUserDefaultPolicy7B897426", + "Users": [ + { + "Ref": "MyUserDC45028B" } ] - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "MyUserDefaultPolicy7B897426", - "Users": [ - { - "Ref": "MyUserDC45028B" } - ] - } } } }); @@ -894,13 +896,13 @@ export = { "Version": "2012-10-17", "Statement": [ { - "Action": [ "s3:GetObject*", "s3:GetBucket*", "s3:List*" ], + "Action": ["s3:GetObject*", "s3:GetBucket*", "s3:List*"], "Condition": { "StringEquals": { "aws:PrincipalOrgID": "o-1234" } }, "Effect": "Allow", "Principal": "*", "Resource": [ - { "Fn::GetAtt": [ "MyBucketF68F3FF0", "Arn" ] }, - { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "MyBucketF68F3FF0", "Arn" ] }, "/*" ] ] } + { "Fn::GetAtt": ["MyBucketF68F3FF0", "Arn"] }, + { "Fn::Join": ["", [{ "Fn::GetAtt": ["MyBucketF68F3FF0", "Arn"] }, "/*"]] } ] } ] @@ -911,20 +913,20 @@ export = { "KeyPolicy": { "Statement": [ { - "Action": [ "kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*", "kms:Put*", "kms:Update*", - "kms:Revoke*", "kms:Disable*", "kms:Get*", "kms:Delete*", "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion" ], + "Action": ["kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*", "kms:Put*", "kms:Update*", + "kms:Revoke*", "kms:Disable*", "kms:Get*", "kms:Delete*", "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion"], "Effect": "Allow", "Principal": { "AWS": { - "Fn::Join": [ "", [ - "arn:", { "Ref": "AWS::Partition" }, ":iam::", { "Ref": "AWS::AccountId" }, ":root" + "Fn::Join": ["", [ + "arn:", { "Ref": "AWS::Partition" }, ":iam::", { "Ref": "AWS::AccountId" }, ":root" ]] } }, "Resource": "*" }, { - "Action": [ "kms:Decrypt", "kms:DescribeKey" ], + "Action": ["kms:Decrypt", "kms:DescribeKey"], "Effect": "Allow", "Resource": "*", "Principal": "*", @@ -948,160 +950,160 @@ export = { expect(stack).toMatch({ "Resources": { "MyBucketKeyC17130CF": { - "Type": "AWS::KMS::Key", - "Properties": { - "Description": "Created by MyBucket", - "KeyPolicy": { - "Statement": [ - { - "Action": [ - "kms:Create*", - "kms:Describe*", - "kms:Enable*", - "kms:List*", - "kms:Put*", - "kms:Update*", - "kms:Revoke*", - "kms:Disable*", - "kms:Get*", - "kms:Delete*", - "kms:ScheduleKeyDeletion", - "kms:CancelKeyDeletion" - ], - "Effect": "Allow", - "Principal": { - "AWS": { - "Fn::Join": [ - "", - [ - "arn:", + "Type": "AWS::KMS::Key", + "Properties": { + "Description": "Created by MyBucket", + "KeyPolicy": { + "Statement": [ { - "Ref": "AWS::Partition" + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" }, - ":iam::", { - "Ref": "AWS::AccountId" - }, - ":root" - ] - ] - } - }, - "Resource": "*" - }, - { - "Action": [ - "kms:Decrypt", - "kms:DescribeKey", - "kms:Encrypt", - "kms:ReEncrypt*", - "kms:GenerateDataKey*", - ], - "Effect": "Allow", - "Principal": { - "AWS": { - "Fn::GetAtt": [ - "MyUserDC45028B", - "Arn" - ] - } - }, - "Resource": "*" + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "MyUserDC45028B", + "Arn" + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" } - ], - "Version": "2012-10-17" - } - }, - "DeletionPolicy": "Retain" + }, + "DeletionPolicy": "Retain" }, "MyBucketF68F3FF0": { - "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain", - "Properties": { - "BucketEncryption": { - "ServerSideEncryptionConfiguration": [ - { - "ServerSideEncryptionByDefault": { - "KMSMasterKeyID": { - "Fn::GetAtt": [ - "MyBucketKeyC17130CF", - "Arn" + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "MyBucketKeyC17130CF", + "Arn" + ] + }, + "SSEAlgorithm": "aws:kms" + } + } ] - }, - "SSEAlgorithm": "aws:kms" } - } - ] } - } }, "MyUserDC45028B": { - "Type": "AWS::IAM::User" + "Type": "AWS::IAM::User" }, "MyUserDefaultPolicy7B897426": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*", - "s3:DeleteObject*", - "s3:PutObject*", - "s3:Abort*" - ], - "Effect": "Allow", - "Resource": [ - { - "Fn::GetAtt": [ - "MyBucketF68F3FF0", - "Arn" - ] - }, - { - "Fn::Join": [ - "", - [ + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ { - "Fn::GetAtt": [ - "MyBucketF68F3FF0", - "Arn" + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } ] }, - "/*" - ] - ] + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyBucketKeyC17130CF", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyUserDefaultPolicy7B897426", + "Users": [ + { + "Ref": "MyUserDC45028B" } ] - }, - { - "Action": [ - "kms:Decrypt", - "kms:DescribeKey", - "kms:Encrypt", - "kms:ReEncrypt*", - "kms:GenerateDataKey*", - ], - "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "MyBucketKeyC17130CF", - "Arn" - ] - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "MyUserDefaultPolicy7B897426", - "Users": [ - { - "Ref": "MyUserDC45028B" } - ] - } } } }); @@ -1124,8 +1126,8 @@ export = { const resources = SynthUtils.toCloudFormation(stack).Resources; const actions = (id: string) => resources[id].Properties.PolicyDocument.Statement[0].Action; - test.deepEqual(actions('WriterDefaultPolicyDC585BCE'), [ 's3:DeleteObject*', 's3:PutObject*', 's3:Abort*' ]); - test.deepEqual(actions('PutterDefaultPolicyAB138DD3'), [ 's3:PutObject*', 's3:Abort*' ]); + test.deepEqual(actions('WriterDefaultPolicyDC585BCE'), ['s3:DeleteObject*', 's3:PutObject*', 's3:Abort*']); + test.deepEqual(actions('PutterDefaultPolicyAB138DD3'), ['s3:PutObject*', 's3:Abort*']); test.deepEqual(actions('DeleterDefaultPolicyCD33B8A0'), 's3:DeleteObject*'); test.done(); }, @@ -1137,105 +1139,105 @@ export = { const stackB = new cdk.Stack(); const user = new iam.User(stackB, 'UserWhoNeedsAccess'); - const theBucketFromStackAAsARefInStackB = s3.Bucket.import(stackB, 'RefToBucketFromStackA', refToBucketFromStackA); + const theBucketFromStackAAsARefInStackB = s3.Bucket.fromBucketAttributes(stackB, 'RefToBucketFromStackA', refToBucketFromStackA); theBucketFromStackAAsARefInStackB.grantRead(user); expect(stackA).toMatch({ "Resources": { - "MyBucketF68F3FF0": { - "Type": "AWS::S3::Bucket", + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", "DeletionPolicy": "Retain", - } + } }, "Outputs": { - "MyBucketBucketArnE260558C": { - "Value": { - "Fn::GetAtt": [ - "MyBucketF68F3FF0", - "Arn" - ] + "MyBucketBucketArnE260558C": { + "Value": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "Export": { + "Name": "Stack:MyBucketBucketArnE260558C" + } }, - "Export": { - "Name": "Stack:MyBucketBucketArnE260558C" - } - }, - "MyBucketBucketName8A027014": { - "Value": { - "Ref": "MyBucketF68F3FF0" + "MyBucketBucketName8A027014": { + "Value": { + "Ref": "MyBucketF68F3FF0" + }, + "Export": { + "Name": "Stack:MyBucketBucketName8A027014" + } }, - "Export": { - "Name": "Stack:MyBucketBucketName8A027014" - } - }, - "MyBucketDomainNameF76B9A7A": { - "Value": { - "Fn::GetAtt": [ - "MyBucketF68F3FF0", - "DomainName" - ] + "MyBucketDomainNameF76B9A7A": { + "Value": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "DomainName" + ] + }, + "Export": { + "Name": "Stack:MyBucketDomainNameF76B9A7A" + } }, - "Export": { - "Name": "Stack:MyBucketDomainNameF76B9A7A" + "MyBucketWebsiteURL9C222788": { + "Value": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "WebsiteURL" + ] + }, + "Export": { "Name": "Stack:MyBucketWebsiteURL9C222788" } } - }, - "MyBucketWebsiteURL9C222788": { - "Value": { - "Fn::GetAtt": [ - "MyBucketF68F3FF0", - "WebsiteURL" - ] - }, - "Export": {"Name": "Stack:MyBucketWebsiteURL9C222788"} - } } }); expect(stackB).toMatch({ "Resources": { - "UserWhoNeedsAccessF8959C3D": { - "Type": "AWS::IAM::User" - }, - "UserWhoNeedsAccessDefaultPolicy6A9EB530": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*" - ], - "Effect": "Allow", - "Resource": [ - { - "Fn::ImportValue": "Stack:MyBucketBucketArnE260558C" - }, - { - "Fn::Join": [ - "", - [ + "UserWhoNeedsAccessF8959C3D": { + "Type": "AWS::IAM::User" + }, + "UserWhoNeedsAccessDefaultPolicy6A9EB530": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ { - "Fn::ImportValue": "Stack:MyBucketBucketArnE260558C" - }, - "/*" - ] - ] - } + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::ImportValue": "Stack:MyBucketBucketArnE260558C" + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::ImportValue": "Stack:MyBucketBucketArnE260558C" + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "UserWhoNeedsAccessDefaultPolicy6A9EB530", + "Users": [ + { + "Ref": "UserWhoNeedsAccessF8959C3D" + } ] } - ], - "Version": "2012-10-17" - }, - "PolicyName": "UserWhoNeedsAccessDefaultPolicy6A9EB530", - "Users": [ - { - "Ref": "UserWhoNeedsAccessF8959C3D" - } - ] } } - } }); test.done(); @@ -1245,83 +1247,83 @@ export = { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); - new cdk.CfnOutput(stack, 'BucketURL', { value: bucket.bucketUrl }); + new cdk.CfnOutput(stack, 'BucketURL', { value: bucket.urlForObject() }); new cdk.CfnOutput(stack, 'MyFileURL', { value: bucket.urlForObject('my/file.txt') }); new cdk.CfnOutput(stack, 'YourFileURL', { value: bucket.urlForObject('/your/file.txt') }); // "/" is optional expect(stack).toMatch({ "Resources": { - "MyBucketF68F3FF0": { - "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain" - } + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain" + } }, "Outputs": { - "BucketURL": { - "Value": { - "Fn::Join": [ - "", - [ - "https://s3.", - { - "Ref": "AWS::Region" - }, - ".", - { - "Ref": "AWS::URLSuffix" + "BucketURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "MyBucketF68F3FF0" + } + ] + ] }, - "/", - { - "Ref": "MyBucketF68F3FF0" + }, + "MyFileURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "MyBucketF68F3FF0" + }, + "/my/file.txt" + ] + ] } - ] - ] }, - }, - "MyFileURL": { - "Value": { - "Fn::Join": [ - "", - [ - "https://s3.", - { - "Ref": "AWS::Region" - }, - ".", - { - "Ref": "AWS::URLSuffix" - }, - "/", - { - "Ref": "MyBucketF68F3FF0" + "YourFileURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "MyBucketF68F3FF0" + }, + "/your/file.txt" + ] + ] }, - "/my/file.txt" - ] - ] } - }, - "YourFileURL": { - "Value": { - "Fn::Join": [ - "", - [ - "https://s3.", - { - "Ref": "AWS::Region" - }, - ".", - { - "Ref": "AWS::URLSuffix" - }, - "/", - { - "Ref": "MyBucketF68F3FF0" - }, - "/your/file.txt" - ] - ] - }, - } } }); @@ -1345,7 +1347,7 @@ export = { "Action": "s3:GetObject", "Effect": "Allow", "Principal": "*", - "Resource": { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "bC3BBCC65", "Arn" ] }, "/*" ] ] } + "Resource": { "Fn::Join": ["", [{ "Fn::GetAtt": ["bC3BBCC65", "Arn"] }, "/*"]] } } ], "Version": "2012-10-17" @@ -1370,7 +1372,7 @@ export = { "Action": "s3:GetObject", "Effect": "Allow", "Principal": "*", - "Resource": { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "bC3BBCC65", "Arn" ] }, "/only/access/these/*" ] ] } + "Resource": { "Fn::Join": ["", [{ "Fn::GetAtt": ["bC3BBCC65", "Arn"] }, "/only/access/these/*"]] } } ], "Version": "2012-10-17" @@ -1392,10 +1394,10 @@ export = { "PolicyDocument": { "Statement": [ { - "Action": [ "s3:GetObject", "s3:PutObject" ], + "Action": ["s3:GetObject", "s3:PutObject"], "Effect": "Allow", "Principal": "*", - "Resource": { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "bC3BBCC65", "Arn" ] }, "/*" ] ] } + "Resource": { "Fn::Join": ["", [{ "Fn::GetAtt": ["bC3BBCC65", "Arn"] }, "/*"]] } } ], "Version": "2012-10-17" @@ -1421,7 +1423,7 @@ export = { "Action": "s3:GetObject", "Effect": "Allow", "Principal": "*", - "Resource": { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "bC3BBCC65", "Arn" ] }, "/*" ] ] }, + "Resource": { "Fn::Join": ["", [{ "Fn::GetAtt": ["bC3BBCC65", "Arn"] }, "/*"]] }, "Condition": { "IpAddress": { "aws:SourceIp": "54.240.143.0/24" } } @@ -1488,8 +1490,36 @@ export = { const bucket = new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index.html' }); - test.deepEqual(bucket.node.resolve(bucket.bucketWebsiteUrl), { 'Fn::GetAtt': [ 'Website32962D0B', 'WebsiteURL' ] }); + test.deepEqual(bucket.node.resolve(bucket.bucketWebsiteUrl), { 'Fn::GetAtt': ['Website32962D0B', 'WebsiteURL'] }); test.done(); } - } + }, + + 'Bucket.fromBucketArn'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const bucket = Bucket.fromBucketArn(stack, 'my-bucket', 'arn:aws:s3:::my_corporate_bucket'); + + // THEN + test.deepEqual(bucket.bucketName, 'my_corporate_bucket'); + test.deepEqual(bucket.bucketArn, 'arn:aws:s3:::my_corporate_bucket'); + test.done(); + }, + + 'Bucket.fromBucketName'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const bucket = Bucket.fromBucketName(stack, 'imported-bucket', 'my-bucket-name'); + + // THEN + test.deepEqual(bucket.bucketName, 'my-bucket-name'); + test.deepEqual(stack.node.resolve(bucket.bucketArn), { + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::my-bucket-name']] + }); + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-s3/test/test.notifications.ts b/packages/@aws-cdk/aws-s3/test/test.notifications.ts index 5f44d8777b79e..c495f3e7f3aee 100644 --- a/packages/@aws-cdk/aws-s3/test/test.notifications.ts +++ b/packages/@aws-cdk/aws-s3/test/test.notifications.ts @@ -323,7 +323,7 @@ export = { 'CloudWatch Events': { 'onPutItem contains the Bucket ARN itself when path is undefined'(test: Test) { const stack = new cdk.Stack(); - const bucket = s3.Bucket.import(stack, 'Bucket', { + const bucket = s3.Bucket.fromBucketAttributes(stack, 'Bucket', { bucketName: 'MyBucket', }); bucket.onPutObject('PutRule'); @@ -366,7 +366,7 @@ export = { "onPutItem contains the path when it's provided"(test: Test) { const stack = new cdk.Stack(); - const bucket = s3.Bucket.import(stack, 'Bucket', { + const bucket = s3.Bucket.fromBucketAttributes(stack, 'Bucket', { bucketName: 'MyBucket', }); bucket.onPutObject('PutRule', undefined, 'my/path.zip'); diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index c49bd44d5a0a1..cfb67cf87411e 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -16,11 +16,13 @@ export interface ISecret extends IResource { /** * The ARN of the secret in AWS Secrets Manager. + * @attribute */ readonly secretArn: string; /** * Retrieve the value of the stored secret as a `SecretValue`. + * @attribute */ readonly secretValue: SecretValue; @@ -34,7 +36,7 @@ export interface ISecret extends IResource { * * @return import props that can be passed back to ``Secret.import``. */ - export(): SecretImportProps; + export(): SecretAttributes; /** * Grants reading the secret value to some role. @@ -87,7 +89,7 @@ export interface SecretProps { /** * Attributes required to import an existing secret into the Stack. */ -export interface SecretImportProps { +export interface SecretAttributes { /** * The encryption key that is used to encrypt the secret, unless the default SecretsManager key is used. */ @@ -106,7 +108,7 @@ abstract class SecretBase extends Resource implements ISecret { public abstract readonly encryptionKey?: kms.IEncryptionKey; public abstract readonly secretArn: string; - public abstract export(): SecretImportProps; + public abstract export(): SecretAttributes; public grantRead(grantee: iam.IGrantable, versionStages?: string[]): iam.Grant { // @see https://docs.aws.amazon.com/fr_fr/secretsmanager/latest/userguide/auth-and-access_identity-based-policies.html @@ -153,15 +155,20 @@ abstract class SecretBase extends Resource implements ISecret { * Creates a new secret in AWS SecretsManager. */ export class Secret extends SecretBase { + + public static fromSecretArn(scope: Construct, id: string, secretArn: string): ISecret { + return Secret.fromSecretAttributes(scope, id, { secretArn }); + } + /** * Import an existing secret into the Stack. * * @param scope the scope of the import. * @param id the ID of the imported Secret in the construct tree. - * @param props the attributes of the imported secret. + * @param attrs the attributes of the imported secret. */ - public static import(scope: Construct, id: string, props: SecretImportProps): ISecret { - return new ImportedSecret(scope, id, props); + public static fromSecretAttributes(scope: Construct, id: string, attrs: SecretAttributes): ISecret { + return new ImportedSecret(scope, id, attrs); } public readonly encryptionKey?: kms.IEncryptionKey; @@ -192,14 +199,14 @@ export class Secret extends SecretBase { * * @returns an AttachedSecret */ - public addTargetAttachment(id: string, options: AttachedSecretOptions): AttachedSecret { - return new AttachedSecret(this, id, { + public addTargetAttachment(id: string, options: AttachedSecretOptions): SecretTargetAttachment { + return new SecretTargetAttachment(this, id, { secret: this, ...options }); } - public export(): SecretImportProps { + public export(): SecretAttributes { return { encryptionKey: this.encryptionKey, secretArn: this.secretArn, @@ -260,21 +267,51 @@ export interface AttachedSecretOptions { /** * Construction properties for an AttachedSecret. */ -export interface AttachedSecretProps extends AttachedSecretOptions { +export interface SecretTargetAttachmentProps extends AttachedSecretOptions { /** * The secret to attach to the target. */ readonly secret: ISecret; } +export interface ISecretTargetAttachment extends ISecret { + /** + * Same as `secretArn` + * + * @attribute + */ + readonly secretTargetAttachmentSecretArn: string; +} + /** * An attached secret. */ -export class AttachedSecret extends SecretBase { +export class SecretTargetAttachment extends SecretBase implements ISecretTargetAttachment { + + public static fromSecretTargetAttachmentSecretArn(scope: Construct, id: string, secretTargetAttachmentSecretArn: string): ISecretTargetAttachment { + class Import extends SecretBase implements ISecretTargetAttachment { + public encryptionKey?: kms.IEncryptionKey | undefined; + public secretArn = secretTargetAttachmentSecretArn; + public secretTargetAttachmentSecretArn = secretTargetAttachmentSecretArn; + public export(): SecretAttributes { + return { + secretArn: this.secretArn + }; + } + } + + return new Import(scope, id); + } + public readonly encryptionKey?: kms.IEncryptionKey; public readonly secretArn: string; - constructor(scope: Construct, id: string, props: AttachedSecretProps) { + /** + * @attribute + */ + public readonly secretTargetAttachmentSecretArn: string; + + constructor(scope: Construct, id: string, props: SecretTargetAttachmentProps) { super(scope, id); const attachment = new secretsmanager.CfnSecretTargetAttachment(this, 'Resource', { @@ -287,9 +324,10 @@ export class AttachedSecret extends SecretBase { // This allows to reference the secret after attachment (dependency). this.secretArn = attachment.secretTargetAttachmentSecretArn; + this.secretTargetAttachmentSecretArn = attachment.secretTargetAttachmentSecretArn; } - public export(): SecretImportProps { + public export(): SecretAttributes { return { encryptionKey: this.encryptionKey, secretArn: this.secretArn, @@ -378,7 +416,7 @@ class ImportedSecret extends SecretBase { public readonly encryptionKey?: kms.IEncryptionKey; public readonly secretArn: string; - constructor(scope: Construct, id: string, private readonly props: SecretImportProps) { + constructor(scope: Construct, id: string, private readonly props: SecretAttributes) { super(scope, id); this.encryptionKey = props.encryptionKey; diff --git a/packages/@aws-cdk/aws-secretsmanager/package.json b/packages/@aws-cdk/aws-secretsmanager/package.json index aac0bf7a3c2b5..3abd20d82b09c 100644 --- a/packages/@aws-cdk/aws-secretsmanager/package.json +++ b/packages/@aws-cdk/aws-secretsmanager/package.json @@ -81,5 +81,11 @@ }, "engines": { "node": ">= 8.10.0" + }, + "awslint": { + "exclude": [ + "from-signature:@aws-cdk/aws-secretsmanager.SecretTargetAttachment.fromSecretTargetAttachmentSecretArn", + "from-attributes:fromSecretTargetAttachmentAttributes" + ] } } diff --git a/packages/@aws-cdk/aws-secretsmanager/test/example.app-with-secret.lit.ts b/packages/@aws-cdk/aws-secretsmanager/test/example.app-with-secret.lit.ts index f5b15b4a6b2b5..3959fcbd86761 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/example.app-with-secret.lit.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/example.app-with-secret.lit.ts @@ -7,7 +7,7 @@ class ExampleStack extends cdk.Stack { super(scope, id); /// !show - const loginSecret = secretsmanager.Secret.import(this, 'Secret', { + const loginSecret = secretsmanager.Secret.fromSecretAttributes(this, 'Secret', { secretArn: 'SomeLogin' }); diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts index 91f361f756d53..f1034c369a7f2 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts @@ -296,7 +296,7 @@ export = { const secretArn = 'arn::of::a::secret'; // WHEN - const secret = secretsmanager.Secret.import(stack, 'Secret', { + const secret = secretsmanager.Secret.fromSecretAttributes(stack, 'Secret', { secretArn, encryptionKey }); @@ -399,7 +399,7 @@ export = { const stack = new Stack(); // WHEN - const imported = secretsmanager.Secret.import(stack, 'Imported', { secretArn: 'my-secret-arn' }).secretJsonValue('password'); + const imported = secretsmanager.Secret.fromSecretAttributes(stack, 'Imported', { secretArn: 'my-secret-arn' }).secretJsonValue('password'); const value = SecretValue.secretsManager('my-secret-arn', { jsonField: 'password' }); // THEN diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/cname-instance.ts b/packages/@aws-cdk/aws-servicediscovery/lib/cname-instance.ts index b465a59f7c22d..feddb68743a3d 100644 --- a/packages/@aws-cdk/aws-servicediscovery/lib/cname-instance.ts +++ b/packages/@aws-cdk/aws-servicediscovery/lib/cname-instance.ts @@ -28,6 +28,7 @@ export interface CnameInstanceProps extends CnameInstanceBaseProps { /* * Instance that is accessible using a domain name (CNAME). + * @resource AWS::ServiceDiscovery::Instance */ export class CnameInstance extends InstanceBase { /** diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/http-namespace.ts b/packages/@aws-cdk/aws-servicediscovery/lib/http-namespace.ts index 637b8547bd33c..4409cd340df90 100644 --- a/packages/@aws-cdk/aws-servicediscovery/lib/http-namespace.ts +++ b/packages/@aws-cdk/aws-servicediscovery/lib/http-namespace.ts @@ -1,15 +1,29 @@ import { Construct } from '@aws-cdk/cdk'; -import { BaseNamespaceProps, NamespaceBase, NamespaceType } from './namespace'; +import { BaseNamespaceProps, INamespace, NamespaceBase, NamespaceType } from './namespace'; import { BaseServiceProps, Service } from './service'; import { CfnHttpNamespace } from './servicediscovery.generated'; -// tslint:disable:no-empty-interface export interface HttpNamespaceProps extends BaseNamespaceProps {} +export interface IHttpNamespace extends INamespace { + /** + * The Amazon Resource Name (ARN) of the namespace, such as + * arn:aws:service-discovery:us-east-1:123456789012:http-namespace/http-namespace-a1bzhi. + * @attribute + */ + readonly httpNamespaceArn: string; + + /** + * The ID of the namespace. + * @attribute + */ + readonly httpNamespaceId: string; +} + /** * Define an HTTP Namespace */ -export class HttpNamespace extends NamespaceBase { +export class HttpNamespace extends NamespaceBase implements IHttpNamespace { /** * A name for the namespace. */ @@ -44,6 +58,9 @@ export class HttpNamespace extends NamespaceBase { this.type = NamespaceType.Http; } + public get httpNamespaceArn() { return this.namespaceArn; } + public get httpNamespaceId() { return this.namespaceId; } + /** * Creates a service within the namespace */ diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/ip-instance.ts b/packages/@aws-cdk/aws-servicediscovery/lib/ip-instance.ts index 799875309cc93..de8eb89130783 100644 --- a/packages/@aws-cdk/aws-servicediscovery/lib/ip-instance.ts +++ b/packages/@aws-cdk/aws-servicediscovery/lib/ip-instance.ts @@ -45,6 +45,8 @@ export interface IpInstanceProps extends IpInstanceBaseProps { /* * Instance that is accessible using an IP address. + * + * @resource AWS::ServiceDiscovery::Instance */ export class IpInstance extends InstanceBase { /** diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/non-ip-instance.ts b/packages/@aws-cdk/aws-servicediscovery/lib/non-ip-instance.ts index 1f75457df6d6e..8e1ad35314e7d 100644 --- a/packages/@aws-cdk/aws-servicediscovery/lib/non-ip-instance.ts +++ b/packages/@aws-cdk/aws-servicediscovery/lib/non-ip-instance.ts @@ -21,6 +21,8 @@ export interface NonIpInstanceProps extends NonIpInstanceBaseProps { /* * Instance accessible using values other than an IP address or a domain name (CNAME). * Specify the other values in Custom attributes. + * + * @resource AWS::ServiceDiscovery::Instance */ export class NonIpInstance extends InstanceBase { /** diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/private-dns-namespace.ts b/packages/@aws-cdk/aws-servicediscovery/lib/private-dns-namespace.ts index 047ca1f89580e..a75a46629fb1f 100644 --- a/packages/@aws-cdk/aws-servicediscovery/lib/private-dns-namespace.ts +++ b/packages/@aws-cdk/aws-servicediscovery/lib/private-dns-namespace.ts @@ -1,6 +1,6 @@ import ec2 = require('@aws-cdk/aws-ec2'); import { Construct } from '@aws-cdk/cdk'; -import { BaseNamespaceProps, NamespaceBase, NamespaceType } from './namespace'; +import { BaseNamespaceProps, INamespace, NamespaceBase, NamespaceType } from './namespace'; import { DnsServiceProps, Service } from './service'; import { CfnPrivateDnsNamespace} from './servicediscovery.generated'; @@ -11,10 +11,24 @@ export interface PrivateDnsNamespaceProps extends BaseNamespaceProps { readonly vpc: ec2.IVpcNetwork; } +export interface IPrivateDnsNamespace extends INamespace { + /** + * The ID of the private namespace. + * @attribute + */ + readonly privateDnsNamespaceId: string; + + /** + * The Amazon Resource Name (ARN) of the private namespace. + * @attribute + */ + readonly privateDnsNamespaceArn: string; +} + /** * Define a Service Discovery HTTP Namespace */ -export class PrivateDnsNamespace extends NamespaceBase { +export class PrivateDnsNamespace extends NamespaceBase implements IPrivateDnsNamespace { /** * The name of the PrivateDnsNamespace. */ @@ -53,6 +67,9 @@ export class PrivateDnsNamespace extends NamespaceBase { this.type = NamespaceType.DnsPrivate; } + public get privateDnsNamespaceArn() { return this.namespaceArn; } + public get privateDnsNamespaceId() { return this.namespaceId; } + /** * Creates a service within the namespace */ diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/public-dns-namespace.ts b/packages/@aws-cdk/aws-servicediscovery/lib/public-dns-namespace.ts index 61f2c7ce33848..70e6b86a2a058 100644 --- a/packages/@aws-cdk/aws-servicediscovery/lib/public-dns-namespace.ts +++ b/packages/@aws-cdk/aws-servicediscovery/lib/public-dns-namespace.ts @@ -1,15 +1,28 @@ import { Construct } from '@aws-cdk/cdk'; -import { BaseNamespaceProps, NamespaceBase, NamespaceType } from './namespace'; +import { BaseNamespaceProps, INamespace, NamespaceBase, NamespaceType } from './namespace'; import { DnsServiceProps, Service } from './service'; import { CfnPublicDnsNamespace} from './servicediscovery.generated'; // tslint:disable:no-empty-interface export interface PublicDnsNamespaceProps extends BaseNamespaceProps {} +export interface IPublicDnsNamespace extends INamespace { + /** + * The ID of the public namespace. + * @attribute + */ + readonly publicDnsNamespaceId: string; + + /** + * The Amazon Resource Name (ARN) of the public namespace. + * @attribute + */ + readonly publicDnsNamespaceArn: string; +} /** * Define a Public DNS Namespace */ -export class PublicDnsNamespace extends NamespaceBase { +export class PublicDnsNamespace extends NamespaceBase implements IPublicDnsNamespace { /** * A name for the namespace. */ @@ -44,6 +57,9 @@ export class PublicDnsNamespace extends NamespaceBase { this.type = NamespaceType.DnsPublic; } + public get publicDnsNamespaceArn() { return this.namespaceArn; } + public get publicDnsNamespaceId() { return this.namespaceId; } + /** * Creates a service within the namespace */ diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/service.ts b/packages/@aws-cdk/aws-servicediscovery/lib/service.ts index 258b3212b7249..0768b3c1fd111 100644 --- a/packages/@aws-cdk/aws-servicediscovery/lib/service.ts +++ b/packages/@aws-cdk/aws-servicediscovery/lib/service.ts @@ -11,6 +11,7 @@ import { CfnService } from './servicediscovery.generated'; export interface IService extends IResource { /** * A name for the Cloudmap Service. + * @attribute */ readonly serviceName: string; @@ -21,11 +22,13 @@ export interface IService extends IResource { /** * The ID of the namespace that you want to use for DNS configuration. + * @attribute */ readonly serviceId: string; /** * The Arn of the namespace that you want to use for DNS configuration. + * @attribute */ readonly serviceArn: string; @@ -125,10 +128,41 @@ export interface ServiceProps extends DnsServiceProps { readonly namespace: INamespace; } +abstract class ServiceBase extends Resource implements IService { + public abstract namespace: INamespace; + public abstract serviceId: string; + public abstract serviceArn: string; + public abstract dnsRecordType: DnsRecordType; + public abstract routingPolicy: RoutingPolicy; + public abstract readonly serviceName: string; +} + +export interface ServiceAttributes { + readonly serviceName: string; + readonly serviceId: string; + readonly serviceArn: string; + readonly dnsRecordType: DnsRecordType; + readonly routingPolicy: RoutingPolicy; +} + /** * Define a CloudMap Service */ -export class Service extends Resource implements IService { +export class Service extends ServiceBase { + + public static fromServiceAttributes(scope: Construct, id: string, attrs: ServiceAttributes): IService { + class Import extends ServiceBase { + public namespace: INamespace; + public serviceId = attrs.serviceId; + public serviceArn = attrs.serviceArn; + public dnsRecordType = attrs.dnsRecordType; + public routingPolicy = attrs.routingPolicy; + public serviceName = attrs.serviceName; + } + + return new Import(scope, id); + } + /** * A name for the Cloudmap Service. */ diff --git a/packages/@aws-cdk/aws-servicediscovery/package.json b/packages/@aws-cdk/aws-servicediscovery/package.json index e8308f3572c00..5521a04618df5 100644 --- a/packages/@aws-cdk/aws-servicediscovery/package.json +++ b/packages/@aws-cdk/aws-servicediscovery/package.json @@ -82,13 +82,20 @@ }, "awslint": { "exclude": [ - "export:@aws-cdk/aws-servicediscovery.IService", - "export:@aws-cdk/aws-servicediscovery.IInstance", - "import-props-interface:@aws-cdk/aws-servicediscovery.ServiceImportProps", - "import-props-interface:@aws-cdk/aws-servicediscovery.InstanceImportProps", - "resource-interface:@aws-cdk/aws-servicediscovery.IHttpNamespace", - "resource-interface:@aws-cdk/aws-servicediscovery.IPublicDnsNamespace", - "resource-interface:@aws-cdk/aws-servicediscovery.IPrivateDnsNamespace" + "from-attributes:fromPrivateDnsNamespaceAttributes", + "from-arn:Service.fromServiceArn", + "from-arn:HttpNamespace.fromHttpNamespaceArn", + "from-attributes:fromHttpNamespaceAttributes", + "from-method:@aws-cdk/aws-servicediscovery.PrivateDnsNamespace", + "from-arn:PrivateDnsNamespace.fromPrivateDnsNamespaceArn", + "from-method:@aws-cdk/aws-servicediscovery.HttpNamespace", + "from-method:@aws-cdk/aws-servicediscovery.PublicDnsNamespace", + "from-arn:PublicDnsNamespace.fromPublicDnsNamespaceArn", + "from-attributes:fromPublicDnsNamespaceAttributes", + "from-method:@aws-cdk/aws-servicediscovery.Service", + "from-attributes:@aws-cdk/aws-servicediscovery.HttpNamespace.fromHttpNamespaceAttributes", + "from-attributes:@aws-cdk/aws-servicediscovery.PrivateDnsNamespace.fromPrivateDnsNamespaceAttributes", + "from-attributes:@aws-cdk/aws-servicediscovery.PublicDnsNamespace.fromPublicDnsNamespaceAttributes" ] } } diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts b/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts index 9496d0c3191e4..b9ff54e33d22c 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts @@ -75,7 +75,7 @@ export interface WhiteListReceiptFilterProps { /** * A white list receipt filter. */ -export class WhiteListReceiptFilter extends Resource { +export class WhiteListReceiptFilter extends Construct { constructor(scope: Construct, id: string, props: WhiteListReceiptFilterProps) { super(scope, id); diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts index 29963c278bc10..f928e6bfe98fa 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts @@ -8,8 +8,9 @@ import { CfnReceiptRuleSet } from './ses.generated'; export interface IReceiptRuleSet extends IResource { /** * The receipt rule set name. + * @attribute */ - readonly name: string; + readonly receiptRuleSetName: string; /** * Adds a new receipt rule in this rule set. The new rule is added after @@ -20,7 +21,7 @@ export interface IReceiptRuleSet extends IResource { /** * Exports this receipt rule set from the stack. */ - export(): ReceiptRuleSetImportProps; + export(): ReceiptRuleSetAttributes; } /** @@ -53,7 +54,7 @@ export interface ReceiptRuleSetProps { * A new or imported receipt rule set. */ abstract class ReceiptRuleSetBase extends Resource implements IReceiptRuleSet { - public abstract readonly name: string; + public abstract readonly receiptRuleSetName: string; private lastAddedRule?: ReceiptRule; @@ -71,7 +72,7 @@ abstract class ReceiptRuleSetBase extends Resource implements IReceiptRuleSet { return this.lastAddedRule; } - public abstract export(): ReceiptRuleSetImportProps; + public abstract export(): ReceiptRuleSetAttributes; /** * Adds a drop spam rule @@ -91,11 +92,17 @@ export class ReceiptRuleSet extends ReceiptRuleSetBase { /** * Import an exported receipt rule set. */ - public static import(scope: Construct, id: string, props: ReceiptRuleSetImportProps): IReceiptRuleSet { - return new ImportedReceiptRuleSet(scope, id, props); + public static fromReceiptRuleSetName(scope: Construct, id: string, receiptRuleSetName: string): IReceiptRuleSet { + class Import extends ReceiptRuleSetBase implements IReceiptRuleSet { + public readonly receiptRuleSetName = receiptRuleSetName; + public export(): ReceiptRuleSetAttributes { + return { receiptRuleSetName }; + } + } + return new Import(scope, id); } - public readonly name: string; + public readonly receiptRuleSetName: string; constructor(scope: Construct, id: string, props?: ReceiptRuleSetProps) { super(scope, id); @@ -104,7 +111,7 @@ export class ReceiptRuleSet extends ReceiptRuleSetBase { ruleSetName: props ? props.name : undefined }); - this.name = resource.receiptRuleSetName; + this.receiptRuleSetName = resource.receiptRuleSetName; if (props) { const rules = props.rules || []; @@ -119,9 +126,9 @@ export class ReceiptRuleSet extends ReceiptRuleSetBase { /** * Exports this receipt rule set from the stack. */ - public export(): ReceiptRuleSetImportProps { + public export(): ReceiptRuleSetAttributes { return { - name: new CfnOutput(this, 'ReceiptRuleSetName', { value: this.name }).makeImportValue().toString() + receiptRuleSetName: new CfnOutput(this, 'ReceiptRuleSetName', { value: this.receiptRuleSetName }).makeImportValue().toString() }; } } @@ -129,29 +136,9 @@ export class ReceiptRuleSet extends ReceiptRuleSetBase { /** * Construction properties for an ImportedReceiptRuleSet. */ -export interface ReceiptRuleSetImportProps { +export interface ReceiptRuleSetAttributes { /** * The receipt rule set name. */ - readonly name: string; -} - -/** - * An imported receipt rule set. - */ -class ImportedReceiptRuleSet extends ReceiptRuleSetBase implements IReceiptRuleSet { - public readonly name: string; - - constructor(scope: Construct, id: string, private readonly props: ReceiptRuleSetImportProps) { - super(scope, id); - - this.name = props.name; - } - - /** - * Exports this receipt rule set from the stack. - */ - public export() { - return this.props; - } + readonly receiptRuleSetName: string; } diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts index 64afd187d9b9c..7f67d844d15cd 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts @@ -10,13 +10,14 @@ import { CfnReceiptRule } from './ses.generated'; export interface IReceiptRule extends IResource { /** * The name of the receipt rule. + * @attribute */ - readonly name: string; + readonly receiptRuleName: string; /** * Exports this receipt rule from the stack. */ - export(): ReceiptRuleImportProps; + export(): ReceiptRuleAttributes; } /** @@ -102,21 +103,25 @@ export interface ReceiptRuleProps extends ReceiptRuleOptions { * A new receipt rule. */ export class ReceiptRule extends Resource implements IReceiptRule { - /** - * Import an exported receipt rule. - */ - public static import(scope: Construct, id: string, props: ReceiptRuleImportProps): IReceiptRule { - return new ImportedReceiptRule(scope, id, props); + + public static fromReceiptRuleName(scope: Construct, id: string, receiptRuleName: string): IReceiptRule { + class Import extends Construct implements IReceiptRule { + public readonly receiptRuleName = receiptRuleName; + public export(): ReceiptRuleAttributes { + return { receiptRuleName }; + } + } + return new Import(scope, id); } - public readonly name: string; + public readonly receiptRuleName: string; private readonly renderedActions = new Array(); constructor(scope: Construct, id: string, props: ReceiptRuleProps) { super(scope, id); const resource = new CfnReceiptRule(this, 'Resource', { - after: props.after ? props.after.name : undefined, + after: props.after ? props.after.receiptRuleName : undefined, rule: { actions: new Token(() => this.getRenderedActions()), enabled: props.enabled === undefined ? true : props.enabled, @@ -125,10 +130,10 @@ export class ReceiptRule extends Resource implements IReceiptRule { scanEnabled: props.scanEnabled, tlsPolicy: props.tlsPolicy }, - ruleSetName: props.ruleSet.name + ruleSetName: props.ruleSet.receiptRuleSetName }); - this.name = resource.receiptRuleName; + this.receiptRuleName = resource.receiptRuleName; if (props.actions) { props.actions.forEach(action => this.addAction(action)); @@ -147,9 +152,9 @@ export class ReceiptRule extends Resource implements IReceiptRule { /** * Exports this receipt rule from the stack. */ - public export(): ReceiptRuleImportProps { + public export(): ReceiptRuleAttributes { return { - name: new CfnOutput(this, 'ReceiptRuleName', { value: this.name }).makeImportValue().toString() + receiptRuleName: new CfnOutput(this, 'ReceiptRuleName', { value: this.receiptRuleName }).makeImportValue().toString() }; } @@ -162,31 +167,11 @@ export class ReceiptRule extends Resource implements IReceiptRule { } } -export interface ReceiptRuleImportProps { +export interface ReceiptRuleAttributes { /** * The name of the receipt rule. */ - readonly name: string; -} - -/** - * An imported receipt rule. - */ -class ImportedReceiptRule extends Construct implements IReceiptRule { - public readonly name: string; - - constructor(scope: Construct, id: string, private readonly props: ReceiptRuleImportProps) { - super(scope, id); - - this.name = props.name; - } - - /** - * Exports this receipt rule from the stack. - */ - public export() { - return this.props; - } + readonly receiptRuleName: string; } // tslint:disable-next-line:no-empty-interface diff --git a/packages/@aws-cdk/aws-ses/test/test.receipt-rule-set.ts b/packages/@aws-cdk/aws-ses/test/test.receipt-rule-set.ts index 83895d3d8aae9..92c91a123b24e 100644 --- a/packages/@aws-cdk/aws-ses/test/test.receipt-rule-set.ts +++ b/packages/@aws-cdk/aws-ses/test/test.receipt-rule-set.ts @@ -93,9 +93,7 @@ export = { const stack = new Stack(); // WHEN - const receiptRuleSet = ReceiptRuleSet.import(stack, 'ImportedRuleSet', { - name: 'MyRuleSet' - }); + const receiptRuleSet = ReceiptRuleSet.fromReceiptRuleSetName(stack, 'ImportedRuleSet', 'MyRuleSet'); receiptRuleSet.addRule('MyRule'); diff --git a/packages/@aws-cdk/aws-ses/test/test.receipt-rule.ts b/packages/@aws-cdk/aws-ses/test/test.receipt-rule.ts index 939af1469a549..9f899df31a311 100644 --- a/packages/@aws-cdk/aws-ses/test/test.receipt-rule.ts +++ b/packages/@aws-cdk/aws-ses/test/test.receipt-rule.ts @@ -117,10 +117,7 @@ export = { const stack = new Stack(); // WHEN - const receiptRule = ReceiptRule.import(stack, 'ImportedRule', { - name: 'MyRule' - }); - + const receiptRule = ReceiptRule.fromReceiptRuleName(stack, 'ImportedRule', 'MyRule'); const receiptRuleSet = new ReceiptRuleSet(stack, 'RuleSet'); receiptRuleSet.addRule('MyRule', { diff --git a/packages/@aws-cdk/aws-sns/lib/topic-base.ts b/packages/@aws-cdk/aws-sns/lib/topic-base.ts index 5768fc5f55c4f..45f02f6707c65 100644 --- a/packages/@aws-cdk/aws-sns/lib/topic-base.ts +++ b/packages/@aws-cdk/aws-sns/lib/topic-base.ts @@ -15,14 +15,20 @@ export interface ITopic extends s3n.IBucketNotificationDestination, autoscaling_api.ILifecycleHookTarget { + /** + * @attribute + */ readonly topicArn: string; + /** + * @attribute + */ readonly topicName: string; /** * Export this Topic */ - export(): TopicImportProps; + export(): TopicAttributes; /** * Subscribe some endpoint to this topic @@ -106,7 +112,7 @@ export abstract class TopicBase extends Resource implements ITopic { /** * Export this Topic */ - public abstract export(): TopicImportProps; + public abstract export(): TopicAttributes; /** * Subscribe some endpoint to this topic @@ -306,7 +312,7 @@ export abstract class TopicBase extends Resource implements ITopic { /** * Reference to an external topic. */ -export interface TopicImportProps { +export interface TopicAttributes { readonly topicArn: string; readonly topicName: string; } diff --git a/packages/@aws-cdk/aws-sns/lib/topic.ts b/packages/@aws-cdk/aws-sns/lib/topic.ts index 04d5b7744215e..88f3f309cda11 100644 --- a/packages/@aws-cdk/aws-sns/lib/topic.ts +++ b/packages/@aws-cdk/aws-sns/lib/topic.ts @@ -1,6 +1,6 @@ import { CfnOutput, Construct } from '@aws-cdk/cdk'; import { CfnTopic } from './sns.generated'; -import { ITopic, TopicBase, TopicImportProps } from './topic-base'; +import { ITopic, TopicAttributes, TopicBase } from './topic-base'; /** * Properties for a new SNS topic @@ -29,11 +29,20 @@ export interface TopicProps { * A new SNS topic */ export class Topic extends TopicBase { + + public static fromTopicArn(scope: Construct, id: string, topicArn: string): ITopic { + // arn:aws:sns:region:account-id:topicname + return Topic.fromTopicAttributes(scope, id, { + topicArn, + topicName: scope.node.stack.parseArn(topicArn).resource + }); + } + /** * Import a Topic defined elsewhere */ - public static import(scope: Construct, id: string, props: TopicImportProps): ITopic { - return new ImportedTopic(scope, id, props); + public static fromTopicAttributes(scope: Construct, id: string, attrs: TopicAttributes): ITopic { + return new ImportedTopic(scope, id, attrs); } public readonly topicArn: string; @@ -56,7 +65,7 @@ export class Topic extends TopicBase { /** * Export this Topic */ - public export(): TopicImportProps { + public export(): TopicAttributes { return { topicArn: new CfnOutput(this, 'TopicArn', { value: this.topicArn }).makeImportValue().toString(), topicName: new CfnOutput(this, 'TopicName', { value: this.topicName }).makeImportValue().toString(), @@ -73,13 +82,13 @@ class ImportedTopic extends TopicBase { protected autoCreatePolicy: boolean = false; - constructor(scope: Construct, id: string, private readonly props: TopicImportProps) { + constructor(scope: Construct, id: string, private readonly props: TopicAttributes) { super(scope, id); this.topicArn = props.topicArn; this.topicName = props.topicName; } - public export(): TopicImportProps { + public export(): TopicAttributes { return this.props; } } diff --git a/packages/@aws-cdk/aws-sns/test/test.sns.ts b/packages/@aws-cdk/aws-sns/test/test.sns.ts index 5e0738a5c29d9..4f8d9c0e829c6 100644 --- a/packages/@aws-cdk/aws-sns/test/test.sns.ts +++ b/packages/@aws-cdk/aws-sns/test/test.sns.ts @@ -693,7 +693,7 @@ export = { // WHEN const ref = topic.export(); - const imported = sns.Topic.import(stack2, 'Imported', ref); + const imported = sns.Topic.fromTopicAttributes(stack2, 'Imported', ref); imported.subscribeQueue(queue); // THEN diff --git a/packages/@aws-cdk/aws-sqs/lib/queue-base.ts b/packages/@aws-cdk/aws-sqs/lib/queue-base.ts index e834682fdd7ae..a0da9686916a1 100644 --- a/packages/@aws-cdk/aws-sqs/lib/queue-base.ts +++ b/packages/@aws-cdk/aws-sqs/lib/queue-base.ts @@ -8,16 +8,19 @@ import { QueuePolicy } from './policy'; export interface IQueue extends IResource, s3n.IBucketNotificationDestination, autoscaling_api.ILifecycleHookTarget { /** * The ARN of this queue + * @attribute */ readonly queueArn: string; /** * The URL of this queue + * @attribute */ readonly queueUrl: string; /** * The name of this queue + * @attribute */ readonly queueName: string; @@ -29,7 +32,7 @@ export interface IQueue extends IResource, s3n.IBucketNotificationDestination, a /** * Export a queue */ - export(): QueueImportProps; + export(): QueueAttributes; /** * Adds a statement to the IAM resource policy associated with this queue. @@ -136,7 +139,7 @@ export abstract class QueueBase extends Resource implements IQueue { /** * Export a queue */ - public abstract export(): QueueImportProps; + public abstract export(): QueueAttributes; /** * Adds a statement to the IAM resource policy associated with this queue. @@ -284,7 +287,7 @@ export abstract class QueueBase extends Resource implements IQueue { /** * Reference to a queue */ -export interface QueueImportProps { +export interface QueueAttributes { /** * The ARN of the queue. */ @@ -293,7 +296,7 @@ export interface QueueImportProps { /** * The URL of the queue. */ - readonly queueUrl: string; + readonly queueUrl?: string; /** * The name of the queue. diff --git a/packages/@aws-cdk/aws-sqs/lib/queue.ts b/packages/@aws-cdk/aws-sqs/lib/queue.ts index 0ca7818b7b8eb..caaa2fd101b1f 100644 --- a/packages/@aws-cdk/aws-sqs/lib/queue.ts +++ b/packages/@aws-cdk/aws-sqs/lib/queue.ts @@ -1,6 +1,6 @@ import kms = require('@aws-cdk/aws-kms'); import { CfnOutput, Construct } from '@aws-cdk/cdk'; -import { IQueue, QueueBase, QueueImportProps } from './queue-base'; +import { IQueue, QueueAttributes, QueueBase } from './queue-base'; import { CfnQueue } from './sqs.generated'; import { validateProps } from './validate-props'; @@ -180,11 +180,38 @@ export enum QueueEncryption { * A new Amazon SQS queue */ export class Queue extends QueueBase { + + public static fromQueueArn(scope: Construct, id: string, queueArn: string): IQueue { + return Queue.fromQueueAttributes(scope, id, { queueArn }); + } + /** * Import an existing queue */ - public static import(scope: Construct, id: string, props: QueueImportProps): IQueue { - return new ImportedQueue(scope, id, props); + public static fromQueueAttributes(scope: Construct, id: string, attrs: QueueAttributes): IQueue { + const stack = scope.node.stack; + const queueName = attrs.queueName || stack.parseArn(attrs.queueArn).resource; + const queueUrl = attrs.queueUrl || `https://sqs.${stack.region}.${stack.urlSuffix}/${stack.accountId}/${queueName}`; + + class Import extends QueueBase { + public readonly queueArn = attrs.queueArn; // arn:aws:sqs:us-east-1:123456789012:queue1 + public readonly queueUrl = queueUrl; + public readonly queueName = queueName; + public readonly encryptionMasterKey = attrs.keyArn + ? kms.EncryptionKey.import(this, 'Key', { keyArn: attrs.keyArn }) + : undefined; + + protected readonly autoCreatePolicy = false; + + /** + * Export a queue + */ + public export() { + return attrs; + } + } + + return new Import(scope, id); } /** @@ -285,7 +312,7 @@ export class Queue extends QueueBase { /** * Export a queue */ - public export(): QueueImportProps { + public export(): QueueAttributes { return { queueArn: new CfnOutput(this, 'QueueArn', { value: this.queueArn }).makeImportValue().toString(), queueUrl: new CfnOutput(this, 'QueueUrl', { value: this.queueUrl }).makeImportValue().toString(), @@ -334,35 +361,3 @@ interface EncryptionProps { readonly kmsMasterKeyId?: string; readonly kmsDataKeyReusePeriodSeconds?: number; } - -/** - * A queue that has been imported - */ -class ImportedQueue extends QueueBase { - public readonly queueArn: string; - public readonly queueUrl: string; - public readonly queueName: string; - public readonly encryptionMasterKey?: kms.IEncryptionKey; - - protected readonly autoCreatePolicy = false; - - constructor(scope: Construct, id: string, private readonly props: QueueImportProps) { - super(scope, id); - this.queueArn = props.queueArn; - this.queueUrl = props.queueUrl; - this.queueName = this.node.stack.parseArn(props.queueArn).resource; - - if (props.keyArn) { - this.encryptionMasterKey = kms.EncryptionKey.import(this, 'Key', { - keyArn: props.keyArn - }); - } - } - - /** - * Export a queue - */ - public export() { - return this.props; - } -} diff --git a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts index 2c574c712f3c7..ae1c6e35d517b 100644 --- a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts +++ b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts @@ -98,7 +98,7 @@ export = { // WHEN const ref = queue.export(); - const imports = sqs.Queue.import(stack, 'Imported', ref); + const imports = sqs.Queue.fromQueueAttributes(stack, 'Imported', ref); // THEN @@ -158,7 +158,7 @@ export = { 'grants also work on imported queues'(test: Test) { const stack = new Stack(); - const queue = Queue.import(stack, 'Import', { + const queue = Queue.fromQueueAttributes(stack, 'Import', { queueArn: 'arn:aws:sqs:us-east-1:123456789012:queue1', queueUrl: 'https://queue-url' }); diff --git a/packages/@aws-cdk/aws-ssm/lib/parameter.ts b/packages/@aws-cdk/aws-ssm/lib/parameter.ts index 27708f0fc4c2b..a3bad500669b0 100644 --- a/packages/@aws-cdk/aws-ssm/lib/parameter.ts +++ b/packages/@aws-cdk/aws-ssm/lib/parameter.ts @@ -1,5 +1,5 @@ import iam = require('@aws-cdk/aws-iam'); -import { Construct, Fn, IResource, Resource, Token } from '@aws-cdk/cdk'; +import { CfnDynamicReference, CfnDynamicReferenceService, Construct, Fn, IResource, Resource, Token } from '@aws-cdk/cdk'; import ssm = require('./ssm.generated'); /** @@ -8,16 +8,19 @@ import ssm = require('./ssm.generated'); export interface IParameter extends IResource { /** * The ARN of the SSM Parameter resource. + * @attribute */ readonly parameterArn: string; /** * The name of the SSM Parameter resource. + * @attribute */ readonly parameterName: string; /** * The type of the SSM Parameter resource. + * @attribute */ readonly parameterType: string; @@ -42,6 +45,8 @@ export interface IParameter extends IResource { export interface IStringParameter extends IParameter { /** * The parameter value. Value must not nest another parameter. Do not use {{}} in the value. + * + * @attribute parameterValue */ readonly stringValue: string; } @@ -53,6 +58,8 @@ export interface IStringListParameter extends IParameter { /** * The parameter value. Value must not nest another parameter. Do not use {{}} in the value. Values in the array * cannot contain commas (``,``). + * + * @attribute parameterValue */ readonly stringListValue: string[]; } @@ -137,57 +144,28 @@ abstract class ParameterBase extends Resource implements IParameter { } } -export interface ParameterProps extends ParameterOptions { - /** - * The type of parameter. - */ - readonly type: ParameterType; - - /** - * The parameter value. - */ - readonly value: string; -} +const STRING_PARAM_TYPE = 'String'; +const STRINGLIST_PARAM_TYPE = 'StringList'; /** - * The type of the SSM parameter. - */ -export enum ParameterType { - STRING = 'String', - STRING_LIST = 'StringList' -} - -/** - * SSM parameter. - * - * Use `StringParameter` and `StringListParameter` for a strong-typed version. + * Creates a new String SSM Parameter. + * @resource AWS::SSM::Parameter */ -export class Parameter extends ParameterBase { - public readonly parameterName: string; - public readonly parameterType: string; - public readonly parameterValue: string; - - constructor(scope: Construct, id: string, props: ParameterProps) { - super(scope, id); +export class StringParameter extends ParameterBase implements IStringParameter { - const resource = new ssm.CfnParameter(this, 'Resource', { - allowedPattern: props.allowedPattern, - description: props.description, - name: props.name, - type: props.type, - value: props.value - }); + /** + * Imports an external string parameter. + */ + public static fromStringParameterName(scope: Construct, id: string, stringParameterName: string): IStringParameter { + class Import extends ParameterBase { + public readonly parameterName = stringParameterName; + public readonly parameterType = STRING_PARAM_TYPE; + public readonly stringValue = new CfnDynamicReference(CfnDynamicReferenceService.Ssm, stringParameterName).toString(); + } - this.parameterName = resource.parameterName; - this.parameterType = resource.parameterType; - this.parameterValue = resource.parameterValue; + return new Import(scope, id); } -} -/** - * Creates a new String SSM Parameter. - */ -export class StringParameter extends ParameterBase implements IStringParameter { public readonly parameterName: string; public readonly parameterType: string; public readonly stringValue: string; @@ -203,7 +181,7 @@ export class StringParameter extends ParameterBase implements IStringParameter { allowedPattern: props.allowedPattern, description: props.description, name: props.name, - type: 'String', + type: STRING_PARAM_TYPE, value: props.stringValue, }); @@ -215,8 +193,23 @@ export class StringParameter extends ParameterBase implements IStringParameter { /** * Creates a new StringList SSM Parameter. + * @resource AWS::SSM::Parameter */ export class StringListParameter extends ParameterBase implements IStringListParameter { + + /** + * Imports an external parameter of type string list. + */ + public static fromStringListParameterName(scope: Construct, id: string, stringListParameterName: string): IStringListParameter { + class Import extends ParameterBase { + public readonly parameterName = stringListParameterName; + public readonly parameterType = STRINGLIST_PARAM_TYPE; + public readonly stringListValue = Fn.split(',', new CfnDynamicReference(CfnDynamicReferenceService.Ssm, stringListParameterName).toString()); + } + + return new Import(scope, id); + } + public readonly parameterName: string; public readonly parameterType: string; public readonly stringListValue: string[]; @@ -236,7 +229,7 @@ export class StringListParameter extends ParameterBase implements IStringListPar allowedPattern: props.allowedPattern, description: props.description, name: props.name, - type: 'StringList', + type: STRINGLIST_PARAM_TYPE, value: props.stringListValue.join(','), }); diff --git a/packages/@aws-cdk/aws-ssm/package.json b/packages/@aws-cdk/aws-ssm/package.json index 5d7ac47e72d57..aa23f126af553 100644 --- a/packages/@aws-cdk/aws-ssm/package.json +++ b/packages/@aws-cdk/aws-ssm/package.json @@ -80,7 +80,9 @@ "exclude": [ "export:@aws-cdk/aws-ssm.IParameter", "import-props-interface:@aws-cdk/aws-ssm.ParameterImportProps", - "resource-attribute:@aws-cdk/aws-ssm.IParameter.parameterValue" + "resource-attribute:@aws-cdk/aws-ssm.IParameter.parameterValue", + "from-attributes:fromStringParameterAttributes", + "from-attributes:fromStringListParameterAttributes" ] } } diff --git a/packages/@aws-cdk/aws-ssm/test/test.parameter.ts b/packages/@aws-cdk/aws-ssm/test/test.parameter.ts index 00adf228b595f..51c1962a5162e 100644 --- a/packages/@aws-cdk/aws-ssm/test/test.parameter.ts +++ b/packages/@aws-cdk/aws-ssm/test/test.parameter.ts @@ -1,5 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); +import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import ssm = require('../lib'); @@ -122,5 +123,53 @@ export = { ]] }); test.done(); + }, + + 'StringParameter.fromName'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const param = ssm.StringParameter.fromStringParameterName(stack, 'MyParamName', 'MyParamName'); + + // THEN + test.deepEqual(stack.node.resolve(param.parameterArn), { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ssm:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':parameterMyParamName' ] ] + }); + test.deepEqual(stack.node.resolve(param.parameterName), 'MyParamName'); + test.deepEqual(stack.node.resolve(param.parameterType), 'String'); + test.deepEqual(stack.node.resolve(param.stringValue), '{{resolve:ssm:MyParamName}}'); + test.done(); + }, + + 'StringListParameter.fromName'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const param = ssm.StringListParameter.fromStringListParameterName(stack, 'MyParamName', 'MyParamName'); + + // THEN + test.deepEqual(stack.node.resolve(param.parameterArn), { + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ssm:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':parameterMyParamName' ] ] + }); + test.deepEqual(stack.node.resolve(param.parameterName), 'MyParamName'); + test.deepEqual(stack.node.resolve(param.parameterType), 'StringList'); + test.deepEqual(stack.node.resolve(param.stringListValue), { 'Fn::Split': [ ',', '{{resolve:ssm:MyParamName}}' ] }); + test.done(); } }; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts index 5dfc83837883c..53ac0026edb0a 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts @@ -16,7 +16,14 @@ export interface ActivityProps { * Define a new StepFunctions activity */ export class Activity extends Resource implements IStepFunctionsTaskResource { + /** + * @attribute + */ public readonly activityArn: string; + + /** + * @attribute + */ public readonly activityName: string; constructor(scope: Construct, id: string, props: ActivityProps = {}) { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 85ea5d41e1682..cf0161bc7cbd1 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -1,7 +1,7 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); -import { CfnOutput, Construct, IResource, Resource } from '@aws-cdk/cdk'; +import { Construct, IResource, Resource } from '@aws-cdk/cdk'; import { StateGraph } from './state-graph'; import { CfnStateMachine } from './stepfunctions.generated'; import { IChainable } from './types'; @@ -44,8 +44,12 @@ export class StateMachine extends Resource implements IStateMachine, events.IEve /** * Import a state machine */ - public static import(scope: Construct, id: string, props: StateMachineImportProps): IStateMachine { - return new ImportedStateMachine(scope, id, props); + public static fromStateMachineArn(scope: Construct, id: string, stateMachineArn: string): IStateMachine { + class Import extends Resource implements IStateMachine { + public readonly stateMachineArn = stateMachineArn; + } + + return new Import(scope, id); } /** @@ -55,6 +59,7 @@ export class StateMachine extends Resource implements IStateMachine, events.IEve /** * The name of the state machine + * @attribute */ public readonly stateMachineName: string; @@ -188,15 +193,6 @@ export class StateMachine extends Resource implements IStateMachine, events.IEve public metricStarted(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric('ExecutionsStarted', props); } - - /** - * Export this state machine - */ - public export(): StateMachineImportProps { - return { - stateMachineArn: new CfnOutput(this, 'StateMachineArn', { value: this.stateMachineArn }).makeImportValue().toString(), - }; - } } /** @@ -205,33 +201,7 @@ export class StateMachine extends Resource implements IStateMachine, events.IEve export interface IStateMachine extends IResource { /** * The ARN of the state machine + * @attribute */ readonly stateMachineArn: string; - - /** - * Export this state machine - */ - export(): StateMachineImportProps; -} - -/** - * Properties for an imported state machine - */ -export interface StateMachineImportProps { - /** - * The ARN of the state machine - */ - readonly stateMachineArn: string; -} - -class ImportedStateMachine extends Resource implements IStateMachine { - public readonly stateMachineArn: string; - constructor(scope: Construct, id: string, private readonly props: StateMachineImportProps) { - super(scope, id); - this.stateMachineArn = props.stateMachineArn; - } - - public export() { - return this.props; - } } diff --git a/packages/@aws-cdk/cdk/lib/fn.ts b/packages/@aws-cdk/cdk/lib/fn.ts index 807ec36b2b01d..1f4e799aa2004 100644 --- a/packages/@aws-cdk/cdk/lib/fn.ts +++ b/packages/@aws-cdk/cdk/lib/fn.ts @@ -11,7 +11,6 @@ import { unresolved } from './unresolved'; * http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html */ export class Fn { - /** * The ``Fn::GetAtt`` intrinsic function returns the value of an attribute * from a resource in the template. @@ -37,6 +36,10 @@ export class Fn { * @returns a token represented as a string */ public static join(delimiter: string, listOfValues: string[]): string { + if (listOfValues.length === 0) { + throw new Error(`FnJoin requires at least one value to be provided`); + } + return new FnJoin(delimiter, listOfValues).toString(); } @@ -50,6 +53,12 @@ export class Fn { * @returns a token represented as a string array */ public static split(delimiter: string, source: string): string[] { + + // short-circut if source is not a token + if (!Token.isToken(source)) { + return source.split(delimiter); + } + return new FnSplit(delimiter, source).toList(); } @@ -60,6 +69,10 @@ export class Fn { * @returns a token represented as a string */ public static select(index: number, array: string[]): string { + if (!Token.isToken(array)) { + return array[index]; + } + return new FnSelect(index, array).toString(); } diff --git a/packages/@aws-cdk/cdk/lib/token.ts b/packages/@aws-cdk/cdk/lib/token.ts index 4ce9792079931..27dd0d89ed065 100644 --- a/packages/@aws-cdk/cdk/lib/token.ts +++ b/packages/@aws-cdk/cdk/lib/token.ts @@ -20,12 +20,19 @@ export const RESOLVE_METHOD = 'resolve'; */ export class Token { /** - * Returns true if obj is a token (i.e. has the resolve() method or is a string - * that includes token markers), or it's a listifictaion of a Token string. + * @deprecated use `Token.isToken` + */ + public static unresolved(obj: any): boolean { + return unresolved(obj); + } + + /** + * Returns true if obj is a token (i.e. has the resolve() method or is a + * string or array which includes token markers). * * @param obj The object to test. */ - public static unresolved(obj: any): boolean { + public static isToken(obj: any): boolean { return unresolved(obj); } diff --git a/packages/@aws-cdk/cdk/test/test.fn.ts b/packages/@aws-cdk/cdk/test/test.fn.ts index 0c323e23a5f9d..da7399d171ecf 100644 --- a/packages/@aws-cdk/cdk/test/test.fn.ts +++ b/packages/@aws-cdk/cdk/test/test.fn.ts @@ -24,7 +24,17 @@ const tokenish = fc.array(nonEmptyString, 2, 2).map(arr => ({ [arr[0]]: arr[1] } const anyValue = fc.oneof(nonEmptyString, tokenish); export = nodeunit.testCase({ - FnJoin: { + 'eager resolution for non-tokens': { + 'Fn.select'(test: nodeunit.Test) { + test.deepEqual(Fn.select(2, [ 'hello', 'you', 'dude' ]), 'dude'); + test.done(); + }, + 'Fn.split'(test: nodeunit.Test) { + test.deepEqual(Fn.split(':', 'hello:world:yeah'), [ 'hello', 'world', 'yeah' ]); + test.done(); + } + }, + 'FnJoin': { 'rejects empty list of arguments to join'(test: nodeunit.Test) { test.throws(() => Fn.join('.', [])); test.done(); diff --git a/tools/awslint/bin/awslint.ts b/tools/awslint/bin/awslint.ts index 10ea203e9aef8..9533216736233 100644 --- a/tools/awslint/bin/awslint.ts +++ b/tools/awslint/bin/awslint.ts @@ -5,9 +5,10 @@ import fs = require('fs-extra'); import reflect = require('jsii-reflect'); import path = require('path'); import yargs = require('yargs'); -import { constructLinter, DiagnosticLevel, moduleLinter, resourceLinter } from '../lib'; +import { AggregateLinter, attributesLinter, cfnResourceLinter, constructLinter, DiagnosticLevel, importsLinter, moduleLinter, resourceLinter } from '../lib'; + +const linter = new AggregateLinter(moduleLinter, constructLinter, cfnResourceLinter, resourceLinter, importsLinter, attributesLinter); -const LINTERS = [ moduleLinter, constructLinter, resourceLinter ]; let stackTrace = false; async function main() { @@ -57,10 +58,8 @@ async function main() { const config = path.join(workdir, 'package.json'); if (command === 'list') { - for (const linter of LINTERS) { - for (const rule of linter.rules) { - console.info(`${colors.cyan(rule.code)}: ${rule.message}`); - } + for (const rule of linter.rules) { + console.info(`${colors.cyan(rule.code)}: ${rule.message}`); } return; } @@ -108,12 +107,10 @@ async function main() { const results = []; - for (const linter of LINTERS) { - results.push(...linter.eval(assembly, { - include: args.include, - exclude: args.exclude, - })); - } + results.push(...linter.eval(assembly, { + include: args.include, + exclude: args.exclude, + })); // Sort errors to the top (highest severity first) results.sort((a, b) => b.level - a.level); @@ -150,7 +147,7 @@ async function main() { } if (color) { - console.error(color(`${DiagnosticLevel[diag.level].toLowerCase()}: ${diag.message} [${colors.bold(diag.rule.code)}:${colors.bold(diag.scope)}]`)); + console.error(color(`${DiagnosticLevel[diag.level].toLowerCase()}: [${colors.bold(`awslint:${diag.rule.code}`)}:${colors.bold(diag.scope)}] ${diag.message}`)); } } diff --git a/tools/awslint/lib/linter.ts b/tools/awslint/lib/linter.ts index 04751e36a137b..1480c03774dde 100644 --- a/tools/awslint/lib/linter.ts +++ b/tools/awslint/lib/linter.ts @@ -15,14 +15,44 @@ export interface LinterOptions { exclude?: string[]; } +export abstract class LinterBase { + public abstract rules: Rule[]; + public abstract eval(assembly: reflect.Assembly, options: LinterOptions | undefined): Diagnostic[]; +} + +export class AggregateLinter extends LinterBase { + private linters: LinterBase[]; + + constructor(...linters: LinterBase[]) { + super(); + this.linters = linters; + } + + public get rules(): Rule[] { + const ret = new Array(); + for (const linter of this.linters) { + ret.push(...linter.rules); + } + return ret; + } + + public eval(assembly: reflect.Assembly, options: LinterOptions | undefined): Diagnostic[] { + const diags = new Array(); + for (const linter of this.linters) { + diags.push(...linter.eval(assembly, options)); + } + return diags; + } +} + /** * Evaluates a bunch of rules against some context. */ -export class Linter { - private readonly _rules: { [name: string]: Rule } = { }; +export class Linter extends LinterBase { + private readonly _rules: { [name: string]: ConcreteRule } = { }; constructor(private readonly init: (assembly: reflect.Assembly) => T | T[] | undefined) { - return; + super(); } public get rules() { @@ -32,7 +62,7 @@ export class Linter { /** * Install another rule. */ - public add(rule: Rule) { + public add(rule: ConcreteRule) { if (rule.code in this._rules) { throw new Error(`rule "${rule.code}" already exists`); } @@ -43,7 +73,7 @@ export class Linter { /** * Evaluate all rules against the context. */ - public eval(assembly: reflect.Assembly, options: LinterOptions | undefined): Array> { + public eval(assembly: reflect.Assembly, options: LinterOptions | undefined): Diagnostic[] { options = options || { }; let ctxs = this.init(assembly); @@ -55,7 +85,7 @@ export class Linter { ctxs = [ ctxs ]; } - const results = new Array>(); + const results = new Array(); for (const ctx of ctxs) { for (const rule of Object.values(this._rules)) { @@ -75,10 +105,10 @@ export class Linter { export class Evaluation { public readonly ctx: T; public readonly options: LinterOptions; - public diagnostics = new Array>(); - private readonly curr: Rule; + public diagnostics = new Array(); + private readonly curr: ConcreteRule; - constructor(ctx: T, rule: Rule, options: LinterOptions) { + constructor(ctx: T, rule: ConcreteRule, options: LinterOptions) { this.ctx = ctx; this.options = options; this.curr = rule; @@ -99,9 +129,8 @@ export class Evaluation { level = DiagnosticLevel.Error; } - const diag: Diagnostic = { + const diag: Diagnostic = { level, - ctx: this.ctx, rule: this.curr, scope, message, @@ -194,10 +223,13 @@ export class Evaluation { } -export interface Rule { +export interface Rule { code: string, message: string; warning?: boolean; +} + +export interface ConcreteRule extends Rule { eval(linter: Evaluation): void; } @@ -231,10 +263,9 @@ export enum DiagnosticLevel { Error, } -export interface Diagnostic { - ctx: T; +export interface Diagnostic { level: DiagnosticLevel; - rule: Rule + rule: Rule; scope: string; message: string; } diff --git a/tools/awslint/lib/rules/attributes.ts b/tools/awslint/lib/rules/attributes.ts new file mode 100644 index 0000000000000..532c1791627be --- /dev/null +++ b/tools/awslint/lib/rules/attributes.ts @@ -0,0 +1,37 @@ +import { Linter } from '../linter'; +import { Attribute, ResourceReflection } from './resource'; + +export const attributesLinter = new Linter(a => { + const result = new Array(); + for (const resource of ResourceReflection.findAll(a)) { + for (const attr of resource.attributes) { + result.push(new AttributeReflection(resource, attr)); + } + } + return result; +}); + +class AttributeReflection { + public readonly fqn: string; + + constructor(public readonly resource: ResourceReflection, public readonly attr: Attribute) { + this.fqn = resource.fqn + '.' + attr.name; + } +} + +attributesLinter.add({ + code: 'attribute-readonly', + message: 'attribute property must be readonly', + eval: e => { + e.assert(e.ctx.attr.property.immutable, e.ctx.fqn); + } +}); + +attributesLinter.add({ + code: 'attribute-tag', + message: 'attribute properties must have an "@attribute" doctag on: ', + eval: e => { + const tag = e.ctx.attr.property.docs.customTag('attribute'); + e.assert(tag, e.ctx.fqn, `${e.ctx.attr.property.parentType.fqn}.${e.ctx.attr.name}`); + } +}); diff --git a/tools/awslint/lib/rules/cfn-resource.ts b/tools/awslint/lib/rules/cfn-resource.ts new file mode 100644 index 0000000000000..a5b0655ff6a7f --- /dev/null +++ b/tools/awslint/lib/rules/cfn-resource.ts @@ -0,0 +1,115 @@ +import camelcase = require('camelcase'); +import reflect = require('jsii-reflect'); +import { Linter } from '../linter'; +import { CORE_MODULE } from './common'; +import { ConstructReflection } from './construct'; +import { ResourceReflection } from './resource'; +const CFN_RESOURCE_BASE_CLASS_FQN = `${CORE_MODULE}.CfnResource`; + +// this linter verifies that we have L2 coverage. it finds all "Cfn" classes and verifies +// that we have a corresponding L1 class for it that's identified as a resource. +export const cfnResourceLinter = new Linter(a => CfnResourceReflection.findAll(a)); + +cfnResourceLinter.add({ + code: 'resource-class', + message: 'every resource must have a resource class (L2)', + warning: true, + eval: e => { + const l2 = ResourceReflection.findAll(e.ctx.classType.assembly).find(r => r.cfn.fullname === e.ctx.fullname); + e.assert(l2, e.ctx.fullname); + } +}); + +export class CfnResourceReflection { + /** + * Finds a Cfn resource class by full CloudFormation resource name (e.g. `AWS::S3::Bucket`) + * @param fullName first two components are case-insensitive (e.g. `aws::s3::Bucket` is equivalent to `Aws::S3::Bucket`) + */ + public static findByName(sys: reflect.TypeSystem, fullName: string) { + const [ org, ns, resource ] = fullName.split('::'); + const fqn = `@aws-cdk/${org.toLocaleLowerCase()}-${ns.toLocaleLowerCase()}.Cfn${resource}`; + if (!sys.tryFindFqn(fqn)) { + return undefined; + } + const cls = sys.findClass(fqn); + return new CfnResourceReflection(cls); + } + + /** + * 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)); + } + + public readonly classType: reflect.ClassType; + public readonly fullname: string; // AWS::S3::Bucket + public readonly namespace: string; // AWS::S3 + public readonly basename: string; // Bucket + public readonly attributeNames: string[]; // (normalized) bucketArn, bucketName, queueUrl + public readonly attributePrefix: string; + public readonly doc: string; // link to CloudFormation docs + + constructor(cls: reflect.ClassType) { + this.classType = cls; + + this.basename = cls.name.substr('Cfn'.length); + + // HACK: extract full CFN name from initializer docs + const initializerDoc = (cls.initializer && cls.initializer.docs.docs.summary) || ''; + const out = /a new `([^`]+)`/.exec(initializerDoc); + const fullname = out && out[1]; + if (!fullname) { + throw new Error(`Unable to extract CloudFormation resource name from initializer documentation of ${cls}`); + } + + this.fullname = fullname; + + this.namespace = fullname.split('::').slice(0, 2).join('::'); + this.attributePrefix = this.basename[0].toLowerCase() + this.basename.slice(1); + + this.attributeNames = cls.ownProperties + .filter(p => (p.docs.docs.custom || {}).cloudformationAttribute) + .map(p => p.docs.customTag('cloudformationAttribute') || '') + .map(p => this.attributePropertyNameFromCfnName(p)); + + this.doc = cls.docs.docs.see || ''; + } + + private attributePropertyNameFromCfnName(name: string): string { + + // special case (someone was smart), special case copied from cfn2ts + if (this.basename === 'SecurityGroup' && name === 'GroupId') { + return 'securityGroupId'; + } + + const cfnName = name.startsWith(this.basename) ? name.slice(this.basename.length) : name; + + // if the CFN attribute name already have the type name as a prefix (i.e. RoleId), we only take the "Id" as the "name". + return this.attributePrefix + camelcase(cfnName, { pascalCase: true }); + } +} \ No newline at end of file diff --git a/tools/awslint/lib/rules/construct.ts b/tools/awslint/lib/rules/construct.ts index e6d18df31c6c4..500705432c1de 100644 --- a/tools/awslint/lib/rules/construct.ts +++ b/tools/awslint/lib/rules/construct.ts @@ -35,6 +35,8 @@ export class ConstructReflection { .map(c => new ConstructReflection(c)); } + public readonly ROOT_CLASS: reflect.ClassType; // cdk.Construct + public readonly fqn: string; public readonly interfaceFqn: string; public readonly propsFqn: string; @@ -42,19 +44,18 @@ export class ConstructReflection { public readonly propsType?: reflect.InterfaceType; public readonly initializer?: reflect.Initializer; public readonly hasPropsArgument: boolean; - public readonly rootClass: reflect.ClassType; // cdk.Construct public readonly sys: reflect.TypeSystem; constructor(public readonly classType: reflect.ClassType) { this.fqn = classType.fqn; this.sys = classType.system; + this.ROOT_CLASS = this.sys.findClass(CONSTRUCT_FQN); this.interfaceFqn = `${classType.assembly.name}.I${classType.name}`; this.propsFqn = `${classType.assembly.name}.${classType.name}Props`; this.interfaceType = this.tryFindInterface(); this.propsType = this.tryFindProps(); this.initializer = classType.initializer; this.hasPropsArgument = this.initializer != null && this.initializer.parameters.length >= 3; - this.rootClass = this.sys.findClass(CONSTRUCT_FQN); } private tryFindInterface() { diff --git a/tools/awslint/lib/rules/imports.ts b/tools/awslint/lib/rules/imports.ts new file mode 100644 index 0000000000000..d140986ec7449 --- /dev/null +++ b/tools/awslint/lib/rules/imports.ts @@ -0,0 +1,113 @@ +import reflect = require('jsii-reflect'); +import { Linter } from '../linter'; +import { AttributeSite, ResourceReflection } from './resource'; + +export const importsLinter = new Linter(assembly => ResourceReflection + .findAll(assembly) + .filter(r => r.construct && r.construct.interfaceType) // only resources that have an interface can have "fromXxx" methods + .map(construct => new ImportsReflection(construct))); + +class ImportsReflection { + public readonly fromMethods: reflect.Method[]; + public readonly prefix: string; + public readonly fromAttributesMethodName: string; + public readonly fromAttributesMethod?: reflect.Method; + public readonly attributesStructName: string; + public readonly attributesStruct?: reflect.InterfaceType; + + constructor(public readonly resource: ResourceReflection) { + const sys = resource.sys; + this.prefix = `from${resource.basename}`; + const classType = resource.construct.classType; + this.fromAttributesMethodName = `${this.prefix}Attributes`; + this.fromAttributesMethod = classType.allMethods.find(x => x.name === this.fromAttributesMethodName); + + this.fromMethods = classType.allMethods.filter(x => + x.static + && x.name.match(`^${this.prefix}[A-Z]`) + && x.name !== this.fromAttributesMethodName); + + const attributesStructFqn = `${classType.fqn}Attributes`; + const attributesStruct = sys.tryFindFqn(attributesStructFqn); + if (attributesStruct) { + if (!attributesStruct.isInterfaceType() || !attributesStruct.isDataType()) { + throw new Error(`Attributes type ${attributesStructFqn} must be an interface struct`); + } + } + this.attributesStructName = attributesStructFqn; + this.attributesStruct = attributesStruct; + } +} + +importsLinter.add({ + code: 'no-static-import', + message: 'static "import" methods are deprecated in favor of "fromAttributes" (see guidelines)', + eval: e => { + const hasImport = e.ctx.resource.construct.classType.allMethods.find(x => x.static && x.name === 'import'); + e.assert(!hasImport, e.ctx.resource.fqn + '.import'); + } +}); + +importsLinter.add({ + code: 'from-method', + message: 'resource should have at least one "fromXxx" static method or "fromXxxAttributes"', + eval: e => { + // no attributes are defined on the interface, so we don't expect any "from" methods. + if (!e.ctx.resource.attributes.some(a => a.site === AttributeSite.Interface)) { + return; + } + + e.assert(e.ctx.fromMethods.length > 0 || e.ctx.fromAttributesMethod, e.ctx.resource.fqn); + } +}); + +importsLinter.add({ + code: 'from-signature', + message: 'invalid method signature for fromXxx method', + eval: e => { + for (const method of e.ctx.fromMethods) { + + // "fromRoleArn" => "roleArn" + const argName = e.ctx.resource.basename[0].toLocaleLowerCase() + method.name.slice('from'.length + 1); + + e.assertSignature(method, { + parameters: [ + { name: 'scope', type: e.ctx.resource.construct.ROOT_CLASS }, + { name: 'id', type: 'string' }, + { name: argName, type: 'string' } + ], + returns: e.ctx.resource.construct.interfaceType + }); + } + } +}); + +importsLinter.add({ + code: 'from-attributes', + message: 'static fromXxxAttributes is a factory of IXxx from its primitive attributes', + eval: e => { + if (!e.ctx.fromAttributesMethod) { + return; + } + + e.assertSignature(e.ctx.fromAttributesMethod, { + parameters: [ + { name: 'scope', type: e.ctx.resource.construct.ROOT_CLASS }, + { name: 'id', type: 'string' }, + { name: 'attrs', type: e.ctx.attributesStruct } + ] + }); + } +}); + +importsLinter.add({ + code: 'from-attributes-struct', + message: 'resource should have an XxxAttributes struct', + eval: e => { + if (!e.ctx.fromAttributesMethod) { + return; // no "fromAttributes" method + } + + e.assert(e.ctx.attributesStruct, e.ctx.attributesStructName); + } +}); diff --git a/tools/awslint/lib/rules/index.ts b/tools/awslint/lib/rules/index.ts index 33524721968fe..3afad529f9eca 100644 --- a/tools/awslint/lib/rules/index.ts +++ b/tools/awslint/lib/rules/index.ts @@ -1,3 +1,6 @@ export * from './construct'; export * from './module'; -export * from './resource'; \ No newline at end of file +export * from './resource'; +export * from './imports'; +export * from './cfn-resource'; +export * from './attributes'; diff --git a/tools/awslint/lib/rules/module.ts b/tools/awslint/lib/rules/module.ts index 25a792cae029e..1b631015d2cf4 100644 --- a/tools/awslint/lib/rules/module.ts +++ b/tools/awslint/lib/rules/module.ts @@ -1,6 +1,6 @@ import reflect = require('jsii-reflect'); import { Linter } from '../linter'; -import { findCfnResources } from './resource'; +import { CfnResourceReflection } from './cfn-resource'; interface ModuleLinterContext { readonly assembly: reflect.Assembly; @@ -8,7 +8,7 @@ interface ModuleLinterContext { } export const moduleLinter = new Linter(assembly => { - const cfnResources = findCfnResources(assembly); + const cfnResources = CfnResourceReflection.findAll(assembly); if (cfnResources.length === 0) { return undefined; // no resources } diff --git a/tools/awslint/lib/rules/resource.ts b/tools/awslint/lib/rules/resource.ts index e3da0f7f482aa..9682e21af81aa 100644 --- a/tools/awslint/lib/rules/resource.ts +++ b/tools/awslint/lib/rules/resource.ts @@ -1,115 +1,121 @@ import reflect = require('jsii-reflect'); import { Linter } from '../linter'; +import { CfnResourceReflection } from './cfn-resource'; import { CORE_MODULE } from './common'; import { ConstructReflection } from './construct'; +import { getDocTag } from './util'; -const CFN_RESOURCE_BASE_CLASS_FQN = `${CORE_MODULE}.CfnResource`; const RESOURCE_BASE_CLASS_FQN = `${CORE_MODULE}.Resource`; const RESOURCE_BASE_INTERFACE_FQN = `${CORE_MODULE}.IResource`; const GRANT_RESULT_FQN = '@aws-cdk/aws-iam.Grant'; -export const resourceLinter = new Linter(assembly => findCfnResources(assembly)); +export const resourceLinter = new Linter(a => ResourceReflection.findAll(a)); -export class ResourceReflection { - public static isResourceConstruct(construct: ConstructReflection) { - const classType = construct.classType; - const baseResource = classType.system.findClass(RESOURCE_BASE_CLASS_FQN); - return classType.extends(baseResource); - } +export interface Attribute { + site: AttributeSite; + property: reflect.Property; + name: string; // bucketArn +} +export enum AttributeSite { + Interface = 'interface', + Class = 'class' +} + +export class ResourceReflection { /** - * @returns all resource constructs + * @returns all resource constructs (everything that extends `cdk.Resource`) */ - public static findAllResources(assembly: reflect.Assembly) { - return findCfnResources(assembly).filter(r => r.hasConstruct); + public static findAll(assembly: reflect.Assembly) { + if (!assembly.system.assemblies.find(a => a.name === CORE_MODULE)) { + 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')) + .map(c => new ResourceReflection(c)); } - public readonly fullname: string; // AWS::S3::Bucket - public readonly namespace: string; // AWS::S3 - public readonly basename: string; // Bucket - public readonly doc: string; // link to CloudFormation docs - public readonly attributeNames: string[]; // bucketArn, bucketName, queueUrl - public readonly attributes: reflect.Property[]; // actual attribute props + public readonly attributes: Attribute[]; // actual attribute props public readonly fqn: string; // expected fqn of resource class public readonly assembly: reflect.Assembly; public readonly sys: reflect.TypeSystem; - - private readonly _construct?: ConstructReflection; // the resource construct - - constructor(public readonly cfnResource: reflect.ClassType) { - this.sys = cfnResource.system; - this.basename = cfnResource.name.substr('Cfn'.length); - this.doc = cfnResource.docs.docs.see || ''; - - // HACK: extract full CFN name from initializer docs - const initializerDoc = (cfnResource.initializer && cfnResource.initializer.docs.docs.summary) || ''; - const out = /a new `([^`]+)`/.exec(initializerDoc); - const fullname = out && out[1]; - if (!fullname) { - throw new Error(`Unable to extract CloudFormation resource name from initializer documentation of ${cfnResource}`); - } - this.fullname = fullname; - this.namespace = fullname.split('::').slice(0, 2).join('::'); - this.attributeNames = parseResourceAttributes(cfnResource); - - function parseResourceAttributes(cfnResourceClass: reflect.ClassType) { - return cfnResourceClass.ownProperties.filter(p => (p.docs.docs.custom || {}).cloudformationAttribute).map(p => p.name); + public readonly cfn: CfnResourceReflection; + public readonly basename: string; // i.e. Bucket + + constructor(public readonly construct: ConstructReflection) { + this.assembly = construct.classType.assembly; + this.sys = this.assembly.system; + + const cfn = tryResolveCfnResource(construct.classType); + if (!cfn) { + throw new Error(`Cannot find L1 class for L2 ${construct.fqn}. ` + + `Is "${guessResourceName(construct.fqn)}" an actual CloudFormation resource. ` + + `If not, use the "@resource" doc tag to indicate the full resource name (e.g. "@resource AWS::Route53::HostedZone")`); } - this.assembly = cfnResource.assembly; - - this.fqn = `${this.assembly.name}.${this.basename}`; - this._construct = ConstructReflection - .findAllConstructs(this.assembly) - .find(c => c.fqn === this.fqn); - + this.cfn = cfn; + this.basename = construct.classType.name; + this.fqn = construct.fqn; this.attributes = this.findAttributeProperties(); } - public get construct(): ConstructReflection { - if (!this._construct) { - throw new Error(`Resource ${this.fullname} does not have a corresponding AWS construct`); - } - return this._construct; - } + /** + * Attribute properties are all the properties that begin with the type name (e.g. bucketXxx). + */ + private findAttributeProperties(): Attribute[] { + const result = new Array(); + + for (const p of this.construct.classType.allProperties) { + // 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'); + if (!p.name.startsWith(this.cfn.attributePrefix) && !tag) { + continue; + } - public get hasConstruct(): boolean { - return !!this._construct; - } + // if there's an `@attribute` doc tag with a value other than "true" + // it should be used as the attribute name instead of the property name + const propertyName = (tag && tag !== 'true') ? tag : p.name; - private findAttributeProperties() { - if (!this.hasConstruct) { - return []; - } + // check if this attribute is defined on an interface or on a class + const property = findDeclarationSite(p); + const site = property.parentType.isInterfaceType() ? AttributeSite.Interface : AttributeSite.Class; - const resiult = new Array(); - for (const attr of this.attributeNames) { - const attribute = this.construct.classType.allProperties.find(p => p.name === attr); - if (attribute) { - resiult.push(attribute); - } + result.push({ + site, + name: propertyName, + property + }); } - return resiult; + return result; } } -resourceLinter.add({ - code: 'resource-class', - message: 'every resource must have a resource class (L2)', - warning: true, - eval: e => { - e.assert(e.ctx.hasConstruct, e.ctx.fqn); +function findDeclarationSite(prop: reflect.Property): reflect.Property { + if (!prop.overrides || (!prop.overrides.isClassType() && !prop.overrides.isInterfaceType())) { + if (!prop.parentType.isClassType() && !prop.parentType.isInterfaceType()) { + throw new Error('invalid parent type'); + } + return prop; } -}); + + const overridesProp = prop.overrides.allProperties.find(p => p.name === prop.name); + if (!overridesProp) { + throw new Error(`Cannot find property ${prop.name} in override site ${prop.overrides.fqn}`); + } + return findDeclarationSite(overridesProp); +} resourceLinter.add({ code: 'resource-class-extends-resource', message: `resource classes must extend "cdk.Resource" directly or indirectly`, eval: e => { - if (!e.ctx.hasConstruct) { return; } - const resourceBase = e.ctx.sys.findClass(RESOURCE_BASE_CLASS_FQN); e.assert(e.ctx.construct.classType.extends(resourceBase), e.ctx.construct.fqn); } @@ -120,7 +126,6 @@ resourceLinter.add({ warning: true, message: 'every resource must have a resource interface', eval: e => { - if (!e.ctx.hasConstruct) { return; } e.assert(e.ctx.construct.interfaceType, e.ctx.construct.fqn); } }); @@ -129,7 +134,7 @@ resourceLinter.add({ code: 'resource-interface-extends-resource', message: 'construct interfaces of AWS resources must extend cdk.IResource', eval: e => { - const resourceInterface = e.ctx.hasConstruct && e.ctx.construct.interfaceType; + const resourceInterface = e.ctx.construct.interfaceType; if (!resourceInterface) { return; } const interfaceBase = e.ctx.sys.findInterface(RESOURCE_BASE_INTERFACE_FQN); @@ -139,24 +144,11 @@ resourceLinter.add({ resourceLinter.add({ code: 'resource-attribute', - message: 'resources must represent all attributes as properties', + message: 'resources must represent all cloudformation attributes as attribute properties. missing property: ', eval: e => { - const resourceInterface = e.ctx.hasConstruct && e.ctx.construct.interfaceType; - if (!resourceInterface) { return; } - - for (const name of e.ctx.attributeNames) { + for (const name of e.ctx.cfn.attributeNames) { const found = e.ctx.attributes.find(a => a.name === name); - e.assert(found, `${e.ctx.fqn}.${name}`); - } - } -}); - -resourceLinter.add({ - code: 'resource-attribute-immutable', - message: 'resource attributes must be immutable (readonly)', - eval: e => { - for (const att of e.ctx.attributes) { - e.assert(att.immutable, att.parentType.fqn + '.' + att.name); + e.assert(found, `${e.ctx.fqn}.${name}`, name); } } }); @@ -165,8 +157,6 @@ resourceLinter.add({ code: 'grant-result', message: `"grant" method must return ${GRANT_RESULT_FQN}`, eval: e => { - if (!e.ctx.hasConstruct) { return; } - const grantResultType = e.ctx.sys.findFqn(GRANT_RESULT_FQN); const grantMethods = e.ctx.construct.classType.allMethods.filter(m => m.name.startsWith('grant')); @@ -178,37 +168,42 @@ resourceLinter.add({ } }); -/** - * Given a jsii assembly, extracts all CloudFormation resources from CFN classes - */ -export function findCfnResources(assembly: reflect.Assembly): ResourceReflection[] { - return assembly.classes.filter(c => isCfnResource(c)).map(layer1 => { - return new ResourceReflection(layer1); - }); - - function isCfnResource(c: reflect.ClassType) { - if (!c.system.includesAssembly(CORE_MODULE)) { - return false; - } +function tryResolveCfnResource(resourceClass: reflect.ClassType): CfnResourceReflection | undefined { + const sys = resourceClass.system; - // skip CfnResource itself - if (c.fqn === CFN_RESOURCE_BASE_CLASS_FQN) { - return false; - } + // if there is a @resource doc tag, it takes precedece + const tag = resourceClass.docs.customTag('resource'); + if (tag) { + return CfnResourceReflection.findByName(sys, tag); + } - if (!ConstructReflection.isConstructClass(c)) { - return false; + // parse the FQN of the class name and see if we can find a matching CFN resource + const guess = guessResourceName(resourceClass.fqn); + if (guess) { + const cfn = CfnResourceReflection.findByName(sys, guess); + if (cfn) { + return cfn; } + } - const cfnResourceClass = c.system.findFqn(CFN_RESOURCE_BASE_CLASS_FQN); - if (!c.extends(cfnResourceClass)) { - return false; + // try to resolve through ancestors + for (const base of resourceClass.getAncestors()) { + const ret = tryResolveCfnResource(base); + if (ret) { + return ret; } + } - if (!c.name.startsWith('Cfn')) { - return false; - } + // failed misrably + return undefined; +} - return true; - } +function guessResourceName(fqn: string) { + const match = /@aws-cdk\/([a-z]+)-([a-z0-9]+)\.([A-Z][a-zA-Z0-9]+)/.exec(fqn); + if (!match) { return undefined; } + + const [ , org, ns, rs ] = match; + if (!org || !ns || !rs) { return undefined; } + + return `${org}::${ns}::${rs}`; } diff --git a/tools/awslint/lib/rules/util.ts b/tools/awslint/lib/rules/util.ts new file mode 100644 index 0000000000000..9a2fb18436bb3 --- /dev/null +++ b/tools/awslint/lib/rules/util.ts @@ -0,0 +1,41 @@ +import reflect = require('jsii-reflect'); + +/** + * Returns a documentation tag. Looks it up in inheritance hierarchy. + * @param documetable starting point + * @param tag the tag to search for + */ +export function getDocTag(documetable: reflect.Documentable, tag: string): string | undefined { + const t = documetable.docs.customTag(tag); + if (t) { return t; } + + if ((documetable instanceof reflect.Property || documetable instanceof reflect.Method) && documetable.overrides) { + if (documetable.overrides.isClassType() || documetable.overrides.isInterfaceType()) { + const baseMembers = documetable.overrides.allMembers.filter(m => m.name === documetable.name); + for (const base of baseMembers) { + const baseTag = getDocTag(base, tag); + if (baseTag) { + return baseTag; + } + } + } + } + + if (documetable instanceof reflect.ClassType || documetable instanceof reflect.InterfaceType) { + for (const base of documetable.interfaces) { + const baseTag = getDocTag(base, tag); + if (baseTag) { + return baseTag; + } + } + } + + if (documetable instanceof reflect.ClassType && documetable.base) { + const baseTag = getDocTag(documetable.base, tag); + if (baseTag) { + return baseTag; + } + } + + return undefined; +} \ No newline at end of file diff --git a/tools/awslint/package.json b/tools/awslint/package.json index 4cf336e704d21..a6220dbba9ddd 100644 --- a/tools/awslint/package.json +++ b/tools/awslint/package.json @@ -14,6 +14,7 @@ "awslint": "bin/awslint" }, "dependencies": { + "camelcase": "^5.3.1", "colors": "^1.3.3", "fs-extra": "^7.0.1", "jsii-reflect": "^0.10.3", diff --git a/tslint.yaml b/tslint.yaml index 631e584de5971..ec1fb108404e5 100644 --- a/tslint.yaml +++ b/tslint.yaml @@ -48,4 +48,8 @@ rules: variable-name: [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"] # Unhandled promises are the source of all kinds of bugs and race conditions... - no-floating-promises: true + no-floating-promises: ["Promise"] + + # We require empty interfaces for AWS construct guideline compliance + no-empty-interface: false +