UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

332 lines (293 loc) 13.3 kB
/* * 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. */ import { HandleCommand } from "@atomist/automation-client/lib/HandleCommand"; import { HandleEvent } from "@atomist/automation-client/lib/HandleEvent"; import { guid } from "@atomist/automation-client/lib/internal/util/string"; import { NoParameters } from "@atomist/automation-client/lib/SmartParameters"; import { Maker } from "@atomist/automation-client/lib/util/constructionUtils"; import { logger } from "@atomist/automation-client/lib/util/logger"; import * as _ from "lodash"; import { AdminCommunicationContext } from "../../api/context/AdminCommunicationContext"; import { enrichGoalSetters, GoalContribution, goalContributors, } from "../../api/dsl/goalContribution"; import { Goal } from "../../api/goal/Goal"; import { ExecuteGoal, GoalProjectListenerRegistration, } from "../../api/goal/GoalInvocation"; import { Goals } from "../../api/goal/Goals"; import { NoProgressReport, ReportProgress, } from "../../api/goal/progress/ReportProgress"; import { TriggeredListenerInvocation } from "../../api/listener/TriggeredListener"; import { validateConfigurationValues } from "../../api/machine/ConfigurationValues"; import { ExtensionPack } from "../../api/machine/ExtensionPack"; import { registrableManager } from "../../api/machine/Registerable"; import { SoftwareDeliveryMachine } from "../../api/machine/SoftwareDeliveryMachine"; import { SoftwareDeliveryMachineConfiguration } from "../../api/machine/SoftwareDeliveryMachineOptions"; import { GoalSetter } from "../../api/mapping/GoalSetter"; import { PushMapping } from "../../api/mapping/PushMapping"; import { PushTest } from "../../api/mapping/PushTest"; import { AnyPush } from "../../api/mapping/support/commonPushTests"; import { PushRules } from "../../api/mapping/support/PushRules"; import { CodeInspectionRegistration } from "../../api/registration/CodeInspectionRegistration"; import { CodeTransformRegistration } from "../../api/registration/CodeTransformRegistration"; import { CommandHandlerRegistration } from "../../api/registration/CommandHandlerRegistration"; import { CommandRegistration } from "../../api/registration/CommandRegistration"; import { EventHandlerRegistration } from "../../api/registration/EventHandlerRegistration"; import { GeneratorRegistration } from "../../api/registration/GeneratorRegistration"; import { GoalApprovalRequestVoteDecisionManager, GoalApprovalRequestVoter, UnanimousGoalApprovalRequestVoteDecisionManager, } from "../../api/registration/goalApprovalRequestVote"; import { IngesterRegistration } from "../../api/registration/IngesterRegistration"; import { InterpretLog } from "../../spi/log/InterpretedLog"; import { DefaultGoalImplementationMapper } from "../goal/DefaultGoalImplementationMapper"; import { GoalSetGoalCompletionListener } from "../listener/goalSetListener"; import { LogSuppressor } from "../log/logInterpreters"; import { HandlerRegistrationManagerSupport } from "./HandlerRegistrationManagerSupport"; import { ListenerRegistrationManagerSupport } from "./ListenerRegistrationManagerSupport"; /** * Abstract support class for implementing a SoftwareDeliveryMachine. */ export abstract class AbstractSoftwareDeliveryMachine<O extends SoftwareDeliveryMachineConfiguration = SoftwareDeliveryMachineConfiguration> extends ListenerRegistrationManagerSupport implements SoftwareDeliveryMachine<O> { public abstract readonly commandHandlers: Array<Maker<HandleCommand>>; public abstract readonly eventHandlers: Array<Maker<HandleEvent<any>>>; public abstract readonly ingesters: string[]; public readonly extensionPacks: ExtensionPack[] = []; protected readonly registrationManager: HandlerRegistrationManagerSupport = new HandlerRegistrationManagerSupport(this); protected readonly disposalGoalSetters: GoalSetter[] = []; protected readonly goalApprovalRequestVoters: GoalApprovalRequestVoter[] = []; protected goalApprovalRequestVoteDecisionManager: GoalApprovalRequestVoteDecisionManager = UnanimousGoalApprovalRequestVoteDecisionManager; private pushMap: GoalSetter; private fulfillmentMapper: DefaultGoalImplementationMapper; public get goalFulfillmentMapper(): DefaultGoalImplementationMapper { if (!this.fulfillmentMapper) { this.fulfillmentMapper = new DefaultGoalImplementationMapper(); } return this.fulfillmentMapper; } /** * Return the PushMapping that will be used on pushes. * Useful in testing goal setting. * @return {PushMapping<Goals>} */ get pushMapping(): PushMapping<Goals> { return this.pushMap; } /** * Provide the implementation for a goal. * The SDM will run it as soon as the goal is ready (all preconditions are met). * If you provide a PushTest, then the SDM can assign different implementations * to the same goal based on the code in the project. * @param {string} implementationName * @param {Goal} goal * @param {ExecuteGoal} goalExecutor * @param options PushTest to narrow matching & InterpretLog that can handle * the log from the goalExecutor function * @return {this} */ public addGoalImplementation(implementationName: string, goal: Goal, goalExecutor: ExecuteGoal, options?: Partial<{ pushTest: PushTest, logInterpreter: InterpretLog, progressReporter: ReportProgress, projectListeners: GoalProjectListenerRegistration | GoalProjectListenerRegistration[], }>): this { const implementation = { implementationName, goal, goalExecutor, pushTest: _.get(options, "pushTest") || AnyPush, logInterpreter: _.get(options, "logInterpreter") || LogSuppressor, progressReporter: _.get(options, "progressReporter") || NoProgressReport, projectListeners: _.get(options, "projectListeners") || [], }; this.goalFulfillmentMapper.addImplementation(implementation); return this; } public addGoalApprovalRequestVoter(vote: GoalApprovalRequestVoter): this { this.goalApprovalRequestVoters.push(vote); return this; } public setGoalApprovalRequestVoteDecisionManager(manager: GoalApprovalRequestVoteDecisionManager): this { this.goalApprovalRequestVoteDecisionManager = manager; return this; } public addCommand<P>(cmd: CommandHandlerRegistration<P>): this { if (this.shouldRegister(cmd)) { this.registrationManager.addCommand(cmd); } return this; } public addGeneratorCommand<P = NoParameters>(gen: GeneratorRegistration<P>): this { if (this.shouldRegister(gen)) { this.registrationManager.addGeneratorCommand(gen); } return this; } public addCodeTransformCommand<P>(ct: CodeTransformRegistration<P>): this { if (this.shouldRegister(ct)) { this.registrationManager.addCodeTransformCommand<P>(ct); } return this; } public addCodeInspectionCommand<R, P>(cir: CodeInspectionRegistration<R, P>): this { if (this.shouldRegister(cir)) { this.registrationManager.addCodeInspectionCommand<R, P>(cir); } return this; } public addEvent<T, P>(e: EventHandlerRegistration<T, P>): this { this.registrationManager.addEvent<T, P>(e); return this; } public addIngester(i: string | IngesterRegistration): this { if (typeof i === "string") { this.registrationManager.addIngester({ ingester: i, }); } else { this.registrationManager.addIngester(i); } return this; } /** * Declare that a goal will become successful based on something outside. * For instance, ArtifactGoal succeeds because of an ImageLink event. * This tells the SDM that it does not need to run anything when this * goal becomes ready. */ public addGoalSideEffect(goal: Goal, sideEffectName: string, registration?: string, pushTest: PushTest = AnyPush): this { this.goalFulfillmentMapper.addSideEffect({ goal, sideEffectName, pushTest, registration, }); return this; } public addGoalContributions(goalContributions: GoalSetter): this { if (!this.pushMap) { this.pushMap = goalContributions; } else { this.pushMap = enrichGoalSetters(this.pushMap, goalContributions); } return this; } public withPushRules(contributor: GoalContribution<any>, ...contributors: Array<GoalContribution<any>>): this { return this.addGoalContributions(goalContributors(contributor, ...contributors)); } public addExtensionPacks(...packs: ExtensionPack[]): this { for (const pack of packs) { const found = this.extensionPacks.find(existing => existing.name === pack.name && existing.vendor === pack.vendor); if (!!found) { pack.name = `${pack.name}-${guid().slice(0, 7)}`; } this.addExtensionPack(pack); if (!!pack.goalContributions) { this.addGoalContributions(pack.goalContributions); } } return this; } private addExtensionPack(pack: ExtensionPack): this { logger.debug("Adding extension pack '%s' version %s from %s", pack.name, pack.version, pack.vendor); validateConfigurationValues(this.configuration, pack); pack.configure(this); this.extensionPacks.push(pack); return this; } /** * Invoke StartupListeners. */ public async notifyStartupListeners(): Promise<void> { const i: AdminCommunicationContext = { addressAdmin: this.configuration.sdm.adminAddressChannels || (async msg => { logger.debug("startup: %j", msg); }), sdm: this, }; // Register the startup listener to schedule the triggered listeners // It is important we schedule the triggers after the startup listeners are done! this.startupListeners.push(async () => this.scheduleTriggeredListeners()); // Execute startupListeners in registration order await this.startupListeners.map(l => l(i)).reduce(p => p.then(), Promise.resolve()); } /** * Schedule the triggered listeners */ public scheduleTriggeredListeners(): void { const i: TriggeredListenerInvocation = { addressAdmin: this.configuration.sdm.adminAddressChannels || (async msg => { logger.debug("trigger: %j", msg); }), sdm: this, }; this.triggeredListeners.forEach(t => { if (t.trigger && t.trigger.cron) { const cj = require("cron"); const cron = new cj.CronJob({ cronTime: t.trigger.cron, onTick: () => t.listener(i), unrefTimeout: true, } as any); cron.start(); } if (t.trigger && t.trigger.interval) { setInterval(() => t.listener(i), t.trigger.interval).unref(); } }); } /** * Construct a new software delivery machine, with zero or * more goal setters. * @param {string} name * @param configuration automation client configuration we're running in * @param {GoalSetter} goalSetters tell me what to do on a push. Hint: start with "whenPushSatisfies(...)" */ protected constructor(public readonly name: string, public readonly configuration: O, goalSetters: Array<GoalSetter | GoalSetter[]>) { super(); registrableManager().register(this); // If we didn't get any goal setters don't register a mapping if (goalSetters.length > 0) { this.pushMap = new PushRules("Goal setters", _.flatten(goalSetters)); } // Register the goal completion listener to update goal set state this.addGoalCompletionListener(GoalSetGoalCompletionListener); } private shouldRegister<P>(cm: CommandRegistration<P>): boolean { return !!cm.registerWhen ? cm.registerWhen(this.configuration) : true; } }