@controlplane/cli
Version:
Control Plane Corporation CLI
405 lines • 15.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.dockerPush = exports.dockerBuild = exports.configureDockerAuth = exports.ImageCmd = void 0;
const fs = require("fs");
const os = require("os");
const options_1 = require("./options");
const child_process_1 = require("child_process");
const generic_1 = require("./generic");
const resolver_1 = require("./resolver");
const query_1 = require("./query");
const command_1 = require("../cli/command");
const url_1 = require("url");
const download_1 = require("../pack/download");
const logger_1 = require("../util/logger");
const functions_1 = require("../util/functions");
class ImageCmd extends command_1.Command {
constructor() {
super(...arguments);
this.command = 'image';
this.describe = 'Manage images and configure Docker login';
}
builder(yargs) {
const imgResolver = (0, resolver_1.kindResolver)('image');
const resolver = {
homeLink: imgResolver.homeLink,
parentLink: imgResolver.parentLink,
kind: imgResolver.kind,
resourceLink(ref, ctx) {
if (!ref) {
return imgResolver.resourceLink(ref, ctx);
}
// XXX this is not good
const marker = '.cpln.io/';
const i = ref === null || ref === void 0 ? void 0 : ref.indexOf(marker);
if (i >= 0) {
ref = ref.substring(i + marker.length);
}
return imgResolver.resourceLink(ref, ctx);
},
};
const schema = {
props: [...query_1.defaultProps, 'repository', 'tag', 'digest'],
};
const opts = [options_1.withOrgOptions, options_1.withStandardOptions];
let prepare = {
async prepare() {
let registry = (await this.session.discovery).endpoints.registry;
if (!registry) {
return;
}
registry = registry.replace('https://', '');
registry = registry.replace('{org}', this.session.context.org);
this.formatHints = {
registry: registry,
};
},
};
const commandName = 'image';
const commandNamePlural = 'images';
const commandNameA = 'an image';
return (yargs
.demandCommand()
.version(false)
.help()
// generic
.command(new generic_1.Get(commandNamePlural, resolver, ...opts).with(prepare).toYargs())
.command(new generic_1.Edit(commandName, resolver, ...opts).with(prepare).toYargs())
.command(new generic_1.Patch(commandName, resolver, ...opts).with(prepare).toYargs())
.command(new generic_1.Delete(commandNamePlural, resolver, ...opts).with(prepare).toYargs())
.command(new generic_1.Query(commandNamePlural, resolver, schema, ...opts).with(prepare).toYargs())
.command(new generic_1.ListPermissions(commandNameA, resolver, ...opts).with(prepare).toYargs())
.command(new generic_1.ViewAccessReport(commandName, resolver, ...opts).toYargs())
.command(new generic_1.Tag(commandNamePlural, resolver, ...opts).with(prepare).toYargs())
// specific
.command(new DockerLogin().toYargs())
.command(new Build().toYargs())
.command(new CopyImage().toYargs()));
}
async handle() { }
}
exports.ImageCmd = ImageCmd;
class DockerLogin extends command_1.Command {
constructor() {
super();
this.command = 'docker-login';
this.describe = "Perform a Docker login to the organization's private registry";
}
builder(yargs) {
return (0, functions_1.pipe)((yargs) => {
return yargs.options({
'ignore-output': {
describe: 'Ignore the output of this command.',
},
});
},
//
options_1.withOrgOptions, options_1.withStandardOptions)(yargs);
}
async handle(args) {
const orgLink = (0, resolver_1.resolveToLink)('org', undefined, this.session.context);
const org = await this.client.get(orgLink);
const profile = this.session.profile;
const registry = configureDockerAuth((await this.session.discovery).endpoints.registry, org.name);
if (args.ignoreOutput) {
return;
}
if (this.session.profile.name != 'anonymous') {
this.session.err(`Docker will authenticate to "${registry}" using docker-credential-cpln in profile "${profile.name}".`);
this.session.err(`As you may change the default profile you are advised to set the profile before using docker:`);
this.session.err(`\n CPLN_PROFILE="${profile.name}"\n`);
}
else {
this.session.err(`Docker will authenticate to "${registry}" using docker-credential-cpln`);
}
}
}
function configureDockerAuth(regEndpoint, orgName, push) {
var _a;
const endpoint = new url_1.URL(regEndpoint);
let registry = endpoint.hostname.replace('{org}', orgName);
if (endpoint.port) {
registry += ':' + endpoint.port;
}
// Check if the docker-credential-cpln binary is reachable only if push is specified
if (push) {
try {
(0, child_process_1.execSync)('docker-credential-cpln version');
}
catch (error) {
throw new Error('The docker-credential-cpln command is not accessible. Please verify your installation and system PATH, or try reinstalling the cpln CLI. For installation instructions, visit: https://docs.controlplane.com/reference/cli');
}
}
// cat ~/.docker/config.json
const dockerCfgPath = `${os.homedir()}/.docker/config.json`;
// load config file
let cfg;
if (fs.existsSync(dockerCfgPath)) {
cfg = JSON.parse(fs.readFileSync(dockerCfgPath, 'utf-8'));
cfg.credHelpers = (_a = cfg.credHelpers) !== null && _a !== void 0 ? _a : {};
}
else {
cfg = {
credHelpers: {},
};
}
// update config only if that would change the file
if (cfg.credHelpers[registry] != 'cpln') {
cfg.credHelpers[registry] = 'cpln';
}
else {
return registry;
}
// try creating the parent folder of the config file just in case
fs.mkdirSync(`${os.homedir()}/.docker`, {
recursive: true,
});
// persist config
fs.writeFileSync(dockerCfgPath, JSON.stringify(cfg, null, 2), {
encoding: 'utf-8',
});
return registry;
}
exports.configureDockerAuth = configureDockerAuth;
function withBuildOptions(yargs) {
return yargs
.parserConfiguration({
'boolean-negation': false,
})
.options({
name: {
requiresArg: true,
demandOption: true,
alias: 'n',
description: 'Name and tag for the image',
},
dockerfile: {
//requiresArg: true,
description: 'Path to Dockerfile (e.g.: PATH/Dockerfile). If set, the builder option is not used',
},
builder: {
default: 'heroku/builder:22',
description: 'Buildpack package to use, for example heroku/builder:22, gcr.io/buildpacks/builder:v1, paketobuildpacks/builder:base, etc.',
},
dir: {
requiresArg: true,
default: '.',
description: 'Directory containing the application',
},
'no-cache': {
boolean: true,
default: false,
description: 'Builds the image without using any cached layers.',
},
push: {
boolean: true,
default: false,
description: "Push the new image to the org's private registry",
},
});
}
class Build extends command_1.Command {
constructor() {
super();
this.command = 'build';
this.describe = 'Build and containerize an application into an image. If using buildpacks, everything after -- will be passed down to the pack executable.';
}
builder(yargs) {
return (0, functions_1.pipe)(
//
withBuildOptions, options_1.withOrgOptions, options_1.withStandardOptions)(yargs);
}
async handle(args) {
var _a;
const orgLink = (0, resolver_1.resolveToLink)('org', undefined, this.session.context);
const org = await this.client.get(orgLink);
const registry = configureDockerAuth((await this.session.discovery).endpoints.registry, org.name, args.push);
const nat = (_a = args.name) === null || _a === void 0 ? void 0 : _a.split(':');
if (!nat || !nat[0] || !nat[1]) {
this.session.abort({ exitCode: 1, message: 'Image name must have a tag (i.e. my-image:tag)' });
}
const image = `${registry}/${args.name}`;
if (typeof args.dockerfile == 'string' || args.dockerfile == true) {
await dockerBuild(args, image);
}
else if (args.dockerfile === undefined && fs.existsSync('Dockerfile')) {
args.dockerfile = 'Dockerfile';
await dockerBuild(args, image);
}
else {
const packPath = await (0, download_1.installPack)(this.env.profileManager.storeRoot);
await packBuild(packPath, args, image);
}
if (args.push) {
await dockerPush(this.session.profile, image);
}
this.session.outFormat({
image: image,
link: `/org/${org.name}/image/${args.name}`,
});
}
}
async function dockerBuild(opts, image) {
const args = [
// Build Args
'build',
'-f',
opts.dockerfile === true ? 'Dockerfile' : opts.dockerfile,
];
// Force x86 on arm chips
if (process.arch == 'arm64') {
args.push('--platform', 'linux/amd64');
}
// Add --no-cache option if specified
if (opts.noCache) {
args.push('--no-cache');
}
// Tag image and add build context
args.push('-t', image, opts.dir);
logger_1.logger.debug(`arguments passed to docker: ${args.join(' ')}`);
return docker(args);
}
exports.dockerBuild = dockerBuild;
async function dockerPush(profile, image) {
return docker([
//
'push',
'--disable-content-trust',
image,
], {
CPLN_PROFILE: profile.name,
});
}
exports.dockerPush = dockerPush;
async function packBuild(packPath, opts, image) {
const packArgs = [
// Pack Args
'build',
'--builder',
opts.builder,
'--path',
opts.dir,
];
// Add --no-cache option if specified
if (opts.noCache) {
packArgs.push('--clear-cache');
}
// Indicate the end of options
if (opts['--']) {
packArgs.push(...opts['--']);
}
// Add target image for the build
packArgs.push(image);
logger_1.logger.debug(`exec: ${packPath} ${packArgs.join(' ')}`);
return waitFor((0, child_process_1.spawn)(packPath, packArgs, { stdio: ['inherit', process.stderr, process.stderr] }));
}
function waitFor(proc) {
return new Promise((resolve, reject) => {
proc.on('exit', (code) => {
if (code == 0) {
resolve();
}
else {
reject(code);
}
});
proc.on('error', reject);
});
}
function withCopyOptions(yargs) {
return yargs.options({
'to-name': {
requiresArg: true,
description: 'Name and tag for the image',
},
'to-org': {
requiresArg: true,
description: 'Target org to copy the image to',
},
'to-profile': {
requiresArg: true,
description: 'Profile to use for accessing the "to-org" argument',
},
cleanup: {
boolean: true,
default: false,
description: 'Cleans up the pulled and retagged image',
},
});
}
class CopyImage extends command_1.Command {
constructor() {
super();
this.command = 'copy <ref>';
this.describe = 'Copy an image from one org to another. This will make sure that docker-login has been run against the source and destination org, then will pull, tag and push the image to the destination org.';
}
builder(yargs) {
return (0, functions_1.pipe)(
//
generic_1.withSingleRef, withCopyOptions, options_1.withAllOptions)(yargs);
}
async handle(args) {
const sourceOrg = this.session.context.org;
if (!sourceOrg) {
this.session.abort({
exitCode: 1,
message: 'No source org was detected, either provide "org" argument, or set it on your profile.',
});
}
const destName = args.toName || args.ref;
let destOrg = '';
if (args.toProfile) {
const toProfile = this.env.profileManager.find(args.toProfile);
destOrg = args.toOrg || (toProfile === null || toProfile === void 0 ? void 0 : toProfile.context.org) || '';
if (!destOrg) {
this.session.abort({
exitCode: 1,
message: `No destination org was detected, either provide "to-org" argument, or set it on the profile given with "to-profile" argument.`,
});
}
}
else {
destOrg = args.toOrg || sourceOrg;
if (!destOrg) {
this.session.abort({ exitCode: 1, message: `No destination org was detected, provide either "to-org" or "to-profile" arguments.` });
}
}
if (destOrg === sourceOrg && !args.toName) {
this.session.abort({ exitCode: 1, message: `"to-name" argument must be provided to copy an image inside the same org.` });
}
if (destOrg === sourceOrg && args.ref === args.toName) {
this.session.abort({ exitCode: 1, message: `Current image name and the copied image name cannot be the same inside the same org.` });
}
const reg = (await this.session.discovery).endpoints.registry;
const sourceRegistry = configureDockerAuth(reg, sourceOrg);
const destRegistry = configureDockerAuth(reg, destOrg);
const sourceImage = `${sourceRegistry}/${args.ref}`;
const destImage = `${destRegistry}/${destName}`;
logger_1.logger.debug(`Copying ${sourceImage} to ${destImage}`);
logger_1.logger.debug(`Pulling ${sourceImage}`);
await docker(['pull', sourceImage], {
CPLN_PROFILE: this.session.context.profile,
});
logger_1.logger.debug(`Tagging as ${destImage}`);
await docker(['tag', sourceImage, destImage]);
logger_1.logger.debug(`Pushing ${destImage}`);
await docker(['push', destImage], {
CPLN_PROFILE: args.toProfile || this.session.context.profile,
});
if (args.cleanup) {
logger_1.logger.debug(`Cleaning up ${sourceImage}, ${destImage}`);
await docker(['rmi', sourceImage, destImage]);
}
}
}
function docker(args, env) {
return waitFor((0, child_process_1.spawn)('docker', args, {
stdio: ['inherit', process.stderr, process.stderr],
env: {
// merge process env and the profile
...process.env,
...(env || {}),
},
}));
}
//# sourceMappingURL=image.js.map
;