Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ecs): Add RepositoryCredentials support #1

Closed
wants to merge 4 commits into from
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
10 changes: 5 additions & 5 deletions packages/@aws-cdk/aws-ecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ cluster.addDefaultAutoScalingGroupCapacity('Capacity', {
const ecsService = new ecs.LoadBalancedEc2Service(this, 'Service', {
cluster,
memoryLimitMiB: 512,
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
});
```

Expand Down Expand Up @@ -134,7 +134,7 @@ To add containers to a task definition, call `addContainer()`:
```ts
const container = fargateTaskDefinition.addContainer("WebContainer", {
// Use an image from DockerHub
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
// ... other options here ...
});
```
Expand All @@ -148,7 +148,7 @@ const ec2TaskDefinition = new ecs.Ec2TaskDefinition(this, 'TaskDef', {

const container = ec2TaskDefinition.addContainer("WebContainer", {
// Use an image from DockerHub
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 1024
// ... other options here ...
});
Expand Down Expand Up @@ -183,8 +183,8 @@ const taskDefinition = new ecs.TaskDefinition(this, 'TaskDef', {
Images supply the software that runs inside the container. Images can be
obtained from either DockerHub or from ECR repositories, or built directly from a local Dockerfile.

* `ecs.ContainerImage.fromDockerHub(imageName)`: use a publicly available image from
DockerHub.
* `ecs.ContainerImage.fromInternet(imageName, { credentials?: MY_SECRET })`: use a public or private image from
Copy link

Choose a reason for hiding this comment

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

I wouldn't write it like this, this isn't copy/pasteable user code. Just add a new line below to do something like:

ecs.ContainerImage.fromInternet(imageName, { credentials: mySecret }): use a private image that needs credentials.

DockerHub or another online registry.
* `ecs.ContainerImage.fromEcrRepository(repo, tag)`: use the given ECR repository as the image
to start. If no tag is provided, "latest" is assumed.
* `ecs.ContainerImage.fromAsset(this, 'Image', { directory: './image' })`: build and upload an
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-ecs/lib/container-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ export class ContainerDefinition extends cdk.Construct {
portMappings: this.portMappings.map(renderPortMapping),
privileged: this.props.privileged,
readonlyRootFilesystem: this.props.readonlyRootFilesystem,
repositoryCredentials: undefined, // FIXME
repositoryCredentials: this.props.image.credentials && this.props.image.renderRepositoryCredentials(),
ulimits: this.ulimits.map(renderUlimit),
user: this.props.user,
volumesFrom: this.volumesFrom.map(renderVolumeFrom),
Expand Down
35 changes: 30 additions & 5 deletions packages/@aws-cdk/aws-ecs/lib/container-image.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import ecr = require('@aws-cdk/aws-ecr');
import secretsmanager = require('@aws-cdk/aws-secretsmanager');
import cdk = require('@aws-cdk/cdk');

import { ContainerDefinition } from './container-definition';
import { CfnTaskDefinition } from './ecs.generated';

/**
* Repository Credential resources
*/
export interface RepositoryCreds {
Copy link

Choose a reason for hiding this comment

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

What are your thoughts for this type? Future proofing? Because it's not strictly necessary today, right?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Correct, today we could potentially add a secret to ContainerImage directly but I want to minimize the need for rewiring if other resources or additional fields become valid in the future. Do you foresee issues with this approach?

/**
* The secret that contains credentials for the image repository
*/
readonly secret: secretsmanager.ISecret;
}

/**
* Constructs for types of container images
*/
export abstract class ContainerImage {
/**
* Reference an image on DockerHub
* Reference an image on DockerHub or another online registry
*/
public static fromDockerHub(name: string) {
return new DockerHubImage(name);
public static fromInternet(name: string, props: InternetHostedImageProps = {}) {
return new InternetHostedImage(name, props);
}

/**
Expand All @@ -33,12 +44,26 @@ export abstract class ContainerImage {
*/
public abstract readonly imageName: string;

/**
* Optional credentials for a private image registry
*/
public abstract readonly credentials?: RepositoryCreds;
Copy link

Choose a reason for hiding this comment

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

Does this need to be publicly accessible? Seems like renderRepositoryCredentials() is all that a consumer of this class should need from it, right?

Copy link
Owner Author

Choose a reason for hiding this comment

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

renderContainerDefinition() needs to see it to determine whether or not to call renderRepositoryCredentials(). ECS will fail to register the task definition if RepositoryCredentials is present but empty.


/**
* Called when the image is used by a ContainerDefinition
*/
public abstract bind(containerDefinition: ContainerDefinition): void;

/**
* Render the Repository credentials to the CloudFormation object
*/
public renderRepositoryCredentials(): CfnTaskDefinition.RepositoryCredentialsProperty {
return {
credentialsParameter: this.credentials ? this.credentials.secret.secretArn : ''
Copy link

Choose a reason for hiding this comment

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

To avoid breaking all existing integration tests, maybe return undefined for this whole object if we don't have credentials?

public renderRepositoryCredentials(): CfnTaskDefinition.RepositoryCredentialsProperty  | undefined {
  if (!this.credentials) { return undefined; }
  return {
    credentialsParameter: this.credentials.secret.secretArn
  } ;
}

Copy link

Choose a reason for hiding this comment

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

And in fact, this method could be abstract here and only be implemented in InternetHostedImage, since that's the only image type that actually has any credentials at all.

Copy link

Choose a reason for hiding this comment

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

Well the others would need to do a base implementation of undefined as well.

Copy link
Owner Author

Choose a reason for hiding this comment

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

To avoid breaking all existing integration tests...

Ah, the conditional here can probably be removed b/c this func should only be called if credentials is populated. See renderContainerDefinition(). Currently, the existing integ tests all pass.

...this method could be abstract here and be implemented in InternetHostedImage...

I tried returning undefined from this method before (as you show) and got yelled at by the compiler b/c it wasn't a return value of type RepositoryCredentialsProperty. So since I have to return a RepositoryCredentialsProperty obj regardless, I'd prefer to consolidate it into one func on ContainerImage. Is there a way around having to return an RepositoryCredentialsProperty in some cases?

Copy link

Choose a reason for hiding this comment

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

got yelled at by the compiler

It's hard to see the change in the code I pasted because it scrolls off the side of the comment box, but you should have written the return type as:

CfnTaskDefinition.RepositoryCredentialsProperty  | undefined

That would have allowed you to return undefined.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Ah, I totally missed that, thanks for pointing it out! In that case I'll update it per the above.

};
}
}

import { AssetImage, AssetImageProps } from './images/asset-image';
import { DockerHubImage } from './images/dockerhub';
import { EcrImage } from './images/ecr';
import { InternetHostedImage, InternetHostedImageProps } from './images/internet-hosted';
4 changes: 3 additions & 1 deletion packages/@aws-cdk/aws-ecs/lib/images/asset-image.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DockerImageAsset } from '@aws-cdk/assets-docker';
import cdk = require('@aws-cdk/cdk');
import { ContainerDefinition } from '../container-definition';
import { ContainerImage } from '../container-image';
import { ContainerImage, RepositoryCreds } from '../container-image';

export interface AssetImageProps {
/**
Expand All @@ -14,7 +14,9 @@ export interface AssetImageProps {
* An image that will be built at synthesis time
*/
export class AssetImage extends ContainerImage {
public readonly credentials?: RepositoryCreds;
private readonly asset: DockerImageAsset;

constructor(scope: cdk.Construct, id: string, props: AssetImageProps) {
super();
this.asset = new DockerImageAsset(scope, id, { directory: props.directory });
Expand Down
15 changes: 0 additions & 15 deletions packages/@aws-cdk/aws-ecs/lib/images/dockerhub.ts

This file was deleted.

3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-ecs/lib/images/ecr.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import ecr = require('@aws-cdk/aws-ecr');
import { ContainerDefinition } from '../container-definition';
import { ContainerImage } from '../container-image';
import { ContainerImage, RepositoryCreds } from '../container-image';

/**
* An image from an ECR repository
*/
export class EcrImage extends ContainerImage {
public readonly imageName: string;
public readonly credentials?: RepositoryCreds;
private readonly repository: ecr.IRepository;

constructor(repository: ecr.IRepository, tag: string) {
Expand Down
33 changes: 33 additions & 0 deletions packages/@aws-cdk/aws-ecs/lib/images/internet-hosted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import secretsmanager = require('@aws-cdk/aws-secretsmanager');
import { ContainerDefinition } from "../container-definition";
import { ContainerImage, RepositoryCreds } from "../container-image";

export interface InternetHostedImageProps {
/**
* Optional secret that houses credentials for the image registry
*/
credentials?: secretsmanager.ISecret;
}

/**
* A container image hosted on DockerHub or another online registry
*/
export class InternetHostedImage extends ContainerImage {
public readonly imageName: string;
public readonly credentials?: RepositoryCreds;

constructor(imageName: string, props: InternetHostedImageProps = {}) {
super();
this.imageName = imageName;

if (props.credentials !== undefined) {
this.credentials = { secret: props.credentials };
}
}

public bind(containerDefinition: ContainerDefinition): void {
if (this.credentials !== undefined) {
this.credentials.secret.grantRead(containerDefinition.taskDefinition.obtainExecutionRole());
}
}
}
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-ecs/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export * from './load-balanced-ecs-service';
export * from './load-balanced-fargate-service-applet';

export * from './images/asset-image';
export * from './images/dockerhub';
export * from './images/internet-hosted';
export * from './images/ecr';

export * from './log-drivers/aws-log-driver';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class LoadBalancedFargateServiceApplet extends cdk.Stack {
memoryMiB: props.memoryMiB,
publicLoadBalancer: props.publicLoadBalancer,
publicTasks: props.publicTasks,
image: ContainerImage.fromDockerHub(props.image),
image: ContainerImage.fromInternet(props.image),
desiredCount: props.desiredCount,
environment: props.environment,
certificate,
Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-ecs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"@aws-cdk/aws-logs": "^0.24.1",
"@aws-cdk/aws-route53": "^0.24.1",
"@aws-cdk/aws-sns": "^0.24.1",
"@aws-cdk/aws-secretsmanager": "^0.24.1",
"@aws-cdk/cdk": "^0.24.1",
"@aws-cdk/cx-api": "^0.24.1"
},
Expand All @@ -97,6 +98,7 @@
"@aws-cdk/aws-iam": "^0.24.1",
"@aws-cdk/aws-logs": "^0.24.1",
"@aws-cdk/aws-route53": "^0.24.1",
"@aws-cdk/aws-secretsmanager": "^0.24.1",
"@aws-cdk/cdk": "^0.24.1"
},
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', {
});

const container = taskDefinition.addContainer('web', {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 256,
});

Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', {
});

const container = taskDefinition.addContainer('web', {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 256,
});
container.addPortMappings({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export = {

const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef');
taskDefinition.addContainer('TheContainer', {
image: ecs.ContainerImage.fromDockerHub('henk'),
image: ecs.ContainerImage.fromInternet('henk'),
memoryLimitMiB: 256
});

Expand Down
32 changes: 16 additions & 16 deletions packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export = {
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef');

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512
});

Expand Down Expand Up @@ -57,7 +57,7 @@ export = {
cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') });
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef');
taskDefinition.addContainer('BaseContainer', {
image: ecs.ContainerImage.fromDockerHub('test'),
image: ecs.ContainerImage.fromInternet('test'),
memoryReservationMiB: 10,
});

Expand All @@ -82,7 +82,7 @@ export = {
cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') });
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef');
taskDefinition.addContainer('BaseContainer', {
image: ecs.ContainerImage.fromDockerHub('test'),
image: ecs.ContainerImage.fromInternet('test'),
memoryReservationMiB: 10,
});

Expand Down Expand Up @@ -128,7 +128,7 @@ export = {
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef');

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512
});

Expand Down Expand Up @@ -158,7 +158,7 @@ export = {
});

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512
});

Expand Down Expand Up @@ -190,7 +190,7 @@ export = {
});

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512
});

Expand Down Expand Up @@ -241,7 +241,7 @@ export = {
});

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512
});

Expand All @@ -267,7 +267,7 @@ export = {
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef');

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512
});

Expand Down Expand Up @@ -296,7 +296,7 @@ export = {
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef');

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512
});

Expand Down Expand Up @@ -327,7 +327,7 @@ export = {
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef');

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512
});

Expand Down Expand Up @@ -358,7 +358,7 @@ export = {
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef');

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512
});

Expand All @@ -385,7 +385,7 @@ export = {
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef');

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512
});

Expand Down Expand Up @@ -415,7 +415,7 @@ export = {
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef');

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512
});

Expand All @@ -442,7 +442,7 @@ export = {
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef');

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512
});

Expand Down Expand Up @@ -473,7 +473,7 @@ export = {
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef');

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512
});

Expand Down Expand Up @@ -501,7 +501,7 @@ export = {
cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') });
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TD', { networkMode: ecs.NetworkMode.Host });
const container = taskDefinition.addContainer('web', {
image: ecs.ContainerImage.fromDockerHub('test'),
image: ecs.ContainerImage.fromInternet('test'),
memoryLimitMiB: 1024,
});
container.addPortMappings({ containerPort: 808 });
Expand Down
Loading