@agilecustoms/envctl 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/Cli.js +16 -19
- package/dist/client/EnvApiClient.js +3 -12
- package/dist/client/TerraformAdapter.js +68 -64
- package/dist/commands/delete.js +3 -1
- package/dist/commands/deploy.js +3 -1
- package/dist/commands/destroy.js +3 -1
- package/dist/commands/logs.js +3 -1
- package/dist/commands/plan.js +3 -1
- package/dist/commands/status.js +3 -1
- package/dist/service/BaseService.js +2 -2
- package/dist/service/EnvService.js +57 -63
- package/dist/service/LocalStateService.js +7 -8
- package/dist/service/LogService.js +5 -13
- package/package.json +1 -1
package/dist/client/Cli.js
CHANGED
|
@@ -1,29 +1,19 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { Readable } from 'node:stream';
|
|
7
|
+
import { pipeline } from 'node:stream/promises';
|
|
2
8
|
import path from 'path';
|
|
3
9
|
import * as readline from 'readline';
|
|
4
10
|
import inquirer from 'inquirer';
|
|
5
11
|
import { ProcessException } from '../exceptions.js';
|
|
6
|
-
function getDirName(cwd) {
|
|
7
|
-
cwd = resolveCwd(cwd);
|
|
8
|
-
return path.basename(cwd);
|
|
9
|
-
}
|
|
10
|
-
function resolveCwd(cwd) {
|
|
11
|
-
if (!cwd) {
|
|
12
|
-
return process.cwd();
|
|
13
|
-
}
|
|
14
|
-
if (path.isAbsolute(cwd)) {
|
|
15
|
-
return cwd;
|
|
16
|
-
}
|
|
17
|
-
return path.resolve(process.cwd(), cwd);
|
|
18
|
-
}
|
|
19
12
|
export class Cli {
|
|
20
|
-
async run(command, args,
|
|
13
|
+
async run(command, args, out_scanner, in_scanner) {
|
|
21
14
|
const spawnOptions = {
|
|
22
15
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
23
16
|
};
|
|
24
|
-
if (cwd) {
|
|
25
|
-
spawnOptions.cwd = cwd;
|
|
26
|
-
}
|
|
27
17
|
const child = spawn(command, args, spawnOptions);
|
|
28
18
|
child.stdout.setEncoding('utf8');
|
|
29
19
|
child.stderr.setEncoding('utf8');
|
|
@@ -92,7 +82,14 @@ export class Cli {
|
|
|
92
82
|
}]);
|
|
93
83
|
return answer;
|
|
94
84
|
}
|
|
95
|
-
getKind(
|
|
96
|
-
return
|
|
85
|
+
getKind() {
|
|
86
|
+
return path.basename(process.cwd());
|
|
87
|
+
}
|
|
88
|
+
async writeInTmpFile(stream, fileName) {
|
|
89
|
+
const outDir = join(tmpdir(), 'envctl');
|
|
90
|
+
await mkdir(outDir, { recursive: true });
|
|
91
|
+
const outPath = join(outDir, fileName);
|
|
92
|
+
await pipeline(Readable.fromWeb(stream), createWriteStream(outPath, { flags: 'w' }));
|
|
93
|
+
return outPath;
|
|
97
94
|
}
|
|
98
95
|
}
|
|
@@ -6,6 +6,9 @@ export class EnvApiClient {
|
|
|
6
6
|
constructor(httpClient) {
|
|
7
7
|
this.httpClient = httpClient;
|
|
8
8
|
}
|
|
9
|
+
async fetch(url) {
|
|
10
|
+
return await fetch(url);
|
|
11
|
+
}
|
|
9
12
|
async get(key) {
|
|
10
13
|
try {
|
|
11
14
|
return await this.httpClient.fetch(`/ci/env/${key}`);
|
|
@@ -43,18 +46,6 @@ export class EnvApiClient {
|
|
|
43
46
|
}
|
|
44
47
|
return { ...env, ephemeral: false, status: EnvStatus.Deploying, ttl: result.ttl };
|
|
45
48
|
}
|
|
46
|
-
async setVars(env, vars) {
|
|
47
|
-
try {
|
|
48
|
-
await this.httpClient.post(`/ci/env/${env.key}/vars`, vars);
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
if (error instanceof BusinessException) {
|
|
52
|
-
throw new KnownException(`Failed to set vars for environment ${env.key}: ${error.message}`);
|
|
53
|
-
}
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
env.vars = { ...env.vars || {}, ...vars };
|
|
57
|
-
}
|
|
58
49
|
async activate(env) {
|
|
59
50
|
try {
|
|
60
51
|
await this.httpClient.post(`/ci/env/${env.key}/activate`);
|
|
@@ -26,9 +26,8 @@ export class TerraformAdapter {
|
|
|
26
26
|
return acc;
|
|
27
27
|
}, new Map());
|
|
28
28
|
}
|
|
29
|
-
getLockFile(
|
|
30
|
-
const
|
|
31
|
-
const lockPath = path.join(dir, '.terraform.lock.hcl');
|
|
29
|
+
getLockFile() {
|
|
30
|
+
const lockPath = path.join(process.cwd(), '.terraform.lock.hcl');
|
|
32
31
|
if (!fs.existsSync(lockPath)) {
|
|
33
32
|
throw new KnownException(`Terraform lock file not found at: ${lockPath}`);
|
|
34
33
|
}
|
|
@@ -39,12 +38,11 @@ export class TerraformAdapter {
|
|
|
39
38
|
throw new KnownException(`Failed to read terraform lock file: ${lockPath}`, { cause: err });
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
|
-
getTerraformBackend(
|
|
41
|
+
getTerraformBackend() {
|
|
43
42
|
if (this.backend) {
|
|
44
43
|
return this.backend;
|
|
45
44
|
}
|
|
46
|
-
const
|
|
47
|
-
const statePath = path.join(dir, '.terraform', 'terraform.tfstate');
|
|
45
|
+
const statePath = path.join(process.cwd(), '.terraform', 'terraform.tfstate');
|
|
48
46
|
if (!fs.existsSync(statePath)) {
|
|
49
47
|
throw new KnownException(`Terraform state file not found at: ${statePath}`);
|
|
50
48
|
}
|
|
@@ -73,23 +71,23 @@ export class TerraformAdapter {
|
|
|
73
71
|
this.backend = backend;
|
|
74
72
|
return backend;
|
|
75
73
|
}
|
|
76
|
-
async lock(newEnv
|
|
74
|
+
async lock(newEnv) {
|
|
77
75
|
const res = {};
|
|
78
|
-
const stateFileContent = this.getTerraformBackend(
|
|
76
|
+
const stateFileContent = this.getTerraformBackend().validateAndGetStateFileContent();
|
|
79
77
|
const stateHash = hash(stateFileContent);
|
|
80
|
-
const config = this.localStateService.load(
|
|
78
|
+
const config = this.localStateService.load();
|
|
81
79
|
if (config.stateHash !== stateHash || newEnv) {
|
|
82
80
|
config.stateHash = stateHash;
|
|
83
81
|
res.stateFile = stateFileContent;
|
|
84
82
|
}
|
|
85
83
|
const linuxArm64 = process.platform === 'linux' && process.arch === 'arm64';
|
|
86
84
|
if (!linuxArm64 || newEnv) {
|
|
87
|
-
let lockFileContent = this.getLockFile(
|
|
85
|
+
let lockFileContent = this.getLockFile();
|
|
88
86
|
if (!linuxArm64) {
|
|
89
87
|
const lockFileHash = hash(lockFileContent);
|
|
90
88
|
if (config.lockHash !== lockFileHash) {
|
|
91
|
-
await this.lockProviders(
|
|
92
|
-
lockFileContent = this.getLockFile(
|
|
89
|
+
await this.lockProviders();
|
|
90
|
+
lockFileContent = this.getLockFile();
|
|
93
91
|
config.lockHash = hash(lockFileContent);
|
|
94
92
|
res.lockFile = lockFileContent;
|
|
95
93
|
}
|
|
@@ -98,12 +96,12 @@ export class TerraformAdapter {
|
|
|
98
96
|
res.lockFile = lockFileContent;
|
|
99
97
|
}
|
|
100
98
|
}
|
|
101
|
-
this.localStateService.save(config
|
|
99
|
+
this.localStateService.save(config);
|
|
102
100
|
return res;
|
|
103
101
|
}
|
|
104
|
-
async lockProviders(
|
|
102
|
+
async lockProviders() {
|
|
105
103
|
console.log('Update lock file');
|
|
106
|
-
await this.cli.run('terraform', ['providers', 'lock', '-platform=linux_arm64']
|
|
104
|
+
await this.cli.run('terraform', ['providers', 'lock', '-platform=linux_arm64']);
|
|
107
105
|
}
|
|
108
106
|
printTime() {
|
|
109
107
|
const now = new Date();
|
|
@@ -113,9 +111,9 @@ export class TerraformAdapter {
|
|
|
113
111
|
});
|
|
114
112
|
console.log(`\nTime EDT: ${edt}, UTC: ${utc}\n`);
|
|
115
113
|
}
|
|
116
|
-
tfArgs(env,
|
|
117
|
-
const varDefs = this.getVarDefs(
|
|
118
|
-
const argVars = this.getArgVars(
|
|
114
|
+
tfArgs(env, args, vars = []) {
|
|
115
|
+
const varDefs = this.getVarDefs();
|
|
116
|
+
const argVars = this.getArgVars(args);
|
|
119
117
|
const specialFields = { ...env, env: env.key };
|
|
120
118
|
const extraArgs = [];
|
|
121
119
|
for (const name of ['env', 'owner', 'ephemeral']) {
|
|
@@ -123,15 +121,15 @@ export class TerraformAdapter {
|
|
|
123
121
|
extraArgs.push('-var=' + name + '=' + specialFields[name]);
|
|
124
122
|
}
|
|
125
123
|
}
|
|
126
|
-
Object.entries(
|
|
124
|
+
Object.entries(vars || {}).forEach(([key, value]) => {
|
|
127
125
|
if (varDefs[key] && !argVars[key]) {
|
|
128
126
|
extraArgs.push('-var=' + key + '=' + value);
|
|
129
127
|
}
|
|
130
128
|
});
|
|
131
|
-
return [...extraArgs, ...
|
|
129
|
+
return [...extraArgs, ...args];
|
|
132
130
|
}
|
|
133
|
-
getVarDefs(
|
|
134
|
-
const dir =
|
|
131
|
+
getVarDefs() {
|
|
132
|
+
const dir = process.cwd();
|
|
135
133
|
const tfFiles = fs.readdirSync(dir)
|
|
136
134
|
.filter(file => file.endsWith('.tf'))
|
|
137
135
|
.map(file => path.join(dir, file));
|
|
@@ -170,10 +168,9 @@ export class TerraformAdapter {
|
|
|
170
168
|
});
|
|
171
169
|
return result;
|
|
172
170
|
}
|
|
173
|
-
getNewVars(
|
|
174
|
-
const varDefs = this.getVarDefs(
|
|
175
|
-
const argVars = this.getArgVars(
|
|
176
|
-
const envVars = envTerraform.vars;
|
|
171
|
+
getNewVars(args, envVars, onDemandVars) {
|
|
172
|
+
const varDefs = this.getVarDefs();
|
|
173
|
+
const argVars = this.getArgVars(args);
|
|
177
174
|
const vars = { ...argVars, ...onDemandVars };
|
|
178
175
|
const newVars = {};
|
|
179
176
|
Object.entries(vars).forEach(([key, value]) => {
|
|
@@ -190,30 +187,13 @@ export class TerraformAdapter {
|
|
|
190
187
|
});
|
|
191
188
|
return newVars;
|
|
192
189
|
}
|
|
193
|
-
async plan(env,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
await this.cli.run('terraform', ['plan', ...args], cwd);
|
|
198
|
-
}
|
|
199
|
-
catch (error) {
|
|
200
|
-
if (!(error instanceof ProcessException)) {
|
|
201
|
-
throw error;
|
|
202
|
-
}
|
|
203
|
-
if (attemptNo < MAX_ATTEMPTS && RETRYABLE_ERRORS.some(err => error.message.includes(err))) {
|
|
204
|
-
console.warn(`Retrying terraform plan due to error: ${error.message}`);
|
|
205
|
-
return this.plan(env, cwd, attemptNo + 1);
|
|
206
|
-
}
|
|
207
|
-
const lockId = this.lockId(error, attemptNo);
|
|
208
|
-
if (lockId) {
|
|
209
|
-
await this.promptUnlock(lockId, cwd);
|
|
210
|
-
console.info('State unlocked, retrying terraform plan');
|
|
211
|
-
return this.plan(env, cwd, attemptNo + 1);
|
|
212
|
-
}
|
|
213
|
-
throw new KnownException(`terraform plan failed with code ${error.code}:\n${error.message}`, { cause: error });
|
|
190
|
+
async plan(env, args, onDemandVars = {}) {
|
|
191
|
+
if (env) {
|
|
192
|
+
args = this.tfArgs(env, args);
|
|
214
193
|
}
|
|
194
|
+
await this._plan(args, onDemandVars);
|
|
215
195
|
}
|
|
216
|
-
async
|
|
196
|
+
async _plan(args, onDemandVars, attemptNo = 1) {
|
|
217
197
|
let inputTfVariable = false;
|
|
218
198
|
let tfVarName = '';
|
|
219
199
|
function out_scanner(line) {
|
|
@@ -232,11 +212,32 @@ export class TerraformAdapter {
|
|
|
232
212
|
tfVarName = '';
|
|
233
213
|
}
|
|
234
214
|
}
|
|
235
|
-
|
|
236
|
-
|
|
215
|
+
console.log('Running: terraform plan -auto-approve', ...args, '\n');
|
|
216
|
+
try {
|
|
217
|
+
await this.cli.run('terraform', ['plan', ...args], out_scanner, in_scanner);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (!(error instanceof ProcessException)) {
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
if (attemptNo < MAX_ATTEMPTS && RETRYABLE_ERRORS.some(err => error.message.includes(err))) {
|
|
224
|
+
console.warn(`Retrying terraform plan due to error: ${error.message}`);
|
|
225
|
+
return this._plan(args, onDemandVars, attemptNo + 1);
|
|
226
|
+
}
|
|
227
|
+
const lockId = this.lockId(error, attemptNo);
|
|
228
|
+
if (lockId) {
|
|
229
|
+
await this.promptUnlock(lockId);
|
|
230
|
+
console.info('State unlocked, retrying terraform plan');
|
|
231
|
+
return this._plan(args, onDemandVars, attemptNo + 1);
|
|
232
|
+
}
|
|
233
|
+
throw new KnownException(`terraform plan failed with code ${error.code}:\n${error.message}`, { cause: error });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async apply(args, attemptNo = 1) {
|
|
237
|
+
console.log('Running: terraform apply ', ...args, '\n');
|
|
237
238
|
this.printTime();
|
|
238
239
|
try {
|
|
239
|
-
await this.cli.run('terraform', ['apply',
|
|
240
|
+
await this.cli.run('terraform', ['apply', ...args]);
|
|
240
241
|
this.printTime();
|
|
241
242
|
}
|
|
242
243
|
catch (error) {
|
|
@@ -245,28 +246,31 @@ export class TerraformAdapter {
|
|
|
245
246
|
}
|
|
246
247
|
if (attemptNo < MAX_ATTEMPTS && RETRYABLE_ERRORS.some(err => error.message.includes(err))) {
|
|
247
248
|
console.warn(`Retrying terraform apply due to error: ${error.message}`);
|
|
248
|
-
return this.apply(
|
|
249
|
+
return this.apply(args, attemptNo + 1);
|
|
249
250
|
}
|
|
250
251
|
const lockId = this.lockId(error, attemptNo);
|
|
251
252
|
if (lockId) {
|
|
252
|
-
await this.promptUnlock(lockId
|
|
253
|
+
await this.promptUnlock(lockId);
|
|
253
254
|
console.info('State unlocked, retrying terraform apply');
|
|
254
|
-
return this.apply(
|
|
255
|
+
return this.apply(args, attemptNo + 1);
|
|
255
256
|
}
|
|
256
257
|
throw new KnownException(`terraform apply failed with code ${error.code}:\n${error.message}`, { cause: error });
|
|
257
258
|
}
|
|
258
259
|
}
|
|
259
|
-
async destroy(env,
|
|
260
|
+
async destroy(env, args, force) {
|
|
261
|
+
args = this.tfArgs(env, args);
|
|
262
|
+
await this._destroy(args, force);
|
|
263
|
+
}
|
|
264
|
+
async _destroy(args, force, attemptNo = 1) {
|
|
260
265
|
let wrongDir = false;
|
|
261
266
|
const scanner = (line) => {
|
|
262
267
|
if (line.includes('Either you have not created any objects yet or the existing objects were')) {
|
|
263
268
|
wrongDir = true;
|
|
264
269
|
}
|
|
265
270
|
};
|
|
266
|
-
const args = this.tfArgs(env, cwd);
|
|
267
271
|
console.log('Running: terraform destroy -auto-approve', ...args, '\n');
|
|
268
272
|
try {
|
|
269
|
-
await this.cli.run('terraform', ['destroy', '-auto-approve', ...args],
|
|
273
|
+
await this.cli.run('terraform', ['destroy', '-auto-approve', ...args], scanner);
|
|
270
274
|
}
|
|
271
275
|
catch (error) {
|
|
272
276
|
if (!(error instanceof ProcessException)) {
|
|
@@ -275,13 +279,13 @@ export class TerraformAdapter {
|
|
|
275
279
|
const lockId = this.lockId(error, attemptNo);
|
|
276
280
|
if (lockId) {
|
|
277
281
|
if (force) {
|
|
278
|
-
await this.forceUnlock(lockId
|
|
282
|
+
await this.forceUnlock(lockId);
|
|
279
283
|
}
|
|
280
284
|
else {
|
|
281
|
-
await this.promptUnlock(lockId
|
|
285
|
+
await this.promptUnlock(lockId);
|
|
282
286
|
}
|
|
283
287
|
console.info('State unlocked, retrying terraform destroy');
|
|
284
|
-
return this.
|
|
288
|
+
return this._destroy(args, force, attemptNo + 1);
|
|
285
289
|
}
|
|
286
290
|
throw new KnownException(`terraform destroy failed with code ${error.code}:\n${error.message}`, { cause: error });
|
|
287
291
|
}
|
|
@@ -298,17 +302,17 @@ export class TerraformAdapter {
|
|
|
298
302
|
}
|
|
299
303
|
}
|
|
300
304
|
}
|
|
301
|
-
async promptUnlock(id
|
|
305
|
+
async promptUnlock(id) {
|
|
302
306
|
const answerYes = await this.cli.promptYesNo('Terraform state is locked. Most often due to previously interrupted process.\n'
|
|
303
307
|
+ 'Rarely (main lock intention) is when another process actively work with the state to avoid inconsistent modification.\n'
|
|
304
308
|
+ 'Do you want to force unlock?: ');
|
|
305
309
|
if (!answerYes) {
|
|
306
310
|
throw new AbortedException();
|
|
307
311
|
}
|
|
308
|
-
await this.forceUnlock(id
|
|
312
|
+
await this.forceUnlock(id);
|
|
309
313
|
}
|
|
310
|
-
async forceUnlock(id
|
|
314
|
+
async forceUnlock(id) {
|
|
311
315
|
console.log('Force unlocking state');
|
|
312
|
-
await this.cli.run('terraform', ['force-unlock', '-force', id]
|
|
316
|
+
await this.cli.run('terraform', ['force-unlock', '-force', id]);
|
|
313
317
|
}
|
|
314
318
|
}
|
package/dist/commands/delete.js
CHANGED
package/dist/commands/deploy.js
CHANGED
package/dist/commands/destroy.js
CHANGED
|
@@ -17,5 +17,7 @@ export function destroy(program) {
|
|
|
17
17
|
}
|
|
18
18
|
async function handler(tfArgs, options) {
|
|
19
19
|
const { force, cwd } = options;
|
|
20
|
-
|
|
20
|
+
if (cwd)
|
|
21
|
+
process.chdir(cwd);
|
|
22
|
+
await envService.destroy(tfArgs, Boolean(force));
|
|
21
23
|
}
|
package/dist/commands/logs.js
CHANGED
package/dist/commands/plan.js
CHANGED
package/dist/commands/status.js
CHANGED
|
@@ -4,10 +4,10 @@ export class BaseService {
|
|
|
4
4
|
constructor(terraformAdapter) {
|
|
5
5
|
this.terraformAdapter = terraformAdapter;
|
|
6
6
|
}
|
|
7
|
-
getKey(key
|
|
7
|
+
getKey(key) {
|
|
8
8
|
if (!key) {
|
|
9
9
|
console.log('Key is not provided, inferring from state file');
|
|
10
|
-
key = this.terraformAdapter.getTerraformBackend(
|
|
10
|
+
key = this.terraformAdapter.getTerraformBackend().getKey();
|
|
11
11
|
}
|
|
12
12
|
return key;
|
|
13
13
|
}
|
|
@@ -11,8 +11,8 @@ export class EnvService extends BaseService {
|
|
|
11
11
|
this.cli = cli;
|
|
12
12
|
this.envApi = envApi;
|
|
13
13
|
}
|
|
14
|
-
async status(key
|
|
15
|
-
key = this.getKey(key
|
|
14
|
+
async status(key) {
|
|
15
|
+
key = this.getKey(key);
|
|
16
16
|
console.log(`Retrieve env ${key}`);
|
|
17
17
|
const env = await this.envApi.get(key);
|
|
18
18
|
if (env === null) {
|
|
@@ -33,7 +33,7 @@ export class EnvService extends BaseService {
|
|
|
33
33
|
console.log(`Expires at ${formattedDate}`);
|
|
34
34
|
}
|
|
35
35
|
if (env.kind) {
|
|
36
|
-
const kind = this.cli.getKind(
|
|
36
|
+
const kind = this.cli.getKind();
|
|
37
37
|
if (env.kind !== kind) {
|
|
38
38
|
console.warn(`Env ${key} kind (dir-name): ${env.kind} - looks like this env was deployed from a different directory`);
|
|
39
39
|
}
|
|
@@ -50,37 +50,34 @@ export class EnvService extends BaseService {
|
|
|
50
50
|
}
|
|
51
51
|
return env;
|
|
52
52
|
}
|
|
53
|
-
checkKind(env
|
|
53
|
+
checkKind(env) {
|
|
54
54
|
if (env.ephemeral)
|
|
55
55
|
return;
|
|
56
56
|
if (env.kind) {
|
|
57
|
-
const kind = this.cli.getKind(
|
|
57
|
+
const kind = this.cli.getKind();
|
|
58
58
|
if (kind !== env.kind) {
|
|
59
59
|
throw new KnownException(`Env ${env.key} kind (dir-name): ${env.kind} - make sure you run this command from the same directory`);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
async plan(tfArgs
|
|
64
|
-
const key = this.terraformAdapter.getTerraformBackend(
|
|
63
|
+
async plan(tfArgs) {
|
|
64
|
+
const key = this.terraformAdapter.getTerraformBackend().getKey();
|
|
65
65
|
const env = await this.tryGetEnv(key);
|
|
66
|
-
let vars = undefined;
|
|
67
66
|
if (env) {
|
|
68
|
-
this.checkKind(env
|
|
67
|
+
this.checkKind(env);
|
|
69
68
|
if (env.status === EnvStatus.Deleting) {
|
|
70
69
|
throw new KnownException(`Env ${env.key} status is DELETING, please wait`);
|
|
71
70
|
}
|
|
72
|
-
vars = env.vars;
|
|
73
71
|
}
|
|
74
|
-
|
|
75
|
-
await this.terraformAdapter.plan(envTerraform, cwd);
|
|
72
|
+
await this.terraformAdapter.plan(env, tfArgs);
|
|
76
73
|
}
|
|
77
74
|
async createEphemeral(key) {
|
|
78
75
|
const createEnv = { key };
|
|
79
76
|
return await this.envApi.createEphemeral(createEnv);
|
|
80
77
|
}
|
|
81
|
-
async lockTerraform(env,
|
|
82
|
-
console.log('
|
|
83
|
-
const res = await this.terraformAdapter.lock(newEnv
|
|
78
|
+
async lockTerraform(env, newEnv = false) {
|
|
79
|
+
console.log('Lock providers');
|
|
80
|
+
const res = await this.terraformAdapter.lock(newEnv);
|
|
84
81
|
if (res.lockFile) {
|
|
85
82
|
env.lockFile = res.lockFile;
|
|
86
83
|
}
|
|
@@ -88,68 +85,67 @@ export class EnvService extends BaseService {
|
|
|
88
85
|
env.stateFile = res.stateFile;
|
|
89
86
|
}
|
|
90
87
|
}
|
|
91
|
-
async
|
|
92
|
-
|
|
88
|
+
async ensurePlan(env, args) {
|
|
89
|
+
if (args.length > 0 && !args[0].startsWith('-')) {
|
|
90
|
+
return args;
|
|
91
|
+
}
|
|
92
|
+
const planFile = '.terraform/envctl.plan';
|
|
93
|
+
await this.terraformAdapter.plan(env, [`-out=${planFile}`, ...args]);
|
|
94
|
+
return [planFile, ...args].filter(arg => !arg.startsWith('-var'));
|
|
95
|
+
}
|
|
96
|
+
async deploy(tfArgs) {
|
|
97
|
+
const key = this.terraformAdapter.getTerraformBackend().getKey();
|
|
93
98
|
let env = await this.tryGetEnv(key);
|
|
94
99
|
if (env === null) {
|
|
95
|
-
const kind = this.cli.getKind(
|
|
100
|
+
const kind = this.cli.getKind();
|
|
96
101
|
console.log(`Inferred kind from directory name: ${kind}`);
|
|
97
102
|
const createEnv = { key, kind };
|
|
98
|
-
await this.lockTerraform(createEnv,
|
|
103
|
+
await this.lockTerraform(createEnv, true);
|
|
104
|
+
tfArgs = await this.ensurePlan(env, tfArgs);
|
|
99
105
|
console.log('Creating env metadata');
|
|
100
106
|
env = await this.envApi.create(createEnv);
|
|
101
|
-
await this.runDeploy(env,
|
|
107
|
+
await this.runDeploy(env, tfArgs);
|
|
102
108
|
return;
|
|
103
109
|
}
|
|
104
|
-
this.checkKind(env
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
await this.lockTerraform(env, cwd);
|
|
112
|
-
await this.envApi.lockForUpdate(env);
|
|
113
|
-
break;
|
|
114
|
-
case EnvStatus.Deploying:
|
|
115
|
-
case EnvStatus.Updating:
|
|
116
|
-
const answerYes = await this.cli.promptYesNo(`Env status is ${env.status}, likely due to an error from a previous run\n
|
|
110
|
+
this.checkKind(env);
|
|
111
|
+
const status = env.status;
|
|
112
|
+
if (status === EnvStatus.Deleting) {
|
|
113
|
+
throw new KnownException(`Env ${env.key} status is DELETING, please wait`);
|
|
114
|
+
}
|
|
115
|
+
if (status === EnvStatus.Deploying || status === EnvStatus.Updating) {
|
|
116
|
+
const answerYes = await this.cli.promptYesNo(`Env status is ${status}, likely due to an error from a previous run\n
|
|
117
117
|
Do you want to proceed with deployment?`);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
break;
|
|
123
|
-
case EnvStatus.Deleting:
|
|
124
|
-
throw new KnownException(`Env ${env.key} status is DELETING, please wait`);
|
|
118
|
+
if (!answerYes) {
|
|
119
|
+
console.log('Aborting deployment');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
125
122
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const onDemandVars = {};
|
|
133
|
-
try {
|
|
134
|
-
await this.terraformAdapter.apply(envTerraform, onDemandVars, cwd);
|
|
135
|
-
}
|
|
136
|
-
finally {
|
|
137
|
-
if (!ephemeral) {
|
|
138
|
-
const newVars = this.terraformAdapter.getNewVars(envTerraform, onDemandVars, cwd);
|
|
139
|
-
if (Object.keys(newVars).length) {
|
|
140
|
-
await this.envApi.setVars(env, newVars);
|
|
141
|
-
}
|
|
123
|
+
if (status === EnvStatus.Init || status === EnvStatus.Active) {
|
|
124
|
+
console.log(`Env status is ${status}`);
|
|
125
|
+
await this.lockTerraform(env);
|
|
126
|
+
tfArgs = await this.ensurePlan(env, tfArgs);
|
|
127
|
+
if (env.status == EnvStatus.Active) {
|
|
128
|
+
console.log('Will lock for update and run terraform apply (to update resources)');
|
|
142
129
|
}
|
|
130
|
+
await this.envApi.lockForUpdate(env);
|
|
131
|
+
await this.runDeploy(env, tfArgs);
|
|
132
|
+
return;
|
|
143
133
|
}
|
|
134
|
+
tfArgs = await this.ensurePlan(env, tfArgs);
|
|
135
|
+
await this.runDeploy(env, tfArgs);
|
|
136
|
+
}
|
|
137
|
+
async runDeploy(env, tfArgs) {
|
|
138
|
+
console.log('Deploying resources');
|
|
139
|
+
await this.terraformAdapter.apply(tfArgs);
|
|
144
140
|
console.log('Activating env (to finish creation)');
|
|
145
141
|
await this.envApi.activate(env);
|
|
146
142
|
}
|
|
147
|
-
async delete(force, key
|
|
148
|
-
key = this.getKey(key
|
|
143
|
+
async delete(force, key) {
|
|
144
|
+
key = this.getKey(key);
|
|
149
145
|
await this.envApi.delete(key, force);
|
|
150
146
|
}
|
|
151
|
-
async destroy(tfArgs, force
|
|
152
|
-
const key = this.terraformAdapter.getTerraformBackend(
|
|
147
|
+
async destroy(tfArgs, force) {
|
|
148
|
+
const key = this.terraformAdapter.getTerraformBackend().getKey();
|
|
153
149
|
const env = await this.get(key);
|
|
154
150
|
if (env.status === EnvStatus.Init) {
|
|
155
151
|
console.log(`Env ${env.key} status is INIT - no resources, nothing to destroy, just deleting metadata`);
|
|
@@ -163,9 +159,7 @@ export class EnvService extends BaseService {
|
|
|
163
159
|
console.log('Lock env to run destroy');
|
|
164
160
|
await this.envApi.lockForUpdate(env);
|
|
165
161
|
}
|
|
166
|
-
|
|
167
|
-
const envTerraform = { key, ephemeral, args: tfArgs, vars };
|
|
168
|
-
await this.terraformAdapter.destroy(envTerraform, force, cwd);
|
|
162
|
+
await this.terraformAdapter.destroy(env, tfArgs, force);
|
|
169
163
|
await this.envApi.delete(key, force);
|
|
170
164
|
console.log('Please wait for ~30 sec before you can create env with same name');
|
|
171
165
|
}
|
|
@@ -3,9 +3,8 @@ import path from 'path';
|
|
|
3
3
|
const CURRENT_VERSION = 1;
|
|
4
4
|
export class LocalStateService {
|
|
5
5
|
config;
|
|
6
|
-
filePath(
|
|
7
|
-
|
|
8
|
-
return path.join(cwd, '.terraform', 'envctl.json');
|
|
6
|
+
filePath() {
|
|
7
|
+
return path.join(process.cwd(), '.terraform', 'envctl.json');
|
|
9
8
|
}
|
|
10
9
|
createEmptyConfig(filePath) {
|
|
11
10
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -13,12 +12,12 @@ export class LocalStateService {
|
|
|
13
12
|
fs.writeFileSync(filePath, JSON.stringify(this.config, null, 2));
|
|
14
13
|
return this.config;
|
|
15
14
|
}
|
|
16
|
-
load(
|
|
15
|
+
load() {
|
|
17
16
|
if (this.config) {
|
|
18
17
|
return this.config;
|
|
19
18
|
}
|
|
20
19
|
console.log('Load local state config');
|
|
21
|
-
const filePath = this.filePath(
|
|
20
|
+
const filePath = this.filePath();
|
|
22
21
|
if (!fs.existsSync(filePath)) {
|
|
23
22
|
console.warn('Local state file does not exist, must have been accidentally deleted, re-initializing');
|
|
24
23
|
return this.createEmptyConfig(filePath);
|
|
@@ -32,13 +31,13 @@ export class LocalStateService {
|
|
|
32
31
|
this.config = config;
|
|
33
32
|
return config;
|
|
34
33
|
}
|
|
35
|
-
save(config
|
|
34
|
+
save(config) {
|
|
36
35
|
if (!this.config) {
|
|
37
36
|
throw new Error('call init or load first');
|
|
38
37
|
}
|
|
39
|
-
const mergedConfig = { ...this.load(
|
|
38
|
+
const mergedConfig = { ...this.load(), ...config };
|
|
40
39
|
const data = JSON.stringify(mergedConfig, null, 2);
|
|
41
|
-
const filePath = this.filePath(
|
|
40
|
+
const filePath = this.filePath();
|
|
42
41
|
fs.writeFileSync(filePath, data);
|
|
43
42
|
}
|
|
44
43
|
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { basename, join } from 'node:path';
|
|
5
|
-
import { Readable } from 'node:stream';
|
|
6
|
-
import { pipeline } from 'node:stream/promises';
|
|
1
|
+
import { basename } from 'node:path';
|
|
7
2
|
import { TerraformAdapter } from '../client/index.js';
|
|
8
3
|
import { KnownException } from '../exceptions.js';
|
|
9
4
|
import { BaseService } from './BaseService.js';
|
|
@@ -15,8 +10,8 @@ export class LogService extends BaseService {
|
|
|
15
10
|
this.cli = cli;
|
|
16
11
|
this.envApi = envApi;
|
|
17
12
|
}
|
|
18
|
-
async getLogs(key
|
|
19
|
-
key = this.getKey(key
|
|
13
|
+
async getLogs(key) {
|
|
14
|
+
key = this.getKey(key);
|
|
20
15
|
const url = await this.envApi.getLogs(key);
|
|
21
16
|
const urlPath = new URL(url).pathname;
|
|
22
17
|
const gzName = basename(urlPath);
|
|
@@ -32,15 +27,12 @@ export class LogService extends BaseService {
|
|
|
32
27
|
}).formatToParts(timestamp);
|
|
33
28
|
const get = (type) => parts.find(p => p.type === type)?.value ?? '';
|
|
34
29
|
const outName = `${get('year')}-${get('month')}-${get('day')}_${get('hour')}.${get('minute')}.${get('second')}.log`;
|
|
35
|
-
const
|
|
36
|
-
await mkdir(outDir, { recursive: true });
|
|
37
|
-
const outPath = join(outDir, outName);
|
|
38
|
-
const res = await fetch(url);
|
|
30
|
+
const res = await this.envApi.fetch(url);
|
|
39
31
|
if (!res.ok) {
|
|
40
32
|
const msg = await res.text();
|
|
41
33
|
throw new KnownException(`Failed to download logs: ${res.status} ${res.statusText}${msg ? ` - ${msg}` : ''}`);
|
|
42
34
|
}
|
|
43
|
-
await
|
|
35
|
+
const outPath = await this.cli.writeInTmpFile(res.body, outName);
|
|
44
36
|
console.log(`Logs saved to: ${outPath}`);
|
|
45
37
|
await this.cli.run('open', ['-R', outPath]);
|
|
46
38
|
}
|