@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
253 lines • 11 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.sanitizeClassifier = exports.resolveClassifierPath = exports.cacheRemove = exports.cacheRestore = exports.NoOpGoalProjectListenerRegistration = exports.cachePut = exports.CacheOutputGoalDataKey = exports.CacheInputGoalDataKey = void 0;
const configuration_1 = require("@atomist/automation-client/lib/configuration");
const fileGlobs_1 = require("@atomist/automation-client/lib/project/fileGlobs");
const projectUtils_1 = require("@atomist/automation-client/lib/project/util/projectUtils");
const _ = require("lodash");
const sdmGoal_1 = require("../../../api-helper/goal/sdmGoal");
const GoalInvocation_1 = require("../../../api/goal/GoalInvocation");
const commonPushTests_1 = require("../../../api/mapping/support/commonPushTests");
const resolvePlaceholder_1 = require("../../machine/yaml/resolvePlaceholder");
const array_1 = require("../../util/misc/array");
const CompressingGoalCache_1 = require("./CompressingGoalCache");
exports.CacheInputGoalDataKey = "@atomist/sdm/input";
exports.CacheOutputGoalDataKey = "@atomist/sdm/output";
const DefaultGoalCache = new CompressingGoalCache_1.CompressingGoalCache();
/**
* Goal listener that performs caching after a goal has been run.
* @param options The options for caching
* @param classifier Whether only a specific classifier, as defined in the options,
* needs to be cached. If omitted, all classifiers are cached.
* @param classifiers Additional classifiers that need to be created.
*/
function cachePut(options, classifier, ...classifiers) {
const allClassifiers = [];
if (classifier) {
allClassifiers.push(classifier, ...(classifiers || []));
}
const entries = !!classifier ?
options.entries.filter(pattern => allClassifiers.includes(pattern.classifier)) :
options.entries;
const listenerName = `caching outputs`;
return {
name: listenerName,
listener: async (p, gi) => {
const { goalEvent } = gi;
if (!!isCacheEnabled(gi) && !process.env.ATOMIST_ISOLATED_GOAL_INIT) {
const cloneEntries = _.cloneDeep(entries);
const goalCache = cacheStore(gi);
for (const entry of cloneEntries) {
const files = [];
if (isGlobFilePattern(entry.pattern)) {
files.push(...(await getFilePathsThroughPattern(p, entry.pattern.globPattern)));
}
else if (isDirectoryPattern(entry.pattern)) {
files.push(entry.pattern.directory);
}
if (!_.isEmpty(files)) {
const resolvedClassifier = await resolveClassifierPath(entry.classifier, gi);
const uri = await goalCache.put(gi, p, files, resolvedClassifier);
if (!!resolvedClassifier && !!uri) {
entry.classifier = resolvedClassifier;
entry.uri = uri;
}
}
}
// Set outputs on the goal data
const data = sdmGoal_1.goalData(goalEvent);
const newData = {
[exports.CacheOutputGoalDataKey]: [
...(data[exports.CacheOutputGoalDataKey] || []),
...cloneEntries,
],
};
goalEvent.data = JSON.stringify(Object.assign(Object.assign({}, (sdmGoal_1.goalData(goalEvent))), newData));
}
},
pushTest: options.pushTest,
events: [GoalInvocation_1.GoalProjectListenerEvent.after],
};
}
exports.cachePut = cachePut;
function isGlobFilePattern(toBeDetermined) {
return toBeDetermined.globPattern !== undefined;
}
function isDirectoryPattern(toBeDetermined) {
return toBeDetermined.directory !== undefined;
}
async function pushTestSucceeds(pushTest, gi, p) {
return (pushTest || commonPushTests_1.AnyPush).mapping({
push: gi.goalEvent.push,
project: p,
id: gi.id,
configuration: gi.configuration,
addressChannels: gi.addressChannels,
context: gi.context,
preferences: gi.preferences,
credentials: gi.credentials,
skill: gi.skill,
});
}
async function invokeCacheMissListeners(optsToUse, p, gi, event) {
for (const cacheMissFallback of array_1.toArray(optsToUse.onCacheMiss)) {
const allEvents = [GoalInvocation_1.GoalProjectListenerEvent.before, GoalInvocation_1.GoalProjectListenerEvent.after];
if ((cacheMissFallback.events || allEvents).filter(e => e === event).length > 0
&& await pushTestSucceeds(cacheMissFallback.pushTest, gi, p)) {
await cacheMissFallback.listener(p, gi, event);
}
}
}
exports.NoOpGoalProjectListenerRegistration = {
name: "NoOpListener",
listener: async () => {
},
pushTest: commonPushTests_1.AnyPush,
};
/**
* Goal listener that performs cache restores before a goal has been run.
* @param options The options for caching
* @param classifier Whether only a specific classifier, as defined in the options,
* needs to be restored. If omitted, all classifiers defined in the options are restored.
* @param classifiers Additional classifiers that need to be restored.
*/
function cacheRestore(options, classifier, ...classifiers) {
const allClassifiers = [];
if (classifier) {
allClassifiers.push(classifier, ...(classifiers || []));
}
const optsToUse = Object.assign({ onCacheMiss: exports.NoOpGoalProjectListenerRegistration }, options);
const classifiersToBeRestored = [];
if (allClassifiers.length > 0) {
classifiersToBeRestored.push(...allClassifiers);
}
else {
classifiersToBeRestored.push(...optsToUse.entries.map(entry => entry.classifier));
}
const listenerName = `restoring inputs`;
return {
name: listenerName,
listener: async (p, gi, event) => {
if (!!isCacheEnabled(gi)) {
const goalCache = cacheStore(gi);
for (const c of classifiersToBeRestored) {
try {
const resolvedClassifier = await resolveClassifierPath(c, gi);
await goalCache.retrieve(gi, p, resolvedClassifier);
}
catch (e) {
await invokeCacheMissListeners(optsToUse, p, gi, event);
}
}
}
else {
await invokeCacheMissListeners(optsToUse, p, gi, event);
}
// Set inputs on the goal data
const { goalEvent } = gi;
const data = sdmGoal_1.goalData(goalEvent);
const newData = {
[exports.CacheInputGoalDataKey]: [
...(data[exports.CacheInputGoalDataKey] || []),
...classifiersToBeRestored.map(c => ({
classifier: c,
})),
],
};
goalEvent.data = JSON.stringify(Object.assign(Object.assign({}, (sdmGoal_1.goalData(goalEvent))), newData));
},
pushTest: optsToUse.pushTest,
events: [GoalInvocation_1.GoalProjectListenerEvent.before],
};
}
exports.cacheRestore = cacheRestore;
/**
* Goal listener that cleans up the cache restores after a goal has been run.
* @param options The options for caching
* @param classifier Whether only a specific classifier, as defined in the options,
* needs to be removed. If omitted, all classifiers are removed.
* @param classifiers Additional classifiers that need to be removed.
*/
function cacheRemove(options, classifier, ...classifiers) {
const allClassifiers = [];
if (classifier) {
allClassifiers.push(...[classifier, ...classifiers]);
}
const classifiersToBeRemoved = [];
if (allClassifiers.length > 0) {
classifiersToBeRemoved.push(...allClassifiers);
}
else {
classifiersToBeRemoved.push(...options.entries.map(entry => entry.classifier));
}
const listenerName = `removing outputs`;
return {
name: listenerName,
listener: async (p, gi) => {
if (!!isCacheEnabled(gi)) {
const goalCache = cacheStore(gi);
for (const c of classifiersToBeRemoved) {
const resolvedClassifier = await resolveClassifierPath(c, gi);
await goalCache.remove(gi, resolvedClassifier);
}
}
},
pushTest: options.pushTest,
events: [GoalInvocation_1.GoalProjectListenerEvent.after],
};
}
exports.cacheRemove = cacheRemove;
async function getFilePathsThroughPattern(project, globPattern) {
const oldExcludes = fileGlobs_1.DefaultExcludes;
fileGlobs_1.DefaultExcludes.splice(0, fileGlobs_1.DefaultExcludes.length); // necessary evil
try {
return await projectUtils_1.gatherFromFiles(project, globPattern, async (f) => f.path);
}
finally {
fileGlobs_1.DefaultExcludes.push(...oldExcludes);
}
}
function isCacheEnabled(gi) {
return _.get(gi.configuration, "sdm.cache.enabled", false);
}
function cacheStore(gi) {
return _.get(gi.configuration, "sdm.cache.store", DefaultGoalCache);
}
/**
* Interpolate information from goal invocation into the classifier.
*/
async function resolveClassifierPath(classifier, gi) {
if (!classifier) {
return gi.context.workspaceId;
}
const wrapper = { classifier };
await configuration_1.resolvePlaceholders(wrapper, v => resolvePlaceholder_1.resolvePlaceholder(v, gi.goalEvent, gi, {}));
return gi.context.workspaceId + "/" + sanitizeClassifier(wrapper.classifier);
}
exports.resolveClassifierPath = resolveClassifierPath;
/**
* Sanitize classifier for use in path. Replace any characters
* which might cause problems on POSIX or MS Windows with "_",
* including path separators. Ensure resulting file is not "hidden".
*/
function sanitizeClassifier(classifier) {
return classifier.replace(/[^-.0-9A-Za-z_+]/g, "_")
.replace(/^\.+/, ""); // hidden
}
exports.sanitizeClassifier = sanitizeClassifier;
//# sourceMappingURL=goalCaching.js.map