UNPKG

10.3 kBPlain TextView Raw
1
2import { saferRemove, asNames } from './utils';
3import { getCurrentProject } from './gcloud';
4import { getCurrentContext, setCurrentContext } from './k8s';
5import { loadVdevConfig } from './vdev-config';
6
7import { render } from './renderer';
8
9import * as fs from 'fs-extra-plus';
10import * as Path from 'path';
11import { callHook } from './hook';
12import { realm_init } from './hook-aws';
13
14
15// --------- Public Types --------- //
16export type RealmType = 'local' | 'gcp' | 'aws' | 'azure';
17export interface Realm {
18
19 // This is the "app name" the global name for the application (containing multiple servives)
20 systemName: string;
21
22 name: string;
23
24 /** The Kubernetes context name (this is required) */
25 context: string | null;
26
27 type: RealmType;
28
29 /** The Google project name */
30 project?: string;
31
32 /**
33 * The for type 'local' it's localhost:5000/, for 'gcr.io/${realm.project}/' for aws, has to be set.
34 *
35 */
36 registry: string;
37
38 /** imageTag to be used (with the starting ':' (default to "latest") */
39 imageTag?: string;
40
41 /** list of default defaultConfigurations (k8s yaml file names without extension) to be used if "kcreate, ..." has not services description */
42 defaultConfigurations?: string[];
43
44 [key: string]: any;
45}
46
47export type RealmByName = { [name: string]: Realm };
48
49export type RealmChange = { profileChanged?: boolean, contextChanged?: boolean };
50// --------- /Public Types --------- //
51
52// --------- Public Realms APIs --------- //
53/**
54 * Set/Change the current (k8s context and eventual google project). Only change what is needed.
55 * @param {*} realm the realm object.
56 * @return {profileChanged?: true, contextChanged?: true} return a change object with what has changed.
57 */
58export async function setRealm(realm: Realm) {
59 const currentRealm = await getCurrentRealm(false);
60 const change: RealmChange = {};
61
62 // NOTE: When realm.project is undefined, it will set the gclougProject to undefined,
63 // which is fine since we want to avoid accidental operation to the project.
64 // FIXME: Needs to handle the case where realm.project is not defined (probably remove the current google project to make sure no side effect)
65 const hookReturn = await callHook(realm, 'realm_set_begin', currentRealm);
66
67 if (hookReturn != null) {
68 change.profileChanged = true;
69 }
70
71 if (realm.context === null) {
72 console.log(`INFO: realm ${realm.name} does not have a kubernetes context, skipping 'kubectl config use-context ...' (current kubectl context still active)`);
73 } else if (!currentRealm || currentRealm.context !== realm.context) {
74 change.contextChanged = true;
75 await setCurrentContext(realm.context);
76 }
77
78
79 return change;
80}
81
82/**
83 * Get the current Realm. Return undefined if not found
84 */
85export async function getCurrentRealm(check = true) {
86 const realms = await loadRealms();
87 const context = await getCurrentContext();
88
89 let realm;
90
91 for (let realmName in realms) {
92 let realmItem = realms[realmName];
93 if (realmItem.context === context) {
94 realm = realmItem;
95 break;
96 }
97 }
98
99 if (check && realm) {
100 await callHook(realm, 'realm_check');
101 }
102
103 // if no context matching, try get the first realm that has no context
104 if (!realm) {
105 realm = Object.values(realms).find(r => r.context === null);
106 }
107
108 return realm;
109}
110
111
112
113/**
114 * Render realm yaml file.
115 * @param realm
116 * @param name name of the yaml file (without extension)
117 * @return The yaml file path
118 */
119export async function renderRealmFile(realm: Realm, name: string): Promise<string> {
120 const realmOutDir = getRealmOutDir(realm);
121
122 const srcYamlFile = getKFile(realm, name);
123 const srcYamlFileName = Path.parse(srcYamlFile).base;
124 const srcYamlContent = await fs.readFile(srcYamlFile, 'utf8');
125
126 const outYamlFile = Path.join(realmOutDir, srcYamlFileName);
127
128 // render the content
129 var data = realm;
130 const outYamlContent = await render(srcYamlContent, data);
131
132 // for now, we do not generate do any template
133 await fs.ensureDir(realmOutDir);
134 await fs.writeFile(outYamlFile, outYamlContent);
135
136 return outYamlFile;
137}
138
139
140
141export function formatAsTable(realms: RealmByName, currentRealm?: Realm | null) {
142 const txts = [];
143 const header = ' ' + 'REALM'.padEnd(20) + 'TYPE'.padEnd(12) + 'PROJECT/PROFILE'.padEnd(20) + 'CONTEXT';
144 txts.push(header);
145
146 const currentRealmName = (currentRealm) ? currentRealm.name : null;
147 const currentProject = (currentRealm) ? currentRealm.project : null;
148 for (let realm of Object.values(realms)) {
149 let row = (realm.name === currentRealmName) ? "* " : " ";
150 row += realm.name.padEnd(20);
151 row += realm.type.padEnd(12);
152 let profile = (realm.type === 'gcp') ? realm.project : realm.profile;
153 profile = (profile == null) ? '' : profile;
154 row += profile.padEnd(20);
155 row += (realm.context ? realm.context : 'NO CONTEXT FOUND');
156 txts.push(row);
157 }
158 return txts.join('\n');
159}
160// --------- /Public Realms APIs --------- //
161
162
163// --------- Resource Management --------- //
164export type TemplateRendered = { name: string, path: string };
165
166/**
167 * Templatize a set of yaml files
168 * @param resourceNames either a single name, comma deliminated set of names, or an array.
169 */
170export async function templatize(realm: Realm, resourceNames?: string | string[]): Promise<TemplateRendered[]> {
171 const names = await getConfigurationNames(realm, resourceNames);
172 const result: TemplateRendered[] = [];
173
174 for (let name of names) {
175 const path = await renderRealmFile(realm, name);
176 result.push({ name, path });
177 }
178 return result;
179}
180// --------- /Resource Management --------- //
181
182/**
183 * Returns a list of k8s configuration file names for a given realm and optional configurations names delimited string or array.
184 * - If configurationNames is an array, then, just return as is.
185 * - If configurationNames is string (e.g., 'web-server, queue') then it will split on ',' and trim each item as returns.
186 * - If no resourceNames then returns all of the resourceNames for the realm.
187 */
188export async function getConfigurationNames(realm: Realm, configurationNames?: string | string[]) {
189
190 // Note: for now, we do the check if the realm has a context here, because this is called for each k*** commands
191 // TODO: Might want to make it more explicit, as validateRealmForKubectlCommand(realm)
192 if (realm.context === null) {
193 throw Error(`Realm '${realm.name}' does not have a Kubernetes context, cannot perform kubectly commands.`);
194 }
195
196 if (configurationNames) {
197 return asNames(configurationNames);
198 } else if (realm.defaultConfigurations) {
199 return realm.defaultConfigurations;
200 } else {
201 return getAllConfigurationNames(realm);
202 }
203}
204
205export function getLocalImageName(realm: Realm, serviceName: string) {
206 return _getImageName(realm, 'localhost:5000/', serviceName);
207}
208
209/**
210 * Note: right now assume remote is on gke cloud (gcr.io/)
211 * @param realm
212 * @param serviceName
213 */
214export function getRemoteImageName(realm: Realm, serviceName: string) {
215 return _getImageName(realm, realm.registry, serviceName);
216}
217
218function _getImageName(realm: Realm, basePath: string, serviceName: string) {
219 const tag = (realm.imageTag) ? realm.imageTag : 'latest';
220 const repoName = getRepositoryName(realm, serviceName);
221 return `${basePath}${repoName}:${tag}`;
222}
223
224export function getRepositoryName(realm: Realm, serviceName: string) {
225 return `${realm.system}-${serviceName}`;
226}
227
228export function assertRealm(realm?: Realm): Realm {
229 if (!realm) {
230 throw new Error(`No realm found, do a 'npm run realm' to see the list of realm, and 'npm run realm realm_name' to set a realm`);
231 }
232 return realm;
233}
234
235
236// --------- Loader --------- //
237export async function loadRealms(): Promise<RealmByName> {
238 const rawConfig = await loadVdevConfig();
239
240 const rawRealms: { [name: string]: any } = rawConfig.realms;
241 const realms: RealmByName = {};
242
243 const base = {
244 system: rawConfig.system,
245 k8sDir: rawConfig.k8sDir
246 }
247 // get the _common variables and delete it from the realm list
248 let _common = {};
249 if (rawRealms._common) {
250 _common = rawRealms._common;
251 delete rawRealms._common;
252 }
253
254 // Create the realm object
255 for (let name in rawRealms) {
256 const rawRealm = rawRealms[name];
257
258 // TODO: must do a deep merge
259 const realm = { ...base, ..._common, ...rawRealm };
260
261 //// determine the type
262 let type: RealmType = 'local';
263 const context: undefined | string = realm.context;
264 if (context) {
265 if (context.startsWith('arn:aws')) {
266 type = 'aws';
267 realm.profile = (realm.profile != null) ? realm.profile : 'default';
268 } else if (context.startsWith('gke')) {
269 type = 'gcp';
270 } else if (realm.registry && realm.registry.includes('azurecr')) {
271 type = 'azure';
272 }
273 } else {
274 realm.context = null;
275 }
276 realm.type = type;
277
278 //// determine registry
279 if (!realm.registry) {
280 if (type === 'local' && realm.context) { // do not create localhost registry for local realm without context
281 realm.registry = 'localhost:5000/';
282 } else if (type === 'gcp') {
283 realm.registry = `gcr.io/${realm.project}/`;
284 } else if (type === 'aws') {
285 console.log(`WARNING - realm ${realm.name} of type 'aws' must have a registry property in the vdev.yaml`);
286 }
287 }
288
289 // set the name
290 realm.name = name;
291
292 // Call hook to finiish initializing the realm (i.e., realm type specific initialization)
293 await callHook(realm, 'realm_init');
294
295 realms[name] = realm;
296 }
297 return realms;
298}
299// --------- /Loader --------- //
300
301
302// --------- Private Helpers --------- //
303/** Get all of the resourceNames for a given realm */
304async function getAllConfigurationNames(realm: Realm): Promise<string[]> {
305 const dir = getRealmSrcDir(realm);
306 const yamlFiles = await fs.glob('*.yaml', dir);
307
308 // return the list of names only
309 if (yamlFiles) {
310 return yamlFiles.map((f: string) => { return Path.basename(f, '.yaml') });
311 } else {
312 return []; // return empty list if nothing found
313 }
314}
315
316function getRealmOutDir(realm: Realm) {
317 return Path.join(realm.k8sDir, '~out/', realm.name + '/');
318}
319
320function getRealmSrcDir(realm: Realm) {
321 return Path.join(realm.k8sDir, realm.yamlDir);
322}
323
324// get the Original Kubernets Yaml file (which could be a template)
325function getKFile(realm: Realm, kName: string) {
326 let k8sDir = getRealmSrcDir(realm);
327 return Path.join(k8sDir, `${kName.trim()}.yaml`);
328}
329
330async function cleanRealmOutDir(realm: Realm) {
331 await saferRemove(getRealmOutDir(realm));
332}
333// --------- /Private Helpers --------- //
334
335
336
337
338
339
340
341
342