UNPKG

10.6 kBPlain TextView Raw
1/*
2 * Copyright © 2019 Atomist, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import { GitProject } from "@atomist/automation-client";
18import {
19 DefaultGoalNameGenerator,
20 FulfillableGoal,
21 FulfillableGoalDetails,
22 FulfillableGoalWithRegistrations,
23 Fulfillment,
24 getGoalDefinitionFrom,
25 Goal,
26 GoalFulfillmentCallback,
27 ImplementationRegistration,
28 RepoContext,
29 SdmGoalEvent,
30 SoftwareDeliveryMachine,
31 testProgressReporter,
32} from "@atomist/sdm";
33import {
34 KubernetesFulfillmentGoalScheduler,
35 KubernetesFulfillmentOptions,
36} from "../../pack/k8s/KubernetesFulfillmentGoalScheduler";
37import {
38 isConfiguredInEnv,
39 KubernetesGoalScheduler,
40} from "../../pack/k8s/KubernetesGoalScheduler";
41import { KubernetesJobDeletingGoalCompletionListenerFactory } from "../../pack/k8s/KubernetesJobDeletingGoalCompletionListener";
42import { toArray } from "../../util/misc/array";
43import {
44 CacheEntry,
45 cachePut,
46 cacheRestore,
47} from "../cache/goalCaching";
48import { dockerContainerScheduler } from "./docker";
49import { k8sContainerScheduler } from "./k8s";
50import {
51 runningAsGoogleCloudFunction,
52 runningInK8s,
53} from "./util";
54
55export const ContainerRegistrationGoalDataKey = "@atomist/sdm/container";
56
57/**
58 * Create and return a container goal with the provided container
59 * specification.
60 *
61 * @param displayName Goal display name
62 * @param registration Goal containers, volumes, cache details, etc.
63 * @return SDM container goal
64 */
65export function container<T extends ContainerRegistration>(displayName: string, registration: T): FulfillableGoal {
66 return new Container({ displayName }).with(registration);
67}
68
69export const ContainerProgressReporter = testProgressReporter({
70 test: /docker 'network' 'create'/i,
71 phase: "starting up",
72}, {
73 test: /docker 'network' 'rm'/i,
74 phase: "shutting down",
75}, {
76 test: /docker 'run' .* '--workdir=[a-zA-Z\/]*' .* '--network-alias=([a-zA-Z \-_]*)'/i,
77 phase: "running $1",
78}, {
79 test: /atm:phase=(.*)/i,
80 phase: "$1",
81});
82
83/**
84 * Ports to expose from container.
85 */
86export interface ContainerPort {
87 /**
88 * Number of port to expose from the container. This must be
89 * a valid port number, 0 < x < 65536.
90 */
91 containerPort: number;
92}
93
94/**
95 * Volumes to mount in container.
96 */
97export interface ContainerVolumeMount {
98 /** Path to mount point of volume. */
99 mountPath: string;
100 /** Name of volume from [[GoalContainer.volumes]]. */
101 name: string;
102}
103
104export interface ContainerSecrets {
105 env?: Array<{ name: string } & GoalContainerSecret>;
106 fileMounts?: Array<{ mountPath: string } & GoalContainerSecret>;
107}
108
109/**
110 * Simplified container abstraction for goals.
111 */
112export interface GoalContainer {
113 /** Unique name for this container within this goal. */
114 name: string;
115 /** Full Docker image name, i.e., `registry/name:tag`. */
116 image: string;
117 /**
118 * Docker command and arguments. We call this `args` rather than
119 * `command` because we think k8s got it right.
120 */
121 args?: string[];
122 /**
123 * Docker image entrypoint. We call this `command` rather than
124 * `entrypoint` because we think k8s got it right.
125 */
126 command?: string[];
127 /**
128 * Environment variables to set in Docker container.
129 */
130 env?: Array<{ name: string, value: string }>;
131 /**
132 * Ports to expose from container.
133 */
134 ports?: ContainerPort[];
135 /**
136 * Volumes to mount in container.
137 */
138 volumeMounts?: ContainerVolumeMount[];
139 /**
140 * Provider secrets that should be made available to the container
141 */
142 secrets?: ContainerSecrets;
143}
144
145export interface GoalContainerProviderSecret {
146 provider: {
147 type: "docker" | "npm" | "maven2" | "scm" | "atomist";
148 names?: string[];
149 };
150}
151
152export interface GoalContainerEncryptedSecret {
153 encrypted: string;
154}
155
156export interface GoalContainerSecret {
157 value: GoalContainerProviderSecret | GoalContainerEncryptedSecret;
158}
159
160/**
161 * Volumes that containers in goal can mount.
162 */
163export interface GoalContainerVolume {
164 /** Volume to be created from local host file system location. */
165 hostPath: {
166 /** Absolute path on host to volume. */
167 path: string;
168 };
169 /** Unique name of volume, referenced by [[ContainerVolumeMount.name]]. */
170 name: string;
171}
172
173/**
174 * File system location of goal project in containers.
175 */
176export const ContainerProjectHome = "/atm/home";
177
178/**
179 * File system location for goal container input.
180 */
181export const ContainerInput = "/atm/input";
182
183/**
184 * File system location for goal container output.
185 */
186export const ContainerOutput = "/atm/output";
187
188/**
189 * Goal execution result file
190 */
191export const ContainerResult = `${ContainerOutput}/result.json`;
192
193/**
194 * Specification of containers and volumes for a container goal.
195 */
196export interface GoalContainerSpec {
197 /**
198 * Containers to run for this goal. The goal result is based on
199 * the exit status of the first element of the `containers` array.
200 * The other containers are considered "sidecar" containers
201 * provided functionality that the main container needs to
202 * function. The working directory of the first container is set
203 * to [[ContainerProjectHome]], which contains the project upon
204 * which the goal should operate.
205 */
206 containers: GoalContainer[];
207 /**
208 * Volumes available to mount in containers.
209 */
210 volumes?: GoalContainerVolume[];
211}
212
213/**
214 * Function signature for callback that can modify and return the
215 * [[ContainerRegistration]] object.
216 */
217export type ContainerSpecCallback =
218 (r: ContainerRegistration, p: GitProject, g: Container, e: SdmGoalEvent, c: RepoContext) => Promise<GoalContainerSpec>;
219
220/**
221 * Container goal artifacts and implementations information.
222 */
223export interface ContainerRegistration extends Partial<ImplementationRegistration>, GoalContainerSpec {
224 /**
225 * Callback function to dynamically modify the goal container spec
226 * when goal is executed.
227 */
228 callback?: ContainerSpecCallback;
229 /**
230 * Cache classifiers to retrieve from cache before starting goal
231 * execution. The values must correspond to output classifiers
232 * from previously executed container goals in the same goal set.
233 */
234 input?: Array<{ classifier: string }>;
235 /**
236 * File path globs to store in cache after goal execution.
237 * They values should be glob paths relative to the root of
238 * the project directory.
239 */
240 output?: CacheEntry[];
241}
242
243/**
244 * Container goal scheduler implementation. The goal execution is
245 * handled as part of the execution of the container.
246 */
247export type ContainerScheduler = (goal: Container, registration: ContainerRegistration) => void;
248
249export interface ContainerGoalDetails extends FulfillableGoalDetails {
250 /**
251 * Container goal scheduler. If no scheduler is provided, the k8s
252 * scheduler is used if the SDM is running in a Kubernetes
253 * cluster, otherwise the Docker scheduler is used.
254 */
255 scheduler?: ContainerScheduler;
256}
257
258/**
259 * Goal run as a container, as seen on TV.
260 */
261export class Container extends FulfillableGoalWithRegistrations<ContainerRegistration> {
262
263 public readonly details: ContainerGoalDetails;
264
265 constructor(details: ContainerGoalDetails = {}, ...dependsOn: Goal[]) {
266 const prefix = "container" + (details.displayName ? `-${details.displayName}` : "");
267 const detailsToUse = { ...details, isolate: true };
268 super(getGoalDefinitionFrom(detailsToUse, DefaultGoalNameGenerator.generateName(prefix)), ...dependsOn);
269 this.details = detailsToUse;
270 }
271
272 public register(sdm: SoftwareDeliveryMachine): void {
273 super.register(sdm);
274
275 const goalSchedulers = toArray(sdm.configuration.sdm.goalScheduler) || [];
276 if (runningInK8s()) {
277 // Make sure that the KubernetesGoalScheduler gets added if needed
278 if (!goalSchedulers.some(gs => gs instanceof KubernetesGoalScheduler)) {
279 if (!process.env.ATOMIST_ISOLATED_GOAL && isConfiguredInEnv("kubernetes", "kubernetes-all")) {
280 sdm.configuration.sdm.goalScheduler = [...goalSchedulers, new KubernetesGoalScheduler()];
281 sdm.addGoalCompletionListener(new KubernetesJobDeletingGoalCompletionListenerFactory(sdm).create());
282 }
283 }
284 } else if (runningAsGoogleCloudFunction()) {
285 const options: KubernetesFulfillmentOptions = sdm.configuration.sdm?.k8s?.fulfillment;
286 if (!goalSchedulers.some(gs => gs instanceof KubernetesFulfillmentGoalScheduler)) {
287 sdm.configuration.sdm.goalScheduler = [
288 ...goalSchedulers,
289 new KubernetesFulfillmentGoalScheduler(options),
290 ];
291 }
292 }
293 }
294
295 public with(registration: ContainerRegistration): this {
296 super.with(registration);
297
298 registration.name = (registration.name || `container-${this.definition.displayName}`).replace(/\.+/g, "-");
299 if (!this.details.scheduler) {
300 if (runningInK8s() || runningAsGoogleCloudFunction()) {
301 this.details.scheduler = k8sContainerScheduler;
302 } else {
303 this.details.scheduler = dockerContainerScheduler;
304 }
305 }
306 this.details.scheduler(this, registration);
307 if (registration.input && registration.input.length > 0) {
308 this.withProjectListener(cacheRestore({ entries: registration.input }));
309 }
310 if (registration.output && registration.output.length > 0) {
311 this.withProjectListener(cachePut({ entries: registration.output }));
312 }
313 return this;
314 }
315
316 public addFulfillment(fulfillment: Fulfillment): this {
317 return super.addFulfillment(fulfillment);
318 }
319
320 public addFulfillmentCallback(cb: GoalFulfillmentCallback): this {
321 return super.addFulfillmentCallback(cb);
322 }
323}