@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 +1 -2
- package/dist/client/CliHelper.js +0 -54
- package/dist/client/EnvApiClient.js +9 -11
- package/dist/client/HttpClient.js +11 -9
- package/dist/client/ProcessRunner.js +21 -6
- package/dist/client/TerraformAdapter.js +108 -21
- package/dist/commands/_keys.js +1 -3
- package/dist/commands/createEphemeral.js +2 -4
- package/dist/commands/deploy.js +2 -3
- package/dist/commands/plan.js +2 -3
- package/dist/model/index.js +0 -3
- package/dist/service/EnvCtl.js +34 -35
- package/package.json +3 -3
- package/dist/model/EnvSize.js +0 -11
- package/dist/model/EnvType.js +0 -5
package/README.md
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## usage
|
|
6
6
|
```shell
|
|
7
|
-
envctl deploy --
|
|
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
|
package/dist/client/CliHelper.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
return { ...env, token, ephemeral: true, status: EnvStatus.Init };
|
|
22
|
+
return res.token;
|
|
24
23
|
}
|
|
25
24
|
async create(env) {
|
|
26
|
-
|
|
27
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
body
|
|
13
|
-
headers
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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,
|
|
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
|
-
|
|
10
|
+
await this.run(scriptPath, args, cwd, scanner);
|
|
10
11
|
}
|
|
11
|
-
async run(command, args, cwd,
|
|
12
|
+
async run(command, args, cwd, out_scanner, in_scanner) {
|
|
12
13
|
const spawnOptions = {
|
|
13
|
-
stdio: ['
|
|
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 (
|
|
23
|
-
|
|
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
|
-
|
|
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,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
const
|
|
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,
|
|
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,
|
|
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,
|
|
79
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
}
|
package/dist/commands/_keys.js
CHANGED
|
@@ -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
|
|
18
|
-
const token = await envCtl.createEphemeral(project, env, owner
|
|
15
|
+
const { project, env, owner } = options;
|
|
16
|
+
const token = await envCtl.createEphemeral(project, env, owner);
|
|
19
17
|
console.log(token);
|
|
20
18
|
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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
|
}
|
package/dist/commands/plan.js
CHANGED
|
@@ -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
|
}
|
package/dist/model/index.js
CHANGED
package/dist/service/EnvCtl.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { KnownException, TerraformInitRequired } from '../exceptions.js';
|
|
2
|
-
import {
|
|
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.
|
|
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
|
-
|
|
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,
|
|
71
|
+
const { envName, anOwner, key, env } = await this.tryGetEnv(input);
|
|
79
72
|
if (env == null) {
|
|
80
|
-
const
|
|
81
|
-
const envTerraform = { ...envInput, ephemeral: false };
|
|
73
|
+
const owner = this.cliHelper.ensureOwner(anOwner);
|
|
82
74
|
await this.terraformAdapter.init(key, cwd);
|
|
83
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
103
|
-
|
|
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,
|
|
99
|
+
const { envName, anOwner, key, env } = await this.tryGetEnv(input);
|
|
108
100
|
if (env === null) {
|
|
109
|
-
const
|
|
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 {
|
|
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},
|
|
123
|
-
|
|
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 {
|
|
138
|
-
const
|
|
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(
|
|
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(
|
|
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 {
|
|
211
|
-
const
|
|
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(
|
|
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.
|
|
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
|
|
32
|
-
"run-deploy": " tsc --sourceMap true && npm run run -- deploy --cwd ../tt-core
|
|
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
|
"-": "",
|
package/dist/model/EnvSize.js
DELETED
|
@@ -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
|
-
};
|