@controlplane/cli
Version:
Control Plane Corporation CLI
371 lines • 19.1 kB
JavaScript
"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