@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
332 lines (293 loc) • 13.3 kB
text/typescript
/*
* 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;
}
}