UNPKG

@atomist/sdm-core

Version:

Atomist Software Delivery Machine - Implementation

351 lines (313 loc) 12.5 kB
/* * Copyright © 2019 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. */ import { DefaultExcludes, GitProject, Project, projectUtils, } from "@atomist/automation-client"; import { AnyPush, ExecuteGoalResult, GoalInvocation, GoalProjectListenerEvent, GoalProjectListenerRegistration, PushTest, } from "@atomist/sdm"; import * as _ from "lodash"; import { toArray } from "../../util/misc/array"; import { CompressingGoalCache } from "./CompressingGoalCache"; export const CacheInputGoalDataKey = "@atomist/sdm/input"; export const CacheOutputGoalDataKey = "@atomist/sdm/output"; /** * Goal cache interface for storing and retrieving arbitrary files produced * by the execution of a goal. * @see FileSystemGoalCache` */ export interface GoalCache { /** * Add a set of files (or directories) to the cache. * @param gi The goal invocation for which the cache needs to be stored. * @param p The project where the files (or directories) reside. * @param files The files (or directories) to be cached. * @param classifier An optional classifier to identify the set of files (or directories to be cached). */ put(gi: GoalInvocation, p: GitProject, files: string | string[], classifier?: string): Promise<void>; /** * Retrieve files from the cache. * @param gi The goal invocation for which the cache needs to be restored. * @param p he project where the files (or directories) need to be restored in. * @param classifier Optionally the classifier of the cache for the files to be restored. If not defined, * all caches for the GoalInvocation are restored. */ retrieve(gi: GoalInvocation, p: GitProject, classifier?: string): Promise<void>; /** * Remove files from the cache. * @param gi The goal invocation for which the cache needs to be removed. * @param classifier Optionally the classifier of the cache for the files to be removed. If not defined, * all classifiers are removed. */ remove(gi: GoalInvocation, classifier?: string): Promise<void>; } /** * Suitable for a limited set of files adhering to a pattern. */ export interface GlobFilePattern { globPattern: string | string[]; } /** * Suitable for caching complete directories, possibly containing a lot of files. */ export interface DirectoryPattern { directory: string; } export interface CacheEntry { classifier: string; pattern: GlobFilePattern | DirectoryPattern; } /** * Core options for goal caching. */ export interface GoalCacheCoreOptions { /** * Optional push test on when to trigger caching */ pushTest?: PushTest; /** * Optional listener functions that should be called when no cache entry is found. */ onCacheMiss?: GoalProjectListenerRegistration | GoalProjectListenerRegistration[]; } /** * Options for putting goal cache entries. */ export interface GoalCacheOptions extends GoalCacheCoreOptions { /** * Collection of glob patterns with classifiers to determine which * files need to be cached between goal invocations, possibly * excluding paths using regular expressions. */ entries: CacheEntry[]; } /** * Options for restoring goal cache entries. */ export interface GoalCacheRestoreOptions extends GoalCacheCoreOptions { entries?: Array<{ classifier: string }>; } const DefaultGoalCache = new 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. */ export function cachePut(options: GoalCacheOptions, classifier?: string, ...classifiers: string[]): GoalProjectListenerRegistration { 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: GitProject, gi: GoalInvocation): Promise<void | ExecuteGoalResult> => { if (!!isCacheEnabled(gi) && !process.env.ATOMIST_ISOLATED_GOAL_INIT) { const goalCache = cacheStore(gi); for (const entry of entries) { 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)) { await goalCache.put(gi, p, files, entry.classifier); } } // Set outputs on the goal data const { goalEvent } = gi; const data = JSON.parse(goalEvent.data || "{}"); const newData = { [CacheOutputGoalDataKey]: [ ...(data[CacheOutputGoalDataKey] || []), ...entries, ], }; goalEvent.data = JSON.stringify({ ...(JSON.parse(goalEvent.data || "{}")), ...newData, }); } }, pushTest: options.pushTest, events: [GoalProjectListenerEvent.after], }; } function isGlobFilePattern(toBeDetermined: any): toBeDetermined is GlobFilePattern { return toBeDetermined.globPattern !== undefined; } function isDirectoryPattern(toBeDetermined: any): toBeDetermined is DirectoryPattern { return toBeDetermined.directory !== undefined; } async function pushTestSucceeds(pushTest: PushTest, gi: GoalInvocation, p: GitProject): Promise<boolean> { return (pushTest || 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, }); } async function invokeCacheMissListeners(optsToUse: GoalCacheOptions | GoalCacheRestoreOptions, p: GitProject, gi: GoalInvocation, event: GoalProjectListenerEvent): Promise<void> { for (const cacheMissFallback of toArray(optsToUse.onCacheMiss)) { const allEvents = [GoalProjectListenerEvent.before, GoalProjectListenerEvent.after]; if ((cacheMissFallback.events || allEvents).filter(e => e === event).length > 0 && await pushTestSucceeds(cacheMissFallback.pushTest, gi, p)) { await cacheMissFallback.listener(p, gi, event); } } } export const NoOpGoalProjectListenerRegistration: GoalProjectListenerRegistration = { name: "NoOpListener", listener: async () => { }, pushTest: 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. */ export function cacheRestore(options: GoalCacheRestoreOptions, classifier?: string, ...classifiers: string[]): GoalProjectListenerRegistration { const allClassifiers = []; if (classifier) { allClassifiers.push(classifier, ...(classifiers || [])); } const optsToUse: GoalCacheRestoreOptions = { onCacheMiss: 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: GitProject, gi: GoalInvocation, event: GoalProjectListenerEvent): Promise<void | ExecuteGoalResult> => { if (!!isCacheEnabled(gi)) { const goalCache = cacheStore(gi); for (const c of classifiersToBeRestored) { try { await goalCache.retrieve(gi, p, c); } 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 = JSON.parse(goalEvent.data || "{}"); const newData = { [CacheInputGoalDataKey]: [ ...(data[CacheInputGoalDataKey] || []), ...classifiersToBeRestored.map(c => ({ classifier: c, })), ], }; goalEvent.data = JSON.stringify({ ...(JSON.parse(goalEvent.data || "{}")), ...newData, }); }, pushTest: optsToUse.pushTest, events: [GoalProjectListenerEvent.before], }; } /** * 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. */ export function cacheRemove(options: GoalCacheOptions, classifier?: string, ...classifiers: string[]): GoalProjectListenerRegistration { 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) { await goalCache.remove(gi, c); } } }, pushTest: options.pushTest, events: [GoalProjectListenerEvent.after], }; } async function getFilePathsThroughPattern(project: Project, globPattern: string | string[]): Promise<string[]> { const oldExcludes = DefaultExcludes; DefaultExcludes.splice(0, DefaultExcludes.length); // necessary evil try { return await projectUtils.gatherFromFiles(project, globPattern, async f => f.path); } finally { DefaultExcludes.push(...oldExcludes); } } function isCacheEnabled(gi: GoalInvocation): boolean { return _.get(gi.configuration, "sdm.cache.enabled", false); } function cacheStore(gi: GoalInvocation): GoalCache { return _.get(gi.configuration, "sdm.cache.store", DefaultGoalCache); }