UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

185 lines 7.78 kB
"use strict"; /* * Copyright © 2020 Atomist, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.sortSpecs = exports.repoSync = exports.kubernetesSync = exports.KubernetesSync = void 0; const logger_1 = require("@atomist/automation-client/lib/util/logger"); const messages_1 = require("../../../api-helper/misc/slack/messages"); const apply_1 = require("../kubernetes/apply"); const secret_1 = require("../kubernetes/secret"); const spec_1 = require("../kubernetes/spec"); const error_1 = require("../support/error"); const name_1 = require("../support/name"); const clone_1 = require("./clone"); const diff_1 = require("./diff"); const repo_1 = require("./repo"); exports.KubernetesSync = "KubernetesSync"; /** * Command to synchronize the resources in a Kubernetes cluster with * the resource specs in the configured sync repo. The sync repo will * be cloned and the resources applied against the Kubernetes API in * lexical order sorted by file name. If no sync repo is configured * in the SDM, the command errors. This command is typically executed * on an interval timer by setting the `intervalMinutes` * [[KubernetesSyncOptions]]. * * If the SDM configuration says this packs commands should be added, * i.e., `sdm.configuration.sdm.k8s.options.addCommands` is `true`, * the command will have the intent `kube sync SDM_NAME`. Otherwise * the command will be registered without an intent. */ function kubernetesSync(sdm) { var _a, _b; const cmd = { name: exports.KubernetesSync, listener: repoSync, }; if ((_b = (_a = sdm.configuration.sdm.k8s) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.addCommands) { cmd.intent = `kube sync ${name_1.cleanName(sdm.configuration.name)}`; } return cmd; } exports.kubernetesSync = kubernetesSync; /** * Clone the sync repo and apply the specs to the Kubernetes cluster. */ async function repoSync(cli) { var _a, _b, _c, _d, _e; const opts = (_b = (_a = cli.configuration.sdm.k8s) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.sync; if (!opts) { const message = `SDM has no sync options defined`; logger_1.logger.error(message); await cli.context.messageClient.respond(message); return { code: 2, message }; } if ((_e = (_d = (_c = cli.configuration.sdm.k8s) === null || _c === void 0 ? void 0 : _c.options) === null || _d === void 0 ? void 0 : _d.sync) === null || _e === void 0 ? void 0 : _e.credentials) { delete cli.configuration.sdm.k8s.options.sync.credentials; } if (!await repo_1.queryForScmProvider(cli.configuration)) { const message = `Failed to get sync repo and credentials, skipping sync`; logger_1.logger.error(message); await cli.context.messageClient.respond(message); return { code: 2, message }; } if (!repo_1.isRemoteRepo(opts.repo)) { const message = `SDM sync option repo is not a valid remote repo`; logger_1.logger.error(message); await cli.context.messageClient.respond(message); return { code: 2, message }; } const projectLoadingParameters = { credentials: opts.credentials, cloneOptions: clone_1.defaultCloneOptions, context: cli.context, id: opts.repo, readOnly: true, }; const slug = `${opts.repo.owner}/${opts.repo.repo}`; try { logger_1.logger.info(`Starting sync of repo ${slug}`); await cli.configuration.sdm.projectLoader.doWithProject(projectLoadingParameters, syncApply(opts)); } catch (e) { const message = `Failed to sync repo ${slug}: ${e.message}`; logger_1.logger.error(message); await cli.context.messageClient.respond(message); return { code: 1, message }; } const result = { code: 0, message: `Successfully completed sync of repo ${slug}`, }; logger_1.logger.info(result.message); try { await cli.context.messageClient.respond(messages_1.slackSuccessMessage("Kubernetes Sync", result.message)); } catch (e) { result.code++; result.message = `${result.message}; Failed to send response message: ${e.message}`; logger_1.logger.error(result.message); } return result; } exports.repoSync = repoSync; /** * Return a function that ensures all specs in `syncRepo` have * corresponding resources in the Kubernetes cluster. If the resource * does not exist, it is created using the spec. If it does exist, it * is patched using the spec. Errors are collected and thrown after * processing all specs so one bad spec does not stop processing. * * @param opts Kubernetes sync options */ function syncApply(opts) { return async (syncRepo) => { const specFiles = await sortSpecs(syncRepo); const errors = []; for (const specFile of specFiles) { logger_1.logger.debug(`Processing spec ${specFile.path}`); try { const specString = await specFile.getContent(); const specs = spec_1.parseKubernetesSpecs(specString); for (let spec of specs) { try { if (spec.kind === "Secret" && opts && opts.secretKey) { spec = await secret_1.decryptSecret(spec, opts.secretKey); } await apply_1.applySpec(spec); } catch (e) { const slug = spec_1.specSlug(spec); e.message = `Failed to apply spec '${slug}' from '${specFile.path}': ${error_1.k8sErrMsg(e)}`; logger_1.logger.error(e.message); errors.push(e); } } } catch (e) { e.message = `Failed to apply '${specFile.path}': ${error_1.k8sErrMsg(e)}`; logger_1.logger.error(e.message); errors.push(e); } } if (errors.length > 0) { errors[0].message = `There were errors during repo sync: ${errors.map(e => e.message).join("; ")}`; throw errors[0]; } return; }; } /** * Consume stream of files from project and sort them by their `path` * property using `localeCompare`. Any file at the root of the * project, i.e., not in a subdirectory, having the extensions * ".json", ".yaml", or ".yml` are considered specs. * * Essentially, this function converts a FileStream into a Promise of * sorted ProjectFiles. * * @param syncRepo Repository of specs to sort * @return Sorted array of specs in project */ function sortSpecs(syncRepo) { return new Promise((resolve, reject) => { const specsStream = syncRepo.streamFiles(diff_1.k8sSpecGlob); const specs = []; specsStream.on("data", f => specs.push(f)); specsStream.on("error", reject); specsStream.on("end", () => resolve(specs.sort((a, b) => a.path.localeCompare(b.path)))); }); } exports.sortSpecs = sortSpecs; //# sourceMappingURL=sync.js.map