UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

371 lines 19.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HelmReleaseOrchestrator = void 0; const tmp = require("tmp"); const path = require("path"); const fs = require("fs"); const jsyaml = require("js-yaml"); const generic_1 = require("../commands/generic"); const objects_1 = require("../util/objects"); const io_1 = require("../util/io"); const helm_base_1 = require("./helm-base"); const helm_release_history_manager_1 = require("./helm-release-history-manager"); const constants_1 = require("./constants"); const functions_1 = require("./functions"); const tags_1 = require("../commands/tags"); class HelmReleaseOrchestrator extends helm_base_1.HelmBase { constructor(client, session, args, ensureDeletion, historyManager) { super(client, session, args, ensureDeletion); // Required parameters this.historyManager = historyManager; // Parameters dependant this.chart = this.extractChartMetadata(); this.description = this.args.description || ''; } // Public Static Functions // static async initialize(client, session, args, ensureDeletion) { // Initialize a new HelmReleaseHistoryManager const historyManager = await helm_release_history_manager_1.HelmReleaseHistoryManager.initialize(client, session, args, ensureDeletion); // Return a new instace of the HelmReleaseOrchestrator return new HelmReleaseOrchestrator(client, session, args, ensureDeletion, historyManager); } // Public Methods // async install() { // Get chart template resources const templateResources = this.getChartTemplateResources(); // Construct the release for the next release revision const release = await this.constructNextReleaseForInstall(); // Create a new release revision const nextReleaseRevision = await this.createNextReleaseRevision('install', release); // Install the chart template into the new release revision await nextReleaseRevision.install(templateResources, this.historyManager); } async upgrade() { // Get chart template resources const templateResources = this.getChartTemplateResources(); // Construct the release for the next release revision const release = await this.constructNextReleaseForUpgrade(); // Create a new release revision const nextReleaseRevision = await this.createNextReleaseRevision('upgrade', release); // Install the chart template into the new release revision await nextReleaseRevision.upgrade(templateResources, this.historyManager); } async rollback() { // Get rollback target const rollbackTarget = await this.getRollbackTarget(); // Construct the release for the next release revision const release = await this.constructNextReleaseForRollback(rollbackTarget); // Create a new release revision for the rollback const nextReleaseRevision = await this.createNextReleaseRevision('rollback', release); // Rollback to the target release await nextReleaseRevision.rollback(rollbackTarget, this.historyManager); } async uninstall() { // Get all release revisions for the specified release (migrate if necessary) const releaseRevisions = await this.historyManager.revealAllReleaseRevisions(); // If there are no release revisions, then this release simply doesn't exist if (releaseRevisions.length === 0) { this.session.abort({ message: `ERROR: Release '${this.releaseName}' does not exist.` }); } // Since release revisions are sorted in ascending order, let's iterate over each // release revision from the end to the start and uninstall each for (let i = releaseRevisions.length - 1; i >= 0; i--) { await releaseRevisions[i].uninstall(); } // Print the status of the uninstall this.session.out(`\nRelease '${this.releaseName}' has been uninstalled successfully!`); } async getRevisions() { return await this.historyManager.revealAllReleaseRevisions(); } async getManifest() { // Fetch the revisioned release const releaseRevision = await this.historyManager.getReleaseRevision(this.releaseName, this.args.revision); // Return the manifest of the revisioned release return releaseRevision.getManifest(); } async getAll() { // Fetch the revisioned release const releaseRevision = await this.historyManager.getReleaseRevision(this.releaseName, this.args.revision); // Return the release object of the revisioned release return releaseRevision.release; } async getConfig() { // Fetch the revisioned release const releaseRevision = await this.historyManager.getReleaseRevision(this.releaseName, this.args.revision); // Initialize the helm release config let config = {}; // Handle all if (this.args.all) { config = { ...releaseRevision.release.chart.values }; } // Handle existing config config = { ...config, ...releaseRevision.release.config, }; // Return the constructed config return config; } // Private Methods // async constructNextReleaseForInstall() { // If there is a previous release revision, then this release has already been installed if (this.historyManager.latestReleaseRevision) { this.session.abort({ message: `ERROR: Release '${this.releaseName}' is already installed. Use the 'cpln helm upgrade' command if you intend to upgrade this release.`, }); } // Extract necessary data from the chart const defaultValues = (0, functions_1.extractHelmDefaultValues)(this.args.chart); const valuesFiles = (0, functions_1.extractHelmCustomValues)(this.args.values); const config = (0, functions_1.extractHelmConfig)(this.args, valuesFiles); // Initialize and return the release return { schemaVersion: constants_1.CPLN_HELM_SCHEMA_LATEST_VERSION, name: this.releaseName, info: { firstDeployed: new Date().toISOString(), lastDeployed: new Date().toISOString(), description: this.description, status: 'pending-install', notes: (await this.getChartNotes()) || '', resources: [], }, chart: { metadata: this.chart, values: defaultValues, }, config: config, manifest: (0, functions_1.generateHelmTemplate)(this.args, this.org, this.gvc), version: this.historyManager.nextVersion, gvc: this.gvc, labels: this.getUserProvidedTags(), // Custom Properties valuesFiles: valuesFiles, }; } async constructNextReleaseForUpgrade() { var _a; // Since this is an upgrade, there will always be a latest revision, but we just want to safely handle that if (this.historyManager.latestReleaseRevision) { // If the latest release is pending, reject the install or upgrade if (constants_1.CPLN_HELM_PENDING_STATUSES.includes(this.historyManager.latestReleaseRevision.release.info.status)) { this.session.abort({ message: `ERROR: Release '${this.releaseName}' is '${this.historyManager.latestReleaseRevision.release.info.status}', please wait for the install or upgrade to finish. If this release revision is still pending or stuck, you will need to manually delete the secret '${this.historyManager.latestReleaseRevision.secret.name}'. Use the 'cpln secret delete ${this.historyManager.latestReleaseRevision.secret.name}' command to do so.`, }); } // Make sure the chart name is the same as the previous release revision if (this.historyManager.latestReleaseRevision.release.chart.metadata.name && // This check if for migration purposes, v1 schema does not have a reference to the chart name this.historyManager.latestReleaseRevision.release.chart.metadata.name !== this.chart.name) { this.session.abort({ message: `ERROR: UPGRADE FAILED: cannot upgrade a release with a different chart name: original "${this.historyManager.latestReleaseRevision.release.chart.metadata.name}", new "${this.chart.name}"`, }); } } // Extract necessary data from the chart const defaultValues = (0, functions_1.extractHelmDefaultValues)(this.args.chart); const valuesFiles = (0, functions_1.extractHelmCustomValues)(this.args.values); const config = (0, functions_1.extractHelmConfig)(this.args, valuesFiles); // Initialize and return the release return { schemaVersion: constants_1.CPLN_HELM_SCHEMA_LATEST_VERSION, name: this.releaseName, info: { firstDeployed: ((_a = this.historyManager.latestReleaseRevision) === null || _a === void 0 ? void 0 : _a.release.info.firstDeployed) || new Date().toISOString(), lastDeployed: new Date().toISOString(), description: this.description, status: 'pending-upgrade', notes: (await this.getChartNotes()) || '', resources: [], }, chart: { metadata: this.chart, values: defaultValues, }, config: config, manifest: (0, functions_1.generateHelmTemplate)(this.args, this.org, this.gvc), version: this.historyManager.nextVersion, gvc: this.gvc, labels: this.getUserProvidedTags(this.historyManager.latestReleaseRevision), // Custom Properties valuesFiles: valuesFiles, }; } async constructNextReleaseForRollback(rollbackTarget) { // Initialize and return the release return { schemaVersion: constants_1.CPLN_HELM_SCHEMA_LATEST_VERSION, name: this.releaseName, info: { firstDeployed: rollbackTarget.release.info.firstDeployed, lastDeployed: new Date().toISOString(), description: this.description, status: 'pending-rollback', notes: rollbackTarget.release.info.notes, resources: [], }, chart: rollbackTarget.release.chart, config: rollbackTarget.release.config, manifest: rollbackTarget.release.manifest, version: this.historyManager.nextVersion, gvc: rollbackTarget.release.gvc, labels: this.getUserProvidedTags(rollbackTarget), // Custom Properties valuesFiles: rollbackTarget.release.valuesFiles, }; } async createNextReleaseRevision(operation, release) { var _a; // Get the secret name for the release const secretName = (0, functions_1.getReleaseSecretName)(release.name, release.version); // Prepare tags let tags = { ...(_a = this.historyManager.latestReleaseRevision) === null || _a === void 0 ? void 0 : _a.secret.tags, ...(0, generic_1.fromTagOptions)(this.args), ...(0, generic_1.fromRemoveTagOptions)(this.args), ...(0, tags_1.expressionToTags)(this.args.stateTag), ...(0, tags_1.withNullValues)((0, objects_1.toArray)(this.args.removeStateTag)), }; // Remove the 'scanned-for-deletion' tag if found since this is a new release if (tags.hasOwnProperty(constants_1.CPLN_HELM_SCANNED_FOR_DELETION_TAG_KEY)) { delete tags[constants_1.CPLN_HELM_SCANNED_FOR_DELETION_TAG_KEY]; } try { // Create the release revision secret await this.createReleaseSecret(release, tags); // Enforce history limit await this.historyManager.enforceHistoryLimit(); // Load and return the created secret into a HelmReleaseRevisionManager instance return await this.loadReleaseRevision(secretName); } catch (e) { this.session.err(`ERROR: Unable to ${operation} the release ${this.releaseName}.`); this.session.abort({ error: e }); } } async getRollbackTarget() { // If there are no release revisions, then there is nothing to rollback to if (this.historyManager.releaseRevisionSecrets.length === 0) { this.session.abort({ message: `ERROR: Release '${this.releaseName}' has no revisions to rollback to.`, }); } // Declare a variable to hold the rollback target let rollbackTarget; // Check if the user provided a revision number if (this.args.revision) { // Find the target release revision secret using the specified revision number const targetSecret = this.historyManager.releaseRevisionSecrets.find((secret) => { var _a; return Number((_a = secret.tags) === null || _a === void 0 ? void 0 : _a.version) === Number(this.args.revision); }); // If the revision number is not found, abort the session if (!targetSecret) { this.session.abort({ message: `ERROR: Release '${this.releaseName}' does not have a revision with version ${this.args.revision}.`, }); } // Create a new HelmReleaseRevisionManager instance out of the target secret rollbackTarget = await this.loadReleaseRevision(targetSecret.name); } else { // Get the latest release revision rollbackTarget = this.historyManager.latestReleaseRevision; // If the latest release revision status is 'deployed', target the latest superseded release revision, otherwise find the deployed release revision if (rollbackTarget && rollbackTarget.release.info.status === 'deployed') { rollbackTarget = await this.historyManager.findLatestSupersededReleaseRevision(); } else { // If the latest release revision is not 'deployed', target the deployed release revision rollbackTarget = this.historyManager.deployedReleaseRevision; } } // If the rollback target is not pointing to a release revision, abort the session if (!rollbackTarget) { this.session.abort({ message: `ERROR: Release '${this.releaseName}' does not have a revision to rollback to.`, }); } // Validate rollback target if (rollbackTarget.release.info.status !== 'superseded' && rollbackTarget.release.info.status !== 'deployed') { this.session.abort({ message: `ERROR: Release with version '${rollbackTarget.release.version}' is invalid, you can only rollback to 'superseded' or 'deployed' release revisions.`, }); } // Update current chart metadata this.chart = rollbackTarget.release.chart.metadata; // Update the description this.description = `Rollback to ${rollbackTarget.release.version}`; // Return the rollback target return rollbackTarget; } async getChartNotes() { // Ignore printing if NOTES.txt does not exist if (!fs.existsSync(path.join(this.args.chart, 'templates', 'NOTES.txt'))) { return Promise.resolve(undefined); } return new Promise((resolve, _) => { // Handle temporary directory for the chart tmp.dir({ unsafeCleanup: true }, (err, tempDirPath, cleanupCallback) => { if (err) { this.session.err(`ERROR: Unable to print notes.`); this.session.abort({ error: err }); } // Set chart path to the temporary directory const templateArgs = { ...this.args, chart: tempDirPath, }; try { // Copy chart into tempDirPath (0, io_1.copyDirectorySync)(this.args.chart, tempDirPath); // Delete all YAML templates from the temporary chart const templatesDir = path.join(tempDirPath, 'templates'); for (const file of fs.readdirSync(templatesDir)) { if (path.extname(file) === '.yaml') { fs.unlinkSync(path.join(templatesDir, file)); } } // Read raw NOTES.txt and write a notes.yaml so helm will render it const notesYamlPath = fs.readFileSync(path.join(templatesDir, 'NOTES.txt'), 'utf8'); fs.writeFileSync(path.join(templatesDir, 'notes.yaml'), jsyaml.dump({ notes: notesYamlPath })); // Run helm template and parse out the notes const template = (0, functions_1.generateHelmTemplate)(templateArgs, this.org, this.session.context.gvc); const notesObj = jsyaml.safeLoad(template); // Delete tmp directory cleanupCallback(); // Resolve with the notes resolve(notesObj.notes); } catch (e) { // Make sure the directory is deleted on exception cleanupCallback(); // Abort session this.session.err(`ERROR: Unable to print notes.`); this.session.abort({ error: e }); } }); }); } extractChartMetadata() { if (!this.args.chart) { return { name: '', appVersion: '' }; } try { return (0, functions_1.extractHelmChartMetadata)(this.args.chart); } catch (e) { this.session.err('ERROR: Unable to read Chart.yaml. Make sure you have a Chart.yaml file in the chart path you provided.'); this.session.abort({ error: e }); } } getChartTemplateResources() { return (0, functions_1.getTemplateResources)(this.session, this.args, this.org, this.gvc); } getUserProvidedTags(releaseRevisionManager) { return { ...releaseRevisionManager === null || releaseRevisionManager === void 0 ? void 0 : releaseRevisionManager.release.labels, ...(0, generic_1.fromTagOptions)(this.args), ...(0, generic_1.fromRemoveTagOptions)(this.args), }; } } exports.HelmReleaseOrchestrator = HelmReleaseOrchestrator; //# sourceMappingURL=helm-release-orchestrator.js.map