@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
185 lines • 7.78 kB
JavaScript
;
/*
* 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