@agilecustoms/envctl 0.36.2 → 0.37.1

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/container.js CHANGED
@@ -3,6 +3,7 @@ import { AwsCredsHelper } from './client/AwsCredsHelper.js';
3
3
  import { CliHelper, EnvApiClient, HttpClient, TerraformAdapter } from './client/index.js';
4
4
  import { ProcessRunner } from './client/ProcessRunner.js';
5
5
  import { ConfigService, EnvService } from './service/index.js';
6
+ import { LocalStateService } from './service/LocalStateService.js';
6
7
  import { LogService } from './service/LogService.js';
7
8
  const cliHelper = new CliHelper();
8
9
  const configService = new ConfigService();
@@ -14,6 +15,7 @@ const httpClient = new HttpClient(awsCredsHelper);
14
15
  const envApiClient = new EnvApiClient(httpClient);
15
16
  const processRunner = new ProcessRunner();
16
17
  const terraformAdapter = new TerraformAdapter(processRunner, cliHelper, backends);
17
- const envService = new EnvService(cliHelper, envApiClient, terraformAdapter);
18
+ const localStateService = new LocalStateService();
19
+ const envService = new EnvService(cliHelper, envApiClient, terraformAdapter, localStateService);
18
20
  const logService = new LogService(envApiClient, terraformAdapter, processRunner);
19
21
  export { awsCredsHelper, configService, envService, logService };
@@ -1,13 +1,17 @@
1
+ import { CliHelper, EnvApiClient, TerraformAdapter } from '../client/index.js';
1
2
  import { KnownException } from '../exceptions.js';
2
- import { EnvStatus, } from '../model/index.js';
3
+ import { EnvStatus } from '../model/index.js';
4
+ import { LocalStateService } from './LocalStateService.js';
3
5
  export class EnvService {
4
6
  cliHelper;
5
7
  envApi;
6
8
  terraformAdapter;
7
- constructor(cliHelper, envApi, terraformAdapter) {
9
+ localStateService;
10
+ constructor(cliHelper, envApi, terraformAdapter, localStateService) {
8
11
  this.cliHelper = cliHelper;
9
12
  this.envApi = envApi;
10
13
  this.terraformAdapter = terraformAdapter;
14
+ this.localStateService = localStateService;
11
15
  }
12
16
  async status(key, cwd) {
13
17
  key = this.terraformAdapter.getKey(key, cwd);
@@ -81,15 +85,32 @@ export class EnvService {
81
85
  return await this.envApi.createEphemeral(createEnv);
82
86
  }
83
87
  async lockTerraform(env, cwd) {
88
+ const config = this.localStateService.load(cwd);
84
89
  console.log('validate terraform.tfstate file');
85
- env.stateFile = this.terraformAdapter.getTerraformBackend(cwd).validateAndGetStateFileContent();
86
- await this.terraformAdapter.lockProviders(cwd);
87
- env.lockFile = this.terraformAdapter.getLockFile(cwd);
90
+ const stateFileContent = this.terraformAdapter.getTerraformBackend(cwd).validateAndGetStateFileContent();
91
+ const stateHash = this.localStateService.hash(stateFileContent);
92
+ if (config.stateHash !== stateHash) {
93
+ config.stateHash = stateHash;
94
+ env.stateFile = stateFileContent;
95
+ }
96
+ const linuxArm64 = process.platform === 'linux' && process.arch === 'arm64';
97
+ if (!linuxArm64) {
98
+ let lockFileContent = this.terraformAdapter.getLockFile(cwd);
99
+ const lockFileHash = this.localStateService.hash(lockFileContent);
100
+ if (config.lockHash !== lockFileHash) {
101
+ await this.terraformAdapter.lockProviders(cwd);
102
+ lockFileContent = this.terraformAdapter.getLockFile(cwd);
103
+ config.lockHash = this.localStateService.hash(lockFileContent);
104
+ env.lockFile = lockFileContent;
105
+ }
106
+ }
107
+ this.localStateService.save(config, cwd);
88
108
  }
89
109
  async deploy(tfArgs, cwd) {
90
110
  const key = this.terraformAdapter.getTerraformBackend(cwd).getKey();
91
111
  let env = await this.tryGetEnv(key);
92
112
  if (env === null) {
113
+ this.localStateService.init(cwd);
93
114
  const kind = this.cliHelper.getKind(cwd);
94
115
  console.log(`Inferred kind from directory name: ${kind}`);
95
116
  const createEnv = { key, kind };
@@ -102,6 +123,10 @@ export class EnvService {
102
123
  this.checkKind(env, cwd);
103
124
  switch (env.status) {
104
125
  case EnvStatus.Init:
126
+ this.localStateService.init(cwd);
127
+ await this.lockTerraform(env, cwd);
128
+ await this.envApi.lockForUpdate(env);
129
+ break;
105
130
  case EnvStatus.Active:
106
131
  if (env.status === EnvStatus.Active) {
107
132
  console.log('Env status is ACTIVE\nWill lock for update and run terraform apply (to update resources)');
@@ -0,0 +1,59 @@
1
+ import { createHash } from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import path from 'path';
4
+ const CURRENT_VERSION = 1;
5
+ export class LocalStateService {
6
+ config;
7
+ constructor() {
8
+ }
9
+ filePath(cwd) {
10
+ cwd = cwd || process.cwd();
11
+ return path.join(cwd, '.terraform', 'envctl.json');
12
+ }
13
+ hash(data) {
14
+ return createHash('sha256').update(data).digest('hex');
15
+ }
16
+ createEmptyConfig(filePath) {
17
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
18
+ this.config = { version: CURRENT_VERSION };
19
+ fs.writeFileSync(filePath, JSON.stringify(this.config, null, 2));
20
+ return this.config;
21
+ }
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
+ load(cwd) {
38
+ if (this.config)
39
+ return this.config;
40
+ console.log('load local state config');
41
+ const filePath = this.filePath(cwd);
42
+ if (!fs.existsSync(filePath)) {
43
+ console.warn('local state file does not exist, must have been accidentally deleted, re-initializing');
44
+ return this.createEmptyConfig(filePath);
45
+ }
46
+ const data = fs.readFileSync(filePath, 'utf-8');
47
+ this.config = JSON.parse(data);
48
+ return this.config;
49
+ }
50
+ save(config, cwd) {
51
+ if (!this.config) {
52
+ throw new Error('call init or load first');
53
+ }
54
+ const mergedConfig = { ...this.load(cwd), ...config };
55
+ const data = JSON.stringify(mergedConfig, null, 2);
56
+ const filePath = this.filePath(cwd);
57
+ fs.writeFileSync(filePath, data);
58
+ }
59
+ }
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.36.2",
4
+ "version": "0.37.1",
5
5
  "author": "Alex Chekulaev",
6
6
  "type": "module",
7
7
  "engines": {
@@ -32,7 +32,7 @@
32
32
  "run-version": " tsc --sourceMap true && npm run run -- --version",
33
33
  "run-configure": "tsc --sourceMap true && npm run run -- configure",
34
34
  "~": "",
35
- "run-core-init": "cd ../tt-core && terraform init -upgrade -backend-config=key=laxa1986",
35
+ "run-core-init": "cd ../tt-core && terraform init -upgrade -backend-config=key=laxa1986 -reconfigure",
36
36
  "run-core-status": " tsc --sourceMap true && npm run run -- status --cwd ../tt-core",
37
37
  "run-core-plan": " tsc --sourceMap true && npm run run -- plan --cwd ../tt-core",
38
38
  "run-core-deploy": " tsc --sourceMap true && npm run run -- deploy --cwd ../tt-core -var=\"env_size=min\"",