@agilecustoms/envctl 1.14.0 → 1.15.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/client/Cli.js
CHANGED
|
@@ -14,15 +14,15 @@ export const NO_TIMEOUT = 0;
|
|
|
14
14
|
export class Cli {
|
|
15
15
|
constructor() {
|
|
16
16
|
}
|
|
17
|
-
async run(command, args,
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
};
|
|
17
|
+
async run(command, args, options = {}) {
|
|
18
|
+
const timeoutMs = options.timeoutMs ?? NO_TIMEOUT;
|
|
19
|
+
const interactive = options.interactive ?? !!options.inScanner;
|
|
20
|
+
const stdio = [interactive ? 'pipe' : 'ignore', 'pipe', 'pipe'];
|
|
21
|
+
const spawnOptions = { stdio };
|
|
22
22
|
const child = spawn(command, args, spawnOptions);
|
|
23
23
|
child.stdout.setEncoding('utf8');
|
|
24
24
|
child.stderr.setEncoding('utf8');
|
|
25
|
-
const rl =
|
|
25
|
+
const rl = interactive
|
|
26
26
|
? readline.createInterface({
|
|
27
27
|
input: process.stdin,
|
|
28
28
|
output: process.stdout,
|
|
@@ -53,13 +53,13 @@ export class Cli {
|
|
|
53
53
|
on('SIGTERM');
|
|
54
54
|
on('SIGHUP');
|
|
55
55
|
rl?.on('line', (line) => {
|
|
56
|
-
|
|
56
|
+
options.inScanner?.(line);
|
|
57
57
|
if (child.stdin.writable) {
|
|
58
58
|
child.stdin.write(line + '\n');
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
61
|
function processLine(line) {
|
|
62
|
-
|
|
62
|
+
options.outScanner?.(line);
|
|
63
63
|
console.log(line);
|
|
64
64
|
}
|
|
65
65
|
let buffer = '';
|
|
@@ -2,7 +2,7 @@ import { BusinessException, KnownException, NotFoundException } from '../excepti
|
|
|
2
2
|
import { logger } from '../logger.js';
|
|
3
3
|
import { EnvStatus } from '../model/index.js';
|
|
4
4
|
import { toLocalTime } from '../util.js';
|
|
5
|
-
import { HttpClient
|
|
5
|
+
import { HttpClient } from './HttpClient.js';
|
|
6
6
|
export class EnvApiClient {
|
|
7
7
|
httpClient;
|
|
8
8
|
constructor(httpClient) {
|
|
@@ -33,7 +33,7 @@ export class EnvApiClient {
|
|
|
33
33
|
}
|
|
34
34
|
throw error;
|
|
35
35
|
}
|
|
36
|
-
return
|
|
36
|
+
return this.httpClient.getUrl(`/public/env/${env.key}/extend?token=${result.token}`);
|
|
37
37
|
}
|
|
38
38
|
async create(env) {
|
|
39
39
|
let result;
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { BusinessException, HttpException, NotFoundException } from '../exceptions.js';
|
|
1
|
+
import { BusinessException, HttpException, KnownException, NotFoundException } from '../exceptions.js';
|
|
2
2
|
import { logger } from '../logger.js';
|
|
3
|
+
import { DEFAULT_HOST } from '../service/ConfigService.js';
|
|
3
4
|
import { ConfigService } from '../service/index.js';
|
|
4
|
-
const HOST = 'cli.maintenance.agilecustoms.com';
|
|
5
|
-
export function toUrl(path) {
|
|
6
|
-
return `https://${HOST}/env-api${path}`;
|
|
7
|
-
}
|
|
8
5
|
export class HttpClient {
|
|
9
6
|
version;
|
|
10
7
|
commandId;
|
|
@@ -26,8 +23,12 @@ export class HttpClient {
|
|
|
26
23
|
}
|
|
27
24
|
return await this.fetch(path, options);
|
|
28
25
|
}
|
|
26
|
+
getUrl(path) {
|
|
27
|
+
const host = this.configService.getHost();
|
|
28
|
+
return `https://${host}/env-api${path}`;
|
|
29
|
+
}
|
|
29
30
|
async fetch(path, options = {}) {
|
|
30
|
-
const url =
|
|
31
|
+
const url = this.getUrl(path);
|
|
31
32
|
const headers = new Headers(options.headers);
|
|
32
33
|
headers.set('x-command-id', this.commandId);
|
|
33
34
|
headers.set('x-client-version', this.version);
|
|
@@ -44,6 +45,15 @@ export class HttpClient {
|
|
|
44
45
|
response = await fetch(url, options);
|
|
45
46
|
}
|
|
46
47
|
catch (error) {
|
|
48
|
+
if (error.cause && error.cause.errno == -3008) {
|
|
49
|
+
const host = error.cause.hostname;
|
|
50
|
+
if (host == DEFAULT_HOST) {
|
|
51
|
+
throw new KnownException('Can not reach backend. Network issue?');
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
throw new KnownException('Unresolvable hostname ' + error.cause.hostname);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
47
57
|
throw new Error('Error (network?) making the request:', { cause: error });
|
|
48
58
|
}
|
|
49
59
|
const contentType = response.headers?.get('Content-Type') || '';
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,8 @@ const program = new Command();
|
|
|
15
15
|
program
|
|
16
16
|
.name('envctl')
|
|
17
17
|
.description('CLI to manage environments')
|
|
18
|
-
.version(pkg.version)
|
|
18
|
+
.version(pkg.version, '', 'Output the version number')
|
|
19
|
+
.helpOption('-h, --help', 'Display help for command')
|
|
19
20
|
.option('--verbose', 'Verbose output (w/ debug logs)');
|
|
20
21
|
configure(program, configService);
|
|
21
22
|
createEphemeral(program, configService, envService);
|
|
@@ -3,6 +3,7 @@ import * as os from 'node:os';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { KnownException } from '../exceptions.js';
|
|
5
5
|
const CONFIG_DIR = path.join(os.homedir(), '.envctl');
|
|
6
|
+
export const DEFAULT_HOST = 'cli.maintenance.agilecustoms.com';
|
|
6
7
|
var EnvKey;
|
|
7
8
|
(function (EnvKey) {
|
|
8
9
|
EnvKey["API_KEY"] = "ENVCTL_API_KEY";
|
|
@@ -18,21 +19,23 @@ export class ConfigService {
|
|
|
18
19
|
config;
|
|
19
20
|
constructor() {
|
|
20
21
|
}
|
|
21
|
-
loadConfig(profile) {
|
|
22
|
+
loadConfig(profile, failOnMissingCustomProfile = true) {
|
|
22
23
|
if (this.config)
|
|
23
24
|
throw new Error('load config second time?');
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
25
|
+
const customProfile = profile || env(EnvKey.PROFILE);
|
|
26
|
+
profile = customProfile || 'default';
|
|
27
27
|
const configPath = path.join(CONFIG_DIR, `${profile}.json`);
|
|
28
28
|
if (fs.existsSync(configPath)) {
|
|
29
29
|
const data = fs.readFileSync(configPath, 'utf-8');
|
|
30
30
|
this.config = JSON.parse(data);
|
|
31
31
|
}
|
|
32
|
+
else if (customProfile && failOnMissingCustomProfile) {
|
|
33
|
+
throw new KnownException(`Profile ${customProfile} doesn't exist`);
|
|
34
|
+
}
|
|
32
35
|
return this.config;
|
|
33
36
|
}
|
|
34
37
|
saveConfig(profile, config) {
|
|
35
|
-
const mergedConfig = { ...this.loadConfig(profile), ...config };
|
|
38
|
+
const mergedConfig = { ...this.loadConfig(profile, false), ...config };
|
|
36
39
|
const data = JSON.stringify(mergedConfig, null, 2);
|
|
37
40
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
38
41
|
const configPath = path.join(CONFIG_DIR, `${profile}.json`);
|
|
@@ -61,4 +64,7 @@ export class ConfigService {
|
|
|
61
64
|
getVarEphemeral() {
|
|
62
65
|
return this.config?.varEphemeral;
|
|
63
66
|
}
|
|
67
|
+
getHost() {
|
|
68
|
+
return this.config?.host || DEFAULT_HOST;
|
|
69
|
+
}
|
|
64
70
|
}
|
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { confirm } from '@inquirer/prompts';
|
|
5
|
-
import {
|
|
5
|
+
import {} from '../client/Cli.js';
|
|
6
6
|
import { AbortedException, KnownException, ProcessException, TimeoutException } from '../exceptions.js';
|
|
7
7
|
import { logger } from '../logger.js';
|
|
8
8
|
import { LocalStateService } from './LocalStateService.js';
|
|
@@ -188,32 +188,14 @@ export class TerraformAdapter {
|
|
|
188
188
|
});
|
|
189
189
|
return result;
|
|
190
190
|
}
|
|
191
|
-
async plan(env, args
|
|
191
|
+
async plan(env, args) {
|
|
192
192
|
args = this.tfArgs(env, args);
|
|
193
|
-
await this._plan(args,
|
|
193
|
+
await this._plan(args, 1);
|
|
194
194
|
}
|
|
195
|
-
async _plan(args,
|
|
196
|
-
let inputTfVariable = false;
|
|
197
|
-
let tfVarName = '';
|
|
198
|
-
function out_scanner(line) {
|
|
199
|
-
inputTfVariable = false;
|
|
200
|
-
const match = line.match(/var\.([a-zA-Z_0-9]+)/);
|
|
201
|
-
if (match) {
|
|
202
|
-
tfVarName = match[1];
|
|
203
|
-
}
|
|
204
|
-
else if (line.includes('Enter a value:')) {
|
|
205
|
-
inputTfVariable = true;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
function in_scanner(line) {
|
|
209
|
-
if (inputTfVariable && tfVarName) {
|
|
210
|
-
onDemandVars[tfVarName] = line;
|
|
211
|
-
tfVarName = '';
|
|
212
|
-
}
|
|
213
|
-
}
|
|
195
|
+
async _plan(args, attemptNo) {
|
|
214
196
|
logger.info('Running: terraform plan ' + args.join(' ') + '\n');
|
|
215
197
|
try {
|
|
216
|
-
await this.cli.run('terraform', ['plan', ...args],
|
|
198
|
+
await this.cli.run('terraform', ['plan', ...args], { interactive: true });
|
|
217
199
|
}
|
|
218
200
|
catch (error) {
|
|
219
201
|
if (!(error instanceof ProcessException)) {
|
|
@@ -221,13 +203,13 @@ export class TerraformAdapter {
|
|
|
221
203
|
}
|
|
222
204
|
if (attemptNo < MAX_ATTEMPTS && RETRYABLE_ERRORS.some(err => error.message.includes(err))) {
|
|
223
205
|
logger.warn(`Retrying terraform plan due to error: ${error.message}`);
|
|
224
|
-
return this._plan(args,
|
|
206
|
+
return this._plan(args, attemptNo + 1);
|
|
225
207
|
}
|
|
226
208
|
const lockId = this.lockId(error, attemptNo);
|
|
227
209
|
if (lockId) {
|
|
228
210
|
await this.promptUnlock(lockId);
|
|
229
211
|
logger.info('State unlocked, retrying terraform plan');
|
|
230
|
-
return this._plan(args,
|
|
212
|
+
return this._plan(args, attemptNo + 1);
|
|
231
213
|
}
|
|
232
214
|
throw new KnownException(`terraform plan failed with code ${error.code}:\n${error.message}`, { cause: error });
|
|
233
215
|
}
|
|
@@ -250,13 +232,13 @@ export class TerraformAdapter {
|
|
|
250
232
|
this.printTime();
|
|
251
233
|
const nowUtcSeconds = Math.floor(this.now() / 1000);
|
|
252
234
|
const gracePeriod = 10;
|
|
253
|
-
const
|
|
254
|
-
if (
|
|
235
|
+
const timeoutMs = (ttl - nowUtcSeconds - gracePeriod) * 1000;
|
|
236
|
+
if (timeoutMs <= 0) {
|
|
255
237
|
throw new KnownException('TTL expired before terraform apply could start');
|
|
256
238
|
}
|
|
257
|
-
logger.debug('timeout(ms): ' +
|
|
239
|
+
logger.debug('timeout(ms): ' + timeoutMs);
|
|
258
240
|
try {
|
|
259
|
-
await this.cli.run('terraform', ['apply', ...args],
|
|
241
|
+
await this.cli.run('terraform', ['apply', ...args], { timeoutMs });
|
|
260
242
|
this.printTime();
|
|
261
243
|
}
|
|
262
244
|
catch (error) {
|
|
@@ -282,14 +264,14 @@ export class TerraformAdapter {
|
|
|
282
264
|
}
|
|
283
265
|
async _destroy(args, force, attemptNo = 1) {
|
|
284
266
|
let wrongDir = false;
|
|
285
|
-
const
|
|
267
|
+
const outScanner = (line) => {
|
|
286
268
|
if (line.includes('Either you have not created any objects yet or the existing objects were')) {
|
|
287
269
|
wrongDir = true;
|
|
288
270
|
}
|
|
289
271
|
};
|
|
290
|
-
logger.info('Running: terraform destroy -auto-approve' + args.join(' ') + '\n');
|
|
272
|
+
logger.info('Running: terraform destroy -auto-approve ' + args.join(' ') + '\n');
|
|
291
273
|
try {
|
|
292
|
-
await this.cli.run('terraform', ['destroy', '-auto-approve', ...args],
|
|
274
|
+
await this.cli.run('terraform', ['destroy', '-auto-approve', ...args], { outScanner, interactive: true });
|
|
293
275
|
}
|
|
294
276
|
catch (error) {
|
|
295
277
|
if (!(error instanceof ProcessException)) {
|