@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.
- package/dist/client/EnvApiClient.js +1 -0
- package/dist/commands/index.js +1 -0
- package/dist/commands/init.js +17 -0
- package/dist/container.js +1 -1
- package/dist/index.js +2 -1
- package/dist/service/BaseService.js +1 -1
- package/dist/service/EnvService.js +37 -33
- package/dist/service/TerraformAdapter.js +84 -38
- package/package.json +14 -6
package/dist/commands/index.js
CHANGED
|
@@ -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(
|
|
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);
|
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
84
|
-
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
100
|
+
getKey() {
|
|
101
|
+
return this.getBackend().getKey();
|
|
102
|
+
}
|
|
103
|
+
validateAndGetStateFile() {
|
|
104
|
+
const fileContent = this.getBackend().validateAndGetStateFileContent();
|
|
87
105
|
const config = this.localStateService.load();
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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.
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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"
|