UNPKG

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