@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
232 lines • 9.59 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.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