@gershy/lilac 0.0.12 → 0.0.13

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.
@@ -0,0 +1,291 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Soil = void 0;
7
+ const disk_1 = require("@gershy/disk");
8
+ const nodejs_proc_1 = __importDefault(require("@gershy/nodejs-proc"));
9
+ const main_ts_1 = require("../main.js");
10
+ const util_retry_1 = __importDefault(require("@gershy/util-retry"));
11
+ const clearing_1 = require("@gershy/clearing");
12
+ const util_http_1 = __importDefault(require("@gershy/util-http"));
13
+ const aws_ts_1 = require("../util/aws.js");
14
+ var Soil;
15
+ (function (Soil) {
16
+ class Base {
17
+ registry;
18
+ constructor(args) {
19
+ this.registry = args.registry;
20
+ }
21
+ async getTerraformPetals(ctx) {
22
+ throw Error('not implemented');
23
+ }
24
+ }
25
+ Soil.Base = Base;
26
+ ;
27
+ class LocalStack extends Base {
28
+ static localStackInternalPort = 4566;
29
+ aws;
30
+ localStackDocker;
31
+ procArgs;
32
+ constructor(args) {
33
+ super(args);
34
+ this.aws = args.aws;
35
+ this.localStackDocker = {
36
+ image: 'localstack/localstack:latest',
37
+ port: LocalStack.localStackInternalPort,
38
+ containerName: 'gershyLilacLocalStack'
39
+ }[merge](args.localStackDocker ?? {});
40
+ this.procArgs = { cwd: disk_1.rootFact, env: process.env };
41
+ }
42
+ getAwsServices() {
43
+ // Note that "overhead" services are essential for initializing localstack:
44
+ // - s3 + ddb used for terraform state locking
45
+ // - sts is used for credential validation
46
+ // - iam is needed for role creation
47
+ const overheadAwsServices = ['s3', 'dynamodb', 'sts', 'iam'];
48
+ return new Set([...overheadAwsServices, ...this.registry.getAwsServices()]);
49
+ }
50
+ async getDockerContainers() {
51
+ const { containerName } = this.localStackDocker;
52
+ const dockerPs = await (0, nodejs_proc_1.default)(`docker ps -a --filter "name=${containerName}" --format "{{.Names}},{{.State}}"`, this.procArgs);
53
+ return dockerPs
54
+ .output
55
+ .split('\n')[map](v => v.trim() || clearing_1.skip)[map](v => v[cut](',', 1))[map](([name, state]) => ({ name, state }))
56
+ // Exclude containers which match the `docker ps` filter but don't have the prefix
57
+ [map](v => (v.name === containerName || v.name[hasHead](`${containerName}-`)) ? v : clearing_1.skip);
58
+ }
59
+ run(args) {
60
+ return args.logger.scope('localStack', {}, async (logger) => {
61
+ // Run a localStack container in docker, enabling `terraform apply` on an aws-like target
62
+ const { image, port, containerName } = this.localStackDocker;
63
+ const awsServices = this.getAwsServices();
64
+ await logger.scope('dockerDeploy', { image, containerName, port }, async (logger) => {
65
+ await (0, nodejs_proc_1.default)('docker info', this.procArgs).catch(({ output }) => Error('docker unavailable')[fire]({ output }));
66
+ logger.log({ $$: 'dockerActive' });
67
+ const containers = await this.getDockerContainers();
68
+ let state = containers.find(c => c.name === containerName)?.state ?? 'nonexistent';
69
+ // First if a container already exists ensure it's compatible with our given config
70
+ if (['running', 'paused', 'exited'][has](state)) {
71
+ const isExistingContainerReusable = await (async () => {
72
+ const { output: inspectJson } = await (0, nodejs_proc_1.default)(`docker inspect ${containerName}`, this.procArgs);
73
+ const [containerInfo] = JSON.parse(inspectJson);
74
+ console.log('DOCKER INSPECT', containerInfo);
75
+ const containerImage = containerInfo.Config.Image;
76
+ const containerEnv = containerInfo.Config.Env[toObj](v => v[cut]('=', 1));
77
+ const containerPort = Number(containerInfo.HostConfig.PortBindings[`${LocalStack.localStackInternalPort}/tcp`]?.[0]?.HostPort ?? 0);
78
+ const services = (containerEnv.SERVICES ?? '').split(',').sort().join(',');
79
+ return true
80
+ && containerImage === image
81
+ && containerPort === port
82
+ && containerEnv.DEFAULT_REGION === this.aws.region
83
+ && services === awsServices[toArr](v => v).sort().join(',');
84
+ })();
85
+ if (isExistingContainerReusable) {
86
+ if (state === 'paused')
87
+ await (0, nodejs_proc_1.default)(`docker unpause ${containerName}`, this.procArgs);
88
+ if (state === 'exited')
89
+ await (0, nodejs_proc_1.default)(`docker start ${containerName}`, this.procArgs);
90
+ logger.log({ $$: 'containerReused' });
91
+ state = 'running';
92
+ }
93
+ else {
94
+ await this.end({ containers });
95
+ logger.log({ $$: 'previousLocalStackRemoved', containers });
96
+ state = 'nonexistent';
97
+ }
98
+ }
99
+ if (state === 'nonexistent') {
100
+ const runCmd = String[baseline](`
101
+ | docker run
102
+ | --rm
103
+ | -d
104
+ | --privileged${'' /* TODO: consider removing? */}
105
+ | --name ${containerName}
106
+ | -p ${port}:${LocalStack.localStackInternalPort}
107
+ | -v /var/run/docker.sock:/var/run/docker.sock
108
+ | -e SERVICES=${awsServices[toArr](v => v).join(',')}
109
+ | -e DEFAULT_REGION=${this.aws.region}
110
+ | ${image}
111
+ `).split('\n')[map](ln => ln.trim() || clearing_1.skip).join(' ');
112
+ await (0, nodejs_proc_1.default)(runCmd, this.procArgs);
113
+ state = 'running';
114
+ }
115
+ if (state !== 'running')
116
+ throw Error('container state unexpected')[mod]({ state });
117
+ });
118
+ const readyEndpoint = {
119
+ $req: null,
120
+ $res: null,
121
+ netProc: { proto: 'http', addr: 'localhost', port },
122
+ path: ['_localstack', 'health'],
123
+ method: 'get'
124
+ };
125
+ const { val: { services } } = await (0, util_retry_1.default)({
126
+ // TODO: If the container already exists, it seems its "s3" and "sts" services become unavailable when we try to reinitialize Soil pointing at it??
127
+ attempts: 20,
128
+ delay: n => Math.min(500, 50 * n),
129
+ fn: async () => {
130
+ // Retry all failures and non-200s
131
+ const res = await (0, util_http_1.default)(readyEndpoint, {}).catch(err => err[fire]({ retry: true }));
132
+ if (res.code !== 200)
133
+ throw Error('unhealthy')[mod]({ retry: true });
134
+ const { ya = [], no = [] } = res.body.services[group](v => v === 'available' ? 'ya' : 'no')[map](group => group[toArr]((v, k) => k));
135
+ const missingServices = no[map](svc => awsServices.has(svc) ? svc : clearing_1.skip);
136
+ if (missingServices.length)
137
+ throw Error('services unavailable')[mod]({ missingServices })[mod]({ retry: true });
138
+ return { services: ya };
139
+ },
140
+ retryable: err => !!err.retry,
141
+ }).catch(err => err[fire]({ numErrs: err.errs.length, errs: null }));
142
+ logger.log({ $$: 'localStackActive', services });
143
+ return {
144
+ aws: { services: [...awsServices], region: this.aws.region, },
145
+ netProc: { proto: 'http', addr: 'localhost', port },
146
+ url: `http://localhost:${port}`
147
+ };
148
+ });
149
+ }
150
+ async end(args) {
151
+ const containers = args?.containers ?? await this.getDockerContainers();
152
+ await (0, nodejs_proc_1.default)(`docker rm -f ${containers.map(c => c.name).join(' ')}`, this.procArgs)
153
+ .catch(err => {
154
+ console.log('ERROR ENDING LOCALSTACK DOCKER CONTAINER:\n', err.output);
155
+ return;
156
+ });
157
+ return containers;
158
+ }
159
+ async getTerraformPetals(ctx) {
160
+ const { aws } = this;
161
+ const awsServices = [...this.getAwsServices()];
162
+ const netProc = { proto: 'http', addr: 'localhost', port: this.localStackDocker.port };
163
+ const localStackUrl = `${netProc.proto}://${netProc.addr}:${netProc.port}`;
164
+ return {
165
+ boot: () => [
166
+ new main_ts_1.PetalTerraform.Terraform({
167
+ $requiredProviders: {
168
+ aws: {
169
+ source: 'hashicorp/aws',
170
+ version: `~> 5.0`
171
+ }
172
+ }
173
+ }),
174
+ new main_ts_1.PetalTerraform.Provider('aws', {
175
+ region: aws.region,
176
+ skipCredentialsValidation: true,
177
+ skipRequestingAccountId: true,
178
+ s3UsePathStyle: true, // Otherwise requests can go to "bucket.s3.amazonaws.com", outside localStack
179
+ // Note our localStack setup always includes s3 and ddb (required for tf state storage)
180
+ $endpoints: awsServices[toObj](svc => [svc, localStackUrl])
181
+ })
182
+ ],
183
+ main: function* (args) {
184
+ yield new main_ts_1.PetalTerraform.Terraform({
185
+ $requiredProviders: {
186
+ aws: {
187
+ source: 'hashicorp/aws',
188
+ version: `~> 5.0` // Consider parameterizing??
189
+ }
190
+ },
191
+ '$backend.s3': {
192
+ region: aws.region,
193
+ encrypt: true,
194
+ bucket: args.s3Name,
195
+ key: `tf`,
196
+ dynamodbTable: args.ddbName,
197
+ usePathStyle: true,
198
+ // Point the S3 backend at LocalStack when testing
199
+ endpoints: awsServices[toObj](svc => [svc, localStackUrl]),
200
+ }
201
+ });
202
+ for (const { term } of aws_ts_1.regions)
203
+ yield new main_ts_1.PetalTerraform.Provider('aws', {
204
+ region: term,
205
+ skipCredentialsValidation: true,
206
+ skipRequestingAccountId: true,
207
+ // Omit the alias for the default provider!
208
+ ...(term !== aws.region && { alias: term.split('-').join('_') }),
209
+ // Point providers at LocalStack when testing
210
+ s3UsePathStyle: true,
211
+ $endpoints: awsServices[toObj](svc => [svc, localStackUrl])
212
+ });
213
+ }
214
+ };
215
+ }
216
+ }
217
+ Soil.LocalStack = LocalStack;
218
+ ;
219
+ class AwsCloud extends Base {
220
+ aws;
221
+ constructor(args) {
222
+ super(args);
223
+ this.aws = args.aws;
224
+ }
225
+ async getTerraformPetals(ctx) {
226
+ const { aws } = this;
227
+ return {
228
+ boot: function* () {
229
+ const tfAwsCredsFile = new main_ts_1.PetalTerraform.File('creds.ini', String[baseline](`
230
+ | [default]
231
+ | aws_region = ${aws.region}
232
+ | aws_access_key_id = ${aws.accessKey.id}
233
+ | aws_secret_access_key = ${aws.accessKey['!secret']}
234
+ `));
235
+ yield tfAwsCredsFile;
236
+ yield new main_ts_1.PetalTerraform.Terraform({
237
+ $requiredProviders: {
238
+ aws: {
239
+ source: 'hashicorp/aws',
240
+ version: `~> 5.0`
241
+ }
242
+ }
243
+ });
244
+ yield new main_ts_1.PetalTerraform.Provider('aws', {
245
+ sharedCredentialsFiles: [tfAwsCredsFile.refStr()],
246
+ profile: 'default', // References a section within the credentials file
247
+ region: aws.region,
248
+ });
249
+ },
250
+ main: function* (args) {
251
+ const credFileProfile = 'default';
252
+ const tfAwsCredsFile = new main_ts_1.PetalTerraform.File('creds.ini', String[baseline](`
253
+ | [${credFileProfile}]
254
+ | aws_region = ${aws.region}
255
+ | aws_access_key_id = ${aws.accessKey.id}
256
+ | aws_secret_access_key = ${aws.accessKey['!secret']}
257
+ `));
258
+ yield tfAwsCredsFile;
259
+ yield new main_ts_1.PetalTerraform.Terraform({
260
+ $requiredProviders: {
261
+ aws: {
262
+ source: 'hashicorp/aws',
263
+ version: `~> 5.0` // Consider parameterizing??
264
+ }
265
+ },
266
+ '$backend.s3': {
267
+ sharedCredentialsFiles: [tfAwsCredsFile.refStr()],
268
+ profile: credFileProfile,
269
+ region: aws.region,
270
+ encrypt: true,
271
+ bucket: args.s3Name,
272
+ key: `tf`,
273
+ dynamodbTable: args.ddbName
274
+ }
275
+ });
276
+ for (const { term } of aws_ts_1.regions)
277
+ yield new main_ts_1.PetalTerraform.Provider('aws', {
278
+ sharedCredentialsFiles: [tfAwsCredsFile.refStr()],
279
+ profile: credFileProfile,
280
+ region: term,
281
+ // Omit the alias for the default provider!
282
+ ...(term !== aws.region && { alias: term.split('-').join('_') }),
283
+ });
284
+ }
285
+ };
286
+ }
287
+ }
288
+ Soil.AwsCloud = AwsCloud;
289
+ ;
290
+ })(Soil || (exports.Soil = Soil = {}));
291
+ ;
@@ -1,5 +1,19 @@
1
1
  export declare const capitalKeys: (v: any) => any;
2
- export declare const regions: {
3
- term: string;
4
- mini: string;
5
- }[];
2
+ export declare const regions: readonly [{
3
+ term: "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
4
+ 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
+ }];
18
+ export type RegionTerm = (typeof regions)[number]['term'];
19
+ export type RegionMini = (typeof regions)[number]['mini'];
@@ -57,5 +57,8 @@ exports.regions = [
57
57
  ][map](region => {
58
58
  const [c, z, num] = region.split('-');
59
59
  const [dir0, dir1 = dir0] = z.match(/central|north|south|east|west/g);
60
- return { term: region, mini: [c, dir0[0], dir1[0], num].join('') };
60
+ return {
61
+ term: region,
62
+ mini: [c, dir0[0], dir1[0], num].join('')
63
+ };
61
64
  });
@@ -1,8 +1,10 @@
1
- import { rootFact } from '@gershy/disk';
2
- import { ProcOpts } from '@gershy/util-nodejs-proc';
3
- type Fact = typeof rootFact;
4
- declare const _default: (fact: Fact, cmd: string, opts?: ProcOpts) => Promise<{
5
- logDb: import("@gershy/disk").Fact;
1
+ import { Fact } from '@gershy/disk';
2
+ import { ProcOpts } from '@gershy/nodejs-proc';
3
+ export type ProcTerraformArgs = ProcOpts & {};
4
+ declare const _default: (fact: Fact, cmd: string, opts?: ProcTerraformArgs) => Promise<{
5
+ logDb: Fact;
6
6
  output: string;
7
- }>;
7
+ }> & {
8
+ proc: import("child_process").ChildProcessWithoutNullStreams;
9
+ };
8
10
  export default _default;
@@ -3,8 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const util_nodejs_proc_1 = __importDefault(require("@gershy/util-nodejs-proc"));
7
- exports.default = (fact, cmd, opts) => {
6
+ const nodejs_proc_1 = __importDefault(require("@gershy/nodejs-proc"));
7
+ exports.default = (fact, cmd, opts = {}) => {
8
8
  const numTailingTfLogLines = 20;
9
9
  const writeLog = async (result) => {
10
10
  const [yr, mo, dy, hr, mn, sc, ms] = new Date().toISOString().match(/([0-9]{4})[-]([0-9]{2})[-]([0-9]{2})[T]([0-9]{2})[:]([0-9]{2})[:]([0-9]{2})[.]([0-9]+)[Z]/).slice(1);
@@ -13,12 +13,13 @@ exports.default = (fact, cmd, opts) => {
13
13
  await logDb.setData(result);
14
14
  return logDb;
15
15
  };
16
- return (0, util_nodejs_proc_1.default)(cmd, {
16
+ const prm = (0, nodejs_proc_1.default)(cmd, {
17
17
  timeoutMs: 0,
18
18
  ...opts,
19
19
  cwd: fact,
20
20
  env: { TF_DATA_DIR: '' }
21
- }).then(async (result) => {
21
+ });
22
+ return Object.assign(prm.then(async (result) => {
22
23
  const logDb = await writeLog(result.output);
23
24
  return { logDb, output: result.output.split('\n').slice(-numTailingTfLogLines).join('\n') };
24
25
  }, async (err) => {
@@ -27,5 +28,5 @@ exports.default = (fact, cmd, opts) => {
27
28
  logDb,
28
29
  ...(err.output ? { output: err.output.split('\n').slice(-numTailingTfLogLines).join('\n') } : { cause: err })
29
30
  });
30
- });
31
+ }), { proc: prm.proc });
31
32
  };
@@ -0,0 +1 @@
1
+ export type SuperIterable<T> = Iterable<T> | Promise<Iterable<T>> | AsyncIterable<T>;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ // TODO: Flower definers (Lilac consumers) will want to use these - make sure they're well-exposed
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  exports.json = exports.embed = void 0;
4
5
  const embed = (v) => '${' + v + '}';
package/cmp/mjs/main.d.ts CHANGED
@@ -2,27 +2,22 @@ import '../sideEffects.js';
2
2
  import { PetalTerraform } from './petal/terraform/terraform.ts';
3
3
  import Logger from '@gershy/logger';
4
4
  import { Fact } from '@gershy/disk';
5
+ import { Soil } from './soil/soil.ts';
6
+ import { SuperIterable } from './util/superIterable.ts';
5
7
  export type Context = {
6
8
  name: string;
7
9
  logger: Logger;
8
10
  fact: Fact;
9
11
  patioFact: Fact;
10
- aws: {
11
- accountId: string;
12
- accessKey: {
13
- id: string;
14
- '!secret': string;
15
- };
16
- region: string;
17
- };
18
12
  maturity: string;
19
13
  debug: boolean;
20
14
  pfx: string;
21
15
  };
22
16
  export declare class Flower {
17
+ static getAwsServices(): Soil.LocalStackAwsService[];
23
18
  constructor();
24
19
  getDependencies(): Generator<Flower>;
25
- getPetals(ctx: Context): Iterable<PetalTerraform.Base> | AsyncIterable<PetalTerraform.Base>;
20
+ getPetals(ctx: Context): SuperIterable<PetalTerraform.Base>;
26
21
  }
27
22
  type RegistryFlowers<R extends Registry<any>, M extends 'real' | 'test'> = R extends Registry<infer Flowers> ? {
28
23
  [K in keyof Flowers]: Flowers[K][M];
@@ -33,6 +28,7 @@ export declare class Registry<Flowers extends Obj<{
33
28
  }> = Obj<never>> {
34
29
  private flowers;
35
30
  constructor(flowers: Flowers);
31
+ getAwsServices(): Soil.LocalStackAwsService[];
36
32
  add<MoreFlowers extends Obj<{
37
33
  real: typeof Flower;
38
34
  test: typeof Flower;
@@ -41,32 +37,23 @@ export declare class Registry<Flowers extends Obj<{
41
37
  }
42
38
  export declare class Garden<Reg extends Registry<any>> {
43
39
  private ctx;
44
- private registry;
45
- private define;
40
+ private reg;
41
+ private def;
46
42
  constructor(args: {
47
- name: string;
48
- logger: Logger;
49
- fact: Fact;
50
- patioFact: Fact;
51
- aws: {
52
- accountId: string;
53
- accessKey: {
54
- id: string;
55
- '!secret': string;
56
- };
57
- region: string;
58
- };
59
- maturity: string;
60
- debug: boolean;
61
- pfx: string;
43
+ context: Context;
62
44
  registry: Reg;
63
- define: Garden<Reg>['define'];
45
+ define: Garden<Reg>['def'];
64
46
  });
65
47
  private getPetals;
66
- prepare(): Promise<Obj<Fact>>;
48
+ genTerraform(deployTarget: Soil.Base): Promise<Obj<Fact>>;
67
49
  private terraformInit;
68
50
  private terraformPlan;
69
51
  private terraformApply;
70
- grow(mode: 'real' | 'test'): Promise<void>;
52
+ grow(deploy: {
53
+ type: 'real';
54
+ soil: Soil.Base;
55
+ } | {
56
+ type: 'test';
57
+ }): Promise<void>;
71
58
  }
72
59
  export * from './petal/terraform/terraform.ts';