UNPKG

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