UNPKG

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