UNPKG

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