UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

405 lines 15.6 kB
"use strict"; 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