@agilecustoms/envctl 0.19.2 → 0.20.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.
@@ -17,16 +17,15 @@ export class EnvApiClient {
17
17
  throw error;
18
18
  }
19
19
  }
20
+ async createEphemeral(env) {
21
+ const res = await this.httpClient.post('/ci/env-ephemeral', env);
22
+ const token = res.token;
23
+ return { ...env, token, ephemeral: true, status: EnvStatus.Init };
24
+ }
20
25
  async create(env) {
21
- const res = await this.httpClient.fetch('/ci/env', {
22
- method: 'POST',
23
- body: JSON.stringify(env),
24
- headers: {
25
- 'Content-Type': 'application/json'
26
- }
27
- });
26
+ const res = await this.httpClient.post('/ci/env', env);
28
27
  const token = res.token;
29
- return { ...env, token, status: EnvStatus.Creating };
28
+ return { ...env, token, ephemeral: false, status: EnvStatus.Deploying };
30
29
  }
31
30
  async activate(env) {
32
31
  await this.httpClient.fetch(`/ci/env/${env.key}/activate`, {
@@ -35,10 +34,10 @@ export class EnvApiClient {
35
34
  env.status = EnvStatus.Active;
36
35
  }
37
36
  async lockForUpdate(env) {
38
- await this.httpClient.fetch(`/ci/env/${env.key}/lock-for-update`, {
37
+ const res = await this.httpClient.fetch(`/ci/env/${env.key}/lock-for-update`, {
39
38
  method: 'POST'
40
39
  });
41
- env.status = EnvStatus.Updating;
40
+ env.status = res.status;
42
41
  }
43
42
  async delete(env) {
44
43
  let result;
@@ -6,6 +6,16 @@ export class HttpClient {
6
6
  constructor(awsCredsHelper) {
7
7
  this.awsCredsHelper = awsCredsHelper;
8
8
  }
9
+ async post(path, body, options = {}) {
10
+ return await this.fetch(path, {
11
+ method: 'POST',
12
+ body: JSON.stringify(body),
13
+ headers: {
14
+ 'Content-Type': 'application/json'
15
+ },
16
+ ...options
17
+ });
18
+ }
9
19
  async fetch(path, options = {}) {
10
20
  const awsCreds = await this.awsCredsHelper.getCredentials();
11
21
  const requestOptions = {
@@ -36,19 +46,12 @@ export class HttpClient {
36
46
  return await response.text();
37
47
  }
38
48
  }
49
+ const message = await response.text();
39
50
  if (response.status === 404) {
40
- const message = await response.text();
41
51
  throw new NotFoundException(message);
42
52
  }
43
53
  if (response.status === 422) {
44
- let message = await response.text();
45
- let params;
46
- if (contentType.includes('application/json')) {
47
- const json = await response.json();
48
- message = json.message;
49
- params = json.params;
50
- }
51
- throw new BusinessException(message, params);
54
+ throw new BusinessException(message);
52
55
  }
53
56
  throw new HttpException(response.status, await response.text());
54
57
  }
@@ -2,13 +2,13 @@ import { Command } from 'commander';
2
2
  import { envCtl } from '../container.js';
3
3
  import { _keys } from './_keys.js';
4
4
  import { wrap } from './_utils.js';
5
- export function apiCreateEphemeral(program) {
5
+ export function createEphemeral(program) {
6
6
  program
7
- .command('api-create-ephemeral')
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
  .option('--project <project>', _keys.PROJECT)
10
10
  .requiredOption('--env <env>', _keys.ENV)
11
- .requiredOption('--owner <owner>', _keys.OWNER)
11
+ .option('--owner <owner>', _keys.OWNER)
12
12
  .requiredOption('--size <size>', _keys.SIZE)
13
13
  .requiredOption('--type <type>', _keys.TYPE)
14
14
  .action(wrap(handler));
@@ -1,5 +1,5 @@
1
- export { apiCreateEphemeral } from './apiCreateEphemeral.js';
2
1
  export { configure } from './configure.js';
2
+ export { createEphemeral } from './createEphemeral.js';
3
3
  export { deleteIt } from './delete.js';
4
4
  export { deploy } from './deploy.js';
5
5
  export { destroy } from './destroy.js';
@@ -22,11 +22,9 @@ export class HttpException extends Error {
22
22
  }
23
23
  export class BusinessException extends HttpException {
24
24
  message;
25
- params;
26
- constructor(message, params) {
25
+ constructor(message) {
27
26
  super(422, message);
28
27
  this.message = message;
29
- this.params = params;
30
28
  this.name = 'BusinessException';
31
29
  }
32
30
  }
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 { apiCreateEphemeral, configure, deleteIt, deploy, destroy, init, plan, status } from './commands/index.js';
5
+ import { configure, createEphemeral, deleteIt, deploy, destroy, init, plan, status } from './commands/index.js';
6
6
  const require = createRequire(import.meta.url);
7
7
  const pkg = require('../package.json');
8
8
  updateNotifier({ pkg, updateCheckInterval: 0 }).notify();
@@ -11,8 +11,8 @@ program
11
11
  .name('envctl')
12
12
  .description('CLI to manage environments')
13
13
  .version(pkg.version);
14
- apiCreateEphemeral(program);
15
14
  configure(program);
15
+ createEphemeral(program);
16
16
  deleteIt(program);
17
17
  deploy(program);
18
18
  destroy(program);
@@ -1,6 +1,7 @@
1
1
  export var EnvStatus;
2
2
  (function (EnvStatus) {
3
- EnvStatus["Creating"] = "CREATING";
3
+ EnvStatus["Init"] = "INIT";
4
+ EnvStatus["Deploying"] = "DEPLOYING";
4
5
  EnvStatus["Active"] = "ACTIVE";
5
6
  EnvStatus["Updating"] = "UPDATING";
6
7
  EnvStatus["Deleting"] = "DELETING";
@@ -1,6 +1,5 @@
1
1
  import { KnownException, TerraformInitRequired } from '../exceptions.js';
2
- import { EnvType, EnvSize } from '../model/index.js';
3
- import { EnvSizeAvgTime, EnvStatus } from '../model/index.js';
2
+ import { EnvSize, EnvSizeAvgTime, EnvStatus, EnvType } from '../model/index.js';
4
3
  export class EnvCtl {
5
4
  cliHelper;
6
5
  envApi;
@@ -46,7 +45,7 @@ export class EnvCtl {
46
45
  }
47
46
  return { envName, owner, key, env };
48
47
  }
49
- checkInput(envName, env, input, cwd) {
48
+ checkInput(env, input, cwd) {
50
49
  if (input.owner && env.owner !== input.owner) {
51
50
  throw new KnownException(`Can not change env owner ${env.owner} -> ${input.owner}`);
52
51
  }
@@ -60,13 +59,8 @@ export class EnvCtl {
60
59
  if ((input.kind || env.kind) && kind !== env.kind) {
61
60
  throw new KnownException(`Can not change env kind ${env.kind} -> ${kind}`);
62
61
  }
63
- const { owner, size, type } = env;
64
- return {
65
- project: input.project, env: envName,
66
- owner, size, type, kind,
67
- };
68
62
  }
69
- checkStatus(env, availableStatuses = [EnvStatus.Active, EnvStatus.Creating, EnvStatus.Updating]) {
63
+ checkStatus(env, availableStatuses = [EnvStatus.Init, EnvStatus.Deploying, EnvStatus.Active, EnvStatus.Updating]) {
70
64
  if (availableStatuses.includes(env.status)) {
71
65
  return;
72
66
  }
@@ -89,8 +83,9 @@ export class EnvCtl {
89
83
  await this.terraformAdapter.plan(envTerraform, tfArgs, cwd);
90
84
  return;
91
85
  }
92
- const envInput = this.checkInput(envName, env, input, cwd);
93
- const envTerraform = { ...envInput, ephemeral: false };
86
+ this.checkInput(env, input, cwd);
87
+ const envOwner = env.owner;
88
+ const envTerraform = { ...env, env: envName, owner: envOwner, ephemeral: false };
94
89
  this.checkStatus(env);
95
90
  await this.promptUnlock(env);
96
91
  if (env.status !== EnvStatus.Active) {
@@ -104,8 +99,8 @@ export class EnvCtl {
104
99
  if (env !== null) {
105
100
  throw new KnownException(`Env ${key} already exists`);
106
101
  }
107
- const createEnv = { key, ephemeral: true, owner, size, type };
108
- const newEnv = await this.envApi.create(createEnv);
102
+ const createEnv = { key, owner, size, type };
103
+ const newEnv = await this.envApi.createEphemeral(createEnv);
109
104
  return newEnv.token;
110
105
  }
111
106
  async deploy(input, tfArgs, cwd) {
@@ -114,13 +109,16 @@ export class EnvCtl {
114
109
  const envInput = await this.cliHelper.parseEnvInput(input, envName, owner, cwd);
115
110
  console.log('Creating env tracking record in DynamoDB');
116
111
  const { size, type, kind } = envInput;
117
- const createEnv = { key, ephemeral: false, owner: envInput.owner, size, type, kind };
112
+ const createEnv = { key, owner: envInput.owner, size, type, kind };
118
113
  const newEnv = await this.envApi.create(createEnv);
119
114
  return await this.runDeploy(newEnv, envName, tfArgs, cwd);
120
115
  }
121
- this.checkInput(envName, env, input, cwd);
116
+ this.checkInput(env, input, cwd);
122
117
  this.checkStatus(env);
123
- if (env.status == EnvStatus.Updating) {
118
+ if (env.status === EnvStatus.Init) {
119
+ await this.envApi.lockForUpdate(env);
120
+ }
121
+ else if (env.status == EnvStatus.Updating) {
124
122
  const answerYes = await this.cliHelper.promptYesNo(`Env status is ${env.status}, likely to be an error from a previous run\n`
125
123
  + 'Do you want to proceed with deployment?');
126
124
  if (!answerYes) {
@@ -137,7 +135,7 @@ export class EnvCtl {
137
135
  async runDeploy(env, envName, tfArgs, cwd) {
138
136
  console.log('Deploying resources');
139
137
  const { owner, size, type, ephemeral } = env;
140
- const envTf = { owner, size, type, ephemeral, env: envName };
138
+ const envTf = { owner: owner || 'system', size, type, ephemeral, env: envName };
141
139
  try {
142
140
  await this.terraformAdapter.apply(envTf, tfArgs, cwd);
143
141
  }
@@ -156,9 +154,14 @@ export class EnvCtl {
156
154
  async delete(project, envName, force, cwd) {
157
155
  const env = await this.get(project, envName);
158
156
  this.checkStatus(env);
157
+ if (env.status === EnvStatus.Init) {
158
+ await this.envApi.delete(env);
159
+ console.log(`Env ${env.key} is deleted`);
160
+ return;
161
+ }
159
162
  if (force) {
160
163
  console.log('Force deletion');
161
- if (env.status !== EnvStatus.Active) {
164
+ if (env.status === EnvStatus.Deploying || env.status === EnvStatus.Updating) {
162
165
  console.log(`Env status is ${env.status}, will unlock it`);
163
166
  await this.envApi.activate(env);
164
167
  }
@@ -186,19 +189,26 @@ export class EnvCtl {
186
189
  async destroy(project, envName, tfArgs, force, cwd) {
187
190
  const env = await this.get(project, envName);
188
191
  this.checkStatus(env);
192
+ if (env.status === EnvStatus.Init) {
193
+ console.log(`Env ${env.key} has no resources, nothing to destroy, call 'delete' command instead`);
194
+ return;
195
+ }
189
196
  if (!force) {
190
197
  const kind = this.cliHelper.ensureKind(undefined, cwd);
191
198
  if (env.kind && env.kind !== kind) {
192
199
  throw new KnownException(`Env ${env.key} kind (dir-name): ${env.kind} - make sure you run destroy from the same directory`);
193
200
  }
194
- await this.promptUnlock(env, [EnvStatus.Creating]);
201
+ await this.promptUnlock(env, [EnvStatus.Deploying]);
202
+ if (env.status === EnvStatus.Deploying) {
203
+ return;
204
+ }
195
205
  }
196
206
  if (env.status === EnvStatus.Active) {
197
207
  console.log('Lock env to run destroy');
198
208
  await this.envApi.lockForUpdate(env);
199
209
  }
200
210
  const { owner, size, type, ephemeral } = env;
201
- const tfEnv = { owner, size, type, ephemeral, env: envName };
211
+ const tfEnv = { owner: owner || 'system', size, type, ephemeral, env: envName };
202
212
  console.log('Destroying env resources');
203
213
  await this.terraformAdapter.destroy(tfEnv, tfArgs, force, cwd);
204
214
  console.log('Unlock env');
@@ -216,13 +226,12 @@ export class EnvCtl {
216
226
  }
217
227
  return env;
218
228
  }
219
- async promptUnlock(env, statuses = [EnvStatus.Creating, EnvStatus.Updating]) {
229
+ async promptUnlock(env, statuses = [EnvStatus.Deploying, EnvStatus.Updating]) {
220
230
  if (statuses.includes(env.status)) {
221
231
  const answerYes = await this.cliHelper.promptYesNo(`Env status is ${env.status}, likely to be an error from a previous run\n`
222
232
  + 'Do you want to unlock it?');
223
233
  if (answerYes) {
224
234
  await this.envApi.activate(env);
225
- env.status = EnvStatus.Active;
226
235
  }
227
236
  }
228
237
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agilecustoms/envctl",
3
3
  "description": "node.js CLI client for manage environments",
4
- "version": "0.19.2",
4
+ "version": "0.20.1",
5
5
  "author": "Alex Chekulaev",
6
6
  "type": "module",
7
7
  "bin": {
@@ -33,12 +33,12 @@
33
33
  "run-delete": " tsc --sourceMap true && npm run run -- delete --cwd ../tt-core",
34
34
  "run-destroy": " tsc --sourceMap true && npm run run -- destroy --cwd ../tt-core",
35
35
  "-": "",
36
- "run-api-create-ephemeral": "tsc --sourceMap true && npm run run -- api-create-ephemeral --env test --owner laxa1986 --size min --type dev",
37
- "run-api-status": " tsc --sourceMap true && npm run run -- status --env test",
38
- "run-api-init": " tsc --sourceMap true && npm run run -- init --env test --cwd ../tt-gitops",
39
- "run-api-deploy": " tsc --sourceMap true && npm run run -- deploy --env test --cwd ../tt-gitops -var-file=versions.tfvars",
40
- "run-api-delete": " tsc --sourceMap true && npm run run -- delete --env test --cwd ../tt-gitops --force",
41
- "run-api-destroy": "tsc --sourceMap true && npm run run -- destroy --env test --cwd ../tt-gitops --force -var-file=versions.tfvars"
36
+ "run-ephemeral-create": " tsc --sourceMap true && npm run run -- create-ephemeral --env test --size min --type dev",
37
+ "run-ephemeral-status": " tsc --sourceMap true && npm run run -- status --env test",
38
+ "run-ephemeral-init": " tsc --sourceMap true && npm run run -- init --env test --cwd ../tt-gitops",
39
+ "run-ephemeral-deploy": " tsc --sourceMap true && npm run run -- deploy --env test --cwd ../tt-gitops -var-file=versions.tfvars",
40
+ "run-ephemeral-delete": " tsc --sourceMap true && npm run run -- delete --env test --cwd ../tt-gitops --force",
41
+ "run-ephemeral-destroy": "tsc --sourceMap true && npm run run -- destroy --env test --cwd ../tt-gitops --force -var-file=versions.tfvars"
42
42
  },
43
43
  "dependencies": {
44
44
  "@aws-sdk/client-sts": "^3.716.0",