1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import { resolvePlaceholders } from "@atomist/automation-client/lib/configuration";
|
18 | import {
|
19 | and,
|
20 | DefaultGoalNameGenerator,
|
21 | FulfillableGoal,
|
22 | Goal,
|
23 | GoalProjectListenerEvent,
|
24 | Goals,
|
25 | PlannedGoal,
|
26 | PlannedGoals,
|
27 | PushListenerInvocation,
|
28 | pushTest,
|
29 | PushTest,
|
30 | } from "@atomist/sdm";
|
31 | import * as camelcaseKeys from "camelcase-keys";
|
32 | import * as yaml from "js-yaml";
|
33 | import * as _ from "lodash";
|
34 | import * as os from "os";
|
35 | import { DeliveryGoals } from "../../machine/configure";
|
36 | import { mapTests } from "../../machine/yaml/mapPushTests";
|
37 | import { toArray } from "../../util/misc/array";
|
38 | import {
|
39 | cachePut,
|
40 | cacheRestore,
|
41 | } from "../cache/goalCaching";
|
42 | import {
|
43 | Container,
|
44 | ContainerProgressReporter,
|
45 | ContainerRegistration,
|
46 | GoalContainer,
|
47 | GoalContainerVolume,
|
48 | } from "./container";
|
49 | import { executeDockerJob } from "./docker";
|
50 |
|
51 | export const hasRepositoryGoals: PushTest = pushTest("has SDM goals", async pli => {
|
52 | return (await pli.project.getFiles(".atomist/*goals.{yml,yaml}")).length > 0;
|
53 | });
|
54 |
|
55 | export function repositoryDrivenContainer(options: { tests?: Record<string, PushTest> } = {}): Goal {
|
56 | return new RepositoryDrivenContainer(options.tests || {});
|
57 | }
|
58 |
|
59 | export class RepositoryDrivenContainer extends FulfillableGoal {
|
60 |
|
61 | constructor(private readonly tests: Record<string, PushTest>) {
|
62 | super({ uniqueName: "repository-driven-goal" });
|
63 |
|
64 | this.addFulfillment({
|
65 | progressReporter: ContainerProgressReporter,
|
66 | goalExecutor: async gi => {
|
67 | const registration = gi.parameters.registration as ContainerRegistration;
|
68 |
|
69 | const c = new Container({ displayName: this.definition.displayName });
|
70 | (c as any).register = () => {
|
71 | };
|
72 | (c as any).addFulfillment = () => c;
|
73 | (c as any).addFulfillmentCallback = () => c;
|
74 | (c as any).withProjectListener = () => c;
|
75 | c.with(registration);
|
76 |
|
77 | return executeDockerJob(c, registration)(gi);
|
78 | },
|
79 | name: DefaultGoalNameGenerator.generateName(`container-docker-${this.definition.displayName}`),
|
80 | });
|
81 |
|
82 | this.withProjectListener({
|
83 | name: "cache-restore",
|
84 | events: [GoalProjectListenerEvent.before],
|
85 | listener: async (p, gi, e) => {
|
86 | const registration = gi.parameters.registration as ContainerRegistration;
|
87 | if (registration.input && registration.input.length > 0) {
|
88 | await cacheRestore({ entries: registration.input }).listener(p, gi, e);
|
89 | }
|
90 | },
|
91 | }).withProjectListener({
|
92 | name: "cache-put",
|
93 | events: [GoalProjectListenerEvent.after],
|
94 | listener: async (p, gi, e) => {
|
95 | const registration = gi.parameters.registration as ContainerRegistration;
|
96 | if (registration.output && registration.output.length > 0) {
|
97 | await cachePut({ entries: registration.output }).listener(p, gi, e);
|
98 | }
|
99 | },
|
100 | });
|
101 |
|
102 | }
|
103 |
|
104 | public async plan(pli: PushListenerInvocation, goals: Goals): Promise<PlannedGoals> {
|
105 | const configYamls = (await pli.project.getFiles(".atomist/*goals.{yml,yaml}"))
|
106 | .sort((f1, f2) => f1.path.localeCompare(f2.path));
|
107 |
|
108 | const plan: PlannedGoals = {};
|
109 | for (const configYaml of configYamls) {
|
110 | const configs = yaml.safeLoadAll(await configYaml.getContent());
|
111 |
|
112 | for (const config of configs) {
|
113 |
|
114 | for (const k in config) {
|
115 |
|
116 | if (config.hasOwnProperty(k)) {
|
117 | const value = config[k];
|
118 | const v = camelcaseKeys(value, { deep: true }) as any;
|
119 | const test = and(...toArray(await mapTests(v.test, this.tests, {})));
|
120 | if (await test.mapping(pli)) {
|
121 | const plannedGoals = toArray(mapGoals(v.goals, {}));
|
122 | plan[k] = {
|
123 | goals: plannedGoals,
|
124 | dependsOn: v.dependsOn,
|
125 | };
|
126 | }
|
127 | }
|
128 | }
|
129 | }
|
130 | }
|
131 |
|
132 | await resolvePlaceholders(plan as any, value => resolvePlaceholder(value, pli));
|
133 |
|
134 | return plan;
|
135 | }
|
136 | }
|
137 |
|
138 | function mapGoals(goals: any, additionalGoals: DeliveryGoals): PlannedGoal | PlannedGoal[] {
|
139 | if (Array.isArray(goals)) {
|
140 | return toArray(goals).map(g => mapGoals(g, additionalGoals)) as PlannedGoal[];
|
141 | } else {
|
142 | if (!!goals.containers) {
|
143 | const name = _.get(goals, "containers.name") || _.get(goals, "containers[0].name");
|
144 | return mapPlannedGoal(
|
145 | name,
|
146 | goals,
|
147 | toArray(goals.containers),
|
148 | toArray(goals.volumes));
|
149 | } else if (!!goals.script) {
|
150 | const script = goals.script;
|
151 | return mapPlannedGoal(script.name, script, [{
|
152 | name: script.name,
|
153 | image: script.image || "ubuntu:latest",
|
154 | command: script.command,
|
155 | args: script.args,
|
156 | }], []);
|
157 | } else {
|
158 | throw new Error(`Unable to construct goal from '${JSON.stringify(goals)}'`);
|
159 | }
|
160 | }
|
161 | }
|
162 |
|
163 | function mapPlannedGoal(name: string, details: any, containers: GoalContainer[], volumes: GoalContainerVolume[]): PlannedGoal {
|
164 |
|
165 | const gd = new Goal({ uniqueName: name, displayName: name });
|
166 | return {
|
167 | details: {
|
168 | displayName: gd.definition.displayName,
|
169 | descriptions: {
|
170 | planned: gd.plannedDescription,
|
171 | requested: gd.requestedDescription,
|
172 | inProcess: gd.inProcessDescription,
|
173 | completed: gd.successDescription,
|
174 | failed: gd.failureDescription,
|
175 | canceled: gd.canceledDescription,
|
176 | stopped: gd.stoppedDescription,
|
177 | waitingForApproval: gd.waitingForApprovalDescription,
|
178 | waitingForPreApproval: gd.waitingForPreApprovalDescription,
|
179 | },
|
180 | retry: details.retry,
|
181 | preApproval: details.preApproval,
|
182 | approval: details.approval,
|
183 | },
|
184 | parameters: {
|
185 | registration: {
|
186 | containers,
|
187 | volumes,
|
188 | input: details.input,
|
189 | output: details.output,
|
190 | },
|
191 | },
|
192 | };
|
193 | }
|
194 |
|
195 | const PlaceholderExpression = /\$\{([.a-zA-Z_-]+)([.:0-9a-zA-Z-_ \" ]+)*\}/g;
|
196 |
|
197 | async function resolvePlaceholder(value: string, pli: PushListenerInvocation): Promise<string> {
|
198 | if (!PlaceholderExpression.test(value)) {
|
199 | return value;
|
200 | }
|
201 | PlaceholderExpression.lastIndex = 0;
|
202 | let currentValue = value;
|
203 | let result: RegExpExecArray;
|
204 |
|
205 | while (result = PlaceholderExpression.exec(currentValue)) {
|
206 | const fm = result[0];
|
207 | let envValue = _.get(pli, result[1]);
|
208 | if (result[1] === "home") {
|
209 | envValue = os.userInfo().homedir;
|
210 | }
|
211 | const defaultValue = result[2] ? result[2].trim().slice(1) : undefined;
|
212 |
|
213 | if (envValue) {
|
214 | currentValue = currentValue.split(fm).join(envValue);
|
215 | } else if (defaultValue) {
|
216 | currentValue = currentValue.split(fm).join(defaultValue);
|
217 | } else {
|
218 | throw new Error(`Placeholder '${result[1]}' can't be resolved`);
|
219 | }
|
220 | PlaceholderExpression.lastIndex = 0;
|
221 | }
|
222 | return currentValue;
|
223 | }
|