@agilecustoms/envctl 1.23.0 → 1.25.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.
@@ -62,6 +62,7 @@ export class EnvApiClient {
62
62
  env.status = EnvStatus.Active;
63
63
  }
64
64
  async lockForUpdate(env) {
65
+ logger.info('Lock for update');
65
66
  const body = {
66
67
  stateFile: env.stateFile,
67
68
  lockFile: env.lockFile
@@ -3,6 +3,7 @@ export { configure } from './configure.js';
3
3
  export { createEphemeral } from './createEphemeral.js';
4
4
  export { deleteIt } from './delete.js';
5
5
  export { destroy } from './destroy.js';
6
+ export { init } from './init.js';
6
7
  export { logs } from './logs.js';
7
8
  export { plan } from './plan.js';
8
9
  export { status } from './status.js';
@@ -0,0 +1,17 @@
1
+ import { Command } from 'commander';
2
+ import { ConfigService, EnvService } from '../service/index.js';
3
+ import { _keys } from './_keys.js';
4
+ import { wrap } from './_utils.js';
5
+ export function init(program, configService, envService) {
6
+ program
7
+ .command('init')
8
+ .description('High level wrapper for terraform init')
9
+ .option('--profile <profile>', _keys.PROFILE)
10
+ .allowUnknownOption(true)
11
+ .argument('[args...]')
12
+ .action(wrap(async (tfArgs, options) => {
13
+ const { profile } = options;
14
+ configService.init(profile, false);
15
+ await envService.init(tfArgs);
16
+ }));
17
+ }
package/dist/container.js CHANGED
@@ -15,7 +15,7 @@ export function buildContainer(appVersion) {
15
15
  const localStateService = new LocalStateService();
16
16
  const nonInteractive = !process.stdout.isTTY || process.env.CI === 'true';
17
17
  const terraformAdapter = new TerraformAdapter(Date.now, configService, cli, localStateService, backends, nonInteractive);
18
- const envService = new EnvService(configService, cli, envApiClient, terraformAdapter);
18
+ const envService = new EnvService(cli, envApiClient, terraformAdapter);
19
19
  const logService = new LogService(cli, envApiClient, terraformAdapter);
20
20
  return { configService, envService, logService };
21
21
  }
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from 'module';
3
3
  import { Command } from 'commander';
4
4
  import updateNotifier from 'update-notifier';
5
- import { configure, createEphemeral, deleteIt, apply, destroy, logs, plan, status } from './commands/index.js';
5
+ import { configure, createEphemeral, deleteIt, apply, destroy, logs, plan, status, init } from './commands/index.js';
6
6
  import { buildContainer } from './container.js';
7
7
  const require = createRequire(import.meta.url);
8
8
  const pkg = require('../package.json');
@@ -23,6 +23,7 @@ configure(program, configService);
23
23
  createEphemeral(program, configService, envService);
24
24
  deleteIt(program, configService, envService);
25
25
  destroy(program, configService, envService);
26
+ init(program, configService, envService);
26
27
  logs(program, configService, logService);
27
28
  plan(program, configService, envService);
28
29
  status(program, configService, envService);
@@ -7,7 +7,7 @@ export class BaseService {
7
7
  getKey(key) {
8
8
  if (!key) {
9
9
  logger.info('Key is not provided, inferring from state file');
10
- key = this.terraformAdapter.getTerraformBackend().getKey();
10
+ key = this.terraformAdapter.getKey();
11
11
  }
12
12
  return key;
13
13
  }
@@ -5,13 +5,12 @@ import { logger } from '../logger.js';
5
5
  import { EnvStatus } from '../model/index.js';
6
6
  import { toLocalTime } from '../util.js';
7
7
  import { BaseService } from './BaseService.js';
8
+ import { ApplyMode, getApplyMode } from './TerraformAdapter.js';
8
9
  export class EnvService extends BaseService {
9
- configService;
10
10
  cli;
11
11
  envApi;
12
- constructor(configService, cli, envApi, terraformAdapter) {
12
+ constructor(cli, envApi, terraformAdapter) {
13
13
  super(terraformAdapter);
14
- this.configService = configService;
15
14
  this.cli = cli;
16
15
  this.envApi = envApi;
17
16
  }
@@ -34,10 +33,7 @@ export class EnvService extends BaseService {
34
33
  }
35
34
  }
36
35
  }
37
- async tryGetEnv(key) {
38
- if (this.configService.getApiKey() === undefined) {
39
- return null;
40
- }
36
+ async getEnv(key) {
41
37
  logger.info(`Check if env ${key} already exists`);
42
38
  const env = await this.envApi.get(key);
43
39
  if (env) {
@@ -48,27 +44,20 @@ export class EnvService extends BaseService {
48
44
  }
49
45
  return env;
50
46
  }
47
+ async init(tfArgs) {
48
+ await this.terraformAdapter.init(tfArgs);
49
+ await this.terraformAdapter.lockProviders();
50
+ }
51
51
  async plan(tfArgs) {
52
- const key = this.terraformAdapter.getTerraformBackend().getKey();
53
- await this.lockTerraform(null, true);
52
+ this.terraformAdapter.validateAndGetStateFile();
53
+ const key = this.terraformAdapter.getKey();
54
+ this.terraformAdapter.getLockFile();
54
55
  await this.terraformAdapter.plan(key, tfArgs);
55
56
  }
56
57
  async createEphemeral(key) {
57
58
  const createEnv = { key };
58
59
  return await this.envApi.createEphemeral(createEnv);
59
60
  }
60
- async lockTerraform(env, newEnv) {
61
- logger.info('Lock providers');
62
- const res = await this.terraformAdapter.lock(newEnv);
63
- if (env !== null) {
64
- if (res.lockFile) {
65
- env.lockFile = res.lockFile;
66
- }
67
- if (res.stateFile) {
68
- env.stateFile = res.stateFile;
69
- }
70
- }
71
- }
72
61
  checkDir(env) {
73
62
  if (env.ephemeral)
74
63
  return;
@@ -80,16 +69,18 @@ export class EnvService extends BaseService {
80
69
  }
81
70
  }
82
71
  async apply(tfArgs) {
83
- const key = this.terraformAdapter.getTerraformBackend().getKey();
84
- let env = await this.tryGetEnv(key);
72
+ const { stateFile, stateUpdated } = this.terraformAdapter.validateAndGetStateFile();
73
+ const key = this.terraformAdapter.getKey();
74
+ const { lockFile, lockUpdated } = this.terraformAdapter.getLockFile();
75
+ const applyMode = getApplyMode(tfArgs);
76
+ let env = await this.getEnv(key);
85
77
  if (env === null) {
86
78
  const dir = this.cli.getDir();
87
- const createEnv = { key, dir };
88
- await this.lockTerraform(createEnv, true);
79
+ const createEnv = { key, dir, stateFile, lockFile };
89
80
  logger.info('Creating env metadata');
90
81
  env = await this.envApi.create(createEnv);
91
82
  await this.runApply(env, tfArgs);
92
- return;
83
+ return env;
93
84
  }
94
85
  this.checkDir(env);
95
86
  if (env.status === EnvStatus.Deleting) {
@@ -101,28 +92,41 @@ export class EnvService extends BaseService {
101
92
  Check logs with 'envctl logs', address the issue and re-run 'envctl delete ${env.key}'`);
102
93
  }
103
94
  const status = env.status;
104
- if (status === EnvStatus.Init || status === EnvStatus.Active) {
105
- const newEnv = status === EnvStatus.Init;
106
- await this.lockTerraform(env, newEnv);
107
- logger.info('Will lock for update and run terraform apply (to update resources)');
95
+ if (status === EnvStatus.Init) {
96
+ if (applyMode === ApplyMode.Default) {
97
+ throw new KnownException('Either run apply with planfile or with flag -auto-approve');
98
+ }
99
+ env.stateFile = stateFile;
100
+ env.lockFile = lockFile;
108
101
  await this.envApi.lockForUpdate(env);
109
102
  await this.runApply(env, tfArgs);
110
- return;
103
+ return env;
104
+ }
105
+ if (stateUpdated) {
106
+ env.stateFile = stateFile;
107
+ }
108
+ if (lockUpdated) {
109
+ env.lockFile = lockFile;
111
110
  }
112
- await this.runApply(env, tfArgs);
111
+ if (status === EnvStatus.Active || stateUpdated || lockUpdated) {
112
+ await this.envApi.lockForUpdate(env);
113
+ }
114
+ return await this.runApply(env, tfArgs);
113
115
  }
114
116
  async runApply(env, tfArgs) {
117
+ this.terraformAdapter.saveHashes();
115
118
  logger.info('Deploying resources');
116
119
  await this.terraformAdapter.apply(env, tfArgs);
117
120
  logger.info('Activating env (to finish creation)');
118
121
  await this.envApi.activate(env);
122
+ return env;
119
123
  }
120
124
  async delete(force, key) {
121
125
  key = this.getKey(key);
122
126
  await this.envApi.delete(key, force);
123
127
  }
124
128
  async destroy(tfArgs, force) {
125
- const key = this.terraformAdapter.getTerraformBackend().getKey();
129
+ const key = this.terraformAdapter.getKey();
126
130
  const env = await this.get(key);
127
131
  if (env.status === EnvStatus.Init) {
128
132
  logger.info(`Env ${env.key} status is INIT - no resources, nothing to destroy, just deleting metadata`);
@@ -16,6 +16,35 @@ export const RETRYABLE_ERRORS = [
16
16
  function hash(data) {
17
17
  return createHash('sha256').update(data).digest('hex');
18
18
  }
19
+ export var ApplyMode;
20
+ (function (ApplyMode) {
21
+ ApplyMode["Plan"] = "PLAN";
22
+ ApplyMode["AutoApprove"] = "AUTO_APPROVE";
23
+ ApplyMode["Default"] = "DEFAULT";
24
+ })(ApplyMode || (ApplyMode = {}));
25
+ export function getApplyMode(tfArgs) {
26
+ let hasAutoApprove = false;
27
+ for (let i = 0; i < tfArgs.length; i++) {
28
+ const arg = tfArgs[i];
29
+ if (arg === '-auto-approve') {
30
+ hasAutoApprove = true;
31
+ continue;
32
+ }
33
+ if (arg.startsWith('-')) {
34
+ if (!arg.includes('=')
35
+ && i + 1 < tfArgs.length
36
+ && !tfArgs[i + 1].startsWith('-')) {
37
+ i++;
38
+ }
39
+ continue;
40
+ }
41
+ return ApplyMode.Plan;
42
+ }
43
+ if (hasAutoApprove) {
44
+ return ApplyMode.AutoApprove;
45
+ }
46
+ return ApplyMode.Default;
47
+ }
19
48
  export class TerraformAdapter {
20
49
  now;
21
50
  configService;
@@ -35,19 +64,7 @@ export class TerraformAdapter {
35
64
  return acc;
36
65
  }, new Map());
37
66
  }
38
- getLockFile() {
39
- const lockPath = path.join(process.cwd(), '.terraform.lock.hcl');
40
- if (!fs.existsSync(lockPath)) {
41
- throw new KnownException(`Terraform lock file not found at: ${lockPath}`);
42
- }
43
- try {
44
- return fs.readFileSync(lockPath, 'utf8');
45
- }
46
- catch (err) {
47
- throw new KnownException(`Failed to read terraform lock file: ${lockPath}`, { cause: err });
48
- }
49
- }
50
- getTerraformBackend() {
67
+ getBackend() {
51
68
  if (this.backend) {
52
69
  return this.backend;
53
70
  }
@@ -80,37 +97,54 @@ export class TerraformAdapter {
80
97
  this.backend = backend;
81
98
  return backend;
82
99
  }
83
- async lock(newEnv) {
84
- const res = {};
85
- const stateFileContent = this.getTerraformBackend().validateAndGetStateFileContent();
86
- const stateHash = hash(stateFileContent);
100
+ getKey() {
101
+ return this.getBackend().getKey();
102
+ }
103
+ validateAndGetStateFile() {
104
+ const fileContent = this.getBackend().validateAndGetStateFileContent();
87
105
  const config = this.localStateService.load();
88
- if (config.stateHash !== stateHash || newEnv) {
89
- config.stateHash = stateHash;
90
- res.stateFile = stateFileContent;
106
+ const fileHash = hash(fileContent);
107
+ const stateUpdated = config.stateHash !== fileHash;
108
+ config.stateHash = fileHash;
109
+ return { stateFile: fileContent, stateUpdated };
110
+ }
111
+ _getLockFile() {
112
+ const lockPath = path.join(process.cwd(), '.terraform.lock.hcl');
113
+ if (!fs.existsSync(lockPath)) {
114
+ throw new KnownException(`Terraform lock file not found at: ${lockPath}`);
91
115
  }
92
- const linuxArm64 = process.platform === 'linux' && process.arch === 'arm64';
93
- if (!linuxArm64 || newEnv) {
94
- let lockFileContent = this.getLockFile();
95
- if (!linuxArm64) {
96
- const lockFileHash = hash(lockFileContent);
97
- if (config.lockHash !== lockFileHash) {
98
- await this.lockProviders();
99
- lockFileContent = this.getLockFile();
100
- config.lockHash = hash(lockFileContent);
101
- res.lockFile = lockFileContent;
102
- }
103
- }
104
- if (newEnv) {
105
- res.lockFile = lockFileContent;
106
- }
116
+ try {
117
+ return fs.readFileSync(lockPath, 'utf8');
118
+ }
119
+ catch (cause) {
120
+ throw new KnownException(`Failed to read terraform lock file: ${lockPath}`, { cause });
121
+ }
122
+ }
123
+ getLockFile() {
124
+ const fileContent = this._getLockFile();
125
+ const config = this.localStateService.load();
126
+ const fileHash = hash(fileContent);
127
+ if (fileHash !== config.lockFileHash) {
128
+ throw new KnownException(`Make sure you're using envctl init instead of terraform init`);
107
129
  }
130
+ const lockUpdated = config.lockHash !== fileHash;
131
+ config.lockHash = fileHash;
132
+ return { lockFile: fileContent, lockUpdated };
133
+ }
134
+ saveHashes() {
135
+ const config = this.localStateService.load();
108
136
  this.localStateService.save(config);
109
- return res;
110
137
  }
111
138
  async lockProviders() {
112
- logger.info('Update lock file');
113
- await this.cli.run('terraform', ['providers', 'lock', '-platform=linux_arm64']);
139
+ const linuxArm64 = process.platform === 'linux' && process.arch === 'arm64';
140
+ if (!linuxArm64) {
141
+ logger.info('Lock providers');
142
+ await this.cli.run('terraform', ['providers', 'lock', '-platform=linux_arm64']);
143
+ }
144
+ const fileContent = this._getLockFile();
145
+ const config = this.localStateService.load();
146
+ config.lockFileHash = hash(fileContent);
147
+ this.localStateService.save(config);
114
148
  }
115
149
  printTime() {
116
150
  const now = new Date();
@@ -179,6 +213,18 @@ export class TerraformAdapter {
179
213
  });
180
214
  return result;
181
215
  }
216
+ async init(args) {
217
+ logger.info('Running: terraform init ' + args.join(' ') + '\n');
218
+ try {
219
+ await this.cli.run('terraform', ['init', ...args], { interactive: true });
220
+ }
221
+ catch (error) {
222
+ if (!(error instanceof ProcessException)) {
223
+ throw error;
224
+ }
225
+ throw new KnownException(`terraform init failed with code ${error.code}:\n${error.message}`, { cause: error });
226
+ }
227
+ }
182
228
  async plan(key, args) {
183
229
  args = this.tfArgs(key, args);
184
230
  await this._plan(args, 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agilecustoms/envctl",
3
- "version": "1.23.0",
3
+ "version": "1.25.0",
4
4
  "description": "node.js CLI client for manage environments",
5
5
  "keywords": [
6
6
  "terraform wrapper",
@@ -44,7 +44,7 @@
44
44
  "-d1-": "",
45
45
  "d1-desc": "dev env (fully fledged ~/.envctl/default.json)",
46
46
  "d1": "CWD=../tt-core npm run it --",
47
- "d1-init": "cd ../tt-core && terraform init -upgrade -backend-config=key=d1 -reconfigure",
47
+ "d1-init": " npm run d1 -- init -reconfigure -upgrade -backend-config=key=d1",
48
48
  "d1-status": "npm run d1 -- status --verbose",
49
49
  "d1-apply": " npm run d1 -- apply --auto-approve -var=env_size=min",
50
50
  "d1-delete": "npm run d1 -- delete",
@@ -52,29 +52,37 @@
52
52
  "-d2-": "",
53
53
  "d2-desc": "dev env destroy",
54
54
  "d2": "CWD=../tt-core npm run it --",
55
- "d2-init": "cd ../tt-core && terraform init -upgrade -backend-config=key=d2 -reconfigure",
55
+ "d2-init": " npm run d2 -- init -reconfigure -upgrade -backend-config=key=d2",
56
56
  "d2-status": " npm run d2 -- status",
57
57
  "d2-apply": " npm run d2 -- apply --auto-approve",
58
58
  "d2-destroy": "npm run d2 -- destroy -var=env_size=min --auto-approve",
59
59
  "-d3-": "",
60
60
  "d3-desc": "dev env plan apply",
61
61
  "d3": "CWD=../tt-core npm run it --",
62
- "d3-init": "cd ../tt-core && terraform init -upgrade -backend-config=key=d3 -reconfigure",
62
+ "d3-init": " npm run d3 -- init -reconfigure -upgrade -backend-config=key=d3",
63
63
  "d3-plan": " npm run d3 -- plan -var=env_size=min -var=env=d3 -out=plan",
64
64
  "d3-apply": " npm run d3 -- apply plan",
65
65
  "d3-delete": "npm run d3 -- delete",
66
+ "-d4-": "",
67
+ "d4-desc": "terraform init and then envctl apply",
68
+ "d4": "CWD=../tt-core npm run it --",
69
+ "d4-init": "cd ../tt-core && rm .terraform.lock.hcl && rm .terraform/envctl.json && terraform init -reconfigure -upgrade -backend-config=key=d4",
70
+ "d4-init1": " npm run d4 -- init -reconfigure -upgrade -backend-config=key=d4",
71
+ "d4-apply": " npm run d4 -- apply -auto-approve -var=env_size=min",
72
+ "d4-delete": "npm run d4 -- delete",
73
+ "d4-status": "npm run d4 -- status",
66
74
  "-e1-": "",
67
75
  "e1-desc": "ephemeral env in CI (must have ENVCTL_API_KEY_ in env vars)",
68
76
  "e1": "CWD=../tt-core CI=true ENVCTL_HOME=non-existing ENVCTL_API_KEY=\"$ENVCTL_API_KEY_\" npm run it --",
69
77
  "e1-create": "npm run e1 -- create-ephemeral e1",
70
- "e1-init": "cd ../tt-core && terraform init -upgrade -backend-config=key=e1 -reconfigure",
78
+ "e1-init": " npm run e1 -- init -reconfigure -upgrade -backend-config=key=e1",
71
79
  "e1-plan": " npm run e1 -- plan -var=env_size=min -var=env=e1 -out=plan",
72
80
  "e1-apply": " npm run e1 -- apply plan",
73
81
  "e1-delete": "npm run e1 -- delete",
74
82
  "-tt-": "",
75
83
  "tt-desc": "deploy TT project from local machine",
76
84
  "tt": "CWD=../tt-gitops AWS_PROFILE=ac-tt-dev-deployer npm run it --",
77
- "run-init": "cd ../tt-gitops && terraform init -upgrade -backend-config=key=laxa1986 -reconfigure",
85
+ "run-init": " npm run tt -- init -reconfigure -upgrade -backend-config=key=laxa1986",
78
86
  "run-status": " npm run tt -- status",
79
87
  "run-apply": " npm run tt -- apply --auto-approve -var=env_size=min -var-file=versions.tfvars",
80
88
  "run-delete": " npm run tt -- delete"