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: Implement CodeArtifact L2 construct #11091

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c8e209a
feat: Added CodeArtifact L2 implementation
jstandish Oct 24, 2020
915e8d0
feat: Added domain and repo tests cases and implementation.
jstandish Oct 24, 2020
d2e98db
feat: Added event subscription
jstandish Oct 25, 2020
51cc8b6
feat: Added validation for repository and domain
jstandish Oct 25, 2020
a51709f
fix: Typo in grantReadWrite jsodc
jstandish Oct 25, 2020
be5460d
feat: Added resource policies. Added integ tests.
jstandish Oct 25, 2020
4109da9
Merge branch 'master' into code-artifact-l2
jstandish Oct 25, 2020
4b2e38c
Merge branch 'master' into code-artifact-l2
jstandish Oct 26, 2020
c4810a8
Merge branch 'master' into code-artifact-l2
jstandish Oct 26, 2020
afbc8d1
chore: Updated readme and package for experimental
jstandish Oct 27, 2020
c2749db
chore: Added service description
jstandish Oct 27, 2020
bc6629b
chore: Added bolding to match styling of getting started docs
jstandish Oct 27, 2020
73dae98
chore: Renamed actions to permissions and made const capitalized.
jstandish Oct 27, 2020
c6309b2
chore: Changed import to be module pattern.
jstandish Oct 27, 2020
abe91a4
chore: Use concrete kms.IKey
jstandish Oct 27, 2020
7ff616c
chore: Code review items
jstandish Oct 28, 2020
1901b74
chore: Code review items
jstandish Oct 28, 2020
2b056f9
chore: Code review items
jstandish Oct 28, 2020
786329f
chore: Added @experimental to exported interfaces
jstandish Oct 30, 2020
6a158db
chore: Code review items
jstandish Oct 30, 2020
b6d7193
chore: Fix integ test.
jstandish Oct 30, 2020
a4a0247
Merge branch 'master' into code-artifact-l2
jstandish Oct 30, 2020
a6e4785
Merge branch 'master' into code-artifact-l2
jstandish Oct 30, 2020
5f4a126
merge
jstandish Oct 30, 2020
bd212b2
chore: Code review items
jstandish Oct 30, 2020
d8442b4
chore: Code review items
jstandish Oct 31, 2020
91648bf
Merge branch 'master' into code-artifact-l2
jstandish Oct 31, 2020
4489488
fix: CFN spec should have had missing required properties.
jstandish Nov 1, 2020
2ba3655
Merge branch 'master' into code-artifact-l2
jstandish Dec 19, 2020
fbde98d
Merge branch 'master' into code-artifact-l2
jstandish Dec 19, 2020
c714c5d
fix: Code review items
jstandish Dec 20, 2020
07ed06b
Merge branch 'code-artifact-l2' of https://github.com/jstandish/aws-c…
jstandish Dec 20, 2020
19cbc00
Merge branch 'master' into code-artifact-l2
jstandish Dec 21, 2020
dc7ca1a
Merge branch 'master' into code-artifact-l2
jstandish Dec 29, 2020
070c4c6
Merge branch 'master' into code-artifact-l2
jstandish Jan 4, 2021
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
122 changes: 120 additions & 2 deletions packages/@aws-cdk/aws-codeartifact/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,130 @@
>
> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib

![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge)

> The APIs of higher level constructs in this module are experimental and under active development.
> They are subject to non-backward compatible changes or removal in any future version. These are
> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be
> announced in the release notes. This means that while you may use them, you may need to update
> your source code when upgrading to a newer version of this package.

---

<!--END STABILITY BANNER-->

This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project.
CodeArtifact is a fully managed artifact repository service that makes it easy for organizations to securely store and share software packages used for application development. You can use CodeArtifact with popular build tools and package managers such as **Maven**, **Gradle**, **npm**, **yarn**, **pip**, and **twine**.

For further information on CodeCommit, see the [AWS CodeArtifact documentation](https://docs.aws.amazon.com/codeartifact).

Add a CodeArtifact Domain to your stack:

```ts
import codeartifact = require('@aws-cdk/aws-codeartifact');
import * as codeartifact from '@aws-cdk/aws-codeartifact';

const domain = new codeartifact.Domain(stack, 'domain', { domainName: 'example-domain' });
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const domain = new codeartifact.Domain(stack, 'domain', { domainName: 'example-domain' });
new codeartifact.Domain(stack, 'domain', { domainName: 'example-domain' });

```

Add a CodeArtifact Repository to your stack:

```ts
import * as codeartifact from '@aws-cdk/aws-codeartifact';

const domain = new codeartifact.Domain(stack, 'domain', { domainName: 'example-domain' });
const repository = new codeartifact.Repository(stack, 'repository', {
repositoryName: 'repository',
domain: domain,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
domain: domain,
domain,

});
```

It is possible to use the `addRepository` method on `codeartifact.Domain` to add repostories implicitly.

```ts
import * as codeartifact from '@aws-cdk/aws-codeartifact';

const domain = new codeartifact.Domain(stack, 'domain', { domainName: 'example-domain' });
const repo1 = new codeartifact.Repository(stack, 'repository_1', { repositoryName: 'repository-1' });
const repo2 = new codeartifact.Repository(stack, 'repository_2', { repositoryName: 'repository-2' });

domain.addRepositories(repo1, repo2);
Comment on lines +53 to +57
Copy link
Contributor

Choose a reason for hiding this comment

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

Take a look at https://github.com/aws/aws-cdk/blob/master/DESIGN_GUIDELINES.md#factories again. Ideally, this addRepository method takes in a RepositoryOption interface, which is a sub-interface of RepositoryProps.

export interface RepositoryOptions {
  readonly repositoryName: string,
  readonly description?: string,
  ...
}
export interface RepositoryProps extends RepositoryOptions {
  readonly domain: IDomain;
}
// THEN
const domain = new codeartifact.Domain(stack, 'Domain');
domain.addRespository({ repositoryName: 'MyRepo', description: 'NPM + Java repo' });


```

## External Connections

Extenal connections can be added by calling `.withExternalConnections(...)` method on a repository. It accepts and
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a sentence (or two) here (and for Upstream Repositories) just explaining what they are? Also, I still think we should defer withExternalConnections until we have a use case.

array of external connecitions. You can also pass the external connections when creating a repository by setting the
`externalConnections` property.

Add an external connection when constructing a new repository

```ts
import * as codeartifact from '@aws-cdk/aws-codeartifact';

const domain = new codeartifact.Domain(stack, 'domain', { domainName: 'example-domain' });
const repository = new codeartifact.Repository(stack, 'repository', {
repositoryName: 'repository',
domain: domain,
externalConnections: [codeartifact.ExternalConnection.NPM]
});
```

Add an external connection to an existing repository

```ts
import * as codeartifact from '@aws-cdk/aws-codeartifact';

const domain = new codeartifact.Domain(stack, 'domain', { domainName: 'example-domain' });
const repository = new codeartifact.Repository(stack, 'repository', {
repositoryName: 'repository',
domain: domain
});


repository.withExternalConnections(codeartifact.ExternalConnection.NPM);
```

## Upstream Repositories

Upstream repositories can be added by calling `.withUpstream(...)` method on a repository. It accepts an array of `IRepository`.
You can also pass the upstream repositories in the `upstreams` property during construction.

Add an upstream when constructing a new repository

```ts
import * as codeartifact from '@aws-cdk/aws-codeartifact';

const domain = new codeartifact.Domain(stack, 'domain', { domainName: 'example-domain' });

const upstreamRepository = new codeartifact.Repository(stack, 'upstream-repository', {
repositoryName: 'upstream-repository',
domain: domain,
});

const repository = new codeartifact.Repository(stack, 'repository', {
repositoryName: 'repository',
domain: domain,
upstreams: [upstreamRepository]
});
```

Add an upstream to an existing repository

```ts
import * as codeartifact from '@aws-cdk/aws-codeartifact';

const domain = new codeartifact.Domain(stack, 'domain', { domainName: 'example-domain' });

const upstreamRepository = new codeartifact.Repository(stack, 'upstream-repository', {
repositoryName: 'upstream-repository',
domain: domain,
});

const repository = new codeartifact.Repository(stack, 'repository', {
repositoryName: 'repository',
domain: domain
});


repository.withUpstream(upstreamRepository);
```
202 changes: 202 additions & 0 deletions packages/@aws-cdk/aws-codeartifact/lib/domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import { Resource, Stack, Lazy, Token } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnDomain } from './codeartifact.generated';
import { IDomain, IRepository, DomainAttributes } from './interfaces';
import { DOMAIN_CREATE_ACTIONS, DOMAIN_LOGIN_ACTIONS, DOMAIN_READ_ACTIONS } from './perms';
import { validate } from './validation';

/**
* Properties for a new CodeArtifact domain
* @experimental
*/
export interface DomainProps {
/**
* The name of the domain
* @default Unique id
*/
readonly domainName?: string;

/**
* The KMS encryption key used for the domain resource.
* @default AWS Managed Key
* @attribute
*/
readonly domainEncryptionKey?: kms.IKey;
/**
* Principal for the resource policy for the domain
* @default AccountRootPrincipal
*/
readonly principal?: iam.IPrincipal
Comment on lines +28 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

If I'm reading this correctly, users should either (1) specify just the principal and get the default permissions; (2) should specify the policyDocument with the policy they want; or (3) specify neither and get the default permissions on the account as a whole? If so, can you make that a bit clearer in the docstrings? Maybe something like this for the principal?

Suggested change
* Principal for the resource policy for the domain
* @default AccountRootPrincipal
*/
readonly principal?: iam.IPrincipal
* Default principal for the resource policy for the domain. Receives full access to the domain.
* Only used if ``policyDocument`` is not specified.
* @default AccountRootPrincipal
*/
readonly principal?: iam.IPrincipal

Then we might even want a check (and error) in the constructor are both specified.


Edit: Actually, looking at this again (after almost finishing the review), I'm wondering if we want this at all (both here, and for Repository). By default -- without specifying a policy -- the account root will have all access to the Domain, so creating this default policy is actually a no-op. At least according to the docs, the statement is optional and we can omit it for that default case. The only time a domain resource policy is needed is for cross-account access.

Here's what I'd propose: let's remove this parameter for now. We can add it in as a non-breaking change later. If a policy document is specified, great, we can use it. Otherwise, we leave the parameter undefined. If a user calls one of the grant methods and a policy document doesn't yet exist, we can create it at that point. Make sense?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you remove, per the above?


/**
* Resource policy for the domain
* @default Open policy that allows principal to reader, create, and generate authorization token.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @default Open policy that allows principal to reader, create, and generate authorization token.
* @default - None. The default allows the account full access to the domain.

*/
readonly policyDocument?: iam.PolicyDocument
}

/**
* A new CodeArtifacft domain
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* A new CodeArtifacft domain
* A CodeArtifact domain

* @experimental
*/
export class Domain extends Resource implements IDomain {
/**
* Import an existing domain provided an ARN
*
* @param scope The parent creating construct
* @param id The construct's name
* @param domainArn Domain ARN (i.e. arn:aws:codeartifact:us-east-2:444455556666:domain/MyDomain)
*/
public static fromDomainArn(scope: Construct, id: string, domainArn: string): IDomain {
return Domain.fromDomainAttributes(scope, id, { domainArn: domainArn });
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return Domain.fromDomainAttributes(scope, id, { domainArn: domainArn });
return Domain.fromDomainAttributes(scope, id, { domainArn });

}

/**
* Import an existing domain
*/
public static fromDomainAttributes(scope: Construct, id: string, attrs: DomainAttributes): IDomain {
const stack = Stack.of(scope);
const domainName = attrs.domainName || stack.parseArn(attrs.domainArn).resourceName;

if (!domainName || domainName == '') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (!domainName || domainName == '') {
if (!domainName) {

'' evaluates as false in Javascript/Typescript.

throw new Error('Domain name is required as a resource name with the ARN');
}

class Import extends Resource implements IDomain {
domainArn: string = '';
Comment on lines +67 to +68
Copy link
Contributor

Choose a reason for hiding this comment

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

You can just inline set these values. Additionally, you can inline the class definition altogether:

Suggested change
class Import extends Resource implements IDomain {
domainArn: string = '';
return new class extends Resource implements IDomain {
public readonly domainArn = attrs.domainArn;
public readonly domainName = domainName;
...
}(scope, id);

domainName?: string | undefined;
domainOwner?: string | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

You should set the domainOwner from the ARN as well.

domainEncryptionKey?: kms.IKey | undefined;
}

const instance = new Import(scope, id);
instance.domainName = domainName;
instance.domainArn = attrs.domainArn;
instance.domainEncryptionKey = attrs.domainEncryptionKey;

return instance;
}

public readonly domainName?: string;
public readonly domainNameAttr?: string;
public readonly domainArn: string = '';
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to initialize these variables; they're initialized in the constructor.

Suggested change
public readonly domainArn: string = '';
public readonly domainArn: string;

public readonly domainOwner?: string = '';
public readonly domainEncryptionKey?: kms.IKey;
public readonly policyDocument?: iam.PolicyDocument;
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's keep this private for now -- unless there's a use case for exposing it. Between the Props and the grant methods, we should be able to cover 99.9% of use cases with this private.

Suggested change
public readonly policyDocument?: iam.PolicyDocument;
private readonly policyDocument?: iam.PolicyDocument;

private readonly cfnDomain: CfnDomain;

constructor(scope: Construct, id: string, props?: DomainProps) {
super(scope, id);

// Set domain and encryption key as we will validate them before creation
const domainName = props?.domainName ?? this.node.uniqueId;
const domainEncryptionKey = props?.domainEncryptionKey ?? null;

this.validateProps(domainName, domainEncryptionKey);

// Create the CFN domain instance
this.cfnDomain = new CfnDomain(this, 'Resource', {
domainName: domainName,
permissionsPolicyDocument: Lazy.anyValue({ produce: () => props?.policyDocument?.toJSON() }),
encryptionKey: domainEncryptionKey?.keyId,
});

this.domainName = domainName;
this.domainNameAttr = this.cfnDomain.attrName;
this.domainArn = this.cfnDomain.attrArn;
this.domainOwner = this.cfnDomain.attrOwner;
this.policyDocument = props?.policyDocument;
}

/**
* Add a repositories to the domain
*/
addRepositories(...repositories: IRepository[]): IDomain {
Copy link
Contributor

Choose a reason for hiding this comment

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

See README comment for how this API definition should be changed.

if (repositories.length > 0) {
repositories.forEach(r => r.assignDomain(this));
}

return this;
}

private validateProps(domainName : string, domainEncryptionKey? : kms.IKey | null) {
if (Token.isUnresolved(domainName)) {
throw new Error(`'domainName' must resolve, got: '${domainName}'`);
}

validate('DomainName',
{ required: true, minLength: 2, maxLength: 50, pattern: /[a-z][a-z0-9\-]{0,48}[a-z0-9]/gi, documentationLink: 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codeartifact-domain.html#cfn-codeartifact-domain-domainname' },
Copy link
Contributor

Choose a reason for hiding this comment

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

:D I just meant to include the doc link as a comment, so future maintainers can update it appropriately. Including it in the validate method isn't strictly necessary. Also, you'll find -- once you create the eslint config -- that this line is too long.

domainName);

validate('EncryptionKey',
{ minLength: 1, maxLength: 2048, pattern: /\S+/gi, documentationLink: 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codeartifact-domain.html#cfn-codeartifact-domain-encryptionkey' },
domainEncryptionKey?.keyArn || '');
}

/**
* Adds a statement to the IAM resource policy associated with this domain.
*/
public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult {

if (!this.policyDocument) {
const p = this.policyDocument || new iam.PolicyDocument();
Copy link
Contributor

Choose a reason for hiding this comment

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

You're never saving this value (this.policyDocument will never be set).


p.addStatements(statement);

return { statementAdded: true, policyDependable: p };
}

return { statementAdded: false };
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not add the statement if the document already exists?

}

private grant(principal: iam.IGrantable, iamActions: string[], resource: string = '*'): iam.Grant {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private grant(principal: iam.IGrantable, iamActions: string[], resource: string = '*'): iam.Grant {
private grant(principal: iam.IGrantable, iamActions: string[]): iam.Grant {

return iam.Grant.addToPrincipalOrResource({
grantee: principal,
actions: iamActions,
resourceArns: [resource],
resource: this,
});
}

/**
* Assign default login, creation, and read for the domain.
* @param principal The principal of for the policy
* @see https://docs.aws.amazon.com/codeartifact/latest/ug/domain-policies.html#domain-policy-example
*/
grantDefaultPolicy(principal: iam.IPrincipal): iam.Grant {
const p = principal;
this.grantLogin(p);
this.grantCreate(p);
return this.grantRead(p);
}

/**
* Adds read actions for the principal to the domain's
* resource policy
* @param principal The principal for the policy
* @see https://docs.aws.amazon.com/codeartifact/latest/ug/domain-spolicies.html
*/
public grantRead(principal: iam.IPrincipal): iam.Grant {
return this.grant(principal, DOMAIN_READ_ACTIONS);
}
/**
* Adds GetAuthorizationToken for the principal to the domain's
* resource policy
* @param principal The principal for the policy
* @see https://docs.aws.amazon.com/codeartifact/latest/ug/domain-policies.html
*/
public grantLogin(principal: iam.IPrincipal): iam.Grant {
Copy link
Contributor

Choose a reason for hiding this comment

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

On second read, I think grantLogin isn't quite intuitive. In the case of "neither name is great", I'd prefer we use the service's terminology.

Suggested change
public grantLogin(principal: iam.IPrincipal): iam.Grant {
public grantAuthorizationToken(principal: iam.IPrincipal): iam.Grant {

return this.grant(principal, DOMAIN_LOGIN_ACTIONS);
}
/**
* Adds CreateRepository for the principal to the domain's
* resource policy
* @param principal The principal for the policy
* @see https://docs.aws.amazon.com/codeartifact/latest/ug/domain-policies.html
*/
public grantCreate(principal: iam.IPrincipal): iam.Grant {
return this.grant(principal, DOMAIN_CREATE_ACTIONS);
}
}
35 changes: 35 additions & 0 deletions packages/@aws-cdk/aws-codeartifact/lib/external-connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* CodeArtifact supports an external connection to the following public repositories.
* @experimental
* @see https://docs.aws.amazon.com/codeartifact/latest/ug/external-connection.html#supported-public-repositories
*/
export enum ExternalConnection {
/**
* NPM public registry
*/
NPM = 'public:npmjs',
/**
* NuGet.org public registry
*/
DOTNET_NUGETORG = 'public:nuget-org',
/**
* Python Package Index
*/
PYTHON_PYPI = 'public:pypi',
/**
* Maven Central
*/
MAVEN_CENTRAL = 'public:maven-central',
/**
* Google Android repository
*/
MAVEN_GOOGLEANDROID = 'public:maven-googleandroid',
/**
* Gradle plugins repository
*/
MAVEN_GRADLEPLUGINS = 'public:maven-gradleplugins',
/**
* CommonsWare Android repository
* */
MAVEN_COMMONSWARE = 'public:maven-commonsware',
}
4 changes: 4 additions & 0 deletions packages/@aws-cdk/aws-codeartifact/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
// AWS::CodeArtifact CloudFormation Resources:
export * from './codeartifact.generated';
export * from './domain';
export * from './repository';
export * from './external-connection';
export * from './interfaces';
Loading