From a38068bdf7254c470d5835146392f16aaefd8c3b Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Wed, 23 Jun 2021 17:45:50 +0100 Subject: [PATCH 1/4] feat(cdk-assets): externally-configured Docker credentials Currently, `cdk-assets` does a single `docker login` with credentials fetched from ECR's `getAuthorizationToken` API. This enables access to (typically) the assets in the environment's ECR repo (`*--container-assets-*`). A pain point for users today is throttling when using images from other sources, especially from DockerHub when using unauthenticated calls. This change introduces a new configuration file at a well-known location (and overridable via the CDK_DOCKER_CREDS_FILE environment variable), which allows specifying per-domain login credentials via either the default ECR auth tokens or via a secret in SecretsManager. If the credentials file is present, a Docker credential helper (docker-credential-cdk-assets) will be set up for each of the configured domains, and used for the `docker build` commands to enable fetching images from both DockerHub or configured ECR repos. Then the "normal" credentials will be assumed for the final publishing step. For backwards compatibility, if no credentials file is present, the existing `docker login` will be done prior to the build step as usual. This PR will be shortly followed by a corresponding PR for the cdk pipelines library to enable users to specify registries and credentials to be fed into this credentials file during various stages of the pipeline (e.g., build/synth, self-update, and asset publishing). related #10999 related #11774 --- packages/cdk-assets/README.md | 35 +++- .../bin/docker-credential-cdk-assets | 2 + .../bin/docker-credential-cdk-assets.ts | 46 +++++ packages/cdk-assets/bin/publish.ts | 112 +---------- packages/cdk-assets/lib/aws.ts | 107 ++++++++++- .../lib/private/docker-credentials.ts | 83 +++++++++ packages/cdk-assets/lib/private/docker.ts | 76 ++++++-- .../lib/private/handlers/container-images.ts | 15 +- packages/cdk-assets/package.json | 3 +- packages/cdk-assets/test/mock-aws.ts | 5 +- .../test/private/docker-credentials.test.ts | 174 ++++++++++++++++++ 11 files changed, 524 insertions(+), 134 deletions(-) create mode 100755 packages/cdk-assets/bin/docker-credential-cdk-assets create mode 100644 packages/cdk-assets/bin/docker-credential-cdk-assets.ts create mode 100644 packages/cdk-assets/lib/private/docker-credentials.ts create mode 100644 packages/cdk-assets/test/private/docker-credentials.test.ts diff --git a/packages/cdk-assets/README.md b/packages/cdk-assets/README.md index c40afcd00c42d..60379e343a330 100644 --- a/packages/cdk-assets/README.md +++ b/packages/cdk-assets/README.md @@ -42,7 +42,7 @@ itself in the following behaviors: image in the local Docker cache) already exists named after the asset's ID, it will not be packaged, but will be uploaded directly to the destination location. - + For assets build by external utilities, the contract is such that cdk-assets expects the utility to manage dedupe detection as well as path/image tag generation. This means that cdk-assets will call the external utility every time generation @@ -153,3 +153,36 @@ on the AWS SDK (through environment variables or `~/.aws/...` config files). * If `${AWS::Region}` is used, it will principally be replaced with the value in the `region` key. If the default region is intended, leave the `region` key out of the manifest at all. + +## Docker image credentials + +For Docker image asset publishing, `cdk-assets` will `docker login` with +credentials from ECR GetAuthorizationToken prior to building and publishing, so +that the Dockerfile can reference images in the account's ECR repo. + +`cdk-assets` can also be configured to read credentials from both ECR and +SecretsManager prior to build by creating a credential configuration at +'~/.cdk/cdk-docker-creds.json' (override this location by setting the +CDK_DOCKER_CREDS_FILE environment variable). The credentials file has the +following format: + +```json +{ + "version": "1.0", + "domainCredentials": { + "domain1.example.com": { + "secretsManagerSecretId": "mySecret", // Can be the secret ID or full ARN + "roleArn": "arn:aws:iam::0123456789012:role/my-role" // (Optional) role with permissions to the secret + }, + "domain2.example.com": { + "ecrRepository": true, + "roleArn": "arn:aws:iam::0123456789012:role/my-role" // (Optional) role with permissions to the repo + } + } +} +``` + +If the credentials file is present, `docker` will be configured to use the +`docker-credential-cdk-assets` credential helper for each of the domains listed +in the file. This helper will assume the role provided (if present), and then fetch +the login credentials from either SecretsManager or ECR. diff --git a/packages/cdk-assets/bin/docker-credential-cdk-assets b/packages/cdk-assets/bin/docker-credential-cdk-assets new file mode 100755 index 0000000000000..3829057860102 --- /dev/null +++ b/packages/cdk-assets/bin/docker-credential-cdk-assets @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('./docker-credential-cdk-assets.js'); diff --git a/packages/cdk-assets/bin/docker-credential-cdk-assets.ts b/packages/cdk-assets/bin/docker-credential-cdk-assets.ts new file mode 100644 index 0000000000000..9ed27331ee381 --- /dev/null +++ b/packages/cdk-assets/bin/docker-credential-cdk-assets.ts @@ -0,0 +1,46 @@ +/** + * Docker Credential Helper to retrieve credentials based on an external configuration file. + * Supports loading credentials from ECR repositories and from Secrets Manager, + * optionally via an assumed role. + * + * The only operation currently supported by this credential helper at this time is the `get` + * command, which receives a domain name as input on stdin and returns a Username/Secret in + * JSON format on stdout. + * + * IMPORTANT - The credential helper must not output anything else besides the final credentials + * in any success case; doing so breaks docker's parsing of the output and causes the login to fail. + */ + +import * as fs from 'fs'; +import { DefaultAwsClient } from '../lib'; + +import { cdkCredentialsConfig, cdkCredentialsConfigFile, fetchDockerLoginCredentials } from '../lib/private/docker-credentials'; + +async function main() { + // Expected invocation is [node, docker-credential-cdk-assets, get] with input fed via STDIN + // For other valid docker commands (store, list, erase), we no-op. + if (process.argv.length !== 3 || process.argv[2] !== 'get') { + process.exit(0); + } + + const config = cdkCredentialsConfig(); + if (!config) { + throw new Error(`unable to find CDK Docker credentials at: ${cdkCredentialsConfigFile()}`); + } + + // Read the domain to fetch from stdin + let rawDomain = fs.readFileSync(0, { encoding: 'utf-8' }).trim(); + rawDomain = rawDomain.includes('://') ? rawDomain : `https://${rawDomain}`; + const domain = new URL(rawDomain).hostname; + + const credentials = await fetchDockerLoginCredentials(new DefaultAwsClient(), config, domain); + + // Write the credentials back to stdout + fs.writeFileSync(1, JSON.stringify(credentials)); +} + +main().catch(e => { + // eslint-disable-next-line no-console + console.error(e.stack); + process.exitCode = 1; +}); diff --git a/packages/cdk-assets/bin/publish.ts b/packages/cdk-assets/bin/publish.ts index e8d251cf82b97..a6a9a81af22ee 100644 --- a/packages/cdk-assets/bin/publish.ts +++ b/packages/cdk-assets/bin/publish.ts @@ -1,10 +1,8 @@ -import * as os from 'os'; import { - AssetManifest, AssetPublishing, ClientOptions, DestinationPattern, EventType, IAws, + AssetManifest, AssetPublishing, DefaultAwsClient, DestinationPattern, EventType, IPublishProgress, IPublishProgressListener, } from '../lib'; -import { Account } from '../lib/aws'; -import { log, LogLevel, VERSION } from './logging'; +import { log, LogLevel } from './logging'; export async function publish(args: { path: string; @@ -56,109 +54,3 @@ class ConsoleProgress implements IPublishProgressListener { log(EVENT_TO_LEVEL[type], `[${event.percentComplete}%] ${type}: ${event.message}`); } } - -/** - * AWS client using the AWS SDK for JS with no special configuration - */ -class DefaultAwsClient implements IAws { - private readonly AWS: typeof import('aws-sdk'); - private account?: Account; - - constructor(profile?: string) { - // Force AWS SDK to look in ~/.aws/credentials and potentially use the configured profile. - process.env.AWS_SDK_LOAD_CONFIG = '1'; - process.env.AWS_STS_REGIONAL_ENDPOINTS = 'regional'; - process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = '1'; - if (profile) { - process.env.AWS_PROFILE = profile; - } - - // We need to set the environment before we load this library for the first time. - // eslint-disable-next-line @typescript-eslint/no-require-imports - this.AWS = require('aws-sdk'); - } - - public async s3Client(options: ClientOptions) { - return new this.AWS.S3(await this.awsOptions(options)); - } - - public async ecrClient(options: ClientOptions) { - return new this.AWS.ECR(await this.awsOptions(options)); - } - - public async discoverPartition(): Promise { - return (await this.discoverCurrentAccount()).partition; - } - - public async discoverDefaultRegion(): Promise { - return this.AWS.config.region || 'us-east-1'; - } - - public async discoverCurrentAccount(): Promise { - if (this.account === undefined) { - const sts = new this.AWS.STS(); - const response = await sts.getCallerIdentity().promise(); - if (!response.Account || !response.Arn) { - log('error', `Unrecognized reponse from STS: '${JSON.stringify(response)}'`); - throw new Error('Unrecognized reponse from STS'); - } - this.account = { - accountId: response.Account!, - partition: response.Arn!.split(':')[1], - }; - } - - return this.account; - } - - private async awsOptions(options: ClientOptions) { - let credentials; - - if (options.assumeRoleArn) { - credentials = await this.assumeRole(options.region, options.assumeRoleArn, options.assumeRoleExternalId); - } - - return { - region: options.region, - customUserAgent: `cdk-assets/${VERSION}`, - credentials, - }; - } - - /** - * Explicit manual AssumeRole call - * - * Necessary since I can't seem to get the built-in support for ChainableTemporaryCredentials to work. - * - * It needs an explicit configuration of `masterCredentials`, we need to put - * a `DefaultCredentialProverChain()` in there but that is not possible. - */ - private async assumeRole(region: string | undefined, roleArn: string, externalId?: string): Promise { - const msg = [ - `Assume ${roleArn}`, - ...externalId ? [`(ExternalId ${externalId})`] : [], - ]; - log('verbose', msg.join(' ')); - - return new this.AWS.ChainableTemporaryCredentials({ - params: { - RoleArn: roleArn, - ExternalId: externalId, - RoleSessionName: `cdk-assets-${safeUsername()}`, - }, - stsConfig: { - region, - customUserAgent: `cdk-assets/${VERSION}`, - }, - }); - } -} - -/** - * Return the username with characters invalid for a RoleSessionName removed - * - * @see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html#API_AssumeRole_RequestParameters - */ -function safeUsername() { - return os.userInfo().username.replace(/[^\w+=,.@-]/g, '@'); -} \ No newline at end of file diff --git a/packages/cdk-assets/lib/aws.ts b/packages/cdk-assets/lib/aws.ts index 40609eb155af7..936f0d94954e6 100644 --- a/packages/cdk-assets/lib/aws.ts +++ b/packages/cdk-assets/lib/aws.ts @@ -1,4 +1,4 @@ -import * as AWS from 'aws-sdk'; +import * as os from 'os'; /** * AWS SDK operations required by Asset Publishing @@ -10,6 +10,7 @@ export interface IAws { s3Client(options: ClientOptions): Promise; ecrClient(options: ClientOptions): Promise; + secretsManagerClient(options: ClientOptions): Promise; } export interface ClientOptions { @@ -35,3 +36,107 @@ export interface Account { */ readonly partition: string; } + +/** + * AWS client using the AWS SDK for JS with no special configuration + */ +export class DefaultAwsClient implements IAws { + private readonly AWS: typeof import('aws-sdk'); + private account?: Account; + + constructor(profile?: string) { + // Force AWS SDK to look in ~/.aws/credentials and potentially use the configured profile. + process.env.AWS_SDK_LOAD_CONFIG = '1'; + process.env.AWS_STS_REGIONAL_ENDPOINTS = 'regional'; + process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = '1'; + if (profile) { + process.env.AWS_PROFILE = profile; + } + + // We need to set the environment before we load this library for the first time. + // eslint-disable-next-line @typescript-eslint/no-require-imports + this.AWS = require('aws-sdk'); + } + + public async s3Client(options: ClientOptions) { + return new this.AWS.S3(await this.awsOptions(options)); + } + + public async ecrClient(options: ClientOptions) { + return new this.AWS.ECR(await this.awsOptions(options)); + } + + public async secretsManagerClient(options: ClientOptions) { + return new this.AWS.SecretsManager(await this.awsOptions(options)); + } + + public async discoverPartition(): Promise { + return (await this.discoverCurrentAccount()).partition; + } + + public async discoverDefaultRegion(): Promise { + return this.AWS.config.region || 'us-east-1'; + } + + public async discoverCurrentAccount(): Promise { + if (this.account === undefined) { + const sts = new this.AWS.STS(); + const response = await sts.getCallerIdentity().promise(); + if (!response.Account || !response.Arn) { + throw new Error(`Unrecognized reponse from STS: '${JSON.stringify(response)}'`); + } + this.account = { + accountId: response.Account!, + partition: response.Arn!.split(':')[1], + }; + } + + return this.account; + } + + private async awsOptions(options: ClientOptions) { + let credentials; + + if (options.assumeRoleArn) { + credentials = await this.assumeRole(options.region, options.assumeRoleArn, options.assumeRoleExternalId); + } + + return { + region: options.region, + customUserAgent: 'cdk-assets', + credentials, + }; + } + + /** + * Explicit manual AssumeRole call + * + * Necessary since I can't seem to get the built-in support for ChainableTemporaryCredentials to work. + * + * It needs an explicit configuration of `masterCredentials`, we need to put + * a `DefaultCredentialProverChain()` in there but that is not possible. + */ + private async assumeRole(region: string | undefined, roleArn: string, externalId?: string): Promise { + return new this.AWS.ChainableTemporaryCredentials({ + params: { + RoleArn: roleArn, + ExternalId: externalId, + RoleSessionName: `cdk-assets-${safeUsername()}`, + }, + stsConfig: { + region, + customUserAgent: 'cdk-assets', + }, + }); + } +} + +/** + * Return the username with characters invalid for a RoleSessionName removed + * + * @see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html#API_AssumeRole_RequestParameters + */ +function safeUsername() { + return os.userInfo().username.replace(/[^\w+=,.@-]/g, '@'); +} + diff --git a/packages/cdk-assets/lib/private/docker-credentials.ts b/packages/cdk-assets/lib/private/docker-credentials.ts new file mode 100644 index 0000000000000..8998c3eeb0751 --- /dev/null +++ b/packages/cdk-assets/lib/private/docker-credentials.ts @@ -0,0 +1,83 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { IAws } from '../aws'; +import { Logger } from './shell'; + +export interface DockerCredentials { + readonly Username: string; + readonly Secret: string; +} + +export interface DockerCredentialsConfig { + readonly version: string; + readonly domainCredentials: Record; +} + +export interface DockerDomainCredentialSource { + readonly secretsManagerSecretId?: string; + readonly ecrRepository?: boolean; + readonly assumeRoleArn?: string; +} + +/** Returns the presumed location of the CDK Docker credentials config file */ +export function cdkCredentialsConfigFile(): string { + return process.env.CDK_DOCKER_CREDS_FILE ?? path.join((os.userInfo().homedir ?? os.homedir()).trim() || '/', '.cdk', 'cdk-docker-creds.json'); +} + +let _cdkCredentials: DockerCredentialsConfig | undefined; +/** Loads and parses the CDK Docker credentials configuration, if it exists. */ +export function cdkCredentialsConfig(): DockerCredentialsConfig | undefined { + if (!_cdkCredentials) { + try { + _cdkCredentials = JSON.parse(fs.readFileSync(cdkCredentialsConfigFile(), { encoding: 'utf-8' })) as DockerCredentialsConfig; + } catch (err) { } + } + return _cdkCredentials; +} + +/** Fetches login credentials from the configured source (e.g., SecretsManager, ECR) */ +export async function fetchDockerLoginCredentials(aws: IAws, config: DockerCredentialsConfig, domain: string) { + if (!Object.keys(config.domainCredentials).includes(domain)) { + throw new Error(`unknown domain ${domain}`); + } + + const domainConfig = config.domainCredentials[domain]; + + if (domainConfig.secretsManagerSecretId) { + const sm = await aws.secretsManagerClient({ assumeRoleArn: domainConfig.assumeRoleArn }); + const secretValue = await sm.getSecretValue({ SecretId: domainConfig.secretsManagerSecretId }).promise(); + if (!secretValue.SecretString) { throw new Error(`unable to fetch SecretString from secret: ${domainConfig.secretsManagerSecretId}`); }; + + const secret = JSON.parse(secretValue.SecretString); + if (!secret.username || !secret.secret) { + throw new Error('malformed secret string ("username" or "secret" field missing)'); + } + + return { Username: secret.username, Secret: secret.secret }; + } else if (domainConfig.ecrRepository) { + const ecr = await aws.ecrClient({ assumeRoleArn: domainConfig.assumeRoleArn }); + const ecrAuthData = await obtainEcrCredentials(ecr); + + return { Username: ecrAuthData.username, Secret: ecrAuthData.password }; + } else { + throw new Error('unknown credential type: no secret ID or ECR repo'); + } +} + +export async function obtainEcrCredentials(ecr: AWS.ECR, logger?: Logger) { + if (logger) { logger('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(':'); + if (!username || !password) { throw new Error('unexpected ECR authData format'); } + + return { + username, + password, + endpoint: authData[0].proxyEndpoint!, + }; +} diff --git a/packages/cdk-assets/lib/private/docker.ts b/packages/cdk-assets/lib/private/docker.ts index eef2bd4392933..b928d5435e0c9 100644 --- a/packages/cdk-assets/lib/private/docker.ts +++ b/packages/cdk-assets/lib/private/docker.ts @@ -1,4 +1,7 @@ -// import * as os from 'os'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { cdkCredentialsConfig, obtainEcrCredentials } from './docker-credentials'; import { Logger, shell, ShellOptions } from './shell'; interface BuildOptions { @@ -13,7 +16,20 @@ interface BuildOptions { readonly buildArgs?: Record; } +export interface DockerCredentialsConfig { + readonly version: string; + readonly domainCredentials: Record; +} + +export interface DockerDomainCredentials { + readonly secretsManagerSecretId?: string; + readonly ecrRepository?: string; +} + export class Docker { + + private configDir: string | undefined = undefined; + constructor(private readonly logger?: Logger) { } @@ -70,9 +86,49 @@ export class Docker { await this.execute(['push', tag]); } + /** + * If a CDK Docker Credentials file exists, creates a new Docker config directory. + * Sets up `docker-credential-cdk-assets` to be the credential helper for each domain in the CDK config. + * All future commands (e.g., `build`, `push`) will use this config. + * + * See https://docs.docker.com/engine/reference/commandline/login/#credential-helpers for more details on cred helpers. + * + * @returns true if CDK config was found and configured, false otherwise + */ + public configureCdkCredentials(): boolean { + const config = cdkCredentialsConfig(); + if (!config) { return false; } + + const configDir = this.createCleanConfig(); + + const domains = Object.keys(config.domainCredentials); + const credHelpers = domains.reduce(function(map: Record, domain) { + map[domain] = 'cdk-assets'; // Use docker-credential-cdk-assets for this domain + return map; + }, {}); + fs.writeFileSync(path.join(configDir, 'config.json'), JSON.stringify({ credHelpers }), { encoding: 'utf-8' }); + + return true; + } + + /** + * Creates a new empty Docker config directory. + * All future commands (e.g., `build`, `push`) will use this config. + * + * This is useful after calling `configureCdkCredentials` to reset to default credentials. + * + * @returns the path to the directory + */ + public createCleanConfig(): string { + this.configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdkDockerConfig')); + return this.configDir; + } + private async execute(args: string[], options: ShellOptions = {}) { + const configArgs = this.configDir ? ['--config', this.configDir] : []; + try { - await shell(['docker', ...args], { logger: this.logger, ...options }); + await shell(['docker', ...configArgs, ...args], { logger: this.logger, ...options }); } catch (e) { if (e.code === 'ENOENT') { throw new Error('Unable to execute \'docker\' in order to build a container asset. Please install \'docker\' and try again.'); @@ -82,22 +138,6 @@ export class Docker { } } -async function obtainEcrCredentials(ecr: AWS.ECR, logger?: Logger) { - if (logger) { logger('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 { - username, - password, - endpoint: authData[0].proxyEndpoint!, - }; -} - function flatten(x: string[][]) { return Array.prototype.concat([], ...x); } diff --git a/packages/cdk-assets/lib/private/handlers/container-images.ts b/packages/cdk-assets/lib/private/handlers/container-images.ts index 1d2b5bedee38e..5efc0d37928bb 100644 --- a/packages/cdk-assets/lib/private/handlers/container-images.ts +++ b/packages/cdk-assets/lib/private/handlers/container-images.ts @@ -31,8 +31,13 @@ export class ContainerImageAssetHandler implements IAssetHandler { if (await this.destinationAlreadyExists(ecr, destination, imageUri)) { return; } if (this.host.aborted) { return; } - // Login before build so that the Dockerfile can reference images in the ECR repo - await this.docker.login(ecr); + // Default behavior is to login before build so that the Dockerfile can reference images in the ECR repo + // However, if we're in a pipelines environment (for example), + // we may have alternative credentials to the default ones to use for the build itself. + // If the special config file is present, delay the login to the default credentials until the push. + // If the config file is present, we will configure and use those credentials for the build. + let cdkDockerCredentialsConfigured = this.docker.configureCdkCredentials(); + if (!cdkDockerCredentialsConfigured) { await this.docker.login(ecr); } const localTagName = this.asset.source.executable ? await this.buildExternalAsset(this.asset.source.executable) @@ -45,6 +50,12 @@ export class ContainerImageAssetHandler implements IAssetHandler { this.host.emitMessage(EventType.UPLOAD, `Push ${imageUri}`); if (this.host.aborted) { return; } await this.docker.tag(localTagName, imageUri); + + if (cdkDockerCredentialsConfigured) { + this.docker.createCleanConfig(); + await this.docker.login(ecr); + } + await this.docker.push(imageUri); } diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index 36cb5b5d542e4..2ba132a9149f7 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -5,7 +5,8 @@ "main": "lib/index.js", "types": "lib/index.d.ts", "bin": { - "cdk-assets": "bin/cdk-assets" + "cdk-assets": "bin/cdk-assets", + "docker-credential-cdk-assets": "bin/docker-credential-cdk-assets" }, "scripts": { "build": "cdk-build", diff --git a/packages/cdk-assets/test/mock-aws.ts b/packages/cdk-assets/test/mock-aws.ts index 262ab495bd430..2d40f7c756a28 100644 --- a/packages/cdk-assets/test/mock-aws.ts +++ b/packages/cdk-assets/test/mock-aws.ts @@ -4,6 +4,7 @@ import * as AWS from 'aws-sdk'; export function mockAws() { const mockEcr = new AWS.ECR(); const mockS3 = new AWS.S3(); + const mockSecretsManager = new AWS.SecretsManager(); // Sane defaults which can be overridden mockS3.getBucketLocation = mockedApiResult({}); @@ -18,11 +19,13 @@ export function mockAws() { return { mockEcr, mockS3, + mockSecretsManager, discoverPartition: jest.fn(() => Promise.resolve('swa')), discoverCurrentAccount: jest.fn(() => Promise.resolve({ accountId: 'current_account', partition: 'swa' })), discoverDefaultRegion: jest.fn(() => Promise.resolve('current_region')), ecrClient: jest.fn(() => Promise.resolve(mockEcr)), s3Client: jest.fn(() => Promise.resolve(mockS3)), + secretsManagerClient: jest.fn(() => Promise.resolve(mockSecretsManager)), }; } @@ -65,4 +68,4 @@ export function mockUpload(expectContent?: string) { }); }), })); -} \ No newline at end of file +} diff --git a/packages/cdk-assets/test/private/docker-credentials.test.ts b/packages/cdk-assets/test/private/docker-credentials.test.ts new file mode 100644 index 0000000000000..566a1aa663840 --- /dev/null +++ b/packages/cdk-assets/test/private/docker-credentials.test.ts @@ -0,0 +1,174 @@ +import * as os from 'os'; +import * as path from 'path'; +import * as mockfs from 'mock-fs'; +import { cdkCredentialsConfig, cdkCredentialsConfigFile, DockerCredentialsConfig, fetchDockerLoginCredentials } from '../../lib/private/docker-credentials'; +import { mockAws, mockedApiFailure, mockedApiResult } from '../mock-aws'; + +const _ENV = process.env; + +let aws: ReturnType; +beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + + aws = mockAws(); + + process.env = { ..._ENV }; +}); + +afterEach(() => { + mockfs.restore(); + process.env = _ENV; +}); + +describe('cdkCredentialsConfigFile', () => { + test('Can be overridden by CDK_DOCKER_CREDS_FILE', () => { + const credsFile = '/tmp/insertfilenamehere_cdk_config.json'; + process.env.CDK_DOCKER_CREDS_FILE = credsFile; + + expect(cdkCredentialsConfigFile()).toEqual(credsFile); + }); + + test('Uses homedir if no process env is set', () => { + expect(cdkCredentialsConfigFile()).toEqual(path.join(os.userInfo().homedir, '.cdk', 'cdk-docker-creds.json')); + }); +}); + +describe('cdkCredentialsConfig', () => { + const credsFile = '/tmp/foo/bar/does/not/exist/config.json'; + beforeEach(() => { process.env.CDK_DOCKER_CREDS_FILE = credsFile; }); + + test('returns undefined if no config exists', () => { + expect(cdkCredentialsConfig()).toBeUndefined(); + }); + + test('returns parsed config if it exists', () => { + mockfs({ + [credsFile]: JSON.stringify({ + version: '0.1', + domainCredentials: { + 'test1.example.com': { secretsManagerSecretId: 'mySecret' }, + 'test2.example.com': { ecrRepository: 'arn:aws:ecr:bar' }, + }, + }), + }); + + const config = cdkCredentialsConfig(); + expect(config).toBeDefined(); + expect(config?.version).toEqual('0.1'); + expect(config?.domainCredentials['test1.example.com']?.secretsManagerSecretId).toEqual('mySecret'); + expect(config?.domainCredentials['test2.example.com']?.ecrRepository).toEqual('arn:aws:ecr:bar'); + }); +}); + +describe('fetchDockerLoginCredentials', () => { + let config: DockerCredentialsConfig; + + beforeEach(() => { + config = { + version: '0.1', + domainCredentials: { + 'misconfigured.example.com': {}, + 'secret.example.com': { secretsManagerSecretId: 'mySecret' }, + 'secretwithrole.example.com': { + secretsManagerSecretId: 'mySecret', + assumeRoleArn: 'arn:aws:iam::0123456789012:role/my-role', + }, + 'ecr.example.com': { ecrRepository: true }, + 'ecrwithrole.example.com': { + ecrRepository: true, + assumeRoleArn: 'arn:aws:iam::0123456789012:role/my-role', + }, + }, + }; + }); + + test('throws on unknown domain', async () => { + await expect(fetchDockerLoginCredentials(aws, config, 'unknowndomain.example.com')).rejects.toThrow(/unknown domain/); + }); + + test('throws on misconfigured domain (no ECR or SM)', async () => { + await expect(fetchDockerLoginCredentials(aws, config, 'misconfigured.example.com')).rejects.toThrow(/unknown credential type/); + }); + + describe('SecretsManager', () => { + test('returns the credentials sucessfully if configured correctly', async () => { + mockSecretWithSecretString({ username: 'secretUser', secret: 'secretPass' }); + + const creds = await fetchDockerLoginCredentials(aws, config, 'secret.example.com'); + + expect(creds).toEqual({ Username: 'secretUser', Secret: 'secretPass' }); + }); + + test('throws when SecretsManager returns an error', async () => { + const errMessage = "Secrets Manager can't find the specified secret."; + aws.mockSecretsManager.getSecretValue = mockedApiFailure('ResourceNotFoundException', errMessage); + + await expect(fetchDockerLoginCredentials(aws, config, 'secret.example.com')).rejects.toThrow(errMessage); + }); + + test('throws when secret does not have the correct fields - key/value', async () => { + mockSecretWithSecretString({ principal: 'foo', credential: 'bar' }); + + await expect(fetchDockerLoginCredentials(aws, config, 'secret.example.com')).rejects.toThrow(/malformed secret string/); + }); + + test('throws when secret does not have the correct fields - plaintext', async () => { + mockSecretWithSecretString('myAPIKey'); + + await expect(fetchDockerLoginCredentials(aws, config, 'secret.example.com')).rejects.toThrow(/malformed secret string/); + }); + }); + + describe('ECR getAuthorizationToken', () => { + test('returns the credentials successfully', async () => { + mockEcrAuthorizationData(Buffer.from('myFoo:myBar', 'utf-8').toString('base64')); + + const creds = await fetchDockerLoginCredentials(aws, config, 'ecr.example.com'); + + expect(creds).toEqual({ Username: 'myFoo', Secret: 'myBar' }); + }); + + test('throws if ECR errors', async () => { + aws.mockEcr.getAuthorizationToken = mockedApiFailure('ServerException', 'uhoh'); + + await expect(fetchDockerLoginCredentials(aws, config, 'ecr.example.com')).rejects.toThrow(/uhoh/); + }); + + test('throws if ECR returns no authData', async () => { + aws.mockEcr.getAuthorizationToken = mockedApiResult({ authorizationData: [] }); + + await expect(fetchDockerLoginCredentials(aws, config, 'ecr.example.com')).rejects.toThrow(/No authorization data received from ECR/); + }); + + test('throws if ECR authData is in an incorrect format', async () => { + mockEcrAuthorizationData('notabase64encodedstring'); + + await expect(fetchDockerLoginCredentials(aws, config, 'ecr.example.com')).rejects.toThrow(/unexpected ECR authData format/); + }); + }); + +}); + +function mockSecretWithSecretString(secretString: any) { + aws.mockSecretsManager.getSecretValue = mockedApiResult({ + ARN: 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:mySecret', + Name: 'mySecret', + VersionId: 'fa81fe61-c167-4aca-969e-4d8df74d4814', + SecretString: JSON.stringify(secretString), + VersionStages: [ + 'AWSCURRENT', + ], + }); +} + +function mockEcrAuthorizationData(authorizationToken: string) { + aws.mockEcr.getAuthorizationToken = mockedApiResult({ + authorizationData: [ + { + authorizationToken, + proxyEndpoint: 'https://0123456789012.dkr.ecr.eu-west-1.amazonaws.com', + }, + ], + }); +} From d8d265fb5c288a30dca9207482e47ea48b8d92aa Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Thu, 24 Jun 2021 12:06:22 +0100 Subject: [PATCH 2/4] update aws-cdk ISDK/MockSdk to match cdk-assets IAws --- packages/aws-cdk/lib/api/aws-auth/sdk.ts | 5 +++++ packages/aws-cdk/lib/util/asset-publishing.ts | 4 ++++ packages/aws-cdk/test/util/mock-sdk.ts | 1 + 3 files changed, 10 insertions(+) diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 888901a8c33bf..ee21d8c17111b 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -29,6 +29,7 @@ export interface ISDK { route53(): AWS.Route53; ecr(): AWS.ECR; elbv2(): AWS.ELBv2; + secretsManager(): AWS.SecretsManager; } /** @@ -113,6 +114,10 @@ export class SDK implements ISDK { return this.wrapServiceErrorHandling(new AWS.ELBv2(this.config)); } + public secretsManager(): AWS.SecretsManager { + return this.wrapServiceErrorHandling(new AWS.SecretsManager(this.config)); + } + public async currentAccount(): Promise { // Get/refresh if necessary before we can access `accessKeyId` await this.forceCredentialRetrieval(); diff --git a/packages/aws-cdk/lib/util/asset-publishing.ts b/packages/aws-cdk/lib/util/asset-publishing.ts index eb929bf03b4f3..3635d2b639002 100644 --- a/packages/aws-cdk/lib/util/asset-publishing.ts +++ b/packages/aws-cdk/lib/util/asset-publishing.ts @@ -59,6 +59,10 @@ class PublishingAws implements cdk_assets.IAws { return (await this.sdk(options)).ecr(); } + public async secretsManagerClient(options: cdk_assets.ClientOptions): Promise { + return (await this.sdk(options)).secretsManager(); + } + /** * Get an SDK appropriate for the given client options */ diff --git a/packages/aws-cdk/test/util/mock-sdk.ts b/packages/aws-cdk/test/util/mock-sdk.ts index edb7be0445626..90392ee14f3c9 100644 --- a/packages/aws-cdk/test/util/mock-sdk.ts +++ b/packages/aws-cdk/test/util/mock-sdk.ts @@ -108,6 +108,7 @@ export class MockSdk implements ISDK { public readonly route53 = jest.fn(); public readonly ecr = jest.fn(); public readonly elbv2 = jest.fn(); + public readonly secretsManager = jest.fn(); public currentAccount(): Promise { return Promise.resolve({ accountId: '123456789012', partition: 'aws' }); From c2082424330e5d5269e6e06f8386cb1ac7b14a2a Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Thu, 24 Jun 2021 14:59:35 +0100 Subject: [PATCH 3/4] PR feedback from Rico --- .../bin/docker-credential-cdk-assets.ts | 2 ++ .../lib/private/docker-credentials.ts | 11 ++++-- packages/cdk-assets/lib/private/docker.ts | 8 +++-- .../lib/private/handlers/container-images.ts | 2 +- .../cdk-assets/test/docker-images.test.ts | 36 +++++++++++++++++++ .../test/private/docker-credentials.test.ts | 32 +++++++++++++++++ 6 files changed, 84 insertions(+), 7 deletions(-) diff --git a/packages/cdk-assets/bin/docker-credential-cdk-assets.ts b/packages/cdk-assets/bin/docker-credential-cdk-assets.ts index 9ed27331ee381..b04f2ba8510bc 100644 --- a/packages/cdk-assets/bin/docker-credential-cdk-assets.ts +++ b/packages/cdk-assets/bin/docker-credential-cdk-assets.ts @@ -30,6 +30,8 @@ async function main() { // Read the domain to fetch from stdin let rawDomain = fs.readFileSync(0, { encoding: 'utf-8' }).trim(); + // Paranoid handling to ensure new URL() doesn't throw if the schema is missing. + // Not convinced docker will ever pass in a url like 'index.docker.io/v1', but just in case... rawDomain = rawDomain.includes('://') ? rawDomain : `https://${rawDomain}`; const domain = new URL(rawDomain).hostname; diff --git a/packages/cdk-assets/lib/private/docker-credentials.ts b/packages/cdk-assets/lib/private/docker-credentials.ts index 8998c3eeb0751..b5c3f42139581 100644 --- a/packages/cdk-assets/lib/private/docker-credentials.ts +++ b/packages/cdk-assets/lib/private/docker-credentials.ts @@ -16,6 +16,8 @@ export interface DockerCredentialsConfig { export interface DockerDomainCredentialSource { readonly secretsManagerSecretId?: string; + readonly secretsUsernameField?: string; + readonly secretsPasswordField?: string; readonly ecrRepository?: boolean; readonly assumeRoleArn?: string; } @@ -50,11 +52,14 @@ export async function fetchDockerLoginCredentials(aws: IAws, config: DockerCrede if (!secretValue.SecretString) { throw new Error(`unable to fetch SecretString from secret: ${domainConfig.secretsManagerSecretId}`); }; const secret = JSON.parse(secretValue.SecretString); - if (!secret.username || !secret.secret) { - throw new Error('malformed secret string ("username" or "secret" field missing)'); + + const usernameField = domainConfig.secretsUsernameField ?? 'username'; + const secretField = domainConfig.secretsPasswordField ?? 'secret'; + if (!secret[usernameField] || !secret[secretField]) { + throw new Error(`malformed secret string ("${usernameField}" or "${secretField}" field missing)`); } - return { Username: secret.username, Secret: secret.secret }; + return { Username: secret[usernameField], Secret: secret[secretField] }; } else if (domainConfig.ecrRepository) { const ecr = await aws.ecrClient({ assumeRoleArn: domainConfig.assumeRoleArn }); const ecrAuthData = await obtainEcrCredentials(ecr); diff --git a/packages/cdk-assets/lib/private/docker.ts b/packages/cdk-assets/lib/private/docker.ts index b928d5435e0c9..3392fe6915a64 100644 --- a/packages/cdk-assets/lib/private/docker.ts +++ b/packages/cdk-assets/lib/private/docker.ts @@ -99,14 +99,16 @@ export class Docker { const config = cdkCredentialsConfig(); if (!config) { return false; } - const configDir = this.createCleanConfig(); + this.resetAuthPlugins(); + // Should never happen; means resetAuthPlugins is broken. + if (!this.configDir) { throw new Error('no active docker config directory selected'); } const domains = Object.keys(config.domainCredentials); const credHelpers = domains.reduce(function(map: Record, domain) { map[domain] = 'cdk-assets'; // Use docker-credential-cdk-assets for this domain return map; }, {}); - fs.writeFileSync(path.join(configDir, 'config.json'), JSON.stringify({ credHelpers }), { encoding: 'utf-8' }); + fs.writeFileSync(path.join(this.configDir, 'config.json'), JSON.stringify({ credHelpers }), { encoding: 'utf-8' }); return true; } @@ -119,7 +121,7 @@ export class Docker { * * @returns the path to the directory */ - public createCleanConfig(): string { + public resetAuthPlugins() { this.configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdkDockerConfig')); return this.configDir; } diff --git a/packages/cdk-assets/lib/private/handlers/container-images.ts b/packages/cdk-assets/lib/private/handlers/container-images.ts index 5efc0d37928bb..ec4327775c37c 100644 --- a/packages/cdk-assets/lib/private/handlers/container-images.ts +++ b/packages/cdk-assets/lib/private/handlers/container-images.ts @@ -52,7 +52,7 @@ export class ContainerImageAssetHandler implements IAssetHandler { await this.docker.tag(localTagName, imageUri); if (cdkDockerCredentialsConfigured) { - this.docker.createCleanConfig(); + this.docker.resetAuthPlugins(); await this.docker.login(ecr); } diff --git a/packages/cdk-assets/test/docker-images.test.ts b/packages/cdk-assets/test/docker-images.test.ts index 8dc4f70b7726a..4cd0b2e2004ed 100644 --- a/packages/cdk-assets/test/docker-images.test.ts +++ b/packages/cdk-assets/test/docker-images.test.ts @@ -1,16 +1,22 @@ jest.mock('child_process'); +import * as fs from 'fs'; import { Manifest } from '@aws-cdk/cloud-assembly-schema'; import * as mockfs from 'mock-fs'; import { AssetManifest, AssetPublishing } from '../lib'; +import * as dockercreds from '../lib/private/docker-credentials'; import { mockAws, mockedApiFailure, mockedApiResult } from './mock-aws'; import { mockSpawn } from './mock-child_process'; + let aws: ReturnType; const absoluteDockerPath = '/simple/cdk.out/dockerdir'; beforeEach(() => { jest.resetAllMocks(); + // By default, assume no externally-configured credentials. + jest.spyOn(dockercreds, 'cdkCredentialsConfig').mockReturnValue(undefined); + mockfs({ '/simple/cdk.out/assets.json': JSON.stringify({ version: Manifest.version(), @@ -216,3 +222,33 @@ test('correctly identify Docker directory if path is absolute', async () => { expect(true).toBeTruthy(); // Expect no exception, satisfy linter expectAllSpawns(); }); + +test('when external credentials are present, explicit Docker config directories are used', async () => { + // Setup -- Mock that we have CDK credentials, and mock fs operations. + jest.spyOn(dockercreds, 'cdkCredentialsConfig').mockReturnValue({ version: '0.1', domainCredentials: {} }); + jest.spyOn(fs, 'mkdtempSync').mockImplementationOnce(() => '/tmp/mockedTempDir'); + jest.spyOn(fs, 'mkdtempSync').mockImplementationOnce(() => '/tmp/otherMockedTempDir'); + jest.spyOn(fs, 'writeFileSync').mockImplementation(jest.fn()); + + let pub = new AssetPublishing(AssetManifest.fromPath('/simple/cdk.out'), { aws }); + aws.mockEcr.describeImages = mockedApiFailure('ImageNotFoundException', 'File does not exist'); + aws.mockEcr.getAuthorizationToken = mockedApiResult({ + authorizationData: [ + { authorizationToken: 'dXNlcjpwYXNz', proxyEndpoint: 'https://proxy.com/' }, + ], + }); + + const expectAllSpawns = mockSpawn( + // Initally use the first created directory with the CDK credentials + { commandLine: ['docker', '--config', '/tmp/mockedTempDir', 'inspect', 'cdkasset-theasset'], exitCode: 1 }, + { commandLine: ['docker', '--config', '/tmp/mockedTempDir', 'build', '--tag', 'cdkasset-theasset', '.'], cwd: absoluteDockerPath }, + { commandLine: ['docker', '--config', '/tmp/mockedTempDir', 'tag', 'cdkasset-theasset', '12345.amazonaws.com/repo:abcdef'] }, + // Prior to push, change to a new (fresh) config directory + { commandLine: ['docker', '--config', '/tmp/otherMockedTempDir', 'login'], prefix: true }, + { commandLine: ['docker', '--config', '/tmp/otherMockedTempDir', 'push', '12345.amazonaws.com/repo:abcdef'] }, + ); + + await pub.publish(); + + expectAllSpawns(); +}); diff --git a/packages/cdk-assets/test/private/docker-credentials.test.ts b/packages/cdk-assets/test/private/docker-credentials.test.ts index 566a1aa663840..6b521c67457b6 100644 --- a/packages/cdk-assets/test/private/docker-credentials.test.ts +++ b/packages/cdk-assets/test/private/docker-credentials.test.ts @@ -74,6 +74,12 @@ describe('fetchDockerLoginCredentials', () => { secretsManagerSecretId: 'mySecret', assumeRoleArn: 'arn:aws:iam::0123456789012:role/my-role', }, + 'secretwithcustomfields.example.com': { + secretsManagerSecretId: 'mySecret', + secretsUsernameField: 'name', + secretsPasswordField: 'apiKey', + assumeRoleArn: 'arn:aws:iam::0123456789012:role/my-role', + }, 'ecr.example.com': { ecrRepository: true }, 'ecrwithrole.example.com': { ecrRepository: true, @@ -107,6 +113,23 @@ describe('fetchDockerLoginCredentials', () => { await expect(fetchDockerLoginCredentials(aws, config, 'secret.example.com')).rejects.toThrow(errMessage); }); + test('supports assuming a role', async () => { + mockSecretWithSecretString({ username: 'secretUser', secret: 'secretPass' }); + + const creds = await fetchDockerLoginCredentials(aws, config, 'secretwithrole.example.com'); + + expect(creds).toEqual({ Username: 'secretUser', Secret: 'secretPass' }); + expect(aws.secretsManagerClient).toHaveBeenCalledWith({ assumeRoleArn: 'arn:aws:iam::0123456789012:role/my-role' }); + }); + + test('supports configuring the secret fields', async () => { + mockSecretWithSecretString({ name: 'secretUser', apiKey: '01234567' }); + + const creds = await fetchDockerLoginCredentials(aws, config, 'secretwithcustomfields.example.com'); + + expect(creds).toEqual({ Username: 'secretUser', Secret: '01234567' }); + }); + test('throws when secret does not have the correct fields - key/value', async () => { mockSecretWithSecretString({ principal: 'foo', credential: 'bar' }); @@ -135,6 +158,15 @@ describe('fetchDockerLoginCredentials', () => { await expect(fetchDockerLoginCredentials(aws, config, 'ecr.example.com')).rejects.toThrow(/uhoh/); }); + test('supports assuming a role', async () => { + mockEcrAuthorizationData(Buffer.from('myFoo:myBar', 'utf-8').toString('base64')); + + const creds = await fetchDockerLoginCredentials(aws, config, 'ecrwithrole.example.com'); + + expect(creds).toEqual({ Username: 'myFoo', Secret: 'myBar' }); + expect(aws.ecrClient).toHaveBeenCalledWith({ assumeRoleArn: 'arn:aws:iam::0123456789012:role/my-role' }); + }); + test('throws if ECR returns no authData', async () => { aws.mockEcr.getAuthorizationToken = mockedApiResult({ authorizationData: [] }); From d45eeb2c65880541849f107acdf46798a2573e20 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Fri, 25 Jun 2021 11:58:44 +0100 Subject: [PATCH 4/4] resetAuthPlugins simply resets to default --- packages/cdk-assets/lib/private/docker.ts | 15 +++++---------- packages/cdk-assets/test/docker-images.test.ts | 7 +++---- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/cdk-assets/lib/private/docker.ts b/packages/cdk-assets/lib/private/docker.ts index 3392fe6915a64..e1fc54429f18f 100644 --- a/packages/cdk-assets/lib/private/docker.ts +++ b/packages/cdk-assets/lib/private/docker.ts @@ -99,12 +99,10 @@ export class Docker { const config = cdkCredentialsConfig(); if (!config) { return false; } - this.resetAuthPlugins(); - // Should never happen; means resetAuthPlugins is broken. - if (!this.configDir) { throw new Error('no active docker config directory selected'); } + this.configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdkDockerConfig')); const domains = Object.keys(config.domainCredentials); - const credHelpers = domains.reduce(function(map: Record, domain) { + const credHelpers = domains.reduce((map: Record, domain) => { map[domain] = 'cdk-assets'; // Use docker-credential-cdk-assets for this domain return map; }, {}); @@ -114,16 +112,13 @@ export class Docker { } /** - * Creates a new empty Docker config directory. - * All future commands (e.g., `build`, `push`) will use this config. + * Removes any configured Docker config directory. + * All future commands (e.g., `build`, `push`) will use the default config. * * This is useful after calling `configureCdkCredentials` to reset to default credentials. - * - * @returns the path to the directory */ public resetAuthPlugins() { - this.configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdkDockerConfig')); - return this.configDir; + this.configDir = undefined; } private async execute(args: string[], options: ShellOptions = {}) { diff --git a/packages/cdk-assets/test/docker-images.test.ts b/packages/cdk-assets/test/docker-images.test.ts index 4cd0b2e2004ed..1f36b88025725 100644 --- a/packages/cdk-assets/test/docker-images.test.ts +++ b/packages/cdk-assets/test/docker-images.test.ts @@ -227,7 +227,6 @@ test('when external credentials are present, explicit Docker config directories // Setup -- Mock that we have CDK credentials, and mock fs operations. jest.spyOn(dockercreds, 'cdkCredentialsConfig').mockReturnValue({ version: '0.1', domainCredentials: {} }); jest.spyOn(fs, 'mkdtempSync').mockImplementationOnce(() => '/tmp/mockedTempDir'); - jest.spyOn(fs, 'mkdtempSync').mockImplementationOnce(() => '/tmp/otherMockedTempDir'); jest.spyOn(fs, 'writeFileSync').mockImplementation(jest.fn()); let pub = new AssetPublishing(AssetManifest.fromPath('/simple/cdk.out'), { aws }); @@ -243,9 +242,9 @@ test('when external credentials are present, explicit Docker config directories { commandLine: ['docker', '--config', '/tmp/mockedTempDir', 'inspect', 'cdkasset-theasset'], exitCode: 1 }, { commandLine: ['docker', '--config', '/tmp/mockedTempDir', 'build', '--tag', 'cdkasset-theasset', '.'], cwd: absoluteDockerPath }, { commandLine: ['docker', '--config', '/tmp/mockedTempDir', 'tag', 'cdkasset-theasset', '12345.amazonaws.com/repo:abcdef'] }, - // Prior to push, change to a new (fresh) config directory - { commandLine: ['docker', '--config', '/tmp/otherMockedTempDir', 'login'], prefix: true }, - { commandLine: ['docker', '--config', '/tmp/otherMockedTempDir', 'push', '12345.amazonaws.com/repo:abcdef'] }, + // Prior to push, revert to the default config directory + { commandLine: ['docker', 'login'], prefix: true }, + { commandLine: ['docker', 'push', '12345.amazonaws.com/repo:abcdef'] }, ); await pub.publish();