UNPKG

4.09 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 { logger } from "@atomist/automation-client";
18import {
19 cancelableGoal,
20 GoalCompletionListener,
21 SdmGoalState,
22 SoftwareDeliveryMachine,
23} from "@atomist/sdm";
24import * as _ from "lodash";
25import {
26 deleteJob,
27 deletePods,
28 listJobs,
29 prettyPrintError,
30 sanitizeName,
31} from "./KubernetesGoalScheduler";
32
33/**
34 * GoalCompletionListener factory that puts completed goal jobs into a ttl cache for later deletion.
35 */
36export class KubernetesJobDeletingGoalCompletionListenerFactory {
37
38 private readonly cache: Map<string, { ttl: number, name: string, namespace: string }> = new Map();
39
40 constructor(private readonly sdm: SoftwareDeliveryMachine) {
41 this.initialize();
42 }
43
44 public create(): GoalCompletionListener {
45 return async gi => {
46 const goalEvent = gi.completedGoal;
47
48 if (goalEvent.state === SdmGoalState.in_process) {
49 return;
50 }
51
52 if (goalEvent.state === SdmGoalState.canceled && !(await cancelableGoal(goalEvent, gi.configuration))) {
53 return;
54 }
55
56 const selector = `atomist.com/goal-set-id=${goalEvent.goalSetId},atomist.com/creator=${sanitizeName(this.sdm.configuration.name)}`;
57 let jobs;
58
59 try {
60 jobs = await listJobs(selector);
61 } catch (e) {
62 logger.warn(`Failed to read k8s jobs: ${prettyPrintError(e)}`);
63 return;
64 }
65
66 logger.debug(
67 `Found k8s jobs for goal set '${goalEvent.goalSetId}': '${
68 jobs.map(j => `${j.metadata.namespace}:${j.metadata.name}`).join(", ")}'`);
69
70 const goalJobs = jobs.filter(j => {
71 const annotations = j.metadata.annotations;
72 if (!!annotations && !!annotations["atomist.com/sdm"]) {
73 const sdmAnnotation = JSON.parse(annotations["atomist.com/sdm"]);
74 return sdmAnnotation.goal.uniqueName === goalEvent.uniqueName;
75 }
76 return false;
77 });
78
79 logger.debug(
80 `Matching k8s job for goal '${goalEvent.uniqueName}' found: '${
81 goalJobs.map(j => `${j.metadata.namespace}:${j.metadata.name}`).join(", ")}'`);
82
83 const ttl: number = _.get(this.sdm.configuration, "sdm.k8s.job.ttl", 1000 * 60 * 2);
84
85 for (const goalJob of goalJobs) {
86 this.cache.set(
87 goalJob.metadata.uid,
88 {
89 ttl: Date.now() + ttl,
90 name: goalJob.metadata.name,
91 namespace: goalJob.metadata.namespace,
92 });
93 }
94 };
95 }
96
97 private initialize(): void {
98 setInterval(async () => {
99 const now = Date.now();
100 for (const uid of this.cache.keys()) {
101 const job = this.cache.get(uid);
102 if (job.ttl <= now) {
103 logger.debug(`Deleting k8s job '${job.namespace}:${job.name}'`);
104 await deleteJob(job);
105
106 logger.debug(`Deleting k8s pods for job '${job.namespace}:${job.name}'`);
107 await deletePods(job);
108 this.cache.delete(uid);
109 }
110 }
111 },
112 _.get(this.sdm.configuration, "sdm.k8s.job.ttlCheckInterval", 15000));
113 }
114}