@agilecustoms/envctl 0.20.1 → 0.21.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/README.md CHANGED
@@ -4,8 +4,7 @@
4
4
 
5
5
  ## usage
6
6
  ```shell
7
- envctl deploy --key tt-alexc --owner laxa1986 --size min --type dev \
8
- -var-file=versions.tfvars -var="env=tt-alex" -var="log_level=debug"
7
+ envctl deploy --env alexc -var-file=versions.tfvars -var="env=alexc" -var="log_level=debug"
9
8
  ```
10
9
 
11
10
  ## setup/update
@@ -1,11 +1,7 @@
1
1
  import path from 'path';
2
2
  import inquirer from 'inquirer';
3
3
  import { KnownException } from '../exceptions.js';
4
- import { EnvSize, EnvType } from '../model/index.js';
5
4
  import { ConfigService } from '../service/index.js';
6
- function isRunFromIdeaMakePlugin() {
7
- return !!process.env['XPC_SERVICE_NAME']?.includes('jetbrains');
8
- }
9
5
  export class CliHelper {
10
6
  configService;
11
7
  constructor(configService) {
@@ -51,49 +47,6 @@ export class CliHelper {
51
47
  console.log(`Inferred kind from directory name: ${kind}`);
52
48
  return kind;
53
49
  }
54
- async parseEnvInput(input, envName, owner, cwd) {
55
- owner = this.ensureOwner(owner);
56
- const { size } = input;
57
- let { type, kind } = input;
58
- kind = this.ensureKind(kind, cwd);
59
- let envSize;
60
- if (size) {
61
- envSize = ensureEnumValue(EnvSize, size, 'size');
62
- }
63
- else {
64
- const answer = await inquirer.prompt([
65
- isRunFromIdeaMakePlugin()
66
- ? {
67
- type: 'input',
68
- name: 'size',
69
- message: `Environment size [${Object.values(EnvSize).join(', ')}]:`,
70
- default: EnvSize.Small,
71
- validate: input => Object.values(EnvSize).includes(input.trim()) ? true : 'Invalid size'
72
- }
73
- : {
74
- type: 'list',
75
- name: 'size',
76
- message: 'Environment size:',
77
- choices: Object.values(EnvSize),
78
- default: EnvSize.Small,
79
- }
80
- ]);
81
- envSize = answer.size;
82
- }
83
- if (!type) {
84
- type = EnvType.Dev;
85
- console.log(`Default environment type: ${EnvType.Dev}`);
86
- }
87
- const envType = ensureEnumValue(EnvType, type || EnvType.Dev, 'type');
88
- return {
89
- project: input.project,
90
- env: envName,
91
- owner,
92
- size: envSize,
93
- type: envType,
94
- kind
95
- };
96
- }
97
50
  }
98
51
  function getDirName(cwd) {
99
52
  cwd = resolveCwd(cwd);
@@ -108,10 +61,3 @@ function resolveCwd(cwd) {
108
61
  }
109
62
  return path.resolve(process.cwd(), cwd);
110
63
  }
111
- function ensureEnumValue(enumObj, value, name) {
112
- const values = Object.values(enumObj);
113
- if (!values.includes(value)) {
114
- throw new KnownException(`Invalid ${name}: "${value}". Must be one of: ${values.join(', ')}`);
115
- }
116
- return value;
117
- }
@@ -19,24 +19,18 @@ export class EnvApiClient {
19
19
  }
20
20
  async createEphemeral(env) {
21
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 };
22
+ return res.token;
24
23
  }
25
24
  async create(env) {
26
- const res = await this.httpClient.post('/ci/env', env);
27
- const token = res.token;
28
- return { ...env, token, ephemeral: false, status: EnvStatus.Deploying };
25
+ await this.httpClient.post('/ci/env', env);
26
+ return { ...env, ephemeral: false, status: EnvStatus.Deploying };
29
27
  }
30
28
  async activate(env) {
31
- await this.httpClient.fetch(`/ci/env/${env.key}/activate`, {
32
- method: 'POST'
33
- });
29
+ await this.httpClient.post(`/ci/env/${env.key}/activate`);
34
30
  env.status = EnvStatus.Active;
35
31
  }
36
32
  async lockForUpdate(env) {
37
- const res = await this.httpClient.fetch(`/ci/env/${env.key}/lock-for-update`, {
38
- method: 'POST'
39
- });
33
+ const res = await this.httpClient.post(`/ci/env/${env.key}/lock-for-update`);
40
34
  env.status = res.status;
41
35
  }
42
36
  async delete(env) {
@@ -55,4 +49,8 @@ export class EnvApiClient {
55
49
  env.status = EnvStatus.Deleting;
56
50
  return result.statusText;
57
51
  }
52
+ async setVars(env, vars) {
53
+ await this.httpClient.post(`/ci/env/${env.key}/vars`, vars);
54
+ env.vars = { ...env.vars || {}, ...vars };
55
+ }
58
56
  }
@@ -7,14 +7,16 @@ export class HttpClient {
7
7
  this.awsCredsHelper = awsCredsHelper;
8
8
  }
9
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
- });
10
+ options.method = 'POST';
11
+ if (body) {
12
+ options.body = JSON.stringify(body);
13
+ const headers = options.headers || {};
14
+ if (!headers['Content-Type']) {
15
+ headers['Content-Type'] = 'application/json';
16
+ }
17
+ options.headers = headers;
18
+ }
19
+ return await this.fetch(path, options);
18
20
  }
19
21
  async fetch(path, options = {}) {
20
22
  const awsCreds = await this.awsCredsHelper.getCredentials();
@@ -53,6 +55,6 @@ export class HttpClient {
53
55
  if (response.status === 422) {
54
56
  throw new BusinessException(message);
55
57
  }
56
- throw new HttpException(response.status, await response.text());
58
+ throw new HttpException(response.status, message);
57
59
  }
58
60
  }
@@ -1,16 +1,17 @@
1
1
  import { spawn } from 'child_process';
2
2
  import path from 'path';
3
+ import * as readline from 'readline';
3
4
  import { fileURLToPath } from 'url';
4
5
  import { ProcessException } from '../exceptions.js';
5
6
  export class ProcessRunner {
6
7
  async runScript(script, args, cwd, scanner) {
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
9
  const scriptPath = path.join(__dirname, `../../scripts/${script}`);
9
- return this.run(scriptPath, args, cwd, scanner);
10
+ await this.run(scriptPath, args, cwd, scanner);
10
11
  }
11
- async run(command, args, cwd, scanner) {
12
+ async run(command, args, cwd, out_scanner, in_scanner) {
12
13
  const spawnOptions = {
13
- stdio: ['inherit', 'pipe', 'pipe'],
14
+ stdio: ['pipe', 'pipe', 'pipe'],
14
15
  };
15
16
  if (cwd) {
16
17
  spawnOptions.cwd = cwd;
@@ -18,9 +19,22 @@ export class ProcessRunner {
18
19
  const child = spawn(command, args, spawnOptions);
19
20
  child.stdout.setEncoding('utf8');
20
21
  child.stderr.setEncoding('utf8');
22
+ const rl = readline.createInterface({
23
+ input: process.stdin,
24
+ output: process.stdout,
25
+ });
26
+ rl.on('line', (line) => {
27
+ if (in_scanner) {
28
+ in_scanner(line);
29
+ }
30
+ child.stdin.write(line + '\n');
31
+ });
32
+ rl.on('SIGINT', () => {
33
+ child.kill('SIGINT');
34
+ });
21
35
  function processLine(line) {
22
- if (scanner)
23
- scanner(line);
36
+ if (out_scanner)
37
+ out_scanner(line);
24
38
  console.log(line);
25
39
  }
26
40
  let buffer = '';
@@ -30,7 +44,7 @@ export class ProcessRunner {
30
44
  buffer = lines.pop() || '';
31
45
  lines.forEach(processLine);
32
46
  if (buffer.includes('Enter a value:')) {
33
- console.log(buffer);
47
+ processLine(buffer);
34
48
  buffer = '';
35
49
  }
36
50
  });
@@ -40,6 +54,7 @@ export class ProcessRunner {
40
54
  });
41
55
  return new Promise((resolve, reject) => {
42
56
  child.on('close', (code) => {
57
+ rl.close();
43
58
  processLine(buffer);
44
59
  if (code === 0) {
45
60
  if (errorBuffer) {
@@ -1,3 +1,5 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
1
3
  import { AbortedException, KnownException, ProcessException, TerraformInitRequired } from '../exceptions.js';
2
4
  const MAX_ATTEMPTS = 2;
3
5
  const RETRYABLE_ERRORS = [
@@ -42,18 +44,85 @@ export class TerraformAdapter {
42
44
  throw new KnownException('Can not find terraform files. Command needs to be run in a directory with terraform files');
43
45
  }
44
46
  }
45
- tfArgs(env, tfArgs) {
46
- return [
47
- `-var=env=${env.env}`,
48
- `-var=owner=${env.owner}`,
49
- `-var=env_size=${env.size}`,
50
- `-var=env_type=${env.type}`,
51
- `-var=ephemeral=${env.ephemeral}`,
52
- ...tfArgs
53
- ];
47
+ tfArgs(env, cwd) {
48
+ const varDefs = this.getVarDefs(cwd);
49
+ const argVars = this.getArgVars(env.args);
50
+ const specialFields = { ...env, env: env.envName };
51
+ const extraArgs = [];
52
+ for (const name of ['env', 'owner', 'ephemeral']) {
53
+ if (varDefs[name] && !argVars[name]) {
54
+ extraArgs.push('-var=' + name + '=' + specialFields[name]);
55
+ }
56
+ }
57
+ Object.entries(env.vars || {}).forEach(([key, value]) => {
58
+ if (varDefs[key] && !argVars[key]) {
59
+ extraArgs.push('-var=' + key + '=' + value);
60
+ }
61
+ });
62
+ return [...extraArgs, ...env.args];
63
+ }
64
+ getVarDefs(cwd) {
65
+ const dir = cwd ?? process.cwd();
66
+ const tfFiles = fs.readdirSync(dir)
67
+ .filter(file => file.endsWith('.tf'))
68
+ .map(file => path.join(dir, file));
69
+ const results = {};
70
+ const varBlockRegex = /variable\s+"(\w+)"\s*{([^}]+)}/g;
71
+ const defaultRegex = /\n[^#\n]*default\s+=\s+/;
72
+ const sensitiveRegex = /\n[^#\n]*sensitive\s+=\s+true/;
73
+ for (const file of tfFiles) {
74
+ const content = fs.readFileSync(file, 'utf8');
75
+ let match;
76
+ while ((match = varBlockRegex.exec(content)) !== null) {
77
+ const varName = match[1];
78
+ const varBody = match[2];
79
+ results[varName] = {
80
+ name: varName,
81
+ default: defaultRegex.test(varBody),
82
+ sensitive: sensitiveRegex.test(varBody),
83
+ };
84
+ }
85
+ }
86
+ return results;
87
+ }
88
+ getArgVars(tfArgs) {
89
+ const result = {};
90
+ tfArgs
91
+ .filter(arg => arg.startsWith('-var='))
92
+ .forEach((arg) => {
93
+ const keyValue = arg.slice(5);
94
+ const eqIndex = keyValue.indexOf('=');
95
+ if (eqIndex === -1) {
96
+ console.log('terraform var argument is not in key=value format:', arg);
97
+ return;
98
+ }
99
+ const key = keyValue.slice(0, eqIndex);
100
+ result[key] = keyValue.slice(eqIndex + 1);
101
+ });
102
+ return result;
54
103
  }
55
- async plan(env, tfArgs, cwd, attemptNo = 1) {
56
- const args = this.tfArgs(env, tfArgs);
104
+ getNewVars(envTerraform, onDemandVars, cwd) {
105
+ const varDefs = this.getVarDefs(cwd);
106
+ const argVars = this.getArgVars(envTerraform.args);
107
+ const envVars = envTerraform.vars;
108
+ const vars = { ...argVars, ...onDemandVars };
109
+ const newVars = {};
110
+ Object.entries(vars).forEach(([key, value]) => {
111
+ const varDef = varDefs[key];
112
+ if (!varDef) {
113
+ console.warn(`Terraform variable ${key} passed, but not found in .tf files`);
114
+ return;
115
+ }
116
+ if (varDef.sensitive)
117
+ return;
118
+ if (!envVars || !envVars[key]) {
119
+ newVars[key] = value;
120
+ }
121
+ });
122
+ return newVars;
123
+ }
124
+ async plan(env, cwd, attemptNo = 1) {
125
+ const args = this.tfArgs(env, cwd);
57
126
  console.log('Running: terraform plan -auto-approve', ...args, '\n');
58
127
  try {
59
128
  await this.processRunner.run('terraform', ['plan', ...args], cwd);
@@ -64,23 +133,41 @@ export class TerraformAdapter {
64
133
  }
65
134
  if (attemptNo < MAX_ATTEMPTS && RETRYABLE_ERRORS.some(err => error.message.includes(err))) {
66
135
  console.warn(`Retrying terraform plan due to error: ${error.message}`);
67
- return this.plan(env, tfArgs, cwd, attemptNo + 1);
136
+ return this.plan(env, cwd, attemptNo + 1);
68
137
  }
69
138
  const lockId = this.lockId(error, attemptNo);
70
139
  if (lockId) {
71
140
  await this.promptUnlock(lockId, cwd);
72
141
  console.info('State unlocked, retrying terraform plan');
73
- return this.plan(env, tfArgs, cwd, attemptNo + 1);
142
+ return this.plan(env, cwd, attemptNo + 1);
74
143
  }
75
144
  throw new KnownException(`terraform plan failed with code ${error.code}:\n${error.message}`, { cause: error });
76
145
  }
77
146
  }
78
- async apply(env, tfArgs, cwd, attemptNo = 1) {
79
- const args = this.tfArgs(env, tfArgs);
147
+ async apply(env, onDemandVars, cwd, attemptNo = 1) {
148
+ let inputTfVariable = false;
149
+ let tfVarName = '';
150
+ function out_scanner(line) {
151
+ inputTfVariable = false;
152
+ const match = line.match(/var\.([a-zA-Z_0-9]+)/);
153
+ if (match) {
154
+ tfVarName = match[1];
155
+ }
156
+ else if (line.includes('Enter a value:')) {
157
+ inputTfVariable = true;
158
+ }
159
+ }
160
+ function in_scanner(line) {
161
+ if (inputTfVariable && tfVarName) {
162
+ onDemandVars[tfVarName] = line;
163
+ tfVarName = '';
164
+ }
165
+ }
166
+ const args = this.tfArgs(env, cwd);
80
167
  console.log('Running: terraform apply -auto-approve', ...args, '\n');
81
168
  this.printTime();
82
169
  try {
83
- await this.processRunner.run('terraform', ['apply', '-auto-approve', ...args], cwd);
170
+ await this.processRunner.run('terraform', ['apply', '-auto-approve', ...args], cwd, out_scanner, in_scanner);
84
171
  this.printTime();
85
172
  }
86
173
  catch (error) {
@@ -92,25 +179,25 @@ export class TerraformAdapter {
92
179
  }
93
180
  if (attemptNo < MAX_ATTEMPTS && RETRYABLE_ERRORS.some(err => error.message.includes(err))) {
94
181
  console.warn(`Retrying terraform apply due to error: ${error.message}`);
95
- return this.apply(env, tfArgs, cwd, attemptNo + 1);
182
+ return this.apply(env, onDemandVars, cwd, attemptNo + 1);
96
183
  }
97
184
  const lockId = this.lockId(error, attemptNo);
98
185
  if (lockId) {
99
186
  await this.promptUnlock(lockId, cwd);
100
187
  console.info('State unlocked, retrying terraform apply');
101
- return this.apply(env, tfArgs, cwd, attemptNo + 1);
188
+ return this.apply(env, onDemandVars, cwd, attemptNo + 1);
102
189
  }
103
190
  throw new KnownException(`terraform apply failed with code ${error.code}:\n${error.message}`, { cause: error });
104
191
  }
105
192
  }
106
- async destroy(env, tfArgs, force, cwd, attemptNo = 1) {
193
+ async destroy(env, force, cwd, attemptNo = 1) {
107
194
  let wrongDir = false;
108
195
  const scanner = (line) => {
109
196
  if (line.includes('Either you have not created any objects yet or the existing objects were')) {
110
197
  wrongDir = true;
111
198
  }
112
199
  };
113
- const args = this.tfArgs(env, tfArgs);
200
+ const args = this.tfArgs(env, cwd);
114
201
  console.log('Running: terraform destroy -auto-approve', ...args, '\n');
115
202
  try {
116
203
  await this.processRunner.run('terraform', ['destroy', '-auto-approve', ...args], cwd, scanner);
@@ -128,7 +215,7 @@ export class TerraformAdapter {
128
215
  await this.promptUnlock(lockId, cwd);
129
216
  }
130
217
  console.info('State unlocked, retrying terraform destroy');
131
- return this.destroy(env, tfArgs, force, cwd, attemptNo + 1);
218
+ return this.destroy(env, force, cwd, attemptNo + 1);
132
219
  }
133
220
  throw new KnownException(`terraform destroy failed with code ${error.code}:\n${error.message}`, { cause: error });
134
221
  }
@@ -4,7 +4,5 @@ export const _keys = {
4
4
  FORCE: 'Force deletion without confirmation',
5
5
  KIND: 'Environment kind: complete project (default) or some slice such as tt-core + tt-web',
6
6
  OWNER: 'Environment owner (GH username)',
7
- PROJECT: 'Project code (like tt). Used when multiple projects deployed in same AWS account',
8
- SIZE: 'Environment size: min, small, full',
9
- TYPE: 'Environment type: dev, prod',
7
+ PROJECT: 'Project code (like tt). Used when multiple projects deployed in same AWS account'
10
8
  };
@@ -9,12 +9,10 @@ export function createEphemeral(program) {
9
9
  .option('--project <project>', _keys.PROJECT)
10
10
  .requiredOption('--env <env>', _keys.ENV)
11
11
  .option('--owner <owner>', _keys.OWNER)
12
- .requiredOption('--size <size>', _keys.SIZE)
13
- .requiredOption('--type <type>', _keys.TYPE)
14
12
  .action(wrap(handler));
15
13
  }
16
14
  async function handler(options) {
17
- const { project, env, owner, size, type } = options;
18
- const token = await envCtl.createEphemeral(project, env, owner, size, type);
15
+ const { project, env, owner } = options;
16
+ const token = await envCtl.createEphemeral(project, env, owner);
19
17
  console.log(token);
20
18
  }
@@ -9,8 +9,6 @@ export function deploy(program) {
9
9
  .option('--project <project>', _keys.PROJECT)
10
10
  .option('--env <env>', _keys.ENV)
11
11
  .option('--owner <owner>', _keys.OWNER)
12
- .option('--size <size>', _keys.SIZE)
13
- .option('--type <type>', _keys.TYPE)
14
12
  .option('--kind <kind>', _keys.KIND)
15
13
  .option('--cwd <cwd>', _keys.CWD)
16
14
  .allowUnknownOption(true)
@@ -19,6 +17,7 @@ export function deploy(program) {
19
17
  }
20
18
  async function handler(tfArgs, options) {
21
19
  await awsCredsHelper.ensureCredentials();
22
- const { cwd, ...envDto } = options;
20
+ const { cwd, env, ...envDto } = options;
21
+ envDto.envName = env;
23
22
  await envCtl.deploy(envDto, tfArgs, cwd);
24
23
  }
@@ -10,8 +10,6 @@ export function plan(program) {
10
10
  .option('--project <project>', _keys.PROJECT)
11
11
  .option('--env <env>', _keys.ENV)
12
12
  .option('--owner <owner>', _keys.OWNER)
13
- .option('--size <size>', _keys.SIZE)
14
- .option('--type <type>', _keys.TYPE)
15
13
  .option('--cwd <cwd>', _keys.CWD)
16
14
  .allowUnknownOption(true)
17
15
  .argument('[args...]')
@@ -19,6 +17,7 @@ export function plan(program) {
19
17
  }
20
18
  async function handler(tfArgs, options) {
21
19
  await awsCredsHelper.ensureCredentials();
22
- const { cwd, ...envDto } = options;
20
+ const { cwd, env, ...envDto } = options;
21
+ envDto.envName = env;
23
22
  await envCtl.plan(envDto, tfArgs, cwd);
24
23
  }
@@ -1,4 +1 @@
1
- export { EnvSize } from './EnvSize.js';
2
- export { EnvSizeAvgTime } from './EnvSize.js';
3
1
  export { EnvStatus } from './EnvStatus.js';
4
- export { EnvType } from './EnvType.js';
@@ -1,5 +1,5 @@
1
1
  import { KnownException, TerraformInitRequired } from '../exceptions.js';
2
- import { EnvSize, EnvSizeAvgTime, EnvStatus, EnvType } from '../model/index.js';
2
+ import { EnvStatus, } from '../model/index.js';
3
3
  export class EnvCtl {
4
4
  cliHelper;
5
5
  envApi;
@@ -27,7 +27,7 @@ export class EnvCtl {
27
27
  }
28
28
  }
29
29
  async tryGetEnv(input) {
30
- let envName = input.env;
30
+ let envName = input.envName;
31
31
  let owner = input.owner;
32
32
  if (!envName) {
33
33
  owner = this.cliHelper.ensureOwner(owner);
@@ -43,18 +43,12 @@ export class EnvCtl {
43
43
  else {
44
44
  console.log(`Env ${key} does not exist`);
45
45
  }
46
- return { envName, owner, key, env };
46
+ return { envName, anOwner: owner, key, env };
47
47
  }
48
48
  checkInput(env, input, cwd) {
49
49
  if (input.owner && env.owner !== input.owner) {
50
50
  throw new KnownException(`Can not change env owner ${env.owner} -> ${input.owner}`);
51
51
  }
52
- if (input.size && env.size !== input.size) {
53
- throw new KnownException(`Can not change env size ${env.size} -> ${input.size}`);
54
- }
55
- if (input.type && env.type !== input.type) {
56
- throw new KnownException(`Can not change env type ${env.type} -> ${input.type}`);
57
- }
58
52
  const kind = this.cliHelper.ensureKind(input.kind, cwd);
59
53
  if ((input.kind || env.kind) && kind !== env.kind) {
60
54
  throw new KnownException(`Can not change env kind ${env.kind} -> ${kind}`);
@@ -65,8 +59,7 @@ export class EnvCtl {
65
59
  return;
66
60
  }
67
61
  if (env.status === EnvStatus.Deleting) {
68
- const time = EnvSizeAvgTime[env.size];
69
- throw new KnownException(`Env ${env.key} status is DELETING, please wait (~${time} min)`);
62
+ throw new KnownException(`Env ${env.key} status is DELETING, please wait`);
70
63
  }
71
64
  throw new KnownException(`Env ${env.key} status is ${env.status}, can not run this command`);
72
65
  }
@@ -75,41 +68,40 @@ export class EnvCtl {
75
68
  await this.terraformAdapter.init(key, cwd);
76
69
  }
77
70
  async plan(input, tfArgs, cwd) {
78
- const { envName, owner, key, env } = await this.tryGetEnv(input);
71
+ const { envName, anOwner, key, env } = await this.tryGetEnv(input);
79
72
  if (env == null) {
80
- const envInput = await this.cliHelper.parseEnvInput(input, envName, owner, cwd);
81
- const envTerraform = { ...envInput, ephemeral: false };
73
+ const owner = this.cliHelper.ensureOwner(anOwner);
82
74
  await this.terraformAdapter.init(key, cwd);
83
- await this.terraformAdapter.plan(envTerraform, tfArgs, cwd);
75
+ const envTerraform = { envName, ephemeral: false, owner, args: tfArgs };
76
+ await this.terraformAdapter.plan(envTerraform, cwd);
84
77
  return;
85
78
  }
86
79
  this.checkInput(env, input, cwd);
87
- const envOwner = env.owner;
88
- const envTerraform = { ...env, env: envName, owner: envOwner, ephemeral: false };
80
+ const owner = env.owner;
89
81
  this.checkStatus(env);
90
82
  await this.promptUnlock(env);
91
83
  if (env.status !== EnvStatus.Active) {
92
84
  throw new KnownException(`Env ${env.key} status is ${env.status}, can not run plan`);
93
85
  }
94
- await this.terraformAdapter.plan(envTerraform, tfArgs, cwd);
86
+ const envTerraform = { envName, ephemeral: false, owner, args: tfArgs, vars: env.vars };
87
+ await this.terraformAdapter.plan(envTerraform, cwd);
95
88
  }
96
- async createEphemeral(project, envName, owner, size, type) {
89
+ async createEphemeral(project, envName, owner) {
97
90
  const key = this.key(project, envName);
98
91
  const env = await this.envApi.get(key);
99
92
  if (env !== null) {
100
93
  throw new KnownException(`Env ${key} already exists`);
101
94
  }
102
- const createEnv = { key, owner, size, type };
103
- const newEnv = await this.envApi.createEphemeral(createEnv);
104
- return newEnv.token;
95
+ const createEnv = { key, owner };
96
+ return await this.envApi.createEphemeral(createEnv);
105
97
  }
106
98
  async deploy(input, tfArgs, cwd) {
107
- const { envName, owner, key, env } = await this.tryGetEnv(input);
99
+ const { envName, anOwner, key, env } = await this.tryGetEnv(input);
108
100
  if (env === null) {
109
- const envInput = await this.cliHelper.parseEnvInput(input, envName, owner, cwd);
101
+ const owner = this.cliHelper.ensureOwner(anOwner);
102
+ const kind = this.cliHelper.ensureKind(input.kind, cwd);
110
103
  console.log('Creating env tracking record in DynamoDB');
111
- const { size, type, kind } = envInput;
112
- const createEnv = { key, owner: envInput.owner, size, type, kind };
104
+ const createEnv = { key, owner, kind };
113
105
  const newEnv = await this.envApi.create(createEnv);
114
106
  return await this.runDeploy(newEnv, envName, tfArgs, cwd);
115
107
  }
@@ -119,8 +111,8 @@ export class EnvCtl {
119
111
  await this.envApi.lockForUpdate(env);
120
112
  }
121
113
  else if (env.status == EnvStatus.Updating) {
122
- const answerYes = await this.cliHelper.promptYesNo(`Env status is ${env.status}, likely to be an error from a previous run\n`
123
- + 'Do you want to proceed with deployment?');
114
+ const answerYes = await this.cliHelper.promptYesNo(`Env status is ${env.status},
115
+ likely to be an error from a previous run\nDo you want to proceed with deployment?`);
124
116
  if (!answerYes) {
125
117
  console.log('Aborting deployment');
126
118
  return;
@@ -134,20 +126,27 @@ export class EnvCtl {
134
126
  }
135
127
  async runDeploy(env, envName, tfArgs, cwd) {
136
128
  console.log('Deploying resources');
137
- const { owner, size, type, ephemeral } = env;
138
- const envTf = { owner: owner || 'system', size, type, ephemeral, env: envName };
129
+ const { ephemeral, vars } = env;
130
+ const envTerraform = { envName, ephemeral, owner: env.owner || 'system', args: tfArgs, vars };
131
+ const onDemandVars = {};
139
132
  try {
140
- await this.terraformAdapter.apply(envTf, tfArgs, cwd);
133
+ await this.terraformAdapter.apply(envTerraform, onDemandVars, cwd);
141
134
  }
142
135
  catch (error) {
143
136
  if (error instanceof TerraformInitRequired) {
144
137
  await this.terraformAdapter.init(env.key, cwd);
145
- await this.terraformAdapter.apply(envTf, tfArgs, cwd);
138
+ await this.terraformAdapter.apply(envTerraform, onDemandVars, cwd);
146
139
  }
147
140
  else {
148
141
  throw error;
149
142
  }
150
143
  }
144
+ finally {
145
+ const newVars = this.terraformAdapter.getNewVars(envTerraform, onDemandVars, cwd);
146
+ if (Object.keys(newVars).length) {
147
+ await this.envApi.setVars(env, newVars);
148
+ }
149
+ }
151
150
  console.log('Activating env (to finish creation)');
152
151
  await this.envApi.activate(env);
153
152
  }
@@ -207,10 +206,10 @@ export class EnvCtl {
207
206
  console.log('Lock env to run destroy');
208
207
  await this.envApi.lockForUpdate(env);
209
208
  }
210
- const { owner, size, type, ephemeral } = env;
211
- const tfEnv = { owner: owner || 'system', size, type, ephemeral, env: envName };
209
+ const { ephemeral, owner, vars } = env;
210
+ const envTerraform = { envName, ephemeral, owner: owner || 'system', args: tfArgs, vars };
212
211
  console.log('Destroying env resources');
213
- await this.terraformAdapter.destroy(tfEnv, tfArgs, force, cwd);
212
+ await this.terraformAdapter.destroy(envTerraform, force, cwd);
214
213
  console.log('Unlock env');
215
214
  await this.envApi.activate(env);
216
215
  console.log(`Schedule env ${env.key} metadata deletion`);
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.20.1",
4
+ "version": "0.21.1",
5
5
  "author": "Alex Chekulaev",
6
6
  "type": "module",
7
7
  "bin": {
@@ -28,8 +28,8 @@
28
28
  "run-version": " tsc --sourceMap true && npm run run -- --version",
29
29
  "run-configure": " tsc --sourceMap true && npm run run -- configure",
30
30
  "run-status": " tsc --sourceMap true && npm run run -- status --cwd ../tt-core",
31
- "run-plan": " tsc --sourceMap true && npm run run -- plan --cwd ../tt-core --size min",
32
- "run-deploy": " tsc --sourceMap true && npm run run -- deploy --cwd ../tt-core --size min",
31
+ "run-plan": " tsc --sourceMap true && npm run run -- plan --cwd ../tt-core",
32
+ "run-deploy": " tsc --sourceMap true && npm run run -- deploy --cwd ../tt-core -var=\"env_size=min\"",
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
  "-": "",
@@ -1,11 +0,0 @@
1
- export var EnvSize;
2
- (function (EnvSize) {
3
- EnvSize["Min"] = "min";
4
- EnvSize["Small"] = "small";
5
- EnvSize["Full"] = "full";
6
- })(EnvSize || (EnvSize = {}));
7
- export const EnvSizeAvgTime = {
8
- [EnvSize.Min]: 2,
9
- [EnvSize.Small]: 5,
10
- [EnvSize.Full]: 10,
11
- };
@@ -1,5 +0,0 @@
1
- export var EnvType;
2
- (function (EnvType) {
3
- EnvType["Dev"] = "dev";
4
- EnvType["Prod"] = "prod";
5
- })(EnvType || (EnvType = {}));