@agilecustoms/envctl 0.30.2 → 0.32.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,8 +1,26 @@
1
- export class S3Backend {
1
+ import { KnownException } from '../exceptions.js';
2
+ import { TerraformBackend } from './TerraformBackend.js';
3
+ export class S3Backend extends TerraformBackend {
2
4
  getType() {
3
5
  return 's3';
4
6
  }
5
- getKey(config) {
6
- return config['key'];
7
+ getKey() {
8
+ const key = this.config['key'];
9
+ if (!key) {
10
+ throw new KnownException(`Terraform backend config does not contain 'key' attribute`);
11
+ }
12
+ return key;
13
+ }
14
+ validateAndGetStateFileContent() {
15
+ const allowedKeys = ['bucket', 'dynamodb_table', 'key', 'region', 'use_lockfile'];
16
+ const nonEmptyKeys = Object.keys(this.config).filter(key => this.config[key] !== null);
17
+ const invalidKeys = nonEmptyKeys.filter(key => !allowedKeys.includes(key));
18
+ if (invalidKeys.length > 0) {
19
+ throw new KnownException(`Terraform local state file ${this.stateFilePath}\n`
20
+ + `backend config contains not allowed keys: ${invalidKeys.join(', ')}\n`
21
+ + `Allowed keys are: ${allowedKeys.join(', ')}\n`
22
+ + `Contact support if you think your backend config is valid`);
23
+ }
24
+ return this.stateFileContent;
7
25
  }
8
26
  }
@@ -1 +1,10 @@
1
- export {};
1
+ export class TerraformBackend {
2
+ stateFilePath = '';
3
+ stateFileContent = '';
4
+ config = {};
5
+ setState(stateFilePath, stateFileContent, config) {
6
+ this.stateFilePath = stateFilePath;
7
+ this.stateFileContent = stateFileContent;
8
+ this.config = config;
9
+ }
10
+ }
@@ -11,6 +11,7 @@ export class TerraformAdapter {
11
11
  processRunner;
12
12
  cliHelper;
13
13
  backends;
14
+ backend = undefined;
14
15
  constructor(processRunner, cliHelper, backends) {
15
16
  this.processRunner = processRunner;
16
17
  this.cliHelper = cliHelper;
@@ -19,21 +20,6 @@ export class TerraformAdapter {
19
20
  return acc;
20
21
  }, new Map());
21
22
  }
22
- getStateFile(cwd) {
23
- const dir = cwd ?? process.cwd();
24
- const statePath = path.join(dir, '.terraform', 'terraform.tfstate');
25
- if (!fs.existsSync(statePath)) {
26
- throw new KnownException(`Terraform state file not found at: ${statePath}`);
27
- }
28
- let content;
29
- try {
30
- content = fs.readFileSync(statePath, 'utf8');
31
- }
32
- catch (err) {
33
- throw new KnownException(`Failed to read terraform state file: ${statePath}`, { cause: err });
34
- }
35
- return content;
36
- }
37
23
  getLockFile(cwd) {
38
24
  const dir = cwd ?? process.cwd();
39
25
  const lockPath = path.join(dir, '.terraform.lock.hcl');
@@ -47,11 +33,25 @@ export class TerraformAdapter {
47
33
  throw new KnownException(`Failed to read terraform lock file: ${lockPath}`, { cause: err });
48
34
  }
49
35
  }
50
- getKey(cwd) {
51
- const stateFile = this.getStateFile(cwd);
36
+ getTerraformBackend(cwd) {
37
+ if (this.backend) {
38
+ return this.backend;
39
+ }
40
+ const dir = cwd ?? process.cwd();
41
+ const statePath = path.join(dir, '.terraform', 'terraform.tfstate');
42
+ if (!fs.existsSync(statePath)) {
43
+ throw new KnownException(`Terraform state file not found at: ${statePath}`);
44
+ }
45
+ let stateFileContent;
46
+ try {
47
+ stateFileContent = fs.readFileSync(statePath, 'utf8');
48
+ }
49
+ catch (err) {
50
+ throw new KnownException(`Failed to read terraform state file: ${statePath}`, { cause: err });
51
+ }
52
52
  let tfstate;
53
53
  try {
54
- tfstate = JSON.parse(stateFile);
54
+ tfstate = JSON.parse(stateFileContent);
55
55
  }
56
56
  catch (err) {
57
57
  throw new KnownException(`Failed to parse terraform state file: .terraform/terraform.tfstate`, { cause: err });
@@ -63,12 +63,9 @@ export class TerraformAdapter {
63
63
  const supportedBackends = Array.from(this.backends.keys()).join(',');
64
64
  throw new KnownException(`Unsupported terraform backend type: ${type}. Supported types: ${supportedBackends}`);
65
65
  }
66
- const config = backendJson?.config;
67
- const key = backend.getKey(config);
68
- if (!key) {
69
- throw new KnownException(`Terraform backend config does not contain 'key' attribute`);
70
- }
71
- return key;
66
+ backend.setState(statePath, stateFileContent, backendJson.config);
67
+ this.backend = backend;
68
+ return backend;
72
69
  }
73
70
  async lockProviders(cwd) {
74
71
  console.log('Update lock file');
@@ -14,7 +14,7 @@ export class EnvCtl {
14
14
  getKey(key, cwd) {
15
15
  if (!key) {
16
16
  console.log('Key is not provided, inferring from .terraform/terraform.tfstate file');
17
- key = this.terraformAdapter.getKey(cwd);
17
+ key = this.terraformAdapter.getTerraformBackend(cwd).getKey();
18
18
  }
19
19
  return key;
20
20
  }
@@ -80,7 +80,7 @@ export class EnvCtl {
80
80
  throw new KnownException(`Env ${env.key} status is ${env.status}, can not run this command`);
81
81
  }
82
82
  async plan(tfArgs, cwd) {
83
- const key = this.terraformAdapter.getKey(cwd);
83
+ const key = this.terraformAdapter.getTerraformBackend(cwd).getKey();
84
84
  const userName = this.configService.getUserName();
85
85
  const env = await this.tryGetEnv(key);
86
86
  let vars = undefined;
@@ -105,19 +105,20 @@ export class EnvCtl {
105
105
  return await this.envApi.createEphemeral(createEnv);
106
106
  }
107
107
  async lockTerraform(env, cwd) {
108
+ console.log('validate terraform.tfstate file');
109
+ env.stateFile = this.terraformAdapter.getTerraformBackend(cwd).validateAndGetStateFileContent();
108
110
  await this.terraformAdapter.lockProviders(cwd);
109
- env.stateFile = this.terraformAdapter.getStateFile(cwd);
110
111
  env.lockFile = this.terraformAdapter.getLockFile(cwd);
111
112
  }
112
113
  async deploy(tfArgs, cwd) {
113
- const key = this.terraformAdapter.getKey(cwd);
114
+ const key = this.terraformAdapter.getTerraformBackend(cwd).getKey();
114
115
  let env = await this.tryGetEnv(key);
115
116
  if (env === null) {
116
117
  const kind = this.cliHelper.getKind(cwd);
117
- console.log('Creating env metadata');
118
118
  const userName = this.configService.getUserName();
119
119
  const createEnv = { key, owner: userName, kind };
120
120
  await this.lockTerraform(createEnv, cwd);
121
+ console.log('Creating env metadata');
121
122
  env = await this.envApi.create(createEnv);
122
123
  }
123
124
  else {
@@ -191,7 +192,7 @@ export class EnvCtl {
191
192
  console.log(message);
192
193
  }
193
194
  async destroy(tfArgs, force, cwd) {
194
- const key = this.terraformAdapter.getKey(cwd);
195
+ const key = this.terraformAdapter.getTerraformBackend(cwd).getKey();
195
196
  const env = await this.get(key);
196
197
  this.checkStatus(env);
197
198
  if (env.status === EnvStatus.Init) {
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": "0.30.2",
4
+ "version": "0.32.0",
5
5
  "author": "Alex Chekulaev",
6
6
  "type": "module",
7
7
  "bin": {