UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

442 lines 21.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Helm = void 0; const lodash_1 = require("lodash"); const functions_1 = require("./functions"); const resolver_1 = require("../commands/resolver"); const constants_1 = require("./constants"); const functions_2 = require("../util/functions"); const errors_1 = require("../util/errors"); const logger_1 = require("../util/logger"); const workload_1 = require("../util/workload"); const objects_1 = require("../util/objects"); const generic_1 = require("../commands/generic"); class Helm { constructor(client, session, args, ensureDeletion) { this.client = client; this.session = session; this.args = args; this.ensuredGvcNames = []; this.ensureDeletion = ensureDeletion; // Parameters dependant this.chart = this.extractChart(); this.stateName = (0, functions_1.getHelmStateName)(args.release || ''); this.stateParentLink = (0, resolver_1.kindResolver)('secret').parentLink(this.session.context); this.stateSelfLink = (0, resolver_1.resolveToLink)('secret', this.stateName, this.session.context); } // Public Methods // async createState(release) { // Initialize a new state const body = { name: this.stateName, kind: 'secret', description: 'Created by cpln', tags: { 'cpln/cli-generated': true, ...(0, generic_1.fromTagOptions)(this.args), }, type: 'opaque', data: { payload: JSON.stringify(release), encoding: 'plain', }, }; // Create the state const axiosResponse = await this.client.axios.put(this.stateParentLink, body); // Use `response.headers.location` to validate the put method return await this.fetchState(axiosResponse.headers.location); } async fetchState(link) { const state = await this.client.get(`${link ? link : this.stateSelfLink}/-reveal`); // Remove unnecessary properties to avoid a 409 on update delete state.version; delete state.created; delete state.lastModified; return state; } async updateState(state, release) { var _a; try { // Update state tags state.tags = { ...((_a = state.tags) !== null && _a !== void 0 ? _a : {}), ...(0, generic_1.fromTagOptions)(this.args), ...(0, generic_1.fromRemoveTagOptions)(this.args), }; // Set state data state.data.payload = JSON.stringify(release); // Update state await this.client.put(this.stateParentLink, state); } catch (e) { this.session.out(`ERROR: Unable to update the state '${release.release}'.`); this.session.abort({ error: e }); } } async deleteState(release) { try { await this.client.axios.delete(this.stateSelfLink); } catch (e) { this.session.out(`ERROR: Unable to delete the state '${release.release}'.`); this.session.abort({ error: e }); } } validateRelease(release) { if (release.schemaVersion !== constants_1.CPLN_HELM_SCHEMA_LATEST_VERSION) { this.session.abort({ message: `ERROR: Expected the version to be ${constants_1.CPLN_HELM_SCHEMA_LATEST_VERSION} but found ${release.schemaVersion}. Please, check for a new CLI update or maybe try to delete the release and its resources manually and then attempt to install the release again.`, }); } } initializeRelease() { return { schemaVersion: constants_1.CPLN_HELM_SCHEMA_LATEST_VERSION, release: this.args.release, deployments: [], }; } async getRelease(state) { var _a; try { let release; const hasSchemaVersion = JSON.parse(state.data.payload); // Migrate release if necessary if (hasSchemaVersion.schemaVersion != constants_1.CPLN_HELM_SCHEMA_LATEST_VERSION) { switch (hasSchemaVersion.schemaVersion) { case '1': const legacy = hasSchemaVersion; const resources = []; for (const resource of legacy.resources) { try { resources.push({ ...resource, template: await this.fetchResource(resource.kind, resource.link), }); } catch (e) { // Ignore 404 if (((_a = e.response) === null || _a === void 0 ? void 0 : _a.status) !== 404) { throw e; } } } release = { schemaVersion: constants_1.CPLN_HELM_SCHEMA_LATEST_VERSION, release: legacy.release, deployments: [ { gvc: '', revision: 1, updated: new Date().toISOString(), status: 'deployed', chart: this.chart.name, appVersion: this.chart.appVersion, description: legacy.description, config: {}, values: {}, valuesFiles: [], resources, }, ], }; break; default: release = hasSchemaVersion; break; } // Update state await this.updateState(state, release); } else { release = hasSchemaVersion; } return release; } catch (e) { this.session.abort({ error: e }); } } async deleteResources(resourcesToDelete) { var _a, _b; let currentKindToEnsureDeletion; let resourceLinksToEnsureDeletion = []; // Sort by priority reverse (0, objects_1.sortIHasKindByPriority)(resourcesToDelete, true); for (const resource of resourcesToDelete) { try { // Skip resource deleteion if specified if (((_a = resource.template.tags) === null || _a === void 0 ? void 0 : _a['helm.sh/resource-policy']) === 'keep') { this.session.out(`Skipped deletion of ${resource.link} because it has the 'helm.sh/resource-policy' tag set to 'keep'`); continue; } // Ensure the deletion of a group of resources of the same kind if (currentKindToEnsureDeletion && currentKindToEnsureDeletion !== resource.kind) { currentKindToEnsureDeletion = undefined; await Promise.all(resourceLinksToEnsureDeletion.map((link) => this.ensureDeletion(link))); resourceLinksToEnsureDeletion = []; } // Consider these kinds of resources to ensure their deletion later if (resource.kind == 'volumeset' || resource.kind == 'workload') { currentKindToEnsureDeletion = resource.kind; resourceLinksToEnsureDeletion.push(resource.link); } // Perform the delete operation await this.client.axios.delete(resource.link); this.session.out(`Deleted ${resource.link}`); } catch (e) { if (((_b = e.response) === null || _b === void 0 ? void 0 : _b.status) !== 404) { this.session.out(`ERROR: Unable to uninstall '${resource.link}'. Please try again.`); this.session.abort({ error: e }); } } } } async cleanupOnFail(createdResources) { if (this.args.cleanupOnFail && createdResources.length > 0) { this.session.out(`Rollback was not successful, performing deletion of created resources...`); await this.deleteResources(createdResources); } } async awaitWorkloadsReadiness(appliedWorkloadLinks) { if (this.args.wait && appliedWorkloadLinks.length > 0) { this.session.out(`\nWaiting for workloads to be ready...`); await this.monitorWorkloadsReadiness(this.args.timeout, this.args.release, appliedWorkloadLinks); this.session.out('\nAll workloads are ready.'); } } async ensureGvcExists(resource, ctx) { var _a, _b, _c; if (objects_1.gvcRelatedKinds.includes(resource.kind)) { // Check if GVC was already ensured, and avoid checking again if (this.ensuredGvcNames.includes(ctx.gvc)) { return; } // Attempt to fetch the GVC, and throw errors accordingly try { const gvcLink = (0, resolver_1.resolveToLink)('gvc', ctx.gvc, ctx); await this.client.get(gvcLink); // If successful, we will get here, and we will mark this GVC as already ensured this.ensuredGvcNames.push(ctx.gvc); } catch (e) { // Handle when GVC does not exist if (((_a = e.response) === null || _a === void 0 ? void 0 : _a.status) === 404) { throw new Error(`ERROR: You attempted to apply ${resource.kind} '${resource.name}' while GVC '${ctx.gvc || 'UNKNOWN'}' does not exist.`); } else if ((_c = (_b = e.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.message) { // Throw the error message along with the status code throw new Error(`ERROR: Unable to fetch GVC '${ctx.gvc || 'UNKNOWN'}' for ${resource.kind} '${resource.name}'. Status: ${e.response.status}, Message: ${e.response.data.message}.`); } else { // Something unexpected happened, throw accordingly throw new Error(`ERROR: Unable to fetch GVC '${ctx.gvc || 'UNKNOWN'}' for ${resource.kind} '${resource.name}'. Message: ${e.message}.`); } } } } async validateResourceReleaseTag(resourceSelfLink) { var _a; try { const resourceToCheck = await this.client.get(resourceSelfLink); if (constants_1.kindsToSkipHelmReleaseTagCheck.includes(resourceToCheck.kind)) { return; } // Throw an error if the resource does not have the cpln/release tag if (!resourceToCheck.tags || !resourceToCheck.tags.hasOwnProperty('cpln/release')) { throw new Error(`ERROR: The resource '${resourceSelfLink}' cannot be updated because it is missing the 'cpln/release' tag. This tag is required to ensure the resource is managed by the correct release. Please add the tag 'cpln/release' with the value '${this.stateName}' to proceed.`); } // Throw an error if the resource does not belong to this release. if (resourceToCheck.tags['cpln/release'] !== this.stateName) { throw new Error(`ERROR: The resource '${resourceSelfLink}' cannot be updated because it is being managed by a different release '${resourceToCheck.tags['cpln/release'].replace('cpln-release-', '')}'. This measure is in place to prevent updates to resources that already belong to a different release.`); } } catch (e) { if (((_a = e.response) === null || _a === void 0 ? void 0 : _a.status) !== 404) { throw e; } } } async shouldApplyResource(lastDeployedDeployment, resourceSelfLink, resource) { // If there is a last deployed deployment, let's compare resources if (lastDeployedDeployment) { // Attempt to find this template resource in the latest deployed deployment const deployedResource = lastDeployedDeployment.resources.find((resource) => resource.link === resourceSelfLink); // If this resource exists in the last deployed deployment, then let's make a comparison if (deployedResource) { // But first, let's attempt to fetch the resource and see if the versions are identical between // the one on the platform and the one in the last deployed deployment try { // Fetch the original resource from the data service const originalResource = await this.client.get(resourceSelfLink); // Comparison is only valid if both versions are identical. // Otherwise, we apply the template because differences are likely intentional. // Comparing is tricky since the data service adds fields (e.g., "description") // that may not exist in the template or were unset by the user. if (originalResource.version === deployedResource.version && (0, lodash_1.isEqual)(deployedResource.template, resource)) { return false; } } catch (e) { // We don't really want to do anything here; // If we are unable to compare, then let's just apply the template resource as is. } } } return true; } async prepareBeforeApply(resources, deploymentManager, deployment, state, release) { var _a, _b, _c; const secretHashMap = {}; // Process secrets in the template resources list and compute their hashes for (const templateResource of resources) { // Ignore non-secret resources if (templateResource.kind !== 'secret') { continue; } // Parse template resource to a Secret object const secret = templateResource; // Ignore secrets that are invalid, we will catch this later on apply if (!secret.name || !secret.data) { continue; } // Map secret name to its data hash secretHashMap[secret.name] = (0, objects_1.computeHash)(secret.data); } // Process workloads and add/update their tag that is responsible for secrets for (const templateResource of resources) { // Ignore non-workload resources if (templateResource.kind !== 'workload') { continue; } // Parse template resource to a Workload object const workload = templateResource; // Extract secret names from the workload const secretNames = (0, workload_1.extractSecretNames)(workload); // Iterate over secret names and update workload tags accordingly for (const secretName of secretNames) { // Fetch secret from API if not already in the list if (!(secretName in secretHashMap)) { // Attempt to fetch the secret, and handle error accordingly try { // Resolve the self-link of the secret const secretLink = (0, resolver_1.resolveToLink)('secret', secretName, this.session.context); // Fetch the secret const fetchedSecret = await this.fetchResource('secret', secretLink); // Compute the hash of the secret and map it to the secret name secretHashMap[secretName] = (0, objects_1.computeHash)(fetchedSecret.data); } catch (e) { let errorMessage = ''; // Handle when the secret does not exist if (((_a = e.response) === null || _a === void 0 ? void 0 : _a.status) === 404) { // Let's map a constant value for non-existing secrets secretHashMap[secretName] = constants_1.workloadSecretTagDefaultValue; } else if ((_c = (_b = e.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.message) { // Throw the error message along with the status code errorMessage = `ERROR: Unable to fetch Secret '${secretName}' that is referenced by Workload '${workload.name}'. Status: ${e.response.status}, Message: ${e.response.data.message}.`; } else { // Something unexpected happened, throw accordingly errorMessage = `ERROR: Unable to fetch Secret '${secretName}' that is referenced by Workload '${workload.name}'. Message: ${e.message}.`; } // Mark deployment as failed and throw error if an error message was found if (errorMessage) { deploymentManager.updateDeploymentStatus(deployment, 'failed', errorMessage); await this.updateState(state, release); this.session.out(`ERROR: Unable to install ${templateResource.kind} '${templateResource.name}'.`); this.session.abort({ error: e }); } } } // Initialize tags if not found if (workload.tags === undefined) { workload.tags = {}; } // Update workload tags workload.tags[`cpln/secret:${secretName}`] = secretHashMap[secretName]; } } } // Private Methods // extractChart() { if (!this.args.chart) { return { name: '', appVersion: '' }; } try { return (0, functions_1.extractHelmChart)(this.args.chart); } catch (e) { this.session.out('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 }); } } async fetchResource(kind, selfLink) { if (kind === 'secret') { selfLink = `${selfLink}/-reveal`; } const resource = await this.client.get(selfLink); // Remove unnecessary properties to avoid a 409 on update delete resource.version; delete resource.created; delete resource.lastModified; return resource; } async monitorWorkloadsReadiness(timeoutSeconds, release, appliedWorkloadLinks) { const promises = []; // Accumulate promises in order to wait for them in parallel for (const workloadLink of appliedWorkloadLinks) { promises.push(this.monitorWorkloadReadiness(workloadLink)); } let timeoutId; const promisesToRace = [Promise.allSettled(promises)]; if (timeoutSeconds) { const { timeoutPromise, timeoutId: _timeoutId } = (0, functions_2.timeoutAsync)(timeoutSeconds * 1000); timeoutId = _timeoutId; promisesToRace.push(timeoutPromise); } try { // Wait for workloads to be ready in parallel await Promise.race(promisesToRace); } catch (e) { if (e instanceof errors_1.TimeoutError) { this.session.abort({ message: `Timeout Error: Not all workloads were ready within ${timeoutSeconds} seconds.\nRelease ${release} has not been installed successfully!`, }); } // Handle any thrown error that occurred while monitoring a workload's readiness this.session.abort({ error: e }); } // Clear timeout if all workloads were ready if (timeoutId) { clearTimeout(timeoutId); } } async monitorWorkloadReadiness(workloadLink) { logger_1.logger.info(`Waiting for workload '${workloadLink}' to be ready...`); await (0, functions_2.retryFn)(async () => { try { const workload = await this.client.get(workloadLink); const deployments = await this.client.get(workloadLink + '/deployment'); const workloadHealth = (0, workload_1.getWorkloadHealth)(deployments.items, workload); if (!workloadHealth.readyLatest) { throw new Error(`Deployments are not ready yet.`); } } catch (e) { logger_1.logger.info(`Workload '${workloadLink}' is not ready yet, retrying. Error: ${e.message}`); throw e; } }, { onFailure() { logger_1.logger.debug(`Timed out, the workload ${workloadLink} is still not ready.`); }, repeatTimes: 9999, }); } } exports.Helm = Helm; //# sourceMappingURL=helm.js.map