@agilecustoms/envctl 1.7.0 → 1.8.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/Cli.js +4 -1
- package/dist/client/EnvApiClient.js +3 -2
- package/dist/client/HttpClient.js +5 -0
- package/dist/commands/_keys.js +2 -1
- package/dist/commands/_utils.js +4 -7
- package/dist/commands/configure.js +2 -0
- package/dist/commands/createEphemeral.js +1 -0
- package/dist/commands/delete.js +1 -0
- package/dist/commands/deploy.js +1 -0
- package/dist/commands/destroy.js +1 -0
- package/dist/commands/logs.js +1 -0
- package/dist/commands/plan.js +1 -0
- package/dist/commands/status.js +1 -0
- package/dist/logger.js +36 -0
- package/dist/service/BaseService.js +2 -1
- package/dist/service/EnvService.js +22 -21
- package/dist/service/LocalStateService.js +4 -3
- package/dist/service/LogService.js +2 -1
- package/dist/service/TerraformAdapter.js +16 -15
- package/package.json +3 -2
package/dist/client/Cli.js
CHANGED
|
@@ -10,8 +10,11 @@ import path from 'path';
|
|
|
10
10
|
import * as readline from 'readline';
|
|
11
11
|
import inquirer from 'inquirer';
|
|
12
12
|
import { ProcessException, TimeoutException } from '../exceptions.js';
|
|
13
|
+
import { logger } from '../logger.js';
|
|
13
14
|
export const NO_TIMEOUT = 0;
|
|
14
15
|
export class Cli {
|
|
16
|
+
constructor() {
|
|
17
|
+
}
|
|
15
18
|
async run(command, args, timeoutMs = NO_TIMEOUT, out_scanner, in_scanner) {
|
|
16
19
|
const needsInteractive = !!in_scanner;
|
|
17
20
|
const spawnOptions = {
|
|
@@ -93,7 +96,7 @@ export class Cli {
|
|
|
93
96
|
}
|
|
94
97
|
if (code === 0) {
|
|
95
98
|
if (errorBuffer) {
|
|
96
|
-
|
|
99
|
+
logger.warn('Process completed successfully, but there were errors:\n' + errorBuffer);
|
|
97
100
|
}
|
|
98
101
|
resolve();
|
|
99
102
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BusinessException, KnownException, NotFoundException } from '../exceptions.js';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
2
3
|
import { EnvStatus } from '../model/index.js';
|
|
3
4
|
import { HttpClient, toUrl } from './HttpClient.js';
|
|
4
5
|
export class EnvApiClient {
|
|
@@ -76,7 +77,7 @@ export class EnvApiClient {
|
|
|
76
77
|
env.status = result.status;
|
|
77
78
|
}
|
|
78
79
|
async delete(key, force = false) {
|
|
79
|
-
|
|
80
|
+
logger.info('Sending delete command');
|
|
80
81
|
let result;
|
|
81
82
|
try {
|
|
82
83
|
result = await this.httpClient.fetch(`/ci/env/${key}?force=${force}`, {
|
|
@@ -89,7 +90,7 @@ export class EnvApiClient {
|
|
|
89
90
|
}
|
|
90
91
|
throw error;
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
+
logger.info(result.message);
|
|
93
94
|
}
|
|
94
95
|
async getLogs(key) {
|
|
95
96
|
let result;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BusinessException, HttpException, NotFoundException } from '../exceptions.js';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
2
3
|
const HOST = 'cli.maintenance.agilecustoms.com';
|
|
3
4
|
export function toUrl(path) {
|
|
4
5
|
return `https://${HOST}/env-api${path}`;
|
|
@@ -19,6 +20,10 @@ export class HttpClient {
|
|
|
19
20
|
}
|
|
20
21
|
async fetch(path, options = {}) {
|
|
21
22
|
const url = toUrl(path);
|
|
23
|
+
if (!options.method) {
|
|
24
|
+
options.method = 'GET';
|
|
25
|
+
}
|
|
26
|
+
logger.debug(`--> ${options.method} ${url} ${JSON.stringify(options)}`);
|
|
22
27
|
let response;
|
|
23
28
|
try {
|
|
24
29
|
response = await fetch(url, options);
|
package/dist/commands/_keys.js
CHANGED
|
@@ -3,5 +3,6 @@ export const _keys = {
|
|
|
3
3
|
FORCE: 'Force deletion without confirmation',
|
|
4
4
|
KEY: 'Environment name/identifier - taken from remote stake key, must be unique for a given customer',
|
|
5
5
|
KIND: 'Environment kind: complete project (default) or some slice such as tt-core + tt-web',
|
|
6
|
-
OWNER: 'Environment owner (GH username)'
|
|
6
|
+
OWNER: 'Environment owner (GH username)',
|
|
7
|
+
VERBOSE: 'Verbose output (w/ debug logs)'
|
|
7
8
|
};
|
package/dist/commands/_utils.js
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
import { ExitPromptError } from '@inquirer/core';
|
|
2
2
|
import { AbortedException, KnownException } from '../exceptions.js';
|
|
3
|
+
import { logger } from '../logger.js';
|
|
3
4
|
export function wrap(callable) {
|
|
4
5
|
return async (...args) => {
|
|
5
|
-
let result;
|
|
6
6
|
try {
|
|
7
|
-
|
|
7
|
+
await callable(...args);
|
|
8
8
|
}
|
|
9
9
|
catch (error) {
|
|
10
10
|
if (error instanceof KnownException) {
|
|
11
|
-
|
|
11
|
+
logger.error(error.message);
|
|
12
12
|
}
|
|
13
13
|
else if (error instanceof AbortedException || error instanceof ExitPromptError) {
|
|
14
14
|
}
|
|
15
15
|
else {
|
|
16
|
-
|
|
16
|
+
logger.error('Unknown error: ' + error);
|
|
17
17
|
}
|
|
18
18
|
process.exit(1);
|
|
19
19
|
}
|
|
20
|
-
if (result !== undefined) {
|
|
21
|
-
console.log(result);
|
|
22
|
-
}
|
|
23
20
|
};
|
|
24
21
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import { configService } from '../container.js';
|
|
4
|
+
import { _keys } from './_keys.js';
|
|
4
5
|
import { wrap } from './_utils.js';
|
|
5
6
|
export function configure(program) {
|
|
6
7
|
program
|
|
7
8
|
.command('configure')
|
|
8
9
|
.description('Configure user settings on your local machine')
|
|
10
|
+
.option('-v, --verbose', _keys.VERBOSE)
|
|
9
11
|
.action(wrap(handler));
|
|
10
12
|
}
|
|
11
13
|
async function handler() {
|
|
@@ -7,6 +7,7 @@ export function createEphemeral(program) {
|
|
|
7
7
|
.command('create-ephemeral')
|
|
8
8
|
.description('Create bare env w/o resources. Used in CI as first step just to pre-generate token for extension')
|
|
9
9
|
.requiredOption('--key <key>', _keys.KEY)
|
|
10
|
+
.option('-v, --verbose', _keys.VERBOSE)
|
|
10
11
|
.action(wrap(handler));
|
|
11
12
|
}
|
|
12
13
|
async function handler(options) {
|
package/dist/commands/delete.js
CHANGED
package/dist/commands/deploy.js
CHANGED
|
@@ -7,6 +7,7 @@ export function deploy(program) {
|
|
|
7
7
|
.command('deploy')
|
|
8
8
|
.description('Create new or update existing environment')
|
|
9
9
|
.option('--cwd <cwd>', _keys.CWD)
|
|
10
|
+
.option('-v, --verbose', _keys.VERBOSE)
|
|
10
11
|
.allowUnknownOption(true)
|
|
11
12
|
.argument('[args...]')
|
|
12
13
|
.action(wrap(handler));
|
package/dist/commands/destroy.js
CHANGED
|
@@ -11,6 +11,7 @@ export function destroy(program) {
|
|
|
11
11
|
+ ' Main use case - test deletion process, basically that you have enough permissions to delete resources')
|
|
12
12
|
.option('--force', _keys.FORCE)
|
|
13
13
|
.option('--cwd <cwd>', _keys.CWD)
|
|
14
|
+
.option('-v, --verbose', _keys.VERBOSE)
|
|
14
15
|
.allowUnknownOption(true)
|
|
15
16
|
.argument('[args...]')
|
|
16
17
|
.action(wrap(handler));
|
package/dist/commands/logs.js
CHANGED
|
@@ -8,6 +8,7 @@ export function logs(program) {
|
|
|
8
8
|
.description('Get most recent env destroy logs')
|
|
9
9
|
.option('--key <key>', _keys.KEY)
|
|
10
10
|
.option('--cwd <cwd>', _keys.CWD)
|
|
11
|
+
.option('-v, --verbose', _keys.VERBOSE)
|
|
11
12
|
.action(wrap(handler));
|
|
12
13
|
}
|
|
13
14
|
async function handler(options) {
|
package/dist/commands/plan.js
CHANGED
|
@@ -8,6 +8,7 @@ export function plan(program) {
|
|
|
8
8
|
.description('High level wrapper for terraform plan. Compliments deploy command: if you plan to deploy env with envctl deploy,'
|
|
9
9
|
+ ' then it is recommended to do plan with envctl plan to guarantee consistent behavior')
|
|
10
10
|
.option('--cwd <cwd>', _keys.CWD)
|
|
11
|
+
.option('-v, --verbose', _keys.VERBOSE)
|
|
11
12
|
.allowUnknownOption(true)
|
|
12
13
|
.argument('[args...]')
|
|
13
14
|
.action(wrap(handler));
|
package/dist/commands/status.js
CHANGED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class Logger {
|
|
2
|
+
now;
|
|
3
|
+
verbose;
|
|
4
|
+
buffer = [];
|
|
5
|
+
constructor(now, verbose) {
|
|
6
|
+
this.now = now;
|
|
7
|
+
this.verbose = verbose;
|
|
8
|
+
}
|
|
9
|
+
debug(msg) {
|
|
10
|
+
this.log('debug', msg);
|
|
11
|
+
}
|
|
12
|
+
info(msg) {
|
|
13
|
+
this.log('info', msg);
|
|
14
|
+
}
|
|
15
|
+
warn(msg) {
|
|
16
|
+
this.log('warn', msg);
|
|
17
|
+
}
|
|
18
|
+
error(msg) {
|
|
19
|
+
this.log('error', msg);
|
|
20
|
+
}
|
|
21
|
+
log(level, msg) {
|
|
22
|
+
const date = new Date(this.now()).toISOString();
|
|
23
|
+
const detailedMessage = `${date} [${level}] ${msg}`;
|
|
24
|
+
this.buffer.push(detailedMessage);
|
|
25
|
+
if (!this.verbose && level === 'debug') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const method = console[level];
|
|
29
|
+
method(this.verbose ? detailedMessage : `${msg}`);
|
|
30
|
+
}
|
|
31
|
+
getBuffer() {
|
|
32
|
+
return this.buffer;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
|
|
36
|
+
export const logger = new Logger(Date.now, verbose);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from '../logger.js';
|
|
1
2
|
export class BaseService {
|
|
2
3
|
terraformAdapter;
|
|
3
4
|
constructor(terraformAdapter) {
|
|
@@ -5,7 +6,7 @@ export class BaseService {
|
|
|
5
6
|
}
|
|
6
7
|
getKey(key) {
|
|
7
8
|
if (!key) {
|
|
8
|
-
|
|
9
|
+
logger.info('Key is not provided, inferring from state file');
|
|
9
10
|
key = this.terraformAdapter.getTerraformBackend().getKey();
|
|
10
11
|
}
|
|
11
12
|
return key;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Cli } from '../client/Cli.js';
|
|
2
2
|
import { EnvApiClient } from '../client/index.js';
|
|
3
3
|
import { KnownException } from '../exceptions.js';
|
|
4
|
+
import { logger } from '../logger.js';
|
|
4
5
|
import { EnvStatus } from '../model/index.js';
|
|
5
6
|
import { BaseService } from './BaseService.js';
|
|
6
7
|
export const PLAN_FILE = '.terraform/envctl.plan';
|
|
@@ -14,13 +15,13 @@ export class EnvService extends BaseService {
|
|
|
14
15
|
}
|
|
15
16
|
async status(key) {
|
|
16
17
|
key = this.getKey(key);
|
|
17
|
-
|
|
18
|
+
logger.info(`Retrieve env ${key}`);
|
|
18
19
|
const env = await this.envApi.get(key);
|
|
19
20
|
if (env === null) {
|
|
20
|
-
|
|
21
|
+
logger.info(`Env ${key} does not exist`);
|
|
21
22
|
return;
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
logger.info(`Env ${key} status: ${env.status}`);
|
|
24
25
|
if (env.status !== EnvStatus.Deleting && env.status !== EnvStatus.DeleteError) {
|
|
25
26
|
const date = new Date(env.ttl * 1000);
|
|
26
27
|
const formattedDate = date.toLocaleString(undefined, {
|
|
@@ -31,23 +32,23 @@ export class EnvService extends BaseService {
|
|
|
31
32
|
second: '2-digit',
|
|
32
33
|
hour12: false,
|
|
33
34
|
});
|
|
34
|
-
|
|
35
|
+
logger.info(`Expires at ${formattedDate}`);
|
|
35
36
|
}
|
|
36
37
|
if (env.kind) {
|
|
37
38
|
const kind = this.cli.getKind();
|
|
38
39
|
if (env.kind !== kind) {
|
|
39
|
-
|
|
40
|
+
logger.warn(`Env ${key} kind (dir-name): ${env.kind} - looks like this env was deployed from a different directory`);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
async tryGetEnv(key) {
|
|
44
|
-
|
|
45
|
+
logger.info(`Check if env ${key} already exists`);
|
|
45
46
|
const env = await this.envApi.get(key);
|
|
46
47
|
if (env) {
|
|
47
|
-
|
|
48
|
+
logger.info(`Found ${key} env`);
|
|
48
49
|
}
|
|
49
50
|
else {
|
|
50
|
-
|
|
51
|
+
logger.info(`Env ${key} does not exist`);
|
|
51
52
|
}
|
|
52
53
|
return env;
|
|
53
54
|
}
|
|
@@ -77,7 +78,7 @@ export class EnvService extends BaseService {
|
|
|
77
78
|
return await this.envApi.createEphemeral(createEnv);
|
|
78
79
|
}
|
|
79
80
|
async lockTerraform(env, newEnv = false) {
|
|
80
|
-
|
|
81
|
+
logger.info('Lock providers');
|
|
81
82
|
const res = await this.terraformAdapter.lock(newEnv);
|
|
82
83
|
if (env !== null) {
|
|
83
84
|
if (res.lockFile) {
|
|
@@ -123,11 +124,11 @@ export class EnvService extends BaseService {
|
|
|
123
124
|
let env = await this.tryGetEnv(key);
|
|
124
125
|
if (env === null) {
|
|
125
126
|
const kind = this.cli.getKind();
|
|
126
|
-
|
|
127
|
+
logger.info(`Inferred kind from directory name: ${kind}`);
|
|
127
128
|
const createEnv = { key, kind };
|
|
128
129
|
await this.lockTerraform(createEnv, true);
|
|
129
130
|
tfArgs = await this.ensurePlan(env, tfArgs);
|
|
130
|
-
|
|
131
|
+
logger.info('Creating env metadata');
|
|
131
132
|
env = await this.envApi.create(createEnv);
|
|
132
133
|
await this.runDeploy(env, tfArgs);
|
|
133
134
|
return;
|
|
@@ -139,16 +140,16 @@ export class EnvService extends BaseService {
|
|
|
139
140
|
const answerYes = await this.cli.promptYesNo(`Env status is ${status}, likely due to an error from a previous run\n
|
|
140
141
|
Do you want to proceed with deployment?`);
|
|
141
142
|
if (!answerYes) {
|
|
142
|
-
|
|
143
|
+
logger.info('Aborting deployment');
|
|
143
144
|
return;
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
147
|
if (status === EnvStatus.Init || status === EnvStatus.Active) {
|
|
147
|
-
|
|
148
|
+
logger.info(`Env status is ${status}`);
|
|
148
149
|
await this.lockTerraform(env);
|
|
149
150
|
tfArgs = await this.ensurePlan(env, tfArgs);
|
|
150
151
|
if (env.status == EnvStatus.Active) {
|
|
151
|
-
|
|
152
|
+
logger.info('Will lock for update and run terraform apply (to update resources)');
|
|
152
153
|
}
|
|
153
154
|
await this.envApi.lockForUpdate(env);
|
|
154
155
|
await this.runDeploy(env, tfArgs);
|
|
@@ -158,9 +159,9 @@ export class EnvService extends BaseService {
|
|
|
158
159
|
await this.runDeploy(env, tfArgs);
|
|
159
160
|
}
|
|
160
161
|
async runDeploy(env, tfArgs) {
|
|
161
|
-
|
|
162
|
+
logger.info('Deploying resources');
|
|
162
163
|
await this.terraformAdapter.apply(tfArgs, env.ttl);
|
|
163
|
-
|
|
164
|
+
logger.info('Activating env (to finish creation)');
|
|
164
165
|
await this.envApi.activate(env);
|
|
165
166
|
}
|
|
166
167
|
async delete(force, key) {
|
|
@@ -171,7 +172,7 @@ export class EnvService extends BaseService {
|
|
|
171
172
|
const key = this.terraformAdapter.getTerraformBackend().getKey();
|
|
172
173
|
const env = await this.get(key);
|
|
173
174
|
if (env.status === EnvStatus.Init) {
|
|
174
|
-
|
|
175
|
+
logger.info(`Env ${env.key} status is INIT - no resources, nothing to destroy, just deleting metadata`);
|
|
175
176
|
await this.envApi.delete(key, force);
|
|
176
177
|
return;
|
|
177
178
|
}
|
|
@@ -179,20 +180,20 @@ export class EnvService extends BaseService {
|
|
|
179
180
|
throw new KnownException(`Env ${env.key} status is DELETING, please wait or re-run with --force`);
|
|
180
181
|
}
|
|
181
182
|
if (env.status === EnvStatus.Active) {
|
|
182
|
-
|
|
183
|
+
logger.info('Lock env to run destroy');
|
|
183
184
|
await this.envApi.lockForUpdate(env);
|
|
184
185
|
}
|
|
185
186
|
await this.terraformAdapter.destroy(env, tfArgs, force);
|
|
186
187
|
await this.envApi.delete(key, force);
|
|
187
|
-
|
|
188
|
+
logger.info('Please wait for ~30 sec before you can create env with same name');
|
|
188
189
|
}
|
|
189
190
|
async get(key) {
|
|
190
|
-
|
|
191
|
+
logger.info(`Retrieve env ${key}`);
|
|
191
192
|
const env = await this.envApi.get(key);
|
|
192
193
|
if (!env) {
|
|
193
194
|
throw new KnownException(`Environment ${key} is not found`);
|
|
194
195
|
}
|
|
195
|
-
|
|
196
|
+
logger.info(`Env status: ${env.status}`);
|
|
196
197
|
return env;
|
|
197
198
|
}
|
|
198
199
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { logger } from '../logger.js';
|
|
3
4
|
const CURRENT_VERSION = 1;
|
|
4
5
|
export class LocalStateService {
|
|
5
6
|
config;
|
|
@@ -16,16 +17,16 @@ export class LocalStateService {
|
|
|
16
17
|
if (this.config) {
|
|
17
18
|
return this.config;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
logger.info('Load local state config');
|
|
20
21
|
const filePath = this.filePath();
|
|
21
22
|
if (!fs.existsSync(filePath)) {
|
|
22
|
-
|
|
23
|
+
logger.warn('Local state file does not exist, must have been accidentally deleted, re-initializing');
|
|
23
24
|
return this.createEmptyConfig(filePath);
|
|
24
25
|
}
|
|
25
26
|
const data = fs.readFileSync(filePath, 'utf-8');
|
|
26
27
|
let config = JSON.parse(data);
|
|
27
28
|
if (config.version != CURRENT_VERSION) {
|
|
28
|
-
|
|
29
|
+
logger.info('Local state file version mismatch, re-initializing');
|
|
29
30
|
config = this.createEmptyConfig(filePath);
|
|
30
31
|
}
|
|
31
32
|
this.config = config;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import {} from '../client/index.js';
|
|
3
3
|
import { KnownException } from '../exceptions.js';
|
|
4
|
+
import { logger } from '../logger.js';
|
|
4
5
|
import { BaseService } from './BaseService.js';
|
|
5
6
|
export class LogService extends BaseService {
|
|
6
7
|
cli;
|
|
@@ -33,7 +34,7 @@ export class LogService extends BaseService {
|
|
|
33
34
|
throw new KnownException(`Failed to download logs: ${res.status} ${res.statusText}${msg ? ` - ${msg}` : ''}`);
|
|
34
35
|
}
|
|
35
36
|
const outPath = await this.cli.writeInTmpFile(res.body, outName);
|
|
36
|
-
|
|
37
|
+
logger.info(`Logs saved to: ${outPath}`);
|
|
37
38
|
await this.cli.run('open', ['-R', outPath]);
|
|
38
39
|
}
|
|
39
40
|
}
|
|
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { NO_TIMEOUT } from '../client/Cli.js';
|
|
5
5
|
import { AbortedException, KnownException, ProcessException, TimeoutException } from '../exceptions.js';
|
|
6
|
+
import { logger } from '../logger.js';
|
|
6
7
|
import { LocalStateService } from './LocalStateService.js';
|
|
7
8
|
export const MAX_ATTEMPTS = 2;
|
|
8
9
|
export const RETRYABLE_ERRORS = [
|
|
@@ -103,7 +104,7 @@ export class TerraformAdapter {
|
|
|
103
104
|
return res;
|
|
104
105
|
}
|
|
105
106
|
async lockProviders() {
|
|
106
|
-
|
|
107
|
+
logger.info('Update lock file');
|
|
107
108
|
await this.cli.run('terraform', ['providers', 'lock', '-platform=linux_arm64']);
|
|
108
109
|
}
|
|
109
110
|
printTime() {
|
|
@@ -112,7 +113,7 @@ export class TerraformAdapter {
|
|
|
112
113
|
const edt = now.toLocaleString('en-US', {
|
|
113
114
|
timeZone: 'America/New_York'
|
|
114
115
|
});
|
|
115
|
-
|
|
116
|
+
logger.info(`\nTime EDT: ${edt}, UTC: ${utc}\n`);
|
|
116
117
|
}
|
|
117
118
|
tfArgs(env, args, vars = []) {
|
|
118
119
|
const varDefs = this.getVarDefs();
|
|
@@ -163,7 +164,7 @@ export class TerraformAdapter {
|
|
|
163
164
|
const keyValue = arg.slice(5);
|
|
164
165
|
const eqIndex = keyValue.indexOf('=');
|
|
165
166
|
if (eqIndex === -1) {
|
|
166
|
-
|
|
167
|
+
logger.error('Terraform var argument is not in key=value format: ' + arg);
|
|
167
168
|
return;
|
|
168
169
|
}
|
|
169
170
|
const key = keyValue.slice(0, eqIndex);
|
|
@@ -179,7 +180,7 @@ export class TerraformAdapter {
|
|
|
179
180
|
Object.entries(vars).forEach(([key, value]) => {
|
|
180
181
|
const varDef = varDefs[key];
|
|
181
182
|
if (!varDef) {
|
|
182
|
-
|
|
183
|
+
logger.warn(`Terraform variable ${key} passed, but not found in .tf files`);
|
|
183
184
|
return;
|
|
184
185
|
}
|
|
185
186
|
if (varDef.sensitive)
|
|
@@ -215,7 +216,7 @@ export class TerraformAdapter {
|
|
|
215
216
|
tfVarName = '';
|
|
216
217
|
}
|
|
217
218
|
}
|
|
218
|
-
|
|
219
|
+
logger.info('Running: terraform plan ' + args.join(' ') + '\n');
|
|
219
220
|
try {
|
|
220
221
|
await this.cli.run('terraform', ['plan', ...args], NO_TIMEOUT, out_scanner, in_scanner);
|
|
221
222
|
}
|
|
@@ -224,20 +225,20 @@ export class TerraformAdapter {
|
|
|
224
225
|
throw error;
|
|
225
226
|
}
|
|
226
227
|
if (attemptNo < MAX_ATTEMPTS && RETRYABLE_ERRORS.some(err => error.message.includes(err))) {
|
|
227
|
-
|
|
228
|
+
logger.warn(`Retrying terraform plan due to error: ${error.message}`);
|
|
228
229
|
return this._plan(args, onDemandVars, attemptNo + 1);
|
|
229
230
|
}
|
|
230
231
|
const lockId = this.lockId(error, attemptNo);
|
|
231
232
|
if (lockId) {
|
|
232
233
|
await this.promptUnlock(lockId);
|
|
233
|
-
|
|
234
|
+
logger.info('State unlocked, retrying terraform plan');
|
|
234
235
|
return this._plan(args, onDemandVars, attemptNo + 1);
|
|
235
236
|
}
|
|
236
237
|
throw new KnownException(`terraform plan failed with code ${error.code}:\n${error.message}`, { cause: error });
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
240
|
async apply(args, ttl) {
|
|
240
|
-
|
|
241
|
+
logger.info('Running: terraform apply ' + args.join(' ') + '\n');
|
|
241
242
|
try {
|
|
242
243
|
await this._apply(args, ttl, 1);
|
|
243
244
|
}
|
|
@@ -258,7 +259,7 @@ export class TerraformAdapter {
|
|
|
258
259
|
if (timeout <= 0) {
|
|
259
260
|
throw new KnownException('TTL expired before terraform apply could start');
|
|
260
261
|
}
|
|
261
|
-
|
|
262
|
+
logger.debug('timeout(ms): ' + timeout);
|
|
262
263
|
try {
|
|
263
264
|
await this.cli.run('terraform', ['apply', ...args], timeout);
|
|
264
265
|
this.printTime();
|
|
@@ -268,13 +269,13 @@ export class TerraformAdapter {
|
|
|
268
269
|
throw error;
|
|
269
270
|
}
|
|
270
271
|
if (attemptNo < MAX_ATTEMPTS && RETRYABLE_ERRORS.some(err => error.message.includes(err))) {
|
|
271
|
-
|
|
272
|
+
logger.warn(`Retrying terraform apply due to error: ${error.message}`);
|
|
272
273
|
return this._apply(args, ttl, attemptNo + 1);
|
|
273
274
|
}
|
|
274
275
|
const lockId = this.lockId(error, attemptNo);
|
|
275
276
|
if (lockId) {
|
|
276
277
|
await this.promptUnlock(lockId);
|
|
277
|
-
|
|
278
|
+
logger.info('State unlocked, retrying terraform apply');
|
|
278
279
|
return this._apply(args, ttl, attemptNo + 1);
|
|
279
280
|
}
|
|
280
281
|
throw new KnownException(`terraform apply failed with code ${error.code}:\n${error.message}`, { cause: error });
|
|
@@ -291,7 +292,7 @@ export class TerraformAdapter {
|
|
|
291
292
|
wrongDir = true;
|
|
292
293
|
}
|
|
293
294
|
};
|
|
294
|
-
|
|
295
|
+
logger.info('Running: terraform destroy -auto-approve' + args.join(' ') + '\n');
|
|
295
296
|
try {
|
|
296
297
|
await this.cli.run('terraform', ['destroy', '-auto-approve', ...args], NO_TIMEOUT, scanner);
|
|
297
298
|
}
|
|
@@ -307,7 +308,7 @@ export class TerraformAdapter {
|
|
|
307
308
|
else {
|
|
308
309
|
await this.promptUnlock(lockId);
|
|
309
310
|
}
|
|
310
|
-
|
|
311
|
+
logger.info('State unlocked, retrying terraform destroy');
|
|
311
312
|
return this._destroy(args, force, attemptNo + 1);
|
|
312
313
|
}
|
|
313
314
|
throw new KnownException(`terraform destroy failed with code ${error.code}:\n${error.message}`, { cause: error });
|
|
@@ -320,7 +321,7 @@ export class TerraformAdapter {
|
|
|
320
321
|
if (attemptNo < MAX_ATTEMPTS && error.message.includes('Error acquiring the state lock')) {
|
|
321
322
|
const match = error.message.match(/ID:\s+([a-f0-9-]{36})/i);
|
|
322
323
|
if (match) {
|
|
323
|
-
|
|
324
|
+
logger.error(error.message);
|
|
324
325
|
return match[1];
|
|
325
326
|
}
|
|
326
327
|
}
|
|
@@ -335,7 +336,7 @@ export class TerraformAdapter {
|
|
|
335
336
|
await this.forceUnlock(id);
|
|
336
337
|
}
|
|
337
338
|
async forceUnlock(id) {
|
|
338
|
-
|
|
339
|
+
logger.info('Force unlocking state');
|
|
339
340
|
await this.cli.run('terraform', ['force-unlock', '-force', id]);
|
|
340
341
|
}
|
|
341
342
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agilecustoms/envctl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "node.js CLI client for manage environments",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"terraform wrapper",
|
|
@@ -37,11 +37,12 @@
|
|
|
37
37
|
"test": "vitest run --coverage",
|
|
38
38
|
"build": "tsc -p tsconfig.build.json",
|
|
39
39
|
"run": "tsc -p tsconfig.build.json --sourceMap true && node dist/index.js",
|
|
40
|
+
"run-help": " npm run run -- deploy --help",
|
|
40
41
|
"run-version": " npm run run -- --version",
|
|
41
42
|
"run-configure": "npm run run -- configure",
|
|
42
43
|
"~": "",
|
|
43
44
|
"run-core-init": " cd ../tt-core && terraform init -upgrade -backend-config=key=laxa1986 -reconfigure",
|
|
44
|
-
"run-core-status": " npm run run -- status --cwd ../tt-core",
|
|
45
|
+
"run-core-status": " npm run run -- status --cwd ../tt-core -v",
|
|
45
46
|
"run-core-plan": " npm run run -- plan --cwd ../tt-core -out=.terraform/plan",
|
|
46
47
|
"run-core-deployp": "npm run run -- deploy --cwd ../tt-core .terraform/plan",
|
|
47
48
|
"run-core-deploy": " npm run run -- deploy --cwd ../tt-core -var=\"env_size=min\"",
|