UNPKG

8.61 kBPlain TextView Raw
1import { getConfigurationNames, renderRealmFile } from './realm';
2import { Realm } from './realm';
3import { asNames, prompt } from './utils';
4import { spawn } from 'p-spawn';
5import { GetPodResponse, PodItem, PodItemFilter, KPod, toKPod } from './k8s-types';
6
7// --------- Public create/delete/logs/restart --------- //
8export async function kcreate(realm: Realm, resourceNames?: string | string[]) {
9 const names = await getConfigurationNames(realm, resourceNames);
10
11 for (let name of names) {
12 const fileName = await renderRealmFile(realm, name);
13 try {
14 const args = ['create', '-f', fileName];
15 await spawn('kubectl', args);
16 } catch (ex) {
17 console.log(`Can't kcreate ${fileName}, skipping`);
18 }
19 console.log();
20 }
21
22}
23
24export async function kapply(realm: Realm, resourceNames?: string | string[]) {
25 const names = await getConfigurationNames(realm, resourceNames);
26
27 for (let name of names) {
28 const fileName = await renderRealmFile(realm, name);
29 try {
30 const args = ['apply', '-f', fileName];
31 addNamespaceIfDefined(realm, args);
32 await spawn('kubectl', args);
33 } catch (ex) {
34 console.log(`Can't kapply ${fileName}, skipping`);
35 }
36 console.log();
37 }
38}
39
40// TODO: need to have a way to force the YES when use as API outside of cmd.
41export async function kdel(realm: Realm, resourceNames?: string | string[]) {
42 const names = await getConfigurationNames(realm, resourceNames);
43
44 // If flagged as prod, we do a
45 if (realm.confirmOnDelete) {
46 const answer = await prompt("Do you really want to delete? (YES to continue)");
47 if (answer !== "YES") {
48 console.log(`Operation not confirmed. Exiting (nothing done).`);
49 return;
50 }
51 }
52
53 for (let kName of names) {
54 const fileName = await renderRealmFile(realm, kName);
55 const args = ['delete', '-f', fileName];
56 addNamespaceIfDefined(realm, args);
57 try {
58 await spawn('kubectl', args);
59 } catch (ex) {
60 console.log(`Can't kdelete ${kName}, skipping`);
61 }
62 console.log();
63 }
64}
65
66let currentLogPodName: string | null = null;
67
68export async function klogs(realm: Realm, resourceNames?: string | string[]) {
69 const names = await getConfigurationNames(realm, resourceNames);
70 const pods = await fetchK8sObjectsByType(realm, 'pods');
71
72 for (let serviceName of names) {
73 const kpods = await getKPods(pods, { labels: { run: `${realm.system}-${serviceName}` } });
74
75 for (const kpod of kpods) {
76 const podName = kpod.name;
77 for (const ctn of kpod.containers) {
78 const ctnName = ctn.name;
79
80 const args = ['logs', '-f', podName, '-c', ctnName];
81 addNamespaceIfDefined(realm, args);
82 console.log(`Will output logs for -> kubectl ${args.join(' ')}`);
83 // Note: here we do not await, because, we want to be able to launch multiple at the same time, and not be blocking.
84 spawn('kubectl', args, {
85 detached: true,
86 onStdout: function (data) {
87 // If we had a log block before, and not the same as this one, we end it.
88 if (currentLogPodName != null && currentLogPodName !== podName) {
89 console.log('-----------\n');
90 }
91
92 // if the current log block is not this one, we put a new start
93 if (currentLogPodName !== podName) {
94 console.log(`----- LOG for: ${serviceName} / ${podName} / ${ctnName}`);
95 currentLogPodName = podName;
96 }
97
98 // print the info
99 process.stdout.write(data);
100 }
101 });
102 }
103
104 }
105 }
106}
107
108
109/**
110 * Will kubectl exec /service/restart.sh (will assume the pods/containers has one).
111 * Assumptions:
112 * - One container per pod.
113 * - Does not check if /service/restart.sh exists, so, will crash if not.
114 * @param runLabelPrefix
115 * @param serviceNamesStr
116 */
117export async function kshRestart(realm: Realm, serviceNamesStr: string) {
118 const serviceNames = asNames(serviceNamesStr);
119
120 const pods = await fetchK8sObjectsByType(realm, 'pods');
121
122 for (let serviceName of serviceNames) {
123 const podNames = await getPodNames(pods, { labels: { run: `${realm.system}-${serviceName}` } });
124
125 for (let podName of podNames) {
126
127 // TODO need to check if there is a /service/restart.sh
128 try {
129 const args = ['exec', podName];
130 addNamespaceIfDefined(realm, args);
131 args.push('--', 'test', '-e', '/service/restart.sh');
132 await spawn('kubectl', args, { toConsole: false });
133 } catch (ex) {
134 console.log(`Skipping service ${serviceName} - '/service/restart.sh' not found.`);
135 continue;
136 }
137 console.log(`\n--- Restarting: ${serviceName} (pod: ${podName})`);
138 const args = ['exec', podName];
139 addNamespaceIfDefined(realm, args);
140 args.push('--', '/service/restart.sh');
141 await spawn('kubectl', args);
142 console.log(`--- DONE: ${serviceName} : ${podName}`);
143 }
144 }
145 // TODO: Run: kubectl exec $(kubectl get pods -l run=halo-web-server --no-headers=true -o custom-columns=:metadata.name) -- /service/restart.sh
146 // TODO: needs to check if the service has restart.sh in the source tree, otherwise warn and skip the service
147}
148
149export async function kexec(realm: Realm, serviceNamesStr: string, commandAndArgs: string[]) {
150 const serviceNames = asNames(serviceNamesStr);
151 const pods = await fetchK8sObjectsByType(realm, 'pods');
152
153 for (let serviceName of serviceNames) {
154 const podNames = await getPodNames(pods, { labels: { run: `${realm.system}-${serviceName}` } });
155
156 for (let podName of podNames) {
157
158 try {
159 let args = ['exec', podName]
160 addNamespaceIfDefined(realm, args);
161
162 args.push('--'); // base arguments
163 args = args.concat(commandAndArgs); // we add the sub command and arguments
164 await spawn('kubectl', args); // for now, we have it in the toConsole, but should put it configurable
165 } catch (ex) {
166 console.log(`Cannot run ${commandAndArgs} on pod ${podName} because ${ex}`);
167 continue;
168 }
169 }
170 }
171}
172// --------- /Public create/delete/logs/restart --------- //
173
174// --------- Public get/set context --------- //
175// fetch the current context
176export async function getCurrentContext(): Promise<string | null> {
177 // kubectl config current-context
178 const psResult = await spawn('kubectl', ['config', 'current-context'], { capture: ['stdout', 'stderr'], ignoreFail: true });
179 if (psResult.stderr) {
180 // console.log(`INFO: ${psResult.stderr}`);
181 return null;
182 } else {
183 return psResult.stdout.toString().trim() as string;
184 }
185
186}
187
188export async function setCurrentContext(name: string) {
189 // TODO: perhaps when null, should unset it: 'kubectl config unset current-context'
190 // downside is that it can create some unwanted side effect, if the user has another context set
191 await spawn('kubectl', ['config', 'use-context', name]);
192}
193// --------- /Public get/set context --------- //
194
195
196// --------- Private Utils --------- //
197function addNamespaceIfDefined(realm: Realm, args: string[]) {
198 const namespace = realm.namespace;
199 if (namespace) {
200 args.push('--namespace', namespace);
201 }
202}
203
204// Fetch the pod for a part
205async function fetchK8sObjectsByType(realm: Realm, type: string) {
206 const args = ['get', type, '-o', 'json'];
207 addNamespaceIfDefined(realm, args);
208 const psResult = await spawn('kubectl', args, { capture: 'stdout' });
209 const podsJsonStr = psResult.stdout.toString();
210 const response = JSON.parse(podsJsonStr) as GetPodResponse;
211 return response.items || [];
212}
213
214
215// return the pod names
216function getPodNames(pods: PodItem[], filter?: { imageName?: string, labels?: { [key: string]: string } }) {
217 const kpods = getKPods(pods, filter);
218 return kpods.map(kpod => kpod.name);
219}
220
221/**
222 * Return the list of kpods for a give list of PodItems and a optional filter.
223 * @param pods PodItem array
224 * @param filter PodItemFilter
225 */
226function getKPods(pods: PodItem[], filter?: PodItemFilter): KPod[] {
227 const kpods: KPod[] = [];
228 for (let item of pods) {
229 let pass = true;
230
231 const itemName = item.metadata.name;
232
233 // test the filter.imageName
234 if (filter && filter.imageName) {
235 // Note: for now, just assume one container per pod, which should be the way to go anyway.
236 const itemImageName = (item.spec && item.spec.containers) ? item.spec.containers[0].image : null;
237 if (itemImageName != null && itemImageName.startsWith(filter.imageName)) {
238 pass = true;
239 } else {
240 // pass = false;
241 continue; // if false, we can stop this item now.
242 }
243 }
244
245 // test the filter.labels
246 if (filter && filter.labels) {
247 for (let labelName in filter.labels) {
248 if (item.metadata.labels[labelName] === filter.labels[labelName]) {
249 pass = true;
250 } else {
251 pass = false;
252 continue; // if false, we can stop now
253 }
254 }
255 }
256
257 //
258 if (pass) {
259 kpods.push(toKPod(item));
260 }
261 }
262
263 return kpods;
264}
265
266// --------- /Private Utils --------- //
\No newline at end of file