@agilecustoms/envctl 1.27.0 → 1.28.1
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/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
## usage
|
|
7
7
|
|
|
8
8
|
```shell
|
|
9
|
-
|
|
9
|
+
envctl init -backend-config=key=laxa1986
|
|
10
10
|
envctl apply -var-file=versions.tfvars -var="log_level=debug"
|
|
11
11
|
envctl delete
|
|
12
12
|
```
|
|
@@ -35,12 +35,6 @@ npm view @agilecustoms/envctl version # show latest version available (without i
|
|
|
35
35
|
5. "Set up connection"
|
|
36
36
|
5. In GH workflow job use `permissions: id-token: write` and release action with input `npm-publish: true`
|
|
37
37
|
|
|
38
|
-
## History/motivation
|
|
39
|
-
|
|
40
|
-
`env-api` is a microservice hosted in 'maintenance' account and working as garbage collector: every environment first
|
|
41
|
-
created in `env-api` and then 'managed' by `env-api`: it deletes env when it is not in use anymore OR can extend lifetime.
|
|
42
|
-
Creation API yields unique ID, so you can safely extend lifetime via this ID
|
|
43
|
-
|
|
44
38
|
### Authorization
|
|
45
39
|
|
|
46
40
|
Main use cases:
|
|
@@ -58,7 +52,7 @@ Then I thought about Node.js - it is available on dev machines and in GitHub act
|
|
|
58
52
|
How to distribute it? First I thought about using `ncc` to bundle in one big .js file
|
|
59
53
|
(as I do for `publish-s3` and `gha-healthcheck`) but it will be hard to use on dev machine...
|
|
60
54
|
|
|
61
|
-
So I ended up publishing this client as
|
|
55
|
+
So I ended up publishing this client as a npm package in npmjs
|
|
62
56
|
- CI environments can install it via GH action `agilecustoms/envctl`
|
|
63
57
|
- developer will install it globally via `npm install -g @agilecustoms/envctl`
|
|
64
58
|
|
package/dist/client/Cli.js
CHANGED
|
@@ -15,6 +15,7 @@ export class Cli {
|
|
|
15
15
|
constructor() {
|
|
16
16
|
}
|
|
17
17
|
async run(command, args, options = {}) {
|
|
18
|
+
const successCodes = options.successCodes ?? [0];
|
|
18
19
|
const timeoutMs = options.timeoutMs ?? NO_TIMEOUT;
|
|
19
20
|
const interactive = options.interactive ?? !!options.inScanner;
|
|
20
21
|
const stdio = [interactive ? 'pipe' : 'ignore', 'pipe', 'pipe'];
|
|
@@ -93,7 +94,7 @@ export class Cli {
|
|
|
93
94
|
reject(new TimeoutException(`Process killed after timeout of ${timeoutMs}ms`));
|
|
94
95
|
return;
|
|
95
96
|
}
|
|
96
|
-
if (code
|
|
97
|
+
if (code !== null && successCodes.includes(code)) {
|
|
97
98
|
if (errorBuffer) {
|
|
98
99
|
logger.warn('Process completed successfully, but there were errors:\n' + errorBuffer);
|
|
99
100
|
}
|
package/dist/commands/_utils.js
CHANGED
|
@@ -20,7 +20,7 @@ export class LocalStateService {
|
|
|
20
20
|
logger.info('Load local state config');
|
|
21
21
|
const filePath = this.filePath();
|
|
22
22
|
if (!fs.existsSync(filePath)) {
|
|
23
|
-
logger.warn('Local state file does not exist,
|
|
23
|
+
logger.warn('Local state file does not exist, create new one');
|
|
24
24
|
return this.createEmptyConfig(filePath);
|
|
25
25
|
}
|
|
26
26
|
const data = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { basename } from 'node:path';
|
|
1
|
+
import { basename, dirname } from 'node:path';
|
|
2
2
|
import {} from '../client/index.js';
|
|
3
3
|
import { KnownException } from '../exceptions.js';
|
|
4
4
|
import { logger } from '../logger.js';
|
|
@@ -17,6 +17,11 @@ export class LogService extends BaseService {
|
|
|
17
17
|
const urlPath = new URL(url).pathname;
|
|
18
18
|
const gzName = basename(urlPath);
|
|
19
19
|
const timestamp = Number(gzName.slice(0, -7)) * 1000;
|
|
20
|
+
const res = await this.envApi.fetch(url);
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
const msg = await res.text();
|
|
23
|
+
throw new KnownException(`Failed to download logs: ${res.status} ${res.statusText}${msg ? ` - ${msg}` : ''}`);
|
|
24
|
+
}
|
|
20
25
|
const parts = new Intl.DateTimeFormat(undefined, {
|
|
21
26
|
year: 'numeric',
|
|
22
27
|
month: '2-digit',
|
|
@@ -28,13 +33,16 @@ export class LogService extends BaseService {
|
|
|
28
33
|
}).formatToParts(timestamp);
|
|
29
34
|
const get = (type) => parts.find(p => p.type === type)?.value ?? '';
|
|
30
35
|
const outName = `${get('year')}-${get('month')}-${get('day')}_${get('hour')}.${get('minute')}.${get('second')}.log`;
|
|
31
|
-
const res = await this.envApi.fetch(url);
|
|
32
|
-
if (!res.ok) {
|
|
33
|
-
const msg = await res.text();
|
|
34
|
-
throw new KnownException(`Failed to download logs: ${res.status} ${res.statusText}${msg ? ` - ${msg}` : ''}`);
|
|
35
|
-
}
|
|
36
36
|
const outPath = await this.cli.writeInTmpFile(res.body, outName);
|
|
37
37
|
logger.info(`Logs saved to: ${outPath}`);
|
|
38
|
-
|
|
38
|
+
if (process.platform === 'darwin') {
|
|
39
|
+
await this.cli.run('open', ['-R', outPath]);
|
|
40
|
+
}
|
|
41
|
+
else if (process.platform === 'win32') {
|
|
42
|
+
await this.cli.run('explorer.exe', [`/select,${outPath}`], { successCodes: [0, 1] });
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
await this.cli.run('xdg-open', [dirname(outPath)]);
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
48
|
}
|
|
@@ -65,6 +65,18 @@ export class TerraformAdapter {
|
|
|
65
65
|
return acc;
|
|
66
66
|
}, new Map());
|
|
67
67
|
}
|
|
68
|
+
async terraform(args, options = {}) {
|
|
69
|
+
try {
|
|
70
|
+
await this.cli.run('terraform', args, options);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof ProcessException && error.code !== null && [-2, -4058].includes(error.code)) {
|
|
74
|
+
const msg = `terraform command failed with code ${error.code}. Perhaps terraform is not installed`;
|
|
75
|
+
throw new KnownException(`${msg}. Details:\n${error.message}`, { cause: error });
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
68
80
|
getBackend() {
|
|
69
81
|
if (this.backend) {
|
|
70
82
|
return this.backend;
|
|
@@ -126,7 +138,7 @@ export class TerraformAdapter {
|
|
|
126
138
|
const config = this.localStateService.load();
|
|
127
139
|
const fileHash = hash(fileContent);
|
|
128
140
|
if (fileHash !== config.lockFileHash) {
|
|
129
|
-
throw new KnownException(`Make sure
|
|
141
|
+
throw new KnownException(`Make sure to call 'envctl init' prior to 'envctl plan/apply'`);
|
|
130
142
|
}
|
|
131
143
|
const lockUpdated = config.lockHash !== fileHash;
|
|
132
144
|
config.lockHash = fileHash;
|
|
@@ -159,7 +171,7 @@ export class TerraformAdapter {
|
|
|
159
171
|
if (!linuxArm64) {
|
|
160
172
|
logger.info('Lock providers');
|
|
161
173
|
const vars = this.getVars(args);
|
|
162
|
-
await this.
|
|
174
|
+
await this.terraform(['providers', 'lock', '-platform=linux_arm64', ...vars]);
|
|
163
175
|
}
|
|
164
176
|
const fileContent = this.getLockFile();
|
|
165
177
|
const config = this.localStateService.load();
|
|
@@ -232,7 +244,7 @@ export class TerraformAdapter {
|
|
|
232
244
|
async init(args) {
|
|
233
245
|
logger.info('Running: terraform init ' + args.join(' ') + '\n');
|
|
234
246
|
try {
|
|
235
|
-
await this.
|
|
247
|
+
await this.terraform(['init', ...args], { interactive: true });
|
|
236
248
|
}
|
|
237
249
|
catch (error) {
|
|
238
250
|
if (!(error instanceof ProcessException)) {
|
|
@@ -248,7 +260,7 @@ export class TerraformAdapter {
|
|
|
248
260
|
async _plan(args, attemptNo) {
|
|
249
261
|
logger.info('Running: terraform plan ' + args.join(' ') + '\n');
|
|
250
262
|
try {
|
|
251
|
-
await this.
|
|
263
|
+
await this.terraform(['plan', ...args], { interactive: true });
|
|
252
264
|
}
|
|
253
265
|
catch (error) {
|
|
254
266
|
if (!(error instanceof ProcessException)) {
|
|
@@ -296,7 +308,7 @@ export class TerraformAdapter {
|
|
|
296
308
|
logger.debug('timeout(ms): ' + timeoutMs);
|
|
297
309
|
logger.info('Running: terraform apply ' + args.join(' ') + '\n');
|
|
298
310
|
try {
|
|
299
|
-
await this.
|
|
311
|
+
await this.terraform(['apply', ...args], { timeoutMs, interactive: true });
|
|
300
312
|
this.printTime();
|
|
301
313
|
}
|
|
302
314
|
catch (error) {
|
|
@@ -329,7 +341,7 @@ export class TerraformAdapter {
|
|
|
329
341
|
};
|
|
330
342
|
logger.info('Running: terraform destroy ' + args.join(' ') + '\n');
|
|
331
343
|
try {
|
|
332
|
-
await this.
|
|
344
|
+
await this.terraform(['destroy', ...args], { outScanner, interactive: true });
|
|
333
345
|
}
|
|
334
346
|
catch (error) {
|
|
335
347
|
if (!(error instanceof ProcessException)) {
|
|
@@ -373,6 +385,6 @@ export class TerraformAdapter {
|
|
|
373
385
|
}
|
|
374
386
|
async forceUnlock(id) {
|
|
375
387
|
logger.info('Force unlocking state');
|
|
376
|
-
await this.
|
|
388
|
+
await this.terraform(['force-unlock', '-force', id]);
|
|
377
389
|
}
|
|
378
390
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agilecustoms/envctl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.1",
|
|
4
4
|
"description": "node.js CLI client for manage environments",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"terraform wrapper",
|
|
@@ -31,19 +31,18 @@
|
|
|
31
31
|
"_comment0": "to run via `npx` you must use `name` (with scope) and have at least one entry in `bin`. Key does not matter when run via `npx`",
|
|
32
32
|
"_comment1": "keys become CLI apps (symlinks) when install globally. ex: npm install -g @agilecustoms/envctl; ls $(which alexc-blah) >> lrwxr-xr-x 1 alexc admin 62 Dec 30 14:29 /opt/homebrew/bin/alexc-blah -> ../lib/node_modules/@agilecustoms/envctl-client/dist/index.js",
|
|
33
33
|
"scripts": {
|
|
34
|
-
"prepare": "if [ \"$CI\" != \"true\" ]; then husky; fi",
|
|
35
34
|
"lint": "eslint *.{ts,mjs} \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
36
35
|
"lint:fix": "npm run lint -- --fix",
|
|
37
36
|
"test": "vitest run --coverage",
|
|
38
37
|
"build": "tsc -p tsconfig.build.json",
|
|
39
|
-
"it": "tsc -p tsconfig.build.json --sourceMap true && TEST_CWD
|
|
38
|
+
"it": "tsc -p tsconfig.build.json --sourceMap true && cross-env TEST_CWD=$CWD node dist/index.js",
|
|
40
39
|
"run-help": " npm run it -- --help",
|
|
41
40
|
"run-version": " npm run it -- --version",
|
|
42
41
|
"run-configure": "npm run it -- configure",
|
|
43
42
|
"run-logs": " npm run it -- logs",
|
|
44
43
|
"-d1-": "",
|
|
45
44
|
"d1-desc": "dev env (fully fledged ~/.envctl/default.json)",
|
|
46
|
-
"d1": "CWD=../tt-core npm run it --",
|
|
45
|
+
"d1": "cross-env CWD=../tt-core npm run it --",
|
|
47
46
|
"d1-init": " npm run d1 -- init -reconfigure -upgrade -backend-config=key=d1",
|
|
48
47
|
"d1-status": "npm run d1 -- status --verbose",
|
|
49
48
|
"d1-apply": " npm run d1 -- apply --auto-approve -var=env_size=min",
|
|
@@ -51,29 +50,29 @@
|
|
|
51
50
|
"d1-logs": " npm run d1 -- logs",
|
|
52
51
|
"-d2-": "",
|
|
53
52
|
"d2-desc": "dev env destroy",
|
|
54
|
-
"d2": "CWD=../tt-core npm run it --",
|
|
53
|
+
"d2": "cross-env CWD=../tt-core npm run it --",
|
|
55
54
|
"d2-init": " npm run d2 -- init -reconfigure -upgrade -backend-config=key=d2",
|
|
56
55
|
"d2-status": " npm run d2 -- status",
|
|
57
56
|
"d2-apply": " npm run d2 -- apply --auto-approve",
|
|
58
57
|
"d2-destroy": "npm run d2 -- destroy -var=env_size=min --auto-approve",
|
|
59
58
|
"-d3-": "",
|
|
60
59
|
"d3-desc": "dev env plan apply",
|
|
61
|
-
"d3": "CWD=../tt-core npm run it --",
|
|
60
|
+
"d3": "cross-env CWD=../tt-core npm run it --",
|
|
62
61
|
"d3-init": " npm run d3 -- init -reconfigure -upgrade -backend-config=key=d3",
|
|
63
62
|
"d3-plan": " npm run d3 -- plan -var=env_size=min -var=env=d3 -out=plan",
|
|
64
63
|
"d3-apply": " npm run d3 -- apply plan",
|
|
65
64
|
"d3-delete": "npm run d3 -- delete",
|
|
66
65
|
"-d4-": "",
|
|
67
66
|
"d4-desc": "terraform init and then envctl apply",
|
|
68
|
-
"d4": "CWD=../tt-core npm run it --",
|
|
69
|
-
"d4-init": "cd ../tt-core &&
|
|
67
|
+
"d4": "cross-env CWD=../tt-core npm run it --",
|
|
68
|
+
"d4-init": "cd ../tt-core && rimraf .terraform.lock.hcl .terraform/envctl.json && terraform init -reconfigure -upgrade -backend-config=key=d4",
|
|
70
69
|
"d4-init1": " npm run d4 -- init -reconfigure -upgrade -backend-config=key=d4",
|
|
71
70
|
"d4-apply": " npm run d4 -- apply -auto-approve -var=env_size=min",
|
|
72
71
|
"d4-delete": "npm run d4 -- delete",
|
|
73
72
|
"d4-status": "npm run d4 -- status",
|
|
74
73
|
"-e1-": "",
|
|
75
74
|
"e1-desc": "ephemeral env in CI (must have ENVCTL_API_KEY_ in env vars)",
|
|
76
|
-
"e1": "CWD=../tt-core CI=true ENVCTL_HOME=non-existing ENVCTL_API_KEY=\"$ENVCTL_API_KEY_\" npm run it --",
|
|
75
|
+
"e1": "cross-env CWD=../tt-core CI=true ENVCTL_HOME=non-existing ENVCTL_API_KEY=\"$ENVCTL_API_KEY_\" npm run it --",
|
|
77
76
|
"e1-create": "npm run e1 -- create-ephemeral e1 --verbose",
|
|
78
77
|
"e1-init": " npm run e1 -- init -reconfigure -upgrade -backend-config=key=e1",
|
|
79
78
|
"e1-plan": " npm run e1 -- plan -var=env_size=min -var=env=e1 -out=plan",
|
|
@@ -81,15 +80,15 @@
|
|
|
81
80
|
"e1-delete": "npm run e1 -- delete",
|
|
82
81
|
"-tt-": "",
|
|
83
82
|
"tt-desc": "deploy TT project from local machine",
|
|
84
|
-
"tt": "CWD=../tt-gitops AWS_PROFILE=ac-tt-dev-deployer npm run it --",
|
|
83
|
+
"tt": "cross-env CWD=../tt-gitops AWS_PROFILE=ac-tt-dev-deployer npm run it --",
|
|
85
84
|
"run-init": " npm run tt -- init -reconfigure -upgrade -backend-config=key=laxa1986 -var-file=versions.tfvars",
|
|
86
85
|
"run-status": " npm run tt -- status",
|
|
87
86
|
"run-apply": " npm run tt -- apply --auto-approve -var=env_size=min -var-file=versions.tfvars",
|
|
88
87
|
"run-delete": " npm run tt -- delete"
|
|
89
88
|
},
|
|
90
89
|
"dependencies": {
|
|
91
|
-
"commander": "^14.0.0",
|
|
92
90
|
"@inquirer/prompts": "^8.0.0",
|
|
91
|
+
"commander": "^14.0.0",
|
|
93
92
|
"update-notifier": "^7.3.1"
|
|
94
93
|
},
|
|
95
94
|
"devDependencies": {
|
|
@@ -97,9 +96,11 @@
|
|
|
97
96
|
"@types/node": "^22.10.2",
|
|
98
97
|
"@types/update-notifier": "^6.0.8",
|
|
99
98
|
"@vitest/coverage-v8": "^4.0.0",
|
|
99
|
+
"cross-env": "^10.1.0",
|
|
100
100
|
"eslint": "^9.22.0",
|
|
101
101
|
"eslint-plugin-import": "^2.31.0",
|
|
102
102
|
"husky": "^9.1.7",
|
|
103
|
+
"rimraf": "^6.1.3",
|
|
103
104
|
"typescript": "^6.0.0",
|
|
104
105
|
"typescript-eslint": "^8.8.1",
|
|
105
106
|
"vitest": "^4.0.0"
|