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: additional options when assuming publishing role | main #58

Closed
wants to merge 5 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
16 changes: 8 additions & 8 deletions .projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions .projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ const project = new typescript.TypeScriptProject({
'@aws-sdk/client-sts',
'@aws-sdk/credential-providers',
'@aws-sdk/lib-storage',
'@smithy/config-resolver',
'@smithy/node-config-provider',
'glob',
'mime',
'yargs',
],
description: 'CDK Asset Publishing Tool',
devDeps: [
'@smithy/config-resolver',
'@smithy/node-config-provider',
'@smithy/types',
'@types/archiver',
'@types/glob',
Expand Down
56 changes: 46 additions & 10 deletions lib/aws.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import * as os from 'os';
import { ECRClient } from '@aws-sdk/client-ecr';
import { CompleteMultipartUploadCommandOutput, PutObjectCommandInput, S3Client } from '@aws-sdk/client-s3';
import {
CompleteMultipartUploadCommandOutput,
PutObjectCommandInput,
S3Client,
} from '@aws-sdk/client-s3';
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
import { GetCallerIdentityCommand, STSClient, STSClientConfig } from '@aws-sdk/client-sts';
import {
GetCallerIdentityCommand,
STSClient,
STSClientConfig,
AssumeRoleRequest,
} from '@aws-sdk/client-sts';
import { fromNodeProviderChain, fromTemporaryCredentials } from '@aws-sdk/credential-providers';
import { Upload } from '@aws-sdk/lib-storage';
import { NODE_REGION_CONFIG_FILE_OPTIONS, NODE_REGION_CONFIG_OPTIONS } from '@smithy/config-resolver';
import {
NODE_REGION_CONFIG_FILE_OPTIONS,
NODE_REGION_CONFIG_OPTIONS,
} from '@smithy/config-resolver';
import { loadConfig } from '@smithy/node-config-provider';
import { AwsCredentialIdentityProvider } from '@smithy/types';
import type { AwsCredentialIdentityProvider } from '@smithy/types';

// Partial because `RoleSessionName` is required in STS, but we have a default value for it.
export type AssumeRoleAdditionalOptions = Partial<
// cloud-assembly-schema validates that `ExternalId` and `RoleArn` are not configured
Omit<AssumeRoleRequest, 'ExternalId' | 'RoleArn'>
>;

/**
* AWS SDK operations required by Asset Publishing
Expand All @@ -21,13 +39,17 @@ export interface IAws {
s3Client(options: ClientOptions): Promise<S3Client>;
ecrClient(options: ClientOptions): Promise<ECRClient>;
secretsManagerClient(options: ClientOptions): Promise<SecretsManagerClient>;
upload(params: PutObjectCommandInput, options?: ClientOptions): Promise<CompleteMultipartUploadCommandOutput>;
upload(
params: PutObjectCommandInput,
options?: ClientOptions
): Promise<CompleteMultipartUploadCommandOutput>;
}

export interface ClientOptions {
region?: string;
assumeRoleArn?: string;
assumeRoleExternalId?: string;
assumeRoleAdditionalOptions?: AssumeRoleAdditionalOptions;
quiet?: boolean;
}

Expand Down Expand Up @@ -68,7 +90,7 @@ export class DefaultAwsClient implements IAws {
process.env.AWS_PROFILE = profile;
const clientConfig: STSClientConfig = {
customUserAgent: USER_AGENT,
}
};
this.config = {
clientConfig,
credentials: fromNodeProviderChain({
Expand All @@ -82,19 +104,22 @@ export class DefaultAwsClient implements IAws {
return new S3Client(await this.awsOptions(options));
}

public async upload(params: PutObjectCommandInput, options: ClientOptions = {}): Promise<CompleteMultipartUploadCommandOutput> {
public async upload(
params: PutObjectCommandInput,
options: ClientOptions = {}
): Promise<CompleteMultipartUploadCommandOutput> {
try {
const upload = new Upload({
client: await this.s3Client(options),
params,
});

return upload.done();
return await upload.done();
} catch (e) {
// TODO: add something more useful here
console.log(e);
throw e;
}
}
}

public async ecrClient(options: ClientOptions): Promise<ECRClient> {
Expand All @@ -121,7 +146,7 @@ export class DefaultAwsClient implements IAws {
}

public async discoverTargetAccount(options: ClientOptions): Promise<Account> {
return this.getAccount(await this.awsOptions(options));
return this.getAccount(options);
}

private async getAccount(options?: ClientOptions): Promise<Account> {
Expand All @@ -145,11 +170,22 @@ export class DefaultAwsClient implements IAws {
if (options) {
config.region = options.region;
if (options.assumeRoleArn) {
if (
options.assumeRoleAdditionalOptions?.Tags &&
options.assumeRoleAdditionalOptions.Tags.length > 0 &&
!options.assumeRoleAdditionalOptions.TransitiveTagKeys
) {
options.assumeRoleAdditionalOptions.TransitiveTagKeys =
// for some reason t.Key is marked as optional in the SDK .d.ts
// so we have to "!".
options.assumeRoleAdditionalOptions.Tags.map((t) => t.Key!);
}
config.credentials = fromTemporaryCredentials({
params: {
RoleArn: options.assumeRoleArn,
ExternalId: options.assumeRoleExternalId,
RoleSessionName: `${USER_AGENT}-${safeUsername()}`,
...(options.assumeRoleAdditionalOptions ?? {}),
},
clientConfig: this.config.clientConfig,
});
Expand Down
6 changes: 3 additions & 3 deletions lib/private/docker-credentials.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr';
import { GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
import { Logger } from './shell';
import { IAws } from '../aws';
import { GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr';

export interface DockerCredentials {
readonly Username: string;
Expand Down Expand Up @@ -107,7 +107,7 @@ export async function obtainEcrCredentials(ecr: ECRClient, logger?: Logger) {
if (logger) {
logger('Fetching ECR authorization token');
}

const authData = (await ecr.send(new GetAuthorizationTokenCommand({}))).authorizationData || [];
if (authData.length === 0) {
throw new Error('No authorization data received from ECR');
Expand Down
13 changes: 10 additions & 3 deletions lib/private/handlers/container-images.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import * as path from 'path';
import { DockerImageDestination } from '@aws-cdk/cloud-assembly-schema';
import { DescribeImagesCommand, DescribeRepositoriesCommand, type ECRClient } from '@aws-sdk/client-ecr';
import {
DescribeImagesCommand,
DescribeRepositoriesCommand,
type ECRClient,
} from '@aws-sdk/client-ecr';
import { DockerImageManifestEntry } from '../../asset-manifest';
import { EventType } from '../../progress';
import { IAssetHandler, IHandlerHost, IHandlerOptions } from '../asset-handler';
Expand Down Expand Up @@ -105,7 +109,10 @@ export class ContainerImageAssetHandler implements IAssetHandler {

const destination = await replaceAwsPlaceholders(this.asset.destination, this.host.aws);
const ecr = await this.host.aws.ecrClient({
...destination,
assumeRoleExternalId: destination.assumeRoleExternalId,
assumeRoleArn: destination.assumeRoleArn,
assumeRoleAdditionalOptions: destination.assumeRoleAdditionalOptions,
region: destination.region,
quiet: options.quiet,
});
const account = async () => (await this.host.aws.discoverCurrentAccount())?.accountId;
Expand Down Expand Up @@ -274,7 +281,7 @@ async function imageExists(ecr: ECRClient, repositoryName: string, imageTag: str
async function repositoryUri(ecr: ECRClient, repositoryName: string): Promise<string | undefined> {
try {
const command = new DescribeRepositoriesCommand({
repositoryNames: [ repositoryName ],
repositoryNames: [repositoryName],
});

const response = await ecr.send(command);
Expand Down
38 changes: 30 additions & 8 deletions lib/private/handlers/files.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { createReadStream, promises as fs } from 'fs';
import * as path from 'path';
import { FileAssetPackaging, FileSource } from '@aws-cdk/cloud-assembly-schema';
import {
GetBucketEncryptionCommand,
GetBucketLocationCommand,
ListObjectsV2Command,
S3Client,
} from '@aws-sdk/client-s3';
import * as mime from 'mime';
import { FileManifestEntry } from '../../asset-manifest';
import { EventType } from '../../progress';
Expand All @@ -9,7 +15,6 @@ import { IAssetHandler, IHandlerHost } from '../asset-handler';
import { pathExists } from '../fs-extra';
import { replaceAwsPlaceholders } from '../placeholders';
import { shell } from '../shell';
import { GetBucketEncryptionCommand, GetBucketLocationCommand, ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3';

/**
* The size of an empty zip file is 22 bytes
Expand All @@ -36,7 +41,10 @@ export class FileAssetHandler implements IAssetHandler {
const s3Url = `s3://${destination.bucketName}/${destination.objectKey}`;
try {
const s3 = await this.host.aws.s3Client({
...destination,
region: destination.region,
assumeRoleArn: destination.assumeRoleArn,
assumeRoleExternalId: destination.assumeRoleExternalId,
assumeRoleAdditionalOptions: destination.assumeRoleAdditionalOptions,
quiet: true,
});
this.host.emitMessage(EventType.CHECK, `Check ${s3Url}`);
Expand All @@ -54,14 +62,27 @@ export class FileAssetHandler implements IAssetHandler {
public async publish(): Promise<void> {
const destination = await replaceAwsPlaceholders(this.asset.destination, this.host.aws);
const s3Url = `s3://${destination.bucketName}/${destination.objectKey}`;
const s3 = await this.host.aws.s3Client(destination);
const s3 = await this.host.aws.s3Client({
region: destination.region,
assumeRoleArn: destination.assumeRoleArn,
assumeRoleExternalId: destination.assumeRoleExternalId,
assumeRoleAdditionalOptions: destination.assumeRoleAdditionalOptions,
});
this.host.emitMessage(EventType.CHECK, `Check ${s3Url}`);

const bucketInfo = BucketInformation.for(this.host);

// A thunk for describing the current account. Used when we need to format an error
// message, not in the success case.
const account = async () => (await this.host.aws.discoverTargetAccount(destination))?.accountId;
const account = async () =>
(
await this.host.aws.discoverTargetAccount({
region: destination.region,
assumeRoleArn: destination.assumeRoleArn,
assumeRoleExternalId: destination.assumeRoleExternalId,
assumeRoleAdditionalOptions: destination.assumeRoleAdditionalOptions,
})
)?.accountId;
switch (await bucketInfo.bucketOwnership(s3, destination.bucketName)) {
case BucketOwnership.MINE:
break;
Expand Down Expand Up @@ -203,7 +224,7 @@ async function objectExists(s3: S3Client, bucket: string, key: string) {
* never retry building those assets without users having to manually clear
* their bucket, which is a bad experience.
*/
const command = new ListObjectsV2Command({
const command = new ListObjectsV2Command({
Bucket: bucket,
Prefix: key,
MaxKeys: 1,
Expand Down Expand Up @@ -270,7 +291,7 @@ class BucketInformation {

private async _bucketOwnership(s3: S3Client, bucket: string): Promise<BucketOwnership> {
try {
const command = new GetBucketLocationCommand({
const command = new GetBucketLocationCommand({
Bucket: bucket,
});
await s3.send(command);
Expand All @@ -293,8 +314,9 @@ class BucketInformation {
const l = encryption?.ServerSideEncryptionConfiguration?.Rules?.length ?? 0;
if (l > 0) {
const apply =
encryption?.ServerSideEncryptionConfiguration?.Rules?.at(0)
?.ApplyServerSideEncryptionByDefault;
encryption?.ServerSideEncryptionConfiguration?.Rules?.at(
0
)?.ApplyServerSideEncryptionByDefault;
let ssealgo = apply?.SSEAlgorithm;
if (ssealgo === 'AES256') return { type: 'aes256' };
if (ssealgo === 'aws:kms') return { type: 'kms', kmsKeyId: apply?.KMSMasterKeyID };
Expand Down
4 changes: 2 additions & 2 deletions package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading