@agilecustoms/envctl 1.1.1 → 1.3.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/{ProcessRunner.js → Cli.js} +28 -1
- package/dist/client/EnvApiClient.js +57 -15
- package/dist/client/HttpClient.js +4 -1
- package/dist/client/TerraformAdapter.js +44 -18
- package/dist/client/index.js +0 -1
- package/dist/commands/_utils.js +2 -2
- package/dist/container.js +6 -7
- package/dist/service/BaseService.js +14 -0
- package/dist/service/EnvService.js +27 -80
- package/dist/service/LocalStateService.js +9 -24
- package/dist/service/LogService.js +8 -8
- package/package.json +24 -22
- package/dist/client/CliHelper.js +0 -30
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
import * as readline from 'readline';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
3
5
|
import { ProcessException } from '../exceptions.js';
|
|
4
|
-
|
|
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
|
+
export class Cli {
|
|
5
20
|
async run(command, args, cwd, out_scanner, in_scanner) {
|
|
6
21
|
const spawnOptions = {
|
|
7
22
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -68,4 +83,16 @@ export class ProcessRunner {
|
|
|
68
83
|
});
|
|
69
84
|
});
|
|
70
85
|
}
|
|
86
|
+
async promptYesNo(message, defaultValue = false) {
|
|
87
|
+
const { answer } = await inquirer.prompt([{
|
|
88
|
+
type: 'confirm',
|
|
89
|
+
name: 'answer',
|
|
90
|
+
message,
|
|
91
|
+
default: defaultValue,
|
|
92
|
+
}]);
|
|
93
|
+
return answer;
|
|
94
|
+
}
|
|
95
|
+
getKind(cwd) {
|
|
96
|
+
return getDirName(cwd);
|
|
97
|
+
}
|
|
71
98
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { KnownException, NotFoundException } from '../exceptions.js';
|
|
1
|
+
import { BusinessException, KnownException, NotFoundException } from '../exceptions.js';
|
|
2
2
|
import { EnvStatus } from '../model/index.js';
|
|
3
|
-
import { HttpClient } from './HttpClient.js';
|
|
3
|
+
import { HttpClient, toUrl } from './HttpClient.js';
|
|
4
4
|
export class EnvApiClient {
|
|
5
5
|
httpClient;
|
|
6
6
|
constructor(httpClient) {
|
|
@@ -18,19 +18,53 @@ export class EnvApiClient {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
async createEphemeral(env) {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
let result;
|
|
22
|
+
try {
|
|
23
|
+
result = await this.httpClient.post('/ci/env-ephemeral', env);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof BusinessException) {
|
|
27
|
+
throw new KnownException(`Failed to create ephemeral environment: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
return toUrl(`/public/env/${env.key}/extend?token=${result.token}`);
|
|
23
32
|
}
|
|
24
33
|
async create(env) {
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
let result;
|
|
35
|
+
try {
|
|
36
|
+
result = await this.httpClient.post('/ci/env', env);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error instanceof BusinessException) {
|
|
40
|
+
throw new KnownException(`Failed to create environment: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
return { ...env, ephemeral: false, status: EnvStatus.Deploying, ttl: result.ttl };
|
|
27
45
|
}
|
|
28
46
|
async setVars(env, vars) {
|
|
29
|
-
|
|
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
|
+
}
|
|
30
56
|
env.vars = { ...env.vars || {}, ...vars };
|
|
31
57
|
}
|
|
32
58
|
async activate(env) {
|
|
33
|
-
|
|
59
|
+
try {
|
|
60
|
+
await this.httpClient.post(`/ci/env/${env.key}/activate`);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof BusinessException) {
|
|
64
|
+
throw new KnownException(`Failed to activate environment ${env.key}: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
34
68
|
env.status = EnvStatus.Active;
|
|
35
69
|
}
|
|
36
70
|
async lockForUpdate(env) {
|
|
@@ -38,25 +72,33 @@ export class EnvApiClient {
|
|
|
38
72
|
stateFile: env.stateFile,
|
|
39
73
|
lockFile: env.lockFile
|
|
40
74
|
};
|
|
41
|
-
|
|
42
|
-
|
|
75
|
+
let result;
|
|
76
|
+
try {
|
|
77
|
+
result = await this.httpClient.post(`/ci/env/${env.key}/lock-for-update`, body);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof BusinessException) {
|
|
81
|
+
throw new KnownException(`Failed to lock environment ${env.key} for update: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
env.status = result.status;
|
|
43
86
|
}
|
|
44
|
-
async delete(
|
|
87
|
+
async delete(key, force = false) {
|
|
45
88
|
console.log('Sending delete command');
|
|
46
89
|
let result;
|
|
47
90
|
try {
|
|
48
|
-
result = await this.httpClient.fetch(`/ci/env/${
|
|
91
|
+
result = await this.httpClient.fetch(`/ci/env/${key}?force=${force}`, {
|
|
49
92
|
method: 'DELETE'
|
|
50
93
|
});
|
|
51
94
|
}
|
|
52
95
|
catch (error) {
|
|
53
96
|
if (error instanceof NotFoundException) {
|
|
54
|
-
throw new KnownException(`Environment ${
|
|
97
|
+
throw new KnownException(`Environment ${key} is not found`);
|
|
55
98
|
}
|
|
56
99
|
throw error;
|
|
57
100
|
}
|
|
58
|
-
|
|
59
|
-
console.log(result.statusText);
|
|
101
|
+
console.log(result.message);
|
|
60
102
|
}
|
|
61
103
|
async getLogs(key) {
|
|
62
104
|
let result;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { BusinessException, HttpException, NotFoundException } from '../exceptions.js';
|
|
2
2
|
const HOST = 'cli.maintenance.agilecustoms.com';
|
|
3
|
+
export function toUrl(path) {
|
|
4
|
+
return `https://${HOST}/env-api${path}`;
|
|
5
|
+
}
|
|
3
6
|
export class HttpClient {
|
|
4
7
|
constructor() { }
|
|
5
8
|
async post(path, body, options = {}) {
|
|
@@ -15,7 +18,7 @@ export class HttpClient {
|
|
|
15
18
|
return await this.fetch(path, options);
|
|
16
19
|
}
|
|
17
20
|
async fetch(path, options = {}) {
|
|
18
|
-
const url =
|
|
21
|
+
const url = toUrl(path);
|
|
19
22
|
let response;
|
|
20
23
|
try {
|
|
21
24
|
response = await fetch(url, options);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { AbortedException, KnownException, ProcessException } from '../exceptions.js';
|
|
5
|
+
import { LocalStateService } from '../service/LocalStateService.js';
|
|
4
6
|
const MAX_ATTEMPTS = 2;
|
|
5
7
|
const RETRYABLE_ERRORS = [
|
|
6
8
|
'ConcurrentModificationException',
|
|
@@ -8,14 +10,17 @@ const RETRYABLE_ERRORS = [
|
|
|
8
10
|
'operation error Lambda: AddPermission, https response error StatusCode: 404',
|
|
9
11
|
`because public policies are prevented by the BlockPublicPolicy setting in S3 Block Public Access`
|
|
10
12
|
];
|
|
13
|
+
function hash(data) {
|
|
14
|
+
return createHash('sha256').update(data).digest('hex');
|
|
15
|
+
}
|
|
11
16
|
export class TerraformAdapter {
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
cli;
|
|
18
|
+
localStateService;
|
|
14
19
|
backends;
|
|
15
20
|
backend = undefined;
|
|
16
|
-
constructor(
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
21
|
+
constructor(cli, localStateService, backends) {
|
|
22
|
+
this.cli = cli;
|
|
23
|
+
this.localStateService = localStateService;
|
|
19
24
|
this.backends = backends.reduce((acc, backend) => {
|
|
20
25
|
acc.set(backend.getType(), backend);
|
|
21
26
|
return acc;
|
|
@@ -34,13 +39,6 @@ export class TerraformAdapter {
|
|
|
34
39
|
throw new KnownException(`Failed to read terraform lock file: ${lockPath}`, { cause: err });
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
|
-
getKey(key, cwd) {
|
|
38
|
-
if (!key) {
|
|
39
|
-
console.log('Key is not provided, inferring from state file');
|
|
40
|
-
key = this.getTerraformBackend(cwd).getKey();
|
|
41
|
-
}
|
|
42
|
-
return key;
|
|
43
|
-
}
|
|
44
42
|
getTerraformBackend(cwd) {
|
|
45
43
|
if (this.backend) {
|
|
46
44
|
return this.backend;
|
|
@@ -75,9 +73,37 @@ export class TerraformAdapter {
|
|
|
75
73
|
this.backend = backend;
|
|
76
74
|
return backend;
|
|
77
75
|
}
|
|
76
|
+
async lock(newEnv, cwd) {
|
|
77
|
+
const res = {};
|
|
78
|
+
const stateFileContent = this.getTerraformBackend(cwd).validateAndGetStateFileContent();
|
|
79
|
+
const stateHash = hash(stateFileContent);
|
|
80
|
+
const config = this.localStateService.load(cwd);
|
|
81
|
+
if (config.stateHash !== stateHash || newEnv) {
|
|
82
|
+
config.stateHash = stateHash;
|
|
83
|
+
res.stateFile = stateFileContent;
|
|
84
|
+
}
|
|
85
|
+
const linuxArm64 = process.platform === 'linux' && process.arch === 'arm64';
|
|
86
|
+
if (!linuxArm64 || newEnv) {
|
|
87
|
+
let lockFileContent = this.getLockFile(cwd);
|
|
88
|
+
if (!linuxArm64) {
|
|
89
|
+
const lockFileHash = hash(lockFileContent);
|
|
90
|
+
if (config.lockHash !== lockFileHash) {
|
|
91
|
+
await this.lockProviders(cwd);
|
|
92
|
+
lockFileContent = this.getLockFile(cwd);
|
|
93
|
+
config.lockHash = hash(lockFileContent);
|
|
94
|
+
res.lockFile = lockFileContent;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (newEnv) {
|
|
98
|
+
res.lockFile = lockFileContent;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
this.localStateService.save(config, cwd);
|
|
102
|
+
return res;
|
|
103
|
+
}
|
|
78
104
|
async lockProviders(cwd) {
|
|
79
105
|
console.log('Update lock file');
|
|
80
|
-
await this.
|
|
106
|
+
await this.cli.run('terraform', ['providers', 'lock', '-platform=linux_arm64'], cwd);
|
|
81
107
|
}
|
|
82
108
|
printTime() {
|
|
83
109
|
const now = new Date();
|
|
@@ -168,7 +194,7 @@ export class TerraformAdapter {
|
|
|
168
194
|
const args = this.tfArgs(env, cwd);
|
|
169
195
|
console.log('Running: terraform plan -auto-approve', ...args, '\n');
|
|
170
196
|
try {
|
|
171
|
-
await this.
|
|
197
|
+
await this.cli.run('terraform', ['plan', ...args], cwd);
|
|
172
198
|
}
|
|
173
199
|
catch (error) {
|
|
174
200
|
if (!(error instanceof ProcessException)) {
|
|
@@ -210,7 +236,7 @@ export class TerraformAdapter {
|
|
|
210
236
|
console.log('Running: terraform apply -auto-approve', ...args, '\n');
|
|
211
237
|
this.printTime();
|
|
212
238
|
try {
|
|
213
|
-
await this.
|
|
239
|
+
await this.cli.run('terraform', ['apply', '-auto-approve', ...args], cwd, out_scanner, in_scanner);
|
|
214
240
|
this.printTime();
|
|
215
241
|
}
|
|
216
242
|
catch (error) {
|
|
@@ -240,7 +266,7 @@ export class TerraformAdapter {
|
|
|
240
266
|
const args = this.tfArgs(env, cwd);
|
|
241
267
|
console.log('Running: terraform destroy -auto-approve', ...args, '\n');
|
|
242
268
|
try {
|
|
243
|
-
await this.
|
|
269
|
+
await this.cli.run('terraform', ['destroy', '-auto-approve', ...args], cwd, scanner);
|
|
244
270
|
}
|
|
245
271
|
catch (error) {
|
|
246
272
|
if (!(error instanceof ProcessException)) {
|
|
@@ -273,7 +299,7 @@ export class TerraformAdapter {
|
|
|
273
299
|
}
|
|
274
300
|
}
|
|
275
301
|
async promptUnlock(id, cwd) {
|
|
276
|
-
const answerYes = await this.
|
|
302
|
+
const answerYes = await this.cli.promptYesNo('Terraform state is locked. Most often due to previously interrupted process.\n'
|
|
277
303
|
+ 'Rarely (main lock intention) is when another process actively work with the state to avoid inconsistent modification.\n'
|
|
278
304
|
+ 'Do you want to force unlock?: ');
|
|
279
305
|
if (!answerYes) {
|
|
@@ -283,6 +309,6 @@ export class TerraformAdapter {
|
|
|
283
309
|
}
|
|
284
310
|
async forceUnlock(id, cwd) {
|
|
285
311
|
console.log('Force unlocking state');
|
|
286
|
-
await this.
|
|
312
|
+
await this.cli.run('terraform', ['force-unlock', '-force', id], cwd);
|
|
287
313
|
}
|
|
288
314
|
}
|
package/dist/client/index.js
CHANGED
package/dist/commands/_utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ExitPromptError } from '@inquirer/core';
|
|
2
|
-
import {
|
|
2
|
+
import { AbortedException, KnownException } from '../exceptions.js';
|
|
3
3
|
export function wrap(callable) {
|
|
4
4
|
return async (...args) => {
|
|
5
5
|
let result;
|
|
@@ -7,7 +7,7 @@ export function wrap(callable) {
|
|
|
7
7
|
result = await callable(...args);
|
|
8
8
|
}
|
|
9
9
|
catch (error) {
|
|
10
|
-
if (error instanceof KnownException
|
|
10
|
+
if (error instanceof KnownException) {
|
|
11
11
|
console.error(error.message);
|
|
12
12
|
}
|
|
13
13
|
else if (error instanceof AbortedException || error instanceof ExitPromptError) {
|
package/dist/container.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { S3Backend } from './backend/S3Backend.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Cli } from './client/Cli.js';
|
|
3
|
+
import { EnvApiClient, HttpClient, TerraformAdapter } from './client/index.js';
|
|
4
4
|
import { ConfigService, EnvService } from './service/index.js';
|
|
5
5
|
import { LocalStateService } from './service/LocalStateService.js';
|
|
6
6
|
import { LogService } from './service/LogService.js';
|
|
7
|
-
const
|
|
7
|
+
const cli = new Cli();
|
|
8
8
|
const configService = new ConfigService();
|
|
9
9
|
const backends = [
|
|
10
10
|
new S3Backend()
|
|
11
11
|
];
|
|
12
12
|
const httpClient = new HttpClient();
|
|
13
13
|
const envApiClient = new EnvApiClient(httpClient);
|
|
14
|
-
const processRunner = new ProcessRunner();
|
|
15
|
-
const terraformAdapter = new TerraformAdapter(processRunner, cliHelper, backends);
|
|
16
14
|
const localStateService = new LocalStateService();
|
|
17
|
-
const
|
|
18
|
-
const
|
|
15
|
+
const terraformAdapter = new TerraformAdapter(cli, localStateService, backends);
|
|
16
|
+
const envService = new EnvService(cli, envApiClient, terraformAdapter);
|
|
17
|
+
const logService = new LogService(cli, envApiClient, terraformAdapter);
|
|
19
18
|
export { configService, envService, logService };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { TerraformAdapter } from '../client/index.js';
|
|
2
|
+
export class BaseService {
|
|
3
|
+
terraformAdapter;
|
|
4
|
+
constructor(terraformAdapter) {
|
|
5
|
+
this.terraformAdapter = terraformAdapter;
|
|
6
|
+
}
|
|
7
|
+
getKey(key, cwd) {
|
|
8
|
+
if (!key) {
|
|
9
|
+
console.log('Key is not provided, inferring from state file');
|
|
10
|
+
key = this.terraformAdapter.getTerraformBackend(cwd).getKey();
|
|
11
|
+
}
|
|
12
|
+
return key;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Cli } from '../client/Cli.js';
|
|
2
|
+
import { EnvApiClient, TerraformAdapter } from '../client/index.js';
|
|
2
3
|
import { KnownException } from '../exceptions.js';
|
|
3
4
|
import { EnvStatus } from '../model/index.js';
|
|
4
|
-
import {
|
|
5
|
-
export class EnvService {
|
|
6
|
-
|
|
5
|
+
import { BaseService } from './BaseService.js';
|
|
6
|
+
export class EnvService extends BaseService {
|
|
7
|
+
cli;
|
|
7
8
|
envApi;
|
|
8
|
-
terraformAdapter
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
this.cliHelper = cliHelper;
|
|
9
|
+
constructor(cli, envApi, terraformAdapter) {
|
|
10
|
+
super(terraformAdapter);
|
|
11
|
+
this.cli = cli;
|
|
12
12
|
this.envApi = envApi;
|
|
13
|
-
this.terraformAdapter = terraformAdapter;
|
|
14
|
-
this.localStateService = localStateService;
|
|
15
13
|
}
|
|
16
14
|
async status(key, cwd) {
|
|
17
|
-
key = this.
|
|
15
|
+
key = this.getKey(key, cwd);
|
|
18
16
|
console.log(`Retrieve env ${key}`);
|
|
19
17
|
const env = await this.envApi.get(key);
|
|
20
18
|
if (env === null) {
|
|
@@ -35,7 +33,7 @@ export class EnvService {
|
|
|
35
33
|
console.log(`Expires at ${formattedDate}`);
|
|
36
34
|
}
|
|
37
35
|
if (env.kind) {
|
|
38
|
-
const kind = this.
|
|
36
|
+
const kind = this.cli.getKind(cwd);
|
|
39
37
|
if (env.kind !== kind) {
|
|
40
38
|
console.warn(`Env ${key} kind (dir-name): ${env.kind} - looks like this env was deployed from a different directory`);
|
|
41
39
|
}
|
|
@@ -56,7 +54,7 @@ export class EnvService {
|
|
|
56
54
|
if (env.ephemeral)
|
|
57
55
|
return;
|
|
58
56
|
if (env.kind) {
|
|
59
|
-
const kind = this.
|
|
57
|
+
const kind = this.cli.getKind(cwd);
|
|
60
58
|
if (kind !== env.kind) {
|
|
61
59
|
throw new KnownException(`Env ${env.key} kind (dir-name): ${env.kind} - make sure you run this command from the same directory`);
|
|
62
60
|
}
|
|
@@ -77,46 +75,24 @@ export class EnvService {
|
|
|
77
75
|
await this.terraformAdapter.plan(envTerraform, cwd);
|
|
78
76
|
}
|
|
79
77
|
async createEphemeral(key) {
|
|
80
|
-
const env = await this.envApi.get(key);
|
|
81
|
-
if (env !== null) {
|
|
82
|
-
throw new KnownException(`Env ${key} already exists`);
|
|
83
|
-
}
|
|
84
78
|
const createEnv = { key };
|
|
85
79
|
return await this.envApi.createEphemeral(createEnv);
|
|
86
80
|
}
|
|
87
81
|
async lockTerraform(env, cwd, newEnv = false) {
|
|
88
|
-
const config = this.localStateService.load(cwd);
|
|
89
82
|
console.log('Validate terraform.tfstate file');
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const linuxArm64 = process.platform === 'linux' && process.arch === 'arm64';
|
|
97
|
-
if (!linuxArm64 || newEnv) {
|
|
98
|
-
let lockFileContent = this.terraformAdapter.getLockFile(cwd);
|
|
99
|
-
if (!linuxArm64) {
|
|
100
|
-
const lockFileHash = this.localStateService.hash(lockFileContent);
|
|
101
|
-
if (config.lockHash !== lockFileHash) {
|
|
102
|
-
await this.terraformAdapter.lockProviders(cwd);
|
|
103
|
-
lockFileContent = this.terraformAdapter.getLockFile(cwd);
|
|
104
|
-
config.lockHash = this.localStateService.hash(lockFileContent);
|
|
105
|
-
env.lockFile = lockFileContent;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (newEnv) {
|
|
109
|
-
env.lockFile = lockFileContent;
|
|
110
|
-
}
|
|
83
|
+
const res = await this.terraformAdapter.lock(newEnv, cwd);
|
|
84
|
+
if (res.lockFile) {
|
|
85
|
+
env.lockFile = res.lockFile;
|
|
86
|
+
}
|
|
87
|
+
if (res.stateFile) {
|
|
88
|
+
env.stateFile = res.stateFile;
|
|
111
89
|
}
|
|
112
|
-
this.localStateService.save(config, cwd);
|
|
113
90
|
}
|
|
114
91
|
async deploy(tfArgs, cwd) {
|
|
115
92
|
const key = this.terraformAdapter.getTerraformBackend(cwd).getKey();
|
|
116
93
|
let env = await this.tryGetEnv(key);
|
|
117
94
|
if (env === null) {
|
|
118
|
-
this.
|
|
119
|
-
const kind = this.cliHelper.getKind(cwd);
|
|
95
|
+
const kind = this.cli.getKind(cwd);
|
|
120
96
|
console.log(`Inferred kind from directory name: ${kind}`);
|
|
121
97
|
const createEnv = { key, kind };
|
|
122
98
|
await this.lockTerraform(createEnv, cwd, true);
|
|
@@ -128,12 +104,8 @@ export class EnvService {
|
|
|
128
104
|
this.checkKind(env, cwd);
|
|
129
105
|
switch (env.status) {
|
|
130
106
|
case EnvStatus.Init:
|
|
131
|
-
this.localStateService.init(cwd);
|
|
132
|
-
await this.lockTerraform(env, cwd, true);
|
|
133
|
-
await this.envApi.lockForUpdate(env);
|
|
134
|
-
break;
|
|
135
107
|
case EnvStatus.Active:
|
|
136
|
-
if (env.status
|
|
108
|
+
if (env.status == EnvStatus.Active) {
|
|
137
109
|
console.log('Env status is ACTIVE\nWill lock for update and run terraform apply (to update resources)');
|
|
138
110
|
}
|
|
139
111
|
await this.lockTerraform(env, cwd);
|
|
@@ -141,7 +113,7 @@ export class EnvService {
|
|
|
141
113
|
break;
|
|
142
114
|
case EnvStatus.Deploying:
|
|
143
115
|
case EnvStatus.Updating:
|
|
144
|
-
const answerYes = await this.
|
|
116
|
+
const answerYes = await this.cli.promptYesNo(`Env status is ${env.status}, likely due to an error from a previous run\n
|
|
145
117
|
Do you want to proceed with deployment?`);
|
|
146
118
|
if (!answerYes) {
|
|
147
119
|
console.log('Aborting deployment');
|
|
@@ -173,44 +145,19 @@ export class EnvService {
|
|
|
173
145
|
await this.envApi.activate(env);
|
|
174
146
|
}
|
|
175
147
|
async delete(force, key, cwd) {
|
|
176
|
-
key = this.
|
|
177
|
-
|
|
178
|
-
if (env.status === EnvStatus.Init) {
|
|
179
|
-
await this.envApi.delete(env);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (force) {
|
|
183
|
-
await this.envApi.delete(env);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
if (env.status === EnvStatus.Deleting) {
|
|
187
|
-
throw new KnownException(`Env ${env.key} status is DELETING, please wait or re-run with --force`);
|
|
188
|
-
}
|
|
189
|
-
const kind = this.cliHelper.getKind(cwd);
|
|
190
|
-
if (env.kind && env.kind !== kind) {
|
|
191
|
-
const answerYes = await this.cliHelper.promptYesNo(`Env ${env.key} kind (dir-name): ${env.kind}\n`
|
|
192
|
-
+ 'You\'re deleting env deployed from different dir\n'
|
|
193
|
-
+ 'Do you want to proceed?');
|
|
194
|
-
if (!answerYes) {
|
|
195
|
-
console.log('Aborting deletion');
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
await this.envApi.delete(env);
|
|
148
|
+
key = this.getKey(key, cwd);
|
|
149
|
+
await this.envApi.delete(key, force);
|
|
200
150
|
}
|
|
201
151
|
async destroy(tfArgs, force, cwd) {
|
|
202
152
|
const key = this.terraformAdapter.getTerraformBackend(cwd).getKey();
|
|
203
153
|
const env = await this.get(key);
|
|
204
154
|
if (env.status === EnvStatus.Init) {
|
|
205
155
|
console.log(`Env ${env.key} status is INIT - no resources, nothing to destroy, just deleting metadata`);
|
|
206
|
-
await this.envApi.delete(
|
|
156
|
+
await this.envApi.delete(key, force);
|
|
207
157
|
return;
|
|
208
158
|
}
|
|
209
|
-
if (!force) {
|
|
210
|
-
|
|
211
|
-
throw new KnownException(`Env ${env.key} status is DELETING, please wait or re-run with --force`);
|
|
212
|
-
}
|
|
213
|
-
this.checkKind(env, cwd);
|
|
159
|
+
if (env.status === EnvStatus.Deleting && !force) {
|
|
160
|
+
throw new KnownException(`Env ${env.key} status is DELETING, please wait or re-run with --force`);
|
|
214
161
|
}
|
|
215
162
|
if (env.status === EnvStatus.Active) {
|
|
216
163
|
console.log('Lock env to run destroy');
|
|
@@ -219,14 +166,14 @@ export class EnvService {
|
|
|
219
166
|
const { ephemeral, vars } = env;
|
|
220
167
|
const envTerraform = { key, ephemeral, args: tfArgs, vars };
|
|
221
168
|
await this.terraformAdapter.destroy(envTerraform, force, cwd);
|
|
222
|
-
await this.envApi.delete(
|
|
169
|
+
await this.envApi.delete(key, force);
|
|
223
170
|
console.log('Please wait for ~30 sec before you can create env with same name');
|
|
224
171
|
}
|
|
225
172
|
async get(key) {
|
|
226
173
|
console.log(`Retrieve env ${key}`);
|
|
227
174
|
const env = await this.envApi.get(key);
|
|
228
175
|
if (!env) {
|
|
229
|
-
throw new KnownException(`Environment ${key}
|
|
176
|
+
throw new KnownException(`Environment ${key} is not found`);
|
|
230
177
|
}
|
|
231
178
|
console.log(`Env status: ${env.status}`);
|
|
232
179
|
return env;
|
|
@@ -1,42 +1,22 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import * as fs from 'node:fs';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
const CURRENT_VERSION = 1;
|
|
5
4
|
export class LocalStateService {
|
|
6
5
|
config;
|
|
7
|
-
constructor() {
|
|
8
|
-
}
|
|
9
6
|
filePath(cwd) {
|
|
10
7
|
cwd = cwd || process.cwd();
|
|
11
8
|
return path.join(cwd, '.terraform', 'envctl.json');
|
|
12
9
|
}
|
|
13
|
-
hash(data) {
|
|
14
|
-
return createHash('sha256').update(data).digest('hex');
|
|
15
|
-
}
|
|
16
10
|
createEmptyConfig(filePath) {
|
|
17
11
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
18
12
|
this.config = { version: CURRENT_VERSION };
|
|
19
13
|
fs.writeFileSync(filePath, JSON.stringify(this.config, null, 2));
|
|
20
14
|
return this.config;
|
|
21
15
|
}
|
|
22
|
-
init(cwd) {
|
|
23
|
-
const filePath = this.filePath(cwd);
|
|
24
|
-
if (!fs.existsSync(filePath)) {
|
|
25
|
-
this.createEmptyConfig(filePath);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
const data = fs.readFileSync(filePath, 'utf-8');
|
|
29
|
-
const config = JSON.parse(data);
|
|
30
|
-
if (config.version == CURRENT_VERSION) {
|
|
31
|
-
this.config = config;
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
console.log('Local state file version mismatch, re-initializing');
|
|
35
|
-
this.createEmptyConfig(filePath);
|
|
36
|
-
}
|
|
37
16
|
load(cwd) {
|
|
38
|
-
if (this.config)
|
|
17
|
+
if (this.config) {
|
|
39
18
|
return this.config;
|
|
19
|
+
}
|
|
40
20
|
console.log('Load local state config');
|
|
41
21
|
const filePath = this.filePath(cwd);
|
|
42
22
|
if (!fs.existsSync(filePath)) {
|
|
@@ -44,8 +24,13 @@ export class LocalStateService {
|
|
|
44
24
|
return this.createEmptyConfig(filePath);
|
|
45
25
|
}
|
|
46
26
|
const data = fs.readFileSync(filePath, 'utf-8');
|
|
47
|
-
|
|
48
|
-
|
|
27
|
+
let config = JSON.parse(data);
|
|
28
|
+
if (config.version != CURRENT_VERSION) {
|
|
29
|
+
console.log('Local state file version mismatch, re-initializing');
|
|
30
|
+
config = this.createEmptyConfig(filePath);
|
|
31
|
+
}
|
|
32
|
+
this.config = config;
|
|
33
|
+
return config;
|
|
49
34
|
}
|
|
50
35
|
save(config, cwd) {
|
|
51
36
|
if (!this.config) {
|
|
@@ -6,17 +6,17 @@ import { Readable } from 'node:stream';
|
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
7
|
import { TerraformAdapter } from '../client/index.js';
|
|
8
8
|
import { KnownException } from '../exceptions.js';
|
|
9
|
-
|
|
9
|
+
import { BaseService } from './BaseService.js';
|
|
10
|
+
export class LogService extends BaseService {
|
|
11
|
+
cli;
|
|
10
12
|
envApi;
|
|
11
|
-
terraformAdapter
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
constructor(cli, envApi, terraformAdapter) {
|
|
14
|
+
super(terraformAdapter);
|
|
15
|
+
this.cli = cli;
|
|
14
16
|
this.envApi = envApi;
|
|
15
|
-
this.terraformAdapter = terraformAdapter;
|
|
16
|
-
this.processRunner = processRunner;
|
|
17
17
|
}
|
|
18
18
|
async getLogs(key, cwd) {
|
|
19
|
-
key = this.
|
|
19
|
+
key = this.getKey(key, cwd);
|
|
20
20
|
const url = await this.envApi.getLogs(key);
|
|
21
21
|
const urlPath = new URL(url).pathname;
|
|
22
22
|
const gzName = basename(urlPath);
|
|
@@ -42,6 +42,6 @@ export class LogService {
|
|
|
42
42
|
}
|
|
43
43
|
await pipeline(Readable.fromWeb(res.body), createWriteStream(outPath, { flags: 'w' }));
|
|
44
44
|
console.log(`Logs saved to: ${outPath}`);
|
|
45
|
-
await this.
|
|
45
|
+
await this.cli.run('open', ['-R', outPath]);
|
|
46
46
|
}
|
|
47
47
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agilecustoms/envctl",
|
|
3
3
|
"description": "node.js CLI client for manage environments",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"author": "Alex Chekulaev",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -26,32 +26,34 @@
|
|
|
26
26
|
"lint": "eslint *.{ts,mjs} \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
27
27
|
"lint:fix": "npm run lint -- --fix",
|
|
28
28
|
"test": "vitest run --coverage",
|
|
29
|
-
"build": "tsc",
|
|
30
|
-
"run": "node dist/index.js",
|
|
31
|
-
"run-version": "
|
|
32
|
-
"run-configure": "
|
|
29
|
+
"build": "tsc -p tsconfig.build.json",
|
|
30
|
+
"run": "tsc -p tsconfig.build.json --sourceMap true && node dist/index.js",
|
|
31
|
+
"run-version": " npm run run -- --version",
|
|
32
|
+
"run-configure": "npm run run -- configure",
|
|
33
33
|
"~": "",
|
|
34
|
-
"run-core-init": "cd ../tt-core && terraform init -upgrade -backend-config=key=laxa1986 -reconfigure",
|
|
35
|
-
"run-core-
|
|
36
|
-
"run-core-
|
|
37
|
-
"run-core-
|
|
38
|
-
"run-core-
|
|
39
|
-
"run-core-
|
|
40
|
-
"run-core-
|
|
34
|
+
"run-core-init": " cd ../tt-core && terraform init -upgrade -backend-config=key=laxa1986 -reconfigure",
|
|
35
|
+
"run-core-tfplan": "cd ../tt-core && terraform plan -out=.terraform/plan",
|
|
36
|
+
"run-core-status": " npm run run -- status --cwd ../tt-core",
|
|
37
|
+
"run-core-plan": " npm run run -- plan --cwd ../tt-core",
|
|
38
|
+
"run-core-deploy": " npm run run -- deploy --cwd ../tt-core -var=\"env_size=min\"",
|
|
39
|
+
"run-core-deployp": "npm run run -- deploy .terraform/plan --cwd ../tt-core",
|
|
40
|
+
"run-core-delete": " npm run run -- delete --cwd ../tt-core",
|
|
41
|
+
"run-core-destroy": "npm run run -- destroy --cwd ../tt-core",
|
|
42
|
+
"run-core-logs": " npm run run -- logs --cwd ../tt-core",
|
|
41
43
|
"*": "",
|
|
42
44
|
"run-init": "cd ../tt-gitops && terraform init -upgrade -backend-config=key=laxa1986 -reconfigure",
|
|
43
|
-
"run-status": "
|
|
44
|
-
"run-plan": "
|
|
45
|
-
"run-deploy": "
|
|
46
|
-
"run-delete": "
|
|
47
|
-
"run-destroy": "
|
|
45
|
+
"run-status": " npm run run -- status --cwd ../tt-gitops",
|
|
46
|
+
"run-plan": " AWS_PROFILE=ac-tt-dev-deployer npm run run -- plan --cwd ../tt-gitops -var=\"env_size=min\" -var-file=versions.tfvars",
|
|
47
|
+
"run-deploy": " AWS_PROFILE=ac-tt-dev-deployer npm run run -- deploy --cwd ../tt-gitops -var=\"env_size=min\" -var-file=versions.tfvars",
|
|
48
|
+
"run-delete": " npm run run -- delete --cwd ../tt-gitops",
|
|
49
|
+
"run-destroy": "AWS_PROFILE=ac-tt-dev-destroyer npm run run -- destroy --cwd ../tt-gitops -var=\"env_size=min\" -var-file=versions.tfvars",
|
|
48
50
|
"-": "",
|
|
49
|
-
"run-ephemeral-create": "
|
|
51
|
+
"run-ephemeral-create": " npm run run -- create-ephemeral --key test",
|
|
50
52
|
"run-ephemeral-init": "cd ../tt-gitops && terraform init -upgrade -backend-config=key=test -reconfigure",
|
|
51
|
-
"run-ephemeral-status": "
|
|
52
|
-
"run-ephemeral-deploy": "
|
|
53
|
-
"run-ephemeral-delete": "
|
|
54
|
-
"run-ephemeral-destroy": "
|
|
53
|
+
"run-ephemeral-status": " npm run run -- status --key test",
|
|
54
|
+
"run-ephemeral-deploy": " npm run run -- deploy --cwd ../tt-gitops -var-file=versions.tfvars",
|
|
55
|
+
"run-ephemeral-delete": " npm run run -- delete --cwd ../tt-gitops --force",
|
|
56
|
+
"run-ephemeral-destroy": "npm run run -- destroy --cwd ../tt-gitops --force -var-file=versions.tfvars"
|
|
55
57
|
},
|
|
56
58
|
"dependencies": {
|
|
57
59
|
"commander": "^14.0.0",
|
package/dist/client/CliHelper.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import inquirer from 'inquirer';
|
|
3
|
-
export class CliHelper {
|
|
4
|
-
constructor() { }
|
|
5
|
-
async promptYesNo(message, defaultValue = false) {
|
|
6
|
-
const { answer } = await inquirer.prompt([{
|
|
7
|
-
type: 'confirm',
|
|
8
|
-
name: 'answer',
|
|
9
|
-
message,
|
|
10
|
-
default: defaultValue,
|
|
11
|
-
}]);
|
|
12
|
-
return answer;
|
|
13
|
-
}
|
|
14
|
-
getKind(cwd) {
|
|
15
|
-
return getDirName(cwd);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
function getDirName(cwd) {
|
|
19
|
-
cwd = resolveCwd(cwd);
|
|
20
|
-
return path.basename(cwd);
|
|
21
|
-
}
|
|
22
|
-
function resolveCwd(cwd) {
|
|
23
|
-
if (!cwd) {
|
|
24
|
-
return process.cwd();
|
|
25
|
-
}
|
|
26
|
-
if (path.isAbsolute(cwd)) {
|
|
27
|
-
return cwd;
|
|
28
|
-
}
|
|
29
|
-
return path.resolve(process.cwd(), cwd);
|
|
30
|
-
}
|