Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/@aws-cdk/assets/lib/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export class Asset extends cdk.Construct {
// for tooling to be able to package and upload a directory to the
// s3 bucket and plug in the bucket name and key in the correct
// parameters.
const asset: cxapi.AssetMetadataEntry = {
const asset: cxapi.FileAssetMetadataEntry = {
path: this.assetPath,
id: this.uniqueId,
packaging: props.packaging,
Expand Down
42 changes: 36 additions & 6 deletions packages/@aws-cdk/cx-api/lib/cxapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,13 @@ export const DEFAULT_ACCOUNT_CONTEXT_KEY = 'aws:cdk:toolkit:default-account';
export const DEFAULT_REGION_CONTEXT_KEY = 'aws:cdk:toolkit:default-region';

export const ASSET_METADATA = 'aws:cdk:asset';
export interface AssetMetadataEntry {

export interface FileAssetMetadataEntry {
/**
* Requested packaging style
*/
packaging: 'zip' | 'file';

/**
* Path on disk to the asset
*/
Expand All @@ -90,11 +96,6 @@ export interface AssetMetadataEntry {
*/
id: string;

/**
* Requested packaging style
*/
packaging: 'zip' | 'file';

/**
* Name of parameter where S3 bucket should be passed in
*/
Expand All @@ -106,6 +107,35 @@ export interface AssetMetadataEntry {
s3KeyParameter: string;
}

export interface ContainerImageAssetMetadataEntry {
/**
* Type of asset
*/
packaging: 'container-image';

/**
* Path on disk to the asset
*/
path: string;

/**
* Logical identifier for the asset
*/
id: string;

/**
* Name of the parameter that takes the repository name
*/
repositoryParameter: string;

/**
* Name of the parameter that takes the tag
*/
tagParameter: string;
}

export type AssetMetadataEntry = FileAssetMetadataEntry | ContainerImageAssetMetadataEntry;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How's jsii dealing with this? Poorly, I think :(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Doesn't actually matter as no user ever needs to call this (I hope).

@eladb, why don't we bundle @aws-cdk/cx-api as part of @aws-cdk/cdk?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we we need to bundle?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it's an implementation detail that there's a shared package with definitions between aws-cdk and @aws-cdk/cdk. Nobody else should need to see or interact with it. Therefore it does not need to be exposed over jsii.

As a practical matter, not exposing over jsii means we can safely use TypeScript-isms that otherwise wouldn't be allowed.


/**
* Metadata key used to print INFO-level messages by the toolkit when an app is syntheized.
*/
Expand Down
100 changes: 99 additions & 1 deletion packages/aws-cdk/lib/api/toolkit-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ export interface Uploaded {
}

export class ToolkitInfo {
public readonly sdk: SDK;

constructor(private readonly props: {
sdk: SDK,
bucketName: string,
bucketEndpoint: string,
environment: cxapi.Environment
}) { }
}) {
}

public get bucketUrl() {
return `https://${this.props.bucketEndpoint}`;
Expand Down Expand Up @@ -73,6 +76,86 @@ export class ToolkitInfo {
return { filename, key, changed: true };
}

/**
* Prepare an ECR repository for uploading to using Docker
*/
public async prepareEcrRepository(id: string, imageTag: string): Promise<EcrRepositoryInfo> {
const ecr = await this.props.sdk.ecr(this.props.environment, Mode.ForWriting);

// Create the repository if it doesn't exist yet
const repositoryName = 'cdk/' + id.replace(/[:/]/g, '-').toLowerCase();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it really make sense to use a cdk/ prefix? My intuition is that the repository name should be something the user controls, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm treating this akin to the Asset bucket. Why should the user not be allowed to care about the asset bucket name, but be allowed to care about repository names?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay- I agree


let repository;
try {
debug(`${repositoryName}: checking for repository.`);
const describeResponse = await ecr.describeRepositories({ repositoryNames: [repositoryName] }).promise();
repository = describeResponse.repositories![0];
} catch (e) {
if (e.code !== 'RepositoryNotFoundException') { throw e; }
}

if (repository) {
try {
debug(`${repositoryName}: checking for image ${imageTag}`);
await ecr.describeImages({ repositoryName, imageIds: [{ imageTag }] }).promise();

// If we got here, the image already exists. Nothing else needs to be done.
return {
alreadyExists: true,
repositoryUri: repository.repositoryUri!,
repositoryArn: repository.repositoryArn!,
};
} catch (e) {
if (e.code !== 'ImageNotFoundException') { throw e; }
}
} else {
debug(`${repositoryName}: creating`);
const response = await ecr.createRepository({ repositoryName }).promise();
repository = response.repository!;

// Better put a lifecycle policy on this so as to not cost too much money
await ecr.putLifecyclePolicy({
repositoryName,
lifecyclePolicyText: JSON.stringify(DEFAULT_REPO_LIFECYCLE)
}).promise();
}

// The repo exists, image just needs to be uploaded. Get auth to do so.

debug(`Fetching ECR authorization token`);
const authData = (await ecr.getAuthorizationToken({ }).promise()).authorizationData || [];
if (authData.length === 0) {
throw new Error('No authorization data received from ECR');
}
const token = Buffer.from(authData[0].authorizationToken!, 'base64').toString('ascii');
const [username, password] = token.split(':');

return {
alreadyExists: false,
repositoryUri: repository.repositoryUri!,
repositoryArn: repository.repositoryArn!,
username,
password,
endpoint: authData[0].proxyEndpoint!,
};
}
}

export type EcrRepositoryInfo = CompleteEcrRepositoryInfo | UploadableEcrRepositoryInfo;

export interface CompleteEcrRepositoryInfo {
repositoryUri: string;
repositoryArn: string;
alreadyExists: true;
}

export interface UploadableEcrRepositoryInfo {
repositoryUri: string;
repositoryArn: string;
alreadyExists: false;
username: string;
password: string;
endpoint: string;
}

async function objectExists(s3: aws.S3, bucket: string, key: string) {
Expand Down Expand Up @@ -114,3 +197,18 @@ function getOutputValue(stack: aws.CloudFormation.Stack, output: string): string
}
return result;
}

const DEFAULT_REPO_LIFECYCLE = {
rules: [
{
rulePriority: 100,
description: 'Retain only 5 images',
selection: {
tagStatus: 'any',
countType: 'imageCountMoreThan',
countNumber: 5,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems excessively low to me. Especially since it is not user-customizable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it should be like 3 months or something

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what scenario will those old images be used though? Every repository is tied to a single stack in a single region, and once the stack is updated all references will point to the new image, which immediately follows deployment.

I can see retaining two images for quick rollback purposes, and I put a little more margin on there "just because" that feels safe.

But 3 months? Don't forget that these images cost money as well. We already have a user starting to complain about the size of their asset bucket.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep in mind, we can't tell if these images are currently deployed somewhere. So if 5 builds happen that fail testing and never get deployed, my production image will be deleted. Time-based policy means I'm banking on deploying at least one a quarter. I would actually say 6 months is safer...

},
action: { type: 'expire' }
}
]
};
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/api/util/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ export class SDK {
});
}

public async ecr(environment: Environment, mode: Mode): Promise<AWS.ECR> {
return new AWS.ECR({
region: environment.region,
credentials: await this.credentialsCache.get(environment.account, mode)
});
}

public async defaultRegion(): Promise<string | undefined> {
return await getCLICompatibleDefaultRegion(this.profile);
}
Expand Down
13 changes: 9 additions & 4 deletions packages/aws-cdk/lib/assets.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ASSET_METADATA, ASSET_PREFIX_SEPARATOR, AssetMetadataEntry, StackMetadata, SynthesizedStack } from '@aws-cdk/cx-api';
// tslint:disable-next-line:max-line-length
import { ASSET_METADATA, ASSET_PREFIX_SEPARATOR, AssetMetadataEntry, FileAssetMetadataEntry, StackMetadata, SynthesizedStack } from '@aws-cdk/cx-api';
import { CloudFormation } from 'aws-sdk';
import fs = require('fs-extra');
import os = require('os');
import path = require('path');
import { ToolkitInfo } from './api/toolkit-info';
import { zipDirectory } from './archive';
import { prepareContainerAsset } from './docker';
import { debug, success } from './logging';

export async function prepareAssets(stack: SynthesizedStack, toolkitInfo?: ToolkitInfo): Promise<CloudFormation.Parameter[]> {
Expand Down Expand Up @@ -35,12 +37,15 @@ async function prepareAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitInfo)
return await prepareZipAsset(asset, toolkitInfo);
case 'file':
return await prepareFileAsset(asset, toolkitInfo);
case 'container-image':
return await prepareContainerAsset(asset, toolkitInfo);
default:
throw new Error(`Unsupported packaging type: ${asset.packaging}`);
// tslint:disable-next-line:max-line-length
throw new Error(`Unsupported packaging type: ${(asset as any).packaging}. You might need to upgrade your aws-cdk toolkit to support this asset type.`);
}
}

async function prepareZipAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitInfo): Promise<CloudFormation.Parameter[]> {
async function prepareZipAsset(asset: FileAssetMetadataEntry, toolkitInfo: ToolkitInfo): Promise<CloudFormation.Parameter[]> {
debug('Preparing zip asset from directory:', asset.path);
const staging = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-assets'));
try {
Expand All @@ -60,7 +65,7 @@ async function prepareZipAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitIn
* @param contentType Content-type to use when uploading to S3 (none will be specified by default)
*/
async function prepareFileAsset(
asset: AssetMetadataEntry,
asset: FileAssetMetadataEntry,
toolkitInfo: ToolkitInfo,
filePath?: string,
contentType?: string): Promise<CloudFormation.Parameter[]> {
Expand Down
Loading