@gershy/lilac 0.0.14 → 0.0.16
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/cmp/cjs/main.d.ts +6 -2
- package/cmp/cjs/main.js +82 -34
- package/cmp/cjs/petal/terraform/terraform.d.ts +1 -0
- package/cmp/cjs/petal/terraform/terraform.js +28 -20
- package/cmp/cjs/soil/soil.d.ts +14 -5
- package/cmp/cjs/soil/soil.js +56 -29
- package/cmp/cjs/util/aws.d.ts +3 -14
- package/cmp/cjs/util/aws.js +6 -6
- package/cmp/cjs/util/normalize.d.ts +1 -0
- package/cmp/cjs/util/normalize.js +8 -7
- package/cmp/cjs/util/procTerraform.d.ts +3 -1
- package/cmp/cjs/util/procTerraform.js +8 -3
- package/cmp/cjs/util/slashEscape.js +1 -1
- package/cmp/cjs/util/terraform.js +1 -1
- package/cmp/mjs/main.d.ts +6 -2
- package/cmp/mjs/main.js +81 -33
- package/cmp/mjs/petal/terraform/terraform.d.ts +1 -0
- package/cmp/mjs/petal/terraform/terraform.js +17 -9
- package/cmp/mjs/soil/soil.d.ts +14 -5
- package/cmp/mjs/soil/soil.js +52 -25
- package/cmp/mjs/util/aws.d.ts +3 -14
- package/cmp/mjs/util/aws.js +6 -6
- package/cmp/mjs/util/normalize.d.ts +1 -0
- package/cmp/mjs/util/normalize.js +3 -2
- package/cmp/mjs/util/procTerraform.d.ts +3 -1
- package/cmp/mjs/util/procTerraform.js +8 -3
- package/cmp/mjs/util/slashEscape.js +1 -1
- package/cmp/mjs/util/terraform.js +1 -1
- package/package.json +11 -10
- package/readme.md +11 -6
package/cmp/cjs/main.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import '../sideEffects.js';
|
|
2
2
|
import { PetalTerraform } from './petal/terraform/terraform.ts';
|
|
3
3
|
import type Logger from '@gershy/logger';
|
|
4
|
-
import type
|
|
4
|
+
import { type Fact } from '@gershy/disk';
|
|
5
|
+
import '@gershy/clearing';
|
|
5
6
|
import { Soil } from './soil/soil.ts';
|
|
6
7
|
import { SuperIterable } from './util/superIterable.ts';
|
|
7
8
|
export type Context = {
|
|
@@ -9,6 +10,7 @@ export type Context = {
|
|
|
9
10
|
logger: Logger;
|
|
10
11
|
fact: Fact;
|
|
11
12
|
patioFact: Fact;
|
|
13
|
+
shedFact: Fact;
|
|
12
14
|
maturity: string;
|
|
13
15
|
debug: boolean;
|
|
14
16
|
pfx: string;
|
|
@@ -18,6 +20,7 @@ export declare class Flower {
|
|
|
18
20
|
constructor();
|
|
19
21
|
getDependencies(): Generator<Flower>;
|
|
20
22
|
getPetals(ctx: Context): SuperIterable<PetalTerraform.Base>;
|
|
23
|
+
cultivate(): Promise<void>;
|
|
21
24
|
}
|
|
22
25
|
type RegistryFlowers<R extends Registry<any>, M extends 'real' | 'test'> = R extends Registry<infer Flowers> ? {
|
|
23
26
|
[K in keyof Flowers]: Flowers[K][M];
|
|
@@ -39,13 +42,14 @@ export declare class Garden<Reg extends Registry<any>> {
|
|
|
39
42
|
private ctx;
|
|
40
43
|
private reg;
|
|
41
44
|
private def;
|
|
45
|
+
private tfProcArgs;
|
|
42
46
|
constructor(args: {
|
|
43
47
|
context: Context;
|
|
44
48
|
registry: Reg;
|
|
45
49
|
define: Garden<Reg>['def'];
|
|
46
50
|
});
|
|
47
51
|
private getPetals;
|
|
48
|
-
genTerraform(
|
|
52
|
+
genTerraform(soil: Soil.Base): Promise<Obj<Fact>>;
|
|
49
53
|
private terraformInit;
|
|
50
54
|
private terraformPlan;
|
|
51
55
|
private terraformApply;
|
package/cmp/cjs/main.js
CHANGED
|
@@ -22,15 +22,24 @@ exports.Garden = exports.Registry = exports.Flower = void 0;
|
|
|
22
22
|
// A more generic (beyond just tf) provider is very hard to support due to the multiplicity of
|
|
23
23
|
// provider/petal combos - e.g. "api" flower would need to support ,api.getCloudformationPetals,
|
|
24
24
|
// api.getTerraformPetals, etc... supporting just terraform for now
|
|
25
|
-
// Watch out when working lilac into an npm dependency, need to allow the user a way to declare
|
|
26
|
-
// their absolute repo path (so "<repo>/..." filenames work in any setup!)
|
|
27
|
-
// Can region be dealt with any better??
|
|
28
25
|
// Support test-mode (Flowers need to be able to do setup, share config, write to volumes, etc)
|
|
29
26
|
const terraform_ts_1 = require("./petal/terraform/terraform.js");
|
|
30
|
-
const
|
|
31
|
-
|
|
27
|
+
const disk_1 = require("@gershy/disk");
|
|
28
|
+
require("@gershy/clearing");
|
|
32
29
|
const util_try_with_healing_1 = __importDefault(require("@gershy/util-try-with-healing"));
|
|
33
30
|
const util_phrasing_1 = __importDefault(require("@gershy/util-phrasing"));
|
|
31
|
+
const nodejs_proc_1 = __importDefault(require("@gershy/nodejs-proc"));
|
|
32
|
+
const { isCls, skip } = cl;
|
|
33
|
+
const toArr = cl.toArr;
|
|
34
|
+
const allObj = cl.allObj;
|
|
35
|
+
const allArr = cl.allArr;
|
|
36
|
+
const has = cl.has;
|
|
37
|
+
const map = cl.map;
|
|
38
|
+
const mod = cl.mod;
|
|
39
|
+
const walk = cl.walk;
|
|
40
|
+
const merge = cl.merge;
|
|
41
|
+
const upper = cl.upper;
|
|
42
|
+
const baseline = cl.baseline;
|
|
34
43
|
class Flower {
|
|
35
44
|
// TODO: The downside of having this static is that different instances may use different
|
|
36
45
|
// services - e.g. api gateway instance may have "useEdge: true", in which case we'd like to
|
|
@@ -47,6 +56,11 @@ class Flower {
|
|
|
47
56
|
getPetals(ctx) {
|
|
48
57
|
throw Error('not implemented');
|
|
49
58
|
}
|
|
59
|
+
async cultivate() {
|
|
60
|
+
// This function is called once all Flowers for a given Garden have been constructed. The main
|
|
61
|
+
// purpose of this phase is to allow Flowers which reference each other via functions to run
|
|
62
|
+
// such functions without running into uninitialized values.
|
|
63
|
+
}
|
|
50
64
|
}
|
|
51
65
|
exports.Flower = Flower;
|
|
52
66
|
;
|
|
@@ -61,7 +75,7 @@ class Registry {
|
|
|
61
75
|
}
|
|
62
76
|
getAwsServices() {
|
|
63
77
|
const services = new Set();
|
|
64
|
-
for (const [k, { real }] of this.flowers)
|
|
78
|
+
for (const [k, { real }] of this.flowers[walk]())
|
|
65
79
|
for (const awsService of real.getAwsServices())
|
|
66
80
|
services.add(awsService);
|
|
67
81
|
return services[toArr](v => v);
|
|
@@ -80,11 +94,21 @@ class Garden {
|
|
|
80
94
|
ctx;
|
|
81
95
|
reg;
|
|
82
96
|
def;
|
|
97
|
+
tfProcArgs;
|
|
83
98
|
constructor(args) {
|
|
84
99
|
const { define, registry, context } = args;
|
|
85
100
|
this.ctx = context;
|
|
86
101
|
this.reg = registry;
|
|
87
102
|
this.def = define;
|
|
103
|
+
this.tfProcArgs = {
|
|
104
|
+
timeoutMs: 0,
|
|
105
|
+
env: {
|
|
106
|
+
...process.env,
|
|
107
|
+
TF_LOG: 'DEBUG',
|
|
108
|
+
TF_DATA_DIR: '',
|
|
109
|
+
TF_CLI_CONFIG_FILE: ''
|
|
110
|
+
}
|
|
111
|
+
};
|
|
88
112
|
}
|
|
89
113
|
async *getPetals() {
|
|
90
114
|
// TODO: We always use the "real" flowers from the registry - this is part of the shift to
|
|
@@ -94,21 +118,22 @@ class Garden {
|
|
|
94
118
|
// conditionally calling `this.registry.get('fake')`...
|
|
95
119
|
const seenFlowers = new Set();
|
|
96
120
|
const seenPetals = new Set();
|
|
97
|
-
for await (const topLevelFlower of await this.def(this.ctx, this.reg.get('real')))
|
|
98
|
-
for (const flower of topLevelFlower.getDependencies())
|
|
99
|
-
if (seenFlowers.has(flower))
|
|
100
|
-
continue;
|
|
121
|
+
for await (const topLevelFlower of await this.def(this.ctx, this.reg.get('real')))
|
|
122
|
+
for (const flower of topLevelFlower.getDependencies())
|
|
101
123
|
seenFlowers.add(flower);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
// Now we've exhaustively referenced all Flowers - we can cultivate them
|
|
125
|
+
await Promise.all(seenFlowers[toArr](f => f.cultivate()));
|
|
126
|
+
for (const flower of seenFlowers) {
|
|
127
|
+
for await (const petal of await flower.getPetals(this.ctx)) {
|
|
128
|
+
if (seenPetals.has(petal))
|
|
129
|
+
continue;
|
|
130
|
+
seenPetals.add(petal);
|
|
131
|
+
yield petal;
|
|
107
132
|
}
|
|
108
133
|
}
|
|
109
134
|
}
|
|
110
|
-
async genTerraform(
|
|
111
|
-
const soilTfPetalsPrm =
|
|
135
|
+
async genTerraform(soil) {
|
|
136
|
+
const soilTfPetalsPrm = soil.getTerraformPetals(this.ctx);
|
|
112
137
|
return this.ctx.logger.scope('garden.genTerraform', {}, async (logger) => {
|
|
113
138
|
const setupTfProj = async (args) => args.logger.scope('tf', { proj: this.ctx.name, tf: args.term }, async (logger) => {
|
|
114
139
|
// Allows a terraform project to be defined in terms of a function which writes to main.tf,
|
|
@@ -123,15 +148,14 @@ class Garden {
|
|
|
123
148
|
'terraform.tfstate.backup'
|
|
124
149
|
]);
|
|
125
150
|
const kids = await args.fact.getKids();
|
|
126
|
-
await Promise.all(kids[toArr]((kid, k) => tfFilesToPreserve.has(k) ?
|
|
151
|
+
await Promise.all(kids[toArr]((kid, k) => tfFilesToPreserve.has(k) ? skip : kid.rem()));
|
|
127
152
|
});
|
|
128
153
|
// Write new terraform
|
|
129
154
|
await logger.scope('files.generate', {}, async (logger) => {
|
|
130
155
|
const stream = await args.fact.kid(['main.tf']).getDataHeadStream();
|
|
131
156
|
await args.setup(args.fact, stream, async (petal) => {
|
|
132
157
|
// Include a utility function the caller can use to easily write petals
|
|
133
|
-
const { tf, files = {} } = await petal.getResult()
|
|
134
|
-
.then(tf => (0, clearing_1.isCls)(tf, String) ? { tf } : tf);
|
|
158
|
+
const { tf, files = {} } = await petal.getResult().then(tf => isCls(tf, String) ? { tf } : tf);
|
|
135
159
|
if (tf)
|
|
136
160
|
await stream.write(`${tf}\n`);
|
|
137
161
|
await Promise.all(files[toArr]((data, kfp) => args.fact.kid(kfp.split('/')).setData(data)));
|
|
@@ -176,7 +200,7 @@ class Garden {
|
|
|
176
200
|
// Create ddb tf state locking table
|
|
177
201
|
await writePetalTfAndFiles(new terraform_ts_1.PetalTerraform.Resource('awsDynamodbTable', 'tfState', {
|
|
178
202
|
name: ddbName,
|
|
179
|
-
billingMode: (0, util_phrasing_1.default)('
|
|
203
|
+
billingMode: (0, util_phrasing_1.default)('camel->snake', 'payPerRequest')[upper](),
|
|
180
204
|
hashKey: 'LockID',
|
|
181
205
|
$attribute: { name: 'LockID', type: 'S' }
|
|
182
206
|
}));
|
|
@@ -203,31 +227,55 @@ class Garden {
|
|
|
203
227
|
});
|
|
204
228
|
});
|
|
205
229
|
}
|
|
206
|
-
|
|
230
|
+
// TODO: Write terraform output to logs??
|
|
231
|
+
async terraformInit(fact) {
|
|
232
|
+
// Consider if we ever want to pass "-reconfigure" and "-migrate-state" options; these are
|
|
233
|
+
// useful if we are moving backends (e.g. one aws account to another), and want to move our
|
|
234
|
+
// full iac definition too
|
|
235
|
+
// Ensure the mirror directory exists in the shed
|
|
236
|
+
const mirrorFact = this.ctx.shedFact.kid(['lilacTerraformMirror']);
|
|
237
|
+
await mirrorFact.kid(['note.txt']).setData(`Root of terraform mirror for @gershy/lilac`);
|
|
207
238
|
return this.ctx.logger.scope('execTf.init', { fact: fact.fsp() }, async (logger) => {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
239
|
+
const { output: result } = await (0, util_try_with_healing_1.default)({
|
|
240
|
+
fn: () => logger.scope('attempt', {}, async (logger) => {
|
|
241
|
+
const configFact = disk_1.tempFact.kid([`${Math.random().toString(36).slice(2)}.terraform.rc`]);
|
|
242
|
+
await configFact.setData(String[baseline](`
|
|
243
|
+
| provider_installation {
|
|
244
|
+
| filesystem_mirror {
|
|
245
|
+
| path = "${mirrorFact.fsp().replaceAll('\\', '/')}"
|
|
246
|
+
| }
|
|
247
|
+
| }
|
|
248
|
+
`));
|
|
249
|
+
return (0, nodejs_proc_1.default)(`terraform init -input=false`, {}[merge](this.tfProcArgs)[merge]({
|
|
250
|
+
cwd: fact,
|
|
251
|
+
env: { TF_CLI_CONFIG_FILE: configFact.fsp() }
|
|
252
|
+
})).finally(() => configFact.rem());
|
|
253
|
+
}),
|
|
254
|
+
canHeal: err => (err?.output ?? '')[has]('Could not retrieve the list of available versions for provider'),
|
|
255
|
+
heal: () => logger.scope('mirror', { fsp: mirrorFact.fsp() }, async (logger) => {
|
|
256
|
+
const { output: result } = await (0, nodejs_proc_1.default)(`terraform providers mirror "${mirrorFact.fsp().replaceAll('\\', '/')}"`, { cwd: fact, timeoutMs: 0 });
|
|
257
|
+
logger.log({ $$: 'result', result });
|
|
258
|
+
})
|
|
215
259
|
});
|
|
216
|
-
logger.log({ $$: 'result',
|
|
260
|
+
logger.log({ $$: 'result', result });
|
|
217
261
|
return result;
|
|
218
262
|
});
|
|
219
263
|
}
|
|
220
264
|
terraformPlan(fact, args) {
|
|
221
265
|
return this.ctx.logger.scope('execTf.plan', { fact: fact.fsp() }, async (logger) => {
|
|
222
|
-
const result = await (0,
|
|
223
|
-
|
|
266
|
+
const { output: result } = await (0, nodejs_proc_1.default)(`terraform plan -input=false`, {}[merge](this.tfProcArgs)[merge]({
|
|
267
|
+
cwd: fact,
|
|
268
|
+
}));
|
|
269
|
+
logger.log({ $$: 'result', result });
|
|
224
270
|
return result;
|
|
225
271
|
});
|
|
226
272
|
}
|
|
227
273
|
terraformApply(fact, args) {
|
|
228
274
|
return this.ctx.logger.scope('execTf.apply', { fact: fact.fsp() }, async (logger) => {
|
|
229
|
-
const result = await (0,
|
|
230
|
-
|
|
275
|
+
const { output: result } = await (0, nodejs_proc_1.default)(`terraform apply -input=false -auto-approve`, {}[merge](this.tfProcArgs)[merge]({
|
|
276
|
+
cwd: fact
|
|
277
|
+
}));
|
|
278
|
+
logger.log({ $$: 'result', result });
|
|
231
279
|
return result;
|
|
232
280
|
});
|
|
233
281
|
}
|
|
@@ -4,10 +4,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.PetalTerraform = void 0;
|
|
7
|
-
|
|
7
|
+
require("@gershy/clearing");
|
|
8
8
|
const slashEscape_ts_1 = __importDefault(require("../../util/slashEscape.js"));
|
|
9
9
|
const petal_ts_1 = __importDefault(require("../petal.js"));
|
|
10
10
|
const util_phrasing_1 = __importDefault(require("@gershy/util-phrasing"));
|
|
11
|
+
const { getClsName, isCls } = cl;
|
|
12
|
+
const hasHead = cl.hasHead;
|
|
13
|
+
const map = cl.map;
|
|
14
|
+
const hasTail = cl.hasTail;
|
|
15
|
+
const indent = cl.indent;
|
|
16
|
+
const mod = cl.mod;
|
|
17
|
+
const toArr = cl.toArr;
|
|
18
|
+
const has = cl.has;
|
|
11
19
|
var PetalTerraform;
|
|
12
20
|
(function (PetalTerraform) {
|
|
13
21
|
class Base extends petal_ts_1.default {
|
|
@@ -15,15 +23,15 @@ var PetalTerraform;
|
|
|
15
23
|
static terraformEncode = (val) => {
|
|
16
24
|
if (val === null)
|
|
17
25
|
return 'null';
|
|
18
|
-
if (
|
|
26
|
+
if (isCls(val, String))
|
|
19
27
|
return val[hasHead]('| ') ? val.slice('| '.length) : `"${(0, slashEscape_ts_1.default)(val, '"\n')}"`;
|
|
20
|
-
if (
|
|
28
|
+
if (isCls(val, Number))
|
|
21
29
|
return `${val.toString(10)}`;
|
|
22
|
-
if (
|
|
30
|
+
if (isCls(val, Boolean))
|
|
23
31
|
return val ? 'true' : 'false';
|
|
24
|
-
if (
|
|
32
|
+
if (isCls(val, Array)) {
|
|
25
33
|
const vals = val[map](v => this.terraformEncode(v));
|
|
26
|
-
if (vals.some(v =>
|
|
34
|
+
if (vals.some(v => isCls(v, String) && v[hasHead]('| ')))
|
|
27
35
|
process.exit(0);
|
|
28
36
|
if (vals.length === 0)
|
|
29
37
|
return '[]';
|
|
@@ -31,7 +39,7 @@ var PetalTerraform;
|
|
|
31
39
|
return `[ ${vals[0]} ]`;
|
|
32
40
|
return `[\n${vals.join(',\n')[indent](' ')}\n]`;
|
|
33
41
|
}
|
|
34
|
-
if (
|
|
42
|
+
if (isCls(val, Object)) {
|
|
35
43
|
// Strings use "| " to avoid any quoting (enabling complex/arbitrary tf)
|
|
36
44
|
// Objects use "$" as a key-prefix to define "nested blocks" instead of "inline maps"
|
|
37
45
|
const keys = Object.keys(val);
|
|
@@ -51,17 +59,17 @@ var PetalTerraform;
|
|
|
51
59
|
if (dedup)
|
|
52
60
|
pcs.pop(); // Remove last item
|
|
53
61
|
// Multi-component keys must pertain to objects
|
|
54
|
-
if (pcs.length > 1 && !
|
|
62
|
+
if (pcs.length > 1 && !isCls(v, Object))
|
|
55
63
|
throw Error('tf key of this form must correspond to object value')[mod]({ k, v });
|
|
56
64
|
// Resolve to raw string?
|
|
57
|
-
if (special &&
|
|
58
|
-
return [(0, util_phrasing_1.default)(
|
|
65
|
+
if (special && isCls(v, String))
|
|
66
|
+
return [(0, util_phrasing_1.default)('camel->snake', pcs[0]), ' = ', this.terraformEncode(v[hasHead]('| ') ? v : `| ${v}`)];
|
|
59
67
|
// Resolve to nested block?
|
|
60
|
-
if (special &&
|
|
61
|
-
return [[(0, util_phrasing_1.default)(
|
|
68
|
+
if (special && isCls(v, Object))
|
|
69
|
+
return [[(0, util_phrasing_1.default)('camel->snake', pcs[0]), ...pcs.slice(1)[map](pc => (0, util_phrasing_1.default)('camel->snake', pc))].join(' '), ' ', this.terraformEncode(v)];
|
|
62
70
|
// Resolve anything else to typical property - use the key exactly as provided (to support,
|
|
63
71
|
// e.g., aws format for keys in policies, any other specific format, etc.)
|
|
64
|
-
return [(0, util_phrasing_1.default)(
|
|
72
|
+
return [(0, util_phrasing_1.default)('camel->snake', pcs[0]), ' = ', this.terraformEncode(v)];
|
|
65
73
|
});
|
|
66
74
|
const len = entryItems.length;
|
|
67
75
|
if (len === 0)
|
|
@@ -73,18 +81,18 @@ var PetalTerraform;
|
|
|
73
81
|
return `{ ${entries[0]} }`;
|
|
74
82
|
return `{\n` + entries.join('\n')[indent](' ') + '\n}';
|
|
75
83
|
}
|
|
76
|
-
throw Error('unexpected val')[mod]({ form:
|
|
84
|
+
throw Error('unexpected val')[mod]({ form: getClsName(val), val });
|
|
77
85
|
};
|
|
78
86
|
constructor() { super(); }
|
|
79
87
|
getType() { throw Error('not implemented'); }
|
|
80
88
|
getHandle() { throw Error('not implemented'); }
|
|
81
89
|
getProps() { throw Error('not implemented'); }
|
|
82
90
|
refStr(props = []) {
|
|
83
|
-
if (!
|
|
91
|
+
if (!isCls(props, Array))
|
|
84
92
|
props = [props];
|
|
85
|
-
const base = `${(0, util_phrasing_1.default)(
|
|
93
|
+
const base = `${(0, util_phrasing_1.default)('camel->snake', this.getType())}.${(0, util_phrasing_1.default)('camel->snake', this.getHandle())}`;
|
|
86
94
|
return props.length
|
|
87
|
-
? `${base}.${props[map](v => (0, util_phrasing_1.default)(
|
|
95
|
+
? `${base}.${props[map](v => (0, util_phrasing_1.default)('camel->snake', v)).join('.')}`
|
|
88
96
|
: base;
|
|
89
97
|
}
|
|
90
98
|
ref(props = []) {
|
|
@@ -130,7 +138,7 @@ var PetalTerraform;
|
|
|
130
138
|
getHandle() { return this.handle; }
|
|
131
139
|
getProps() { return this.props; }
|
|
132
140
|
async getResultHeader() {
|
|
133
|
-
return `resource "${(0, util_phrasing_1.default)(
|
|
141
|
+
return `resource "${(0, util_phrasing_1.default)('camel->snake', this.type)}" "${(0, util_phrasing_1.default)('camel->snake', this.handle)}"`;
|
|
134
142
|
}
|
|
135
143
|
}
|
|
136
144
|
PetalTerraform.Resource = Resource;
|
|
@@ -145,7 +153,7 @@ var PetalTerraform;
|
|
|
145
153
|
}
|
|
146
154
|
getProps() { return this.props; }
|
|
147
155
|
async getResultHeader() {
|
|
148
|
-
return `provider "${(0, util_phrasing_1.default)(
|
|
156
|
+
return `provider "${(0, util_phrasing_1.default)('camel->snake', this.name)}"`;
|
|
149
157
|
}
|
|
150
158
|
}
|
|
151
159
|
PetalTerraform.Provider = Provider;
|
|
@@ -164,7 +172,7 @@ var PetalTerraform;
|
|
|
164
172
|
getHandle() { return this.handle; }
|
|
165
173
|
getProps() { return this.props; }
|
|
166
174
|
async getResultHeader() {
|
|
167
|
-
return `data "${(0, util_phrasing_1.default)(
|
|
175
|
+
return `data "${(0, util_phrasing_1.default)('camel->snake', this.type)}" "${(0, util_phrasing_1.default)('camel->snake', this.handle)}"`;
|
|
168
176
|
}
|
|
169
177
|
refStr(props = []) {
|
|
170
178
|
return `data.${super.refStr(props)}`;
|
package/cmp/cjs/soil/soil.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Context, PetalTerraform, Registry } from '../main.ts';
|
|
2
2
|
import { RegionTerm } from '../util/aws.ts';
|
|
3
|
+
import '@gershy/clearing';
|
|
3
4
|
import { NetProc } from '@gershy/util-http';
|
|
4
5
|
import { SuperIterable } from '../util/superIterable.ts';
|
|
5
6
|
import type Logger from '@gershy/logger';
|
|
7
|
+
import { RestApi } from '@aws-sdk/client-api-gateway';
|
|
6
8
|
export declare namespace Soil {
|
|
7
9
|
type PetalProjArgs = {
|
|
8
10
|
s3Name: string;
|
|
@@ -13,11 +15,14 @@ export declare namespace Soil {
|
|
|
13
15
|
};
|
|
14
16
|
type LocalStackAwsService = never | 'acm' | 'apigateway' | 'cloudformation' | 'cloudwatch' | 'config' | 'dynamodb' | 'dynamodbstreams' | 'ec2' | 'es' | 'events' | 'firehose' | 'iam' | 'kinesis' | 'kms' | 'lambda' | 'logs' | 'opensearch' | 'redshift' | 'resource' | 'resourcegroupstaggingapi' | 'route53' | 'route53resolver' | 's3' | 's3control' | 'scheduler' | 'secretsmanager' | 'ses' | 'sns' | 'sqs' | 'ssm' | 'stepfunctions' | 'sts' | 'support' | 'swf' | 'transcribe';
|
|
15
17
|
type BaseArgs = {
|
|
18
|
+
logger: Logger;
|
|
16
19
|
registry: Registry<any>;
|
|
17
20
|
};
|
|
18
21
|
class Base {
|
|
22
|
+
protected logger: Logger;
|
|
19
23
|
protected registry: Registry<any>;
|
|
20
24
|
constructor(args: BaseArgs);
|
|
25
|
+
getRegion(): string;
|
|
21
26
|
getTerraformPetals(ctx: Context): Promise<PetalProjResult>;
|
|
22
27
|
}
|
|
23
28
|
type LocalStackArgs = BaseArgs & {
|
|
@@ -34,19 +39,22 @@ export declare namespace Soil {
|
|
|
34
39
|
private static localStackInternalPort;
|
|
35
40
|
private aws;
|
|
36
41
|
private localStackDocker;
|
|
37
|
-
private procArgs;
|
|
38
42
|
constructor(args: LocalStackArgs);
|
|
43
|
+
getRegion(): "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
|
|
39
44
|
private getAwsServices;
|
|
40
45
|
private getDockerContainers;
|
|
41
|
-
run(
|
|
42
|
-
logger: Logger;
|
|
43
|
-
}): Promise<{
|
|
46
|
+
run(): Promise<{
|
|
44
47
|
aws: {
|
|
45
48
|
services: LocalStackAwsService[];
|
|
46
49
|
region: "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
|
|
47
50
|
};
|
|
48
51
|
netProc: NetProc;
|
|
49
|
-
|
|
52
|
+
getApis: () => Promise<{
|
|
53
|
+
[x: string]: RestApi & {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
};
|
|
57
|
+
}>;
|
|
50
58
|
}>;
|
|
51
59
|
end(args?: {
|
|
52
60
|
containers?: Awaited<ReturnType<Soil.LocalStack['getDockerContainers']>>;
|
|
@@ -71,6 +79,7 @@ export declare namespace Soil {
|
|
|
71
79
|
class AwsCloud extends Base {
|
|
72
80
|
private aws;
|
|
73
81
|
constructor(args: AwsCloudArgs);
|
|
82
|
+
getRegion(): "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
|
|
74
83
|
getTerraformPetals(ctx: Context): Promise<{
|
|
75
84
|
boot: () => Generator<PetalTerraform.Terraform | PetalTerraform.Provider | PetalTerraform.File, void, unknown>;
|
|
76
85
|
main: (args: any) => Generator<PetalTerraform.Terraform | PetalTerraform.Provider | PetalTerraform.File, void, unknown>;
|
package/cmp/cjs/soil/soil.js
CHANGED
|
@@ -7,18 +7,38 @@ exports.Soil = void 0;
|
|
|
7
7
|
const nodejs_proc_1 = __importDefault(require("@gershy/nodejs-proc"));
|
|
8
8
|
const main_ts_1 = require("../main.js");
|
|
9
9
|
const util_retry_1 = __importDefault(require("@gershy/util-retry"));
|
|
10
|
-
|
|
10
|
+
require("@gershy/clearing");
|
|
11
11
|
const util_http_1 = __importDefault(require("@gershy/util-http"));
|
|
12
12
|
const aws_ts_1 = require("../util/aws.js");
|
|
13
|
+
const client_api_gateway_1 = require("@aws-sdk/client-api-gateway");
|
|
14
|
+
const { skip } = clearing;
|
|
15
|
+
const merge = cl.merge;
|
|
16
|
+
const map = cl.map;
|
|
17
|
+
const cut = cl.cut;
|
|
18
|
+
const fire = cl.fire;
|
|
19
|
+
const hasHead = cl.hasHead;
|
|
20
|
+
const has = cl.has;
|
|
21
|
+
const toObj = cl.toObj;
|
|
22
|
+
const toArr = cl.toArr;
|
|
23
|
+
const baseline = cl.baseline;
|
|
24
|
+
const mod = cl.mod;
|
|
25
|
+
const group = cl.group;
|
|
26
|
+
// TODO: Consider splitting each Soil implementation into its own unit
|
|
27
|
+
// (Soil.LocalStack unit can, e.g., isolate the `@aws-sdk/client-api-gateway` dependency)
|
|
13
28
|
var Soil;
|
|
14
29
|
(function (Soil) {
|
|
15
30
|
class Base {
|
|
31
|
+
logger;
|
|
16
32
|
registry;
|
|
17
33
|
constructor(args) {
|
|
34
|
+
this.logger = args.logger;
|
|
18
35
|
this.registry = args.registry;
|
|
19
36
|
}
|
|
37
|
+
getRegion() {
|
|
38
|
+
throw Error('method definition missing');
|
|
39
|
+
}
|
|
20
40
|
async getTerraformPetals(ctx) {
|
|
21
|
-
throw Error('
|
|
41
|
+
throw Error('method definition missing');
|
|
22
42
|
}
|
|
23
43
|
}
|
|
24
44
|
Soil.Base = Base;
|
|
@@ -27,17 +47,16 @@ var Soil;
|
|
|
27
47
|
static localStackInternalPort = 4566;
|
|
28
48
|
aws;
|
|
29
49
|
localStackDocker;
|
|
30
|
-
procArgs;
|
|
31
50
|
constructor(args) {
|
|
32
|
-
super(args);
|
|
51
|
+
super({ ...args, logger: args.logger.kid('localStack') });
|
|
33
52
|
this.aws = args.aws;
|
|
34
53
|
this.localStackDocker = {
|
|
35
54
|
image: 'localstack/localstack:latest',
|
|
36
55
|
port: LocalStack.localStackInternalPort,
|
|
37
56
|
containerName: 'gershyLilacLocalStack'
|
|
38
57
|
}[merge](args.localStackDocker ?? {});
|
|
39
|
-
this.procArgs = { env: process.env };
|
|
40
58
|
}
|
|
59
|
+
getRegion() { return this.aws.region; }
|
|
41
60
|
getAwsServices() {
|
|
42
61
|
// Note that "overhead" services are essential for initializing localstack:
|
|
43
62
|
// - s3 + ddb used for terraform state locking
|
|
@@ -48,29 +67,29 @@ var Soil;
|
|
|
48
67
|
}
|
|
49
68
|
async getDockerContainers() {
|
|
50
69
|
const { containerName } = this.localStackDocker;
|
|
51
|
-
const dockerPs = await (0, nodejs_proc_1.default)(`docker ps -a --filter "name=${containerName}" --format "{{.Names}},{{.State}}"
|
|
70
|
+
const dockerPs = await (0, nodejs_proc_1.default)(`docker ps -a --filter "name=${containerName}" --format "{{.Names}},{{.State}}"`);
|
|
52
71
|
return dockerPs
|
|
53
72
|
.output
|
|
54
|
-
.split('\n')[map](v => v.trim() ||
|
|
73
|
+
.split('\n')[map](v => v.trim() || skip)[map](v => v[cut](',', 1))[map](([name, state]) => ({ name, state }))
|
|
55
74
|
// Exclude containers which match the `docker ps` filter but don't have the prefix
|
|
56
|
-
[map](v => (v.name === containerName || v.name[hasHead](`${containerName}-`)) ? v :
|
|
75
|
+
[map](v => (v.name === containerName || v.name[hasHead](`${containerName}-`)) ? v : skip);
|
|
57
76
|
}
|
|
58
|
-
run(
|
|
59
|
-
return
|
|
77
|
+
run() {
|
|
78
|
+
return this.logger.scope('run', {}, async (logger) => {
|
|
60
79
|
// Run a localStack container in docker, enabling `terraform apply` on an aws-like target
|
|
61
80
|
const { image, port, containerName } = this.localStackDocker;
|
|
62
81
|
const awsServices = this.getAwsServices();
|
|
63
82
|
await logger.scope('dockerDeploy', { image, containerName, port }, async (logger) => {
|
|
64
|
-
await (0, nodejs_proc_1.default)('docker info'
|
|
83
|
+
await (0, nodejs_proc_1.default)('docker info').catch(({ output }) => Error('docker unavailable')[fire]({ output }));
|
|
65
84
|
logger.log({ $$: 'dockerActive' });
|
|
66
85
|
const containers = await this.getDockerContainers();
|
|
67
86
|
let state = containers.find(c => c.name === containerName)?.state ?? 'nonexistent';
|
|
68
87
|
// First if a container already exists ensure it's compatible with our given config
|
|
69
88
|
if (['running', 'paused', 'exited'][has](state)) {
|
|
70
89
|
const isExistingContainerReusable = await (async () => {
|
|
71
|
-
const { output: inspectJson } = await (0, nodejs_proc_1.default)(`docker inspect ${containerName}
|
|
90
|
+
const { output: inspectJson } = await (0, nodejs_proc_1.default)(`docker inspect ${containerName}`);
|
|
72
91
|
const [containerInfo] = JSON.parse(inspectJson);
|
|
73
|
-
|
|
92
|
+
logger.log({ $$: 'reusableCheck', containerInfo });
|
|
74
93
|
const containerImage = containerInfo.Config.Image;
|
|
75
94
|
const containerEnv = containerInfo.Config.Env[toObj](v => v[cut]('=', 1));
|
|
76
95
|
const containerPort = Number(containerInfo.HostConfig.PortBindings[`${LocalStack.localStackInternalPort}/tcp`]?.[0]?.HostPort ?? 0);
|
|
@@ -83,9 +102,9 @@ var Soil;
|
|
|
83
102
|
})();
|
|
84
103
|
if (isExistingContainerReusable) {
|
|
85
104
|
if (state === 'paused')
|
|
86
|
-
await (0, nodejs_proc_1.default)(`docker unpause ${containerName}
|
|
105
|
+
await (0, nodejs_proc_1.default)(`docker unpause ${containerName}`);
|
|
87
106
|
if (state === 'exited')
|
|
88
|
-
await (0, nodejs_proc_1.default)(`docker start ${containerName}
|
|
107
|
+
await (0, nodejs_proc_1.default)(`docker start ${containerName}`);
|
|
89
108
|
logger.log({ $$: 'containerReused' });
|
|
90
109
|
state = 'running';
|
|
91
110
|
}
|
|
@@ -107,8 +126,8 @@ var Soil;
|
|
|
107
126
|
| -e SERVICES=${awsServices[toArr](v => v).join(',')}
|
|
108
127
|
| -e DEFAULT_REGION=${this.aws.region}
|
|
109
128
|
| ${image}
|
|
110
|
-
`).split('\n')[map](ln => ln.trim() ||
|
|
111
|
-
await (0, nodejs_proc_1.default)(runCmd
|
|
129
|
+
`).split('\n')[map](ln => ln.trim() || skip).join(' ');
|
|
130
|
+
await (0, nodejs_proc_1.default)(runCmd);
|
|
112
131
|
state = 'running';
|
|
113
132
|
}
|
|
114
133
|
if (state !== 'running')
|
|
@@ -131,29 +150,36 @@ var Soil;
|
|
|
131
150
|
if (res.code !== 200)
|
|
132
151
|
throw Error('unhealthy')[mod]({ retry: true });
|
|
133
152
|
const { ya = [], no = [] } = res.body.services[group](v => v === 'available' ? 'ya' : 'no')[map](group => group[toArr]((v, k) => k));
|
|
134
|
-
const missingServices = no[map](svc => awsServices.has(svc) ? svc :
|
|
153
|
+
const missingServices = no[map](svc => awsServices.has(svc) ? svc : skip);
|
|
135
154
|
if (missingServices.length)
|
|
136
155
|
throw Error('services unavailable')[mod]({ missingServices })[mod]({ retry: true });
|
|
137
|
-
return { services: ya };
|
|
156
|
+
return { res, services: ya };
|
|
138
157
|
},
|
|
139
158
|
retryable: err => !!err.retry,
|
|
140
159
|
}).catch(err => err[fire]({ numErrs: err.errs.length, errs: null }));
|
|
141
|
-
logger.log({ $$: '
|
|
160
|
+
logger.log({ $$: 'result', services });
|
|
161
|
+
const netProc = { proto: 'http', addr: 'localhost', port };
|
|
142
162
|
return {
|
|
143
|
-
aws: { services: [...awsServices], region: this.aws.region
|
|
144
|
-
netProc
|
|
145
|
-
|
|
163
|
+
aws: { services: [...awsServices], region: this.aws.region },
|
|
164
|
+
netProc,
|
|
165
|
+
getApis: async () => {
|
|
166
|
+
const client = new client_api_gateway_1.APIGatewayClient({ region: this.aws.region, endpoint: `${netProc.proto}://${netProc.addr}:${netProc.port}` });
|
|
167
|
+
const apiRes = await client.send(new client_api_gateway_1.GetRestApisCommand({}));
|
|
168
|
+
const apis = (apiRes.items ?? []);
|
|
169
|
+
return apis[toObj](api => [api.name, api]);
|
|
170
|
+
}
|
|
146
171
|
};
|
|
147
172
|
});
|
|
148
173
|
}
|
|
149
174
|
async end(args) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
.catch(err => {
|
|
153
|
-
|
|
154
|
-
|
|
175
|
+
return this.logger.scope('end', {}, async (logger) => {
|
|
176
|
+
const containers = args?.containers ?? await this.getDockerContainers();
|
|
177
|
+
await (0, nodejs_proc_1.default)(`docker rm -f ${containers.map(c => c.name).join(' ')}`).catch(err => {
|
|
178
|
+
logger.log({ $$: 'glitch', cmdOutput: err.output });
|
|
179
|
+
return;
|
|
180
|
+
});
|
|
181
|
+
return containers;
|
|
155
182
|
});
|
|
156
|
-
return containers;
|
|
157
183
|
}
|
|
158
184
|
async getTerraformPetals(ctx) {
|
|
159
185
|
const { aws } = this;
|
|
@@ -221,6 +247,7 @@ var Soil;
|
|
|
221
247
|
super(args);
|
|
222
248
|
this.aws = args.aws;
|
|
223
249
|
}
|
|
250
|
+
getRegion() { return this.aws.region; }
|
|
224
251
|
async getTerraformPetals(ctx) {
|
|
225
252
|
const { aws } = this;
|
|
226
253
|
return {
|
package/cmp/cjs/util/aws.d.ts
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
|
+
import '@gershy/clearing';
|
|
1
2
|
export declare const capitalKeys: (v: any) => any;
|
|
2
|
-
export declare const regions:
|
|
3
|
+
export declare const regions: {
|
|
3
4
|
term: "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
|
|
4
5
|
mini: `${"ca" | "us"}${"c" | "u"}${"c" | "u" | "e" | "w"}${"1" | "2"}`;
|
|
5
|
-
}
|
|
6
|
-
term: "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
|
|
7
|
-
mini: `${"ca" | "us"}${"c" | "u"}${"c" | "u" | "e" | "w"}${"1" | "2"}`;
|
|
8
|
-
}, {
|
|
9
|
-
term: "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
|
|
10
|
-
mini: `${"ca" | "us"}${"c" | "u"}${"c" | "u" | "e" | "w"}${"1" | "2"}`;
|
|
11
|
-
}, {
|
|
12
|
-
term: "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
|
|
13
|
-
mini: `${"ca" | "us"}${"c" | "u"}${"c" | "u" | "e" | "w"}${"1" | "2"}`;
|
|
14
|
-
}, {
|
|
15
|
-
term: "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
|
|
16
|
-
mini: `${"ca" | "us"}${"c" | "u"}${"c" | "u" | "e" | "w"}${"1" | "2"}`;
|
|
17
|
-
}];
|
|
6
|
+
}[];
|
|
18
7
|
export type RegionTerm = (typeof regions)[number]['term'];
|
|
19
8
|
export type RegionMini = (typeof regions)[number]['mini'];
|