@agilecustoms/envctl 1.1.1 → 1.2.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.
@@ -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
- export class ProcessRunner {
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,4 +1,4 @@
1
- import { KnownException, NotFoundException } from '../exceptions.js';
1
+ import { BusinessException, KnownException, NotFoundException } from '../exceptions.js';
2
2
  import { EnvStatus } from '../model/index.js';
3
3
  import { HttpClient } from './HttpClient.js';
4
4
  export class EnvApiClient {
@@ -18,19 +18,53 @@ export class EnvApiClient {
18
18
  }
19
19
  }
20
20
  async createEphemeral(env) {
21
- const { token } = await this.httpClient.post('/ci/env-ephemeral', env);
22
- return token;
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 result.token;
23
32
  }
24
33
  async create(env) {
25
- const { ttl } = await this.httpClient.post('/ci/env', env);
26
- return { ...env, ephemeral: false, status: EnvStatus.Deploying, ttl };
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
- await this.httpClient.post(`/ci/env/${env.key}/vars`, 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
+ }
30
56
  env.vars = { ...env.vars || {}, ...vars };
31
57
  }
32
58
  async activate(env) {
33
- await this.httpClient.post(`/ci/env/${env.key}/activate`);
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
- const { status } = await this.httpClient.post(`/ci/env/${env.key}/lock-for-update`, body);
42
- env.status = status;
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(env) {
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/${env.key}`, {
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 ${env.key} is not found`);
97
+ throw new KnownException(`Environment ${key} is not found`);
55
98
  }
56
99
  throw error;
57
100
  }
58
- env.status = EnvStatus.Deleting;
59
- console.log(result.statusText);
101
+ console.log(result.message);
60
102
  }
61
103
  async getLogs(key) {
62
104
  let result;
@@ -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
- processRunner;
13
- cliHelper;
17
+ cli;
18
+ localStateService;
14
19
  backends;
15
20
  backend = undefined;
16
- constructor(processRunner, cliHelper, backends) {
17
- this.processRunner = processRunner;
18
- this.cliHelper = cliHelper;
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.processRunner.run('terraform', ['providers', 'lock', '-platform=linux_arm64'], cwd);
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.processRunner.run('terraform', ['plan', ...args], cwd);
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.processRunner.run('terraform', ['apply', '-auto-approve', ...args], cwd, out_scanner, in_scanner);
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.processRunner.run('terraform', ['destroy', '-auto-approve', ...args], cwd, scanner);
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.cliHelper.promptYesNo('Terraform state is locked. Most often due to previously interrupted process.\n'
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.processRunner.run('terraform', ['force-unlock', '-force', id], cwd);
312
+ await this.cli.run('terraform', ['force-unlock', '-force', id], cwd);
287
313
  }
288
314
  }
@@ -1,4 +1,3 @@
1
- export { CliHelper } from './CliHelper.js';
2
1
  export { EnvApiClient } from './EnvApiClient.js';
3
2
  export { HttpClient } from './HttpClient.js';
4
3
  export { TerraformAdapter } from './TerraformAdapter.js';
@@ -1,5 +1,5 @@
1
1
  import { ExitPromptError } from '@inquirer/core';
2
- import { BusinessException, AbortedException, KnownException } from '../exceptions.js';
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 || error instanceof BusinessException) {
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 { CliHelper, EnvApiClient, HttpClient, TerraformAdapter } from './client/index.js';
3
- import { ProcessRunner } from './client/ProcessRunner.js';
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 cliHelper = new CliHelper();
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 envService = new EnvService(cliHelper, envApiClient, terraformAdapter, localStateService);
18
- const logService = new LogService(envApiClient, terraformAdapter, processRunner);
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 { CliHelper, EnvApiClient, TerraformAdapter } from '../client/index.js';
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 { LocalStateService } from './LocalStateService.js';
5
- export class EnvService {
6
- cliHelper;
5
+ import { BaseService } from './BaseService.js';
6
+ export class EnvService extends BaseService {
7
+ cli;
7
8
  envApi;
8
- terraformAdapter;
9
- localStateService;
10
- constructor(cliHelper, envApi, terraformAdapter, localStateService) {
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.terraformAdapter.getKey(key, cwd);
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.cliHelper.getKind(cwd);
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.cliHelper.getKind(cwd);
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 stateFileContent = this.terraformAdapter.getTerraformBackend(cwd).validateAndGetStateFileContent();
91
- const stateHash = this.localStateService.hash(stateFileContent);
92
- if (config.stateHash !== stateHash || newEnv) {
93
- config.stateHash = stateHash;
94
- env.stateFile = stateFileContent;
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.localStateService.init(cwd);
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 === EnvStatus.Active) {
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.cliHelper.promptYesNo(`Env status is ${env.status}, likely due to an error from a previous run\n
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.terraformAdapter.getKey(key, cwd);
177
- const env = await this.get(key);
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(env);
156
+ await this.envApi.delete(key, force);
207
157
  return;
208
158
  }
209
- if (!force) {
210
- if (env.status === EnvStatus.Deleting) {
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(env);
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} does not exist`);
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
- this.config = JSON.parse(data);
48
- return this.config;
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
- export class LogService {
9
+ import { BaseService } from './BaseService.js';
10
+ export class LogService extends BaseService {
11
+ cli;
10
12
  envApi;
11
- terraformAdapter;
12
- processRunner;
13
- constructor(envApi, terraformAdapter, processRunner) {
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.terraformAdapter.getKey(key, cwd);
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.processRunner.run('open', ['-R', outPath]);
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.1.1",
4
+ "version": "1.2.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": " tsc --sourceMap true && npm run run -- --version",
32
- "run-configure": "tsc --sourceMap true && npm run 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-status": " tsc --sourceMap true && npm run run -- status --cwd ../tt-core",
36
- "run-core-plan": " tsc --sourceMap true && npm run run -- plan --cwd ../tt-core",
37
- "run-core-deploy": " tsc --sourceMap true && npm run run -- deploy --cwd ../tt-core -var=\"env_size=min\"",
38
- "run-core-delete": " tsc --sourceMap true && npm run run -- delete --cwd ../tt-core",
39
- "run-core-destroy": "tsc --sourceMap true && npm run run -- destroy --cwd ../tt-core",
40
- "run-core-logs": " tsc --sourceMap true && npm run run -- logs --cwd ../tt-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": " tsc --sourceMap true && npm run run -- status --cwd ../tt-gitops",
44
- "run-plan": " tsc --sourceMap true && AWS_PROFILE=ac-tt-dev-deployer npm run run -- plan --cwd ../tt-gitops -var=\"env_size=min\" -var-file=versions.tfvars",
45
- "run-deploy": " tsc --sourceMap true && AWS_PROFILE=ac-tt-dev-deployer npm run run -- deploy --cwd ../tt-gitops -var=\"env_size=min\" -var-file=versions.tfvars",
46
- "run-delete": " tsc --sourceMap true && npm run run -- delete --cwd ../tt-gitops",
47
- "run-destroy": "tsc --sourceMap true && AWS_PROFILE=ac-tt-dev-destroyer npm run run -- destroy --cwd ../tt-gitops -var=\"env_size=min\" -var-file=versions.tfvars",
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": " tsc --sourceMap true && npm run run -- create-ephemeral --key test",
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": " tsc --sourceMap true && npm run run -- status --key test",
52
- "run-ephemeral-deploy": " tsc --sourceMap true && npm run run -- deploy --cwd ../tt-gitops -var-file=versions.tfvars",
53
- "run-ephemeral-delete": " tsc --sourceMap true && npm run run -- delete --cwd ../tt-gitops --force",
54
- "run-ephemeral-destroy": "tsc --sourceMap true && npm run run -- destroy --cwd ../tt-gitops --force -var-file=versions.tfvars"
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",
@@ -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
- }