@agilecustoms/envctl 0.20.1 → 0.21.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/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,6 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { glob } from 'glob';
1
4
  import { AbortedException, KnownException, ProcessException, TerraformInitRequired } from '../exceptions.js';
2
5
  const MAX_ATTEMPTS = 2;
3
6
  const RETRYABLE_ERRORS = [
@@ -42,18 +45,83 @@ export class TerraformAdapter {
42
45
  throw new KnownException('Can not find terraform files. Command needs to be run in a directory with terraform files');
43
46
  }
44
47
  }
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
- ];
48
+ tfArgs(env, cwd) {
49
+ const varDefs = this.getVarDefs(cwd);
50
+ const argVars = this.getArgVars(env.args);
51
+ const specialFields = { ...env, env: env.envName };
52
+ const extraArgs = [];
53
+ for (const name of ['env', 'owner', 'ephemeral']) {
54
+ if (varDefs[name] && !argVars[name]) {
55
+ extraArgs.push('-var=' + name + '=' + specialFields[name]);
56
+ }
57
+ }
58
+ Object.entries(env.vars || {}).forEach(([key, value]) => {
59
+ if (varDefs[key] && !argVars[key]) {
60
+ extraArgs.push('-var=' + key + '=' + value);
61
+ }
62
+ });
63
+ return [...extraArgs, ...env.args];
64
+ }
65
+ getVarDefs(cwd) {
66
+ const dir = cwd ?? process.cwd();
67
+ const tfFiles = glob.sync(path.join(dir, '*.tf'));
68
+ const results = {};
69
+ const varBlockRegex = /variable\s+"(\w+)"\s*{([^}]+)}/g;
70
+ const defaultRegex = /\n[^#\n]*default\s+=\s+/;
71
+ const sensitiveRegex = /\n[^#\n]*sensitive\s+=\s+true/;
72
+ for (const file of tfFiles) {
73
+ const content = fs.readFileSync(file, 'utf8');
74
+ let match;
75
+ while ((match = varBlockRegex.exec(content)) !== null) {
76
+ const varName = match[1];
77
+ const varBody = match[2];
78
+ results[varName] = {
79
+ name: varName,
80
+ default: defaultRegex.test(varBody),
81
+ sensitive: sensitiveRegex.test(varBody),
82
+ };
83
+ }
84
+ }
85
+ return results;
86
+ }
87
+ getArgVars(tfArgs) {
88
+ const result = {};
89
+ tfArgs
90
+ .filter(arg => arg.startsWith('-var='))
91
+ .forEach((arg) => {
92
+ const keyValue = arg.slice(5);
93
+ const eqIndex = keyValue.indexOf('=');
94
+ if (eqIndex === -1) {
95
+ console.log('terraform var argument is not in key=value format:', arg);
96
+ return;
97
+ }
98
+ const key = keyValue.slice(0, eqIndex);
99
+ result[key] = keyValue.slice(eqIndex + 1);
100
+ });
101
+ return result;
54
102
  }
55
- async plan(env, tfArgs, cwd, attemptNo = 1) {
56
- const args = this.tfArgs(env, tfArgs);
103
+ getNewVars(envTerraform, onDemandVars, cwd) {
104
+ const varDefs = this.getVarDefs(cwd);
105
+ const argVars = this.getArgVars(envTerraform.args);
106
+ const envVars = envTerraform.vars;
107
+ const vars = { ...argVars, ...onDemandVars };
108
+ const newVars = {};
109
+ Object.entries(vars).forEach(([key, value]) => {
110
+ const varDef = varDefs[key];
111
+ if (!varDef) {
112
+ console.warn(`Terraform variable ${key} passed, but not found in .tf files`);
113
+ return;
114
+ }
115
+ if (varDef.sensitive)
116
+ return;
117
+ if (!envVars || !envVars[key]) {
118
+ newVars[key] = value;
119
+ }
120
+ });
121
+ return newVars;
122
+ }
123
+ async plan(env, cwd, attemptNo = 1) {
124
+ const args = this.tfArgs(env, cwd);
57
125
  console.log('Running: terraform plan -auto-approve', ...args, '\n');
58
126
  try {
59
127
  await this.processRunner.run('terraform', ['plan', ...args], cwd);
@@ -64,23 +132,41 @@ export class TerraformAdapter {
64
132
  }
65
133
  if (attemptNo < MAX_ATTEMPTS && RETRYABLE_ERRORS.some(err => error.message.includes(err))) {
66
134
  console.warn(`Retrying terraform plan due to error: ${error.message}`);
67
- return this.plan(env, tfArgs, cwd, attemptNo + 1);
135
+ return this.plan(env, cwd, attemptNo + 1);
68
136
  }
69
137
  const lockId = this.lockId(error, attemptNo);
70
138
  if (lockId) {
71
139
  await this.promptUnlock(lockId, cwd);
72
140
  console.info('State unlocked, retrying terraform plan');
73
- return this.plan(env, tfArgs, cwd, attemptNo + 1);
141
+ return this.plan(env, cwd, attemptNo + 1);
74
142
  }
75
143
  throw new KnownException(`terraform plan failed with code ${error.code}:\n${error.message}`, { cause: error });
76
144
  }
77
145
  }
78
- async apply(env, tfArgs, cwd, attemptNo = 1) {
79
- const args = this.tfArgs(env, tfArgs);
146
+ async apply(env, onDemandVars, cwd, attemptNo = 1) {
147
+ let inputTfVariable = false;
148
+ let tfVarName = '';
149
+ function out_scanner(line) {
150
+ inputTfVariable = false;
151
+ const match = line.match(/var\.([a-zA-Z_0-9]+)/);
152
+ if (match) {
153
+ tfVarName = match[1];
154
+ }
155
+ else if (line.includes('Enter a value:')) {
156
+ inputTfVariable = true;
157
+ }
158
+ }
159
+ function in_scanner(line) {
160
+ if (inputTfVariable && tfVarName) {
161
+ onDemandVars[tfVarName] = line;
162
+ tfVarName = '';
163
+ }
164
+ }
165
+ const args = this.tfArgs(env, cwd);
80
166
  console.log('Running: terraform apply -auto-approve', ...args, '\n');
81
167
  this.printTime();
82
168
  try {
83
- await this.processRunner.run('terraform', ['apply', '-auto-approve', ...args], cwd);
169
+ await this.processRunner.run('terraform', ['apply', '-auto-approve', ...args], cwd, out_scanner, in_scanner);
84
170
  this.printTime();
85
171
  }
86
172
  catch (error) {
@@ -92,25 +178,25 @@ export class TerraformAdapter {
92
178
  }
93
179
  if (attemptNo < MAX_ATTEMPTS && RETRYABLE_ERRORS.some(err => error.message.includes(err))) {
94
180
  console.warn(`Retrying terraform apply due to error: ${error.message}`);
95
- return this.apply(env, tfArgs, cwd, attemptNo + 1);
181
+ return this.apply(env, onDemandVars, cwd, attemptNo + 1);
96
182
  }
97
183
  const lockId = this.lockId(error, attemptNo);
98
184
  if (lockId) {
99
185
  await this.promptUnlock(lockId, cwd);
100
186
  console.info('State unlocked, retrying terraform apply');
101
- return this.apply(env, tfArgs, cwd, attemptNo + 1);
187
+ return this.apply(env, onDemandVars, cwd, attemptNo + 1);
102
188
  }
103
189
  throw new KnownException(`terraform apply failed with code ${error.code}:\n${error.message}`, { cause: error });
104
190
  }
105
191
  }
106
- async destroy(env, tfArgs, force, cwd, attemptNo = 1) {
192
+ async destroy(env, force, cwd, attemptNo = 1) {
107
193
  let wrongDir = false;
108
194
  const scanner = (line) => {
109
195
  if (line.includes('Either you have not created any objects yet or the existing objects were')) {
110
196
  wrongDir = true;
111
197
  }
112
198
  };
113
- const args = this.tfArgs(env, tfArgs);
199
+ const args = this.tfArgs(env, cwd);
114
200
  console.log('Running: terraform destroy -auto-approve', ...args, '\n');
115
201
  try {
116
202
  await this.processRunner.run('terraform', ['destroy', '-auto-approve', ...args], cwd, scanner);
@@ -128,7 +214,7 @@ export class TerraformAdapter {
128
214
  await this.promptUnlock(lockId, cwd);
129
215
  }
130
216
  console.info('State unlocked, retrying terraform destroy');
131
- return this.destroy(env, tfArgs, force, cwd, attemptNo + 1);
217
+ return this.destroy(env, force, cwd, attemptNo + 1);
132
218
  }
133
219
  throw new KnownException(`terraform destroy failed with code ${error.code}:\n${error.message}`, { cause: error });
134
220
  }
@@ -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.0",
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 = {}));