packages/cdk-assets/lib/private/docker-credentials.ts (106 lines of code) (raw):
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { IAws, IECRClient } from '../aws';
import type { EventEmitter } from '../progress';
import { EventType } from '../progress';
export interface DockerCredentials {
readonly Username: string;
readonly Secret: string;
}
export interface DockerCredentialsConfig {
readonly version: string;
readonly domainCredentials: Record<string, DockerDomainCredentialSource>;
}
export interface DockerDomainCredentialSource {
readonly secretsManagerSecretId?: string;
readonly secretsUsernameField?: string;
readonly secretsPasswordField?: 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 {
}
}
return _cdkCredentials;
}
/**
* Just for testing
*/
export function _clearCdkCredentialsConfigCache() {
_cdkCredentials = undefined;
}
/** Fetches login credentials from the configured source (e.g., SecretsManager, ECR) */
export async function fetchDockerLoginCredentials(
aws: IAws,
config: DockerCredentialsConfig,
endpoint: string,
) {
// Paranoid handling to ensure new URL() doesn't throw if the schema is missing
// For official docker registry, docker will pass https://index.docker.io/v1/
endpoint = endpoint.includes('://') ? endpoint : `https://${endpoint}`;
const domain = new URL(endpoint).hostname;
if (
!Object.keys(config.domainCredentials).includes(domain) &&
!Object.keys(config.domainCredentials).includes(endpoint)
) {
throw new Error(`unknown domain ${domain}`);
}
let domainConfig = config.domainCredentials[domain] ?? config.domainCredentials[endpoint];
if (domainConfig.secretsManagerSecretId) {
const sm = await aws.secretsManagerClient({ assumeRoleArn: domainConfig.assumeRoleArn });
const secretValue = await sm.getSecretValue({
SecretId: domainConfig.secretsManagerSecretId,
});
if (!secretValue.SecretString) {
throw new Error(
`unable to fetch SecretString from secret: ${domainConfig.secretsManagerSecretId}`,
);
}
const secret = JSON.parse(secretValue.SecretString);
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[usernameField], Secret: secret[secretField] };
} 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: IECRClient, eventEmitter?: EventEmitter) {
if (eventEmitter) {
eventEmitter(EventType.DEBUG, 'Fetching ECR authorization token');
}
const authData = (await ecr.getAuthorizationToken()).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!,
};
}