UNPKG

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