UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

763 lines 32.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HelmGetValues = exports.HelmGetNotes = exports.HelmGetManifest = exports.HelmGetAll = exports.HelmGet = exports.HelmHistory = exports.HelmList = exports.HelmUninstall = exports.HelmRollback = exports.HelmUpgrade = exports.HelmInstall = exports.HelmTemplate = exports.HelmCmd = void 0; const command_1 = require("../cli/command"); const functions_1 = require("../util/functions"); const generic_1 = require("./generic"); const options_1 = require("./options"); const functions_2 = require("../helm/functions"); const k8s_crd_exporter_1 = require("../k8s-crd-exporter"); const helpers_1 = require("../k8s-crd-exporter/helpers"); const helm_release_orchestrator_1 = require("../helm/helm-release-orchestrator"); const resolver_1 = require("./resolver"); const resultFetcher_1 = require("../rest/resultFetcher"); const helm_release_history_manager_1 = require("../helm/helm-release-history-manager"); const helm_release_migrator_1 = require("../helm/helm-release-migrator"); const constants_1 = require("../helm/constants"); function withSingleRelease(yargs) { return yargs.positional('release', { type: 'string', describe: 'The release name', demandOption: false, }); } function withSingleChart(yargs) { return yargs.positional('chart', { type: 'string', describe: 'Path to chart', demandOption: true, }); } function withSingleRevision(yargs) { return yargs.positional('revision', { type: 'string', describe: `Revision (version) number. If this argument is omitted or set to 0, it will roll back to the previous release. To see revision numbers, run 'cpln helm history RELEASE'.`, }); } function withHelmUpgradeOptions(yargs) { return yargs.options({ 'history-limit': { type: 'number', default: 10, describe: 'Maximum number of revisions saved per release. Use 0 for no limit', }, install: { type: 'boolean', describe: "If a release by this name doesn't already exist, run an install", }, }); } function withHelmTemplateOptions(yargs) { return yargs.options({ 'dependency-update': { type: 'boolean', describe: 'Update dependencies if they are missing before installing the chart', }, description: { type: 'string', describe: 'Add a custom description', alias: 'desc', }, 'generate-name': { type: 'boolean', describe: 'Generate the name (and omit the NAME parameter)', alias: 'g', }, 'post-renderer': { type: 'string', describe: 'The path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path', }, 'post-renderer-args': { default: [], multiple: true, describe: 'An argument to the post-renderer (can specify multiple or separate values: --post-renderer-args arg1 --post-renderer-args arg2) (default [])', }, repo: { type: 'string', describe: 'Chart repository url where to locate the requested chart', }, set: { multiple: true, describe: 'Set values on the command line (can specify multiple or separate values: --set key1=val1 --set key2=val2)', }, 'set-string': { multiple: true, describe: 'Set STRING values on the command line (can specify multiple or separate values: --set-string key1=val1 --set-string key2=val2)', }, 'set-file': { multiple: true, describe: 'Set values from respective files specified via the command line (can specify multiple or separate values: --set-file key1=path1 --set-file key2=path2)', }, values: { type: 'string', multiple: true, describe: 'Specify values in a YAML file or a URL (can specify multiple or separate values: --values value1.yaml --values values2.yaml)', alias: 'f', }, verify: { type: 'boolean', describe: 'Verify the package before using it', }, version: { type: 'string', describe: 'Specify a version constraint for the chart version to use. This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). If this is not specified, the latest version is used', }, username: { type: 'string', describe: 'Chart repository username where to locate the requested chart', }, password: { type: 'string', describe: 'Chart repository password where to locate the requested chart', }, 'ca-file': { type: 'string', describe: 'Verify certificates of HTTPS-enabled servers using this CA bundle', }, 'cert-file': { type: 'string', describe: 'Identify HTTPS client using this SSL certificate file', }, 'key-file': { type: 'string', describe: 'Identify HTTPS client using this SSL key file', }, 'insecure-skip-tls-verify': { type: 'boolean', describe: 'Skip tls certificate checks for the chart download', }, 'render-subchart-notes': { type: 'boolean', describe: 'If set, render subchart notes along with the parent on install/upgrade', }, }); } function withHelmGetOptions(yargs) { return yargs.options({ revision: { type: 'number', describe: 'get the named release with revision', }, }); } function withHelmGetValuesOptions(yargs) { return yargs.options({ all: { type: 'boolean', describe: 'dump all (computed) values', alias: 'a', }, }); } function withWaitOptions(yargs) { return yargs.options({ wait: { type: 'boolean', describe: `If set, will wait until all Workloads are in a ready state before marking the release as successful. It will wait for as long as --timeout`, }, timeout: { type: 'number', default: 5 * 60, describe: 'The amount of seconds to wait for workloads to be ready before timing out. Works only if the "wait" option is set to true.', }, }); } function withCleanupOnFail(yargs) { return yargs.options({ 'cleanup-on-fail': { type: 'boolean', default: false, describe: `allow deletion of new resources created in this rollback when rollback fails`, }, }); } function withStateTagRemovableOptions(yargs) { return yargs.options({ 'state-tag': { description: 'Attach tags to the Helm secret (e.g., --state-tag drink=water)', requiresArg: true, multiple: true, }, 'remove-state-tag': { description: 'Remove tags from the Helm secret (e.g., --remove-state-tag tagname)', requiresArg: true, multiple: true, }, }); } // Commands // class HelmCmd extends command_1.Command { constructor() { super(...arguments); this.command = 'helm'; this.describe = 'Manage helm releases on cpln'; } builder(yargs) { return yargs .demandCommand() .version(false) .help() .command(new HelmGet().toYargs()) .command(new HelmHistory().toYargs()) .command(new HelmInstall().toYargs()) .command(new HelmList().toYargs()) .command(new HelmRollback().toYargs()) .command(new HelmTemplate().toYargs()) .command(new HelmUninstall().toYargs()) .command(new HelmUpgrade().toYargs()); } handle() { } } exports.HelmCmd = HelmCmd; class HelmTemplate extends command_1.Command { constructor() { super(...arguments); this.command = 'template [release] [chart]'; this.describe = 'Generate cpln resources from a template'; } builder(yargs) { return (0, functions_1.pipe)( // withSingleRelease, withSingleChart, withHelmTemplateOptions, helpers_1.withK8sCrdExporterOptions, options_1.withAllOptions)(yargs); } async handle(args) { // Org is required this.requireOrg(); // Process Helm arguments try { (0, functions_2.processHelmArgs)(args); } catch (e) { // Ignore the missing release name error, it is completely fine to template without a release name if (e.message != constants_1.HELM_MISSING_RELEASE_NAME_ERROR) { this.session.abort({ message: e.message }); } } // Output the template in different formats switch (args.output) { case 'crd': const templateResources = (0, functions_2.getHelmTemplateResult)(this.session, args, this.session.context.org, this.session.context.gvc).resources; // Create a new CRD exporter const exporter = new k8s_crd_exporter_1.K8sCrdExporter(this.session); // Construct the batch request const batchRequest = { resources: templateResources, }; // Perform the batch request const batchResponse = await exporter.batch(batchRequest, args.namespace, args.keep); // Determine output format let outputFormat; // Extract and show resources in the default format (defaults to JSON if no default is specified) switch (this.session.profile.format.output) { case 'json': case 'json-slim': case 'yaml': case 'yaml-slim': outputFormat = this.session.profile.format.output || 'yaml'; break; default: outputFormat = 'yaml'; break; } // Output the items in the determined format this.session.outFormat(batchResponse.items, { output: outputFormat, color: this.session.profile.format.color, _separateDocs: true, }); break; case 'json': case 'json-slim': case 'names': this.session.outFormat((0, functions_2.runHelmTemplate)(args, this.session.context.org, this.session.context.gvc, args.debug || args.verbose)); break; default: this.session.out((0, functions_2.runHelmTemplate)(args, this.session.context.org, this.session.context.gvc, args.debug || args.verbose)); break; } } } exports.HelmTemplate = HelmTemplate; class HelmInstall extends command_1.Command { constructor() { super(...arguments); this.command = 'install [release] [chart]'; this.aliases = ['apply']; this.describe = 'Install a release'; } builder(yargs) { return (0, functions_1.pipe)( // withSingleRelease, withSingleChart, withWaitOptions, withHelmTemplateOptions, generic_1.withTagRemovableOptions, withStateTagRemovableOptions, options_1.withAllOptions)(yargs); } async handle(args) { // Org is required to proceed with any Helm operation this.requireOrg(); // Process Helm arguments and normalize them to match Helm semantics try { (0, functions_2.processHelmArgs)(args); } catch (e) { // Abort the session immediately if Helm argument processing fails this.session.abort({ message: e.message }); } // Pull the chart from the repository if necessary and replace args.chart with extracted path const disposeTmpChartDir = (0, functions_2.prepareChartIfNecessary)(this.session, args); try { // Initialize the Helm release orchestrator for managing Helm lifecycle const helmOrchestrator = await helm_release_orchestrator_1.HelmReleaseOrchestrator.initialize(this.client, this.session, args, this.ensureDeletion); // Perform the install of the Helm release using the orchestrator await helmOrchestrator.install(); } catch (e) { // Attempt to remove the temporary chart directory if it was created try { if (disposeTmpChartDir !== null) { disposeTmpChartDir(); } } catch (e) { // Swallow the error to keep cleanup idempotent and non-fatal } // Rethrow the original error after cleanup has been attempted throw e; } } } exports.HelmInstall = HelmInstall; class HelmUpgrade extends command_1.Command { constructor() { super(...arguments); this.command = 'upgrade [release] [chart]'; this.describe = 'Upgrade a release'; } builder(yargs) { return (0, functions_1.pipe)( // withSingleRelease, withSingleChart, withWaitOptions, withHelmUpgradeOptions, withHelmTemplateOptions, generic_1.withTagRemovableOptions, withStateTagRemovableOptions, options_1.withAllOptions)(yargs); } async handle(args) { // Org is required to proceed with any Helm operation this.requireOrg(); // Process Helm arguments and normalize them to match Helm semantics try { (0, functions_2.processHelmArgs)(args); } catch (e) { // Abort the session immediately if Helm argument processing fails this.session.abort({ message: e.message }); } // Pull the chart from the repository if necessary and replace args.chart with extracted path const disposeTmpChartDir = (0, functions_2.prepareChartIfNecessary)(this.session, args); try { // Initialize the Helm release orchestrator for managing Helm lifecycle const helmOrchestrator = await helm_release_orchestrator_1.HelmReleaseOrchestrator.initialize(this.client, this.session, args, this.ensureDeletion); // Count how many revisions we have to determine if this release has been installed before const revisions = await helmOrchestrator.getRevisions(); // Declare a flag to indicate if the release exists or not let releaseExists = revisions.length > 0; // Abort with an error if the release does not exist and the user did not request installation if (!releaseExists && !args.install) { this.session.abort({ message: `ERROR: Release "${args.release}" does not exist. Use the --install flag with this command to install the release if it is missing.`, }); } // Perform an install of the Helm release if it does not exist and the user requested install, otherwise perform an upgrade if (!releaseExists && args.install) { await helmOrchestrator.install(); } else { // Perform the upgrade of the Helm release using the orchestrator await helmOrchestrator.upgrade(); } } catch (e) { // Attempt to remove the temporary chart directory if it was created try { if (disposeTmpChartDir !== null) { disposeTmpChartDir(); } } catch (e) { // Swallow the error to keep cleanup idempotent and non-fatal } // Rethrow the original error after cleanup has been attempted throw e; } } } exports.HelmUpgrade = HelmUpgrade; class HelmRollback extends command_1.Command { constructor() { super(...arguments); this.command = 'rollback <release> [revision]'; this.describe = 'Roll back a release to a previous revision'; } builder(yargs) { return (0, functions_1.pipe)( // withSingleRelease, withSingleRevision, withCleanupOnFail, withWaitOptions, options_1.withAllOptions)(yargs); } async handle(args) { // Org is required this.requireOrg(); // Initialize the Helm Orchestrator const helmOrchestrator = await helm_release_orchestrator_1.HelmReleaseOrchestrator.initialize(this.client, this.session, args, this.ensureDeletion); // Rollback to a specific release await helmOrchestrator.rollback(); } } exports.HelmRollback = HelmRollback; class HelmUninstall extends command_1.Command { constructor() { super(...arguments); this.command = 'uninstall <release>'; this.aliases = ['destroy', 'del', 'delete', 'un']; this.describe = 'Uninstall a release'; } builder(yargs) { return (0, functions_1.pipe)( // withSingleRelease, options_1.withRequestOptions, options_1.withAllOptions)(yargs); } async handle(args) { // Org is required this.requireOrg(); // Initialize the Helm Orchestrator const helmOrchestrator = await helm_release_orchestrator_1.HelmReleaseOrchestrator.initialize(this.client, this.session, args, this.ensureDeletion); // Uninstall the release and its revisions await helmOrchestrator.uninstall(); } } exports.HelmUninstall = HelmUninstall; class HelmList extends command_1.Command { constructor() { super(...arguments); this.command = 'list'; this.describe = 'List releases'; } // Public Methods // builder(yargs) { return (0, functions_1.pipe)( // options_1.withOrgOptions, options_1.withStandardOptions)(yargs); } async handle(args) { // Org is required this.requireOrg(); // Migrate legacy releases if found, this is so we can have them in the list later on await this.migrateLegacyReleases(args); // The final items for the table display const items = await this.collectTableItems(args); // Prepare accumulator const accumulator = { kind: 'list', itemKind: constants_1.CPLN_HELM_LIST_KIND, items: items, links: [], }; if (this.session.format.max) { accumulator.items = accumulator.items.slice(0, this.session.format.max); } // Output the deployment list with format await this.session.outFormat(accumulator); } // Private Methods // async migrateLegacyReleases(args) { // A variable to hold all migration promises to await for in parallel const promises = []; // Construct the parent link of the secrets const secretParentLink = (0, resolver_1.kindResolver)('secret').parentLink(this.session.context); // Fetch all secrets that start with the legacy cpln helm release prefix const secretList = await this.client.post(`${secretParentLink}/-query`, { kind: 'secret', spec: { match: 'any', terms: [ { op: '~', property: 'name', value: constants_1.CPLN_RELEASE_NAME_PREFIX_LEGACY, }, ], }, }); // Continue fetching all secrets that match the query terms await (0, resultFetcher_1.fetchPages)(this.client, 0, secretList); // Treat the secret list items as an array of secrets const secrets = secretList.items; // If there were no legacy releases found, skip migration if (secrets.length === 0) { return; } // Iterate over each secret and accumulate migrate promises so we can await migrations in parallel for (const secret of secrets) { promises.push(this.migrateRelease(secret, args)); } // Migrate all accumulated migration promises in parallel await Promise.all(promises); } async collectTableItems(args) { // The final items for the table display const items = []; // A variable to hold all release history manager instances to await for in parallel const promises = []; // Get all release names const releaseNames = await this.getAllReleaseNames(); // Iterate over the release names and collect release history manager initialization promises, // so we can await for their initialization in parallel for (const releaseName of releaseNames) { // Clone base arguments and specify the current release name const newArgs = { ...args, release: releaseName }; // Collect the initialize promise promises.push(helm_release_history_manager_1.HelmReleaseHistoryManager.initialize(this.client, this.session, newArgs, this.ensureDeletion)); } // Await for the initialization of all release history managers in parallel const historyManagers = await Promise.all(promises); // Iterate over history managers and create table items for (const historyManager of historyManagers) { // Placeholder for the selected release revision let releaseRevision; // Prefer the deployed revision if available, otherwise use the latest revision if (historyManager.deployedReleaseRevision) { releaseRevision = historyManager.deployedReleaseRevision; } else { releaseRevision = historyManager.latestReleaseRevision; } // Extract key details and append to the items list for display items.push({ release: releaseRevision.release.name, gvc: releaseRevision.release.gvc, revision: releaseRevision.release.version, updated: releaseRevision.release.info.lastDeployed, status: releaseRevision.release.info.status, chart: releaseRevision.release.chart.metadata.name, appVersion: releaseRevision.release.chart.metadata.appVersion, }); } // Return the items for table display return items; } async getAllReleaseNames() { // The unique set of release names const releaseNames = new Set(); // Construct the parent link of the secrets const secretParentLink = (0, resolver_1.kindResolver)('secret').parentLink(this.session.context); // Fetch all secrets that start with the cpln helm release prefix const secretList = await this.client.post(`${secretParentLink}/-query`, { kind: 'secret', spec: { match: 'any', terms: [ { op: '~', property: 'name', value: constants_1.CPLN_RELEASE_NAME_PREFIX, }, ], }, }); // Continue fetching all secrets that match the query terms await (0, resultFetcher_1.fetchPages)(this.client, 0, secretList); // Treat the secret list items as an array of secrets const secrets = secretList.items; // Since each release could have multiple revisions (secrets), then we should uniqule accumulate release names from the secrets for (const secret of secrets) { // Skip secret if it is an invalid release revision secret if (!secret.tags || !secret.tags.hasOwnProperty('name')) { continue; } // Extract the release name from the secret tags const releaseName = secret.tags.name; // Skip if the release name has already been collected if (releaseNames.has(releaseName)) { continue; } // Add the release name to the release names set releaseNames.add(releaseName); } // No limit requested: return the full set if (this.session.format.max === undefined || this.session.format.max < 1) { return releaseNames; } // Convert the Set to an array, take the first `max` items, and wrap them back into a new Set const slicedArray = [...releaseNames].slice(0, this.session.format.max); // Return the limited Set return new Set(slicedArray); } async migrateRelease(secret, args) { // Construct the self link of the secret const selfLink = (0, resolver_1.resolveToLink)('secret', secret.name, this.session.context); // Reveal the legacy secret const revealedSecret = await this.client.get(`${selfLink}/-reveal`); // If we got here, then that legacy release exists and we need to migrate it const helmMigrator = new helm_release_migrator_1.HelmReleaseMigrator(this.client, this.session, args, this.ensureDeletion, revealedSecret); // Migrate the release await helmMigrator.migrate(); } } exports.HelmList = HelmList; class HelmHistory extends command_1.Command { constructor() { super(...arguments); this.command = 'history <release>'; this.describe = 'Fetch release history'; } builder(yargs) { return (0, functions_1.pipe)( // withSingleRelease, options_1.withAllOptions)(yargs); } async handle(args) { // Org is required this.requireOrg(); // The final items for the table display const items = []; // Initialize the Helm Orchestrator const helmOrchestrator = await helm_release_orchestrator_1.HelmReleaseOrchestrator.initialize(this.client, this.session, args, this.ensureDeletion); // Get release revisions const releaseRevisions = await helmOrchestrator.getRevisions(); // Throw an error if deployments were empty if (releaseRevisions.length == 0) { this.session.abort({ message: `ERROR: Release '${args.release}' has no revisions.` }); } // Iterate over each release revision and construct a table item for (const releaseRevision of releaseRevisions) { items.push({ revision: releaseRevision.release.version, updated: releaseRevision.release.info.lastDeployed, status: releaseRevision.release.info.status, chart: releaseRevision.release.chart.metadata.name, appVersion: releaseRevision.release.chart.metadata.appVersion, description: releaseRevision.release.info.description, }); } // Prepare accumulator const accumulator = { kind: 'list', itemKind: constants_1.CPLN_HELM_DEPLOYMENT_KIND, items: items, links: [], }; if (this.session.format.max) { accumulator.items = accumulator.items.slice(0, this.session.format.max); } // Output the state with format await this.session.outFormat(accumulator); } } exports.HelmHistory = HelmHistory; class HelmGet extends command_1.Command { constructor() { super(); this.command = 'get'; this.describe = 'Download extended information of a named release'; } builder(yargs) { return (yargs .demandCommand() .version(false) .help() // specific .command(new HelmGetAll().toYargs()) .command(new HelmGetManifest().toYargs()) .command(new HelmGetNotes().toYargs()) .command(new HelmGetValues().toYargs())); } handle() { } } exports.HelmGet = HelmGet; class HelmGetAll extends command_1.Command { constructor() { super(...arguments); this.command = 'all <release>'; this.describe = 'Download all information for a named release'; } builder(yargs) { return (0, functions_1.pipe)( // withSingleRelease, withHelmGetOptions, options_1.withOrgOptions, options_1.withStandardOptions)(yargs); } async handle(args) { // Org is required this.requireOrg(); // Initialize the Helm Orchestrator const helmOrchestrator = await helm_release_orchestrator_1.HelmReleaseOrchestrator.initialize(this.client, this.session, args, this.ensureDeletion); // Get the release object of the specified release const release = await helmOrchestrator.getAll(); // Output the release with format await this.session.outFormat(release); } } exports.HelmGetAll = HelmGetAll; class HelmGetManifest extends command_1.Command { constructor() { super(...arguments); this.command = 'manifest <release>'; this.describe = 'Download the manifest for a named release'; } builder(yargs) { return (0, functions_1.pipe)( // withSingleRelease, withHelmGetOptions, options_1.withOrgOptions, options_1.withStandardOptions)(yargs); } async handle(args) { // Org is required this.requireOrg(); // Initialize the Helm Orchestrator const helmOrchestrator = await helm_release_orchestrator_1.HelmReleaseOrchestrator.initialize(this.client, this.session, args, this.ensureDeletion); // Get the manifest of the specified release const manifest = await helmOrchestrator.getManifest(); // Output the state with format await this.session.outFormat(manifest); } } exports.HelmGetManifest = HelmGetManifest; class HelmGetNotes extends command_1.Command { constructor() { super(...arguments); this.command = 'notes <release>'; this.describe = 'Download the notes for a named release'; } builder(yargs) { return (0, functions_1.pipe)( // withSingleRelease, withHelmGetOptions, options_1.withOrgOptions, options_1.withStandardOptions)(yargs); } async handle(args) { // Org is required this.requireOrg(); // Initialize the Helm Orchestrator const helmOrchestrator = await helm_release_orchestrator_1.HelmReleaseOrchestrator.initialize(this.client, this.session, args, this.ensureDeletion); // Get the release object of the specified release const release = await helmOrchestrator.getAll(); // Output the notes this.session.out(release.info.notes); } } exports.HelmGetNotes = HelmGetNotes; class HelmGetValues extends command_1.Command { constructor() { super(...arguments); this.command = 'values <release>'; this.describe = 'Download the values file for a named release'; } builder(yargs) { return (0, functions_1.pipe)( // withSingleRelease, withHelmGetValuesOptions, withHelmGetOptions, options_1.withOrgOptions, options_1.withStandardOptions)(yargs); } async handle(args) { // Org is required this.requireOrg(); // Initialize the Helm Orchestrator const helmOrchestrator = await helm_release_orchestrator_1.HelmReleaseOrchestrator.initialize(this.client, this.session, args, this.ensureDeletion); // Get the config of the specified release const config = await helmOrchestrator.getConfig(); // Output the state with format await this.session.outFormat(config); } } exports.HelmGetValues = HelmGetValues; //# sourceMappingURL=helm.js.map