1 | import { glob, saferRemove } from 'backlib';
|
2 | import * as fs from 'fs-extra';
|
3 | import * as Path from 'path';
|
4 | import { asArray } from 'utils-min';
|
5 | import { BaseObj } from './base';
|
6 | import { Block } from './block';
|
7 | import { _getImageName } from './docker';
|
8 | import { callHook } from './hook';
|
9 | import { getCurrentContext, setCurrentContext } from './k8s';
|
10 | import { render } from './renderer';
|
11 | import { asNames } from './utils';
|
12 | import { loadVdevConfig } from './vdev-config';
|
13 |
|
14 |
|
15 |
|
16 | export type RealmType = 'local' | 'gcp' | 'aws' | 'azure';
|
17 | export interface Realm extends BaseObj {
|
18 |
|
19 |
|
20 | yamlDirs: string[];
|
21 |
|
22 | name: string;
|
23 |
|
24 |
|
25 | context: string | null;
|
26 |
|
27 | type: RealmType;
|
28 |
|
29 |
|
30 | project?: string;
|
31 |
|
32 | |
33 |
|
34 |
|
35 |
|
36 | registry: string;
|
37 |
|
38 |
|
39 | defaultConfigurations?: string[];
|
40 |
|
41 | [key: string]: any;
|
42 | }
|
43 |
|
44 | export type RealmByName = { [name: string]: Realm };
|
45 |
|
46 | export type RealmChange = { profileChanged?: boolean, contextChanged?: boolean };
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 | export async function setRealm(realm: Realm) {
|
56 | const currentRealm = await getCurrentRealm(false);
|
57 | const change: RealmChange = {};
|
58 |
|
59 |
|
60 |
|
61 |
|
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 |
|
81 |
|
82 | export 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 |
|
101 | if (!realm) {
|
102 | realm = Object.values(realms).find(r => r.context === null);
|
103 | }
|
104 |
|
105 | return realm;
|
106 | }
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 | export 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 |
|
126 | var data = realm;
|
127 | const outYamlContent = await render(srcYamlContent, data);
|
128 |
|
129 |
|
130 | await fs.ensureDir(realmOutDir);
|
131 | await fs.writeFile(outYamlFile, outYamlContent);
|
132 |
|
133 | return outYamlFile;
|
134 | }
|
135 |
|
136 |
|
137 |
|
138 | export 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 |
|
158 |
|
159 |
|
160 |
|
161 | export type TemplateRendered = { name: string, path: string };
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 | export 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 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 | export async function getConfigurationNames(realm: Realm, configurationNames?: string | string[]) {
|
186 |
|
187 |
|
188 |
|
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 |
|
205 |
|
206 |
|
207 |
|
208 | export function getRemoteImageName(block: Block, realm: Realm) {
|
209 | return _getImageName(block, realm.registry);
|
210 | }
|
211 |
|
212 |
|
213 | export 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 |
|
222 | export 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 |
|
232 | let _common = {};
|
233 | if (rawRealms._common) {
|
234 | _common = rawRealms._common;
|
235 | delete rawRealms._common;
|
236 | }
|
237 |
|
238 |
|
239 | for (let name in rawRealms) {
|
240 | const rawRealm = rawRealms[name];
|
241 |
|
242 |
|
243 | const realm = { ...base, ..._common, ...rawRealm };
|
244 |
|
245 |
|
246 |
|
247 | realm.imageTag = rawConfig.imageTag;
|
248 |
|
249 |
|
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 |
|
267 | if (!realm.registry) {
|
268 | if (type === 'local' && realm.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 |
|
278 | realm.name = name;
|
279 |
|
280 |
|
281 | realm.yamlDirs = realm.yamlDirs ?? asArray(realm.yamlDir);
|
282 |
|
283 |
|
284 |
|
285 | await callHook(realm, 'realm_init');
|
286 |
|
287 | realms[name] = realm;
|
288 | }
|
289 | return realms;
|
290 | }
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 | async 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 |
|
308 | function getRealmOutDir(realm: Realm) {
|
309 | return Path.join(realm.yamlDirs[0], '.out/', realm.name + '/');
|
310 | }
|
311 |
|
312 |
|
313 |
|
314 |
|
315 | async 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 |
|
327 | async function cleanRealmOutDir(realm: Realm) {
|
328 | await saferRemove(getRealmOutDir(realm));
|
329 | }
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|