@agilecustoms/envctl 1.22.0 → 1.24.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/client/HttpClient.js +13 -2
- package/dist/commands/_utils.js +1 -1
- 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 +36 -33
- package/dist/service/TerraformAdapter.js +75 -38
- package/package.json +16 -8
|
@@ -64,7 +64,18 @@ export class HttpClient {
|
|
|
64
64
|
throw new Error('Error (network?) making the request:', { cause: error });
|
|
65
65
|
}
|
|
66
66
|
const contentType = response.headers?.get('Content-Type') || '';
|
|
67
|
-
|
|
67
|
+
let body = await response.text();
|
|
68
|
+
if (contentType.includes('application/json')) {
|
|
69
|
+
try {
|
|
70
|
+
body = JSON.parse(body);
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
if (e instanceof SyntaxError) {
|
|
74
|
+
throw new Error(`Malformed json response: ${body}`, { cause: e });
|
|
75
|
+
}
|
|
76
|
+
throw new Error('Unexpected error when parsing JSON response', { cause: e });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
68
79
|
if (response.ok) {
|
|
69
80
|
return body;
|
|
70
81
|
}
|
|
@@ -72,7 +83,7 @@ export class HttpClient {
|
|
|
72
83
|
throw new NotFoundException(body);
|
|
73
84
|
}
|
|
74
85
|
if (response.status === 422) {
|
|
75
|
-
throw new BusinessException(body);
|
|
86
|
+
throw new BusinessException(body.message);
|
|
76
87
|
}
|
|
77
88
|
if (response.status === 426) {
|
|
78
89
|
throw new KnownException(body.message);
|
package/dist/commands/_utils.js
CHANGED
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,19 @@ 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
54
|
await this.terraformAdapter.plan(key, tfArgs);
|
|
55
55
|
}
|
|
56
56
|
async createEphemeral(key) {
|
|
57
57
|
const createEnv = { key };
|
|
58
58
|
return await this.envApi.createEphemeral(createEnv);
|
|
59
59
|
}
|
|
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
60
|
checkDir(env) {
|
|
73
61
|
if (env.ephemeral)
|
|
74
62
|
return;
|
|
@@ -80,16 +68,18 @@ export class EnvService extends BaseService {
|
|
|
80
68
|
}
|
|
81
69
|
}
|
|
82
70
|
async apply(tfArgs) {
|
|
83
|
-
const
|
|
84
|
-
|
|
71
|
+
const { stateFile, stateUpdated } = this.terraformAdapter.validateAndGetStateFile();
|
|
72
|
+
const key = this.terraformAdapter.getKey();
|
|
73
|
+
const { lockFile, lockUpdated } = this.terraformAdapter.getLockFile();
|
|
74
|
+
const applyMode = getApplyMode(tfArgs);
|
|
75
|
+
let env = await this.getEnv(key);
|
|
85
76
|
if (env === null) {
|
|
86
77
|
const dir = this.cli.getDir();
|
|
87
|
-
const createEnv = { key, dir };
|
|
88
|
-
await this.lockTerraform(createEnv, true);
|
|
78
|
+
const createEnv = { key, dir, stateFile, lockFile };
|
|
89
79
|
logger.info('Creating env metadata');
|
|
90
80
|
env = await this.envApi.create(createEnv);
|
|
91
81
|
await this.runApply(env, tfArgs);
|
|
92
|
-
return;
|
|
82
|
+
return env;
|
|
93
83
|
}
|
|
94
84
|
this.checkDir(env);
|
|
95
85
|
if (env.status === EnvStatus.Deleting) {
|
|
@@ -101,28 +91,41 @@ export class EnvService extends BaseService {
|
|
|
101
91
|
Check logs with 'envctl logs', address the issue and re-run 'envctl delete ${env.key}'`);
|
|
102
92
|
}
|
|
103
93
|
const status = env.status;
|
|
104
|
-
if (status === EnvStatus.Init
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
94
|
+
if (status === EnvStatus.Init) {
|
|
95
|
+
if (applyMode === ApplyMode.Default) {
|
|
96
|
+
throw new KnownException('Either run apply with planfile or with flag -auto-approve');
|
|
97
|
+
}
|
|
98
|
+
env.stateFile = stateFile;
|
|
99
|
+
env.lockFile = lockFile;
|
|
108
100
|
await this.envApi.lockForUpdate(env);
|
|
109
101
|
await this.runApply(env, tfArgs);
|
|
110
|
-
return;
|
|
102
|
+
return env;
|
|
103
|
+
}
|
|
104
|
+
if (stateUpdated) {
|
|
105
|
+
env.stateFile = stateFile;
|
|
106
|
+
}
|
|
107
|
+
if (lockUpdated) {
|
|
108
|
+
env.lockFile = lockFile;
|
|
111
109
|
}
|
|
112
|
-
|
|
110
|
+
if (status === EnvStatus.Active || stateUpdated || lockUpdated) {
|
|
111
|
+
await this.envApi.lockForUpdate(env);
|
|
112
|
+
}
|
|
113
|
+
return await this.runApply(env, tfArgs);
|
|
113
114
|
}
|
|
114
115
|
async runApply(env, tfArgs) {
|
|
116
|
+
this.terraformAdapter.saveHashes();
|
|
115
117
|
logger.info('Deploying resources');
|
|
116
118
|
await this.terraformAdapter.apply(env, tfArgs);
|
|
117
119
|
logger.info('Activating env (to finish creation)');
|
|
118
120
|
await this.envApi.activate(env);
|
|
121
|
+
return env;
|
|
119
122
|
}
|
|
120
123
|
async delete(force, key) {
|
|
121
124
|
key = this.getKey(key);
|
|
122
125
|
await this.envApi.delete(key, force);
|
|
123
126
|
}
|
|
124
127
|
async destroy(tfArgs, force) {
|
|
125
|
-
const key = this.terraformAdapter.
|
|
128
|
+
const key = this.terraformAdapter.getKey();
|
|
126
129
|
const env = await this.get(key);
|
|
127
130
|
if (env.status === EnvStatus.Init) {
|
|
128
131
|
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,45 @@ 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
|
-
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
|
+
let fileContent;
|
|
117
|
+
try {
|
|
118
|
+
fileContent = fs.readFileSync(lockPath, 'utf8');
|
|
119
|
+
}
|
|
120
|
+
catch (cause) {
|
|
121
|
+
throw new KnownException(`Failed to read terraform lock file: ${lockPath}`, { cause });
|
|
107
122
|
}
|
|
123
|
+
const config = this.localStateService.load();
|
|
124
|
+
const fileHash = hash(fileContent);
|
|
125
|
+
const lockUpdated = config.lockHash !== fileHash;
|
|
126
|
+
config.stateHash = fileHash;
|
|
127
|
+
return { lockFile: fileContent, lockUpdated };
|
|
128
|
+
}
|
|
129
|
+
saveHashes() {
|
|
130
|
+
const config = this.localStateService.load();
|
|
108
131
|
this.localStateService.save(config);
|
|
109
|
-
return res;
|
|
110
132
|
}
|
|
111
133
|
async lockProviders() {
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
const linuxArm64 = process.platform === 'linux' && process.arch === 'arm64';
|
|
135
|
+
if (!linuxArm64) {
|
|
136
|
+
logger.info('Lock providers');
|
|
137
|
+
await this.cli.run('terraform', ['providers', 'lock', '-platform=linux_arm64']);
|
|
138
|
+
}
|
|
114
139
|
}
|
|
115
140
|
printTime() {
|
|
116
141
|
const now = new Date();
|
|
@@ -179,6 +204,18 @@ export class TerraformAdapter {
|
|
|
179
204
|
});
|
|
180
205
|
return result;
|
|
181
206
|
}
|
|
207
|
+
async init(args) {
|
|
208
|
+
logger.info('Running: terraform init ' + args.join(' ') + '\n');
|
|
209
|
+
try {
|
|
210
|
+
await this.cli.run('terraform', ['init', ...args], { interactive: true });
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (!(error instanceof ProcessException)) {
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
throw new KnownException(`terraform init failed with code ${error.code}:\n${error.message}`, { cause: error });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
182
219
|
async plan(key, args) {
|
|
183
220
|
args = this.tfArgs(key, args);
|
|
184
221
|
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.24.0",
|
|
4
4
|
"description": "node.js CLI client for manage environments",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"terraform wrapper",
|
|
@@ -44,36 +44,44 @@
|
|
|
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
|
-
"d1-apply": " npm run d1 -- apply --auto-approve",
|
|
49
|
+
"d1-apply": " npm run d1 -- apply --auto-approve -var=env_size=min",
|
|
50
50
|
"d1-delete": "npm run d1 -- delete",
|
|
51
|
+
"d1-logs": " npm run d1 -- logs",
|
|
51
52
|
"-d2-": "",
|
|
52
53
|
"d2-desc": "dev env destroy",
|
|
53
54
|
"d2": "CWD=../tt-core npm run it --",
|
|
54
|
-
"d2-init": "
|
|
55
|
+
"d2-init": " npm run d2 -- init -reconfigure -upgrade -backend-config=key=d2",
|
|
55
56
|
"d2-status": " npm run d2 -- status",
|
|
56
|
-
"d2-apply": " npm run d2 -- apply --auto-approve
|
|
57
|
+
"d2-apply": " npm run d2 -- apply --auto-approve",
|
|
57
58
|
"d2-destroy": "npm run d2 -- destroy -var=env_size=min --auto-approve",
|
|
58
59
|
"-d3-": "",
|
|
59
60
|
"d3-desc": "dev env plan apply",
|
|
60
61
|
"d3": "CWD=../tt-core npm run it --",
|
|
61
|
-
"d3-init": "
|
|
62
|
+
"d3-init": " npm run d3 -- init -reconfigure -upgrade -backend-config=key=d3",
|
|
62
63
|
"d3-plan": " npm run d3 -- plan -var=env_size=min -var=env=d3 -out=plan",
|
|
63
64
|
"d3-apply": " npm run d3 -- apply plan",
|
|
64
65
|
"d3-delete": "npm run d3 -- delete",
|
|
66
|
+
"-d4-": "",
|
|
67
|
+
"d4-desc": "terraform plan and then envctl apply",
|
|
68
|
+
"d4": "CWD=../tt-core npm run it --",
|
|
69
|
+
"d4-init": "cd ../tt-core && rm .terraform.lock.hcl && terraform init -reconfigure -upgrade -backend-config=key=d4",
|
|
70
|
+
"d4-plan": "cd ../tt-core && terraform plan -var=env_size=min -var=env=d4 -out=plan",
|
|
71
|
+
"d4-apply": " npm run d4 -- apply plan",
|
|
72
|
+
"d4-delete": "npm run d4 -- delete",
|
|
65
73
|
"-e1-": "",
|
|
66
74
|
"e1-desc": "ephemeral env in CI (must have ENVCTL_API_KEY_ in env vars)",
|
|
67
75
|
"e1": "CWD=../tt-core CI=true ENVCTL_HOME=non-existing ENVCTL_API_KEY=\"$ENVCTL_API_KEY_\" npm run it --",
|
|
68
76
|
"e1-create": "npm run e1 -- create-ephemeral e1",
|
|
69
|
-
"e1-init": "
|
|
77
|
+
"e1-init": " npm run e1 -- init -reconfigure -upgrade -backend-config=key=e1",
|
|
70
78
|
"e1-plan": " npm run e1 -- plan -var=env_size=min -var=env=e1 -out=plan",
|
|
71
79
|
"e1-apply": " npm run e1 -- apply plan",
|
|
72
80
|
"e1-delete": "npm run e1 -- delete",
|
|
73
81
|
"-tt-": "",
|
|
74
82
|
"tt-desc": "deploy TT project from local machine",
|
|
75
83
|
"tt": "CWD=../tt-gitops AWS_PROFILE=ac-tt-dev-deployer npm run it --",
|
|
76
|
-
"run-init": "
|
|
84
|
+
"run-init": " npm run tt -- init -reconfigure -upgrade -backend-config=key=laxa1986",
|
|
77
85
|
"run-status": " npm run tt -- status",
|
|
78
86
|
"run-apply": " npm run tt -- apply --auto-approve -var=env_size=min -var-file=versions.tfvars",
|
|
79
87
|
"run-delete": " npm run tt -- delete"
|