@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 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 { Fact } from '@gershy/disk';
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(deployTarget: Soil.Base): Promise<Obj<Fact>>;
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 clearing_1 = require("@gershy/clearing");
31
- const procTerraform_ts_1 = __importDefault(require("./util/procTerraform.js"));
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
- for await (const petal of await flower.getPetals(this.ctx)) {
103
- if (seenPetals.has(petal))
104
- continue;
105
- yield petal;
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(deployTarget) {
111
- const soilTfPetalsPrm = deployTarget.getTerraformPetals(this.ctx);
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) ? clearing_1.skip : kid.rem()));
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)('payPerRequest', 'camel', 'snake')[upper](),
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
- terraformInit(fact, args) {
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
- // Consider if we ever want to pass "-reconfigure" and "-migrate-state" options; these are
209
- // useful if we are moving backends (e.g. one aws account to another), and want to move our
210
- // full iac definition too
211
- // TODO: Some terraform commands fail when offline - can this be covered up? Possibly by
212
- // checking terraform binaries into the repo? (Cross-platform nightmare though...)
213
- const result = await (0, procTerraform_ts_1.default)(fact, `terraform init -input=false`, {
214
- onData: async (mode, data) => logger.log({ $$: 'notice', mode, data }) ?? null
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', logFp: result.logDb.toString(), msg: result.output });
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, procTerraform_ts_1.default)(fact, `terraform plan -input=false`);
223
- logger.log({ $$: 'result', logFp: result.logDb.toString(), msg: result.output });
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, procTerraform_ts_1.default)(fact, `terraform apply -input=false -auto-approve`);
230
- logger.log({ $$: 'result', logFp: result.logDb.toString(), msg: result.output });
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
  }
@@ -1,3 +1,4 @@
1
+ import '@gershy/clearing';
1
2
  import Petal from '../petal.ts';
2
3
  export declare namespace PetalTerraform {
3
4
  class Base extends Petal {
@@ -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
- const clearing_1 = require("@gershy/clearing");
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 ((0, clearing_1.isCls)(val, String))
26
+ if (isCls(val, String))
19
27
  return val[hasHead]('| ') ? val.slice('| '.length) : `"${(0, slashEscape_ts_1.default)(val, '"\n')}"`;
20
- if ((0, clearing_1.isCls)(val, Number))
28
+ if (isCls(val, Number))
21
29
  return `${val.toString(10)}`;
22
- if ((0, clearing_1.isCls)(val, Boolean))
30
+ if (isCls(val, Boolean))
23
31
  return val ? 'true' : 'false';
24
- if ((0, clearing_1.isCls)(val, Array)) {
32
+ if (isCls(val, Array)) {
25
33
  const vals = val[map](v => this.terraformEncode(v));
26
- if (vals.some(v => (0, clearing_1.isCls)(v, String) && v[hasHead]('| ')))
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 ((0, clearing_1.isCls)(val, Object)) {
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 && !(0, clearing_1.isCls)(v, Object))
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 && (0, clearing_1.isCls)(v, String))
58
- return [(0, util_phrasing_1.default)(pcs[0], 'camel', 'snake'), ' = ', this.terraformEncode(v[hasHead]('| ') ? v : `| ${v}`)];
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 && (0, clearing_1.isCls)(v, Object))
61
- return [[(0, util_phrasing_1.default)(pcs[0], 'camel', 'snake'), ...pcs.slice(1)[map](pc => (0, util_phrasing_1.default)(pc, 'camel', 'snake'))].join(' '), ' ', this.terraformEncode(v)];
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)(pcs[0], 'camel', 'snake'), ' = ', this.terraformEncode(v)];
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: (0, clearing_1.getClsName)(val), val });
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 (!(0, clearing_1.isCls)(props, Array))
91
+ if (!isCls(props, Array))
84
92
  props = [props];
85
- const base = `${(0, util_phrasing_1.default)(this.getType(), 'camel', 'snake')}.${(0, util_phrasing_1.default)(this.getHandle(), 'camel', 'snake')}`;
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)(v, 'camel', 'snake')).join('.')}`
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)(this.type, 'camel', 'snake')}" "${(0, util_phrasing_1.default)(this.handle, 'camel', 'snake')}"`;
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)(this.name, 'camel', 'snake')}"`;
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)(this.type, 'camel', 'snake')}" "${(0, util_phrasing_1.default)(this.handle, 'camel', 'snake')}"`;
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)}`;
@@ -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(args: {
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
- url: string;
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>;
@@ -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
- const clearing_1 = require("@gershy/clearing");
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('not implemented');
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}}"`, this.procArgs);
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() || clearing_1.skip)[map](v => v[cut](',', 1))[map](([name, state]) => ({ name, state }))
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 : clearing_1.skip);
75
+ [map](v => (v.name === containerName || v.name[hasHead](`${containerName}-`)) ? v : skip);
57
76
  }
58
- run(args) {
59
- return args.logger.scope('localStack', {}, async (logger) => {
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', this.procArgs).catch(({ output }) => Error('docker unavailable')[fire]({ output }));
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}`, this.procArgs);
90
+ const { output: inspectJson } = await (0, nodejs_proc_1.default)(`docker inspect ${containerName}`);
72
91
  const [containerInfo] = JSON.parse(inspectJson);
73
- console.log('DOCKER INSPECT', containerInfo);
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}`, this.procArgs);
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}`, this.procArgs);
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() || clearing_1.skip).join(' ');
111
- await (0, nodejs_proc_1.default)(runCmd, this.procArgs);
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 : clearing_1.skip);
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({ $$: 'localStackActive', services });
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: { proto: 'http', addr: 'localhost', port },
145
- url: `http://localhost:${port}`
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
- const containers = args?.containers ?? await this.getDockerContainers();
151
- await (0, nodejs_proc_1.default)(`docker rm -f ${containers.map(c => c.name).join(' ')}`, this.procArgs)
152
- .catch(err => {
153
- console.log('ERROR ENDING LOCALSTACK DOCKER CONTAINER:\n', err.output);
154
- return;
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 {
@@ -1,19 +1,8 @@
1
+ import '@gershy/clearing';
1
2
  export declare const capitalKeys: (v: any) => any;
2
- export declare const regions: readonly [{
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'];