@agilecustoms/envctl 0.11.0 → 0.12.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.
@@ -19,6 +19,7 @@ export class TerraformAdapter {
19
19
  console.log(`\nTime EST: ${est}, UTC: ${utc}\n`);
20
20
  }
21
21
  async init(key, cwd) {
22
+ console.log('Run terraform init to download providers, this doesn\'t create any resources in AWS even S3 object');
22
23
  let emptyDir = false;
23
24
  const scanner = (line) => {
24
25
  if (line.includes('Terraform initialized in an empty directory!')) {
@@ -40,22 +41,40 @@ export class TerraformAdapter {
40
41
  throw new KnownException('Can not find terraform files. Command needs to be run in a directory with terraform files');
41
42
  }
42
43
  }
43
- tfArgs(envAttrs, tfArgs) {
44
+ tfArgs(envDto, tfArgs) {
44
45
  return [
45
- `-var=env=${envAttrs.env}`,
46
- `-var=owner=${envAttrs.owner}`,
47
- `-var=env_size=${envAttrs.size}`,
48
- `-var=env_type=${envAttrs.type}`,
46
+ `-var=env=${envDto.env}`,
47
+ `-var=owner=${envDto.owner}`,
48
+ `-var=env_size=${envDto.size}`,
49
+ `-var=env_type=${envDto.type}`,
49
50
  ...tfArgs
50
51
  ];
51
52
  }
52
- async plan(envAttrs, tfArgs, cwd) {
53
- const args = this.tfArgs(envAttrs, tfArgs);
53
+ async plan(envDto, tfArgs, cwd, attemptNo = 1) {
54
+ const args = this.tfArgs(envDto, tfArgs);
54
55
  console.log('Running: terraform plan -auto-approve', ...args);
55
- await this.processRunner.run('terraform', ['plan', ...args], cwd);
56
+ try {
57
+ await this.processRunner.run('terraform', ['plan', ...args], cwd);
58
+ }
59
+ catch (error) {
60
+ if (error instanceof ProcessException) {
61
+ if (attemptNo < MAX_ATTEMPTS && RETRYABLE_ERRORS.some(err => error.message.includes(err))) {
62
+ console.warn(`Retrying terraform plan due to error: ${error.message}`);
63
+ return this.plan(envDto, tfArgs, cwd, attemptNo + 1);
64
+ }
65
+ const lockId = this.lockId(attemptNo, error);
66
+ if (lockId) {
67
+ await this.promptUnlock(lockId, cwd);
68
+ console.info('State unlocked, retrying terraform plan');
69
+ return this.plan(envDto, tfArgs, cwd, attemptNo + 1);
70
+ }
71
+ throw new KnownException(`terraform plan failed with code ${error.code}:\n${error.message}`, { cause: error });
72
+ }
73
+ throw error;
74
+ }
56
75
  }
57
- async deploy(envAttrs, tfArgs, cwd, attemptNo = 1) {
58
- const args = this.tfArgs(envAttrs, tfArgs);
76
+ async deploy(envDto, tfArgs, cwd, attemptNo = 1) {
77
+ const args = this.tfArgs(envDto, tfArgs);
59
78
  console.log('Running: terraform apply -auto-approve', ...args);
60
79
  this.printTime();
61
80
  try {
@@ -66,30 +85,36 @@ export class TerraformAdapter {
66
85
  if (error instanceof ProcessException) {
67
86
  if (attemptNo < MAX_ATTEMPTS && RETRYABLE_ERRORS.some(err => error.message.includes(err))) {
68
87
  console.warn(`Retrying terraform apply due to error: ${error.message}`);
69
- return this.deploy(envAttrs, tfArgs, cwd, attemptNo + 1);
88
+ return this.deploy(envDto, tfArgs, cwd, attemptNo + 1);
70
89
  }
71
- if (attemptNo < MAX_ATTEMPTS && error.message.includes('Error acquiring the state lock')) {
72
- const match = error.message.match(/ID:\s+([a-f0-9-]{36})/i);
73
- if (match) {
74
- console.error(error.message);
75
- const answerYes = await promptYesNo('Terraform state is locked. Most often due to previously interrupted process.\n'
76
- + 'Rarely (main lock intention) is when another process actively work with the state to avoid inconsistent modification.\n'
77
- + 'Do you want to force unlock? (y/n): ');
78
- if (answerYes) {
79
- console.log('Force unlocking state');
80
- const id = match[1];
81
- await this.processRunner.run('terraform', ['force-unlock', '-force', id], cwd);
82
- console.info('State unlocked, retrying terraform apply');
83
- return this.deploy(envAttrs, tfArgs, cwd, attemptNo + 1);
84
- }
85
- else {
86
- throw new ExitException();
87
- }
88
- }
90
+ const lockId = this.lockId(attemptNo, error);
91
+ if (lockId) {
92
+ await this.promptUnlock(lockId, cwd);
93
+ console.info('State unlocked, retrying terraform apply');
94
+ return this.deploy(envDto, tfArgs, cwd, attemptNo + 1);
89
95
  }
90
96
  throw new KnownException(`terraform apply failed with code ${error.code}:\n${error.message}`, { cause: error });
91
97
  }
92
98
  throw error;
93
99
  }
94
100
  }
101
+ lockId(attemptNo, error) {
102
+ if (attemptNo < MAX_ATTEMPTS && error.message.includes('Error acquiring the state lock')) {
103
+ const match = error.message.match(/ID:\s+([a-f0-9-]{36})/i);
104
+ if (match) {
105
+ console.error(error.message);
106
+ return match[1];
107
+ }
108
+ }
109
+ }
110
+ async promptUnlock(id, cwd) {
111
+ const answerYes = await promptYesNo('Terraform state is locked. Most often due to previously interrupted process.\n'
112
+ + 'Rarely (main lock intention) is when another process actively work with the state to avoid inconsistent modification.\n'
113
+ + 'Do you want to force unlock?: ');
114
+ if (!answerYes) {
115
+ throw new ExitException();
116
+ }
117
+ console.log('Force unlocking state');
118
+ await this.processRunner.run('terraform', ['force-unlock', '-force', id], cwd);
119
+ }
95
120
  }
@@ -43,15 +43,13 @@ export async function parseEnvDto(options) {
43
43
  envSize = ensureEnumValue(EnvSize, size, 'size');
44
44
  }
45
45
  else {
46
- const answer = await inquirer.prompt([
47
- {
46
+ const answer = await inquirer.prompt([{
48
47
  type: 'list',
49
48
  name: 'size',
50
49
  message: 'Environment size:',
51
50
  choices: Object.values(EnvSize),
52
51
  default: EnvSize.Small,
53
- }
54
- ]);
52
+ }]);
55
53
  envSize = answer.size;
56
54
  }
57
55
  const envType = ensureEnumValue(EnvType, type, 'type');
@@ -25,14 +25,12 @@ export function wrap(callable) {
25
25
  };
26
26
  }
27
27
  export async function promptYesNo(message, defaultValue = false) {
28
- const { answer } = await inquirer.prompt([
29
- {
28
+ const { answer } = await inquirer.prompt([{
30
29
  type: 'confirm',
31
30
  name: 'answer',
32
31
  message,
33
32
  default: defaultValue,
34
- },
35
- ]);
33
+ }]);
36
34
  return answer;
37
35
  }
38
36
  export function ensureKind(kind, cwd) {
@@ -12,7 +12,13 @@ export class EnvCtl {
12
12
  return project ? `${project}-${env}` : env;
13
13
  }
14
14
  async plan(envDto, tfArgs, cwd) {
15
- console.log('Deploying resources');
15
+ const key = this.key(envDto.project, envDto.env);
16
+ console.log(`Check if env ${key} already exists`);
17
+ const env = await this.envApi.get(key);
18
+ if (env !== null) {
19
+ this.checkEnv(env, envDto);
20
+ }
21
+ await this.terraformAdapter.init(key, cwd);
16
22
  await this.terraformAdapter.plan(envDto, tfArgs, cwd);
17
23
  }
18
24
  async deploy(envDto, tfArgs, cwd) {
@@ -21,7 +27,6 @@ export class EnvCtl {
21
27
  const env = await this.envApi.get(key);
22
28
  if (env === null) {
23
29
  console.log(`Env ${key} does not exist, creating it`);
24
- console.log('First run terraform init to download providers, this doesn\'t create any resources in AWS even S3 object');
25
30
  await this.terraformAdapter.init(key, cwd);
26
31
  console.log('Creating env tracking record in DynamoDB');
27
32
  const { owner, size, type, kind } = envDto;
@@ -29,21 +34,7 @@ export class EnvCtl {
29
34
  await this.envApi.create(createEnv);
30
35
  return await this.runDeploy(key, envDto, tfArgs, cwd);
31
36
  }
32
- if (env.ephemeral) {
33
- throw new KnownException(`Attempted to convert ephemeral env ${key} to non-ephemeral`);
34
- }
35
- if (env.owner !== envDto.owner) {
36
- throw new KnownException(`Can not change env owner ${env.owner} -> ${envDto.owner}`);
37
- }
38
- if (env.size !== envDto.size) {
39
- throw new KnownException(`Can not change env size ${env.size} -> ${envDto.size}`);
40
- }
41
- if (env.type !== envDto.type) {
42
- throw new KnownException(`Can not change env type ${env.type} -> ${envDto.type}`);
43
- }
44
- if (env.kind !== envDto.kind) {
45
- throw new KnownException(`Can not change env kind ${env.kind} -> ${envDto.kind}`);
46
- }
37
+ this.checkEnv(env, envDto);
47
38
  switch (env.status) {
48
39
  case EnvStatus.Creating:
49
40
  case EnvStatus.Updating: {
@@ -58,14 +49,28 @@ export class EnvCtl {
58
49
  console.log('Env status is ACTIVE\nWill lock for update and run terraform apply (to update resources)');
59
50
  await this.envApi.lockForUpdate(key);
60
51
  await this.runDeploy(key, envDto, tfArgs, cwd);
61
- break;
62
- }
63
- case EnvStatus.Deleting: {
64
- const time = EnvSizeAvgTime[env.size];
65
- throw new KnownException(`Env status is DELETING, please wait (~${time} min)`);
66
52
  }
67
- default:
68
- throw new KnownException(`Unsupported environment status: ${env.status}`);
53
+ }
54
+ }
55
+ checkEnv(env, envDto) {
56
+ if (env.ephemeral) {
57
+ throw new KnownException(`Attempted to convert ephemeral env to non-ephemeral`);
58
+ }
59
+ if (env.owner !== envDto.owner) {
60
+ throw new KnownException(`Can not change env owner ${env.owner} -> ${envDto.owner}`);
61
+ }
62
+ if (env.size !== envDto.size) {
63
+ throw new KnownException(`Can not change env size ${env.size} -> ${envDto.size}`);
64
+ }
65
+ if (env.type !== envDto.type) {
66
+ throw new KnownException(`Can not change env type ${env.type} -> ${envDto.type}`);
67
+ }
68
+ if (env.kind !== envDto.kind) {
69
+ throw new KnownException(`Can not change env kind ${env.kind} -> ${envDto.kind}`);
70
+ }
71
+ if (env.status === EnvStatus.Deleting) {
72
+ const time = EnvSizeAvgTime[env.size];
73
+ throw new KnownException(`Env status is DELETING, please wait (~${time} min)`);
69
74
  }
70
75
  }
71
76
  async runDeploy(key, envDto, tfArgs, cwd) {
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.11.0",
4
+ "version": "0.12.0",
5
5
  "author": "Alex Chekulaev",
6
6
  "type": "module",
7
7
  "bin": {