From a43db2808739204c4692719e1f48a65b4716a2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Thu, 8 Nov 2018 16:24:43 +0100 Subject: [PATCH 01/15] chore: Specify a Cloud Assembly Looking forward to supporting a more integrated deployment experience with support of multiple stacks (with deployment order constraints honored) as well as assets, this proposes a specification for a *Cloud Assembly* that determines a document storage format. Fixes: #956 --- specifications/cloud_assembly.md | 390 +++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 specifications/cloud_assembly.md diff --git a/specifications/cloud_assembly.md b/specifications/cloud_assembly.md new file mode 100644 index 0000000000000..fa210656b7647 --- /dev/null +++ b/specifications/cloud_assembly.md @@ -0,0 +1,390 @@ +# Cloud Assembly Specification, Version 1.0 + +The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, +**RECOMMENDED**, **MAY**, and **OPTIONAL** in this document are to be interpreted as described in [RFC 2119] when they +are spelled out in bold, capital letters (as they are shown here). + +## Introduction +A *Cloud Assembly* is a document container fail designed to hold the components of *cloud-native* applications, +including all the parts that are needed in order to deploy those to *the cloud*. This document exposes the specification +of the *Cloud Assembly* format as well as requirements imposed on *Cloud Assemblers* and *Cloud Runtimes*. + +### Goals +The design goals for the *Cloud Assembly Specification* are the following: +* The *Cloud Assembly Specification* is extensible. +* The *Cloud Assembly Specification* is cloud-agnostic. +* The *Cloud Assembly Specification* is easy to implement and use. +* The *Cloud Assembly Specification* supports authenticity and integrity guarantees. +* A *Cloud Assembly* is self-contained, making deployments reproductible. + +## Specification +A *Cloud Assembly* is a ZIP archive that **SHOULD** conform to the [ISO/IEC 21320-1:2015] *Document Container File* +standard. Use of the `deflate` compression method is **RECOMMENDED** in order to minimize the size of the resulting +file. *Cloud Assembly* files **SHOULD** use the `.cloud` extension in order to make them easier to recognize by users. + +Documents in the archive can be stored with any name and directory structure, however the following entries at the root +of the archive are reserved for special use: +* `manifest.json` **MUST** be present and contains the [manifest document](#manifest-document) for the *Cloud Assembly*. +* `signature.asc`, when present, **MUST** contain the [digital signature](#digital-signature) of the *Cloud Assembly*. + +### Manifest Document +The `manifest.json` file is the entry point of the *Cloud Assembly*. It **MUST** be a valid [JSON] document composed of +a single `object` that conforms to the following schema: + +Key |Type |Required|Description +--------------|---------------------|:------:|----------- +`schema` |`string` |Required|The schema for the document. **MUST** be `cloud-assembly/1.0`. +`drops` |`Map` |Required|A mapping of [*Logical ID*](#logical-id) to [`Drop`](#drop). +`missing` |`Map`| |A mapping of context keys to [missing information](#missing). + +The [JSON] specification allows for keys to be specified multiple times in a given `object`. However, *Cloud Assembly* +consumers **MAY** assume keys are unique, and [*Cloud Assemblers*](#cloud-assemblers) **SHOULD** avoid generating +duplicate keys. If duplicate keys are present, the latest specified value **SHOULD** be preferred. + +### Logical ID +*Logical IDs* are `string`s that uniquely identify [`Drop`](#drop)s in the context of a *Cloud Assembly*. +* A *Logical ID* **MUST NOT** be empty. +* A *Logical ID* **SHOULD NOT** exceed `256` characters. +* A *Logical ID* **MUST** be composed of only the following ASCII printable characters: + + Upper-case letters: `A` (`0x41`) through `Z` (`0x5A`) + + Lower-case letters: `a` (`0x61`) through `z` (`0x7A`) + + Numeric characters: `0` (`0x30`) through `9` (`0x39`) + + Plus: `+` (`0x2B`) + + Minus: `-` (`0x2D`) + + Forward-slash: `/` (`0x2F`) + + Underscore: `_` (`0x5F`) + +### `Drop` +`Drop`s are the building blocks of *Cloud Assemblies*. They model a part of the *cloud-native* application that can be +deployed independently, provided it's dependencies are fulfilled. `Drop`s are specified using [JSON] objects that +**MUST** conform to the following schema: + +Key |Type |Required|Description +-------------|-----------------|:------:|----------- +`type` |`string` |Required|The [*Drop Type*](#drop-type) specifier of this `Drop`. +`environment`|`string` |required|The [environment](#environment) specifier for this `Drop`. +`metadata` |`Map`| |Arbitrary key-value pairs associated with this `Drop`. +`properties` |`Map`| |The properties of this `Drop` as documented by its maintainers. + +Each [`Drop` Type](#drop-type) can produce outputs that can be used in order to allow other `Drop`s to consume the +resources they procude. Each `Drop` implementer is responsible to document the output attributes it supports. References +to these outputs are modeled using special `string` tokens within entries of the `properties` section of `Drop`s: + +``` +${LogicalId.attributeName} + ╰───┬───╯ ╰─────┬─────╯ + │ └─ The name of the output attribute + └───────────── The Logical ID of the Drop +``` + +The following escape sequences are valid: +* `\\` encodes the `\` literal +* `\${` encodes the `${` literal + +Deployment systems **SHOULD** return an error upon encountering an occurrence of the `/` literal that is not part of a +valid escape sequence. + +#### Drop Type +Every `Drop` has a type specifier, which allows *Cloud Assembly* consumers to know how to deploy it. The type specifiers +are `string`s that use an URI-like syntax (`protocol://path`), providing the coordinates to a reference implementation +for the `Drop` behavior. + +Deployment systems **MUST** support at least one protocol, and **SHOULD** support all the protocols specified in +the following sub-sections. + +##### The `npm` protocol +Type specifiers using the `npm` protocol have the following format: +``` +npm://[@namespace/]package/ClassName[@version] +╰┬╯ ╰────┬────╯ ╰──┬──╯ ╰───┬───╯ ╰──┬──╯ + │ │ │ │ └─ Optional version specifier + │ │ │ └─────────── Fully qualified name of the Handler class + │ │ └──────────────────── Name of the NPM package + │ └────────────────────────────── Optional NPM namespace + └───────────────────────────────────────── NPM protocol specifier +``` + +#### Environment +`Environment`s help Deployment systems determine where to deploy a particular `Drop`. Thy are `string`s that use an +URI-like syntax (`protocol://path`). + +Deployment systems **MUST** support at least one protocol, and **SHOULD** support all the protocols specified in the +following sub-sections. + +##### The `aws` protocol +Environments using the `aws` protocol have the following format: +``` +aws://account/region +╰┬╯ ╰──┬──╯ ╰──┬─╯ + │ │ └─ The name of an AWS region (e.g: eu-west-1) + │ └───────── An AWS account ID (e.g: 123456789012) + └───────────────── AWS protocol specifier +``` + +### `Missing` +[`Drop`s](#drop) may require contextual information to be available in order to correctly participate in a +*Cloud Assembly*. When information is missing, *Cloud Assembly* producers report the missing information by adding +entries to the `missing` section of the [manifest document](#manifest-document). The values are [JSON] `object`s that +**MUST** conform to the following schema: + +Key |Type |Required|Description +---------------|-----------------|:------:|----------- +`provider` |`string` |Required|A tag that identifies the entity that should provide the information. +`props` |`Map`|Required|Properties that are required in order to obtain the missing information. + +### Digital Signature +#### Signing +*Cloud Assemblers* **SHOULD** support digital signature of *Cloud Assemblies*. When support for digital signature is +present, *Cloud Assemblers*: +* **MUST** require the user to specify which [PGP][RFC 4880] key should be used. + +##### Signing Algorithm + + +#### Verifying +Deployment systems **SHOULD** support verifying signed *Cloud Assemblies*. If support for signature verification is not +present, a warning **MUST** be emitted when processing a *Cloud Assembly* that contains the `signature.asc` file. + +Deployment systems that support verifying signed *Cloud Assemblies*: +* **SHOULD** allow the user to *require* that an assembly is signed. When this requirement is active, an error **MUST** + be returned when attempting to deploy an un-signed *Cloud Assembly*. +* **MUST** verify the integrity and authenticity of signed *Cloud Assemblies* prior to attempting to load any file + included in it, except for `signature.asc`. + * An error **MUST** be raised if the *Cloud Assembly*'s integirty is not verified by the signature. + * An error **MUST** be raised if the [PGP][RFC 4880] key has expired according to the signature timestamp. + * An error **MUST** be raised if the [PGP][RFC 4880] key is known to have been revoked. Deployment systems **MAY** + trust locally available information pertaining to the key's validity. +* **SHOULD** allow the user to specify a list of trusted [PGP][RFC 4880] keys. + +## Annex +### Examples of `Drop`s for the AWS Cloud +#### `@aws-cdk/aws-cloudformation.StackDrop` +A [*CloudFormation* stack][CFN Stack]. + +##### Properties +Property |Type |Required|Description +-------------|--------------------|:------:|----------- +`template` |`string` |Required|The assembly-relative path to the *CloudFormation* template document. +`parameters` |`Map`| |Parameters to be passed to the [stack][CFN Stack] upon deployment. +`stackPolicy`|`string` | |The assembly-relative path to the [Stack Policy][CFN Stack Policy]. + +##### Output Attributes +Attribute |Type |Description +----------|--------------------|----------- +`outputs` |`Map`|Data returned by [*CloudFormation* Outputs][CFN Outputs] of the stack. +`stackArn`|`string` |The ARN of the [stack][CFN Stack]. + +##### Example +```json +{ + "type": "npm://@aws-cdk/aws-cloudformation.StackDrop", + "environment": "aws://000000000000/bermuda-triangle-1", + "properties": { + "template": "my-stack/template.yml", + "parameters": { + "bucketName": "${helperStack.bucketName}", + "objectKey": "${helperStack.objectKey}" + }, + "stackPolicy": "my-stack/policy.json" + } +} +``` + +[CFN Stack]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacks.html +[CFN Stack Policy]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html +[CFN Outputs]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html + +#### `@aws-cdk/assets.FileDrop` +A file that needs to be uploaded to an *S3* bucket. + +##### Properties +Property |Type |Required|Description +------------|--------|:------:|----------- +`file` |`string`|Required|The assembly-relative path to the file that will be uploaded. +`bucketName`|`string`|Required|The name of the bucket where this file will be uploaded. +`objectKey` |`string`|Required|The key at which to place the object in the bucket. + +##### Output Attributes +Attribute |Type |Description +------------|--------|----------- +`bucketName`|`string`|The name of the bucket where the file was uploaded. +`objectKey` |`string`|The key at which the file was uploaded in the bucket. + +##### Example +```json +{ + "type": "npm://@aws-cdk/assets.FileDrop", + "environment": "aws://000000000000/bermuda-triangle-1", + "properties": { + "file": "assets/file.bin", + "bucket": "${helperStack.bucketName}", + "objectKey": "assets/da39a3ee5e6b4b0d3255bfef95601890afd80709/nifty-asset.png" + } +} +``` + +#### `@aws-cdk/aws-ecr.DockerImageDrop` +A Docker image to be published to an *ECR* registry. + +##### Properties +Property |Type |Required|Description +------------|--------|:------:|----------- +`savedImage`|`string`|Required|The assembly-relative path to the tar archive obtained from `docker image save`. +`imageName` |`string`|Required|The name of the image (e.g: `000000000000.dkr.ecr.bermuda-triangle-1.amazon.com/name`). +`tagName` |`string`| |The name of the tag to use when pushing the image (default: `latest`). + +##### Output Attributes +Attribute |Type |Description +--------------|--------|----------- +`exactImageId`|`string`|An absolute reference to the published image version (`imageName@DIGEST`). +`imageName` |`string`|The full tagged image name (`imageName:tagName`). + +##### Example +```json +{ + "type": "npm://@aws-cdk/aws-ecr.DockerImageDrop", + "environment": "aws://000000000000/bermuda-triangle-1", + "properties": { + "savedImage": "docker/37e6de0b24fa.tar", + "imageName": "${helperStack.ecrImageName}", + "tagName": "latest" + } +} +``` + +### Example +Here is an example the contents of a complete *Cloud Assembly* that deploys AWS resources: +``` +☁️ my-assembly.cloud +├─ manifest.json Cloud Assembly manifest +├─ stacks +│ ├─ PipelineStack.yml CloudFormation template +│ ├─ ServiceStack-beta.yml CloudFormation template +│ ├─ ServiceStack-beta.stack-policy.json CloudFormation stack policy +│ ├─ ServiceStack-prod.yml CloudFormation template +│ └─ ServiceStack-prod.stack-policy.json CloudFormation stack policy +├─ docker +│ └─ docker-image.tar Saved Docker image (docker image save) +├─ assets +│ └─ static-website Files for a static website +│ ├─ index.html +│ └─ style.css +└─ signature.asc Cloud Assembly digital signature +``` + +#### `manifest.json` +```json +{ + "schema": "cloud-assembly/1.0", + "drops": { + "PipelineStack": { + "type": "npm://@aws-cdk/aws-cloudformation.StackDrop", + "environment": "aws://123456789012/eu-west-1", + "properties": { + "template": "stacks/PipelineStack.yml" + } + }, + "ServiceStack-beta": { + "type": "npm://@aws-cdk/aws-cloudformation.StackDrop", + "environment": "aws://123456789012/eu-west-1", + "properties": { + "template": "stacks/ServiceStack-beta.yml", + "stackPolicy": "stacks/ServiceStack-beta.stack-policy.json", + "parameters": { + "image": "${DockerImage.exactImageId}", + "websiteFilesBucket": "${StaticFiles.bucketName}", + "websiteFilesKeyPrefix": "${StaticFiles.keyPrefix}", + } + } + }, + "ServiceStack-prod": { + "type": "npm://@aws-cdk/aws-cloudformation.StackDrop", + "environment": "aws://123456789012/eu-west-1", + "properties": { + "template": "stacks/ServiceStack-prod.yml", + "stackPolicy": "stacks/ServiceStack-prod.stack-policy.json", + "parameters": { + "image": "${DockerImage.exactImageId}", + "websiteFilesBucket": "${StaticFiles.bucketName}", + "websiteFilesKeyPrefix": "${StaticFiles.keyPrefix}", + } + } + }, + "DockerImage": { + "type": "npm://@aws-cdk/aws-ecr.DockerImageDrop", + "environment": "aws://123456789012/eu-west-1", + "properties": { + "savedImage": "docker/docker-image.tar", + "imageName": "${PipelineStack.ecrImageName}" + } + }, + "StaticFiles": { + "type": "npm://@aws-cdk/assets.DirectoryDrop", + "environment": "aws://123456789012/eu-west-1", + "properties": { + "directory": "assets/static-website", + "bucketName": "${PipelineStack.stagingBucket}" + } + } + } +} +``` + +#### `signature.asc` +```pgp +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA256 + +{ + "algorithm": "SHA-256", + "items": { + "assets/static-website/index.html": { + "size": ..., + "hash": "..." + }, + "assets/static-website/style.css": { + "size": ..., + "hash": "..." + }, + "docker/docker-image.tar": { + "size": ..., + "hash": "..." + }, + "manifest.json": { + "size": ..., + "hash": "..." + }, + "stacks/PipelineStack.yml": { + "size": ..., + "hash": "..." + }, + "stacks/ServiceStack-beta.stack-policy.json": { + "size": ..., + "hash": "..." + }, + "stacks/ServiceStack-beta.yml": { + "size": ..., + "hash": "..." + }, + "stacks/ServiceStack-prod.stack-policy.json": { + "size": ..., + "hash": "..." + }, + "stacks/ServiceStack-prod.yml": { + "size": ..., + "hash": "..." + }, + }, + "nonce": "mUz0aYEhMlVmhJLNr5sizPKlJx1Kv38ApBc12NW6wPE=", + "timestamp": "2018-11-06T14:56:23Z" +} +-----BEGIN PGP SIGNATURE----- +[...] +-----END PGP SIGNATURE----- +``` + + +[RFC 2119]: https://tools.ietf.org/html/rfc2119 +[ISO/IEC 21320-1:2015]: https://www.iso.org/standard/60101.html +[JSON]: https://www.json.org +[RFC 4880]: https://tools.ietf.org/html/rfc4880 From 4325c6f809e7386d14cd115308bfdcfbd79c8dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Mon, 12 Nov 2018 10:14:03 +0100 Subject: [PATCH 02/15] First wave of feedback from @eladb --- specifications/cloud_assembly.md | 54 ++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/specifications/cloud_assembly.md b/specifications/cloud_assembly.md index fa210656b7647..e1761fdb80975 100644 --- a/specifications/cloud_assembly.md +++ b/specifications/cloud_assembly.md @@ -5,11 +5,11 @@ The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, ** are spelled out in bold, capital letters (as they are shown here). ## Introduction -A *Cloud Assembly* is a document container fail designed to hold the components of *cloud-native* applications, -including all the parts that are needed in order to deploy those to *the cloud*. This document exposes the specification -of the *Cloud Assembly* format as well as requirements imposed on *Cloud Assemblers* and *Cloud Runtimes*. +A *Cloud Assembly* is a self-contained document container designed to hold the components of *cloud applications*, +including all the parts that are needed in order to deploy those to a *cloud* provider. This document is the +specification of the *Cloud Assembly* format as well as requirements imposed on *Cloud Assemblers* and *Cloud Runtimes*. -### Goals +### Design Goals The design goals for the *Cloud Assembly Specification* are the following: * The *Cloud Assembly Specification* is extensible. * The *Cloud Assembly Specification* is cloud-agnostic. @@ -34,7 +34,7 @@ a single `object` that conforms to the following schema: Key |Type |Required|Description --------------|---------------------|:------:|----------- `schema` |`string` |Required|The schema for the document. **MUST** be `cloud-assembly/1.0`. -`drops` |`Map` |Required|A mapping of [*Logical ID*](#logical-id) to [`Drop`](#drop). +`drops` |`Map` |Required|A mapping of [*Logical ID*](#logical-id) to [Drop](#drop). `missing` |`Map`| |A mapping of context keys to [missing information](#missing). The [JSON] specification allows for keys to be specified multiple times in a given `object`. However, *Cloud Assembly* @@ -42,7 +42,7 @@ consumers **MAY** assume keys are unique, and [*Cloud Assemblers*](#cloud-assemb duplicate keys. If duplicate keys are present, the latest specified value **SHOULD** be preferred. ### Logical ID -*Logical IDs* are `string`s that uniquely identify [`Drop`](#drop)s in the context of a *Cloud Assembly*. +*Logical IDs* are `string`s that uniquely identify [Drop](#drop)s in the context of a *Cloud Assembly*. * A *Logical ID* **MUST NOT** be empty. * A *Logical ID* **SHOULD NOT** exceed `256` characters. * A *Logical ID* **MUST** be composed of only the following ASCII printable characters: @@ -53,22 +53,30 @@ duplicate keys. If duplicate keys are present, the latest specified value **SHOU + Minus: `-` (`0x2D`) + Forward-slash: `/` (`0x2F`) + Underscore: `_` (`0x5F`) +* A *Logical ID* **MUST NOT** contain the `.` (`0x2E`) character as it is used in the string substitution pattern for + cross-drop references to separate the *Logical ID* from the *attribute* name. -### `Drop` -`Drop`s are the building blocks of *Cloud Assemblies*. They model a part of the *cloud-native* application that can be -deployed independently, provided it's dependencies are fulfilled. `Drop`s are specified using [JSON] objects that -**MUST** conform to the following schema: +In other words, *Logical IDs* are expected to match the following regular expression: +```js +/^[A-Za-z0-9+\/_-]{1,256}$/ +``` + +### Drop +Clouds are made of Drops. Thet are the building blocks of *Cloud Assemblies*. They model a part of the +*cloud application* that can be deployed independently, provided it's dependencies are fulfilled. Drops are specified +using [JSON] objects that **MUST** conform to the following schema: Key |Type |Required|Description -------------|-----------------|:------:|----------- -`type` |`string` |Required|The [*Drop Type*](#drop-type) specifier of this `Drop`. -`environment`|`string` |required|The [environment](#environment) specifier for this `Drop`. -`metadata` |`Map`| |Arbitrary key-value pairs associated with this `Drop`. -`properties` |`Map`| |The properties of this `Drop` as documented by its maintainers. +`type` |`string` |Required|The [*Drop Type*](#drop-type) specifier of this Drop. +`environment`|`string` |required|The target [environment](#environment) in which Drop is deployed. +`metadata` |`Map`| |Arbitrary key-value pairs associated with this Drop. +`properties` |`Map`| |The properties of this Drop as documented by its maintainers. -Each [`Drop` Type](#drop-type) can produce outputs that can be used in order to allow other `Drop`s to consume the -resources they procude. Each `Drop` implementer is responsible to document the output attributes it supports. References -to these outputs are modeled using special `string` tokens within entries of the `properties` section of `Drop`s: +Each [Drop Type](#drop-type) can produce output strings that allow Drops to provide informations that other Drops can +use when composing the *cloud application*. Each Drop implementer is responsible to document the output attributes it +supports. References to these outputs are modeled using special `string` tokens within entries of the `properties` +section of Drops: ``` ${LogicalId.attributeName} @@ -85,9 +93,9 @@ Deployment systems **SHOULD** return an error upon encountering an occurrence of valid escape sequence. #### Drop Type -Every `Drop` has a type specifier, which allows *Cloud Assembly* consumers to know how to deploy it. The type specifiers +Every Drop has a type specifier, which allows *Cloud Assembly* consumers to know how to deploy it. The type specifiers are `string`s that use an URI-like syntax (`protocol://path`), providing the coordinates to a reference implementation -for the `Drop` behavior. +for the Drop behavior. Deployment systems **MUST** support at least one protocol, and **SHOULD** support all the protocols specified in the following sub-sections. @@ -105,8 +113,8 @@ npm://[@namespace/]package/ClassName[@version] ``` #### Environment -`Environment`s help Deployment systems determine where to deploy a particular `Drop`. Thy are `string`s that use an -URI-like syntax (`protocol://path`). +Environments help Deployment systems determine where to deploy a particular Drop. They are referenced by `string`s that +use an URI-like syntax (`protocol://path`). Deployment systems **MUST** support at least one protocol, and **SHOULD** support all the protocols specified in the following sub-sections. @@ -122,7 +130,7 @@ aws://account/region ``` ### `Missing` -[`Drop`s](#drop) may require contextual information to be available in order to correctly participate in a +[Drops](#drop) may require contextual information to be available in order to correctly participate in a *Cloud Assembly*. When information is missing, *Cloud Assembly* producers report the missing information by adding entries to the `missing` section of the [manifest document](#manifest-document). The values are [JSON] `object`s that **MUST** conform to the following schema: @@ -157,7 +165,7 @@ Deployment systems that support verifying signed *Cloud Assemblies*: * **SHOULD** allow the user to specify a list of trusted [PGP][RFC 4880] keys. ## Annex -### Examples of `Drop`s for the AWS Cloud +### Examples of Drops for the AWS Cloud #### `@aws-cdk/aws-cloudformation.StackDrop` A [*CloudFormation* stack][CFN Stack]. From 36f8998b8e114e8bf38d02774bdb516099716fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Thu, 15 Nov 2018 10:52:06 +0100 Subject: [PATCH 03/15] Add RI for the CloudAssembly specification --- packages/@aws-cdk/cloud-assembly/.gitignore | 8 + packages/@aws-cdk/cloud-assembly/.jsii | 225 ++++++++++++++++++ packages/@aws-cdk/cloud-assembly/.npmignore | 11 + packages/@aws-cdk/cloud-assembly/LICENSE | 201 ++++++++++++++++ packages/@aws-cdk/cloud-assembly/NOTICE | 2 + packages/@aws-cdk/cloud-assembly/README.md | 2 + .../@aws-cdk/cloud-assembly/lib/index.d.ts | 2 + packages/@aws-cdk/cloud-assembly/lib/index.js | 7 + packages/@aws-cdk/cloud-assembly/lib/index.ts | 2 + .../@aws-cdk/cloud-assembly/lib/manifest.d.ts | 54 +++++ .../@aws-cdk/cloud-assembly/lib/manifest.js | 3 + .../@aws-cdk/cloud-assembly/lib/manifest.ts | 32 +++ .../cloud-assembly/lib/validate-manifest.d.ts | 14 ++ .../cloud-assembly/lib/validate-manifest.js | 115 +++++++++ .../cloud-assembly/lib/validate-manifest.ts | 123 ++++++++++ packages/@aws-cdk/cloud-assembly/package.json | 72 ++++++ .../@aws-cdk/cloud-assembly/schema/.gitignore | 1 + .../test/test.validate-manifest.d.ts | 2 + .../test/test.validate-manifest.js | 104 ++++++++ .../test/test.validate-manifest.ts | 109 +++++++++ .../@aws-cdk/cloud-assembly/tsconfig.json | 28 +++ specifications/cloud_assembly.md | 72 ++++-- 22 files changed, 1165 insertions(+), 24 deletions(-) create mode 100644 packages/@aws-cdk/cloud-assembly/.gitignore create mode 100644 packages/@aws-cdk/cloud-assembly/.jsii create mode 100644 packages/@aws-cdk/cloud-assembly/.npmignore create mode 100644 packages/@aws-cdk/cloud-assembly/LICENSE create mode 100644 packages/@aws-cdk/cloud-assembly/NOTICE create mode 100644 packages/@aws-cdk/cloud-assembly/README.md create mode 100644 packages/@aws-cdk/cloud-assembly/lib/index.d.ts create mode 100644 packages/@aws-cdk/cloud-assembly/lib/index.js create mode 100644 packages/@aws-cdk/cloud-assembly/lib/index.ts create mode 100644 packages/@aws-cdk/cloud-assembly/lib/manifest.d.ts create mode 100644 packages/@aws-cdk/cloud-assembly/lib/manifest.js create mode 100644 packages/@aws-cdk/cloud-assembly/lib/manifest.ts create mode 100644 packages/@aws-cdk/cloud-assembly/lib/validate-manifest.d.ts create mode 100644 packages/@aws-cdk/cloud-assembly/lib/validate-manifest.js create mode 100644 packages/@aws-cdk/cloud-assembly/lib/validate-manifest.ts create mode 100644 packages/@aws-cdk/cloud-assembly/package.json create mode 100644 packages/@aws-cdk/cloud-assembly/schema/.gitignore create mode 100644 packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.d.ts create mode 100644 packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.js create mode 100644 packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.ts create mode 100644 packages/@aws-cdk/cloud-assembly/tsconfig.json diff --git a/packages/@aws-cdk/cloud-assembly/.gitignore b/packages/@aws-cdk/cloud-assembly/.gitignore new file mode 100644 index 0000000000000..448d3c6ad1a2a --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/.gitignore @@ -0,0 +1,8 @@ + +dist +.LAST_PACKAGE +.LAST_BUILD +*.snk +.nyc_output +coverage +.nycrc \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly/.jsii b/packages/@aws-cdk/cloud-assembly/.jsii new file mode 100644 index 0000000000000..af917c4e04f8e --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/.jsii @@ -0,0 +1,225 @@ +{ + "author": { + "name": "Amazon Web Services", + "organization": true, + "roles": [ + "author" + ], + "url": "https://aws.amazon.com" + }, + "bundled": { + "jsonschema": "^1.2.4" + }, + "description": "Reference implementation for the Cloud Assembly specification", + "homepage": "https://github.com/awslabs/aws-cdk", + "license": "Apache-2.0", + "name": "@aws-cdk/cloud-assembly", + "readme": { + "markdown": "## Reference implementation for the Cloud Assembly specification\nThis module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project." + }, + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-cdk.git" + }, + "schema": "jsii/1.0", + "targets": { + "dotnet": { + "assemblyOriginatorKeyFile": "../../key.snk", + "namespace": "Amazon.CDK.CloudAssembly", + "packageId": "Amazon.CDK.CloudAssembly", + "signAssembly": true + }, + "java": { + "maven": { + "artifactId": "cdk-cloud-assembly", + "groupId": "software.amazon.awscdk" + }, + "package": "software.amazon.awscdk.cloudassembly" + }, + "js": { + "npm": "@aws-cdk/cloud-assembly" + } + }, + "types": { + "@aws-cdk/cloud-assembly.Drop": { + "assembly": "@aws-cdk/cloud-assembly", + "datatype": true, + "fqn": "@aws-cdk/cloud-assembly.Drop", + "kind": "interface", + "name": "Drop", + "properties": [ + { + "abstract": true, + "docs": { + "pattern": "^[^:]+://.+$" + }, + "name": "environment", + "type": { + "primitive": "string" + } + }, + { + "abstract": true, + "docs": { + "pattern": "^[^:]+://.+$" + }, + "name": "type", + "type": { + "primitive": "string" + } + }, + { + "abstract": true, + "docs": { + "minItems": "1" + }, + "name": "dependsOn", + "type": { + "collection": { + "elementtype": { + "primitive": "string" + }, + "kind": "array" + }, + "optional": true + } + }, + { + "abstract": true, + "docs": { + "minProperties": "1" + }, + "name": "metadata", + "type": { + "collection": { + "elementtype": { + "fqn": "@aws-cdk/cloud-assembly.Metadata" + }, + "kind": "map" + }, + "optional": true + } + }, + { + "abstract": true, + "docs": { + "minProperties": "1" + }, + "name": "properties", + "type": { + "collection": { + "elementtype": { + "primitive": "any" + }, + "kind": "map" + }, + "optional": true + } + } + ] + }, + "@aws-cdk/cloud-assembly.Manifest": { + "assembly": "@aws-cdk/cloud-assembly", + "datatype": true, + "fqn": "@aws-cdk/cloud-assembly.Manifest", + "kind": "interface", + "name": "Manifest", + "properties": [ + { + "abstract": true, + "name": "drops", + "type": { + "collection": { + "elementtype": { + "fqn": "@aws-cdk/cloud-assembly.Drop" + }, + "kind": "map" + } + } + }, + { + "abstract": true, + "name": "schema", + "type": { + "primitive": "string" + } + }, + { + "abstract": true, + "docs": { + "minProperties": "1" + }, + "name": "missing", + "type": { + "collection": { + "elementtype": { + "fqn": "@aws-cdk/cloud-assembly.Missing" + }, + "kind": "map" + }, + "optional": true + } + } + ] + }, + "@aws-cdk/cloud-assembly.Metadata": { + "assembly": "@aws-cdk/cloud-assembly", + "datatype": true, + "fqn": "@aws-cdk/cloud-assembly.Metadata", + "kind": "interface", + "name": "Metadata", + "properties": [ + { + "abstract": true, + "name": "tag", + "type": { + "primitive": "string" + } + }, + { + "abstract": true, + "name": "value", + "type": { + "primitive": "any" + } + } + ] + }, + "@aws-cdk/cloud-assembly.Missing": { + "assembly": "@aws-cdk/cloud-assembly", + "datatype": true, + "fqn": "@aws-cdk/cloud-assembly.Missing", + "kind": "interface", + "name": "Missing", + "properties": [ + { + "abstract": true, + "docs": { + "minProperties": "1" + }, + "name": "props", + "type": { + "collection": { + "elementtype": { + "primitive": "any" + }, + "kind": "map" + } + } + }, + { + "abstract": true, + "docs": { + "pattern": "^[^:]+://.+$" + }, + "name": "provider", + "type": { + "primitive": "string" + } + } + ] + } + }, + "version": "0.16.0", + "fingerprint": "96keiznBnV4YBFKnCtU/T+BZEtUCSx6Q4BHSzy96nrc=" +} diff --git a/packages/@aws-cdk/cloud-assembly/.npmignore b/packages/@aws-cdk/cloud-assembly/.npmignore new file mode 100644 index 0000000000000..2def7f128ae25 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/.npmignore @@ -0,0 +1,11 @@ + +dist +.LAST_PACKAGE +.LAST_BUILD +*.snk +*.ts +!*.d.ts +!*.js +coverage +.nyc_output +*.tgz \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly/LICENSE b/packages/@aws-cdk/cloud-assembly/LICENSE new file mode 100644 index 0000000000000..1739faaebb745 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/cloud-assembly/NOTICE b/packages/@aws-cdk/cloud-assembly/NOTICE new file mode 100644 index 0000000000000..95fd48569c743 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/cloud-assembly/README.md b/packages/@aws-cdk/cloud-assembly/README.md new file mode 100644 index 0000000000000..1b8ebcbd0241c --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/README.md @@ -0,0 +1,2 @@ +## Reference implementation for the Cloud Assembly specification +This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project. \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly/lib/index.d.ts b/packages/@aws-cdk/cloud-assembly/lib/index.d.ts new file mode 100644 index 0000000000000..8fe1d654f83a5 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/lib/index.d.ts @@ -0,0 +1,2 @@ +export * from './manifest'; +export * from './validate-manifest'; diff --git a/packages/@aws-cdk/cloud-assembly/lib/index.js b/packages/@aws-cdk/cloud-assembly/lib/index.js new file mode 100644 index 0000000000000..8a83f0a4b1392 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/lib/index.js @@ -0,0 +1,7 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +Object.defineProperty(exports, "__esModule", { value: true }); +__export(require("./validate-manifest")); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQUNBLHlDQUFvQyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCAqIGZyb20gJy4vbWFuaWZlc3QnO1xuZXhwb3J0ICogZnJvbSAnLi92YWxpZGF0ZS1tYW5pZmVzdCc7XG4iXX0= \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly/lib/index.ts b/packages/@aws-cdk/cloud-assembly/lib/index.ts new file mode 100644 index 0000000000000..8fe1d654f83a5 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/lib/index.ts @@ -0,0 +1,2 @@ +export * from './manifest'; +export * from './validate-manifest'; diff --git a/packages/@aws-cdk/cloud-assembly/lib/manifest.d.ts b/packages/@aws-cdk/cloud-assembly/lib/manifest.d.ts new file mode 100644 index 0000000000000..aa999ff8558f1 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/lib/manifest.d.ts @@ -0,0 +1,54 @@ +export interface Manifest { + schema: 'cloud-assembly/1.0'; + drops: { + [logicalId: string]: Drop; + }; + /** + * @minProperties 1 + */ + missing?: { + [key: string]: Missing; + }; +} +export interface Drop { + /** + * @minItems 1 + */ + dependsOn?: string[]; + /** + * @pattern ^[^:]+://.+$ + */ + type: string; + /** + * @pattern ^[^:]+://.+$ + */ + environment: string; + /** + * @minProperties 1 + */ + metadata?: { + [key: string]: Metadata; + }; + /** + * @minProperties 1 + */ + properties?: { + [name: string]: any; + }; +} +export interface Missing { + /** + * @pattern ^[^:]+://.+$ + */ + provider: string; + /** + * @minProperties 1 + */ + props: { + [key: string]: any; + }; +} +export interface Metadata { + tag: string; + value: any; +} diff --git a/packages/@aws-cdk/cloud-assembly/lib/manifest.js b/packages/@aws-cdk/cloud-assembly/lib/manifest.js new file mode 100644 index 0000000000000..e6f44bba378ef --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/lib/manifest.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFuaWZlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJtYW5pZmVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGludGVyZmFjZSBNYW5pZmVzdCB7XG4gIHNjaGVtYTogJ2Nsb3VkLWFzc2VtYmx5LzEuMCc7XG4gIGRyb3BzOiB7IFtsb2dpY2FsSWQ6IHN0cmluZ106IERyb3AgfTtcbiAgLyoqXG4gICAqIEBtaW5Qcm9wZXJ0aWVzIDFcbiAgICovXG4gIG1pc3Npbmc/OiB7IFtrZXk6IHN0cmluZ106IE1pc3NpbmcgfTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBEcm9wIHtcbiAgLyoqXG4gICAqIEBtaW5JdGVtcyAxXG4gICAqL1xuICBkZXBlbmRzT24/OiBzdHJpbmdbXTtcbiAgLyoqXG4gICAqIEBwYXR0ZXJuIF5bXjpdKzovLy4rJFxuICAgKi9cbiAgdHlwZTogc3RyaW5nO1xuICAvKipcbiAgICogQHBhdHRlcm4gXlteOl0rOi8vLiskXG4gICAqL1xuICBlbnZpcm9ubWVudDogc3RyaW5nO1xuICAvKipcbiAgICogQG1pblByb3BlcnRpZXMgMVxuICAgKi9cbiAgbWV0YWRhdGE/OiB7IFtrZXk6IHN0cmluZ106IE1ldGFkYXRhIH07XG4gIC8qKlxuICAgKiBAbWluUHJvcGVydGllcyAxXG4gICAqL1xuICBwcm9wZXJ0aWVzPzogeyBbbmFtZTogc3RyaW5nXTogYW55IH1cbn1cblxuZXhwb3J0IGludGVyZmFjZSBNaXNzaW5nIHtcbiAgLyoqXG4gICAqIEBwYXR0ZXJuIF5bXjpdKzovLy4rJFxuICAgKi9cbiAgcHJvdmlkZXI6IHN0cmluZztcbiAgLyoqXG4gICAqIEBtaW5Qcm9wZXJ0aWVzIDFcbiAgICovXG4gIHByb3BzOiB7IFtrZXk6IHN0cmluZ106IGFueSB9O1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIE1ldGFkYXRhIHtcbiAgdGFnOiBzdHJpbmc7XG4gIHZhbHVlOiBhbnk7XG59XG4iXX0= \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly/lib/manifest.ts b/packages/@aws-cdk/cloud-assembly/lib/manifest.ts new file mode 100644 index 0000000000000..5104dabbea7d6 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/lib/manifest.ts @@ -0,0 +1,32 @@ +export interface Manifest { + schema: 'cloud-assembly/1.0'; + drops: { [logicalId: string]: Drop }; + missing?: { [key: string]: Missing }; +} + +export interface Drop { + dependsOn?: string[]; + /** + * @pattern ^[^:]+://.+$ + */ + type: string; + /** + * @pattern ^[^:]+://.+$ + */ + environment: string; + metadata?: { [key: string]: Metadata }; + properties?: { [name: string]: any } +} + +export interface Missing { + /** + * @pattern ^[^:]+://.+$ + */ + provider: string; + props: { [key: string]: any }; +} + +export interface Metadata { + kind: string; + value: any; +} diff --git a/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.d.ts b/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.d.ts new file mode 100644 index 0000000000000..1955ad3f8995f --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.d.ts @@ -0,0 +1,14 @@ +import jsonschema = require('jsonschema'); +import { Manifest } from './manifest'; +export declare const schema: jsonschema.Schema; +/** + * Validate that ``obj`` is a valid Cloud Assembly manifest document, both syntactically and semantically. The semantic + * validation ensures all Drop references are valid (they point to an existing Drop in the same manifest document), and + * that there are no cycles in the dependency graph described by the manifest. + * + * @param obj the object to be validated. + * + * @returns ``obj`` + * @throws Error if ``obj`` is not a Cloud Assembly manifest document or if it is semantically invalid. + */ +export declare function validateManifest(obj: unknown): Manifest; diff --git a/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.js b/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.js new file mode 100644 index 0000000000000..18b15b5de9591 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.js @@ -0,0 +1,115 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const jsonschema = require("jsonschema"); +// tslint:disable-next-line:no-var-requires +exports.schema = require('../schema/manifest.schema.json'); +/** + * Validate that ``obj`` is a valid Cloud Assembly manifest document, both syntactically and semantically. The semantic + * validation ensures all Drop references are valid (they point to an existing Drop in the same manifest document), and + * that there are no cycles in the dependency graph described by the manifest. + * + * @param obj the object to be validated. + * + * @returns ``obj`` + * @throws Error if ``obj`` is not a Cloud Assembly manifest document or if it is semantically invalid. + */ +function validateManifest(obj) { + const validator = new jsonschema.Validator(); + const result = validator.validate(obj, exports.schema); + if (result.valid) { + return validateSemantics(obj); + } + throw new Error(`Invalid Cloud Assembly manifest: ${result}`); +} +exports.validateManifest = validateManifest; +function validateSemantics(manifest) { + const dependencyGraph = {}; + for (const logicalId of Object.keys(manifest.drops)) { + assertValidLogicalId(logicalId); + const drop = manifest.drops[logicalId]; + const references = dependencyGraph[logicalId] = listReferences(drop, logicalId); + for (const ref of references) { + if (!(ref.logicalId in manifest.drops)) { + throw new Error(`${logicalId} depends on undefined drop through ${ref.context}.`); + } + } + } + assertNoCycles(); + return manifest; + function assertNoCycles() { + for (const logicalId of Object.keys(dependencyGraph)) { + for (const reference of dependencyGraph[logicalId]) { + reference.subreferences = dependencyGraph[reference.logicalId]; + } + } + const cycles = Object.keys(dependencyGraph) + .map(shortestCycle) + .filter(cycle => cycle.length > 0) + .sort((l, r) => l.length - r.length); + if (cycles.length > 0) { + const cyclesDecription = cycles.map(cycle => `- ${cycle.join(' => ')}`); + throw new Error(`Found dependency cycles:\n${cyclesDecription.join('\n')}`); + } + function shortestCycle(fromId) { + const toProcess = dependencyGraph[fromId].map(ref => ({ ref, path: [fromId] })); + const visited = new Set(); + while (toProcess.length > 0) { + const candidate = toProcess.pop(); + if (candidate.ref.logicalId === fromId) { + return [...candidate.path, candidate.ref.context]; + } + if (!visited.has(candidate.ref.logicalId)) { + toProcess.unshift(...candidate.ref.subreferences.map(ref => ({ ref, path: [...candidate.path, candidate.ref.context] }))); + visited.add(candidate.ref.logicalId); + } + } + return []; + } + } +} +function assertValidLogicalId(str) { + const regex = /^[A-Za-z0-9+\/_-]{1,256}$/; + if (!str.match(regex)) { + throw new Error(`Invalid logical ID: ${str} (does not match ${regex})`); + } +} +function listReferences(drop, dropId) { + const result = new Array(); + for (const logicalId of drop.dependsOn || []) { + result.push({ logicalId, context: `dependsOn ${logicalId}` }); + } + result.push(...listTokens(drop, dropId)); + return result; +} +function listTokens(obj, path) { + const result = new Array(); + if (typeof obj === 'string') { + const tokens = obj.match(/\\*\$\{[A-Za-z0-9+\/_-]{1,256}\.[^}]+\}/g); + for (const token of tokens || []) { + const parts = token.match(/(\\*)(\$\{([A-Za-z0-9+\/_-]{1,256})\.[^}]+\})/); + if (parts[1].length % 2 !== 0) { + // This one's quoted, so skip it. + continue; + } + result.push({ + logicalId: parts[3], + context: `${path} "${parts[2]}"` + }); + } + } + else if (typeof obj !== 'object' || obj == null) { + return []; + } + else if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; i++) { + result.push(...listTokens(obj[i], `${path}[${i}]`)); + } + } + else { + for (const key of Object.keys(obj)) { + result.push(...listTokens(obj[key], `${path}.${key}`)); + } + } + return result; +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdGUtbWFuaWZlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJ2YWxpZGF0ZS1tYW5pZmVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLHlDQUEwQztBQUcxQywyQ0FBMkM7QUFDOUIsUUFBQSxNQUFNLEdBQXNCLE9BQU8sQ0FBQyxnQ0FBZ0MsQ0FBQyxDQUFDO0FBRW5GOzs7Ozs7Ozs7R0FTRztBQUNILFNBQWdCLGdCQUFnQixDQUFDLEdBQVk7SUFDM0MsTUFBTSxTQUFTLEdBQUcsSUFBSSxVQUFVLENBQUMsU0FBUyxFQUFFLENBQUM7SUFDN0MsTUFBTSxNQUFNLEdBQUcsU0FBUyxDQUFDLFFBQVEsQ0FBQyxHQUFHLEVBQUUsY0FBTSxDQUFDLENBQUM7SUFDL0MsSUFBSSxNQUFNLENBQUMsS0FBSyxFQUFFO1FBQ2hCLE9BQU8saUJBQWlCLENBQUMsR0FBZSxDQUFDLENBQUM7S0FDM0M7SUFDRCxNQUFNLElBQUksS0FBSyxDQUFDLG9DQUFvQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO0FBQ2hFLENBQUM7QUFQRCw0Q0FPQztBQUVELFNBQVMsaUJBQWlCLENBQUMsUUFBa0I7SUFDM0MsTUFBTSxlQUFlLEdBQWtDLEVBQUUsQ0FBQztJQUMxRCxLQUFLLE1BQU0sU0FBUyxJQUFJLE1BQU0sQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxFQUFFO1FBQ25ELG9CQUFvQixDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQ2hDLE1BQU0sSUFBSSxHQUFHLFFBQVEsQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLENBQUM7UUFDdkMsTUFBTSxVQUFVLEdBQUcsZUFBZSxDQUFDLFNBQVMsQ0FBQyxHQUFHLGNBQWMsQ0FBQyxJQUFJLEVBQUUsU0FBUyxDQUFDLENBQUM7UUFDaEYsS0FBSyxNQUFNLEdBQUcsSUFBSSxVQUFVLEVBQUU7WUFDNUIsSUFBSSxDQUFDLENBQUMsR0FBRyxDQUFDLFNBQVMsSUFBSSxRQUFRLENBQUMsS0FBSyxD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S0FBSyxRQUFRLElBQUksR0FBRyxJQUFJLElBQUksRUFBRTtRQUNqRCxPQUFPLEVBQUUsQ0FBQztLQUNYO1NBQU0sSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxFQUFFO1FBQzdCLEtBQUssSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFHLENBQUMsR0FBRyxHQUFHLENBQUMsTUFBTSxFQUFHLENBQUMsRUFBRSxFQUFFO1lBQ3JDLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxVQUFVLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLEdBQUcsSUFBSSxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztTQUNyRDtLQUNGO1NBQU07UUFDTCxLQUFLLE1BQU0sR0FBRyxJQUFJLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUU7WUFDbEMsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLFVBQVUsQ0FBRSxHQUFXLENBQUMsR0FBRyxDQUFDLEVBQUUsR0FBRyxJQUFJLElBQUksR0FBRyxFQUFFLENBQUMsQ0FBQyxDQUFDO1NBQ2pFO0tBQ0Y7SUFDRCxPQUFPLE1BQU0sQ0FBQztBQUNoQixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGpzb25zY2hlbWEgPSByZXF1aXJlKCdqc29uc2NoZW1hJyk7XG5pbXBvcnQgeyBEcm9wLCBNYW5pZmVzdCB9IGZyb20gJy4vbWFuaWZlc3QnO1xuXG4vLyB0c2xpbnQ6ZGlzYWJsZS1uZXh0LWxpbmU6bm8tdmFyLXJlcXVpcmVzXG5leHBvcnQgY29uc3Qgc2NoZW1hOiBqc29uc2NoZW1hLlNjaGVtYSA9IHJlcXVpcmUoJy4uL3NjaGVtYS9tYW5pZmVzdC5zY2hlbWEuanNvbicpO1xuXG4vKipcbiAqIFZhbGlkYXRlIHRoYXQgYGBvYmpgYCBpcyBhIHZhbGlkIENsb3VkIEFzc2VtYmx5IG1hbmlmZXN0IGRvY3VtZW50LCBib3RoIHN5bnRhY3RpY2FsbHkgYW5kIHNlbWFudGljYWxseS4gVGhlIHNlbWFudGljXG4gKiB2YWxpZGF0aW9uIGVuc3VyZXMgYWxsIERyb3AgcmVmZXJlbmNlcyBhcmUgdmFsaWQgKHRoZXkgcG9pbnQgdG8gYW4gZXhpc3RpbmcgRHJvcCBpbiB0aGUgc2FtZSBtYW5pZmVzdCBkb2N1bWVudCksIGFuZFxuICogdGhhdCB0aGVyZSBhcmUgbm8gY3ljbGVzIGluIHRoZSBkZXBlbmRlbmN5IGdyYXBoIGRlc2NyaWJlZCBieSB0aGUgbWFuaWZlc3QuXG4gKlxuICogQHBhcmFtIG9iaiB0aGUgb2JqZWN0IHRvIGJlIHZhbGlkYXRlZC5cbiAqXG4gKiBAcmV0dXJucyBgYG9iamBgXG4gKiBAdGhyb3dzIEVycm9yIGlmIGBgb2JqYGAgaXMgbm90IGEgQ2xvdWQgQXNzZW1ibHkgbWFuaWZlc3QgZG9jdW1lbnQgb3IgaWYgaXQgaXMgc2VtYW50aWNhbGx5IGludmFsaWQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiB2YWxpZGF0ZU1hbmlmZXN0KG9iajogdW5rbm93bik6IE1hbmlmZXN0IHtcbiAgY29uc3QgdmFsaWRhdG9yID0gbmV3IGpzb25zY2hlbWEuVmFsaWRhdG9yKCk7XG4gIGNvbnN0IHJlc3VsdCA9IHZhbGlkYXRvci52YWxpZGF0ZShvYmosIHNjaGVtYSk7XG4gIGlmIChyZXN1bHQudmFsaWQpIHtcbiAgICByZXR1cm4gdmFsaWRhdGVTZW1hbnRpY3Mob2JqIGFzIE1hbmlmZXN0KTtcbiAgfVxuICB0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgQ2xvdWQgQXNzZW1ibHkgbWFuaWZlc3Q6ICR7cmVzdWx0fWApO1xufVxuXG5mdW5jdGlvbiB2YWxpZGF0ZVNlbWFudGljcyhtYW5pZmVzdDogTWFuaWZlc3QpOiBNYW5pZmVzdCB7XG4gIGNvbnN0IGRlcGVuZGVuY3lHcmFwaDogeyBbaWQ6IHN0cmluZ106IFJlZmVyZW5jZVtdIH0gPSB7fTtcbiAgZm9yIChjb25zdCBsb2dpY2FsSWQgb2YgT2JqZWN0LmtleXMobWFuaWZlc3QuZHJvcHMpKSB7XG4gICAgYXNzZXJ0VmFsaWRMb2dpY2FsSWQobG9naWNhbElkKTtcbiAgICBjb25zdCBkcm9wID0gbWFuaWZlc3QuZHJvcHNbbG9naWNhbElkXTtcbiAgICBjb25zdCByZWZlcmVuY2VzID0gZGVwZW5kZW5jeUdyYXBoW2xvZ2ljYWxJZF0gPSBsaXN0UmVmZXJlbmNlcyhkcm9wLCBsb2dpY2FsSWQpO1xuICAgIGZvciAoY29uc3QgcmVmIG9mIHJlZmVyZW5jZXMpIHtcbiAgICAgIGlmICghKHJlZi5sb2dpY2FsSWQgaW4gbWFuaWZlc3QuZHJvcHMpKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgJHtsb2dpY2FsSWR9IGRlcGVuZHMgb24gdW5kZWZpbmVkIGRyb3AgdGhyb3VnaCAke3JlZi5jb250ZXh0fS5gKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgYXNzZXJ0Tm9DeWNsZXMoKTtcbiAgcmV0dXJuIG1hbmlmZXN0O1xuXG4gIGZ1bmN0aW9uIGFzc2VydE5vQ3ljbGVzKCkge1xuICAgIGZvciAoY29uc3QgbG9naWNhbElkIG9mIE9iamVjdC5rZXlzKGRlcGVuZGVuY3lHcmFwaCkpIHtcbiAgICAgIGZvciAoY29uc3QgcmVmZXJlbmNlIG9mIGRlcGVuZGVuY3lHcmFwaFtsb2dpY2FsSWRdKSB7XG4gICAgICAgIHJlZmVyZW5jZS5zdWJyZWZlcmVuY2VzID0gZGVwZW5kZW5jeUdyYXBoW3JlZmVyZW5jZS5sb2dpY2FsSWRdO1xuICAgICAgfVxuICAgIH1cbiAgICBjb25zdCBjeWNsZXMgPSBPYmplY3Qua2V5cyhkZXBlbmRlbmN5R3JhcGgpXG4gICAgICAgICAgICAgICAgICAgICAgICAgLm1hcChzaG9ydGVzdEN5Y2xlKVxuICAgICAgICAgICAgICAgICAgICAgICAgIC5maWx0ZXIoY3ljbGUgPT4gY3ljbGUubGVuZ3RoID4gMClcbiAgICAgICAgICAgICAgICAgICAgICAgICAuc29ydCgobCwgcikgPT4gbC5sZW5ndGggLSByLmxlbmd0aCk7XG4gICAgaWYgKGN5Y2xlcy5sZW5ndGggPiAwKSB7XG4gICAgICBjb25zdCBjeWNsZXNEZWNyaXB0aW9uID0gY3ljbGVzLm1hcChjeWNsZSA9PiBgLSAke2N5Y2xlLmpvaW4oJyA9PiAnKX1gKTtcbiAgICAgIHRocm93IG5ldyBFcnJvcihgRm91bmQgZGVwZW5kZW5jeSBjeWNsZXM6XFxuJHtjeWNsZXNEZWNyaXB0aW9uLmpvaW4oJ1xcbicpfWApO1xuICAgIH1cblxuICAgIGZ1bmN0aW9uIHNob3J0ZXN0Q3ljbGUoZnJvbUlkOiBzdHJpbmcpOiBzdHJpbmdbXSB7XG4gICAgICBjb25zdCB0b1Byb2Nlc3MgPSBkZXBlbmRlbmN5R3JhcGhbZnJvbUlkXS5tYXAocmVmID0+ICh7IHJlZiwgcGF0aDogW2Zyb21JZF0gfSkpO1xuICAgICAgY29uc3QgdmlzaXRlZCA9IG5ldyBTZXQ8c3RyaW5nPigpO1xuICAgICAgd2hpbGUgKHRvUHJvY2Vzcy5sZW5ndGggPiAwKSB7XG4gICAgICAgIGNvbnN0IGNhbmRpZGF0ZSA9IHRvUHJvY2Vzcy5wb3AoKSE7XG4gICAgICAgIGlmIChjYW5kaWRhdGUucmVmLmxvZ2ljYWxJZCA9PT0gZnJvbUlkKSB7XG4gICAgICAgICAgcmV0dXJuIFsuLi5jYW5kaWRhdGUucGF0aCwgY2FuZGlkYXRlLnJlZi5jb250ZXh0XTtcbiAgICAgICAgfVxuICAgICAgICBpZiAoIXZpc2l0ZWQuaGFzKGNhbmRpZGF0ZS5yZWYubG9naWNhbElkKSkge1xuICAgICAgICAgIHRvUHJvY2Vzcy51bnNoaWZ0KC4uLmNhbmRpZGF0ZS5yZWYuc3VicmVmZXJlbmNlcyEubWFwKHJlZiA9PiAoeyByZWYsIHBhdGg6IFsuLi5jYW5kaWRhdGUucGF0aCwgY2FuZGlkYXRlLnJlZi5jb250ZXh0XSB9KSkpO1xuICAgICAgICAgIHZpc2l0ZWQuYWRkKGNhbmRpZGF0ZS5yZWYubG9naWNhbElkKTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgICAgcmV0dXJuIFtdO1xuICAgIH1cbiAgfVxufVxuXG5mdW5jdGlvbiBhc3NlcnRWYWxpZExvZ2ljYWxJZChzdHI6IHN0cmluZyk6IHZvaWQge1xuICBjb25zdCByZWdleCA9IC9eW0EtWmEtejAtOStcXC9fLV17MSwyNTZ9JC87XG4gIGlmICghc3RyLm1hdGNoKHJlZ2V4KSkge1xuICAgIHRocm93IG5ldyBFcnJvcihgSW52YWxpZCBsb2dpY2FsIElEOiAke3N0cn0gKGRvZXMgbm90IG1hdGNoICR7cmVnZXh9KWApO1xuICB9XG59XG5cbmZ1bmN0aW9uIGxpc3RSZWZlcmVuY2VzKGRyb3A6IERyb3AsIGRyb3BJZDogc3RyaW5nKTogUmVmZXJlbmNlW10ge1xuICBjb25zdCByZXN1bHQgPSBuZXcgQXJyYXk8UmVmZXJlbmNlPigpO1xuICBmb3IgKGNvbnN0IGxvZ2ljYWxJZCBvZiBkcm9wLmRlcGVuZHNPbiB8fCBbXSkge1xuICAgIHJlc3VsdC5wdXNoKHsgbG9naWNhbElkLCBjb250ZXh0OiBgZGVwZW5kc09uICR7bG9naWNhbElkfWAgfSk7XG4gIH1cbiAgcmVzdWx0LnB1c2goLi4ubGlzdFRva2Vucyhkcm9wLCBkcm9wSWQpKTtcbiAgcmV0dXJuIHJlc3VsdDtcbn1cblxuZnVuY3Rpb24gbGlzdFRva2VucyhvYmo6IHVua25vd24sIHBhdGg6IHN0cmluZyk6IFJlZmVyZW5jZVtdIHtcbiAgY29uc3QgcmVzdWx0ID0gbmV3IEFycmF5PFJlZmVyZW5jZT4oKTtcbiAgaWYgKHR5cGVvZiBvYmogPT09ICdzdHJpbmcnKSB7XG4gICAgY29uc3QgdG9rZW5zID0gb2JqLm1hdGNoKC9cXFxcKlxcJFxce1tBLVphLXowLTkrXFwvXy1dezEsMjU2fVxcLltefV0rXFx9L2cpO1xuICAgIGZvciAoY29uc3QgdG9rZW4gb2YgdG9rZW5zIHx8IFtdKSB7XG4gICAgICBjb25zdCBwYXJ0cyA9IHRva2VuLm1hdGNoKC8oXFxcXCopKFxcJFxceyhbQS1aYS16MC05K1xcL18tXXsxLDI1Nn0pXFwuW159XStcXH0pLykhO1xuICAgICAgaWYgKHBhcnRzWzFdLmxlbmd0aCAlIDIgIT09IDApIHtcbiAgICAgICAgLy8gVGhpcyBvbmUncyBxdW90ZWQsIHNvIHNraXAgaXQuXG4gICAgICAgIGNvbnRpbnVlO1xuICAgICAgfVxuICAgICAgcmVzdWx0LnB1c2goe1xuICAgICAgICBsb2dpY2FsSWQ6IHBhcnRzWzNdLFxuICAgICAgICBjb250ZXh0OiBgJHtwYXRofSBcIiR7cGFydHNbMl19XCJgXG4gICAgICB9KTtcbiAgICB9XG4gIH0gZWxzZSBpZiAodHlwZW9mIG9iaiAhPT0gJ29iamVjdCcgfHwgb2JqID09IG51bGwpIHtcbiAgICByZXR1cm4gW107XG4gIH0gZWxzZSBpZiAoQXJyYXkuaXNBcnJheShvYmopKSB7XG4gICAgZm9yIChsZXQgaSA9IDAgOyBpIDwgb2JqLmxlbmd0aCA7IGkrKykge1xuICAgICAgcmVzdWx0LnB1c2goLi4ubGlzdFRva2VucyhvYmpbaV0sIGAke3BhdGh9WyR7aX1dYCkpO1xuICAgIH1cbiAgfSBlbHNlIHtcbiAgICBmb3IgKGNvbnN0IGtleSBvZiBPYmplY3Qua2V5cyhvYmopKSB7XG4gICAgICByZXN1bHQucHVzaCguLi5saXN0VG9rZW5zKChvYmogYXMgYW55KVtrZXldLCBgJHtwYXRofS4ke2tleX1gKSk7XG4gICAgfVxuICB9XG4gIHJldHVybiByZXN1bHQ7XG59XG5cbmludGVyZmFjZSBSZWZlcmVuY2Uge1xuICBsb2dpY2FsSWQ6IHN0cmluZztcbiAgY29udGV4dDogc3RyaW5nO1xuICBzdWJyZWZlcmVuY2VzPzogUmVmZXJlbmNlW107XG59XG4iXX0= \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.ts b/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.ts new file mode 100644 index 0000000000000..4b613e2047017 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.ts @@ -0,0 +1,123 @@ +import jsonschema = require('jsonschema'); +import { Drop, Manifest } from './manifest'; + +// tslint:disable-next-line:no-var-requires +export const schema: jsonschema.Schema = require('../schema/manifest.schema.json'); + +/** + * Validate that ``obj`` is a valid Cloud Assembly manifest document, both syntactically and semantically. The semantic + * validation ensures all Drop references are valid (they point to an existing Drop in the same manifest document), and + * that there are no cycles in the dependency graph described by the manifest. + * + * @param obj the object to be validated. + * + * @returns ``obj`` + * @throws Error if ``obj`` is not a Cloud Assembly manifest document or if it is semantically invalid. + */ +export function validateManifest(obj: unknown): Manifest { + const validator = new jsonschema.Validator(); + const result = validator.validate(obj, schema); + if (result.valid) { + return validateSemantics(obj as Manifest); + } + throw new Error(`Invalid Cloud Assembly manifest: ${result}`); +} + +function validateSemantics(manifest: Manifest): Manifest { + const dependencyGraph: { [id: string]: Reference[] } = {}; + for (const logicalId of Object.keys(manifest.drops)) { + assertValidLogicalId(logicalId); + const drop = manifest.drops[logicalId]; + const references = dependencyGraph[logicalId] = listReferences(drop, logicalId); + for (const ref of references) { + if (!(ref.logicalId in manifest.drops)) { + throw new Error(`${logicalId} depends on undefined drop through ${ref.context}.`); + } + } + } + assertNoCycles(); + return manifest; + + function assertNoCycles() { + for (const logicalId of Object.keys(dependencyGraph)) { + for (const reference of dependencyGraph[logicalId]) { + reference.subreferences = dependencyGraph[reference.logicalId]; + } + } + const cycles = Object.keys(dependencyGraph) + .map(shortestCycle) + .filter(cycle => cycle.length > 0) + .sort((l, r) => l.length - r.length); + if (cycles.length > 0) { + const cyclesDecription = cycles.map(cycle => `- ${cycle.join(' => ')}`); + throw new Error(`Found dependency cycles:\n${cyclesDecription.join('\n')}`); + } + + function shortestCycle(fromId: string): string[] { + const toProcess = dependencyGraph[fromId].map(ref => ({ ref, path: [fromId] })); + const visited = new Set(); + while (toProcess.length > 0) { + const candidate = toProcess.pop()!; + if (candidate.ref.logicalId === fromId) { + return [...candidate.path, candidate.ref.context]; + } + if (!visited.has(candidate.ref.logicalId)) { + toProcess.unshift(...candidate.ref.subreferences!.map(ref => ({ ref, path: [...candidate.path, candidate.ref.context] }))); + visited.add(candidate.ref.logicalId); + } + } + return []; + } + } +} + +function assertValidLogicalId(str: string): void { + const regex = /^[A-Za-z0-9+\/_-]{1,256}$/; + if (!str.match(regex)) { + throw new Error(`Invalid logical ID: ${str} (does not match ${regex})`); + } +} + +function listReferences(drop: Drop, dropId: string): Reference[] { + const result = new Array(); + for (const logicalId of drop.dependsOn || []) { + result.push({ logicalId, context: `dependsOn ${logicalId}` }); + } + result.push(...listTokens(drop, dropId)); + return result; +} + +function listTokens(obj: unknown, path: string): Reference[] { + const result = new Array(); + if (typeof obj === 'string') { + const tokens = obj.match(/\\*\$\{[A-Za-z0-9+\/_-]{1,256}\.[^}]+\}/g); + for (const token of tokens || []) { + const parts = token.match(/(\\*)(\$\{([A-Za-z0-9+\/_-]{1,256})\.[^}]+\})/)!; + if (parts[1].length % 2 !== 0) { + // This one's quoted, so skip it. + continue; + } + result.push({ + logicalId: parts[3], + context: `${path} "${parts[2]}"` + }); + } + } else if (typeof obj !== 'object' || obj == null) { + return []; + } else if (Array.isArray(obj)) { + for (let i = 0 ; i < obj.length ; i++) { + result.push(...listTokens(obj[i], `${path}[${i}]`)); + } + } else { + for (const key of Object.keys(obj)) { + result.push(...listTokens((obj as any)[key], `${path}.${key}`)); + } + } + return result; +} + +interface Reference { + logicalId: string; + context: string; + subreferences?: Reference[]; +} diff --git a/packages/@aws-cdk/cloud-assembly/package.json b/packages/@aws-cdk/cloud-assembly/package.json new file mode 100644 index 0000000000000..09f4e9f17c38d --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/package.json @@ -0,0 +1,72 @@ +{ + "name": "@aws-cdk/cloud-assembly", + "description": "Reference implementation for the Cloud Assembly specification", + "version": "0.17.0", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "targets": { + "java": { + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "cdk-cloud-assembly" + }, + "package": "software.amazon.awscdk.cloudassembly" + }, + "sphinx": {}, + "dotnet": { + "namespace": "Amazon.CDK.CloudAssembly", + "packageId": "Amazon.CDK.CloudAssembly", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk" + } + }, + "outdir": "dist" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "pkglint": "pkglint -f", + "package": "cdk-package", + "test": "cdk-test" + }, + "cdk-build": { + "pre": [ + "typescript-json-schema", + "lib/manifest.ts", + "Manifest", + "--excludePrivate", + "--noExtraProps", + "--required", + "--strictNullChecks", + "--topRef", + "--out=schema/manifest.schema.json" + ] + }, + "dependencies": { + "jsonschema": "^1.2.4" + }, + "devDependencies": { + "cdk-build-tools": "^0.17.0", + "pkglint": "^0.17.0", + "typescript-json-schema": "^0.33.0" + }, + "bundleDependencies": [ + "jsonschema" + ], + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-cdk.git" + }, + "homepage": "https://github.com/awslabs/aws-cdk", + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "keywords": [ + "aws", + "cdk" + ] +} diff --git a/packages/@aws-cdk/cloud-assembly/schema/.gitignore b/packages/@aws-cdk/cloud-assembly/schema/.gitignore new file mode 100644 index 0000000000000..8b5675c4597c7 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/schema/.gitignore @@ -0,0 +1 @@ +*.schema.json diff --git a/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.d.ts b/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.d.ts new file mode 100644 index 0000000000000..10b43061559fc --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.d.ts @@ -0,0 +1,2 @@ +declare const _default: void; +export = _default; diff --git a/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.js b/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.js new file mode 100644 index 0000000000000..8db2ef5dc170f --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.js @@ -0,0 +1,104 @@ +"use strict"; +const nodeunit = require("nodeunit"); +const lib_1 = require("../lib"); +const SAMPLE_MANIFEST = { + schema: "cloud-assembly/1.0", + drops: { + "PipelineStack": { + type: "npm://@aws-cdk/aws-cloudformation.StackDrop", + environment: "aws://123456789012/eu-west-1", + properties: { + template: "stacks/PipelineStack.yml" + } + }, + "ServiceStack-beta": { + type: "npm://@aws-cdk/aws-cloudformation.StackDrop", + environment: "aws://123456789012/eu-west-1", + properties: { + template: "stacks/ServiceStack-beta.yml", + stackPolicy: "stacks/ServiceStack-beta.stack-policy.json", + parameters: { + image: "${DockerImage.exactImageId}", + websiteFilesBucket: "${StaticFiles.bucketName}", + websiteFilesKeyPrefix: "${StaticFiles.keyPrefix}", + } + } + }, + "ServiceStack-prod": { + type: "npm://@aws-cdk/aws-cloudformation.StackDrop", + environment: "aws://123456789012/eu-west-1", + properties: { + template: "stacks/ServiceStack-prod.yml", + stackPolicy: "stacks/ServiceStack-prod.stack-policy.json", + parameters: { + image: "${DockerImage.exactImageId}", + websiteFilesBucket: "${StaticFiles.bucketName}", + websiteFilesKeyPrefix: "${StaticFiles.keyPrefix}", + } + } + }, + "DockerImage": { + type: "npm://@aws-cdk/aws-ecr.DockerImageDrop", + environment: "aws://123456789012/eu-west-1", + properties: { + savedImage: "docker/docker-image.tar", + imageName: "${PipelineStack.ecrImageName}" + } + }, + "StaticFiles": { + type: "npm://@aws-cdk/assets.DirectoryDrop", + environment: "aws://123456789012/eu-west-1", + properties: { + directory: "assets/static-website", + bucketName: "${PipelineStack.stagingBucket}" + } + } + } +}; +module.exports = nodeunit.testCase({ + validateManifest: { + 'successfully loads the example manifest'(test) { + test.doesNotThrow(() => lib_1.validateManifest(SAMPLE_MANIFEST)); + test.done(); + }, + 'rejects a document where the schema is invalid'(test) { + const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); + badManifest.schema = 'foo/1.0-bar'; + test.throws(() => lib_1.validateManifest(badManifest), /instance\.schema is not one of enum values/); + test.done(); + }, + 'rejects a document without drops'(test) { + const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); + delete badManifest.drops; + test.throws(() => lib_1.validateManifest(badManifest), /instance requires property "drops"/); + test.done(); + }, + 'rejects a document with an illegal Logical ID'(test) { + const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); + badManifest.drops['Pipeline.Stack'] = badManifest.drops.PipelineStack; + test.throws(() => lib_1.validateManifest(badManifest), /Invalid logical ID: Pipeline\.Stack/); + test.done(); + }, + 'rejects a document with unresolved dependsOn'(test) { + const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); + badManifest.drops.PipelineStack.dependsOn = ['DoesNotExist']; + test.throws(() => lib_1.validateManifest(badManifest), /PipelineStack depends on undefined drop through dependsOn DoesNotExist/); + test.done(); + }, + 'rejects a document with direct circular dependency via dependsOn'(test) { + const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); + badManifest.drops.PipelineStack.dependsOn = ['PipelineStack']; + test.throws(() => lib_1.validateManifest(badManifest), /PipelineStack => dependsOn PipelineStack/); + test.done(); + }, + 'rejects a document with indirect circular dependency'(test) { + const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); + badManifest.drops.StaticFiles.dependsOn = ['ServiceStack-beta']; + test.throws(() => lib_1.validateManifest(badManifest), + // tslint:disable-next-line:max-line-length + /StaticFiles => dependsOn ServiceStack-beta => ServiceStack-beta\.properties\.parameters\.websiteFilesKeyPrefix "\${StaticFiles\.keyPrefix}"/); + test.done(); + } + } +}); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVzdC52YWxpZGF0ZS1tYW5pZmVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInRlc3QudmFsaWRhdGUtbWFuaWZlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLHFDQUFzQztBQUN0QyxnQ0FBMEM7QUFzRDFDLE1BQU0sZUFBZSxHQUFHO0lBQ3RCLE1BQU0sRUFBRSxvQkFBb0I7SUFDNUIsS0FBSyxFQUFFO1FBQ0wsZUFBZSxFQUFFO1lBQ2YsSUFBSSxFQUFFLDZDQUE2QztZQUNuRCxXQUFXLEVBQUUsOEJBQThCO1lBQzNDLFVBQVUsRUFBRTtnQkFDVixRQUFRLEVBQUUsMEJBQTBCO2FBQ3JDO1NBQ0Y7UUFDRCxtQkFBbUIsRUFBRTtZQUNuQixJQUFJLEVBQUUsNkNBQTZDO1lBQ25ELFdBQVcsRUFBRSw4QkFBOEI7WUFDM0MsVUFBVSxFQUFFO2dCQUNWLFFBQVEsRUFBRSw4QkFBOEI7Z0JBQ3hDLFdBQVcsRUFBRSw0Q0FBNEM7Z0JBQ3pELFVBQVUsRUFBRTtvQkFDVixLQUFLLEVBQUUsNkJBQTZCO29CQUNwQyxrQkFBa0IsRUFBRSwyQkFBMkI7b0JBQy9DLHFCQUFxQixFQUFFLDBCQUEwQjtpQkFDbEQ7YUFDRjtTQUNGO1FBQ0QsbUJBQW1CLEVBQUU7WUFDbkIsSUFBSSxFQUFFLDZDQUE2QztZQUNuRCxXQUFXLEVBQUUsOEJBQThCO1lBQzNDLFVBQVUsRUFBRTtnQkFDVixRQUFRLEVBQUUsOEJBQThCO2dCQUN4QyxXQUFXLEVBQUUsNENBQTRDO2dCQUN6RCxVQUFVLEVBQUU7b0JBQ1YsS0FBSyxFQUFFLDZCQUE2QjtvQkFDcEMsa0JBQWtCLEVBQUUsMkJBQTJCO29CQUMvQyxxQkFBcUIsRUFBRSwwQkFBMEI7aUJBQ2xEO2FBQ0Y7U0FDRjtRQUNELGFBQWEsRUFBRTtZQUNiLElBQUksRUFBRSx3Q0FBd0M7WUFDOUMsV0FBVyxFQUFFLDhCQUE4QjtZQUMzQyxVQUFVLEVBQUU7Z0JBQ1YsVUFBVSxFQUFFLHlCQUF5QjtnQkFDckMsU0FBUyxFQUFFLCtCQUErQjthQUMzQztTQUNGO1FBQ0QsYUFBYSxFQUFFO1lBQ2IsSUFBSSxFQUFFLHFDQUFxQztZQUMzQyxXQUFXLEVBQUUsOEJBQThCO1lBQzNDLFVBQVUsRUFBRTtnQkFDVixTQUFTLEVBQUUsdUJBQXVCO2dCQUNsQyxVQUFVLEVBQUUsZ0NBQWdDO2FBQzdDO1NBQ0Y7S0FDRjtDQUNGLENBQUM7QUF6R0YsaUJBQVMsUUFBUSxDQUFDLFFBQVEsQ0FBQztJQUN6QixnQkFBZ0IsRUFBRTtRQUNoQix5Q0FBeUMsQ0FBQyxJQUFtQjtZQUMzRCxJQUFJLENBQUMsWUFBWSxDQUFDLEdBQUcsRUFBRSxDQUFDLHNCQUFnQixDQUFDLGVBQWUsQ0FBQyxDQUFDLENBQUM7WUFDM0QsSUFBSSxDQUFDLElBQUksRUFBRSxDQUFDO1FBQ2QsQ0FBQztRQUNELGdEQUFnRCxDQUFDLElBQW1CO1lBQ2xFLE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxlQUFlLENBQUMsQ0FBQyxDQUFDO1lBQ2hFLFdBQVcsQ0FBQyxNQUFNLEdBQUcsYUFBYSxDQUFDO1lBQ25DLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxFQUFFLENBQUMsc0JBQWdCLENBQUMsV0FBVyxDQUFDLEVBQ25DLDRDQUE0QyxDQUFDLENBQUM7WUFDMUQsSUFBSSxDQUFDLElBQUksRUFBRSxDQUFDO1FBQ2QsQ0FBQztRQUNELGtDQUFrQyxDQUFDLElBQW1CO1lBQ3BELE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxlQUFlLENBQUMsQ0FBQyxDQUFDO1lBQ2hFLE9BQU8sV0FBVyxDQUFDLEtBQUssQ0FBQztZQUN6QixJQUFJLENBQUMsTUFBTSxDQUFDLEdBQUcsRUFBRSxDQUFDLHNCQUFnQixDQUFDLFdBQVcsQ0FBQyxFQUNuQyxvQ0FBb0MsQ0FBQyxDQUFDO1lBQ2xELElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUNkLENBQUM7UUFDRCwrQ0FBK0MsQ0FBQyxJQUFtQjtZQUNqRSxNQUFNLFdBQVcsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsZUFBZSxDQUFDLENBQUMsQ0FBQztZQUNoRSxXQUFXLENBQUMsS0FBSyxDQUFDLGdCQUFnQixDQUFDLEdBQUcsV0FBVyxDQUFDLEtBQUssQ0FBQyxhQUFhLENBQUM7WUFDdEUsSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLEVBQUUsQ0FBQyxzQkFBZ0IsQ0FBQyxXQUFXLENBQUMsRUFDbkMscUNBQXFDLENBQUMsQ0FBQztZQUNuRCxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDZCxDQUFDO1FBQ0QsOENBQThDLENBQUMsSUFBbUI7WUFDaEUsTUFBTSxXQUFXLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLGVBQWUsQ0FBQyxDQUFDLENBQUM7WUFDaEUsV0FBVyxDQUFDLEtBQUssQ0FBQyxhQUFhLENBQUMsU0FBUyxHQUFHLENBQUMsY0FBYyxDQUFDLENBQUM7WUFDN0QsSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLEVBQUUsQ0FBQyxzQkFBZ0IsQ0FBQyxXQUFXLENBQUMsRUFDbkMsd0VBQXdFLENBQUMsQ0FBQztZQUN0RixJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDZCxDQUFDO1FBQ0Qsa0VBQWtFLENBQUMsSUFBbUI7WUFDcEYsTUFBTSxXQUFXLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLGVBQWUsQ0FBQyxDQUFDLENBQUM7WUFDaEUsV0FBVyxDQUFDLEtBQUssQ0FBQyxhQUFhLENBQUMsU0FBUyxHQUFHLENBQUMsZUFBZSxDQUFDLENBQUM7WUFDOUQsSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLEVBQUUsQ0FBQyxzQkFBZ0IsQ0FBQyxXQUFXLENBQUMsRUFDbkMsMENBQTBDLENBQUMsQ0FBQztZQUN4RCxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDZCxDQUFDO1FBQ0Qsc0RBQXNELENBQUMsSUFBbUI7WUFDeEUsTUFBTSxXQUFXLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLGVBQWUsQ0FBQyxDQUFDLENBQUM7WUFDaEUsV0FBVyxDQUFDLEtBQUssQ0FBQyxXQUFXLENBQUMsU0FBUyxHQUFHLENBQUMsbUJBQW1CLENBQUMsQ0FBQztZQUNoRSxJQUFJLENBQUMsTUFBTSxDQUFDLEdBQUcsRUFBRSxDQUFDLHNCQUFnQixDQUFDLFdBQVcsQ0FBQztZQUNuQywyQ0FBMkM7WUFDM0MsNklBQTZJLENBQUMsQ0FBQztZQUMzSixJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDZCxDQUFDO0tBQ0Y7Q0FDRixDQUFDLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgbm9kZXVuaXQgPSByZXF1aXJlKCdub2RldW5pdCcpO1xuaW1wb3J0IHsgdmFsaWRhdGVNYW5pZmVzdCB9IGZyb20gJy4uL2xpYic7XG5cbmV4cG9ydCA9IG5vZGV1bml0LnRlc3RDYXNlKHtcbiAgdmFsaWRhdGVNYW5pZmVzdDoge1xuICAgICdzdWNjZXNzZnVsbHkgbG9hZHMgdGhlIGV4YW1wbGUgbWFuaWZlc3QnKHRlc3Q6IG5vZGV1bml0LlRlc3QpIHtcbiAgICAgIHRlc3QuZG9lc05vdFRocm93KCgpID0+IHZhbGlkYXRlTWFuaWZlc3QoU0FNUExFX01BTklGRVNUKSk7XG4gICAgICB0ZXN0LmRvbmUoKTtcbiAgICB9LFxuICAgICdyZWplY3RzIGEgZG9jdW1lbnQgd2hlcmUgdGhlIHNjaGVtYSBpcyBpbnZhbGlkJyh0ZXN0OiBub2RldW5pdC5UZXN0KSB7XG4gICAgICBjb25zdCBiYWRNYW5pZmVzdCA9IEpTT04ucGFyc2UoSlNPTi5zdHJpbmdpZnkoU0FNUExFX01BTklGRVNUKSk7XG4gICAgICBiYWRNYW5pZmVzdC5zY2hlbWEgPSAnZm9vLzEuMC1iYXInO1xuICAgICAgdGVzdC50aHJvd3MoKCkgPT4gdmFsaWRhdGVNYW5pZmVzdChiYWRNYW5pZmVzdCksXG4gICAgICAgICAgICAgICAgICAvaW5zdGFuY2VcXC5zY2hlbWEgaXMgbm90IG9uZSBvZiBlbnVtIHZhbHVlcy8pO1xuICAgICAgdGVzdC5kb25lKCk7XG4gICAgfSxcbiAgICAncmVqZWN0cyBhIGRvY3VtZW50IHdpdGhvdXQgZHJvcHMnKHRlc3Q6IG5vZGV1bml0LlRlc3QpIHtcbiAgICAgIGNvbnN0IGJhZE1hbmlmZXN0ID0gSlNPTi5wYXJzZShKU09OLnN0cmluZ2lmeShTQU1QTEVfTUFOSUZFU1QpKTtcbiAgICAgIGRlbGV0ZSBiYWRNYW5pZmVzdC5kcm9wcztcbiAgICAgIHRlc3QudGhyb3dzKCgpID0+IHZhbGlkYXRlTWFuaWZlc3QoYmFkTWFuaWZlc3QpLFxuICAgICAgICAgICAgICAgICAgL2luc3RhbmNlIHJlcXVpcmVzIHByb3BlcnR5IFwiZHJvcHNcIi8pO1xuICAgICAgdGVzdC5kb25lKCk7XG4gICAgfSxcbiAgICAncmVqZWN0cyBhIGRvY3VtZW50IHdpdGggYW4gaWxsZWdhbCBMb2dpY2FsIElEJyh0ZXN0OiBub2RldW5pdC5UZXN0KSB7XG4gICAgICBjb25zdCBiYWRNYW5pZmVzdCA9IEpTT04ucGFyc2UoSlNPTi5zdHJpbmdpZnkoU0FNUExFX01BTklGRVNUKSk7XG4gICAgICBiYWRNYW5pZmVzdC5kcm9wc1snUGlwZWxpbmUuU3RhY2snXSA9IGJhZE1hbmlmZXN0LmRyb3BzLlBpcGVsaW5lU3RhY2s7XG4gICAgICB0ZXN0LnRocm93cygoKSA9PiB2YWxpZGF0ZU1hbmlmZXN0KGJhZE1hbmlmZXN0KSxcbiAgICAgICAgICAgICAgICAgIC9JbnZhbGlkIGxvZ2ljYWwgSUQ6IFBpcGVsaW5lXFwuU3RhY2svKTtcbiAgICAgIHRlc3QuZG9uZSgpO1xuICAgIH0sXG4gICAgJ3JlamVjdHMgYSBkb2N1bWVudCB3aXRoIHVucmVzb2x2ZWQgZGVwZW5kc09uJyh0ZXN0OiBub2RldW5pdC5UZXN0KSB7XG4gICAgICBjb25zdCBiYWRNYW5pZmVzdCA9IEpTT04ucGFyc2UoSlNPTi5zdHJpbmdpZnkoU0FNUExFX01BTklGRVNUKSk7XG4gICAgICBiYWRNYW5pZmVzdC5kcm9wcy5QaXBlbGluZVN0YWNrLmRlcGVuZHNPbiA9IFsnRG9lc05vdEV4aXN0J107XG4gICAgICB0ZXN0LnRocm93cygoKSA9PiB2YWxpZGF0ZU1hbmlmZXN0KGJhZE1hbmlmZXN0KSxcbiAgICAgICAgICAgICAgICAgIC9QaXBlbGluZVN0YWNrIGRlcGVuZHMgb24gdW5kZWZpbmVkIGRyb3AgdGhyb3VnaCBkZXBlbmRzT24gRG9lc05vdEV4aXN0Lyk7XG4gICAgICB0ZXN0LmRvbmUoKTtcbiAgICB9LFxuICAgICdyZWplY3RzIGEgZG9jdW1lbnQgd2l0aCBkaXJlY3QgY2lyY3VsYXIgZGVwZW5kZW5jeSB2aWEgZGVwZW5kc09uJyh0ZXN0OiBub2RldW5pdC5UZXN0KSB7XG4gICAgICBjb25zdCBiYWRNYW5pZmVzdCA9IEpTT04ucGFyc2UoSlNPTi5zdHJpbmdpZnkoU0FNUExFX01BTklGRVNUKSk7XG4gICAgICBiYWRNYW5pZmVzdC5kcm9wcy5QaXBlbGluZVN0YWNrLmRlcGVuZHNPbiA9IFsnUGlwZWxpbmVTdGFjayddO1xuICAgICAgdGVzdC50aHJvd3MoKCkgPT4gdmFsaWRhdGVNYW5pZmVzdChiYWRNYW5pZmVzdCksXG4gICAgICAgICAgICAgICAgICAvUGlwZWxpbmVTdGFjayA9PiBkZXBlbmRzT24gUGlwZWxpbmVTdGFjay8pO1xuICAgICAgdGVzdC5kb25lKCk7XG4gICAgfSxcbiAgICAncmVqZWN0cyBhIGRvY3VtZW50IHdpdGggaW5kaXJlY3QgY2lyY3VsYXIgZGVwZW5kZW5jeScodGVzdDogbm9kZXVuaXQuVGVzdCkge1xuICAgICAgY29uc3QgYmFkTWFuaWZlc3QgPSBKU09OLnBhcnNlKEpTT04uc3RyaW5naWZ5KFNBTVBMRV9NQU5JRkVTVCkpO1xuICAgICAgYmFkTWFuaWZlc3QuZHJvcHMuU3RhdGljRmlsZXMuZGVwZW5kc09uID0gWydTZXJ2aWNlU3RhY2stYmV0YSddO1xuICAgICAgdGVzdC50aHJvd3MoKCkgPT4gdmFsaWRhdGVNYW5pZmVzdChiYWRNYW5pZmVzdCksXG4gICAgICAgICAgICAgICAgICAvLyB0c2xpbnQ6ZGlzYWJsZS1uZXh0LWxpbmU6bWF4LWxpbmUtbGVuZ3RoXG4gICAgICAgICAgICAgICAgICAvU3RhdGljRmlsZXMgPT4gZGVwZW5kc09uIFNlcnZpY2VTdGFjay1iZXRhID0+IFNlcnZpY2VTdGFjay1iZXRhXFwucHJvcGVydGllc1xcLnBhcmFtZXRlcnNcXC53ZWJzaXRlRmlsZXNLZXlQcmVmaXggXCJcXCR7U3RhdGljRmlsZXNcXC5rZXlQcmVmaXh9XCIvKTtcbiAgICAgIHRlc3QuZG9uZSgpO1xuICAgIH1cbiAgfVxufSk7XG5cbmNvbnN0IFNBTVBMRV9NQU5JRkVTVCA9IHtcbiAgc2NoZW1hOiBcImNsb3VkLWFzc2VtYmx5LzEuMFwiLFxuICBkcm9wczoge1xuICAgIFwiUGlwZWxpbmVTdGFja1wiOiB7XG4gICAgICB0eXBlOiBcIm5wbTovL0Bhd3MtY2RrL2F3cy1jbG91ZGZvcm1hdGlvbi5TdGFja0Ryb3BcIixcbiAgICAgIGVudmlyb25tZW50OiBcImF3czovLzEyMzQ1Njc4OTAxMi9ldS13ZXN0LTFcIixcbiAgICAgIHByb3BlcnRpZXM6IHtcbiAgICAgICAgdGVtcGxhdGU6IFwic3RhY2tzL1BpcGVsaW5lU3RhY2sueW1sXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiU2VydmljZVN0YWNrLWJldGFcIjoge1xuICAgICAgdHlwZTogXCJucG06Ly9AYXdzLWNkay9hd3MtY2xvdWRmb3JtYXRpb24uU3RhY2tEcm9wXCIsXG4gICAgICBlbnZpcm9ubWVudDogXCJhd3M6Ly8xMjM0NTY3ODkwMTIvZXUtd2VzdC0xXCIsXG4gICAgICBwcm9wZXJ0aWVzOiB7XG4gICAgICAgIHRlbXBsYXRlOiBcInN0YWNrcy9TZXJ2aWNlU3RhY2stYmV0YS55bWxcIixcbiAgICAgICAgc3RhY2tQb2xpY3k6IFwic3RhY2tzL1NlcnZpY2VTdGFjay1iZXRhLnN0YWNrLXBvbGljeS5qc29uXCIsXG4gICAgICAgIHBhcmFtZXRlcnM6IHtcbiAgICAgICAgICBpbWFnZTogXCIke0RvY2tlckltYWdlLmV4YWN0SW1hZ2VJZH1cIixcbiAgICAgICAgICB3ZWJzaXRlRmlsZXNCdWNrZXQ6IFwiJHtTdGF0aWNGaWxlcy5idWNrZXROYW1lfVwiLFxuICAgICAgICAgIHdlYnNpdGVGaWxlc0tleVByZWZpeDogXCIke1N0YXRpY0ZpbGVzLmtleVByZWZpeH1cIixcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0sXG4gICAgXCJTZXJ2aWNlU3RhY2stcHJvZFwiOiB7XG4gICAgICB0eXBlOiBcIm5wbTovL0Bhd3MtY2RrL2F3cy1jbG91ZGZvcm1hdGlvbi5TdGFja0Ryb3BcIixcbiAgICAgIGVudmlyb25tZW50OiBcImF3czovLzEyMzQ1Njc4OTAxMi9ldS13ZXN0LTFcIixcbiAgICAgIHByb3BlcnRpZXM6IHtcbiAgICAgICAgdGVtcGxhdGU6IFwic3RhY2tzL1NlcnZpY2VTdGFjay1wcm9kLnltbFwiLFxuICAgICAgICBzdGFja1BvbGljeTogXCJzdGFja3MvU2VydmljZVN0YWNrLXByb2Quc3RhY2stcG9saWN5Lmpzb25cIixcbiAgICAgICAgcGFyYW1ldGVyczoge1xuICAgICAgICAgIGltYWdlOiBcIiR7RG9ja2VySW1hZ2UuZXhhY3RJbWFnZUlkfVwiLFxuICAgICAgICAgIHdlYnNpdGVGaWxlc0J1Y2tldDogXCIke1N0YXRpY0ZpbGVzLmJ1Y2tldE5hbWV9XCIsXG4gICAgICAgICAgd2Vic2l0ZUZpbGVzS2V5UHJlZml4OiBcIiR7U3RhdGljRmlsZXMua2V5UHJlZml4fVwiLFxuICAgICAgICB9XG4gICAgICB9XG4gICAgfSxcbiAgICBcIkRvY2tlckltYWdlXCI6IHtcbiAgICAgIHR5cGU6IFwibnBtOi8vQGF3cy1jZGsvYXdzLWVjci5Eb2NrZXJJbWFnZURyb3BcIixcbiAgICAgIGVudmlyb25tZW50OiBcImF3czovLzEyMzQ1Njc4OTAxMi9ldS13ZXN0LTFcIixcbiAgICAgIHByb3BlcnRpZXM6IHtcbiAgICAgICAgc2F2ZWRJbWFnZTogXCJkb2NrZXIvZG9ja2VyLWltYWdlLnRhclwiLFxuICAgICAgICBpbWFnZU5hbWU6IFwiJHtQaXBlbGluZVN0YWNrLmVjckltYWdlTmFtZX1cIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJTdGF0aWNGaWxlc1wiOiB7XG4gICAgICB0eXBlOiBcIm5wbTovL0Bhd3MtY2RrL2Fzc2V0cy5EaXJlY3RvcnlEcm9wXCIsXG4gICAgICBlbnZpcm9ubWVudDogXCJhd3M6Ly8xMjM0NTY3ODkwMTIvZXUtd2VzdC0xXCIsXG4gICAgICBwcm9wZXJ0aWVzOiB7XG4gICAgICAgIGRpcmVjdG9yeTogXCJhc3NldHMvc3RhdGljLXdlYnNpdGVcIixcbiAgICAgICAgYnVja2V0TmFtZTogXCIke1BpcGVsaW5lU3RhY2suc3RhZ2luZ0J1Y2tldH1cIlxuICAgICAgfVxuICAgIH1cbiAgfVxufTtcbiJdfQ== \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.ts b/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.ts new file mode 100644 index 0000000000000..ad56b209fb461 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.ts @@ -0,0 +1,109 @@ +import nodeunit = require('nodeunit'); +import { validateManifest } from '../lib'; + +export = nodeunit.testCase({ + validateManifest: { + 'successfully loads the example manifest'(test: nodeunit.Test) { + test.doesNotThrow(() => validateManifest(SAMPLE_MANIFEST)); + test.done(); + }, + 'rejects a document where the schema is invalid'(test: nodeunit.Test) { + const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); + badManifest.schema = 'foo/1.0-bar'; + test.throws(() => validateManifest(badManifest), + /instance\.schema is not one of enum values/); + test.done(); + }, + 'rejects a document without drops'(test: nodeunit.Test) { + const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); + delete badManifest.drops; + test.throws(() => validateManifest(badManifest), + /instance requires property "drops"/); + test.done(); + }, + 'rejects a document with an illegal Logical ID'(test: nodeunit.Test) { + const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); + badManifest.drops['Pipeline.Stack'] = badManifest.drops.PipelineStack; + test.throws(() => validateManifest(badManifest), + /Invalid logical ID: Pipeline\.Stack/); + test.done(); + }, + 'rejects a document with unresolved dependsOn'(test: nodeunit.Test) { + const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); + badManifest.drops.PipelineStack.dependsOn = ['DoesNotExist']; + test.throws(() => validateManifest(badManifest), + /PipelineStack depends on undefined drop through dependsOn DoesNotExist/); + test.done(); + }, + 'rejects a document with direct circular dependency via dependsOn'(test: nodeunit.Test) { + const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); + badManifest.drops.PipelineStack.dependsOn = ['PipelineStack']; + test.throws(() => validateManifest(badManifest), + /PipelineStack => dependsOn PipelineStack/); + test.done(); + }, + 'rejects a document with indirect circular dependency'(test: nodeunit.Test) { + const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); + badManifest.drops.StaticFiles.dependsOn = ['ServiceStack-beta']; + test.throws(() => validateManifest(badManifest), + // tslint:disable-next-line:max-line-length + /StaticFiles => dependsOn ServiceStack-beta => ServiceStack-beta\.properties\.parameters\.websiteFilesKeyPrefix "\${StaticFiles\.keyPrefix}"/); + test.done(); + } + } +}); + +const SAMPLE_MANIFEST = { + schema: "cloud-assembly/1.0", + drops: { + "PipelineStack": { + type: "npm://@aws-cdk/aws-cloudformation.StackDrop", + environment: "aws://123456789012/eu-west-1", + properties: { + template: "stacks/PipelineStack.yml" + } + }, + "ServiceStack-beta": { + type: "npm://@aws-cdk/aws-cloudformation.StackDrop", + environment: "aws://123456789012/eu-west-1", + properties: { + template: "stacks/ServiceStack-beta.yml", + stackPolicy: "stacks/ServiceStack-beta.stack-policy.json", + parameters: { + image: "${DockerImage.exactImageId}", + websiteFilesBucket: "${StaticFiles.bucketName}", + websiteFilesKeyPrefix: "${StaticFiles.keyPrefix}", + } + } + }, + "ServiceStack-prod": { + type: "npm://@aws-cdk/aws-cloudformation.StackDrop", + environment: "aws://123456789012/eu-west-1", + properties: { + template: "stacks/ServiceStack-prod.yml", + stackPolicy: "stacks/ServiceStack-prod.stack-policy.json", + parameters: { + image: "${DockerImage.exactImageId}", + websiteFilesBucket: "${StaticFiles.bucketName}", + websiteFilesKeyPrefix: "${StaticFiles.keyPrefix}", + } + } + }, + "DockerImage": { + type: "npm://@aws-cdk/aws-ecr.DockerImageDrop", + environment: "aws://123456789012/eu-west-1", + properties: { + savedImage: "docker/docker-image.tar", + imageName: "${PipelineStack.ecrImageName}" + } + }, + "StaticFiles": { + type: "npm://@aws-cdk/assets.DirectoryDrop", + environment: "aws://123456789012/eu-west-1", + properties: { + directory: "assets/static-website", + bucketName: "${PipelineStack.stagingBucket}" + } + } + } +}; diff --git a/packages/@aws-cdk/cloud-assembly/tsconfig.json b/packages/@aws-cdk/cloud-assembly/tsconfig.json new file mode 100644 index 0000000000000..e479a2f1664cc --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + "charset": "utf8", + "declaration": true, + "experimentalDecorators": true, + "inlineSourceMap": true, + "inlineSources": true, + "lib": [ + "es2016", + "es2017.object", + "es2017.string" + ], + "module": "CommonJS", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "target": "ES2018" + }, + "_generated_by_jsii_": "Generated by jsii - safe to delete, and ideally should be in .gitignore" +} diff --git a/specifications/cloud_assembly.md b/specifications/cloud_assembly.md index e1761fdb80975..adeb3951f119b 100644 --- a/specifications/cloud_assembly.md +++ b/specifications/cloud_assembly.md @@ -19,8 +19,8 @@ The design goals for the *Cloud Assembly Specification* are the following: ## Specification A *Cloud Assembly* is a ZIP archive that **SHOULD** conform to the [ISO/IEC 21320-1:2015] *Document Container File* -standard. Use of the `deflate` compression method is **RECOMMENDED** in order to minimize the size of the resulting -file. *Cloud Assembly* files **SHOULD** use the `.cloud` extension in order to make them easier to recognize by users. +standard. *Cloud Assembly* files **SHOULD** use the `.cloud` extension in order to make them easier to recognize by +users. Documents in the archive can be stored with any name and directory structure, however the following entries at the root of the archive are reserved for special use: @@ -38,8 +38,8 @@ Key |Type |Required|Description `missing` |`Map`| |A mapping of context keys to [missing information](#missing). The [JSON] specification allows for keys to be specified multiple times in a given `object`. However, *Cloud Assembly* -consumers **MAY** assume keys are unique, and [*Cloud Assemblers*](#cloud-assemblers) **SHOULD** avoid generating -duplicate keys. If duplicate keys are present, the latest specified value **SHOULD** be preferred. +consumers **MAY** assume keys are unique, and *Cloud Assemblers* **SHOULD** avoid generating duplicate keys. If +duplicate keys are present and the manifest parser permits it, the latest specified value **SHOULD** be preferred. ### Logical ID *Logical IDs* are `string`s that uniquely identify [Drop](#drop)s in the context of a *Cloud Assembly*. @@ -63,15 +63,16 @@ In other words, *Logical IDs* are expected to match the following regular expres ### Drop Clouds are made of Drops. Thet are the building blocks of *Cloud Assemblies*. They model a part of the -*cloud application* that can be deployed independently, provided it's dependencies are fulfilled. Drops are specified +*cloud application* that can be deployed independently, provided its dependencies are fulfilled. Drops are specified using [JSON] objects that **MUST** conform to the following schema: -Key |Type |Required|Description --------------|-----------------|:------:|----------- -`type` |`string` |Required|The [*Drop Type*](#drop-type) specifier of this Drop. -`environment`|`string` |required|The target [environment](#environment) in which Drop is deployed. -`metadata` |`Map`| |Arbitrary key-value pairs associated with this Drop. -`properties` |`Map`| |The properties of this Drop as documented by its maintainers. +Key |Type |Required|Description +-------------|----------------------|:------:|----------- +`type` |`string` |Required|The [*Drop Type*](#drop-type) specifier of this Drop. +`environment`|`string` |required|The target [environment](#environment) in which Drop is deployed. +`dependsOn` |`string[]` | |*Logical IDs* of other Drops that must be deployed before this one. +`metadata` |`Map`| |Arbitrary named [metadata](#metadata) associated with this Drop. +`properties` |`Map` | |The properties of this Drop as documented by its maintainers. Each [Drop Type](#drop-type) can produce output strings that allow Drops to provide informations that other Drops can use when composing the *cloud application*. Each Drop implementer is responsible to document the output attributes it @@ -89,9 +90,12 @@ The following escape sequences are valid: * `\\` encodes the `\` literal * `\${` encodes the `${` literal -Deployment systems **SHOULD** return an error upon encountering an occurrence of the `/` literal that is not part of a +Deployment systems **SHOULD** return an error upon encountering an occurrence of the `\` literal that is not part of a valid escape sequence. +Drops **MUST NOT** cause circular dependencies. Deployment systems **SHOULD** detect cycles and fail upon discovering +one. + #### Drop Type Every Drop has a type specifier, which allows *Cloud Assembly* consumers to know how to deploy it. The type specifiers are `string`s that use an URI-like syntax (`protocol://path`), providing the coordinates to a reference implementation @@ -129,7 +133,25 @@ aws://account/region └───────────────── AWS protocol specifier ``` -### `Missing` +### Metadata +Metadata can be attached to [Drops](#drop) to allow tools that work with *Cloud Assemblies* to share additional +information about the *cloud application*. Metadata **SHOULD NOT** be used to convey data that is necessary for +correctly process the *Cloud Assembly*, since any tool that consumes a *Cloud Assembly* **MAY** choose to ignore any or +all Metadata. + +Key |Type |Required|Description +-------|--------|:------:|----------- +`kind` |`string`|Required|A token identifying the kind of metadata. +`value`|`any` |Required|The value associated with this metadata. + +A common use-case for Metadata is reporting warning or error messages that were emitted during the creation of the +*Cloud Assembly*, so that deployment systems can present this information to the user. Warning and error messages +**SHOULD** set the `kind` field to `warning` and `error` respectively, and the `value` field **SHOULD** contain a single +`string`. Deployment systems **MAY** reject *Cloud Assemblies* that include [Drops](#drop) that carry one or more +`error` Metadata entries, and they **SHOULD** surface `warning` messages to the user, either directly through their user +interface, or in the execution log. + +### Missing [Drops](#drop) may require contextual information to be available in order to correctly participate in a *Cloud Assembly*. When information is missing, *Cloud Assembly* producers report the missing information by adding entries to the `missing` section of the [manifest document](#manifest-document). The values are [JSON] `object`s that @@ -166,6 +188,8 @@ Deployment systems that support verifying signed *Cloud Assemblies*: ## Annex ### Examples of Drops for the AWS Cloud +The Drop specifications provided in this section are for illustration purpose only. + #### `@aws-cdk/aws-cloudformation.StackDrop` A [*CloudFormation* stack][CFN Stack]. @@ -177,10 +201,10 @@ Property |Type |Required|Description `stackPolicy`|`string` | |The assembly-relative path to the [Stack Policy][CFN Stack Policy]. ##### Output Attributes -Attribute |Type |Description -----------|--------------------|----------- -`outputs` |`Map`|Data returned by [*CloudFormation* Outputs][CFN Outputs] of the stack. -`stackArn`|`string` |The ARN of the [stack][CFN Stack]. +Attribute |Type |Description +---------------|--------------------|----------- +`output.`|`string`|Data returned by the [*CloudFormation* Outputs][CFN Output] named `` of the stack. +`stackArn` |`string`|The ARN of the [stack][CFN Stack]. ##### Example ```json @@ -190,8 +214,8 @@ Attribute |Type |Description "properties": { "template": "my-stack/template.yml", "parameters": { - "bucketName": "${helperStack.bucketName}", - "objectKey": "${helperStack.objectKey}" + "bucketName": "${helperStack.output.bucketName}", + "objectKey": "${helperStack.output.objectKey}" }, "stackPolicy": "my-stack/policy.json" } @@ -225,7 +249,7 @@ Attribute |Type |Description "environment": "aws://000000000000/bermuda-triangle-1", "properties": { "file": "assets/file.bin", - "bucket": "${helperStack.bucketName}", + "bucket": "${helperStack.outputs.bucketName}", "objectKey": "assets/da39a3ee5e6b4b0d3255bfef95601890afd80709/nifty-asset.png" } } @@ -238,7 +262,7 @@ A Docker image to be published to an *ECR* registry. Property |Type |Required|Description ------------|--------|:------:|----------- `savedImage`|`string`|Required|The assembly-relative path to the tar archive obtained from `docker image save`. -`imageName` |`string`|Required|The name of the image (e.g: `000000000000.dkr.ecr.bermuda-triangle-1.amazon.com/name`). +`pushTarget`|`string`|Required|Where the image should be pushed to (e.g: `.dkr.ecr..amazon.com/`). `tagName` |`string`| |The name of the tag to use when pushing the image (default: `latest`). ##### Output Attributes @@ -254,7 +278,7 @@ Attribute |Type |Description "environment": "aws://000000000000/bermuda-triangle-1", "properties": { "savedImage": "docker/37e6de0b24fa.tar", - "imageName": "${helperStack.ecrImageName}", + "imageName": "${helperStack.output.ecrImageName}", "tagName": "latest" } } @@ -323,7 +347,7 @@ Here is an example the contents of a complete *Cloud Assembly* that deploys AWS "environment": "aws://123456789012/eu-west-1", "properties": { "savedImage": "docker/docker-image.tar", - "imageName": "${PipelineStack.ecrImageName}" + "imageName": "${PipelineStack.output.ecrImageName}" } }, "StaticFiles": { @@ -331,7 +355,7 @@ Here is an example the contents of a complete *Cloud Assembly* that deploys AWS "environment": "aws://123456789012/eu-west-1", "properties": { "directory": "assets/static-website", - "bucketName": "${PipelineStack.stagingBucket}" + "bucketName": "${PipelineStack.output.stagingBucket}" } } } From 27bd29b3ca4badd960babc09eb374d1323debc00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Thu, 15 Nov 2018 11:28:55 +0100 Subject: [PATCH 04/15] Remove references to 'the user' --- specifications/cloud_assembly.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/specifications/cloud_assembly.md b/specifications/cloud_assembly.md index adeb3951f119b..6b95e4bc9b05b 100644 --- a/specifications/cloud_assembly.md +++ b/specifications/cloud_assembly.md @@ -145,11 +145,11 @@ Key |Type |Required|Description `value`|`any` |Required|The value associated with this metadata. A common use-case for Metadata is reporting warning or error messages that were emitted during the creation of the -*Cloud Assembly*, so that deployment systems can present this information to the user. Warning and error messages +*Cloud Assembly*, so that deployment systems can present this information to users or logs. Warning and error messages **SHOULD** set the `kind` field to `warning` and `error` respectively, and the `value` field **SHOULD** contain a single `string`. Deployment systems **MAY** reject *Cloud Assemblies* that include [Drops](#drop) that carry one or more -`error` Metadata entries, and they **SHOULD** surface `warning` messages to the user, either directly through their user -interface, or in the execution log. +`error` Metadata entries, and they **SHOULD** surface `warning` messages, either directly through their user interface, +or in the execution log. ### Missing [Drops](#drop) may require contextual information to be available in order to correctly participate in a @@ -166,7 +166,7 @@ Key |Type |Required|Description #### Signing *Cloud Assemblers* **SHOULD** support digital signature of *Cloud Assemblies*. When support for digital signature is present, *Cloud Assemblers*: -* **MUST** require the user to specify which [PGP][RFC 4880] key should be used. +* **MUST** require configuration of the [PGP][RFC 4880] key that will be used for signing. ##### Signing Algorithm @@ -176,7 +176,7 @@ Deployment systems **SHOULD** support verifying signed *Cloud Assemblies*. If su present, a warning **MUST** be emitted when processing a *Cloud Assembly* that contains the `signature.asc` file. Deployment systems that support verifying signed *Cloud Assemblies*: -* **SHOULD** allow the user to *require* that an assembly is signed. When this requirement is active, an error **MUST** +* **SHOULD** be configurable to *require* that an assembly is signed. When this requirement is active, an error **MUST** be returned when attempting to deploy an un-signed *Cloud Assembly*. * **MUST** verify the integrity and authenticity of signed *Cloud Assemblies* prior to attempting to load any file included in it, except for `signature.asc`. @@ -184,7 +184,7 @@ Deployment systems that support verifying signed *Cloud Assemblies*: * An error **MUST** be raised if the [PGP][RFC 4880] key has expired according to the signature timestamp. * An error **MUST** be raised if the [PGP][RFC 4880] key is known to have been revoked. Deployment systems **MAY** trust locally available information pertaining to the key's validity. -* **SHOULD** allow the user to specify a list of trusted [PGP][RFC 4880] keys. +* **SHOULD** allow configuration of a list of trusted [PGP][RFC 4880] keys. ## Annex ### Examples of Drops for the AWS Cloud From af266bce67bb92d5d4140e34024dee43634fd132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Thu, 15 Nov 2018 13:10:39 +0100 Subject: [PATCH 05/15] Elaborate on the digital signature process --- specifications/cloud_assembly.md | 52 +++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/specifications/cloud_assembly.md b/specifications/cloud_assembly.md index 6b95e4bc9b05b..d0fca7db6382d 100644 --- a/specifications/cloud_assembly.md +++ b/specifications/cloud_assembly.md @@ -169,7 +169,56 @@ present, *Cloud Assemblers*: * **MUST** require configuration of the [PGP][RFC 4880] key that will be used for signing. ##### Signing Algorithm - +The digital signature of *Cloud Assemblies* starts by establishing an attestation document that provides cryptographic +summary information about the contents of the signed assembly. It is a [JSON] document composed of a single `object` +with the following fields: + +Field |Type |Description +-----------|----------------------|----------- +`timestamp`|`string` |The [ISO 8601] timestamp of the attestation document creation time +`algorithm`|`string` |The hashing algorithm used to derive the `FileData` hashes. +`nonce` |`string` |The nonce used when deriving the `FileData` hashes. +`items` |`Map`|Summary information about the attested files. + +The `algorithm` field **MUST** be set to the standard identifier of a standard hashing algorithm, such as `SHA256`. +Algorithms that are vulnerable to known collision attacks **SHOULD** not be used. + +The `nonce` field **MUST** be set to a byte array generated using a cryptographically secure random number generator. A +`nonce` **MUST NOT** be re-used. It **MUST** be composed of at least `32` bytes, and **SHOULD** be the same length or +larger than the size of the output of the chosen `algorithm`. + +The `items` field **MUST** contain one entry for each file in the *Cloud Assembly*, keyed on the relative path to the +file within the container document, with a value that contains the following keys: +Key |Type |Description +------|--------|----------- +`size`|`string`|The decimal representation of the file size in bytes. +`hash`|`string`|The base-64 encoded result of hashing the file's content appended with the `nonce` using the `algorithm`. + +Here is a schematic example: +```js +{ + // When this attestation doucment was created + "timestamp": "2018-11-15T11:08:52", + // The hashing algorithm for the attestation is SHA256 + "algorithm": "SHA256", + // 32 bytes of cryptographically-secure randomness + "nonce": "2tDLdIoy1VtzLFjfzXVqzsNJHa9862y/WQgqKzC9+xs=", + "items": { + "data/data.bin": { + // The file is really 1024 times the character 'a' + "size": "1024", + // SHA256( + ) + "hash": "HIKJYDnT92EKILbFt2SOzA8dWF0YMEBHS72xLSw4lok=" + }, + /* ...other files of the assembly... */ + } +} +``` + +Once the attestation is ready, it is digitally *signed* using the configured [PGP][RFC 4880] key. The key **MUST** be +valid as of the `timestamp` field included in the attestation. The siganture **MUST** not be detached, and is +**RECOMMENDED** to use the *cleartext signature framework* described in section 7 of [RFC 4880] so the attestation can +be read by a human. #### Verifying Deployment systems **SHOULD** support verifying signed *Cloud Assemblies*. If support for signature verification is not @@ -420,3 +469,4 @@ Hash: SHA256 [ISO/IEC 21320-1:2015]: https://www.iso.org/standard/60101.html [JSON]: https://www.json.org [RFC 4880]: https://tools.ietf.org/html/rfc4880 +[ISO 8601]: https://www.iso.org/standard/40874.html From 7878c5172cf6f05ae8db1e217a1bb5c9f1329dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Thu, 15 Nov 2018 14:11:43 +0100 Subject: [PATCH 06/15] Remove some unnecessary files --- packages/@aws-cdk/cloud-assembly/.gitignore | 7 +- packages/@aws-cdk/cloud-assembly/.jsii | 225 ------------------ .../@aws-cdk/cloud-assembly/lib/index.d.ts | 2 - packages/@aws-cdk/cloud-assembly/lib/index.js | 7 - .../@aws-cdk/cloud-assembly/lib/manifest.d.ts | 54 ----- .../@aws-cdk/cloud-assembly/lib/manifest.js | 3 - .../@aws-cdk/cloud-assembly/lib/manifest.ts | 2 + .../cloud-assembly/lib/validate-manifest.d.ts | 14 -- .../cloud-assembly/lib/validate-manifest.js | 115 --------- .../test/test.validate-manifest.d.ts | 2 - .../test/test.validate-manifest.js | 104 -------- 11 files changed, 7 insertions(+), 528 deletions(-) delete mode 100644 packages/@aws-cdk/cloud-assembly/.jsii delete mode 100644 packages/@aws-cdk/cloud-assembly/lib/index.d.ts delete mode 100644 packages/@aws-cdk/cloud-assembly/lib/index.js delete mode 100644 packages/@aws-cdk/cloud-assembly/lib/manifest.d.ts delete mode 100644 packages/@aws-cdk/cloud-assembly/lib/manifest.js delete mode 100644 packages/@aws-cdk/cloud-assembly/lib/validate-manifest.d.ts delete mode 100644 packages/@aws-cdk/cloud-assembly/lib/validate-manifest.js delete mode 100644 packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.d.ts delete mode 100644 packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.js diff --git a/packages/@aws-cdk/cloud-assembly/.gitignore b/packages/@aws-cdk/cloud-assembly/.gitignore index 448d3c6ad1a2a..5a91a47f592d4 100644 --- a/packages/@aws-cdk/cloud-assembly/.gitignore +++ b/packages/@aws-cdk/cloud-assembly/.gitignore @@ -2,7 +2,10 @@ dist .LAST_PACKAGE .LAST_BUILD -*.snk +.jsii .nyc_output +.nycrc +*.snk +*.js +*.d.ts coverage -.nycrc \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly/.jsii b/packages/@aws-cdk/cloud-assembly/.jsii deleted file mode 100644 index af917c4e04f8e..0000000000000 --- a/packages/@aws-cdk/cloud-assembly/.jsii +++ /dev/null @@ -1,225 +0,0 @@ -{ - "author": { - "name": "Amazon Web Services", - "organization": true, - "roles": [ - "author" - ], - "url": "https://aws.amazon.com" - }, - "bundled": { - "jsonschema": "^1.2.4" - }, - "description": "Reference implementation for the Cloud Assembly specification", - "homepage": "https://github.com/awslabs/aws-cdk", - "license": "Apache-2.0", - "name": "@aws-cdk/cloud-assembly", - "readme": { - "markdown": "## Reference implementation for the Cloud Assembly specification\nThis module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project." - }, - "repository": { - "type": "git", - "url": "https://github.com/awslabs/aws-cdk.git" - }, - "schema": "jsii/1.0", - "targets": { - "dotnet": { - "assemblyOriginatorKeyFile": "../../key.snk", - "namespace": "Amazon.CDK.CloudAssembly", - "packageId": "Amazon.CDK.CloudAssembly", - "signAssembly": true - }, - "java": { - "maven": { - "artifactId": "cdk-cloud-assembly", - "groupId": "software.amazon.awscdk" - }, - "package": "software.amazon.awscdk.cloudassembly" - }, - "js": { - "npm": "@aws-cdk/cloud-assembly" - } - }, - "types": { - "@aws-cdk/cloud-assembly.Drop": { - "assembly": "@aws-cdk/cloud-assembly", - "datatype": true, - "fqn": "@aws-cdk/cloud-assembly.Drop", - "kind": "interface", - "name": "Drop", - "properties": [ - { - "abstract": true, - "docs": { - "pattern": "^[^:]+://.+$" - }, - "name": "environment", - "type": { - "primitive": "string" - } - }, - { - "abstract": true, - "docs": { - "pattern": "^[^:]+://.+$" - }, - "name": "type", - "type": { - "primitive": "string" - } - }, - { - "abstract": true, - "docs": { - "minItems": "1" - }, - "name": "dependsOn", - "type": { - "collection": { - "elementtype": { - "primitive": "string" - }, - "kind": "array" - }, - "optional": true - } - }, - { - "abstract": true, - "docs": { - "minProperties": "1" - }, - "name": "metadata", - "type": { - "collection": { - "elementtype": { - "fqn": "@aws-cdk/cloud-assembly.Metadata" - }, - "kind": "map" - }, - "optional": true - } - }, - { - "abstract": true, - "docs": { - "minProperties": "1" - }, - "name": "properties", - "type": { - "collection": { - "elementtype": { - "primitive": "any" - }, - "kind": "map" - }, - "optional": true - } - } - ] - }, - "@aws-cdk/cloud-assembly.Manifest": { - "assembly": "@aws-cdk/cloud-assembly", - "datatype": true, - "fqn": "@aws-cdk/cloud-assembly.Manifest", - "kind": "interface", - "name": "Manifest", - "properties": [ - { - "abstract": true, - "name": "drops", - "type": { - "collection": { - "elementtype": { - "fqn": "@aws-cdk/cloud-assembly.Drop" - }, - "kind": "map" - } - } - }, - { - "abstract": true, - "name": "schema", - "type": { - "primitive": "string" - } - }, - { - "abstract": true, - "docs": { - "minProperties": "1" - }, - "name": "missing", - "type": { - "collection": { - "elementtype": { - "fqn": "@aws-cdk/cloud-assembly.Missing" - }, - "kind": "map" - }, - "optional": true - } - } - ] - }, - "@aws-cdk/cloud-assembly.Metadata": { - "assembly": "@aws-cdk/cloud-assembly", - "datatype": true, - "fqn": "@aws-cdk/cloud-assembly.Metadata", - "kind": "interface", - "name": "Metadata", - "properties": [ - { - "abstract": true, - "name": "tag", - "type": { - "primitive": "string" - } - }, - { - "abstract": true, - "name": "value", - "type": { - "primitive": "any" - } - } - ] - }, - "@aws-cdk/cloud-assembly.Missing": { - "assembly": "@aws-cdk/cloud-assembly", - "datatype": true, - "fqn": "@aws-cdk/cloud-assembly.Missing", - "kind": "interface", - "name": "Missing", - "properties": [ - { - "abstract": true, - "docs": { - "minProperties": "1" - }, - "name": "props", - "type": { - "collection": { - "elementtype": { - "primitive": "any" - }, - "kind": "map" - } - } - }, - { - "abstract": true, - "docs": { - "pattern": "^[^:]+://.+$" - }, - "name": "provider", - "type": { - "primitive": "string" - } - } - ] - } - }, - "version": "0.16.0", - "fingerprint": "96keiznBnV4YBFKnCtU/T+BZEtUCSx6Q4BHSzy96nrc=" -} diff --git a/packages/@aws-cdk/cloud-assembly/lib/index.d.ts b/packages/@aws-cdk/cloud-assembly/lib/index.d.ts deleted file mode 100644 index 8fe1d654f83a5..0000000000000 --- a/packages/@aws-cdk/cloud-assembly/lib/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './manifest'; -export * from './validate-manifest'; diff --git a/packages/@aws-cdk/cloud-assembly/lib/index.js b/packages/@aws-cdk/cloud-assembly/lib/index.js deleted file mode 100644 index 8a83f0a4b1392..0000000000000 --- a/packages/@aws-cdk/cloud-assembly/lib/index.js +++ /dev/null @@ -1,7 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -Object.defineProperty(exports, "__esModule", { value: true }); -__export(require("./validate-manifest")); -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQUNBLHlDQUFvQyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCAqIGZyb20gJy4vbWFuaWZlc3QnO1xuZXhwb3J0ICogZnJvbSAnLi92YWxpZGF0ZS1tYW5pZmVzdCc7XG4iXX0= \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly/lib/manifest.d.ts b/packages/@aws-cdk/cloud-assembly/lib/manifest.d.ts deleted file mode 100644 index aa999ff8558f1..0000000000000 --- a/packages/@aws-cdk/cloud-assembly/lib/manifest.d.ts +++ /dev/null @@ -1,54 +0,0 @@ -export interface Manifest { - schema: 'cloud-assembly/1.0'; - drops: { - [logicalId: string]: Drop; - }; - /** - * @minProperties 1 - */ - missing?: { - [key: string]: Missing; - }; -} -export interface Drop { - /** - * @minItems 1 - */ - dependsOn?: string[]; - /** - * @pattern ^[^:]+://.+$ - */ - type: string; - /** - * @pattern ^[^:]+://.+$ - */ - environment: string; - /** - * @minProperties 1 - */ - metadata?: { - [key: string]: Metadata; - }; - /** - * @minProperties 1 - */ - properties?: { - [name: string]: any; - }; -} -export interface Missing { - /** - * @pattern ^[^:]+://.+$ - */ - provider: string; - /** - * @minProperties 1 - */ - props: { - [key: string]: any; - }; -} -export interface Metadata { - tag: string; - value: any; -} diff --git a/packages/@aws-cdk/cloud-assembly/lib/manifest.js b/packages/@aws-cdk/cloud-assembly/lib/manifest.js deleted file mode 100644 index e6f44bba378ef..0000000000000 --- a/packages/@aws-cdk/cloud-assembly/lib/manifest.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFuaWZlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJtYW5pZmVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGludGVyZmFjZSBNYW5pZmVzdCB7XG4gIHNjaGVtYTogJ2Nsb3VkLWFzc2VtYmx5LzEuMCc7XG4gIGRyb3BzOiB7IFtsb2dpY2FsSWQ6IHN0cmluZ106IERyb3AgfTtcbiAgLyoqXG4gICAqIEBtaW5Qcm9wZXJ0aWVzIDFcbiAgICovXG4gIG1pc3Npbmc/OiB7IFtrZXk6IHN0cmluZ106IE1pc3NpbmcgfTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBEcm9wIHtcbiAgLyoqXG4gICAqIEBtaW5JdGVtcyAxXG4gICAqL1xuICBkZXBlbmRzT24/OiBzdHJpbmdbXTtcbiAgLyoqXG4gICAqIEBwYXR0ZXJuIF5bXjpdKzovLy4rJFxuICAgKi9cbiAgdHlwZTogc3RyaW5nO1xuICAvKipcbiAgICogQHBhdHRlcm4gXlteOl0rOi8vLiskXG4gICAqL1xuICBlbnZpcm9ubWVudDogc3RyaW5nO1xuICAvKipcbiAgICogQG1pblByb3BlcnRpZXMgMVxuICAgKi9cbiAgbWV0YWRhdGE/OiB7IFtrZXk6IHN0cmluZ106IE1ldGFkYXRhIH07XG4gIC8qKlxuICAgKiBAbWluUHJvcGVydGllcyAxXG4gICAqL1xuICBwcm9wZXJ0aWVzPzogeyBbbmFtZTogc3RyaW5nXTogYW55IH1cbn1cblxuZXhwb3J0IGludGVyZmFjZSBNaXNzaW5nIHtcbiAgLyoqXG4gICAqIEBwYXR0ZXJuIF5bXjpdKzovLy4rJFxuICAgKi9cbiAgcHJvdmlkZXI6IHN0cmluZztcbiAgLyoqXG4gICAqIEBtaW5Qcm9wZXJ0aWVzIDFcbiAgICovXG4gIHByb3BzOiB7IFtrZXk6IHN0cmluZ106IGFueSB9O1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIE1ldGFkYXRhIHtcbiAgdGFnOiBzdHJpbmc7XG4gIHZhbHVlOiBhbnk7XG59XG4iXX0= \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly/lib/manifest.ts b/packages/@aws-cdk/cloud-assembly/lib/manifest.ts index 5104dabbea7d6..15b7fc1e181ea 100644 --- a/packages/@aws-cdk/cloud-assembly/lib/manifest.ts +++ b/packages/@aws-cdk/cloud-assembly/lib/manifest.ts @@ -1,3 +1,5 @@ +export const MANIFEST_FILE_NAME = 'manifest.json'; + export interface Manifest { schema: 'cloud-assembly/1.0'; drops: { [logicalId: string]: Drop }; diff --git a/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.d.ts b/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.d.ts deleted file mode 100644 index 1955ad3f8995f..0000000000000 --- a/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import jsonschema = require('jsonschema'); -import { Manifest } from './manifest'; -export declare const schema: jsonschema.Schema; -/** - * Validate that ``obj`` is a valid Cloud Assembly manifest document, both syntactically and semantically. The semantic - * validation ensures all Drop references are valid (they point to an existing Drop in the same manifest document), and - * that there are no cycles in the dependency graph described by the manifest. - * - * @param obj the object to be validated. - * - * @returns ``obj`` - * @throws Error if ``obj`` is not a Cloud Assembly manifest document or if it is semantically invalid. - */ -export declare function validateManifest(obj: unknown): Manifest; diff --git a/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.js b/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.js deleted file mode 100644 index 18b15b5de9591..0000000000000 --- a/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.js +++ /dev/null @@ -1,115 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const jsonschema = require("jsonschema"); -// tslint:disable-next-line:no-var-requires -exports.schema = require('../schema/manifest.schema.json'); -/** - * Validate that ``obj`` is a valid Cloud Assembly manifest document, both syntactically and semantically. The semantic - * validation ensures all Drop references are valid (they point to an existing Drop in the same manifest document), and - * that there are no cycles in the dependency graph described by the manifest. - * - * @param obj the object to be validated. - * - * @returns ``obj`` - * @throws Error if ``obj`` is not a Cloud Assembly manifest document or if it is semantically invalid. - */ -function validateManifest(obj) { - const validator = new jsonschema.Validator(); - const result = validator.validate(obj, exports.schema); - if (result.valid) { - return validateSemantics(obj); - } - throw new Error(`Invalid Cloud Assembly manifest: ${result}`); -} -exports.validateManifest = validateManifest; -function validateSemantics(manifest) { - const dependencyGraph = {}; - for (const logicalId of Object.keys(manifest.drops)) { - assertValidLogicalId(logicalId); - const drop = manifest.drops[logicalId]; - const references = dependencyGraph[logicalId] = listReferences(drop, logicalId); - for (const ref of references) { - if (!(ref.logicalId in manifest.drops)) { - throw new Error(`${logicalId} depends on undefined drop through ${ref.context}.`); - } - } - } - assertNoCycles(); - return manifest; - function assertNoCycles() { - for (const logicalId of Object.keys(dependencyGraph)) { - for (const reference of dependencyGraph[logicalId]) { - reference.subreferences = dependencyGraph[reference.logicalId]; - } - } - const cycles = Object.keys(dependencyGraph) - .map(shortestCycle) - .filter(cycle => cycle.length > 0) - .sort((l, r) => l.length - r.length); - if (cycles.length > 0) { - const cyclesDecription = cycles.map(cycle => `- ${cycle.join(' => ')}`); - throw new Error(`Found dependency cycles:\n${cyclesDecription.join('\n')}`); - } - function shortestCycle(fromId) { - const toProcess = dependencyGraph[fromId].map(ref => ({ ref, path: [fromId] })); - const visited = new Set(); - while (toProcess.length > 0) { - const candidate = toProcess.pop(); - if (candidate.ref.logicalId === fromId) { - return [...candidate.path, candidate.ref.context]; - } - if (!visited.has(candidate.ref.logicalId)) { - toProcess.unshift(...candidate.ref.subreferences.map(ref => ({ ref, path: [...candidate.path, candidate.ref.context] }))); - visited.add(candidate.ref.logicalId); - } - } - return []; - } - } -} -function assertValidLogicalId(str) { - const regex = /^[A-Za-z0-9+\/_-]{1,256}$/; - if (!str.match(regex)) { - throw new Error(`Invalid logical ID: ${str} (does not match ${regex})`); - } -} -function listReferences(drop, dropId) { - const result = new Array(); - for (const logicalId of drop.dependsOn || []) { - result.push({ logicalId, context: `dependsOn ${logicalId}` }); - } - result.push(...listTokens(drop, dropId)); - return result; -} -function listTokens(obj, path) { - const result = new Array(); - if (typeof obj === 'string') { - const tokens = obj.match(/\\*\$\{[A-Za-z0-9+\/_-]{1,256}\.[^}]+\}/g); - for (const token of tokens || []) { - const parts = token.match(/(\\*)(\$\{([A-Za-z0-9+\/_-]{1,256})\.[^}]+\})/); - if (parts[1].length % 2 !== 0) { - // This one's quoted, so skip it. - continue; - } - result.push({ - logicalId: parts[3], - context: `${path} "${parts[2]}"` - }); - } - } - else if (typeof obj !== 'object' || obj == null) { - return []; - } - else if (Array.isArray(obj)) { - for (let i = 0; i < obj.length; i++) { - result.push(...listTokens(obj[i], `${path}[${i}]`)); - } - } - else { - for (const key of Object.keys(obj)) { - result.push(...listTokens(obj[key], `${path}.${key}`)); - } - } - return result; -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdGUtbWFuaWZlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJ2YWxpZGF0ZS1tYW5pZmVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLHlDQUEwQztBQUcxQywyQ0FBMkM7QUFDOUIsUUFBQSxNQUFNLEdBQXNCLE9BQU8sQ0FBQyxnQ0FBZ0MsQ0FBQyxDQUFDO0FBRW5GOzs7Ozs7Ozs7R0FTRztBQUNILFNBQWdCLGdCQUFnQixDQUFDLEdBQVk7SUFDM0MsTUFBTSxTQUFTLEdBQUcsSUFBSSxVQUFVLENBQUMsU0FBUyxFQUFFLENBQUM7SUFDN0MsTUFBTSxNQUFNLEdBQUcsU0FBUyxDQUFDLFFBQVEsQ0FBQyxHQUFHLEVBQUUsY0FBTSxDQUFDLENBQUM7SUFDL0MsSUFBSSxNQUFNLENBQUMsS0FBSyxFQUFFO1FBQ2hCLE9BQU8saUJBQWlCLENBQUMsR0FBZSxDQUFDLENBQUM7S0FDM0M7SUFDRCxNQUFNLElBQUksS0FBSyxDQUFDLG9DQUFvQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO0FBQ2hFLENBQUM7QUFQRCw0Q0FPQztBQUVELFNBQVMsaUJBQWlCLENBQUMsUUFBa0I7SUFDM0MsTUFBTSxlQUFlLEdBQWtDLEVBQUUsQ0FBQztJQUMxRCxLQUFLLE1BQU0sU0FBUyxJQUFJLE1BQU0sQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxFQUFFO1FBQ25ELG9CQUFvQixDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQ2hDLE1BQU0sSUFBSSxHQUFHLFFBQVEsQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLENBQUM7UUFDdkMsTUFBTSxVQUFVLEdBQUcsZUFBZSxDQUFDLFNBQVMsQ0FBQyxHQUFHLGNBQWMsQ0FBQyxJQUFJLEVBQUUsU0FBUyxDQUFDLENBQUM7UUFDaEYsS0FBSyxNQUFNLEdBQUcsSUFBSSxVQUFVLEVBQUU7WUFDNUIsSUFBSSxDQUFDLENBQUMsR0FBRyxDQUFDLFNBQVMsSUFBSSxRQUFRLENBQUMsS0FBSyxD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S0FBSyxRQUFRLElBQUksR0FBRyxJQUFJLElBQUksRUFBRTtRQUNqRCxPQUFPLEVBQUUsQ0FBQztLQUNYO1NBQU0sSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxFQUFFO1FBQzdCLEtBQUssSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFHLENBQUMsR0FBRyxHQUFHLENBQUMsTUFBTSxFQUFHLENBQUMsRUFBRSxFQUFFO1lBQ3JDLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxVQUFVLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLEdBQUcsSUFBSSxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztTQUNyRDtLQUNGO1NBQU07UUFDTCxLQUFLLE1BQU0sR0FBRyxJQUFJLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUU7WUFDbEMsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLFVBQVUsQ0FBRSxHQUFXLENBQUMsR0FBRyxDQUFDLEVBQUUsR0FBRyxJQUFJLElBQUksR0FBRyxFQUFFLENBQUMsQ0FBQyxDQUFDO1NBQ2pFO0tBQ0Y7SUFDRCxPQUFPLE1BQU0sQ0FBQztBQUNoQixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGpzb25zY2hlbWEgPSByZXF1aXJlKCdqc29uc2NoZW1hJyk7XG5pbXBvcnQgeyBEcm9wLCBNYW5pZmVzdCB9IGZyb20gJy4vbWFuaWZlc3QnO1xuXG4vLyB0c2xpbnQ6ZGlzYWJsZS1uZXh0LWxpbmU6bm8tdmFyLXJlcXVpcmVzXG5leHBvcnQgY29uc3Qgc2NoZW1hOiBqc29uc2NoZW1hLlNjaGVtYSA9IHJlcXVpcmUoJy4uL3NjaGVtYS9tYW5pZmVzdC5zY2hlbWEuanNvbicpO1xuXG4vKipcbiAqIFZhbGlkYXRlIHRoYXQgYGBvYmpgYCBpcyBhIHZhbGlkIENsb3VkIEFzc2VtYmx5IG1hbmlmZXN0IGRvY3VtZW50LCBib3RoIHN5bnRhY3RpY2FsbHkgYW5kIHNlbWFudGljYWxseS4gVGhlIHNlbWFudGljXG4gKiB2YWxpZGF0aW9uIGVuc3VyZXMgYWxsIERyb3AgcmVmZXJlbmNlcyBhcmUgdmFsaWQgKHRoZXkgcG9pbnQgdG8gYW4gZXhpc3RpbmcgRHJvcCBpbiB0aGUgc2FtZSBtYW5pZmVzdCBkb2N1bWVudCksIGFuZFxuICogdGhhdCB0aGVyZSBhcmUgbm8gY3ljbGVzIGluIHRoZSBkZXBlbmRlbmN5IGdyYXBoIGRlc2NyaWJlZCBieSB0aGUgbWFuaWZlc3QuXG4gKlxuICogQHBhcmFtIG9iaiB0aGUgb2JqZWN0IHRvIGJlIHZhbGlkYXRlZC5cbiAqXG4gKiBAcmV0dXJucyBgYG9iamBgXG4gKiBAdGhyb3dzIEVycm9yIGlmIGBgb2JqYGAgaXMgbm90IGEgQ2xvdWQgQXNzZW1ibHkgbWFuaWZlc3QgZG9jdW1lbnQgb3IgaWYgaXQgaXMgc2VtYW50aWNhbGx5IGludmFsaWQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiB2YWxpZGF0ZU1hbmlmZXN0KG9iajogdW5rbm93bik6IE1hbmlmZXN0IHtcbiAgY29uc3QgdmFsaWRhdG9yID0gbmV3IGpzb25zY2hlbWEuVmFsaWRhdG9yKCk7XG4gIGNvbnN0IHJlc3VsdCA9IHZhbGlkYXRvci52YWxpZGF0ZShvYmosIHNjaGVtYSk7XG4gIGlmIChyZXN1bHQudmFsaWQpIHtcbiAgICByZXR1cm4gdmFsaWRhdGVTZW1hbnRpY3Mob2JqIGFzIE1hbmlmZXN0KTtcbiAgfVxuICB0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgQ2xvdWQgQXNzZW1ibHkgbWFuaWZlc3Q6ICR7cmVzdWx0fWApO1xufVxuXG5mdW5jdGlvbiB2YWxpZGF0ZVNlbWFudGljcyhtYW5pZmVzdDogTWFuaWZlc3QpOiBNYW5pZmVzdCB7XG4gIGNvbnN0IGRlcGVuZGVuY3lHcmFwaDogeyBbaWQ6IHN0cmluZ106IFJlZmVyZW5jZVtdIH0gPSB7fTtcbiAgZm9yIChjb25zdCBsb2dpY2FsSWQgb2YgT2JqZWN0LmtleXMobWFuaWZlc3QuZHJvcHMpKSB7XG4gICAgYXNzZXJ0VmFsaWRMb2dpY2FsSWQobG9naWNhbElkKTtcbiAgICBjb25zdCBkcm9wID0gbWFuaWZlc3QuZHJvcHNbbG9naWNhbElkXTtcbiAgICBjb25zdCByZWZlcmVuY2VzID0gZGVwZW5kZW5jeUdyYXBoW2xvZ2ljYWxJZF0gPSBsaXN0UmVmZXJlbmNlcyhkcm9wLCBsb2dpY2FsSWQpO1xuICAgIGZvciAoY29uc3QgcmVmIG9mIHJlZmVyZW5jZXMpIHtcbiAgICAgIGlmICghKHJlZi5sb2dpY2FsSWQgaW4gbWFuaWZlc3QuZHJvcHMpKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgJHtsb2dpY2FsSWR9IGRlcGVuZHMgb24gdW5kZWZpbmVkIGRyb3AgdGhyb3VnaCAke3JlZi5jb250ZXh0fS5gKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgYXNzZXJ0Tm9DeWNsZXMoKTtcbiAgcmV0dXJuIG1hbmlmZXN0O1xuXG4gIGZ1bmN0aW9uIGFzc2VydE5vQ3ljbGVzKCkge1xuICAgIGZvciAoY29uc3QgbG9naWNhbElkIG9mIE9iamVjdC5rZXlzKGRlcGVuZGVuY3lHcmFwaCkpIHtcbiAgICAgIGZvciAoY29uc3QgcmVmZXJlbmNlIG9mIGRlcGVuZGVuY3lHcmFwaFtsb2dpY2FsSWRdKSB7XG4gICAgICAgIHJlZmVyZW5jZS5zdWJyZWZlcmVuY2VzID0gZGVwZW5kZW5jeUdyYXBoW3JlZmVyZW5jZS5sb2dpY2FsSWRdO1xuICAgICAgfVxuICAgIH1cbiAgICBjb25zdCBjeWNsZXMgPSBPYmplY3Qua2V5cyhkZXBlbmRlbmN5R3JhcGgpXG4gICAgICAgICAgICAgICAgICAgICAgICAgLm1hcChzaG9ydGVzdEN5Y2xlKVxuICAgICAgICAgICAgICAgICAgICAgICAgIC5maWx0ZXIoY3ljbGUgPT4gY3ljbGUubGVuZ3RoID4gMClcbiAgICAgICAgICAgICAgICAgICAgICAgICAuc29ydCgobCwgcikgPT4gbC5sZW5ndGggLSByLmxlbmd0aCk7XG4gICAgaWYgKGN5Y2xlcy5sZW5ndGggPiAwKSB7XG4gICAgICBjb25zdCBjeWNsZXNEZWNyaXB0aW9uID0gY3ljbGVzLm1hcChjeWNsZSA9PiBgLSAke2N5Y2xlLmpvaW4oJyA9PiAnKX1gKTtcbiAgICAgIHRocm93IG5ldyBFcnJvcihgRm91bmQgZGVwZW5kZW5jeSBjeWNsZXM6XFxuJHtjeWNsZXNEZWNyaXB0aW9uLmpvaW4oJ1xcbicpfWApO1xuICAgIH1cblxuICAgIGZ1bmN0aW9uIHNob3J0ZXN0Q3ljbGUoZnJvbUlkOiBzdHJpbmcpOiBzdHJpbmdbXSB7XG4gICAgICBjb25zdCB0b1Byb2Nlc3MgPSBkZXBlbmRlbmN5R3JhcGhbZnJvbUlkXS5tYXAocmVmID0+ICh7IHJlZiwgcGF0aDogW2Zyb21JZF0gfSkpO1xuICAgICAgY29uc3QgdmlzaXRlZCA9IG5ldyBTZXQ8c3RyaW5nPigpO1xuICAgICAgd2hpbGUgKHRvUHJvY2Vzcy5sZW5ndGggPiAwKSB7XG4gICAgICAgIGNvbnN0IGNhbmRpZGF0ZSA9IHRvUHJvY2Vzcy5wb3AoKSE7XG4gICAgICAgIGlmIChjYW5kaWRhdGUucmVmLmxvZ2ljYWxJZCA9PT0gZnJvbUlkKSB7XG4gICAgICAgICAgcmV0dXJuIFsuLi5jYW5kaWRhdGUucGF0aCwgY2FuZGlkYXRlLnJlZi5jb250ZXh0XTtcbiAgICAgICAgfVxuICAgICAgICBpZiAoIXZpc2l0ZWQuaGFzKGNhbmRpZGF0ZS5yZWYubG9naWNhbElkKSkge1xuICAgICAgICAgIHRvUHJvY2Vzcy51bnNoaWZ0KC4uLmNhbmRpZGF0ZS5yZWYuc3VicmVmZXJlbmNlcyEubWFwKHJlZiA9PiAoeyByZWYsIHBhdGg6IFsuLi5jYW5kaWRhdGUucGF0aCwgY2FuZGlkYXRlLnJlZi5jb250ZXh0XSB9KSkpO1xuICAgICAgICAgIHZpc2l0ZWQuYWRkKGNhbmRpZGF0ZS5yZWYubG9naWNhbElkKTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgICAgcmV0dXJuIFtdO1xuICAgIH1cbiAgfVxufVxuXG5mdW5jdGlvbiBhc3NlcnRWYWxpZExvZ2ljYWxJZChzdHI6IHN0cmluZyk6IHZvaWQge1xuICBjb25zdCByZWdleCA9IC9eW0EtWmEtejAtOStcXC9fLV17MSwyNTZ9JC87XG4gIGlmICghc3RyLm1hdGNoKHJlZ2V4KSkge1xuICAgIHRocm93IG5ldyBFcnJvcihgSW52YWxpZCBsb2dpY2FsIElEOiAke3N0cn0gKGRvZXMgbm90IG1hdGNoICR7cmVnZXh9KWApO1xuICB9XG59XG5cbmZ1bmN0aW9uIGxpc3RSZWZlcmVuY2VzKGRyb3A6IERyb3AsIGRyb3BJZDogc3RyaW5nKTogUmVmZXJlbmNlW10ge1xuICBjb25zdCByZXN1bHQgPSBuZXcgQXJyYXk8UmVmZXJlbmNlPigpO1xuICBmb3IgKGNvbnN0IGxvZ2ljYWxJZCBvZiBkcm9wLmRlcGVuZHNPbiB8fCBbXSkge1xuICAgIHJlc3VsdC5wdXNoKHsgbG9naWNhbElkLCBjb250ZXh0OiBgZGVwZW5kc09uICR7bG9naWNhbElkfWAgfSk7XG4gIH1cbiAgcmVzdWx0LnB1c2goLi4ubGlzdFRva2Vucyhkcm9wLCBkcm9wSWQpKTtcbiAgcmV0dXJuIHJlc3VsdDtcbn1cblxuZnVuY3Rpb24gbGlzdFRva2VucyhvYmo6IHVua25vd24sIHBhdGg6IHN0cmluZyk6IFJlZmVyZW5jZVtdIHtcbiAgY29uc3QgcmVzdWx0ID0gbmV3IEFycmF5PFJlZmVyZW5jZT4oKTtcbiAgaWYgKHR5cGVvZiBvYmogPT09ICdzdHJpbmcnKSB7XG4gICAgY29uc3QgdG9rZW5zID0gb2JqLm1hdGNoKC9cXFxcKlxcJFxce1tBLVphLXowLTkrXFwvXy1dezEsMjU2fVxcLltefV0rXFx9L2cpO1xuICAgIGZvciAoY29uc3QgdG9rZW4gb2YgdG9rZW5zIHx8IFtdKSB7XG4gICAgICBjb25zdCBwYXJ0cyA9IHRva2VuLm1hdGNoKC8oXFxcXCopKFxcJFxceyhbQS1aYS16MC05K1xcL18tXXsxLDI1Nn0pXFwuW159XStcXH0pLykhO1xuICAgICAgaWYgKHBhcnRzWzFdLmxlbmd0aCAlIDIgIT09IDApIHtcbiAgICAgICAgLy8gVGhpcyBvbmUncyBxdW90ZWQsIHNvIHNraXAgaXQuXG4gICAgICAgIGNvbnRpbnVlO1xuICAgICAgfVxuICAgICAgcmVzdWx0LnB1c2goe1xuICAgICAgICBsb2dpY2FsSWQ6IHBhcnRzWzNdLFxuICAgICAgICBjb250ZXh0OiBgJHtwYXRofSBcIiR7cGFydHNbMl19XCJgXG4gICAgICB9KTtcbiAgICB9XG4gIH0gZWxzZSBpZiAodHlwZW9mIG9iaiAhPT0gJ29iamVjdCcgfHwgb2JqID09IG51bGwpIHtcbiAgICByZXR1cm4gW107XG4gIH0gZWxzZSBpZiAoQXJyYXkuaXNBcnJheShvYmopKSB7XG4gICAgZm9yIChsZXQgaSA9IDAgOyBpIDwgb2JqLmxlbmd0aCA7IGkrKykge1xuICAgICAgcmVzdWx0LnB1c2goLi4ubGlzdFRva2VucyhvYmpbaV0sIGAke3BhdGh9WyR7aX1dYCkpO1xuICAgIH1cbiAgfSBlbHNlIHtcbiAgICBmb3IgKGNvbnN0IGtleSBvZiBPYmplY3Qua2V5cyhvYmopKSB7XG4gICAgICByZXN1bHQucHVzaCguLi5saXN0VG9rZW5zKChvYmogYXMgYW55KVtrZXldLCBgJHtwYXRofS4ke2tleX1gKSk7XG4gICAgfVxuICB9XG4gIHJldHVybiByZXN1bHQ7XG59XG5cbmludGVyZmFjZSBSZWZlcmVuY2Uge1xuICBsb2dpY2FsSWQ6IHN0cmluZztcbiAgY29udGV4dDogc3RyaW5nO1xuICBzdWJyZWZlcmVuY2VzPzogUmVmZXJlbmNlW107XG59XG4iXX0= \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.d.ts b/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.d.ts deleted file mode 100644 index 10b43061559fc..0000000000000 --- a/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const _default: void; -export = _default; diff --git a/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.js b/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.js deleted file mode 100644 index 8db2ef5dc170f..0000000000000 --- a/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.js +++ /dev/null @@ -1,104 +0,0 @@ -"use strict"; -const nodeunit = require("nodeunit"); -const lib_1 = require("../lib"); -const SAMPLE_MANIFEST = { - schema: "cloud-assembly/1.0", - drops: { - "PipelineStack": { - type: "npm://@aws-cdk/aws-cloudformation.StackDrop", - environment: "aws://123456789012/eu-west-1", - properties: { - template: "stacks/PipelineStack.yml" - } - }, - "ServiceStack-beta": { - type: "npm://@aws-cdk/aws-cloudformation.StackDrop", - environment: "aws://123456789012/eu-west-1", - properties: { - template: "stacks/ServiceStack-beta.yml", - stackPolicy: "stacks/ServiceStack-beta.stack-policy.json", - parameters: { - image: "${DockerImage.exactImageId}", - websiteFilesBucket: "${StaticFiles.bucketName}", - websiteFilesKeyPrefix: "${StaticFiles.keyPrefix}", - } - } - }, - "ServiceStack-prod": { - type: "npm://@aws-cdk/aws-cloudformation.StackDrop", - environment: "aws://123456789012/eu-west-1", - properties: { - template: "stacks/ServiceStack-prod.yml", - stackPolicy: "stacks/ServiceStack-prod.stack-policy.json", - parameters: { - image: "${DockerImage.exactImageId}", - websiteFilesBucket: "${StaticFiles.bucketName}", - websiteFilesKeyPrefix: "${StaticFiles.keyPrefix}", - } - } - }, - "DockerImage": { - type: "npm://@aws-cdk/aws-ecr.DockerImageDrop", - environment: "aws://123456789012/eu-west-1", - properties: { - savedImage: "docker/docker-image.tar", - imageName: "${PipelineStack.ecrImageName}" - } - }, - "StaticFiles": { - type: "npm://@aws-cdk/assets.DirectoryDrop", - environment: "aws://123456789012/eu-west-1", - properties: { - directory: "assets/static-website", - bucketName: "${PipelineStack.stagingBucket}" - } - } - } -}; -module.exports = nodeunit.testCase({ - validateManifest: { - 'successfully loads the example manifest'(test) { - test.doesNotThrow(() => lib_1.validateManifest(SAMPLE_MANIFEST)); - test.done(); - }, - 'rejects a document where the schema is invalid'(test) { - const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); - badManifest.schema = 'foo/1.0-bar'; - test.throws(() => lib_1.validateManifest(badManifest), /instance\.schema is not one of enum values/); - test.done(); - }, - 'rejects a document without drops'(test) { - const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); - delete badManifest.drops; - test.throws(() => lib_1.validateManifest(badManifest), /instance requires property "drops"/); - test.done(); - }, - 'rejects a document with an illegal Logical ID'(test) { - const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); - badManifest.drops['Pipeline.Stack'] = badManifest.drops.PipelineStack; - test.throws(() => lib_1.validateManifest(badManifest), /Invalid logical ID: Pipeline\.Stack/); - test.done(); - }, - 'rejects a document with unresolved dependsOn'(test) { - const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); - badManifest.drops.PipelineStack.dependsOn = ['DoesNotExist']; - test.throws(() => lib_1.validateManifest(badManifest), /PipelineStack depends on undefined drop through dependsOn DoesNotExist/); - test.done(); - }, - 'rejects a document with direct circular dependency via dependsOn'(test) { - const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); - badManifest.drops.PipelineStack.dependsOn = ['PipelineStack']; - test.throws(() => lib_1.validateManifest(badManifest), /PipelineStack => dependsOn PipelineStack/); - test.done(); - }, - 'rejects a document with indirect circular dependency'(test) { - const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); - badManifest.drops.StaticFiles.dependsOn = ['ServiceStack-beta']; - test.throws(() => lib_1.validateManifest(badManifest), - // tslint:disable-next-line:max-line-length - /StaticFiles => dependsOn ServiceStack-beta => ServiceStack-beta\.properties\.parameters\.websiteFilesKeyPrefix "\${StaticFiles\.keyPrefix}"/); - test.done(); - } - } -}); -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVzdC52YWxpZGF0ZS1tYW5pZmVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInRlc3QudmFsaWRhdGUtbWFuaWZlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLHFDQUFzQztBQUN0QyxnQ0FBMEM7QUFzRDFDLE1BQU0sZUFBZSxHQUFHO0lBQ3RCLE1BQU0sRUFBRSxvQkFBb0I7SUFDNUIsS0FBSyxFQUFFO1FBQ0wsZUFBZSxFQUFFO1lBQ2YsSUFBSSxFQUFFLDZDQUE2QztZQUNuRCxXQUFXLEVBQUUsOEJBQThCO1lBQzNDLFVBQVUsRUFBRTtnQkFDVixRQUFRLEVBQUUsMEJBQTBCO2FBQ3JDO1NBQ0Y7UUFDRCxtQkFBbUIsRUFBRTtZQUNuQixJQUFJLEVBQUUsNkNBQTZDO1lBQ25ELFdBQVcsRUFBRSw4QkFBOEI7WUFDM0MsVUFBVSxFQUFFO2dCQUNWLFFBQVEsRUFBRSw4QkFBOEI7Z0JBQ3hDLFdBQVcsRUFBRSw0Q0FBNEM7Z0JBQ3pELFVBQVUsRUFBRTtvQkFDVixLQUFLLEVBQUUsNkJBQTZCO29CQUNwQyxrQkFBa0IsRUFBRSwyQkFBMkI7b0JBQy9DLHFCQUFxQixFQUFFLDBCQUEwQjtpQkFDbEQ7YUFDRjtTQUNGO1FBQ0QsbUJBQW1CLEVBQUU7WUFDbkIsSUFBSSxFQUFFLDZDQUE2QztZQUNuRCxXQUFXLEVBQUUsOEJBQThCO1lBQzNDLFVBQVUsRUFBRTtnQkFDVixRQUFRLEVBQUUsOEJBQThCO2dCQUN4QyxXQUFXLEVBQUUsNENBQTRDO2dCQUN6RCxVQUFVLEVBQUU7b0JBQ1YsS0FBSyxFQUFFLDZCQUE2QjtvQkFDcEMsa0JBQWtCLEVBQUUsMkJBQTJCO29CQUMvQyxxQkFBcUIsRUFBRSwwQkFBMEI7aUJBQ2xEO2FBQ0Y7U0FDRjtRQUNELGFBQWEsRUFBRTtZQUNiLElBQUksRUFBRSx3Q0FBd0M7WUFDOUMsV0FBVyxFQUFFLDhCQUE4QjtZQUMzQyxVQUFVLEVBQUU7Z0JBQ1YsVUFBVSxFQUFFLHlCQUF5QjtnQkFDckMs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QXNELENBQUMsSUFBbUI7WUFDeEUsTUFBTSxXQUFXLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLGVBQWUsQ0FBQyxDQUFDLENBQUM7WUFDaEUsV0FBVyxDQUFDLEtBQUssQ0FBQyxXQUFXLENBQUMsU0FBUyxHQUFHLENBQUMsbUJBQW1CLENBQUMsQ0FBQztZQUNoRSxJQUFJLENBQUMsTUFBTSxDQUFDLEdBQUcsRUFBRSxDQUFDLHNCQUFnQixDQUFDLFdBQVcsQ0FBQztZQUNuQywyQ0FBMkM7WUFDM0MsNklBQTZJLENBQUMsQ0FBQztZQUMzSixJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDZCxDQUFDO0tBQ0Y7Q0FDRixDQUFDLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgbm9kZXVuaXQgPSByZXF1aXJlKCdub2RldW5pdCcpO1xuaW1wb3J0IHsgdmFsaWRhdGVNYW5pZmVzdCB9IGZyb20gJy4uL2xpYic7XG5cbmV4cG9ydCA9IG5vZGV1bml0LnRlc3RDYXNlKHtcbiAgdmFsaWRhdGVNYW5pZmVzdDoge1xuICAgICdzdWNjZXNzZnVsbHkgbG9hZHMgdGhlIGV4YW1wbGUgbWFuaWZlc3QnKHRlc3Q6IG5vZGV1bml0LlRlc3QpIHtcbiAgICAgIHRlc3QuZG9lc05vdFRocm93KCgpID0+IHZhbGlkYXRlTWFuaWZlc3QoU0FNUExFX01BTklGRVNUKSk7XG4gICAgICB0ZXN0LmRvbmUoKTtcbiAgICB9LFxuICAgICdyZWplY3RzIGEgZG9jdW1lbnQgd2hlcmUgdGhlIHNjaGVtYSBpcyBpbnZhbGlkJyh0ZXN0OiBub2RldW5pdC5UZXN0KSB7XG4gICAgICBjb25zdCBiYWRNYW5pZmVzdCA9IEpTT04ucGFyc2UoSlNPTi5zdHJpbmdpZnkoU0FNUExFX01BTklGRVNUKSk7XG4gICAgICBiYWRNYW5pZmVzdC5zY2hlbWEgPSAnZm9vLzEuMC1iYXInO1xuICAgICAgdGVzdC50aHJvd3MoKCkgPT4gdmFsaWRhdGVNYW5pZmVzdChiYWRNYW5pZmVzdCksXG4gICAgICAgICAgICAgICAgICAvaW5zdGFuY2VcXC5zY2hlbWEgaXMgbm90IG9uZSBvZiBlbnVtIHZhbHVlcy8pO1xuICAgICAgdGVzdC5kb25lKCk7XG4gICAgfSxcbiAgICAncmVqZWN0cyBhIGRvY3VtZW50IHdpdGhvdXQgZHJvcHMnKHRlc3Q6IG5vZGV1bml0LlRlc3QpIHtcbiAgICAgIGNvbnN0IGJhZE1hbmlmZXN0ID0gSlNPTi5wYXJzZShKU09OLnN0cmluZ2lmeShTQU1QTEVfTUFOSUZFU1QpKTtcbiAgICAgIGRlbGV0ZSBiYWRNYW5pZmVzdC5kcm9wcztcbiAgICAgIHRlc3QudGhyb3dzKCgpID0+IHZhbGlkYXRlTWFuaWZlc3QoYmFkTWFuaWZlc3QpLFxuICAgICAgICAgICAgICAgICAgL2luc3RhbmNlIHJlcXVpcmVzIHByb3BlcnR5IFwiZHJvcHNcIi8pO1xuICAgICAgdGVzdC5kb25lKCk7XG4gICAgfSxcbiAgICAncmVqZWN0cyBhIGRvY3VtZW50IHdpdGggYW4gaWxsZWdhbCBMb2dpY2FsIElEJyh0ZXN0OiBub2RldW5pdC5UZXN0KSB7XG4gICAgICBjb25zdCBiYWRNYW5pZmVzdCA9IEpTT04ucGFyc2UoSlNPTi5zdHJpbmdpZnkoU0FNUExFX01BTklGRVNUKSk7XG4gICAgICBiYWRNYW5pZmVzdC5kcm9wc1snUGlwZWxpbmUuU3RhY2snXSA9IGJhZE1hbmlmZXN0LmRyb3BzLlBpcGVsaW5lU3RhY2s7XG4gICAgICB0ZXN0LnRocm93cygoKSA9PiB2YWxpZGF0ZU1hbmlmZXN0KGJhZE1hbmlmZXN0KSxcbiAgICAgICAgICAgICAgICAgIC9JbnZhbGlkIGxvZ2ljYWwgSUQ6IFBpcGVsaW5lXFwuU3RhY2svKTtcbiAgICAgIHRlc3QuZG9uZSgpO1xuICAgIH0sXG4gICAgJ3JlamVjdHMgYSBkb2N1bWVudCB3aXRoIHVucmVzb2x2ZWQgZGVwZW5kc09uJyh0ZXN0OiBub2RldW5pdC5UZXN0KSB7XG4gICAgICBjb25zdCBiYWRNYW5pZmVzdCA9IEpTT04ucGFyc2UoSlNPTi5zdHJpbmdpZnkoU0FNUExFX01BTklGRVNUKSk7XG4gICAgICBiYWRNYW5pZmVzdC5kcm9wcy5QaXBlbGluZVN0YWNrLmRlcGVuZHNPbiA9IFsnRG9lc05vdEV4aXN0J107XG4gICAgICB0ZXN0LnRocm93cygoKSA9PiB2YWxpZGF0ZU1hbmlmZXN0KGJhZE1hbmlmZXN0KSxcbiAgICAgICAgICAgICAgICAgIC9QaXBlbGluZVN0YWNrIGRlcGVuZHMgb24gdW5kZWZpbmVkIGRyb3AgdGhyb3VnaCBkZXBlbmRzT24gRG9lc05vdEV4aXN0Lyk7XG4gICAgICB0ZXN0LmRvbmUoKTtcbiAgICB9LFxuICAgICdyZWplY3RzIGEgZG9jdW1lbnQgd2l0aCBkaXJlY3QgY2lyY3VsYXIgZGVwZW5kZW5jeSB2aWEgZGVwZW5kc09uJyh0ZXN0OiBub2RldW5pdC5UZXN0KSB7XG4gICAgICBjb25zdCBiYWRNYW5pZmVzdCA9IEpTT04ucGFyc2UoSlNPTi5zdHJpbmdpZnkoU0FNUExFX01BTklGRVNUKSk7XG4gICAgICBiYWRNYW5pZmVzdC5kcm9wcy5QaXBlbGluZVN0YWNrLmRlcGVuZHNPbiA9IFsnUGlwZWxpbmVTdGFjayddO1xuICAgICAgdGVzdC50aHJvd3MoKCkgPT4gdmFsaWRhdGVNYW5pZmVzdChiYWRNYW5pZmVzdCksXG4gICAgICAgICAgICAgICAgICAvUGlwZWxpbmVTdGFjayA9PiBkZXBlbmRzT24gUGlwZWxpbmVTdGFjay8pO1xuICAgICAgdGVzdC5kb25lKCk7XG4gICAgfSxcbiAgICAncmVqZWN0cyBhIGRvY3VtZW50IHdpdGggaW5kaXJlY3QgY2lyY3VsYXIgZGVwZW5kZW5jeScodGVzdDogbm9kZXVuaXQuVGVzdCkge1xuICAgICAgY29uc3QgYmFkTWFuaWZlc3QgPSBKU09OLnBhcnNlKEpTT04uc3RyaW5naWZ5KFNBTVBMRV9NQU5JRkVTVCkpO1xuICAgICAgYmFkTWFuaWZlc3QuZHJvcHMuU3RhdGljRmlsZXMuZGVwZW5kc09uID0gWydTZXJ2aWNlU3RhY2stYmV0YSddO1xuICAgICAgdGVzdC50aHJvd3MoKCkgPT4gdmFsaWRhdGVNYW5pZmVzdChiYWRNYW5pZmVzdCksXG4gICAgICAgICAgICAgICAgICAvLyB0c2xpbnQ6ZGlzYWJsZS1uZXh0LWxpbmU6bWF4LWxpbmUtbGVuZ3RoXG4gICAgICAgICAgICAgICAgICAvU3RhdGljRmlsZXMgPT4gZGVwZW5kc09uIFNlcnZpY2VTdGFjay1iZXRhID0+IFNlcnZpY2VTdGFjay1iZXRhXFwucHJvcGVydGllc1xcLnBhcmFtZXRlcnNcXC53ZWJzaXRlRmlsZXNLZXlQcmVmaXggXCJcXCR7U3RhdGljRmlsZXNcXC5rZXlQcmVmaXh9XCIvKTtcbiAgICAgIHRlc3QuZG9uZSgpO1xuICAgIH1cbiAgfVxufSk7XG5cbmNvbnN0IFNBTVBMRV9NQU5JRkVTVCA9IHtcbiAgc2NoZW1hOiBcImNsb3VkLWFzc2VtYmx5LzEuMFwiLFxuICBkcm9wczoge1xuICAgIFwiUGlwZWxpbmVTdGFja1wiOiB7XG4gICAgICB0eXBlOiBcIm5wbTovL0Bhd3MtY2RrL2F3cy1jbG91ZGZvcm1hdGlvbi5TdGFja0Ryb3BcIixcbiAgICAgIGVudmlyb25tZW50OiBcImF3czovLzEyMzQ1Njc4OTAxMi9ldS13ZXN0LTFcIixcbiAgICAgIHByb3BlcnRpZXM6IHtcbiAgICAgICAgdGVtcGxhdGU6IFwic3RhY2tzL1BpcGVsaW5lU3RhY2sueW1sXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiU2VydmljZVN0YWNrLWJldGFcIjoge1xuICAgICAgdHlwZTogXCJucG06Ly9AYXdzLWNkay9hd3MtY2xvdWRmb3JtYXRpb24uU3RhY2tEcm9wXCIsXG4gICAgICBlbnZpcm9ubWVudDogXCJhd3M6Ly8xMjM0NTY3ODkwMTIvZXUtd2VzdC0xXCIsXG4gICAgICBwcm9wZXJ0aWVzOiB7XG4gICAgICAgIHRlbXBsYXRlOiBcInN0YWNrcy9TZXJ2aWNlU3RhY2stYmV0YS55bWxcIixcbiAgICAgICAgc3RhY2tQb2xpY3k6IFwic3RhY2tzL1NlcnZpY2VTdGFjay1iZXRhLnN0YWNrLXBvbGljeS5qc29uXCIsXG4gICAgICAgIHBhcmFtZXRlcnM6IHtcbiAgICAgICAgICBpbWFnZTogXCIke0RvY2tlckltYWdlLmV4YWN0SW1hZ2VJZH1cIixcbiAgICAgICAgICB3ZWJzaXRlRmlsZXNCdWNrZXQ6IFwiJHtTdGF0aWNGaWxlcy5idWNrZXROYW1lfVwiLFxuICAgICAgICAgIHdlYnNpdGVGaWxlc0tleVByZWZpeDogXCIke1N0YXRpY0ZpbGVzLmtleVByZWZpeH1cIixcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0sXG4gICAgXCJTZXJ2aWNlU3RhY2stcHJvZFwiOiB7XG4gICAgICB0eXBlOiBcIm5wbTovL0Bhd3MtY2RrL2F3cy1jbG91ZGZvcm1hdGlvbi5TdGFja0Ryb3BcIixcbiAgICAgIGVudmlyb25tZW50OiBcImF3czovLzEyMzQ1Njc4OTAxMi9ldS13ZXN0LTFcIixcbiAgICAgIHByb3BlcnRpZXM6IHtcbiAgICAgICAgdGVtcGxhdGU6IFwic3RhY2tzL1NlcnZpY2VTdGFjay1wcm9kLnltbFwiLFxuICAgICAgICBzdGFja1BvbGljeTogXCJzdGFja3MvU2VydmljZVN0YWNrLXByb2Quc3RhY2stcG9saWN5Lmpzb25cIixcbiAgICAgICAgcGFyYW1ldGVyczoge1xuICAgICAgICAgIGltYWdlOiBcIiR7RG9ja2VySW1hZ2UuZXhhY3RJbWFnZUlkfVwiLFxuICAgICAgICAgIHdlYnNpdGVGaWxlc0J1Y2tldDogXCIke1N0YXRpY0ZpbGVzLmJ1Y2tldE5hbWV9XCIsXG4gICAgICAgICAgd2Vic2l0ZUZpbGVzS2V5UHJlZml4OiBcIiR7U3RhdGljRmlsZXMua2V5UHJlZml4fVwiLFxuICAgICAgICB9XG4gICAgICB9XG4gICAgfSxcbiAgICBcIkRvY2tlckltYWdlXCI6IHtcbiAgICAgIHR5cGU6IFwibnBtOi8vQGF3cy1jZGsvYXdzLWVjci5Eb2NrZXJJbWFnZURyb3BcIixcbiAgICAgIGVudmlyb25tZW50OiBcImF3czovLzEyMzQ1Njc4OTAxMi9ldS13ZXN0LTFcIixcbiAgICAgIHByb3BlcnRpZXM6IHtcbiAgICAgICAgc2F2ZWRJbWFnZTogXCJkb2NrZXIvZG9ja2VyLWltYWdlLnRhclwiLFxuICAgICAgICBpbWFnZU5hbWU6IFwiJHtQaXBlbGluZVN0YWNrLmVjckltYWdlTmFtZX1cIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJTdGF0aWNGaWxlc1wiOiB7XG4gICAgICB0eXBlOiBcIm5wbTovL0Bhd3MtY2RrL2Fzc2V0cy5EaXJlY3RvcnlEcm9wXCIsXG4gICAgICBlbnZpcm9ubWVudDogXCJhd3M6Ly8xMjM0NTY3ODkwMTIvZXUtd2VzdC0xXCIsXG4gICAgICBwcm9wZXJ0aWVzOiB7XG4gICAgICAgIGRpcmVjdG9yeTogXCJhc3NldHMvc3RhdGljLXdlYnNpdGVcIixcbiAgICAgICAgYnVja2V0TmFtZTogXCIke1BpcGVsaW5lU3RhY2suc3RhZ2luZ0J1Y2tldH1cIlxuICAgICAgfVxuICAgIH1cbiAgfVxufTtcbiJdfQ== \ No newline at end of file From c033016da5f1136cfb68724685b09a18ae5aac43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Thu, 15 Nov 2018 14:12:15 +0100 Subject: [PATCH 07/15] Generate cloud assemblies out of CDK Apps --- packages/@aws-cdk/cdk/lib/app.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/app.ts b/packages/@aws-cdk/cdk/lib/app.ts index f3e7c6a90ff8a..d9047528323b4 100644 --- a/packages/@aws-cdk/cdk/lib/app.ts +++ b/packages/@aws-cdk/cdk/lib/app.ts @@ -1,3 +1,4 @@ +import cloudAssembly = require('@aws-cdk/cloud-assembly'); import cxapi = require('@aws-cdk/cx-api'); import fs = require('fs'); import path = require('path'); @@ -42,14 +43,20 @@ export class App extends Root { return; } - const result: cxapi.SynthesizeResponse = { - version: cxapi.PROTO_RESPONSE_VERSION, - stacks: this.synthesizeStacks(Object.keys(this.stacks)), - runtime: this.collectRuntimeInformation() + const manifest: cloudAssembly.Manifest = { + schema: 'cloud-assembly/1.0', + drops: { CDKMetadata: this.collectRuntimeInformation() }, }; - const outfile = path.join(outdir, cxapi.OUTFILE_NAME); - fs.writeFileSync(outfile, JSON.stringify(result, undefined, 2)); + for (const stack of this.synthesizeStacks(Object.keys(this.stacks))) { + manifest.drops[`stacks/${stack.name}`] = { + type: 'npm://@aws-cdk/cdk/CloudFormationStackDroplet', + environment: `aws://${stack.environment.account}/${stack.environment.region}`, + }; + } + + const outfile = path.join(outdir, cloudAssembly.MANIFEST_FILE_NAME); + fs.writeFileSync(outfile, JSON.stringify(manifest, undefined, 2)); } /** @@ -123,7 +130,7 @@ export class App extends Root { } } - private collectRuntimeInformation(): cxapi.AppRuntime { + private collectRuntimeInformation(): cloudAssembly.Drop { const libraries: { [name: string]: string } = {}; for (const fileName of Object.keys(require.cache)) { @@ -133,7 +140,11 @@ export class App extends Root { } } - return { libraries }; + return { + type: 'npm://@aws-cdk/cdk/CDKMetadataDroplet', + environment: '*', + properties: { libraries } + }; } private getStack(stackname: string) { From d923d68af0c2bca837b715698bffebbd9d985e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Fri, 16 Nov 2018 12:59:06 +0100 Subject: [PATCH 08/15] Add stackName to the CloudFormation.StackDrop --- specifications/cloud_assembly.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specifications/cloud_assembly.md b/specifications/cloud_assembly.md index d0fca7db6382d..467d0c8e2dc24 100644 --- a/specifications/cloud_assembly.md +++ b/specifications/cloud_assembly.md @@ -245,6 +245,7 @@ A [*CloudFormation* stack][CFN Stack]. ##### Properties Property |Type |Required|Description -------------|--------------------|:------:|----------- +`stackName` |`string` |Required|The name of the *CloudFormation* stack once deployed. `template` |`string` |Required|The assembly-relative path to the *CloudFormation* template document. `parameters` |`Map`| |Parameters to be passed to the [stack][CFN Stack] upon deployment. `stackPolicy`|`string` | |The assembly-relative path to the [Stack Policy][CFN Stack Policy]. From b619cbddbe55227ae6ab41d2f47f71e37e38cac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Mon, 19 Nov 2018 10:33:06 +0100 Subject: [PATCH 09/15] chore: Add rule to guarantee node runtime compatibility This adds a rule to make sure the codebase is all configured to support node runtimes complying with `>= 8.11.0`, by ensuring that the configured `@types/node` version starts with `^8.11.`. In order to allow applying this rule to the top-level `package.json`, a feature was added to `pkglint` that allows users to confgure `include` and `exclude` filters as part of their `package.json`'s `pkglint` setup. Exclusions always have precedence over inclusions, and specifying `disable: true` has the same effect as specifying `exclude: '*'`. Both the `include` and `exclude` values can contain patterns that use `*` as a wild-card that maches any number of characters (including no character at all), and can be either a single pattern, or an array of patterns. `include` defaults to `*`. --- package.json | 4 +- packages/@aws-cdk/app-delivery/package.json | 2 +- packages/@aws-cdk/assets/package.json | 2 +- packages/@aws-cdk/aws-amazonmq/package.json | 2 +- packages/@aws-cdk/aws-apigateway/package.json | 2 +- .../aws-applicationautoscaling/package.json | 4 +- packages/@aws-cdk/aws-appstream/package.json | 2 +- packages/@aws-cdk/aws-appsync/package.json | 2 +- packages/@aws-cdk/aws-athena/package.json | 2 +- .../@aws-cdk/aws-autoscaling/package.json | 2 +- .../aws-autoscalingplans/package.json | 2 +- packages/@aws-cdk/aws-batch/package.json | 2 +- packages/@aws-cdk/aws-budgets/package.json | 2 +- .../aws-certificatemanager/package.json | 2 +- packages/@aws-cdk/aws-cloud9/package.json | 2 +- .../@aws-cdk/aws-cloudformation/package.json | 4 +- packages/@aws-cdk/aws-cloudtrail/package.json | 2 +- packages/@aws-cdk/aws-cloudwatch/package.json | 2 +- packages/@aws-cdk/aws-codebuild/package.json | 2 +- packages/@aws-cdk/aws-codecommit/package.json | 2 +- packages/@aws-cdk/aws-codedeploy/package.json | 2 +- .../aws-codepipeline-api/package.json | 2 +- .../@aws-cdk/aws-codepipeline/package.json | 2 +- packages/@aws-cdk/aws-cognito/package.json | 2 +- packages/@aws-cdk/aws-config/package.json | 2 +- .../@aws-cdk/aws-datapipeline/package.json | 2 +- packages/@aws-cdk/aws-dax/package.json | 2 +- .../aws-directoryservice/package.json | 2 +- packages/@aws-cdk/aws-dms/package.json | 2 +- packages/@aws-cdk/aws-dynamodb/package.json | 2 +- packages/@aws-cdk/aws-ec2/package.json | 2 +- packages/@aws-cdk/aws-ecr/package.json | 2 +- packages/@aws-cdk/aws-efs/package.json | 2 +- packages/@aws-cdk/aws-eks/package.json | 2 +- .../@aws-cdk/aws-elasticache/package.json | 2 +- .../aws-elasticbeanstalk/package.json | 2 +- .../aws-elasticloadbalancing/package.json | 2 +- .../@aws-cdk/aws-elasticsearch/package.json | 2 +- packages/@aws-cdk/aws-emr/package.json | 2 +- packages/@aws-cdk/aws-events/package.json | 2 +- packages/@aws-cdk/aws-gamelift/package.json | 2 +- packages/@aws-cdk/aws-glue/package.json | 2 +- packages/@aws-cdk/aws-guardduty/package.json | 2 +- packages/@aws-cdk/aws-iam/package.json | 2 +- packages/@aws-cdk/aws-inspector/package.json | 2 +- packages/@aws-cdk/aws-iot/package.json | 2 +- packages/@aws-cdk/aws-iot1click/package.json | 2 +- packages/@aws-cdk/aws-kinesis/package.json | 2 +- .../aws-kinesisanalytics/package.json | 2 +- .../@aws-cdk/aws-kinesisfirehose/package.json | 2 +- packages/@aws-cdk/aws-kms/package.json | 2 +- .../aws-lambda-event-sources/package.json | 2 +- packages/@aws-cdk/aws-lambda/package.json | 2 +- packages/@aws-cdk/aws-logs/package.json | 2 +- packages/@aws-cdk/aws-neptune/package.json | 2 +- packages/@aws-cdk/aws-opsworks/package.json | 2 +- .../@aws-cdk/aws-quickstarts/package.json | 2 +- packages/@aws-cdk/aws-rds/package.json | 2 +- packages/@aws-cdk/aws-redshift/package.json | 2 +- packages/@aws-cdk/aws-route53/package.json | 2 +- .../@aws-cdk/aws-s3-deployment/package.json | 2 +- .../aws-s3-notifications/package.json | 2 +- packages/@aws-cdk/aws-s3/package.json | 2 +- packages/@aws-cdk/aws-sagemaker/package.json | 2 +- packages/@aws-cdk/aws-sdb/package.json | 2 +- packages/@aws-cdk/aws-serverless/package.json | 2 +- .../@aws-cdk/aws-servicecatalog/package.json | 2 +- .../aws-servicediscovery/package.json | 2 +- packages/@aws-cdk/aws-ses/package.json | 2 +- packages/@aws-cdk/aws-sns/package.json | 2 +- packages/@aws-cdk/aws-sqs/package.json | 2 +- packages/@aws-cdk/aws-ssm/package.json | 2 +- .../@aws-cdk/aws-stepfunctions/package.json | 2 +- packages/@aws-cdk/aws-waf/package.json | 2 +- .../@aws-cdk/aws-wafregional/package.json | 2 +- packages/@aws-cdk/aws-workspaces/package.json | 2 +- packages/@aws-cdk/cdk/package.json | 4 +- packages/@aws-cdk/runtime-values/package.json | 2 +- packages/aws-cdk/package.json | 4 +- packages/simple-resource-bundler/package.json | 4 +- tools/cfn2ts/package.json | 4 +- tools/merkle-build/package.json | 4 +- tools/pkglint/bin/pkglint.ts | 19 +- tools/pkglint/lib/packagejson.ts | 78 ++++++- tools/pkglint/lib/rules.ts | 193 ++++++++++++++---- tools/pkglint/lib/util.ts | 12 +- tools/pkglint/package.json | 4 +- tools/pkgtools/package.json | 4 +- tools/y-npm/package.json | 4 +- 89 files changed, 336 insertions(+), 158 deletions(-) diff --git a/package.json b/package.json index e4e23b8284074..9bcc5529d5c0e 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "aws-cdk", "pkglint": { - "ignore": true + "include": "dependencies/node-version" }, "scripts": { "pkglint": "tools/pkglint/bin/pkglint -f ." }, "devDependencies": { - "@types/node": "^10.9.4", + "@types/node": "^8.11.38", "@types/nodeunit": "^0.0.30", "conventional-changelog-cli": "^2.0.5", "lerna": "^3.3.0", diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index 366d64bee41e1..6f7a33020876a 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -65,4 +65,4 @@ "@aws-cdk/aws-codepipeline-api": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/assets/package.json b/packages/@aws-cdk/assets/package.json index b58d34201984c..e57306f7f897b 100644 --- a/packages/@aws-cdk/assets/package.json +++ b/packages/@aws-cdk/assets/package.json @@ -67,4 +67,4 @@ "@aws-cdk/aws-s3": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-amazonmq/package.json b/packages/@aws-cdk/aws-amazonmq/package.json index 634ddc32d3096..21c122e6c6571 100644 --- a/packages/@aws-cdk/aws-amazonmq/package.json +++ b/packages/@aws-cdk/aws-amazonmq/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index aa9c8f4d99285..55e3336d8726d 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -69,4 +69,4 @@ "@aws-cdk/aws-lambda": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index 8d242db978054..d8b41832a38a0 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -55,7 +55,7 @@ "@aws-cdk/assert": "^0.17.0", "cdk-build-tools": "^0.17.0", "cfn2ts": "^0.17.0", - "fast-check": "^1.6.1", + "fast-check": "^1.7.0", "pkglint": "^0.17.0" }, "dependencies": { @@ -69,4 +69,4 @@ "@aws-cdk/aws-iam": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-appstream/package.json b/packages/@aws-cdk/aws-appstream/package.json index 50980f24c1490..9aed410a4e97e 100644 --- a/packages/@aws-cdk/aws-appstream/package.json +++ b/packages/@aws-cdk/aws-appstream/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-appsync/package.json b/packages/@aws-cdk/aws-appsync/package.json index 1cfeba7602f68..34bc41f42f24f 100644 --- a/packages/@aws-cdk/aws-appsync/package.json +++ b/packages/@aws-cdk/aws-appsync/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-athena/package.json b/packages/@aws-cdk/aws-athena/package.json index cbb746530a406..cce9ceacaee66 100644 --- a/packages/@aws-cdk/aws-athena/package.json +++ b/packages/@aws-cdk/aws-athena/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-autoscaling/package.json b/packages/@aws-cdk/aws-autoscaling/package.json index 149dc43f5c83e..f3e2bd3ae718e 100644 --- a/packages/@aws-cdk/aws-autoscaling/package.json +++ b/packages/@aws-cdk/aws-autoscaling/package.json @@ -75,4 +75,4 @@ "@aws-cdk/aws-sns": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-autoscalingplans/package.json b/packages/@aws-cdk/aws-autoscalingplans/package.json index 0b90e34a3f1f1..aabf657a20a07 100644 --- a/packages/@aws-cdk/aws-autoscalingplans/package.json +++ b/packages/@aws-cdk/aws-autoscalingplans/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-batch/package.json b/packages/@aws-cdk/aws-batch/package.json index 5214c2b0faa17..df12b3e6d7486 100644 --- a/packages/@aws-cdk/aws-batch/package.json +++ b/packages/@aws-cdk/aws-batch/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-budgets/package.json b/packages/@aws-cdk/aws-budgets/package.json index 08f18a8979ea7..46f406b13b64f 100644 --- a/packages/@aws-cdk/aws-budgets/package.json +++ b/packages/@aws-cdk/aws-budgets/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-certificatemanager/package.json b/packages/@aws-cdk/aws-certificatemanager/package.json index fa42155970dc3..06d00e34447fd 100644 --- a/packages/@aws-cdk/aws-certificatemanager/package.json +++ b/packages/@aws-cdk/aws-certificatemanager/package.json @@ -65,4 +65,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cloud9/package.json b/packages/@aws-cdk/aws-cloud9/package.json index 5cb9bd98a4175..fe88406d82d81 100644 --- a/packages/@aws-cdk/aws-cloud9/package.json +++ b/packages/@aws-cdk/aws-cloud9/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cloudformation/package.json b/packages/@aws-cdk/aws-cloudformation/package.json index e151f18242ec6..ae6b2d981b192 100644 --- a/packages/@aws-cdk/aws-cloudformation/package.json +++ b/packages/@aws-cdk/aws-cloudformation/package.json @@ -58,7 +58,7 @@ "devDependencies": { "@aws-cdk/assert": "^0.17.0", "@aws-cdk/aws-events": "^0.17.0", - "@types/lodash": "^4.14.116", + "@types/lodash": "^4.14.118", "cdk-build-tools": "^0.17.0", "cdk-integ-tools": "^0.17.0", "cfn2ts": "^0.17.0", @@ -80,4 +80,4 @@ "@aws-cdk/aws-sns": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index abdad976f1784..26b1e93c65bfb 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -70,4 +70,4 @@ "@aws-cdk/aws-kms": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cloudwatch/package.json b/packages/@aws-cdk/aws-cloudwatch/package.json index a876d44388bd1..47a594297a127 100644 --- a/packages/@aws-cdk/aws-cloudwatch/package.json +++ b/packages/@aws-cdk/aws-cloudwatch/package.json @@ -67,4 +67,4 @@ "@aws-cdk/aws-iam": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index 9869363b34ba1..49c7f1156f9d7 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -87,4 +87,4 @@ "@aws-cdk/aws-s3": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 10b83a9715130..4d54201b58279 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -76,4 +76,4 @@ "@aws-cdk/aws-events": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codedeploy/package.json b/packages/@aws-cdk/aws-codedeploy/package.json index 9ad9e4fd713f3..52a0823cfaf62 100644 --- a/packages/@aws-cdk/aws-codedeploy/package.json +++ b/packages/@aws-cdk/aws-codedeploy/package.json @@ -79,4 +79,4 @@ "@aws-cdk/aws-iam": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline-api/package.json b/packages/@aws-cdk/aws-codepipeline-api/package.json index 1d5e992e02ce6..aec8446a5f23b 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/package.json +++ b/packages/@aws-cdk/aws-codepipeline-api/package.json @@ -68,4 +68,4 @@ "@aws-cdk/aws-iam": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 8e89e80564184..be447cba43ae1 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -85,4 +85,4 @@ "@aws-cdk/aws-s3": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index 2fb76afee9e97..b0b73b51102ff 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-config/package.json b/packages/@aws-cdk/aws-config/package.json index d6ab28f176f25..d468ec56c0a1a 100644 --- a/packages/@aws-cdk/aws-config/package.json +++ b/packages/@aws-cdk/aws-config/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-datapipeline/package.json b/packages/@aws-cdk/aws-datapipeline/package.json index 011317ba04cc6..6b475fb098bd9 100644 --- a/packages/@aws-cdk/aws-datapipeline/package.json +++ b/packages/@aws-cdk/aws-datapipeline/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-dax/package.json b/packages/@aws-cdk/aws-dax/package.json index f760a549ec28a..19d2145ba23ec 100644 --- a/packages/@aws-cdk/aws-dax/package.json +++ b/packages/@aws-cdk/aws-dax/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-directoryservice/package.json b/packages/@aws-cdk/aws-directoryservice/package.json index cd799b3b344a8..1386d69d27f1f 100644 --- a/packages/@aws-cdk/aws-directoryservice/package.json +++ b/packages/@aws-cdk/aws-directoryservice/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-dms/package.json b/packages/@aws-cdk/aws-dms/package.json index 17c65c07a86e5..8e9f84d7838e5 100644 --- a/packages/@aws-cdk/aws-dms/package.json +++ b/packages/@aws-cdk/aws-dms/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 95dde20b17b3c..8477795b5c40a 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -69,4 +69,4 @@ "@aws-cdk/aws-iam": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index d6e0ed527cd3c..8a37e82518d95 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -66,4 +66,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index 864d29c0a4b7b..962bf62398381 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -67,4 +67,4 @@ "@aws-cdk/aws-iam": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-efs/package.json b/packages/@aws-cdk/aws-efs/package.json index 23044406ec285..70e62b0616599 100644 --- a/packages/@aws-cdk/aws-efs/package.json +++ b/packages/@aws-cdk/aws-efs/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 799601924c1aa..db58537a41274 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-elasticache/package.json b/packages/@aws-cdk/aws-elasticache/package.json index d90ec285be68b..73940497e9237 100644 --- a/packages/@aws-cdk/aws-elasticache/package.json +++ b/packages/@aws-cdk/aws-elasticache/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-elasticbeanstalk/package.json b/packages/@aws-cdk/aws-elasticbeanstalk/package.json index 5efb9043954be..b18c52b60b255 100644 --- a/packages/@aws-cdk/aws-elasticbeanstalk/package.json +++ b/packages/@aws-cdk/aws-elasticbeanstalk/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/package.json b/packages/@aws-cdk/aws-elasticloadbalancing/package.json index 61d88121fb257..957c76c621fd4 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/package.json @@ -69,4 +69,4 @@ "@aws-cdk/aws-ec2": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-elasticsearch/package.json b/packages/@aws-cdk/aws-elasticsearch/package.json index 03ad553f26b6a..f15f4ec1aab93 100644 --- a/packages/@aws-cdk/aws-elasticsearch/package.json +++ b/packages/@aws-cdk/aws-elasticsearch/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-emr/package.json b/packages/@aws-cdk/aws-emr/package.json index 6d2f41701dd3e..708dc52f0048c 100644 --- a/packages/@aws-cdk/aws-emr/package.json +++ b/packages/@aws-cdk/aws-emr/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-events/package.json b/packages/@aws-cdk/aws-events/package.json index c160930c8a658..82fb380c942f2 100644 --- a/packages/@aws-cdk/aws-events/package.json +++ b/packages/@aws-cdk/aws-events/package.json @@ -66,4 +66,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-gamelift/package.json b/packages/@aws-cdk/aws-gamelift/package.json index 238031fde39b3..087c7c0272d9e 100644 --- a/packages/@aws-cdk/aws-gamelift/package.json +++ b/packages/@aws-cdk/aws-gamelift/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-glue/package.json b/packages/@aws-cdk/aws-glue/package.json index 7a31fdfa49778..8c5d7c611c2f7 100644 --- a/packages/@aws-cdk/aws-glue/package.json +++ b/packages/@aws-cdk/aws-glue/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-guardduty/package.json b/packages/@aws-cdk/aws-guardduty/package.json index aaf41bfb765dc..f0a6ef3e4807f 100644 --- a/packages/@aws-cdk/aws-guardduty/package.json +++ b/packages/@aws-cdk/aws-guardduty/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-iam/package.json b/packages/@aws-cdk/aws-iam/package.json index d7e14bbbbad6a..e68e5f765edc3 100644 --- a/packages/@aws-cdk/aws-iam/package.json +++ b/packages/@aws-cdk/aws-iam/package.json @@ -67,4 +67,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-inspector/package.json b/packages/@aws-cdk/aws-inspector/package.json index 226c8e60f2041..f5338b0f94c11 100644 --- a/packages/@aws-cdk/aws-inspector/package.json +++ b/packages/@aws-cdk/aws-inspector/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-iot/package.json b/packages/@aws-cdk/aws-iot/package.json index deb1b52337405..f9b49669df18c 100644 --- a/packages/@aws-cdk/aws-iot/package.json +++ b/packages/@aws-cdk/aws-iot/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-iot1click/package.json b/packages/@aws-cdk/aws-iot1click/package.json index 92ae74a4fbad5..49fa1d4f35b4f 100644 --- a/packages/@aws-cdk/aws-iot1click/package.json +++ b/packages/@aws-cdk/aws-iot1click/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-kinesis/package.json b/packages/@aws-cdk/aws-kinesis/package.json index e07cc9f46390b..1d8134ade703b 100644 --- a/packages/@aws-cdk/aws-kinesis/package.json +++ b/packages/@aws-cdk/aws-kinesis/package.json @@ -69,4 +69,4 @@ "@aws-cdk/aws-logs": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-kinesisanalytics/package.json b/packages/@aws-cdk/aws-kinesisanalytics/package.json index b9bedb7596cda..45c6296114ed5 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics/package.json +++ b/packages/@aws-cdk/aws-kinesisanalytics/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-kinesisfirehose/package.json b/packages/@aws-cdk/aws-kinesisfirehose/package.json index 16a23dd9d35c3..e83b6f07e2d5f 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/package.json +++ b/packages/@aws-cdk/aws-kinesisfirehose/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-kms/package.json b/packages/@aws-cdk/aws-kms/package.json index 6b0c1765a4639..55026acf9c43e 100644 --- a/packages/@aws-cdk/aws-kms/package.json +++ b/packages/@aws-cdk/aws-kms/package.json @@ -67,4 +67,4 @@ "@aws-cdk/aws-iam": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-lambda-event-sources/package.json b/packages/@aws-cdk/aws-lambda-event-sources/package.json index 9b01c5439ea48..41e50a575d7a5 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/package.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/package.json @@ -69,4 +69,4 @@ "@aws-cdk/aws-sns": "^0.17.0", "@aws-cdk/aws-sqs": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 62800cf2dcdb8..c15284237a69a 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -91,4 +91,4 @@ "@aws-cdk/aws-stepfunctions": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-logs/package.json b/packages/@aws-cdk/aws-logs/package.json index 3b45fb0a37410..2a35e618e59b0 100644 --- a/packages/@aws-cdk/aws-logs/package.json +++ b/packages/@aws-cdk/aws-logs/package.json @@ -69,4 +69,4 @@ "@aws-cdk/aws-iam": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-neptune/package.json b/packages/@aws-cdk/aws-neptune/package.json index d20780af777f0..e9871c04ec426 100644 --- a/packages/@aws-cdk/aws-neptune/package.json +++ b/packages/@aws-cdk/aws-neptune/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-opsworks/package.json b/packages/@aws-cdk/aws-opsworks/package.json index 1000b02caacc9..3b4a830f9076e 100644 --- a/packages/@aws-cdk/aws-opsworks/package.json +++ b/packages/@aws-cdk/aws-opsworks/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-quickstarts/package.json b/packages/@aws-cdk/aws-quickstarts/package.json index cb09528956afa..3745f4b1baf18 100644 --- a/packages/@aws-cdk/aws-quickstarts/package.json +++ b/packages/@aws-cdk/aws-quickstarts/package.json @@ -63,4 +63,4 @@ "@aws-cdk/aws-ec2": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/package.json b/packages/@aws-cdk/aws-rds/package.json index 500445a267f3d..1592c1cddeb88 100644 --- a/packages/@aws-cdk/aws-rds/package.json +++ b/packages/@aws-cdk/aws-rds/package.json @@ -69,4 +69,4 @@ "@aws-cdk/aws-ec2": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-redshift/package.json b/packages/@aws-cdk/aws-redshift/package.json index d30f3dc5bcaa7..8a51eda0fca66 100644 --- a/packages/@aws-cdk/aws-redshift/package.json +++ b/packages/@aws-cdk/aws-redshift/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 6d81f59c74c25..a5451674af97d 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -69,4 +69,4 @@ "@aws-cdk/aws-ec2": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-s3-deployment/package.json b/packages/@aws-cdk/aws-s3-deployment/package.json index ce993f3f62959..58ec7bfa87898 100644 --- a/packages/@aws-cdk/aws-s3-deployment/package.json +++ b/packages/@aws-cdk/aws-s3-deployment/package.json @@ -85,4 +85,4 @@ "@aws-cdk/aws-s3": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-s3-notifications/package.json b/packages/@aws-cdk/aws-s3-notifications/package.json index 6c4b1c89588e8..32963ad82e620 100644 --- a/packages/@aws-cdk/aws-s3-notifications/package.json +++ b/packages/@aws-cdk/aws-s3-notifications/package.json @@ -59,4 +59,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-s3/package.json b/packages/@aws-cdk/aws-s3/package.json index d7227f7849951..5ecf66e5299ca 100644 --- a/packages/@aws-cdk/aws-s3/package.json +++ b/packages/@aws-cdk/aws-s3/package.json @@ -73,4 +73,4 @@ "@aws-cdk/aws-s3-notifications": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-sagemaker/package.json b/packages/@aws-cdk/aws-sagemaker/package.json index ac34336568e1d..b47702f222795 100644 --- a/packages/@aws-cdk/aws-sagemaker/package.json +++ b/packages/@aws-cdk/aws-sagemaker/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-sdb/package.json b/packages/@aws-cdk/aws-sdb/package.json index f49f9717ac544..010bc026524ce 100644 --- a/packages/@aws-cdk/aws-sdb/package.json +++ b/packages/@aws-cdk/aws-sdb/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-serverless/package.json b/packages/@aws-cdk/aws-serverless/package.json index f47af6bad40fb..f50c1df8b2278 100644 --- a/packages/@aws-cdk/aws-serverless/package.json +++ b/packages/@aws-cdk/aws-serverless/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-servicecatalog/package.json b/packages/@aws-cdk/aws-servicecatalog/package.json index 5c8981507356d..c380b400f9135 100644 --- a/packages/@aws-cdk/aws-servicecatalog/package.json +++ b/packages/@aws-cdk/aws-servicecatalog/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-servicediscovery/package.json b/packages/@aws-cdk/aws-servicediscovery/package.json index e4c1d37bcb8ed..c9cd2d96c0359 100644 --- a/packages/@aws-cdk/aws-servicediscovery/package.json +++ b/packages/@aws-cdk/aws-servicediscovery/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ses/package.json b/packages/@aws-cdk/aws-ses/package.json index 165de289049be..0b5d0ea738882 100644 --- a/packages/@aws-cdk/aws-ses/package.json +++ b/packages/@aws-cdk/aws-ses/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-sns/package.json b/packages/@aws-cdk/aws-sns/package.json index 3cf1f0fcd8a3f..db010c03d5a60 100644 --- a/packages/@aws-cdk/aws-sns/package.json +++ b/packages/@aws-cdk/aws-sns/package.json @@ -78,4 +78,4 @@ "@aws-cdk/aws-sqs": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index baf0765914cf9..49970048d1134 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -73,4 +73,4 @@ "@aws-cdk/aws-s3-notifications": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ssm/package.json b/packages/@aws-cdk/aws-ssm/package.json index 33610ad02e6d3..abb5e9d048618 100644 --- a/packages/@aws-cdk/aws-ssm/package.json +++ b/packages/@aws-cdk/aws-ssm/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-stepfunctions/package.json b/packages/@aws-cdk/aws-stepfunctions/package.json index 7bf02f5fb6ab5..f874634fbde17 100644 --- a/packages/@aws-cdk/aws-stepfunctions/package.json +++ b/packages/@aws-cdk/aws-stepfunctions/package.json @@ -71,4 +71,4 @@ "@aws-cdk/aws-iam": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-waf/package.json b/packages/@aws-cdk/aws-waf/package.json index ab464b7c74d2d..e300ae0e3de81 100644 --- a/packages/@aws-cdk/aws-waf/package.json +++ b/packages/@aws-cdk/aws-waf/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-wafregional/package.json b/packages/@aws-cdk/aws-wafregional/package.json index 3562d07cf5bc8..70eb6943cc49b 100644 --- a/packages/@aws-cdk/aws-wafregional/package.json +++ b/packages/@aws-cdk/aws-wafregional/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-workspaces/package.json b/packages/@aws-cdk/aws-workspaces/package.json index 0a2eb05baaeea..0235d69bee218 100644 --- a/packages/@aws-cdk/aws-workspaces/package.json +++ b/packages/@aws-cdk/aws-workspaces/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/cdk/package.json b/packages/@aws-cdk/cdk/package.json index 423e3ce3bedea..6cd6846fe360d 100644 --- a/packages/@aws-cdk/cdk/package.json +++ b/packages/@aws-cdk/cdk/package.json @@ -53,7 +53,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/js-base64": "^2.3.1", - "@types/lodash": "^4.14.117", + "@types/lodash": "^4.14.118", "cdk-build-tools": "^0.17.0", "cfn2ts": "^0.17.0", "fast-check": "^1.7.0", @@ -73,4 +73,4 @@ "peerDependencies": { "@aws-cdk/cx-api": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/runtime-values/package.json b/packages/@aws-cdk/runtime-values/package.json index 744275e6704f4..9c6630d4e7943 100644 --- a/packages/@aws-cdk/runtime-values/package.json +++ b/packages/@aws-cdk/runtime-values/package.json @@ -66,4 +66,4 @@ "@aws-cdk/aws-iam": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } -} \ No newline at end of file +} diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 9f931cdc4cc15..0537aa871c8f4 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -32,7 +32,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/archiver": "^2.1.2", - "@types/fs-extra": "^4.0.8", + "@types/fs-extra": "^5.0.4", "@types/minimatch": "^3.0.3", "@types/mockery": "^1.4.29", "@types/request": "^2.47.1", @@ -53,7 +53,7 @@ "camelcase": "^5.0.0", "colors": "^1.2.1", "decamelize": "^2.0.0", - "fs-extra": "^4.0.2", + "fs-extra": "^7.0.0", "json-diff": "^0.3.1", "minimatch": ">=3.0", "promptly": "^0.2.0", diff --git a/packages/simple-resource-bundler/package.json b/packages/simple-resource-bundler/package.json index 85b10793d39d5..4546a17aafde4 100644 --- a/packages/simple-resource-bundler/package.json +++ b/packages/simple-resource-bundler/package.json @@ -22,13 +22,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^4.0.8", + "@types/fs-extra": "^5.0.4", "@types/yargs": "^8.0.3", "cdk-build-tools": "^0.17.0", "pkglint": "^0.17.0" }, "dependencies": { - "fs-extra": "^4.0.2", + "fs-extra": "^7.0.0", "source-map-support": "^0.5.6", "yargs": "^9.0.1" }, diff --git a/tools/cfn2ts/package.json b/tools/cfn2ts/package.json index 44ca387b311db..69c49a9ce5486 100644 --- a/tools/cfn2ts/package.json +++ b/tools/cfn2ts/package.json @@ -34,12 +34,12 @@ "codemaker": "^0.6.4", "colors": "^1.2.1", "fast-json-patch": "^2.0.6", - "fs-extra": "^4.0.2", + "fs-extra": "^7.0.0", "source-map-support": "^0.5.6", "yargs": "^9.0.1" }, "devDependencies": { - "@types/fs-extra": "^4.0.8", + "@types/fs-extra": "^5.0.4", "@types/yargs": "^8.0.3", "cdk-build-tools": "^0.17.0", "pkglint": "^0.17.0" diff --git a/tools/merkle-build/package.json b/tools/merkle-build/package.json index f6004cf63d790..f6d8bb084627c 100644 --- a/tools/merkle-build/package.json +++ b/tools/merkle-build/package.json @@ -21,11 +21,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^4.0.8", + "@types/fs-extra": "^5.0.4", "pkglint": "^0.17.0" }, "dependencies": { - "fs-extra": "^4.0.2" + "fs-extra": "^7.0.0" }, "keywords": [ "aws", diff --git a/tools/pkglint/bin/pkglint.ts b/tools/pkglint/bin/pkglint.ts index c97363058ae4a..8fdcfc9913b0f 100644 --- a/tools/pkglint/bin/pkglint.ts +++ b/tools/pkglint/bin/pkglint.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node +import path = require('path'); import yargs = require('yargs'); -import { findPackageJsons, PackageJson, ValidationRule } from '../lib'; +import { findPackageJsons, ValidationRule } from '../lib'; // tslint:disable:no-shadowed-variable const argv = yargs @@ -11,24 +12,22 @@ const argv = yargs // Our version of yargs doesn't support positional arguments yet argv.directory = argv._[0]; -if (argv.directory == null) { - argv.directory = "."; -} +argv.directory = path.resolve(argv.directory || '.', process.cwd()); async function main(): Promise { const ruleClasses = require('../lib/rules'); const rules: ValidationRule[] = Object.keys(ruleClasses).map(key => new ruleClasses[key]()).filter(obj => obj instanceof ValidationRule); - const pkgs = findPackageJsons(argv.directory).filter(shouldIncludePackage); + const pkgs = findPackageJsons(argv.directory); - rules.forEach(rule => pkgs.forEach(pkg => rule.prepare(pkg))); - rules.forEach(rule => pkgs.forEach(pkg => rule.validate(pkg))); + rules.forEach(rule => pkgs.filter(pkg => pkg.shouldApply(rule)).forEach(pkg => rule.prepare(pkg))); + rules.forEach(rule => pkgs.filter(pkg => pkg.shouldApply(rule)).forEach(pkg => rule.validate(pkg))); if (argv.fix) { pkgs.forEach(pkg => pkg.applyFixes()); } - pkgs.forEach(pkg => pkg.displayReports()); + pkgs.forEach(pkg => pkg.displayReports(argv.directory)); if (pkgs.some(p => p.hasReports)) { throw new Error('Some package.json files had errors'); @@ -40,7 +39,3 @@ main().catch((e) => { console.error(e); process.exit(1); }); - -function shouldIncludePackage(pkg: PackageJson) { - return !(pkg.json.pkglint && pkg.json.pkglint.ignore); -} diff --git a/tools/pkglint/lib/packagejson.ts b/tools/pkglint/lib/packagejson.ts index aaeb6e9ad8bf0..98cdb3da06a10 100644 --- a/tools/pkglint/lib/packagejson.ts +++ b/tools/pkglint/lib/packagejson.ts @@ -1,3 +1,4 @@ +import colors = require('colors/safe'); import fs = require('fs-extra'); import path = require('path'); @@ -27,7 +28,8 @@ export function findPackageJsons(root: string): PackageJson[] { if (file !== 'node_modules' && (fs.lstatSync(fullPath)).isDirectory()) { recurse(fullPath); } - } } + } + } recurse(root); return ret; @@ -36,6 +38,8 @@ export function findPackageJsons(root: string): PackageJson[] { export type Fixer = () => void; export interface Report { + ruleName: string; + message: string; fix?: Fixer; @@ -45,15 +49,40 @@ export interface Report { * Class representing a package.json file and the issues we found with it */ export class PackageJson { - public readonly json: any; + public readonly json: { [key: string]: any }; public readonly packageRoot: string; public readonly packageName: string; + + private readonly includeRules: RegExp[]; + private readonly excludeRules: RegExp[]; + private reports: Report[] = []; constructor(public readonly fullPath: string) { this.json = JSON.parse(fs.readFileSync(fullPath, { encoding: 'utf-8' })); this.packageRoot = path.dirname(path.resolve(fullPath)); this.packageName = this.json.name; + + const disabled = this.json.pkglint && this.json.pkglint.ignore; + this.includeRules = _forceArray(this.json.pkglint && this.json.pkglint.include) || [/^.*$/]; + this.excludeRules = _forceArray(this.json.pkglint && this.json.pkglint.exclude) || (disabled ? [/^.*$/] : []); + + function _forceArray(arg: string | string[] | undefined): RegExp[] | undefined { + if (arg == null) { return arg; } + if (Array.isArray(arg)) { return arg.map(_toRegExp); } + return [_toRegExp(arg)]; + } + + function _toRegExp(pattern: string): RegExp { + pattern = pattern.split('*').map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*'); + return new RegExp(`^${pattern}$`); + } + } + + public shouldApply(rule: ValidationRule): boolean { + const included = this.includeRules.find(r => r.test(rule.name)) != null; + const excluded = this.excludeRules.find(r => r.test(rule.name)) != null; + return included && !excluded; } public save() { @@ -93,10 +122,12 @@ export class PackageJson { this.reports = nonFixable; } - public displayReports() { + public displayReports(relativeTo: string) { if (this.hasReports) { - process.stderr.write(`${path.resolve(this.fullPath)}\n`); - this.reports.forEach(report => process.stderr.write(`- ${report.message}\n`)); + process.stderr.write(`In package ${colors.blue(path.relative(relativeTo, this.fullPath))}\n`); + this.reports.forEach(report => { + process.stderr.write(`- [${colors.yellow(report.ruleName)}] ${report.message}${report.fix ? colors.green(' (fixable)') : ''}\n`); + }); } } @@ -159,6 +190,14 @@ export class PackageJson { return key !== undefined ? deps[key] : undefined; } + /** + * @param predicate the predicate to select dependencies to be extracted + * @returns the list of dependencies matching a pattern. + */ + public getDependencies(predicate: (s: string) => boolean): Array<{ name: string, version: string }> { + return Object.keys(this.json.dependencies || {}).filter(predicate).map(name => ({ name, version: this.json.dependencies[name] })); + } + /** * Adds a devDependency to the package. */ @@ -170,6 +209,17 @@ export class PackageJson { this.json.devDependencies[module] = version; } + /** + * Adds a dependency to the package. + */ + public addDependency(module: string, version = '*') { + if (!('dependencies' in this.json)) { + this.json.dependencies = {}; + } + + this.json.dependencies[module] = version; + } + public removeDevDependency(moduleOrPredicate: ((s: string) => boolean) | string) { if (!('devDependencies' in this.json)) { return; @@ -186,6 +236,22 @@ export class PackageJson { } } + public removeDependency(moduleOrPredicate: ((s: string) => boolean) | string) { + if (!('dependencies' in this.json)) { + return; + } + + const predicate: (s: string) => boolean = typeof(moduleOrPredicate) === 'string' + ? x => x === moduleOrPredicate + : moduleOrPredicate; + + for (const m of Object.keys(this.json.dependencies)) { + if (predicate(m)) { + delete this.json.dependencies[m]; + } + } + } + public addPeerDependency(module: string, version: string) { if (!('peerDependencies' in this.json)) { this.json.peerDependencies = {}; @@ -250,6 +316,8 @@ export class PackageJson { * Interface for validation rules */ export abstract class ValidationRule { + public abstract readonly name: string; + /** * Will be executed for every package definition once, used to collect statistics */ diff --git a/tools/pkglint/lib/rules.ts b/tools/pkglint/lib/rules.ts index 41ff747a18a4e..dae34d17d8b42 100644 --- a/tools/pkglint/lib/rules.ts +++ b/tools/pkglint/lib/rules.ts @@ -10,6 +10,8 @@ import { deepGet, deepSet, expectDevDependency, expectJSON, fileShouldBe, fileSh * Verify that the package name matches the directory name */ export class PackageNameMatchesDirectoryName extends ValidationRule { + public readonly name = 'naming/package-matches-directory'; + public validate(pkg: PackageJson): void { const parts = pkg.packageRoot.split(path.sep); @@ -17,7 +19,7 @@ export class PackageNameMatchesDirectoryName extends ValidationRule { ? parts.slice(parts.length - 2).join('/') : parts[parts.length - 1]; - expectJSON(pkg, 'name', expectedName); + expectJSON(this.name, pkg, 'name', expectedName); } } @@ -25,9 +27,11 @@ export class PackageNameMatchesDirectoryName extends ValidationRule { * Verify that all packages have a description */ export class DescriptionIsRequired extends ValidationRule { + public readonly name = 'package-info/require-description'; + public validate(pkg: PackageJson): void { if (!pkg.json.description) { - pkg.report({ message: 'Description is required' }); + pkg.report({ ruleName: this.name, message: 'Description is required' }); } } } @@ -36,9 +40,11 @@ export class DescriptionIsRequired extends ValidationRule { * Repository must be our GitHub repo */ export class RepositoryCorrect extends ValidationRule { + public readonly name = 'package-info/repository'; + public validate(pkg: PackageJson): void { - expectJSON(pkg, 'repository.type', 'git'); - expectJSON(pkg, 'repository.url', 'https://github.com/awslabs/aws-cdk.git'); + expectJSON(this.name, pkg, 'repository.type', 'git'); + expectJSON(this.name, pkg, 'repository.url', 'https://github.com/awslabs/aws-cdk.git'); } } @@ -46,8 +52,10 @@ export class RepositoryCorrect extends ValidationRule { * Homepage must point to the GitHub repository page. */ export class HomepageCorrect extends ValidationRule { + public readonly name = 'package-info/homepage'; + public validate(pkg: PackageJson): void { - expectJSON(pkg, 'homepage', 'https://github.com/awslabs/aws-cdk'); + expectJSON(this.name, pkg, 'homepage', 'https://github.com/awslabs/aws-cdk'); } } @@ -55,8 +63,10 @@ export class HomepageCorrect extends ValidationRule { * The license must be Apache-2.0. */ export class License extends ValidationRule { + public readonly name = 'package-info/license'; + public validate(pkg: PackageJson): void { - expectJSON(pkg, 'license', 'Apache-2.0'); + expectJSON(this.name, pkg, 'license', 'Apache-2.0'); } } @@ -64,8 +74,10 @@ export class License extends ValidationRule { * There must be a license file that corresponds to the Apache-2.0 license. */ export class LicenseFile extends ValidationRule { + public readonly name = 'license/license-file'; + public validate(pkg: PackageJson): void { - fileShouldBe(pkg, 'LICENSE', LICENSE); + fileShouldBe(this.name, pkg, 'LICENSE', LICENSE); } } @@ -73,8 +85,10 @@ export class LicenseFile extends ValidationRule { * There must be a NOTICE file. */ export class NoticeFile extends ValidationRule { + public readonly name = 'license/notice-file'; + public validate(pkg: PackageJson): void { - fileShouldBe(pkg, 'NOTICE', NOTICE); + fileShouldBe(this.name, pkg, 'NOTICE', NOTICE); } } @@ -82,10 +96,12 @@ export class NoticeFile extends ValidationRule { * Author must be AWS (as an Organization) */ export class AuthorAWS extends ValidationRule { + public readonly name = 'package-info/author'; + public validate(pkg: PackageJson): void { - expectJSON(pkg, 'author.name', 'Amazon Web Services'); - expectJSON(pkg, 'author.url', 'https://aws.amazon.com'); - expectJSON(pkg, 'author.organization', true); + expectJSON(this.name, pkg, 'author.name', 'Amazon Web Services'); + expectJSON(this.name, pkg, 'author.url', 'https://aws.amazon.com'); + expectJSON(this.name, pkg, 'author.organization', true); } } @@ -93,10 +109,13 @@ export class AuthorAWS extends ValidationRule { * There must be a README.md file. */ export class ReadmeFile extends ValidationRule { + public readonly name = 'package-info/README.md'; + public validate(pkg: PackageJson): void { const readmeFile = path.join(pkg.packageRoot, 'README.md'); if (!fs.existsSync(readmeFile)) { pkg.report({ + ruleName: this.name, message: 'There must be a README.md file at the root of the package', fix: () => fs.writeFileSync( readmeFile, @@ -111,9 +130,12 @@ export class ReadmeFile extends ValidationRule { * Keywords must contain CDK keywords and be sorted */ export class CDKKeywords extends ValidationRule { + public readonly name = 'package-info/keywords'; + public validate(pkg: PackageJson): void { if (!pkg.json.keywords) { pkg.report({ + ruleName: this.name, message: 'Must have keywords', fix: () => { pkg.json.keywords = []; } }); @@ -123,6 +145,7 @@ export class CDKKeywords extends ValidationRule { if (keywords.indexOf('cdk') === -1) { pkg.report({ + ruleName: this.name, message: 'Keywords must mention CDK', fix: () => { pkg.json.keywords.splice(0, 0, 'cdk'); } }); @@ -130,6 +153,7 @@ export class CDKKeywords extends ValidationRule { if (keywords.indexOf('aws') === -1) { pkg.report({ + ruleName: this.name, message: 'Keywords must mention AWS', fix: () => { pkg.json.keywords.splice(0, 0, 'aws'); } }); @@ -141,21 +165,24 @@ export class CDKKeywords extends ValidationRule { * JSII Java package is required and must look sane */ export class JSIIJavaPackageIsRequired extends ValidationRule { + public readonly name = 'jsii/java'; + public validate(pkg: PackageJson): void { if (!isJSII(pkg)) { return; } const moduleName = cdkModuleName(pkg.json.name); - expectJSON(pkg, 'jsii.targets.java.maven.groupId', 'software.amazon.awscdk'); - expectJSON(pkg, 'jsii.targets.java.maven.artifactId', moduleName.mavenArtifactId, /-/g); + expectJSON(this.name, pkg, 'jsii.targets.java.maven.groupId', 'software.amazon.awscdk'); + expectJSON(this.name, pkg, 'jsii.targets.java.maven.artifactId', moduleName.mavenArtifactId, /-/g); const java = deepGet(pkg.json, ['jsii', 'targets', 'java', 'package']) as string | undefined; - expectJSON(pkg, 'jsii.targets.java.package', moduleName.javaPackage, /\./g); + expectJSON(this.name, pkg, 'jsii.targets.java.package', moduleName.javaPackage, /\./g); if (java) { const expectedPrefix = moduleName.javaPackage.split('.').slice(0, 3).join('.'); const actualPrefix = java.split('.').slice(0, 3).join('.'); if (expectedPrefix !== actualPrefix) { pkg.report({ + ruleName: this.name, message: `JSII "java" package must share the first 3 elements of the expected one: ${expectedPrefix} vs ${actualPrefix}`, fix: () => deepSet(pkg.json, ['jsii', 'targets', 'java', 'package'], moduleName.javaPackage) }); @@ -165,32 +192,36 @@ export class JSIIJavaPackageIsRequired extends ValidationRule { } export class JSIISphinxTarget extends ValidationRule { + public readonly name = 'jsii/sphinx'; + public validate(pkg: PackageJson): void { if (!isJSII(pkg)) { return; } - expectJSON(pkg, 'jsii.targets.sphinx', { }); + expectJSON(this.name, pkg, 'jsii.targets.sphinx', { }); } } export class CDKPackage extends ValidationRule { + public readonly name = 'package-info/scripts/package'; + public validate(pkg: PackageJson): void { // skip private packages if (pkg.json.private) { return; } const merkleMarker = '.LAST_PACKAGE'; - expectJSON(pkg, 'scripts.package', 'cdk-package'); + expectJSON(this.name, pkg, 'scripts.package', 'cdk-package'); const outdir = 'dist'; // if this is if (isJSII(pkg)) { - expectJSON(pkg, 'jsii.outdir', outdir); + expectJSON(this.name, pkg, 'jsii.outdir', outdir); } - fileShouldContain(pkg, '.npmignore', outdir); - fileShouldContain(pkg, '.gitignore', outdir); - fileShouldContain(pkg, '.npmignore', merkleMarker); - fileShouldContain(pkg, '.gitignore', merkleMarker); + fileShouldContain(this.name, pkg, '.npmignore', outdir); + fileShouldContain(this.name, pkg, '.gitignore', outdir); + fileShouldContain(this.name, pkg, '.npmignore', merkleMarker); + fileShouldContain(this.name, pkg, '.gitignore', merkleMarker); } } @@ -199,11 +230,14 @@ export class CDKPackage extends ValidationRule { * level. */ export class NoJsiiDep extends ValidationRule { + public readonly name = 'dependencies/no-jsii'; + public validate(pkg: PackageJson): void { const predicate = (s: string) => s.startsWith('jsii'); if (pkg.getDevDependency(predicate)) { pkg.report({ + ruleName: this.name, message: 'packages should not have a devDep on jsii since it is defined at the repo level', fix: () => pkg.removeDevDependency(predicate) }); @@ -211,6 +245,45 @@ export class NoJsiiDep extends ValidationRule { } } +/** + * Verifies that the expected versions of node will be supported. + */ +export class NodeCompatibility extends ValidationRule { + public readonly name = 'dependencies/node-version'; + + public validate(pkg: PackageJson): void { + const atTypesNode = pkg.getDevDependency('@types/node'); + if (atTypesNode && !atTypesNode.startsWith('^8.11.')) { + pkg.report({ + ruleName: this.name, + message: `packages must support node version 8 and up, but ${atTypesNode} is declared`, + fix: () => pkg.addDevDependency('@types/node', '^8.11.38') + }); + } + } +} + +/** + * Verifies that the ``@types/`` dependencies are correctly recorded in ``devDependencies`` and not ``dependencies``. + */ +export class NoAtTypesInDependencies extends ValidationRule { + public readonly name = 'dependencies/at-types'; + + public validate(pkg: PackageJson): void { + const predicate = (s: string) => s.startsWith('@types/'); + for (const dependency of pkg.getDependencies(predicate)) { + pkg.report({ + ruleName: this.name, + message: `dependency on ${dependency.name}@${dependency.version} must be in devDependencies`, + fix: () => { + pkg.addDevDependency(dependency.name, dependency.version); + pkg.removeDependency(predicate); + } + }); + } + } +} + /** * Computes the module name for various other purposes (java package, ...) */ @@ -237,18 +310,21 @@ function cdkModuleName(name: string) { * JSII .NET namespace is required and must look sane */ export class JSIIDotNetNamespaceIsRequired extends ValidationRule { + public readonly name = 'jsii/dotnet'; + public validate(pkg: PackageJson): void { if (!isJSII(pkg)) { return; } const dotnet = deepGet(pkg.json, ['jsii', 'targets', 'dotnet', 'namespace']) as string | undefined; const moduleName = cdkModuleName(pkg.json.name); - expectJSON(pkg, 'jsii.targets.dotnet.namespace', moduleName.dotnetNamespace, /\./g, /*case insensitive*/ true); + expectJSON(this.name, pkg, 'jsii.targets.dotnet.namespace', moduleName.dotnetNamespace, /\./g, /*case insensitive*/ true); if (dotnet) { const actualPrefix = dotnet.split('.').slice(0, 2).join('.'); const expectedPrefix = moduleName.dotnetNamespace.split('.').slice(0, 2).join('.'); if (actualPrefix !== expectedPrefix) { pkg.report({ + ruleName: this.name, message: `.NET namespace must share the first two segments of the default namespace, '${expectedPrefix}' vs '${actualPrefix}'`, fix: () => deepSet(pkg.json, ['jsii', 'targets', 'dotnet', 'namespace'], moduleName.dotnetNamespace) }); @@ -261,6 +337,8 @@ export class JSIIDotNetNamespaceIsRequired extends ValidationRule { * Strong-naming all .NET assemblies is required. */ export class JSIIDotNetStrongNameIsRequired extends ValidationRule { + public readonly name = 'jsii/dotnet/strong-name'; + public validate(pkg: PackageJson): void { if (!isJSII(pkg)) { return; } @@ -268,6 +346,7 @@ export class JSIIDotNetStrongNameIsRequired extends ValidationRule { const signAssemblyExpected = true; if (signAssembly !== signAssemblyExpected) { pkg.report({ + ruleName: this.name, message: `.NET packages must have strong-name signing enabled.`, fix: () => deepSet(pkg.json, ['jsii', 'targets', 'dotnet', 'signAssembly'], signAssemblyExpected) }); @@ -277,6 +356,7 @@ export class JSIIDotNetStrongNameIsRequired extends ValidationRule { const assemblyOriginatorKeyFileExpected = "../../key.snk"; if (assemblyOriginatorKeyFile !== assemblyOriginatorKeyFileExpected) { pkg.report({ + ruleName: this.name, message: `.NET packages must use the strong name key fetched by fetch-dotnet-snk.sh`, fix: () => deepSet(pkg.json, ['jsii', 'targets', 'dotnet', 'assemblyOriginatorKeyFile'], assemblyOriginatorKeyFileExpected) }); @@ -288,10 +368,12 @@ export class JSIIDotNetStrongNameIsRequired extends ValidationRule { * The package must depend on cdk-build-tools */ export class MustDependOnBuildTools extends ValidationRule { + public readonly name = 'dependencies/build-tools'; + public validate(pkg: PackageJson): void { if (!shouldUseCDKBuildTools(pkg)) { return; } - expectDevDependency(pkg, 'cdk-build-tools', '^' + monoRepoVersion()); + expectDevDependency(this.name, pkg, 'cdk-build-tools', '^' + monoRepoVersion()); } } @@ -299,15 +381,17 @@ export class MustDependOnBuildTools extends ValidationRule { * Build script must be 'cdk-build' */ export class MustUseCDKBuild extends ValidationRule { + public readonly name = 'package-info/scripts/build'; + public validate(pkg: PackageJson): void { if (!shouldUseCDKBuildTools(pkg)) { return; } - expectJSON(pkg, 'scripts.build', 'cdk-build'); + expectJSON(this.name, pkg, 'scripts.build', 'cdk-build'); // cdk-build will write a hash file that we have to ignore. const merkleMarker = '.LAST_BUILD'; - fileShouldContain(pkg, '.gitignore', merkleMarker); - fileShouldContain(pkg, '.npmignore', merkleMarker); + fileShouldContain(this.name, pkg, '.gitignore', merkleMarker); + fileShouldContain(this.name, pkg, '.npmignore', merkleMarker); } } @@ -328,6 +412,8 @@ export class MustUseCDKBuild extends ValidationRule { * libraries unnecessarily. */ export class RegularDependenciesMustSatisfyPeerDependencies extends ValidationRule { + public readonly name = 'dependencies/peer-dependencies-satisfied'; + public validate(pkg: PackageJson): void { for (const [depName, peerVersion] of Object.entries(pkg.peerDependencies)) { const depVersion = pkg.dependencies[depName]; @@ -336,6 +422,7 @@ export class RegularDependenciesMustSatisfyPeerDependencies extends ValidationRu // Make sure that depVersion satisfies peerVersion. if (!semver.intersects(depVersion, peerVersion)) { pkg.report({ + ruleName: this.name, message: `dependency ${depName}: concrete version ${depVersion} does not match peer version '${peerVersion}'`, fix: () => pkg.addPeerDependency(depName, depVersion) }); @@ -345,17 +432,21 @@ export class RegularDependenciesMustSatisfyPeerDependencies extends ValidationRu } export class MustIgnoreSNK extends ValidationRule { + public readonly name = 'ignore/strong-name-key'; + public validate(pkg: PackageJson): void { - fileShouldContain(pkg, '.npmignore', '*.snk'); - fileShouldContain(pkg, '.gitignore', '*.snk'); + fileShouldContain(this.name, pkg, '.npmignore', '*.snk'); + fileShouldContain(this.name, pkg, '.gitignore', '*.snk'); } } export class NpmIgnoreForJsiiModules extends ValidationRule { + public readonly name = 'ignore/jsii'; + public validate(pkg: PackageJson): void { if (!isJSII(pkg)) { return; } - fileShouldContain(pkg, '.npmignore', + fileShouldContain(this.name, pkg, '.npmignore', '*.ts', '!*.d.ts', '!*.js', @@ -371,6 +462,8 @@ export class NpmIgnoreForJsiiModules extends ValidationRule { * the test script uses "nodeunit" */ export class GlobalDevDependencies extends ValidationRule { + public readonly name = 'dependencies/global-dev'; + public validate(pkg: PackageJson): void { const deps = [ @@ -385,6 +478,7 @@ export class GlobalDevDependencies extends ValidationRule { for (const dep of deps) { if (pkg.getDevDependency(dep)) { pkg.report({ + ruleName: this.name, message: `devDependency ${dep} is defined at the repo level`, fix: () => pkg.removeDevDependency(dep) }); @@ -397,10 +491,12 @@ export class GlobalDevDependencies extends ValidationRule { * Must use 'cdk-watch' command */ export class MustUseCDKWatch extends ValidationRule { + public readonly name = 'package-info/scripts/watch'; + public validate(pkg: PackageJson): void { if (!shouldUseCDKBuildTools(pkg)) { return; } - expectJSON(pkg, 'scripts.watch', 'cdk-watch'); + expectJSON(this.name, pkg, 'scripts.watch', 'cdk-watch'); } } @@ -408,17 +504,19 @@ export class MustUseCDKWatch extends ValidationRule { * Must use 'cdk-test' command */ export class MustUseCDKTest extends ValidationRule { + public readonly name = 'package-info/scripts/test'; + public validate(pkg: PackageJson): void { if (!shouldUseCDKBuildTools(pkg)) { return; } if (!hasTestDirectory(pkg)) { return; } - expectJSON(pkg, 'scripts.test', 'cdk-test'); + expectJSON(this.name, pkg, 'scripts.test', 'cdk-test'); // 'cdk-test' will calculate coverage, so have the appropriate // files in .gitignore. - fileShouldContain(pkg, '.gitignore', '.nyc_output'); - fileShouldContain(pkg, '.gitignore', 'coverage'); - fileShouldContain(pkg, '.gitignore', '.nycrc'); + fileShouldContain(this.name, pkg, '.gitignore', '.nyc_output'); + fileShouldContain(this.name, pkg, '.gitignore', 'coverage'); + fileShouldContain(this.name, pkg, '.gitignore', '.nycrc'); } } @@ -428,22 +526,27 @@ export class MustUseCDKTest extends ValidationRule { * This commands comes from the dev-dependency cdk-integ-tools. */ export class MustHaveIntegCommand extends ValidationRule { + public readonly name = 'package-info/scripts/integ'; + public validate(pkg: PackageJson): void { if (!hasIntegTests(pkg)) { return; } - expectJSON(pkg, 'scripts.integ', 'cdk-integ'); - expectDevDependency(pkg, 'cdk-integ-tools', '^' + monoRepoVersion()); + expectJSON(this.name, pkg, 'scripts.integ', 'cdk-integ'); + expectDevDependency(this.name, pkg, 'cdk-integ-tools', '^' + monoRepoVersion()); } } export class PkgLintAsScript extends ValidationRule { + public readonly name = 'package-info/scripts/pkglint'; + public validate(pkg: PackageJson): void { const script = 'pkglint -f'; - expectDevDependency(pkg, 'pkglint', '^' + monoRepoVersion()); + expectDevDependency(this.name, pkg, 'pkglint', '^' + monoRepoVersion()); if (!pkg.npmScript('pkglint')) { pkg.report({ + ruleName: this.name, message: 'a script called "pkglint" must be included to allow fixing package linting issues', fix: () => pkg.changeNpmScript('pkglint', () => script) }); @@ -451,6 +554,7 @@ export class PkgLintAsScript extends ValidationRule { if (pkg.npmScript('pkglint') !== script) { pkg.report({ + ruleName: this.name, message: 'the pkglint script should be: ' + script, fix: () => pkg.changeNpmScript('pkglint', () => script) }); @@ -459,15 +563,18 @@ export class PkgLintAsScript extends ValidationRule { } export class NoStarDeps extends ValidationRule { + public readonly name = 'dependencies/no-star'; + public validate(pkg: PackageJson) { - reportStarDeps(pkg.json.depedencies); - reportStarDeps(pkg.json.devDependencies); + reportStarDeps(this.name, pkg.json.depedencies); + reportStarDeps(this.name, pkg.json.devDependencies); - function reportStarDeps(deps?: any) { + function reportStarDeps(ruleName: string, deps?: any) { deps = deps || {}; Object.keys(deps).forEach(d => { if (deps[d] === '*') { pkg.report({ + ruleName, message: `star dependency not allowed for ${d}` }); } @@ -487,6 +594,8 @@ interface VersionCount { * NOTE: this rule will only be useful when validating multiple package.jsons at the same time */ export class AllVersionsTheSame extends ValidationRule { + public readonly name = 'dependencies/versions-consistent'; + private readonly ourPackages: {[pkg: string]: string} = {}; private readonly usedDeps: {[pkg: string]: VersionCount[]} = {}; @@ -541,7 +650,7 @@ export class AllVersionsTheSame extends ValidationRule { private validateDep(pkg: PackageJson, depField: string, dep: string) { if (dep in this.ourPackages) { - expectJSON(pkg, depField + '.' + dep, this.ourPackages[dep]); + expectJSON(this.name, pkg, depField + '.' + dep, this.ourPackages[dep]); return; } @@ -551,7 +660,7 @@ export class AllVersionsTheSame extends ValidationRule { const versions = this.usedDeps[dep]; versions.sort((a, b) => b.count - a.count); - expectJSON(pkg, depField + '.' + dep, versions[0].version); + expectJSON(this.name, pkg, depField + '.' + dep, versions[0].version); } } diff --git a/tools/pkglint/lib/util.ts b/tools/pkglint/lib/util.ts index bdb9975d418fa..b2cd1715c3464 100644 --- a/tools/pkglint/lib/util.ts +++ b/tools/pkglint/lib/util.ts @@ -5,11 +5,12 @@ import { PackageJson } from "./packagejson"; /** * Expect a particular JSON key to be a given value */ -export function expectJSON(pkg: PackageJson, jsonPath: string, expected: any, ignore?: RegExp, caseInsensitive: boolean = false) { +export function expectJSON(ruleName: string, pkg: PackageJson, jsonPath: string, expected: any, ignore?: RegExp, caseInsensitive: boolean = false) { const parts = jsonPath.split('.'); const actual = deepGet(pkg.json, parts); if (applyCaseInsensitive(applyIgnore(actual)) !== applyCaseInsensitive(applyIgnore(expected))) { pkg.report({ + ruleName, message: `${jsonPath} should be ${JSON.stringify(expected)}${ignore ? ` (ignoring ${ignore})` : ''}, is ${JSON.stringify(actual)}`, fix: () => { deepSet(pkg.json, parts, expected); } }); @@ -31,11 +32,12 @@ export function expectJSON(pkg: PackageJson, jsonPath: string, expected: any, ig /** * Export a package-level file to contain a given line */ -export function fileShouldContain(pkg: PackageJson, fileName: string, ...lines: string[]) { +export function fileShouldContain(ruleName: string, pkg: PackageJson, fileName: string, ...lines: string[]) { for (const line of lines) { const doesContain = pkg.fileContainsSync(fileName, line); if (!doesContain) { pkg.report({ + ruleName, message: `${fileName} should contain '${line}'`, fix: () => pkg.addToFileSync(fileName, line) }); @@ -46,10 +48,11 @@ export function fileShouldContain(pkg: PackageJson, fileName: string, ...lines: /** * Export a package-level file to contain specific content */ -export function fileShouldBe(pkg: PackageJson, fileName: string, content: string) { +export function fileShouldBe(ruleName: string, pkg: PackageJson, fileName: string, content: string) { const isContent = pkg.fileIsSync(fileName, content); if (!isContent) { pkg.report({ + ruleName, message: `${fileName} should contain exactly '${content}'`, fix: () => pkg.writeFileSync(fileName, content) }); @@ -59,10 +62,11 @@ export function fileShouldBe(pkg: PackageJson, fileName: string, content: string /** * Enforce a dev dependency */ -export function expectDevDependency(pkg: PackageJson, packageName: string, version: string) { +export function expectDevDependency(ruleName: string, pkg: PackageJson, packageName: string, version: string) { const actualVersion = pkg.getDevDependency(packageName); if (version !== actualVersion) { pkg.report({ + ruleName, message: `Missing devDependency: ${packageName} @ ${version}`, fix: () => pkg.addDevDependency(packageName, version) }); diff --git a/tools/pkglint/package.json b/tools/pkglint/package.json index a5d998cfd1c10..0b7b718ee9263 100644 --- a/tools/pkglint/package.json +++ b/tools/pkglint/package.json @@ -32,12 +32,14 @@ }, "license": "Apache-2.0", "devDependencies": { + "@types/colors": "^1.2.1", "@types/fs-extra": "^4.0.8", + "@types/semver": "^5.5.0", "@types/yargs": "^8.0.3" }, "dependencies": { - "@types/semver": "^5.5.0", "case": "^1.5.5", + "colors": "^1.3.2", "fs-extra": "^4.0.2", "semver": "^5.6.0", "yargs": "^9.0.1" diff --git a/tools/pkgtools/package.json b/tools/pkgtools/package.json index d7dac35025246..9c1a4266487e3 100644 --- a/tools/pkgtools/package.json +++ b/tools/pkgtools/package.json @@ -26,13 +26,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^4.0.8", + "@types/fs-extra": "^5.0.4", "@types/yargs": "^8.0.3", "cdk-build-tools": "^0.17.0", "pkglint": "^0.17.0" }, "dependencies": { - "fs-extra": "^4.0.2", + "fs-extra": "^7.0.0", "yargs": "^9.0.1" }, "keywords": [ diff --git a/tools/y-npm/package.json b/tools/y-npm/package.json index a6a9675632c7a..4253687db39af 100644 --- a/tools/y-npm/package.json +++ b/tools/y-npm/package.json @@ -26,14 +26,14 @@ }, "dependencies": { "colors": "^1.2.1", - "fs-extra": "^4.0.2", + "fs-extra": "^7.0.0", "semver": "^5.5.0", "source-map-support": "^0.5.6", "verdaccio": "^3.2.0" }, "devDependencies": { "@types/colors": "^1.2.1", - "@types/fs-extra": "^4.0.8", + "@types/fs-extra": "^5.0.4", "@types/semver": "^5.5.0", "cdk-build-tools": "^0.17.0", "pkglint": "^0.17.0" From 8f05c1dbce14f0d94f063f07c04b5daa7f5bbcf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Thu, 22 Nov 2018 10:37:44 +0100 Subject: [PATCH 10/15] Integrate latest version bump --- packages/@aws-cdk/cloud-assembly/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly/package.json b/packages/@aws-cdk/cloud-assembly/package.json index 09f4e9f17c38d..d809197718cf3 100644 --- a/packages/@aws-cdk/cloud-assembly/package.json +++ b/packages/@aws-cdk/cloud-assembly/package.json @@ -1,7 +1,7 @@ { "name": "@aws-cdk/cloud-assembly", "description": "Reference implementation for the Cloud Assembly specification", - "version": "0.17.0", + "version": "0.18.1", "main": "lib/index.js", "types": "lib/index.d.ts", "jsii": { @@ -47,8 +47,8 @@ "jsonschema": "^1.2.4" }, "devDependencies": { - "cdk-build-tools": "^0.17.0", - "pkglint": "^0.17.0", + "cdk-build-tools": "^0.18.1", + "pkglint": "^0.18.1", "typescript-json-schema": "^0.33.0" }, "bundleDependencies": [ From 55714ee2b17fdaf285e59463ed477a8a879e1253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Fri, 23 Nov 2018 11:03:06 +0100 Subject: [PATCH 11/15] WIP Checkpoint --- packages/@aws-cdk/cdk/lib/app.ts | 87 +++++++++++++++++++++++++++--- packages/@aws-cdk/cdk/package.json | 8 ++- packages/aws-cdk/package.json | 1 + 3 files changed, 86 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/app.ts b/packages/@aws-cdk/cdk/lib/app.ts index d9047528323b4..d182365319c6c 100644 --- a/packages/@aws-cdk/cdk/lib/app.ts +++ b/packages/@aws-cdk/cdk/lib/app.ts @@ -2,6 +2,7 @@ import cloudAssembly = require('@aws-cdk/cloud-assembly'); import cxapi = require('@aws-cdk/cx-api'); import fs = require('fs'); import path = require('path'); +import YAML = require('yaml'); import { Stack } from './cloudformation/stack'; import { Construct, MetadataEntry, PATH_SEP, Root } from './core/construct'; import { resolve } from './core/tokens'; @@ -45,18 +46,84 @@ export class App extends Root { const manifest: cloudAssembly.Manifest = { schema: 'cloud-assembly/1.0', - drops: { CDKMetadata: this.collectRuntimeInformation() }, + drops: {}, }; + const cdkMetadata = this.collectRuntimeInformation(); + for (const stack of this.synthesizeStacks(Object.keys(this.stacks))) { - manifest.drops[`stacks/${stack.name}`] = { + manifest.drops[stack.name] = { type: 'npm://@aws-cdk/cdk/CloudFormationStackDroplet', environment: `aws://${stack.environment.account}/${stack.environment.region}`, + properties: { + stackName: stack.name, + template: _saveTemplateSync(stack), + parameters: _wireAssetsSync(stack), + }, + metadata: { + 'aws:cdk:metadata': { + kind: 'aws:cdk:libraries', + value: cdkMetadata + } + } }; } const outfile = path.join(outdir, cloudAssembly.MANIFEST_FILE_NAME); fs.writeFileSync(outfile, JSON.stringify(manifest, undefined, 2)); + + function _saveTemplateSync(stack: cxapi.SynthesizedStack): string { + const fileName = 'template.yml'; + const stackDir = stack.name; + + const absoluteDir = path.join(outdir!, stackDir); + if (!fs.existsSync(absoluteDir)) { + fs.mkdirSync(path.join(absoluteDir)); + } + fs.writeFileSync(path.join(absoluteDir, fileName), + YAML.stringify(stack.template), + { encoding: 'utf-8' }); + return path.join(stackDir, fileName); + } + + function _wireAssetsSync(stack: cxapi.SynthesizedStack): { [name: string]: string } { + const result: { [name: string]: string} = {}; + for (const key of Object.keys(stack.metadata)) { + const entries = stack.metadata[key]; + for (const entry of entries.filter(md => md.type === cxapi.ASSET_METADATA && md.data != null)) { + const data = entry.data! as cxapi.AssetMetadataEntry; + const filePath = path.join(...key.split('/')); + const fileName = path.basename(data.path); + const absoluteFile = path.join(_mkdirp(path.join(outdir!, filePath)), fileName); + if (fs.existsSync(absoluteFile)) { + if (fs.lstatSync(absoluteFile).isDirectory()) { + fs.rmdirSync(absoluteFile); + } else { + fs.unlinkSync(absoluteFile); + } + } + fs.symlinkSync(data.path, absoluteFile); + + const dropId = `${key}`; + manifest.drops[dropId] = { + type: `npm://@aws-cdk/asset/Asset`, + environment: `aws://${stack.environment.account}/${stack.environment.region}`, + properties: { + packaging: data.packaging, + path: path.join(filePath, fileName) + } + }; + if (data.packaging === 'zip' || data.packaging === 'file') { + result[data.s3BucketParameter] = `\${${dropId}.s3BucketName}`; + result[data.s3KeyParameter] = `\${${dropId}.s3ObjectKey}`; + } else if (data.packaging === 'container-image') { + result[data.repositoryParameter] = `\${${dropId}.repository}`; + result[data.tagParameter] = `\${${dropId}.tag}`; + } + } + } + return result; + } } /** @@ -130,7 +197,7 @@ export class App extends Root { } } - private collectRuntimeInformation(): cloudAssembly.Drop { + private collectRuntimeInformation(): { [name: string]: string } { const libraries: { [name: string]: string } = {}; for (const fileName of Object.keys(require.cache)) { @@ -140,11 +207,7 @@ export class App extends Root { } } - return { - type: 'npm://@aws-cdk/cdk/CDKMetadataDroplet', - environment: '*', - properties: { libraries } - }; + return libraries; } private getStack(stackname: string) { @@ -216,3 +279,11 @@ function findNpmPackage(fileName: string): { name: string, version: string, priv return s; } } + +function _mkdirp(pth: string): string { + if (!fs.existsSync(pth)) { + _mkdirp(path.dirname(pth)); + fs.mkdirSync(pth); + } + return pth; +} diff --git a/packages/@aws-cdk/cdk/package.json b/packages/@aws-cdk/cdk/package.json index 0a5e245f85398..d85a4941298e2 100644 --- a/packages/@aws-cdk/cdk/package.json +++ b/packages/@aws-cdk/cdk/package.json @@ -54,6 +54,7 @@ "devDependencies": { "@types/js-base64": "^2.3.1", "@types/lodash": "^4.14.118", + "@types/yaml": "^1.0.1", "cdk-build-tools": "^0.18.1", "cfn2ts": "^0.18.1", "fast-check": "^1.7.0", @@ -61,13 +62,16 @@ "pkglint": "^0.18.1" }, "dependencies": { + "@aws-cdk/cloud-assembly": "^0.18.1", "@aws-cdk/cx-api": "^0.18.1", "js-base64": "^2.4.5", - "json-diff": "^0.3.1" + "json-diff": "^0.3.1", + "yaml": "^1.0.2" }, "bundledDependencies": [ "json-diff", - "js-base64" + "js-base64", + "yaml" ], "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index eea4b095c60c0..18469fc8f3bf7 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -47,6 +47,7 @@ }, "dependencies": { "@aws-cdk/applet-js": "^0.18.1", + "@aws-cdk/cloud-assembly": "^0.18.1", "@aws-cdk/cloudformation-diff": "^0.18.1", "@aws-cdk/cx-api": "^0.18.1", "archiver": "^2.1.1", From 9e49de4e663ad299f6edbda7344c6ee8fe67d026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Fri, 23 Nov 2018 14:14:37 +0100 Subject: [PATCH 12/15] Introduce 'cdk package' command to generate Cloud Assembly --- packages/@aws-cdk/cdk/lib/app.ts | 98 ++------------- packages/@aws-cdk/cdk/package.json | 10 +- packages/aws-cdk/lib/commands/context.ts | 5 +- packages/aws-cdk/lib/commands/package.ts | 145 +++++++++++++++++++++++ packages/aws-cdk/lib/settings.ts | 9 +- 5 files changed, 164 insertions(+), 103 deletions(-) create mode 100644 packages/aws-cdk/lib/commands/package.ts diff --git a/packages/@aws-cdk/cdk/lib/app.ts b/packages/@aws-cdk/cdk/lib/app.ts index d182365319c6c..f3e7c6a90ff8a 100644 --- a/packages/@aws-cdk/cdk/lib/app.ts +++ b/packages/@aws-cdk/cdk/lib/app.ts @@ -1,8 +1,6 @@ -import cloudAssembly = require('@aws-cdk/cloud-assembly'); import cxapi = require('@aws-cdk/cx-api'); import fs = require('fs'); import path = require('path'); -import YAML = require('yaml'); import { Stack } from './cloudformation/stack'; import { Construct, MetadataEntry, PATH_SEP, Root } from './core/construct'; import { resolve } from './core/tokens'; @@ -44,86 +42,14 @@ export class App extends Root { return; } - const manifest: cloudAssembly.Manifest = { - schema: 'cloud-assembly/1.0', - drops: {}, + const result: cxapi.SynthesizeResponse = { + version: cxapi.PROTO_RESPONSE_VERSION, + stacks: this.synthesizeStacks(Object.keys(this.stacks)), + runtime: this.collectRuntimeInformation() }; - const cdkMetadata = this.collectRuntimeInformation(); - - for (const stack of this.synthesizeStacks(Object.keys(this.stacks))) { - manifest.drops[stack.name] = { - type: 'npm://@aws-cdk/cdk/CloudFormationStackDroplet', - environment: `aws://${stack.environment.account}/${stack.environment.region}`, - properties: { - stackName: stack.name, - template: _saveTemplateSync(stack), - parameters: _wireAssetsSync(stack), - }, - metadata: { - 'aws:cdk:metadata': { - kind: 'aws:cdk:libraries', - value: cdkMetadata - } - } - }; - } - - const outfile = path.join(outdir, cloudAssembly.MANIFEST_FILE_NAME); - fs.writeFileSync(outfile, JSON.stringify(manifest, undefined, 2)); - - function _saveTemplateSync(stack: cxapi.SynthesizedStack): string { - const fileName = 'template.yml'; - const stackDir = stack.name; - - const absoluteDir = path.join(outdir!, stackDir); - if (!fs.existsSync(absoluteDir)) { - fs.mkdirSync(path.join(absoluteDir)); - } - fs.writeFileSync(path.join(absoluteDir, fileName), - YAML.stringify(stack.template), - { encoding: 'utf-8' }); - return path.join(stackDir, fileName); - } - - function _wireAssetsSync(stack: cxapi.SynthesizedStack): { [name: string]: string } { - const result: { [name: string]: string} = {}; - for (const key of Object.keys(stack.metadata)) { - const entries = stack.metadata[key]; - for (const entry of entries.filter(md => md.type === cxapi.ASSET_METADATA && md.data != null)) { - const data = entry.data! as cxapi.AssetMetadataEntry; - const filePath = path.join(...key.split('/')); - const fileName = path.basename(data.path); - const absoluteFile = path.join(_mkdirp(path.join(outdir!, filePath)), fileName); - if (fs.existsSync(absoluteFile)) { - if (fs.lstatSync(absoluteFile).isDirectory()) { - fs.rmdirSync(absoluteFile); - } else { - fs.unlinkSync(absoluteFile); - } - } - fs.symlinkSync(data.path, absoluteFile); - - const dropId = `${key}`; - manifest.drops[dropId] = { - type: `npm://@aws-cdk/asset/Asset`, - environment: `aws://${stack.environment.account}/${stack.environment.region}`, - properties: { - packaging: data.packaging, - path: path.join(filePath, fileName) - } - }; - if (data.packaging === 'zip' || data.packaging === 'file') { - result[data.s3BucketParameter] = `\${${dropId}.s3BucketName}`; - result[data.s3KeyParameter] = `\${${dropId}.s3ObjectKey}`; - } else if (data.packaging === 'container-image') { - result[data.repositoryParameter] = `\${${dropId}.repository}`; - result[data.tagParameter] = `\${${dropId}.tag}`; - } - } - } - return result; - } + const outfile = path.join(outdir, cxapi.OUTFILE_NAME); + fs.writeFileSync(outfile, JSON.stringify(result, undefined, 2)); } /** @@ -197,7 +123,7 @@ export class App extends Root { } } - private collectRuntimeInformation(): { [name: string]: string } { + private collectRuntimeInformation(): cxapi.AppRuntime { const libraries: { [name: string]: string } = {}; for (const fileName of Object.keys(require.cache)) { @@ -207,7 +133,7 @@ export class App extends Root { } } - return libraries; + return { libraries }; } private getStack(stackname: string) { @@ -279,11 +205,3 @@ function findNpmPackage(fileName: string): { name: string, version: string, priv return s; } } - -function _mkdirp(pth: string): string { - if (!fs.existsSync(pth)) { - _mkdirp(path.dirname(pth)); - fs.mkdirSync(pth); - } - return pth; -} diff --git a/packages/@aws-cdk/cdk/package.json b/packages/@aws-cdk/cdk/package.json index d85a4941298e2..cedaadfc0f90a 100644 --- a/packages/@aws-cdk/cdk/package.json +++ b/packages/@aws-cdk/cdk/package.json @@ -54,7 +54,6 @@ "devDependencies": { "@types/js-base64": "^2.3.1", "@types/lodash": "^4.14.118", - "@types/yaml": "^1.0.1", "cdk-build-tools": "^0.18.1", "cfn2ts": "^0.18.1", "fast-check": "^1.7.0", @@ -62,19 +61,16 @@ "pkglint": "^0.18.1" }, "dependencies": { - "@aws-cdk/cloud-assembly": "^0.18.1", "@aws-cdk/cx-api": "^0.18.1", "js-base64": "^2.4.5", - "json-diff": "^0.3.1", - "yaml": "^1.0.2" + "json-diff": "^0.3.1" }, "bundledDependencies": [ "json-diff", - "js-base64", - "yaml" + "js-base64" ], "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { "@aws-cdk/cx-api": "^0.18.1" } -} +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/commands/context.ts b/packages/aws-cdk/lib/commands/context.ts index e77d0cbf35864..285c3b601f191 100644 --- a/packages/aws-cdk/lib/commands/context.ts +++ b/packages/aws-cdk/lib/commands/context.ts @@ -20,8 +20,7 @@ export const builder = { }; export async function handler(args: yargs.Arguments): Promise { - const configuration = new Configuration(); - await configuration.load(); + const configuration = await new Configuration().load(); const context = configuration.projectConfig.get(['context']) || {}; @@ -111,4 +110,4 @@ function enumerate1(xs: T[]): Array<[number, T]> { i += 1; } return ret; -} \ No newline at end of file +} diff --git a/packages/aws-cdk/lib/commands/package.ts b/packages/aws-cdk/lib/commands/package.ts new file mode 100644 index 0000000000000..55bacff4ee371 --- /dev/null +++ b/packages/aws-cdk/lib/commands/package.ts @@ -0,0 +1,145 @@ +import cloudAssembly = require('@aws-cdk/cloud-assembly'); +import cxapi = require('@aws-cdk/cx-api'); +import archiver = require('archiver'); +import fs = require('fs-extra'); +import path = require('path'); +import process = require('process'); +import YAML = require('yaml'); +import yargs = require('yargs'); +import { SDK } from '../api'; +import { AppStacks } from '../api/cxapp/stacks'; +import { print, warning } from '../logging'; +import { Configuration, Settings } from '../settings'; + +export const command = 'package'; +export const describe = 'Package a CDK Application to a Cloud Assembly'; +export const builder: yargs.CommandBuilder = { + out: { + type: 'string', + desc: 'The file to output the Cloud Assembly to', + default: 'assembly.cloud', + } +}; + +export async function handler(argv: yargs.Arguments): Promise { + const configuration = await new Configuration(_argumentsToSettings(argv)).load(); + const appStacks = new AppStacks(argv, configuration, new SDK({ + profile: argv.profile, + proxyAddress: argv.proxy, + ec2creds: argv.ec2creds, + })); + const outFile = path.resolve(process.cwd(), argv.out); + await _createCloudAssembly(await appStacks.synthesizeStacks(), outFile); + print(outFile); + return 0; +} + +function _argumentsToSettings(argv: yargs.Arguments) { + const context: any = {}; + + // Turn list of KEY=VALUE strings into an object + for (const assignment of (argv.context || [])) { + const parts = assignment.split('=', 2); + if (parts.length === 2) { + if (parts[0].match(/^aws:.+/)) { + throw new Error(`User-provided context cannot use keys prefixed with 'aws:', but ${parts[0]} was provided.`); + } + context[parts[0]] = parts[1]; + } else { + warning('Context argument is not an assignment (key=value): %s', assignment); + } + } + + return new Settings({ + app: argv.app, + context, + toolkitStackName: argv.toolkitStackName, + versionReporting: argv.versionReporting, + pathMetadata: argv.pathMetadata, + }); +} + +function _createCloudAssembly(synth: cxapi.SynthesizeResponse, outFile: string): Promise { + return new Promise(async (ok, ko) => { + const writeStream = fs.createWriteStream(outFile); + const archive = archiver('zip', { zlib: { level: 9 } }); + + writeStream.on('close', ok); + archive.on('error', ko); + archive.on('warning', ko); + archive.pipe(writeStream); + + const manifest: cloudAssembly.Manifest = { + schema: 'cloud-assembly/1.0', + drops: {} + }; + + await Promise.all(synth.stacks.map(stack => _makeStackDrop(stack, archive, manifest))); + + archive.append(JSON.stringify(manifest, null, 2), { name: 'manifest.json' }); + + archive.finalize(); + }); +} + +async function _makeStackDrop(stack: cxapi.SynthesizedStack, + archive: archiver.Archiver, + manifest: cloudAssembly.Manifest): Promise { + const name = `${stack.name}/template.yml`; + archive.append(YAML.stringify(stack.template, { schema: 'yaml-1.1' }), { name }); + const parameters = await _processAssets(stack, archive, manifest); + manifest.drops[stack.name] = { + type: 'npm://@aws-cdk/cdk/Stack', + environment: `aws://${stack.environment.account}/${stack.environment.region}`, + properties: { + name: stack.name, + template: name, + parameters + } + }; +} + +async function _processAssets(stack: cxapi.SynthesizedStack, + archive: archiver.Archiver, + manifest: cloudAssembly.Manifest): Promise<{ [name: string]: string }> { + const result: { [name: string]: string } = {}; + for (const key of Object.keys(stack.metadata)) { + const assets = stack.metadata[key].filter(entry => entry.type === cxapi.ASSET_METADATA && entry.data) + .map(entry => entry.data! as cxapi.AssetMetadataEntry); + for (const asset of assets) { + const dropName = `${stack.name}/${asset.id}`; + let name: string; + switch (asset.packaging) { + case 'file': + name = path.join(key, path.basename(asset.path)); + archive.file(asset.path, { name }); + manifest.drops[dropName] = { + type: 'npm://@aws-cdk/cdk/S3Object', + environment: `aws://${stack.environment.account}/${stack.environment.region}`, + properties: { path: name } + }; + result[asset.s3BucketParameter] = '${' + dropName + '.s3Bucket}'; + result[asset.s3KeyParameter] = '${' + dropName + '.s3ObjectKey}'; + break; + case 'zip': + const zip = archiver('zip', { zlib: { level: 9 } }); + name = path.join(key, `${path.basename(asset.path)}.zip`); + archive.append(zip, { name }); + zip.directory(asset.path, ''); + zip.finalize(); + manifest.drops[dropName] = { + type: 'npm://@aws-cdk/cdk/S3Object', + environment: `aws://${stack.environment.account}/${stack.environment.region}`, + properties: { path: name } + }; + result[asset.s3BucketParameter] = '${' + dropName + '.s3Bucket}'; + result[asset.s3KeyParameter] = '${' + dropName + '.s3ObjectKey}'; + break; + case 'container-image': + default: + throw new Error(`Unsupported asset packaging ${asset.packaging} used at ${key}`); + } + } + } + return result; +} diff --git a/packages/aws-cdk/lib/settings.ts b/packages/aws-cdk/lib/settings.ts index 07ff6716c0aaf..1de7d4445d0b0 100644 --- a/packages/aws-cdk/lib/settings.ts +++ b/packages/aws-cdk/lib/settings.ts @@ -25,9 +25,12 @@ export class Configuration { /** * Load all config */ - public async load() { - await this.userConfig.load(PER_USER_DEFAULTS); - await this.projectConfig.load(DEFAULTS); + public async load(): Promise { + await Promise.all([ + this.userConfig.load(PER_USER_DEFAULTS), + this.projectConfig.load(DEFAULTS) + ]); + return this; } /** From 03ad5ae173536d3618c8075641d739948a234835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Fri, 23 Nov 2018 17:50:06 +0100 Subject: [PATCH 13/15] Add code to handle existing asset types, too --- .../cloud-assembly/lib/attestation.ts | 23 ++ packages/@aws-cdk/cloud-assembly/lib/index.ts | 1 + .../@aws-cdk/cloud-assembly/lib/manifest.ts | 2 +- packages/aws-cdk/lib/commands/package.ts | 92 +------- packages/aws-cdk/lib/util/cloud-assembly.ts | 223 ++++++++++++++++++ packages/aws-cdk/package.json | 1 + 6 files changed, 258 insertions(+), 84 deletions(-) create mode 100644 packages/@aws-cdk/cloud-assembly/lib/attestation.ts create mode 100644 packages/aws-cdk/lib/util/cloud-assembly.ts diff --git a/packages/@aws-cdk/cloud-assembly/lib/attestation.ts b/packages/@aws-cdk/cloud-assembly/lib/attestation.ts new file mode 100644 index 0000000000000..0721fe21da32c --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly/lib/attestation.ts @@ -0,0 +1,23 @@ +/** + * The schema for the Attestation document that is used for the Digital Signature of Cloud Assemblies. + */ +export interface Attestation { + /** The hashing algorithm used by this Attestation */ + algorithm: string; + /** The items that this attestation is about */ + items: { [path: string]: FileData }; + /** The base64-encoded nonce used to hash items in the attestation */ + nonce: string; + /** The time at which this attestation was made */ + timestamp: Date; +} + +/** + * Attestation data about a particular file. + */ +export interface FileData { + /** The size of the file, in bytes, expressed as a Base-10 string */ + size: string; + /** The base64-encoded hash of the file contents, salted with the attestation nonce */ + hash: string; +} diff --git a/packages/@aws-cdk/cloud-assembly/lib/index.ts b/packages/@aws-cdk/cloud-assembly/lib/index.ts index 8fe1d654f83a5..5c603357a9002 100644 --- a/packages/@aws-cdk/cloud-assembly/lib/index.ts +++ b/packages/@aws-cdk/cloud-assembly/lib/index.ts @@ -1,2 +1,3 @@ +export * from './attestation'; export * from './manifest'; export * from './validate-manifest'; diff --git a/packages/@aws-cdk/cloud-assembly/lib/manifest.ts b/packages/@aws-cdk/cloud-assembly/lib/manifest.ts index 15b7fc1e181ea..ba434beb422ff 100644 --- a/packages/@aws-cdk/cloud-assembly/lib/manifest.ts +++ b/packages/@aws-cdk/cloud-assembly/lib/manifest.ts @@ -16,7 +16,7 @@ export interface Drop { * @pattern ^[^:]+://.+$ */ environment: string; - metadata?: { [key: string]: Metadata }; + metadata?: { [key: string]: Metadata[] }; properties?: { [name: string]: any } } diff --git a/packages/aws-cdk/lib/commands/package.ts b/packages/aws-cdk/lib/commands/package.ts index 55bacff4ee371..054dbedc08e69 100644 --- a/packages/aws-cdk/lib/commands/package.ts +++ b/packages/aws-cdk/lib/commands/package.ts @@ -1,15 +1,12 @@ -import cloudAssembly = require('@aws-cdk/cloud-assembly'); import cxapi = require('@aws-cdk/cx-api'); -import archiver = require('archiver'); -import fs = require('fs-extra'); import path = require('path'); import process = require('process'); -import YAML = require('yaml'); import yargs = require('yargs'); import { SDK } from '../api'; import { AppStacks } from '../api/cxapp/stacks'; import { print, warning } from '../logging'; import { Configuration, Settings } from '../settings'; +import { CloudAssembly} from '../util/cloud-assembly'; export const command = 'package'; export const describe = 'Package a CDK Application to a Cloud Assembly'; @@ -59,87 +56,16 @@ function _argumentsToSettings(argv: yargs.Arguments) { }); } -function _createCloudAssembly(synth: cxapi.SynthesizeResponse, outFile: string): Promise { +async function _createCloudAssembly(synth: cxapi.SynthesizeResponse, outFile: string): Promise { return new Promise(async (ok, ko) => { - const writeStream = fs.createWriteStream(outFile); - const archive = archiver('zip', { zlib: { level: 9 } }); + const cloudAssembly = new CloudAssembly(outFile); + cloudAssembly.on('close', ok); + cloudAssembly.on('error', ko); - writeStream.on('close', ok); - archive.on('error', ko); - archive.on('warning', ko); - archive.pipe(writeStream); - - const manifest: cloudAssembly.Manifest = { - schema: 'cloud-assembly/1.0', - drops: {} - }; - - await Promise.all(synth.stacks.map(stack => _makeStackDrop(stack, archive, manifest))); - - archive.append(JSON.stringify(manifest, null, 2), { name: 'manifest.json' }); - - archive.finalize(); - }); -} - -async function _makeStackDrop(stack: cxapi.SynthesizedStack, - archive: archiver.Archiver, - manifest: cloudAssembly.Manifest): Promise { - const name = `${stack.name}/template.yml`; - archive.append(YAML.stringify(stack.template, { schema: 'yaml-1.1' }), { name }); - const parameters = await _processAssets(stack, archive, manifest); - manifest.drops[stack.name] = { - type: 'npm://@aws-cdk/cdk/Stack', - environment: `aws://${stack.environment.account}/${stack.environment.region}`, - properties: { - name: stack.name, - template: name, - parameters + for (const stack of synth.stacks) { + cloudAssembly.addStack(stack); } - }; -} -async function _processAssets(stack: cxapi.SynthesizedStack, - archive: archiver.Archiver, - manifest: cloudAssembly.Manifest): Promise<{ [name: string]: string }> { - const result: { [name: string]: string } = {}; - for (const key of Object.keys(stack.metadata)) { - const assets = stack.metadata[key].filter(entry => entry.type === cxapi.ASSET_METADATA && entry.data) - .map(entry => entry.data! as cxapi.AssetMetadataEntry); - for (const asset of assets) { - const dropName = `${stack.name}/${asset.id}`; - let name: string; - switch (asset.packaging) { - case 'file': - name = path.join(key, path.basename(asset.path)); - archive.file(asset.path, { name }); - manifest.drops[dropName] = { - type: 'npm://@aws-cdk/cdk/S3Object', - environment: `aws://${stack.environment.account}/${stack.environment.region}`, - properties: { path: name } - }; - result[asset.s3BucketParameter] = '${' + dropName + '.s3Bucket}'; - result[asset.s3KeyParameter] = '${' + dropName + '.s3ObjectKey}'; - break; - case 'zip': - const zip = archiver('zip', { zlib: { level: 9 } }); - name = path.join(key, `${path.basename(asset.path)}.zip`); - archive.append(zip, { name }); - zip.directory(asset.path, ''); - zip.finalize(); - manifest.drops[dropName] = { - type: 'npm://@aws-cdk/cdk/S3Object', - environment: `aws://${stack.environment.account}/${stack.environment.region}`, - properties: { path: name } - }; - result[asset.s3BucketParameter] = '${' + dropName + '.s3Bucket}'; - result[asset.s3KeyParameter] = '${' + dropName + '.s3ObjectKey}'; - break; - case 'container-image': - default: - throw new Error(`Unsupported asset packaging ${asset.packaging} used at ${key}`); - } - } - } - return result; + cloudAssembly.finalize(); + }); } diff --git a/packages/aws-cdk/lib/util/cloud-assembly.ts b/packages/aws-cdk/lib/util/cloud-assembly.ts new file mode 100644 index 0000000000000..01b96d799a29b --- /dev/null +++ b/packages/aws-cdk/lib/util/cloud-assembly.ts @@ -0,0 +1,223 @@ +import cloudAssembly = require('@aws-cdk/cloud-assembly'); +import cxapi = require('@aws-cdk/cx-api'); +import archiver = require('archiver'); +import crypto = require('crypto'); +import events = require('events'); +import fs = require('fs-extra'); +import path = require('path'); +import stream = require('stream'); +import { promisify } from 'util'; +import { toYAML } from '../serialize'; + +const HASH_ALGORITHM = 'SHA256'; +const HASH_LENGTH = 32; // 256 bites + +// tslint:disable-next-line:no-var-requires kbpgp does not provide typescript typings +const kbpgp = require('kbpgp'); + +export interface CloudAssemblyOptions { + /** + * The private key part of an OpenPGP key in ASCII-armored format. If it is locked, the passphrase must be provided by + * ``privateKeyPassphrase``. + * + * @default no signature + */ + privateKey?: string; + + /** + * The passphrase necessary to unlock the ``privateKey`` if it was provided, or a function that returns the + * passphrase. + * + * @default no passphrase + */ + privateKeyPassphrase?: string | (() => string); +} + +/** + * A utility class to build a Cloud Assembly. + */ +export class CloudAssembly { + private readonly archiver: archiver.Archiver; + private readonly errorEmitter = new events.EventEmitter(); + private readonly fileData: { [path: string]: cloudAssembly.FileData } = {}; + private readonly manifest: cloudAssembly.Manifest = { schema: 'cloud-assembly/1.0', drops: {} }; + private readonly nonce = crypto.randomBytes(HASH_LENGTH); + private readonly writer: fs.WriteStream; + + /** + * Creates a new Cloud Assembly. + * + * @param file the path at which the Cloud Assembly will be created. + */ + constructor(file: string, private readonly options: CloudAssemblyOptions = {}) { + this.writer = fs.createWriteStream(file); + this.archiver = archiver('zip', { zlib: { level: 9 } }); + this.archiver.on('warning', err => this.errorEmitter.emit('error', err)); + this.archiver.on('error', err => this.errorEmitter.emit('error', err)); + this.archiver.pipe(this.writer); + } + + /** + * The ``close`` event is fired when the CloudAssembly has been written to disk. + * @param event ``'close'``. + * @param listener the event handler. + * @returns this + */ + public on(event: 'close', listener: () => void): this; + /** + * The ``error`` handler is fired when an error occurs when preparing the Cloud Assembly. + * @param event ``'error'``. + * @param listener the event handler. + * @returns this + */ + public on(event: 'error', listener: (error: Error) => void): this; + public on(event: 'close' | 'error', listener: (error: Error) => void): this { + if (event === 'close') { + this.writer.on(event, listener); + } else { + this.errorEmitter.on(event, listener); + } + return this; + } + + public addStack(stack: cxapi.SynthesizedStack): this { + const stackMetadata: { [key: string]: cloudAssembly.Metadata[] } = {}; + const parameters: { [name: string]: string } = {}; + for (const key of Object.keys(stack.metadata)) { + for (const metadata of stack.metadata[key]) { + if (metadata.type === cxapi.ASSET_METADATA) { + const asset = metadata.data! as cxapi.AssetMetadataEntry; + this._registerAsset(stack, asset, parameters); + } else { + stackMetadata[key] = stackMetadata[key] || []; + stackMetadata[key].push({ kind: metadata.type, value: { data: metadata.data, trace: metadata.trace } }); + } + } + } + + const templatePath = `${stack.name}/template.yml`; + this._addFile(templatePath, toYAML(stack.template)).catch(err => this.errorEmitter.emit('error', err)); + this.manifest.drops[stack.name] = { + type: 'npm://aws-cdk/cloudformation-stack', + environment: _stackEnvironment(stack), + metadata: stackMetadata, + properties: { + templatePath, + parameters + } + }; + return this; + } + + /** + * Finalizes this Cloud Assembly. + */ + public finalize(): void { + const manifest = JSON.stringify(this.manifest, null, 2); + this._addFile('manifest.json', manifest) + .then(() => { + if (!this.options.privateKey) { + return this.archiver.finalize(); + } + const attestation: cloudAssembly.Attestation = { + algorithm: HASH_ALGORITHM, + items: this.fileData, + nonce: this.nonce.toString('base64'), + timestamp: new Date() + }; + const stringToSign = JSON.stringify(attestation, null, 2); + _pgpSign(stringToSign, this.options.privateKey, this.options.privateKeyPassphrase) + .then(signature => { + this.archiver.append(signature, { name: 'signature.asc' }); + this.archiver.finalize(); + }) + .catch(e => this.errorEmitter.emit('error', e)); + }) + .catch(e => this.errorEmitter.emit('error', e)); + } + + private _addFile(name: string, content: Buffer | stream.Readable | string): Promise { + return new Promise((ok, ko) => { + if (typeof content === 'string' || Buffer.isBuffer(content)) { + const data = content; + content = new stream.Readable(); + content.push(data); + content.push(null /* no more data */); + } + const hash = crypto.createHash(HASH_ALGORITHM); + let size = 0; + const hashingStream = new stream.Transform({ + transform(this: stream.Transform, chunk: any, encoding: string, callback: stream.TransformCallback): void { + try { + chunk = Buffer.from(chunk); + size += chunk.byteLength; + if (size > Number.MAX_SAFE_INTEGER) { + throw new Error(`Attempted to add an object that is too large for Javascript (max size: ${Number.MAX_SAFE_INTEGER} bytes)`); + } + hash.update(chunk); + this.push(chunk, encoding); + callback(); + } catch (e) { + callback(e); + } + } + }); + hashingStream.on('error', ko); + hashingStream.on('end', () => { + if (name in this.fileData) { + ko(new Error(`A file was already added at path ${name}`)); + } + this.fileData[name] = { + hash: hash.update(this.nonce).digest('base64'), + size: size.toString(10) + }; + ok(); + }); + this.archiver.append(content.pipe(hashingStream), { name }); + }); + } + + private _registerAsset(stack: cxapi.SynthesizedStack, asset: cxapi.AssetMetadataEntry, parameters: { [name: string]: string }): void { + const filePath = `${stack.name}/assets/${asset.id}/${path.basename(asset.path)}`; + const dropName = `${stack.name}/${asset.id}`; + switch (asset.packaging) { + case 'file': + this._addFile(filePath, fs.createReadStream(asset.path)).catch(e => { throw e; }); + this.manifest.drops[dropName] = { + type: 'npm://aws-cdk/s3-object', + environment: _stackEnvironment(stack), + properties: { filePath }, + }; + parameters[asset.s3BucketParameter] = `\${${dropName}.s3BucketName}`; + parameters[asset.s3KeyParameter] = `\${${dropName}.s3ObjectKey}`; + break; + case 'zip': + const zip = archiver('zip', { zlib: { level: 9 } }).directory(asset.path, ''); + this._addFile(`${filePath}.zip`, zip).catch(e => { throw e; }); + zip.finalize(); + this.manifest.drops[dropName] = { + type: 'npm://aws-cdk/s3-object', + environment: _stackEnvironment(stack), + properties: { filePath: `${filePath}.zip` } + }; + parameters[asset.s3BucketParameter] = `\${${dropName}.s3BucketName}`; + parameters[asset.s3KeyParameter] = `\${${dropName}.s3ObjectKey}`; + break; + case 'container-image': + default: + throw new Error(`Unsupported asset packaging: ${asset.packaging}`); + } + } +} + +async function _pgpSign(msg: string, privateKey: string, passphrase?: string | (() => string)): Promise { + const keyManager = await promisify(kbpgp.KeyManager.import_from_armored_pgp)({ armored: privateKey }); + if (keyManager.is_pgp_locked()) { + await promisify(keyManager.unlock_pgp).call(keyManager, { passphrase }); + } + return await promisify(kbpgp.clearsign)({ msg, signing_key: keyManager.find_signing_pgp_key() }); +} + +function _stackEnvironment(stack: cxapi.SynthesizedStack): string { + return `aws://${stack.environment.account}/${stack.environment.region}`; +} diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 18469fc8f3bf7..396ff4bba8d81 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -57,6 +57,7 @@ "decamelize": "^2.0.0", "fs-extra": "^7.0.0", "json-diff": "^0.3.1", + "kbpgp": "^2.0.82", "minimatch": ">=3.0", "promptly": "^0.2.0", "proxy-agent": "^3.0.1", From 606662c5a5a21b979005dfd31c6cec73a5d1d8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Mon, 26 Nov 2018 14:13:03 +0100 Subject: [PATCH 14/15] Rename drops to droplets --- .../@aws-cdk/cloud-assembly/lib/manifest.ts | 4 +- .../cloud-assembly/lib/validate-manifest.ts | 10 +-- .../test/test.validate-manifest.ts | 39 +++++---- packages/aws-cdk/lib/util/cloud-assembly.ts | 8 +- specifications/cloud_assembly.md | 80 +++++++++---------- 5 files changed, 73 insertions(+), 68 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly/lib/manifest.ts b/packages/@aws-cdk/cloud-assembly/lib/manifest.ts index ba434beb422ff..2c476443eb1dd 100644 --- a/packages/@aws-cdk/cloud-assembly/lib/manifest.ts +++ b/packages/@aws-cdk/cloud-assembly/lib/manifest.ts @@ -2,11 +2,11 @@ export const MANIFEST_FILE_NAME = 'manifest.json'; export interface Manifest { schema: 'cloud-assembly/1.0'; - drops: { [logicalId: string]: Drop }; + droplets: { [logicalId: string]: Droplet }; missing?: { [key: string]: Missing }; } -export interface Drop { +export interface Droplet { dependsOn?: string[]; /** * @pattern ^[^:]+://.+$ diff --git a/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.ts b/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.ts index 4b613e2047017..5ea86ac7b94c9 100644 --- a/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.ts +++ b/packages/@aws-cdk/cloud-assembly/lib/validate-manifest.ts @@ -1,5 +1,5 @@ import jsonschema = require('jsonschema'); -import { Drop, Manifest } from './manifest'; +import { Droplet, Manifest } from './manifest'; // tslint:disable-next-line:no-var-requires export const schema: jsonschema.Schema = require('../schema/manifest.schema.json'); @@ -25,12 +25,12 @@ export function validateManifest(obj: unknown): Manifest { function validateSemantics(manifest: Manifest): Manifest { const dependencyGraph: { [id: string]: Reference[] } = {}; - for (const logicalId of Object.keys(manifest.drops)) { + for (const logicalId of Object.keys(manifest.droplets)) { assertValidLogicalId(logicalId); - const drop = manifest.drops[logicalId]; + const drop = manifest.droplets[logicalId]; const references = dependencyGraph[logicalId] = listReferences(drop, logicalId); for (const ref of references) { - if (!(ref.logicalId in manifest.drops)) { + if (!(ref.logicalId in manifest.droplets)) { throw new Error(`${logicalId} depends on undefined drop through ${ref.context}.`); } } @@ -78,7 +78,7 @@ function assertValidLogicalId(str: string): void { } } -function listReferences(drop: Drop, dropId: string): Reference[] { +function listReferences(drop: Droplet, dropId: string): Reference[] { const result = new Array(); for (const logicalId of drop.dependsOn || []) { result.push({ logicalId, context: `dependsOn ${logicalId}` }); diff --git a/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.ts b/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.ts index ad56b209fb461..ab2faa4b07385 100644 --- a/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.ts +++ b/packages/@aws-cdk/cloud-assembly/test/test.validate-manifest.ts @@ -1,5 +1,5 @@ import nodeunit = require('nodeunit'); -import { validateManifest } from '../lib'; +import { Manifest, validateManifest } from '../lib'; export = nodeunit.testCase({ validateManifest: { @@ -8,43 +8,43 @@ export = nodeunit.testCase({ test.done(); }, 'rejects a document where the schema is invalid'(test: nodeunit.Test) { - const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); - badManifest.schema = 'foo/1.0-bar'; + const badManifest = _clone(SAMPLE_MANIFEST); + (badManifest as any).schema = 'foo/1.0-bar'; test.throws(() => validateManifest(badManifest), /instance\.schema is not one of enum values/); test.done(); }, - 'rejects a document without drops'(test: nodeunit.Test) { - const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); - delete badManifest.drops; + 'rejects a document without droplets'(test: nodeunit.Test) { + const badManifest = _clone(SAMPLE_MANIFEST); + delete badManifest.droplets; test.throws(() => validateManifest(badManifest), - /instance requires property "drops"/); + /instance requires property "droplets"/); test.done(); }, 'rejects a document with an illegal Logical ID'(test: nodeunit.Test) { - const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); - badManifest.drops['Pipeline.Stack'] = badManifest.drops.PipelineStack; + const badManifest = _clone(SAMPLE_MANIFEST); + badManifest.droplets['Pipeline.Stack'] = badManifest.droplets.PipelineStack; test.throws(() => validateManifest(badManifest), /Invalid logical ID: Pipeline\.Stack/); test.done(); }, 'rejects a document with unresolved dependsOn'(test: nodeunit.Test) { - const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); - badManifest.drops.PipelineStack.dependsOn = ['DoesNotExist']; + const badManifest = _clone(SAMPLE_MANIFEST); + badManifest.droplets.PipelineStack.dependsOn = ['DoesNotExist']; test.throws(() => validateManifest(badManifest), /PipelineStack depends on undefined drop through dependsOn DoesNotExist/); test.done(); }, 'rejects a document with direct circular dependency via dependsOn'(test: nodeunit.Test) { - const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); - badManifest.drops.PipelineStack.dependsOn = ['PipelineStack']; + const badManifest = _clone(SAMPLE_MANIFEST); + badManifest.droplets.PipelineStack.dependsOn = ['PipelineStack']; test.throws(() => validateManifest(badManifest), /PipelineStack => dependsOn PipelineStack/); test.done(); }, 'rejects a document with indirect circular dependency'(test: nodeunit.Test) { - const badManifest = JSON.parse(JSON.stringify(SAMPLE_MANIFEST)); - badManifest.drops.StaticFiles.dependsOn = ['ServiceStack-beta']; + const badManifest = _clone(SAMPLE_MANIFEST); + badManifest.droplets.StaticFiles.dependsOn = ['ServiceStack-beta']; test.throws(() => validateManifest(badManifest), // tslint:disable-next-line:max-line-length /StaticFiles => dependsOn ServiceStack-beta => ServiceStack-beta\.properties\.parameters\.websiteFilesKeyPrefix "\${StaticFiles\.keyPrefix}"/); @@ -53,9 +53,10 @@ export = nodeunit.testCase({ } }); -const SAMPLE_MANIFEST = { +/* Don't assume this has meaning beyond being a syntactically valid manifest document */ +const SAMPLE_MANIFEST: Manifest = { schema: "cloud-assembly/1.0", - drops: { + droplets: { "PipelineStack": { type: "npm://@aws-cdk/aws-cloudformation.StackDrop", environment: "aws://123456789012/eu-west-1", @@ -107,3 +108,7 @@ const SAMPLE_MANIFEST = { } } }; + +function _clone(value: T): T { + return JSON.parse(JSON.stringify(value)); +} diff --git a/packages/aws-cdk/lib/util/cloud-assembly.ts b/packages/aws-cdk/lib/util/cloud-assembly.ts index 01b96d799a29b..82f5a72a03032 100644 --- a/packages/aws-cdk/lib/util/cloud-assembly.ts +++ b/packages/aws-cdk/lib/util/cloud-assembly.ts @@ -40,7 +40,7 @@ export class CloudAssembly { private readonly archiver: archiver.Archiver; private readonly errorEmitter = new events.EventEmitter(); private readonly fileData: { [path: string]: cloudAssembly.FileData } = {}; - private readonly manifest: cloudAssembly.Manifest = { schema: 'cloud-assembly/1.0', drops: {} }; + private readonly manifest: cloudAssembly.Manifest = { schema: 'cloud-assembly/1.0', droplets: {} }; private readonly nonce = crypto.randomBytes(HASH_LENGTH); private readonly writer: fs.WriteStream; @@ -97,7 +97,7 @@ export class CloudAssembly { const templatePath = `${stack.name}/template.yml`; this._addFile(templatePath, toYAML(stack.template)).catch(err => this.errorEmitter.emit('error', err)); - this.manifest.drops[stack.name] = { + this.manifest.droplets[stack.name] = { type: 'npm://aws-cdk/cloudformation-stack', environment: _stackEnvironment(stack), metadata: stackMetadata, @@ -183,7 +183,7 @@ export class CloudAssembly { switch (asset.packaging) { case 'file': this._addFile(filePath, fs.createReadStream(asset.path)).catch(e => { throw e; }); - this.manifest.drops[dropName] = { + this.manifest.droplets[dropName] = { type: 'npm://aws-cdk/s3-object', environment: _stackEnvironment(stack), properties: { filePath }, @@ -195,7 +195,7 @@ export class CloudAssembly { const zip = archiver('zip', { zlib: { level: 9 } }).directory(asset.path, ''); this._addFile(`${filePath}.zip`, zip).catch(e => { throw e; }); zip.finalize(); - this.manifest.drops[dropName] = { + this.manifest.droplets[dropName] = { type: 'npm://aws-cdk/s3-object', environment: _stackEnvironment(stack), properties: { filePath: `${filePath}.zip` } diff --git a/specifications/cloud_assembly.md b/specifications/cloud_assembly.md index 467d0c8e2dc24..9a1c4d5f96586 100644 --- a/specifications/cloud_assembly.md +++ b/specifications/cloud_assembly.md @@ -34,7 +34,7 @@ a single `object` that conforms to the following schema: Key |Type |Required|Description --------------|---------------------|:------:|----------- `schema` |`string` |Required|The schema for the document. **MUST** be `cloud-assembly/1.0`. -`drops` |`Map` |Required|A mapping of [*Logical ID*](#logical-id) to [Drop](#drop). +`droplets` |`Map` |Required|A mapping of [*Logical ID*](#logical-id) to [Droplet](#droplet). `missing` |`Map`| |A mapping of context keys to [missing information](#missing). The [JSON] specification allows for keys to be specified multiple times in a given `object`. However, *Cloud Assembly* @@ -42,7 +42,7 @@ consumers **MAY** assume keys are unique, and *Cloud Assemblers* **SHOULD** avoi duplicate keys are present and the manifest parser permits it, the latest specified value **SHOULD** be preferred. ### Logical ID -*Logical IDs* are `string`s that uniquely identify [Drop](#drop)s in the context of a *Cloud Assembly*. +*Logical IDs* are `string`s that uniquely identify [Droplet](#droplet)s in the context of a *Cloud Assembly*. * A *Logical ID* **MUST NOT** be empty. * A *Logical ID* **SHOULD NOT** exceed `256` characters. * A *Logical ID* **MUST** be composed of only the following ASCII printable characters: @@ -54,36 +54,36 @@ duplicate keys are present and the manifest parser permits it, the latest specif + Forward-slash: `/` (`0x2F`) + Underscore: `_` (`0x5F`) * A *Logical ID* **MUST NOT** contain the `.` (`0x2E`) character as it is used in the string substitution pattern for - cross-drop references to separate the *Logical ID* from the *attribute* name. + cross-droplet references to separate the *Logical ID* from the *attribute* name. In other words, *Logical IDs* are expected to match the following regular expression: ```js /^[A-Za-z0-9+\/_-]{1,256}$/ ``` -### Drop -Clouds are made of Drops. Thet are the building blocks of *Cloud Assemblies*. They model a part of the -*cloud application* that can be deployed independently, provided its dependencies are fulfilled. Drops are specified +### Droplet +Clouds are made of Droplets. Thet are the building blocks of *Cloud Assemblies*. They model a part of the +*cloud application* that can be deployed independently, provided its dependencies are fulfilled. Droplets are specified using [JSON] objects that **MUST** conform to the following schema: Key |Type |Required|Description -------------|----------------------|:------:|----------- -`type` |`string` |Required|The [*Drop Type*](#drop-type) specifier of this Drop. -`environment`|`string` |required|The target [environment](#environment) in which Drop is deployed. -`dependsOn` |`string[]` | |*Logical IDs* of other Drops that must be deployed before this one. -`metadata` |`Map`| |Arbitrary named [metadata](#metadata) associated with this Drop. -`properties` |`Map` | |The properties of this Drop as documented by its maintainers. +`type` |`string` |Required|The [*Droplet Type*](#droplet-type) specifier of this Droplet. +`environment`|`string` |required|The target [environment](#environment) in which Droplet is deployed. +`dependsOn` |`string[]` | |*Logical IDs* of other Droplets that must be deployed before this one. +`metadata` |`Map`| |Arbitrary named [metadata](#metadata) associated with this Droplet. +`properties` |`Map` | |The properties of this Droplet as documented by its maintainers. -Each [Drop Type](#drop-type) can produce output strings that allow Drops to provide informations that other Drops can -use when composing the *cloud application*. Each Drop implementer is responsible to document the output attributes it -supports. References to these outputs are modeled using special `string` tokens within entries of the `properties` -section of Drops: +Each [Droplet Type](#droplet-type) can produce output strings that allow Droplets to provide informations that other +[Droplets](#droplet) can use when composing the *cloud application*. Each Droplet implementer is responsible to document +the output attributes it supports. References to these outputs are modeled using special `string` tokens within entries +of the `properties` section of Droplets: ``` ${LogicalId.attributeName} ╰───┬───╯ ╰─────┬─────╯ │ └─ The name of the output attribute - └───────────── The Logical ID of the Drop + └───────────── The Logical ID of the Droplet ``` The following escape sequences are valid: @@ -93,13 +93,13 @@ The following escape sequences are valid: Deployment systems **SHOULD** return an error upon encountering an occurrence of the `\` literal that is not part of a valid escape sequence. -Drops **MUST NOT** cause circular dependencies. Deployment systems **SHOULD** detect cycles and fail upon discovering +Droplets **MUST NOT** cause circular dependencies. Deployment systems **SHOULD** detect cycles and fail upon discovering one. -#### Drop Type -Every Drop has a type specifier, which allows *Cloud Assembly* consumers to know how to deploy it. The type specifiers -are `string`s that use an URI-like syntax (`protocol://path`), providing the coordinates to a reference implementation -for the Drop behavior. +#### Droplet Type +Every Droplet has a type specifier, which allows *Cloud Assembly* consumers to know how to deploy it. The type +specifiers are `string`s that use an URI-like syntax (`protocol://path`), providing the coordinates to a reference +implementation for the Droplet behavior. Deployment systems **MUST** support at least one protocol, and **SHOULD** support all the protocols specified in the following sub-sections. @@ -117,8 +117,8 @@ npm://[@namespace/]package/ClassName[@version] ``` #### Environment -Environments help Deployment systems determine where to deploy a particular Drop. They are referenced by `string`s that -use an URI-like syntax (`protocol://path`). +Environments help Deployment systems determine where to deploy a particular Droplet. They are referenced by `string`s +that use an URI-like syntax (`protocol://path`). Deployment systems **MUST** support at least one protocol, and **SHOULD** support all the protocols specified in the following sub-sections. @@ -134,7 +134,7 @@ aws://account/region ``` ### Metadata -Metadata can be attached to [Drops](#drop) to allow tools that work with *Cloud Assemblies* to share additional +Metadata can be attached to [Droplets](#droplet) to allow tools that work with *Cloud Assemblies* to share additional information about the *cloud application*. Metadata **SHOULD NOT** be used to convey data that is necessary for correctly process the *Cloud Assembly*, since any tool that consumes a *Cloud Assembly* **MAY** choose to ignore any or all Metadata. @@ -147,12 +147,12 @@ Key |Type |Required|Description A common use-case for Metadata is reporting warning or error messages that were emitted during the creation of the *Cloud Assembly*, so that deployment systems can present this information to users or logs. Warning and error messages **SHOULD** set the `kind` field to `warning` and `error` respectively, and the `value` field **SHOULD** contain a single -`string`. Deployment systems **MAY** reject *Cloud Assemblies* that include [Drops](#drop) that carry one or more +`string`. Deployment systems **MAY** reject *Cloud Assemblies* that include [Droplets](#droplet) that carry one or more `error` Metadata entries, and they **SHOULD** surface `warning` messages, either directly through their user interface, or in the execution log. ### Missing -[Drops](#drop) may require contextual information to be available in order to correctly participate in a +[Droplets](#droplet) may require contextual information to be available in order to correctly participate in a *Cloud Assembly*. When information is missing, *Cloud Assembly* producers report the missing information by adding entries to the `missing` section of the [manifest document](#manifest-document). The values are [JSON] `object`s that **MUST** conform to the following schema: @@ -236,10 +236,10 @@ Deployment systems that support verifying signed *Cloud Assemblies*: * **SHOULD** allow configuration of a list of trusted [PGP][RFC 4880] keys. ## Annex -### Examples of Drops for the AWS Cloud -The Drop specifications provided in this section are for illustration purpose only. +### Examples of Droplets for the AWS Cloud +The Droplet specifications provided in this section are for illustration purpose only. -#### `@aws-cdk/aws-cloudformation.StackDrop` +#### `@aws-cdk/aws-cloudformation.StackDroplet` A [*CloudFormation* stack][CFN Stack]. ##### Properties @@ -259,7 +259,7 @@ Attribute |Type |Description ##### Example ```json { - "type": "npm://@aws-cdk/aws-cloudformation.StackDrop", + "type": "npm://@aws-cdk/aws-cloudformation.StackDroplet", "environment": "aws://000000000000/bermuda-triangle-1", "properties": { "template": "my-stack/template.yml", @@ -276,7 +276,7 @@ Attribute |Type |Description [CFN Stack Policy]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html [CFN Outputs]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html -#### `@aws-cdk/assets.FileDrop` +#### `@aws-cdk/assets.FileDroplet` A file that needs to be uploaded to an *S3* bucket. ##### Properties @@ -295,7 +295,7 @@ Attribute |Type |Description ##### Example ```json { - "type": "npm://@aws-cdk/assets.FileDrop", + "type": "npm://@aws-cdk/assets.FileDroplet", "environment": "aws://000000000000/bermuda-triangle-1", "properties": { "file": "assets/file.bin", @@ -305,7 +305,7 @@ Attribute |Type |Description } ``` -#### `@aws-cdk/aws-ecr.DockerImageDrop` +#### `@aws-cdk/aws-ecr.DockerImageDroplet` A Docker image to be published to an *ECR* registry. ##### Properties @@ -324,7 +324,7 @@ Attribute |Type |Description ##### Example ```json { - "type": "npm://@aws-cdk/aws-ecr.DockerImageDrop", + "type": "npm://@aws-cdk/aws-ecr.DockerImageDroplet", "environment": "aws://000000000000/bermuda-triangle-1", "properties": { "savedImage": "docker/37e6de0b24fa.tar", @@ -358,16 +358,16 @@ Here is an example the contents of a complete *Cloud Assembly* that deploys AWS ```json { "schema": "cloud-assembly/1.0", - "drops": { + "droplets": { "PipelineStack": { - "type": "npm://@aws-cdk/aws-cloudformation.StackDrop", + "type": "npm://@aws-cdk/aws-cloudformation.StackDroplet", "environment": "aws://123456789012/eu-west-1", "properties": { "template": "stacks/PipelineStack.yml" } }, "ServiceStack-beta": { - "type": "npm://@aws-cdk/aws-cloudformation.StackDrop", + "type": "npm://@aws-cdk/aws-cloudformation.StackDroplet", "environment": "aws://123456789012/eu-west-1", "properties": { "template": "stacks/ServiceStack-beta.yml", @@ -380,7 +380,7 @@ Here is an example the contents of a complete *Cloud Assembly* that deploys AWS } }, "ServiceStack-prod": { - "type": "npm://@aws-cdk/aws-cloudformation.StackDrop", + "type": "npm://@aws-cdk/aws-cloudformation.StackDroplet", "environment": "aws://123456789012/eu-west-1", "properties": { "template": "stacks/ServiceStack-prod.yml", @@ -393,7 +393,7 @@ Here is an example the contents of a complete *Cloud Assembly* that deploys AWS } }, "DockerImage": { - "type": "npm://@aws-cdk/aws-ecr.DockerImageDrop", + "type": "npm://@aws-cdk/aws-ecr.DockerImageDroplet", "environment": "aws://123456789012/eu-west-1", "properties": { "savedImage": "docker/docker-image.tar", @@ -401,7 +401,7 @@ Here is an example the contents of a complete *Cloud Assembly* that deploys AWS } }, "StaticFiles": { - "type": "npm://@aws-cdk/assets.DirectoryDrop", + "type": "npm://@aws-cdk/assets.DirectoryDroplet", "environment": "aws://123456789012/eu-west-1", "properties": { "directory": "assets/static-website", From 173764f7d754da764ca22d4232cdf09effe3e9c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Mon, 26 Nov 2018 14:36:54 +0100 Subject: [PATCH 15/15] Handle missing data --- packages/aws-cdk/lib/util/cloud-assembly.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/util/cloud-assembly.ts b/packages/aws-cdk/lib/util/cloud-assembly.ts index 82f5a72a03032..7cc54631bdf3f 100644 --- a/packages/aws-cdk/lib/util/cloud-assembly.ts +++ b/packages/aws-cdk/lib/util/cloud-assembly.ts @@ -90,7 +90,7 @@ export class CloudAssembly { this._registerAsset(stack, asset, parameters); } else { stackMetadata[key] = stackMetadata[key] || []; - stackMetadata[key].push({ kind: metadata.type, value: { data: metadata.data, trace: metadata.trace } }); + stackMetadata[key].push({ kind: metadata.type, value: { data: metadata.data, trace: metadata.trace } }); } } } @@ -106,6 +106,13 @@ export class CloudAssembly { parameters } }; + + for (const key of Object.keys(stack.missing || {})) { + const missingData = stack.missing![key]; + this.manifest.missing = this.manifest.missing || {}; + this.manifest.missing[key] = missingData; + } + return this; }