@agilecustoms/envctl 1.3.1 → 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.
@@ -9,27 +9,11 @@ import path from 'path';
9
9
  import * as readline from 'readline';
10
10
  import inquirer from 'inquirer';
11
11
  import { ProcessException } from '../exceptions.js';
12
- function getDirName(cwd) {
13
- cwd = resolveCwd(cwd);
14
- return path.basename(cwd);
15
- }
16
- function resolveCwd(cwd) {
17
- if (!cwd) {
18
- return process.cwd();
19
- }
20
- if (path.isAbsolute(cwd)) {
21
- return cwd;
22
- }
23
- return path.resolve(process.cwd(), cwd);
24
- }
25
12
  export class Cli {
26
- async run(command, args, cwd, out_scanner, in_scanner) {
13
+ async run(command, args, out_scanner, in_scanner) {
27
14
  const spawnOptions = {
28
15
  stdio: ['pipe', 'pipe', 'pipe'],
29
16
  };
30
- if (cwd) {
31
- spawnOptions.cwd = cwd;
32
- }
33
17
  const child = spawn(command, args, spawnOptions);
34
18
  child.stdout.setEncoding('utf8');
35
19
  child.stderr.setEncoding('utf8');
@@ -98,8 +82,8 @@ export class Cli {
98
82
  }]);
99
83
  return answer;
100
84
  }
101
- getKind(cwd) {
102
- return getDirName(cwd);
85
+ getKind() {
86
+ return path.basename(process.cwd());
103
87
  }
104
88
  async writeInTmpFile(stream, fileName) {
105
89
  const outDir = join(tmpdir(), 'envctl');
@@ -46,18 +46,6 @@ export class EnvApiClient {
46
46
  }
47
47
  return { ...env, ephemeral: false, status: EnvStatus.Deploying, ttl: result.ttl };
48
48
  }
49
- async setVars(env, vars) {
50
- try {
51
- await this.httpClient.post(`/ci/env/${env.key}/vars`, vars);
52
- }
53
- catch (error) {
54
- if (error instanceof BusinessException) {
55
- throw new KnownException(`Failed to set vars for environment ${env.key}: ${error.message}`);
56
- }
57
- throw error;
58
- }
59
- env.vars = { ...env.vars || {}, ...vars };
60
- }
61
49
  async activate(env) {
62
50
  try {
63
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(cwd) {
30
- const dir = cwd ?? process.cwd();
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(cwd) {
41
+ getTerraformBackend() {
43
42
  if (this.backend) {
44
43
  return this.backend;
45
44
  }
46
- const dir = cwd ?? process.cwd();
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, cwd) {
74
+ async lock(newEnv) {
77
75
  const res = {};
78
- const stateFileContent = this.getTerraformBackend(cwd).validateAndGetStateFileContent();
76
+ const stateFileContent = this.getTerraformBackend().validateAndGetStateFileContent();
79
77
  const stateHash = hash(stateFileContent);
80
- const config = this.localStateService.load(cwd);
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(cwd);
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(cwd);
92
- lockFileContent = this.getLockFile(cwd);
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, cwd);
99
+ this.localStateService.save(config);
102
100
  return res;
103
101
  }
104
- async lockProviders(cwd) {
102
+ async lockProviders() {
105
103
  console.log('Update lock file');
106
- await this.cli.run('terraform', ['providers', 'lock', '-platform=linux_arm64'], cwd);
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, cwd) {
117
- const varDefs = this.getVarDefs(cwd);
118
- const argVars = this.getArgVars(env.args);
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(env.vars || {}).forEach(([key, value]) => {
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, ...env.args];
129
+ return [...extraArgs, ...args];
132
130
  }
133
- getVarDefs(cwd) {
134
- const dir = cwd ?? process.cwd();
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(envTerraform, onDemandVars, cwd) {
174
- const varDefs = this.getVarDefs(cwd);
175
- const argVars = this.getArgVars(envTerraform.args);
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, cwd, attemptNo = 1) {
194
- const args = this.tfArgs(env, cwd);
195
- console.log('Running: terraform plan -auto-approve', ...args, '\n');
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 apply(env, onDemandVars, cwd, attemptNo = 1) {
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
- const args = this.tfArgs(env, cwd);
236
- console.log('Running: terraform apply -auto-approve', ...args, '\n');
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', '-auto-approve', ...args], cwd, out_scanner, in_scanner);
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(env, onDemandVars, cwd, attemptNo + 1);
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, cwd);
253
+ await this.promptUnlock(lockId);
253
254
  console.info('State unlocked, retrying terraform apply');
254
- return this.apply(env, onDemandVars, cwd, attemptNo + 1);
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, force, cwd, attemptNo = 1) {
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], cwd, scanner);
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, cwd);
282
+ await this.forceUnlock(lockId);
279
283
  }
280
284
  else {
281
- await this.promptUnlock(lockId, cwd);
285
+ await this.promptUnlock(lockId);
282
286
  }
283
287
  console.info('State unlocked, retrying terraform destroy');
284
- return this.destroy(env, force, cwd, attemptNo + 1);
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, cwd) {
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, cwd);
312
+ await this.forceUnlock(id);
309
313
  }
310
- async forceUnlock(id, cwd) {
314
+ async forceUnlock(id) {
311
315
  console.log('Force unlocking state');
312
- await this.cli.run('terraform', ['force-unlock', '-force', id], cwd);
316
+ await this.cli.run('terraform', ['force-unlock', '-force', id]);
313
317
  }
314
318
  }
@@ -13,5 +13,7 @@ export function deleteIt(program) {
13
13
  }
14
14
  async function handler(options) {
15
15
  const { key, force, cwd } = options;
16
- await envService.delete(Boolean(force), key, cwd);
16
+ if (cwd)
17
+ process.chdir(cwd);
18
+ await envService.delete(Boolean(force), key);
17
19
  }
@@ -13,5 +13,7 @@ export function deploy(program) {
13
13
  }
14
14
  async function handler(tfArgs, options) {
15
15
  const { cwd } = options;
16
- await envService.deploy(tfArgs, cwd);
16
+ if (cwd)
17
+ process.chdir(cwd);
18
+ await envService.deploy(tfArgs);
17
19
  }
@@ -17,5 +17,7 @@ export function destroy(program) {
17
17
  }
18
18
  async function handler(tfArgs, options) {
19
19
  const { force, cwd } = options;
20
- await envService.destroy(tfArgs, Boolean(force), cwd);
20
+ if (cwd)
21
+ process.chdir(cwd);
22
+ await envService.destroy(tfArgs, Boolean(force));
21
23
  }
@@ -12,5 +12,7 @@ export function logs(program) {
12
12
  }
13
13
  async function handler(options) {
14
14
  const { key, cwd } = options;
15
- await logService.getLogs(key, cwd);
15
+ if (cwd)
16
+ process.chdir(cwd);
17
+ await logService.getLogs(key);
16
18
  }
@@ -14,5 +14,7 @@ export function plan(program) {
14
14
  }
15
15
  async function handler(tfArgs, options) {
16
16
  const { cwd } = options;
17
- await envService.plan(tfArgs, cwd);
17
+ if (cwd)
18
+ process.chdir(cwd);
19
+ await envService.plan(tfArgs);
18
20
  }
@@ -12,5 +12,7 @@ export function status(program) {
12
12
  }
13
13
  async function handler(options) {
14
14
  const { key, cwd } = options;
15
- await envService.status(key, cwd);
15
+ if (cwd)
16
+ process.chdir(cwd);
17
+ await envService.status(key);
16
18
  }
@@ -4,10 +4,10 @@ export class BaseService {
4
4
  constructor(terraformAdapter) {
5
5
  this.terraformAdapter = terraformAdapter;
6
6
  }
7
- getKey(key, cwd) {
7
+ getKey(key) {
8
8
  if (!key) {
9
9
  console.log('Key is not provided, inferring from state file');
10
- key = this.terraformAdapter.getTerraformBackend(cwd).getKey();
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, cwd) {
15
- key = this.getKey(key, cwd);
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(cwd);
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, cwd) {
53
+ checkKind(env) {
54
54
  if (env.ephemeral)
55
55
  return;
56
56
  if (env.kind) {
57
- const kind = this.cli.getKind(cwd);
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, cwd) {
64
- const key = this.terraformAdapter.getTerraformBackend(cwd).getKey();
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, cwd);
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
- const envTerraform = { key, ephemeral: false, args: tfArgs, vars };
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, cwd, newEnv = false) {
82
- console.log('Validate terraform.tfstate file');
83
- const res = await this.terraformAdapter.lock(newEnv, cwd);
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 deploy(tfArgs, cwd) {
92
- const key = this.terraformAdapter.getTerraformBackend(cwd).getKey();
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(cwd);
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, cwd, true);
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, key, tfArgs, cwd);
107
+ await this.runDeploy(env, tfArgs);
102
108
  return;
103
109
  }
104
- this.checkKind(env, cwd);
105
- switch (env.status) {
106
- case EnvStatus.Init:
107
- case EnvStatus.Active:
108
- if (env.status == EnvStatus.Active) {
109
- console.log('Env status is ACTIVE\nWill lock for update and run terraform apply (to update resources)');
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
- if (!answerYes) {
119
- console.log('Aborting deployment');
120
- return;
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
- await this.runDeploy(env, key, tfArgs, cwd);
127
- }
128
- async runDeploy(env, key, tfArgs, cwd) {
129
- console.log('Deploying resources');
130
- const { ephemeral, vars } = env;
131
- const envTerraform = { key, ephemeral, args: tfArgs, vars };
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, cwd) {
148
- key = this.getKey(key, cwd);
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, cwd) {
152
- const key = this.terraformAdapter.getTerraformBackend(cwd).getKey();
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
- const { ephemeral, vars } = env;
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(cwd) {
7
- cwd = cwd || process.cwd();
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(cwd) {
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(cwd);
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, cwd) {
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(cwd), ...config };
38
+ const mergedConfig = { ...this.load(), ...config };
40
39
  const data = JSON.stringify(mergedConfig, null, 2);
41
- const filePath = this.filePath(cwd);
40
+ const filePath = this.filePath();
42
41
  fs.writeFileSync(filePath, data);
43
42
  }
44
43
  }
@@ -10,8 +10,8 @@ export class LogService extends BaseService {
10
10
  this.cli = cli;
11
11
  this.envApi = envApi;
12
12
  }
13
- async getLogs(key, cwd) {
14
- key = this.getKey(key, cwd);
13
+ async getLogs(key) {
14
+ key = this.getKey(key);
15
15
  const url = await this.envApi.getLogs(key);
16
16
  const urlPath = new URL(url).pathname;
17
17
  const gzName = basename(urlPath);
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.3.1",
4
+ "version": "1.4.0",
5
5
  "author": "Alex Chekulaev",
6
6
  "type": "module",
7
7
  "engines": {