UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

232 lines 9.59 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.uniqueSpecFile = exports.matchSpec = exports.sameObject = exports.syncResources = exports.syncApplication = void 0; const configuration_1 = require("@atomist/automation-client/lib/configuration"); const string_1 = require("@atomist/automation-client/lib/internal/util/string"); const projectUtils = require("@atomist/automation-client/lib/project/util/projectUtils"); const child_process_1 = require("@atomist/automation-client/lib/util/child_process"); const logger_1 = require("@atomist/automation-client/lib/util/logger"); const CachingProjectLoader_1 = require("../../../api-helper/project/CachingProjectLoader"); const config_1 = require("../config"); const spec_1 = require("../deploy/spec"); const request_1 = require("../kubernetes/request"); const spec_2 = require("../kubernetes/spec"); const retry_1 = require("../support/retry"); const clone_1 = require("./clone"); const diff_1 = require("./diff"); const tag_1 = require("./tag"); /** * Synchronize changes from deploying app to the configured syncRepo. * If no syncRepo is configured, do nothing. * * @param app Kubernetes application change that triggered the sync * @param resources Kubernetes resource objects to synchronize * @param action Action performed, "upsert" or "delete" */ async function syncApplication(app, resources, action = "upsert") { const slug = request_1.appName(app); const syncOpts = configuration_1.configurationValue("sdm.k8s.options.sync", {}); if (!config_1.validSyncOptions(syncOpts)) { return; } const syncRepo = syncOpts.repo; if (resources.length < 1) { return; } const projectLoadingParameters = { credentials: syncOpts.credentials, cloneOptions: clone_1.defaultCloneOptions, id: syncRepo, readOnly: false, }; const projectLoader = configuration_1.configurationValue("sdm.projectLoader", new CachingProjectLoader_1.CachingProjectLoader()); try { await projectLoader.doWithProject(projectLoadingParameters, syncResources(app, resources, action, syncOpts)); } catch (e) { e.message = `Failed to perform sync resources from ${slug} to sync repo ${syncRepo.owner}/${syncRepo.repo}: ${e.message}`; logger_1.logger.error(e.message); throw e; } return; } exports.syncApplication = syncApplication; /** * Update the sync repo with the changed resources from a * KubernetesApplication. For each changed resource in `resources`, * loop through all the existing Kubernetes spec files, i.e., those * that match [[k8sSpecGlob]], to see if the apiVersion, kind, name, * and namespace, which may be undefined, match. If a match is found, * update that spec file. If no match is found, create a unique file * name and store the resource spec in it. If changes are made, * commit and push the changes. * * @param app Kubernetes application object * @param resources Resources that were upserted as part of this application * @param action Action performed, "upsert" or "delete" * @param opts Repo sync options, passed to the sync action * @return Function that updates the sync repo with the resource specs */ function syncResources(app, resources, action, opts) { return async (syncProject) => { const slug = `${syncProject.id.owner}/${syncProject.id.repo}`; const aName = request_1.appName(app); const specs = []; await projectUtils.doWithFiles(syncProject, diff_1.k8sSpecGlob, async (file) => { try { const spec = await spec_1.parseKubernetesSpecFile(file); specs.push({ file, spec }); } catch (e) { logger_1.logger.warn(`Failed to process sync repo ${slug} spec ${file.path}, ignoring: ${e.message}`); } }); const [syncAction, syncVerb] = (action === "delete") ? [resourceDeleted, "Delete"] : [resourceUpserted, "Update"]; for (const resource of resources) { const fileSpec = matchSpec(resource, specs); await syncAction(resource, syncProject, fileSpec, opts); } if (await syncProject.isClean()) { return; } try { const v = request_1.isKubernetesApplication(app) ? app.image.replace(/^.*:/, ":") : ""; await syncProject.commit(`${syncVerb} ${aName}${v}\n\n[atomist:generated] ${tag_1.commitTag()}\n`); } catch (e) { e.message = `Failed to commit resource changes for ${aName} to sync repo ${slug}: ${e.message}`; logger_1.logger.error(e.message); throw e; } try { await syncProject.push(); } catch (e) { logger_1.logger.warn(`Failed on initial sync repo ${slug} push attempt: ${e.message}`); try { await retry_1.logRetry(async () => { const pullResult = await child_process_1.execPromise("git", ["pull", "--rebase"], { cwd: syncProject.baseDir }); logger_1.logger.debug(`Sync project 'git pull --rebase': ${pullResult.stdout}; ${pullResult.stderr}`); await syncProject.push(); }, `sync project ${slug} git pull and push`); } catch (e) { e.message = `Failed sync repo ${slug} pull and rebase retries: ${e.message}`; logger_1.logger.error(e.message); throw e; } } }; } exports.syncResources = syncResources; /** * Persist the creation of or update to a resource to the sync repo * project. * * @param resource Kubernetes resource that was upserted * @param p Sync repo project * @param fs File and spec object that matches resource, may be undefined */ async function resourceUpserted(resource, p, fs, opts) { let format = "yaml"; if (fs && fs.file) { format = (/\.ya?ml$/.test(fs.file.path)) ? "yaml" : "json"; } else if (opts.specFormat) { format = opts.specFormat; } const stringifyOptions = { format, secretKey: opts.secretKey, }; const resourceString = await spec_2.kubernetesSpecStringify(resource, stringifyOptions); if (fs) { await fs.file.setContent(resourceString); } else { const specPath = await uniqueSpecFile(resource, p, format); await p.addFile(specPath, resourceString); } } /** * Safely persist the deletion of a resource to the sync repo project. * If `fs` is `undefined`, do nothing. * * @param resource Kubernetes resource that was upserted * @param p Sync repo project * @param fs File and spec object that matches resource, may be `undefined` */ async function resourceDeleted(resource, p, fs) { if (fs) { await p.deleteFile(fs.file.path); } } /** * Determine if two Kubernetes resource specifications represent the * same object. When determining if they are the same, only the kind, * name, and namespace, which may be `undefined`, must match. The * apiVersion is not considered when matching because the same * resource can appear under different API versions. Other object * properties are not considered. * * @param a First Kubernetes object spec to match * @param b Second Kubernetes object spec to match * @return `true` if specs match, `false` otherwise */ function sameObject(a, b) { return a && b && a.metadata && b.metadata && a.kind === b.kind && a.metadata.name === b.metadata.name && a.metadata.namespace === b.metadata.namespace; } exports.sameObject = sameObject; /** * Search `fileSpecs` for a spec that matches `spec`. To be * considered a match, the kind, name, and namespace, which may be * undefined, must match. The apiVersion is not considered when * matching because the same resource can appear under different API * versions. * * @param spec Kubernetes object spec to match * @param fileSpecs Array of spec and file objects to search * @return First file and spec object to match spec or `undefined` if no match is found */ function matchSpec(spec, fileSpecs) { return fileSpecs.find(fs => sameObject(spec, fs.spec)); } exports.matchSpec = matchSpec; /** * Return a unique name for a resource spec that lexically sorts so * resources that should be created earlier than others sort earlier * than others. * * @param resource Kubernetes object spec * @param p Kubernetes spec project * @return Unique spec file name that sorts properly */ async function uniqueSpecFile(resource, p, format) { const specRoot = spec_2.kubernetesSpecFileBasename(resource); const specExt = `.${format}`; let specPath = specRoot + specExt; while (await p.getFile(specPath)) { specPath = specRoot + "_" + string_1.guid().split("-")[0] + specExt; } return specPath; } exports.uniqueSpecFile = uniqueSpecFile; //# sourceMappingURL=application.js.map