@agilecustoms/envctl 1.25.1 → 1.26.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 +12 -37
- package/dist/client/HttpClient.js +9 -2
- package/dist/exceptions.js +2 -2
- package/dist/logger.js +7 -1
- package/dist/service/EnvService.js +23 -39
- package/dist/service/TerraformAdapter.js +2 -5
- package/dist/util.js +4 -2
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { KnownException, NotFoundException, UnauthorizedException } from '../exceptions.js';
|
|
2
2
|
import { logger } from '../logger.js';
|
|
3
3
|
import { EnvStatus } from '../model/index.js';
|
|
4
|
-
import {
|
|
4
|
+
import { format, toDate } from '../util.js';
|
|
5
5
|
import { HttpClient } from './HttpClient.js';
|
|
6
6
|
export class EnvApiClient {
|
|
7
7
|
httpClient;
|
|
@@ -12,8 +12,9 @@ export class EnvApiClient {
|
|
|
12
12
|
return await fetch(url);
|
|
13
13
|
}
|
|
14
14
|
async get(key) {
|
|
15
|
+
logger.info(`Retrieve environment ${key}`);
|
|
15
16
|
try {
|
|
16
|
-
return await this.httpClient.fetch(`/
|
|
17
|
+
return await this.httpClient.fetch(`/env/${key}`);
|
|
17
18
|
}
|
|
18
19
|
catch (error) {
|
|
19
20
|
if (error instanceof NotFoundException) {
|
|
@@ -28,37 +29,20 @@ export class EnvApiClient {
|
|
|
28
29
|
result = await this.httpClient.post('/ci/env-ephemeral', env);
|
|
29
30
|
}
|
|
30
31
|
catch (error) {
|
|
31
|
-
if (error instanceof
|
|
32
|
-
throw new KnownException(`
|
|
32
|
+
if (error instanceof UnauthorizedException) {
|
|
33
|
+
throw new KnownException(`Developers are no authorized to create ephemeral environment\nPlease create dedicated CI user (with its own API key) per each pipeline`);
|
|
33
34
|
}
|
|
34
35
|
throw error;
|
|
35
36
|
}
|
|
36
37
|
return this.httpClient.getUrl(`/public/env/${env.key}/extend?token=${result.token}`);
|
|
37
38
|
}
|
|
38
39
|
async create(env) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
result = await this.httpClient.post('/ci/env', env);
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
if (error instanceof BusinessException) {
|
|
45
|
-
throw new KnownException(`Failed to create environment: ${error.message}`);
|
|
46
|
-
}
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
logger.debug(`Env ${env.key} created with TTL ${toLocalTime(result.ttl)}`);
|
|
40
|
+
const result = await this.httpClient.post('/env', env);
|
|
41
|
+
logger.debug(`Environment ${env.key} created, will expire at ${format(toDate(result.ttl))}`);
|
|
50
42
|
return { ...env, ephemeral: false, status: EnvStatus.Deploying, ttl: result.ttl };
|
|
51
43
|
}
|
|
52
44
|
async activate(env) {
|
|
53
|
-
|
|
54
|
-
await this.httpClient.post(`/ci/env/${env.key}/activate`);
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
if (error instanceof BusinessException) {
|
|
58
|
-
throw new KnownException(`Failed to activate environment ${env.key}: ${error.message}`);
|
|
59
|
-
}
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
45
|
+
await this.httpClient.post(`/env/${env.key}/activate`);
|
|
62
46
|
env.status = EnvStatus.Active;
|
|
63
47
|
}
|
|
64
48
|
async lockForUpdate(env) {
|
|
@@ -67,23 +51,14 @@ export class EnvApiClient {
|
|
|
67
51
|
stateFile: env.stateFile,
|
|
68
52
|
lockFile: env.lockFile
|
|
69
53
|
};
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
result = await this.httpClient.post(`/ci/env/${env.key}/lock-for-update`, body);
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
if (error instanceof BusinessException) {
|
|
76
|
-
throw new KnownException(`Failed to lock environment ${env.key} for update: ${error.message}`);
|
|
77
|
-
}
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
54
|
+
const result = await this.httpClient.post(`/env/${env.key}/lock-for-update`, body);
|
|
80
55
|
env.status = result.status;
|
|
81
56
|
}
|
|
82
57
|
async delete(key, force = false) {
|
|
83
58
|
logger.info('Sending delete command');
|
|
84
59
|
let result;
|
|
85
60
|
try {
|
|
86
|
-
result = await this.httpClient.fetch(`/
|
|
61
|
+
result = await this.httpClient.fetch(`/env/${key}?force=${force}`, {
|
|
87
62
|
method: 'DELETE'
|
|
88
63
|
});
|
|
89
64
|
}
|
|
@@ -98,7 +73,7 @@ export class EnvApiClient {
|
|
|
98
73
|
async getLogs(key) {
|
|
99
74
|
let result;
|
|
100
75
|
try {
|
|
101
|
-
result = await this.httpClient.fetch(`/
|
|
76
|
+
result = await this.httpClient.fetch(`/env/${key}/logs`);
|
|
102
77
|
}
|
|
103
78
|
catch (error) {
|
|
104
79
|
if (error instanceof NotFoundException) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HttpException, KnownException, NotFoundException, UnauthorizedException } from '../exceptions.js';
|
|
2
2
|
import { logger } from '../logger.js';
|
|
3
3
|
import { DEFAULT_HOST } from '../service/ConfigService.js';
|
|
4
4
|
import { ConfigService } from '../service/index.js';
|
|
@@ -63,6 +63,7 @@ export class HttpClient {
|
|
|
63
63
|
}
|
|
64
64
|
throw new Error('Error (network?) making the request:', { cause: error });
|
|
65
65
|
}
|
|
66
|
+
logger.debug(`response status: ${response.status}, text: ${response.statusText}`);
|
|
66
67
|
const contentType = response.headers?.get('Content-Type') || '';
|
|
67
68
|
let body = await response.text();
|
|
68
69
|
if (contentType.includes('application/json')) {
|
|
@@ -79,11 +80,17 @@ export class HttpClient {
|
|
|
79
80
|
if (response.ok) {
|
|
80
81
|
return body;
|
|
81
82
|
}
|
|
83
|
+
if (response.status === 401) {
|
|
84
|
+
throw new KnownException('API key is not recognized');
|
|
85
|
+
}
|
|
86
|
+
if (response.status === 403) {
|
|
87
|
+
throw new UnauthorizedException(JSON.stringify(body));
|
|
88
|
+
}
|
|
82
89
|
if (response.status === 404) {
|
|
83
90
|
throw new NotFoundException(body);
|
|
84
91
|
}
|
|
85
92
|
if (response.status === 422) {
|
|
86
|
-
throw new
|
|
93
|
+
throw new KnownException(body.message);
|
|
87
94
|
}
|
|
88
95
|
if (response.status === 426) {
|
|
89
96
|
throw new KnownException(body.message);
|
package/dist/exceptions.js
CHANGED
|
@@ -16,10 +16,10 @@ export class HttpException extends Error {
|
|
|
16
16
|
this.name = 'HttpException';
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
export class
|
|
19
|
+
export class UnauthorizedException extends HttpException {
|
|
20
20
|
constructor(message) {
|
|
21
21
|
super(message);
|
|
22
|
-
this.name = '
|
|
22
|
+
this.name = 'UnauthorizedException';
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
export class NotFoundException extends HttpException {
|
package/dist/logger.js
CHANGED
|
@@ -22,7 +22,13 @@ export class Logger {
|
|
|
22
22
|
this.log('error', msg);
|
|
23
23
|
}
|
|
24
24
|
log(level, msg) {
|
|
25
|
-
const date = new Date(this.now()).
|
|
25
|
+
const date = new Date(this.now()).toLocaleString('en-US', {
|
|
26
|
+
hour: '2-digit',
|
|
27
|
+
minute: '2-digit',
|
|
28
|
+
second: '2-digit',
|
|
29
|
+
fractionalSecondDigits: 3,
|
|
30
|
+
hour12: false,
|
|
31
|
+
});
|
|
26
32
|
const detailedMessage = `${date} [${level}] ${msg}`;
|
|
27
33
|
this.buffer.push(detailedMessage);
|
|
28
34
|
if (!this.isDebugEnabled() && level === 'debug') {
|
|
@@ -3,7 +3,7 @@ import { EnvApiClient } from '../client/index.js';
|
|
|
3
3
|
import { KnownException } from '../exceptions.js';
|
|
4
4
|
import { logger } from '../logger.js';
|
|
5
5
|
import { EnvStatus } from '../model/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { format, toDate } from '../util.js';
|
|
7
7
|
import { BaseService } from './BaseService.js';
|
|
8
8
|
import { ApplyMode, getApplyMode } from './TerraformAdapter.js';
|
|
9
9
|
export class EnvService extends BaseService {
|
|
@@ -16,34 +16,22 @@ export class EnvService extends BaseService {
|
|
|
16
16
|
}
|
|
17
17
|
async status(key) {
|
|
18
18
|
key = this.getKey(key);
|
|
19
|
-
logger.info(`Retrieve env ${key}`);
|
|
20
19
|
const env = await this.envApi.get(key);
|
|
21
20
|
if (env === null) {
|
|
22
|
-
logger.info(`
|
|
21
|
+
logger.info(`Environment ${key} does not exist`);
|
|
23
22
|
return;
|
|
24
23
|
}
|
|
25
|
-
logger.info(`
|
|
24
|
+
logger.info(`Environment ${key} status: ${env.status}`);
|
|
26
25
|
if (env.status !== EnvStatus.Deleting && env.status !== EnvStatus.DeleteError) {
|
|
27
|
-
logger.info(`Expires at ${
|
|
26
|
+
logger.info(`Expires at ${format(toDate(env.ttl))}`);
|
|
28
27
|
}
|
|
29
28
|
if (env.dir) {
|
|
30
29
|
const dir = this.cli.getDir();
|
|
31
30
|
if (env.dir !== dir) {
|
|
32
|
-
logger.warn(`
|
|
31
|
+
logger.warn(`Environment ${key} was deployed from ${env.dir}`);
|
|
33
32
|
}
|
|
34
33
|
}
|
|
35
34
|
}
|
|
36
|
-
async getEnv(key) {
|
|
37
|
-
logger.info(`Check if env ${key} already exists`);
|
|
38
|
-
const env = await this.envApi.get(key);
|
|
39
|
-
if (env) {
|
|
40
|
-
logger.info(`Env ${key} status: ${env.status}`);
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
logger.info(`Env ${key} does not exist`);
|
|
44
|
-
}
|
|
45
|
-
return env;
|
|
46
|
-
}
|
|
47
35
|
async init(tfArgs) {
|
|
48
36
|
await this.terraformAdapter.init(tfArgs);
|
|
49
37
|
await this.terraformAdapter.lockProviders();
|
|
@@ -64,7 +52,7 @@ export class EnvService extends BaseService {
|
|
|
64
52
|
if (env.dir) {
|
|
65
53
|
const dir = this.cli.getDir();
|
|
66
54
|
if (dir !== env.dir) {
|
|
67
|
-
throw new KnownException(`
|
|
55
|
+
throw new KnownException(`Environment ${env.key} was deployed from ${env.dir} - make sure you run this command from the same directory`);
|
|
68
56
|
}
|
|
69
57
|
}
|
|
70
58
|
}
|
|
@@ -72,27 +60,29 @@ export class EnvService extends BaseService {
|
|
|
72
60
|
const { stateFile, stateUpdated } = this.terraformAdapter.validateAndGetStateFile();
|
|
73
61
|
const key = this.terraformAdapter.getKey();
|
|
74
62
|
const { lockFile, lockUpdated } = this.terraformAdapter.validateAndGetLockFile();
|
|
75
|
-
|
|
76
|
-
let env = await this.getEnv(key);
|
|
63
|
+
let env = await this.envApi.get(key);
|
|
77
64
|
if (env === null) {
|
|
65
|
+
logger.info(`Environment ${key} does not exist`);
|
|
78
66
|
const dir = this.cli.getDir();
|
|
79
67
|
const createEnv = { key, dir, stateFile, lockFile };
|
|
80
|
-
logger.info('Creating
|
|
68
|
+
logger.info('Creating environment metadata');
|
|
81
69
|
env = await this.envApi.create(createEnv);
|
|
82
70
|
await this.runApply(env, tfArgs);
|
|
83
71
|
return env;
|
|
84
72
|
}
|
|
73
|
+
logger.info(`Environment ${key} status: ${env.status}`);
|
|
85
74
|
this.checkDir(env);
|
|
86
75
|
if (env.status === EnvStatus.Deleting) {
|
|
87
|
-
throw new KnownException(`
|
|
76
|
+
throw new KnownException(`Environment ${env.key} status is DELETING, please wait.\n
|
|
88
77
|
If it is DELETING more than 10 minutes, you can try to run 'envctl delete ${env.key} --force' to remove it`);
|
|
89
78
|
}
|
|
90
79
|
if (env.status === EnvStatus.DeleteError) {
|
|
91
|
-
throw new KnownException(`
|
|
80
|
+
throw new KnownException(`Environment ${env.key} status is DELETE_ERROR, most likely lack of permissions.\n
|
|
92
81
|
Check logs with 'envctl logs', address the issue and re-run 'envctl delete ${env.key}'`);
|
|
93
82
|
}
|
|
94
83
|
const status = env.status;
|
|
95
84
|
if (status === EnvStatus.Init) {
|
|
85
|
+
const applyMode = getApplyMode(tfArgs);
|
|
96
86
|
if (applyMode === ApplyMode.Default) {
|
|
97
87
|
throw new KnownException('Either run apply with planfile or with flag -auto-approve');
|
|
98
88
|
}
|
|
@@ -115,9 +105,8 @@ export class EnvService extends BaseService {
|
|
|
115
105
|
}
|
|
116
106
|
async runApply(env, tfArgs) {
|
|
117
107
|
this.terraformAdapter.saveHashes();
|
|
118
|
-
logger.info('Deploying resources');
|
|
119
108
|
await this.terraformAdapter.apply(env, tfArgs);
|
|
120
|
-
logger.info('
|
|
109
|
+
logger.info('Activate environment');
|
|
121
110
|
await this.envApi.activate(env);
|
|
122
111
|
return env;
|
|
123
112
|
}
|
|
@@ -127,30 +116,25 @@ export class EnvService extends BaseService {
|
|
|
127
116
|
}
|
|
128
117
|
async destroy(tfArgs, force) {
|
|
129
118
|
const key = this.terraformAdapter.getKey();
|
|
130
|
-
const env = await this.get(key);
|
|
119
|
+
const env = await this.envApi.get(key);
|
|
120
|
+
if (!env) {
|
|
121
|
+
throw new KnownException(`Environment ${key} is not found`);
|
|
122
|
+
}
|
|
123
|
+
logger.info(`Environment status: ${env.status}`);
|
|
131
124
|
if (env.status === EnvStatus.Init) {
|
|
132
|
-
logger.info(`
|
|
125
|
+
logger.info(`Environment ${env.key} status is INIT - no resources, nothing to destroy, just deleting metadata`);
|
|
133
126
|
await this.envApi.delete(key, force);
|
|
134
127
|
return;
|
|
135
128
|
}
|
|
136
129
|
if (env.status === EnvStatus.Deleting && !force) {
|
|
137
|
-
throw new KnownException(`
|
|
130
|
+
throw new KnownException(`Environment ${env.key} status is DELETING, please wait or re-run with --force`);
|
|
138
131
|
}
|
|
139
132
|
if (env.status === EnvStatus.Active) {
|
|
140
|
-
logger.info('Lock
|
|
133
|
+
logger.info('Lock environment to run destroy');
|
|
141
134
|
await this.envApi.lockForUpdate(env);
|
|
142
135
|
}
|
|
143
136
|
await this.terraformAdapter.destroy(key, tfArgs, force);
|
|
144
137
|
await this.envApi.delete(key, force);
|
|
145
|
-
logger.info('Please wait for ~30 sec before you can create
|
|
146
|
-
}
|
|
147
|
-
async get(key) {
|
|
148
|
-
logger.info(`Retrieve env ${key}`);
|
|
149
|
-
const env = await this.envApi.get(key);
|
|
150
|
-
if (!env) {
|
|
151
|
-
throw new KnownException(`Environment ${key} is not found`);
|
|
152
|
-
}
|
|
153
|
-
logger.info(`Env status: ${env.status}`);
|
|
154
|
-
return env;
|
|
138
|
+
logger.info('Please wait for ~30 sec before you can create environment with same name');
|
|
155
139
|
}
|
|
156
140
|
}
|
|
@@ -5,6 +5,7 @@ import { confirm } from '@inquirer/prompts';
|
|
|
5
5
|
import {} from '../client/Cli.js';
|
|
6
6
|
import { AbortedException, KnownException, ProcessException, TimeoutException } from '../exceptions.js';
|
|
7
7
|
import { logger } from '../logger.js';
|
|
8
|
+
import { format } from '../util.js';
|
|
8
9
|
import { LocalStateService } from './LocalStateService.js';
|
|
9
10
|
export const MAX_ATTEMPTS = 2;
|
|
10
11
|
export const RETRYABLE_ERRORS = [
|
|
@@ -148,11 +149,7 @@ export class TerraformAdapter {
|
|
|
148
149
|
}
|
|
149
150
|
printTime() {
|
|
150
151
|
const now = new Date();
|
|
151
|
-
|
|
152
|
-
const edt = now.toLocaleString('en-US', {
|
|
153
|
-
timeZone: 'America/New_York'
|
|
154
|
-
});
|
|
155
|
-
logger.info(`\nTime EDT: ${edt}, UTC: ${utc}\n`);
|
|
152
|
+
logger.info(`\nLocal time: ${format(now)}, UTC: ${now.toISOString()}\n`);
|
|
156
153
|
}
|
|
157
154
|
tfArgs(key, args) {
|
|
158
155
|
const varDefs = this.getVarDefs();
|
package/dist/util.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agilecustoms/envctl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.0",
|
|
4
4
|
"description": "node.js CLI client for manage environments",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"terraform wrapper",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"-e1-": "",
|
|
75
75
|
"e1-desc": "ephemeral env in CI (must have ENVCTL_API_KEY_ in env vars)",
|
|
76
76
|
"e1": "CWD=../tt-core CI=true ENVCTL_HOME=non-existing ENVCTL_API_KEY=\"$ENVCTL_API_KEY_\" npm run it --",
|
|
77
|
-
"e1-create": "npm run e1 -- create-ephemeral e1",
|
|
77
|
+
"e1-create": "npm run e1 -- create-ephemeral e1 --verbose",
|
|
78
78
|
"e1-init": " npm run e1 -- init -reconfigure -upgrade -backend-config=key=e1",
|
|
79
79
|
"e1-plan": " npm run e1 -- plan -var=env_size=min -var=env=e1 -out=plan",
|
|
80
80
|
"e1-apply": " npm run e1 -- apply plan",
|