@gershy/lilac 0.0.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.
Files changed (62) hide show
  1. package/cmp/cjs/main.d.ts +75 -0
  2. package/cmp/cjs/main.js +299 -0
  3. package/cmp/cjs/package.json +1 -0
  4. package/cmp/cjs/petal/petal.d.ts +3 -0
  5. package/cmp/cjs/petal/petal.js +6 -0
  6. package/cmp/cjs/petal/terraform/terraform.d.ts +82 -0
  7. package/cmp/cjs/petal/terraform/terraform.js +194 -0
  8. package/cmp/cjs/util/aws.d.ts +5 -0
  9. package/cmp/cjs/util/aws.js +61 -0
  10. package/cmp/cjs/util/awsTerraform.d.ts +3 -0
  11. package/cmp/cjs/util/awsTerraform.js +9 -0
  12. package/cmp/cjs/util/capitalize.d.ts +2 -0
  13. package/cmp/cjs/util/capitalize.js +12 -0
  14. package/cmp/cjs/util/hash.d.ts +2 -0
  15. package/cmp/cjs/util/hash.js +14 -0
  16. package/cmp/cjs/util/logger.d.ts +20 -0
  17. package/cmp/cjs/util/logger.js +150 -0
  18. package/cmp/cjs/util/normalize.d.ts +2 -0
  19. package/cmp/cjs/util/normalize.js +25 -0
  20. package/cmp/cjs/util/procTerraform.d.ts +8 -0
  21. package/cmp/cjs/util/procTerraform.js +31 -0
  22. package/cmp/cjs/util/slashEscape.d.ts +2 -0
  23. package/cmp/cjs/util/slashEscape.js +10 -0
  24. package/cmp/cjs/util/snakeCase.d.ts +2 -0
  25. package/cmp/cjs/util/snakeCase.js +3 -0
  26. package/cmp/cjs/util/terraform.d.ts +2 -0
  27. package/cmp/cjs/util/terraform.js +11 -0
  28. package/cmp/cjs/util/tryWithHealing.d.ts +7 -0
  29. package/cmp/cjs/util/tryWithHealing.js +11 -0
  30. package/cmp/mjs/main.d.ts +75 -0
  31. package/cmp/mjs/main.js +276 -0
  32. package/cmp/mjs/package.json +1 -0
  33. package/cmp/mjs/petal/petal.d.ts +3 -0
  34. package/cmp/mjs/petal/petal.js +3 -0
  35. package/cmp/mjs/petal/terraform/terraform.d.ts +82 -0
  36. package/cmp/mjs/petal/terraform/terraform.js +188 -0
  37. package/cmp/mjs/util/aws.d.ts +5 -0
  38. package/cmp/mjs/util/aws.js +54 -0
  39. package/cmp/mjs/util/awsTerraform.d.ts +3 -0
  40. package/cmp/mjs/util/awsTerraform.js +5 -0
  41. package/cmp/mjs/util/capitalize.d.ts +2 -0
  42. package/cmp/mjs/util/capitalize.js +10 -0
  43. package/cmp/mjs/util/hash.d.ts +2 -0
  44. package/cmp/mjs/util/hash.js +12 -0
  45. package/cmp/mjs/util/logger.d.ts +20 -0
  46. package/cmp/mjs/util/logger.js +147 -0
  47. package/cmp/mjs/util/normalize.d.ts +2 -0
  48. package/cmp/mjs/util/normalize.js +23 -0
  49. package/cmp/mjs/util/procTerraform.d.ts +8 -0
  50. package/cmp/mjs/util/procTerraform.js +26 -0
  51. package/cmp/mjs/util/slashEscape.d.ts +2 -0
  52. package/cmp/mjs/util/slashEscape.js +8 -0
  53. package/cmp/mjs/util/snakeCase.d.ts +2 -0
  54. package/cmp/mjs/util/snakeCase.js +1 -0
  55. package/cmp/mjs/util/terraform.d.ts +2 -0
  56. package/cmp/mjs/util/terraform.js +6 -0
  57. package/cmp/mjs/util/tryWithHealing.d.ts +7 -0
  58. package/cmp/mjs/util/tryWithHealing.js +9 -0
  59. package/cmp/sideEffects.d.ts +2 -0
  60. package/license +15 -0
  61. package/package.json +55 -0
  62. package/readme.md +39 -0
@@ -0,0 +1,75 @@
1
+ import '../sideEffects.js';
2
+ import { PetalTerraform } from './petal/terraform/terraform.ts';
3
+ import Logger from './util/logger.ts';
4
+ import { Fact } from '@gershy/disk';
5
+ export type Context = {
6
+ name: string;
7
+ logger: Logger;
8
+ fact: Fact;
9
+ patioFact: Fact;
10
+ aws: {
11
+ accountId: string;
12
+ accessKey: {
13
+ id: string;
14
+ '!secret': string;
15
+ };
16
+ region: string;
17
+ };
18
+ maturity: string;
19
+ debug: boolean;
20
+ pfx: string;
21
+ };
22
+ export declare class Flower {
23
+ constructor();
24
+ getDependencies(): Generator<Flower>;
25
+ getPetals(ctx: Context): Iterable<PetalTerraform.Base> | AsyncIterable<PetalTerraform.Base>;
26
+ }
27
+ type RegistryFlowers<R extends Registry<any>, M extends 'real' | 'test'> = R extends Registry<infer Flowers> ? {
28
+ [K in keyof Flowers]: Flowers[K][M];
29
+ } : never;
30
+ export declare class Registry<Flowers extends Obj<{
31
+ real: typeof Flower;
32
+ test: typeof Flower;
33
+ }> = Obj<never>> {
34
+ private flowers;
35
+ constructor(flowers: Flowers);
36
+ add<MoreFlowers extends Obj<{
37
+ real: typeof Flower;
38
+ test: typeof Flower;
39
+ }>>(flowers: MoreFlowers): Registry<Omit<Flowers, keyof MoreFlowers> & MoreFlowers>;
40
+ get<Mode extends 'real' | 'test'>(mode: Mode): RegistryFlowers<Registry<Flowers>, Mode>;
41
+ }
42
+ export declare class Garden<Reg extends Registry<any>> {
43
+ private ctx;
44
+ private registry;
45
+ private define;
46
+ 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;
62
+ registry: Reg;
63
+ define: Garden<Reg>['define'];
64
+ });
65
+ private getPetals;
66
+ prepare(): Promise<{
67
+ bootFact: Fact;
68
+ mainFact: Fact;
69
+ }>;
70
+ private terraformInit;
71
+ private terraformPlan;
72
+ private terraformApply;
73
+ grow(mode: 'real' | 'test'): Promise<void>;
74
+ }
75
+ export * from './petal/terraform/terraform.ts';
@@ -0,0 +1,299 @@
1
+ "use strict";
2
+ // TODO:
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
16
+ };
17
+ var __importDefault = (this && this.__importDefault) || function (mod) {
18
+ return (mod && mod.__esModule) ? mod : { "default": mod };
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.Garden = exports.Registry = exports.Flower = void 0;
22
+ // A more generic (beyond just tf) provider is very hard to support due to the multiplicity of
23
+ // provider/petal combos - e.g. "api" flower would need to support ,api.getCloudformationPetals,
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
+ // Support test-mode (Flowers need to be able to do setup, share config, write to volumes, etc)
29
+ const terraform_ts_1 = require("./petal/terraform/terraform.js");
30
+ const aws_ts_1 = require("./util/aws.js");
31
+ const clearing_1 = require("@gershy/clearing");
32
+ const procTerraform_ts_1 = __importDefault(require("./util/procTerraform.js"));
33
+ const { File, Provider, Terraform } = terraform_ts_1.PetalTerraform;
34
+ class Flower {
35
+ constructor() { }
36
+ *getDependencies() {
37
+ yield this;
38
+ }
39
+ getPetals(ctx) {
40
+ throw Error('not implemented');
41
+ }
42
+ }
43
+ exports.Flower = Flower;
44
+ ;
45
+ class Registry {
46
+ flowers;
47
+ constructor(flowers) {
48
+ this.flowers = {}[merge](flowers);
49
+ }
50
+ add(flowers) {
51
+ return new Registry({ ...this.flowers, ...flowers });
52
+ }
53
+ get(mode) {
54
+ return this.flowers[map]((v) => v[mode]);
55
+ }
56
+ }
57
+ exports.Registry = Registry;
58
+ ;
59
+ class Garden {
60
+ // Note this class currently is coupled to terraform logic
61
+ ctx;
62
+ registry;
63
+ define;
64
+ constructor(args) {
65
+ const { define, registry, ...ctx } = args;
66
+ this.ctx = ctx;
67
+ this.registry = registry;
68
+ this.define = define;
69
+ }
70
+ async *getPetals(mode) {
71
+ const seenFlowers = new Set();
72
+ const seenPetals = new Set();
73
+ for await (const topLevelFlower of this.define(this.ctx, this.registry.get(mode))) {
74
+ for (const flower of topLevelFlower.getDependencies()) {
75
+ if (seenFlowers.has(flower))
76
+ continue;
77
+ seenFlowers.add(flower);
78
+ for await (const petal of flower.getPetals(this.ctx)) {
79
+ if (seenPetals.has(petal))
80
+ continue;
81
+ yield petal;
82
+ }
83
+ }
84
+ }
85
+ }
86
+ async prepare( /* Note this only pertains to "real" mode */) {
87
+ return this.ctx.logger.scope('garden.prepare', {}, async (logger) => {
88
+ const setupTfProj = async (args) => args.logger.scope('tf', { proj: this.ctx.name, tf: args.term }, async (logger) => {
89
+ // Allows a terraform project to be defined in terms of a function which writes to main.tf,
90
+ // and adds any arbitrary additional files to the terraform project
91
+ // Clean up previous terraform
92
+ await logger.scope('files.reset', {}, async (logger) => {
93
+ // We only want to update iac - need to preserve terraform state management
94
+ const tfFilesToPreserve = new Set([
95
+ '.terraform',
96
+ '.terraform.lock.hcl',
97
+ 'terraform.tfstate',
98
+ 'terraform.tfstate.backup'
99
+ ]);
100
+ const kids = await args.fact.getKids();
101
+ await Promise.all(kids[toArr]((kid, k) => tfFilesToPreserve.has(k) ? clearing_1.skip : kid.rem()));
102
+ });
103
+ // Write new terraform
104
+ await logger.scope('files.generate', {}, async (logger) => {
105
+ const stream = await args.fact.kid(['main.tf']).getDataHeadStream();
106
+ await args.setup(args.fact, stream);
107
+ await stream.end(); // TODO: @gershy/disk should allow `await headStream.end()`
108
+ });
109
+ return args.fact;
110
+ });
111
+ // We generate *two* terraform projects for every logical project - overall we want a
112
+ // terraform project which saves its state in the cloud; in order to do this we need to first
113
+ // provision the cloud storage engines to save the terraform state. The "boot" tf project
114
+ // takes care of this, and "main" uses the storage engine provisioned by "boot"!
115
+ return Promise[allObj]({
116
+ bootFact: setupTfProj({
117
+ term: 'boot',
118
+ logger,
119
+ fact: this.ctx.fact.kid(['boot']),
120
+ setup: async (fact, mainWritable) => {
121
+ await mainWritable.write(String[baseline](`
122
+ | terraform {
123
+ | required_providers {
124
+ | aws = {
125
+ | source = "hashicorp/aws"
126
+ | version = "~> 5.0"
127
+ | }
128
+ | }
129
+ | }
130
+ | provider "aws" {
131
+ | shared_credentials_files = [ "creds.ini" ]
132
+ | profile = "default"
133
+ | region = "ca-central-1"
134
+ | }
135
+ | resource "aws_s3_bucket" "tf_state" {
136
+ | bucket = "${this.ctx.pfx}-tf-state"
137
+ | }
138
+ | resource "aws_s3_bucket_ownership_controls" "tf_state" {
139
+ | bucket = aws_s3_bucket.tf_state.bucket
140
+ | rule {
141
+ | object_ownership = "ObjectWriter"
142
+ | }
143
+ | }
144
+ | resource "aws_s3_bucket_acl" "tf_state" {
145
+ | bucket = aws_s3_bucket.tf_state.bucket
146
+ | acl = "private"
147
+ | depends_on = [ aws_s3_bucket_ownership_controls.tf_state ]
148
+ | }
149
+ | resource "aws_dynamodb_table" "tf_state" {
150
+ | name = "${this.ctx.pfx}-tf-state"
151
+ | billing_mode = "PAY_PER_REQUEST"
152
+ | hash_key = "LockID"
153
+ | attribute {
154
+ | name = "LockID"
155
+ | type = "S"
156
+ | }
157
+ | }
158
+ `));
159
+ await fact.kid(['creds.ini']).setData(String[baseline](`
160
+ | [default]
161
+ | aws_region = ${this.ctx.aws.region}
162
+ | aws_access_key_id = ${this.ctx.aws.accessKey.id}
163
+ | aws_secret_access_key = ${this.ctx.aws.accessKey['!secret']}
164
+ `));
165
+ }
166
+ }),
167
+ mainFact: setupTfProj({
168
+ term: 'main',
169
+ logger,
170
+ fact: this.ctx.fact.kid(['main']),
171
+ setup: async (fact, mainWritable) => {
172
+ const garden = this;
173
+ const iteratePetals = async function* () {
174
+ const tfAwsCredsFile = new File('creds.ini', String[baseline](`
175
+ | [default]
176
+ | aws_region = ${garden.ctx.aws.region}
177
+ | aws_access_key_id = ${garden.ctx.aws.accessKey.id}
178
+ | aws_secret_access_key = ${garden.ctx.aws.accessKey['!secret']}
179
+ `));
180
+ yield tfAwsCredsFile;
181
+ const terraform = new Terraform({
182
+ $requiredProviders: {
183
+ aws: {
184
+ source: 'hashicorp/aws',
185
+ version: `~> 5.0` // Consider parameterizing??
186
+ }
187
+ },
188
+ '$backend.s3': {
189
+ region: garden.ctx.aws.region,
190
+ encrypt: true,
191
+ // Note references not allowed in terraform.backend!!
192
+ bucket: `${garden.ctx.pfx}-tf-state`,
193
+ key: `tf`,
194
+ dynamodbTable: `${garden.ctx.pfx}-tf-state`, // Dynamodb table is aws-account-wide
195
+ sharedCredentialsFiles: [tfAwsCredsFile.tfRef()],
196
+ profile: 'default', // References a section within the credentials file
197
+ }
198
+ });
199
+ yield terraform;
200
+ for (const { term } of aws_ts_1.regions)
201
+ yield new Provider('aws', {
202
+ sharedCredentialsFiles: [tfAwsCredsFile.tfRef()],
203
+ profile: 'default', // References a section within the credentials file
204
+ region: term,
205
+ // Omit the alias for the default provider!
206
+ ...(term !== garden.ctx.aws.region && { alias: term.split('-').join('_') })
207
+ });
208
+ yield* garden.getPetals('real');
209
+ };
210
+ const patioTfHclFact = garden.ctx.patioFact.kid(['main', '.terraform.lock.hcl']);
211
+ const tfHclData = await patioTfHclFact.getData('str');
212
+ if (tfHclData)
213
+ await fact.kid(['.terraform.lock.hcl']).setData(tfHclData);
214
+ for await (const petal of iteratePetals()) {
215
+ const result = await (async () => {
216
+ const result = await petal.getResult();
217
+ if (!(0, clearing_1.isCls)(result, Object))
218
+ return { tf: result, files: {} };
219
+ return { files: {}, ...result };
220
+ })();
221
+ if (result.tf)
222
+ await mainWritable.write(`${result.tf}\n`);
223
+ await Promise.all(result.files[toArr]((data, kfp) => fact.kid(kfp.split('/')).setData(data)));
224
+ }
225
+ }
226
+ })
227
+ });
228
+ });
229
+ }
230
+ terraformInit(fact, args) {
231
+ return this.ctx.logger.scope('execTf.init', {}, async (logger) => {
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
+ // TODO: Some terraform commands fail when offline - can this be covered up?
236
+ const result = await (0, procTerraform_ts_1.default)(fact, `terraform init -input=false`);
237
+ logger.log({ $$: 'result', logFp: result.logDb.toString(), msg: result.output });
238
+ return result;
239
+ });
240
+ }
241
+ terraformPlan(fact, args) {
242
+ return this.ctx.logger.scope('execTf.plan', {}, async (logger) => {
243
+ const result = await (0, procTerraform_ts_1.default)(fact, `terraform plan -input=false`);
244
+ logger.log({ $$: 'result', logFp: result.logDb.toString(), msg: result.output });
245
+ return result;
246
+ });
247
+ }
248
+ terraformApply(fact, args) {
249
+ return this.ctx.logger.scope('execTf.apply', {}, async (logger) => {
250
+ const result = await (0, procTerraform_ts_1.default)(fact, `terraform apply -input=false -auto-approve`);
251
+ logger.log({ $$: 'result', logFp: result.logDb.toString(), msg: result.output });
252
+ return result;
253
+ });
254
+ }
255
+ async grow(mode) {
256
+ // - Avoid tf preventing deletions on populated dbs - e.g. s3 tf needs "force_destroy = true"
257
+ // - Note `terraform init` generates a ".terraform.lock.hcl" file - this should be checked into
258
+ // consumer's source control! May need consumers to provide an optional `repoFact`, and if
259
+ // present after `terraform init` we can copy the ".terraform.lock.hcl" file
260
+ // Note that test mode does not involve any iac - it all runs locally
261
+ if (mode === 'test')
262
+ throw Error('test mode not implemented yet');
263
+ const { bootFact, mainFact } = await this.prepare();
264
+ const tryWithHealing = async (args) => {
265
+ const { fn, heal, canHeal } = args;
266
+ return fn().catch(async (err) => {
267
+ if (!canHeal(err))
268
+ throw err;
269
+ await heal();
270
+ return fn();
271
+ });
272
+ };
273
+ const logicalApply = (args) => {
274
+ return tryWithHealing({
275
+ fn: () => this.terraformApply(args.mainFact),
276
+ canHeal: err => (err.output ?? '')[has]('please run "terraform init"'),
277
+ heal: () => tryWithHealing({
278
+ fn: async () => {
279
+ await this.terraformInit(args.mainFact);
280
+ await this.ctx.patioFact.kid(['main', '.terraform.lock.hcl']).setData(await args.mainFact.kid(['.terraform.lock.hcl']).getData('str'));
281
+ },
282
+ canHeal: err => true,
283
+ heal: () => tryWithHealing({
284
+ fn: () => this.terraformApply(args.bootFact),
285
+ canHeal: err => (err.output ?? '')[has]('please run "terraform init"'),
286
+ heal: () => this.terraformInit(args.bootFact)
287
+ })
288
+ })
289
+ });
290
+ };
291
+ // TODO: HEEERE do `logicalApply` instead!
292
+ await this.terraformInit(bootFact);
293
+ // const result = await logicalApply({ mainFact, bootFact });
294
+ //const result = await execTf.init(bootFact);
295
+ }
296
+ }
297
+ exports.Garden = Garden;
298
+ ;
299
+ __exportStar(require("./petal/terraform/terraform.js"), exports);
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,3 @@
1
+ export default class Petal {
2
+ constructor();
3
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class Petal {
4
+ constructor() { }
5
+ }
6
+ exports.default = Petal;
@@ -0,0 +1,82 @@
1
+ import Petal from '../petal.ts';
2
+ export declare namespace PetalTerraform {
3
+ class Base extends Petal {
4
+ static terraformEncode: (val: Json) => string;
5
+ constructor();
6
+ getType(): string;
7
+ getHandle(): string;
8
+ getProps(): {
9
+ [key: string]: Json;
10
+ };
11
+ tfRef(props?: string | string[]): string;
12
+ tfRefp(props?: string | string[]): `| ${string}`;
13
+ getResultHeader(): Promise<string>;
14
+ getResult(): Promise<string | {
15
+ tf: string;
16
+ files?: Obj<string | Buffer>;
17
+ }>;
18
+ }
19
+ class Terraform extends Base {
20
+ private props;
21
+ constructor(props: {
22
+ [key: string]: Json;
23
+ });
24
+ getProps(): {
25
+ [key: string]: Json;
26
+ };
27
+ getResultHeader(): Promise<string>;
28
+ }
29
+ class Resource extends Base {
30
+ private type;
31
+ private handle;
32
+ private props;
33
+ constructor(type: string, handle: string, props: {
34
+ [key: string]: Json;
35
+ });
36
+ getType(): string;
37
+ getHandle(): string;
38
+ getProps(): {
39
+ [key: string]: Json;
40
+ };
41
+ getResultHeader(): Promise<string>;
42
+ }
43
+ class Provider extends Base {
44
+ private name;
45
+ private props;
46
+ constructor(name: string, props: {
47
+ [key: string]: Json;
48
+ });
49
+ getProps(): {
50
+ [key: string]: Json;
51
+ };
52
+ getResultHeader(): Promise<string>;
53
+ }
54
+ class Data extends Base {
55
+ private type;
56
+ private handle;
57
+ private props;
58
+ constructor(type: string, handle: string, props: {
59
+ [key: string]: Json;
60
+ });
61
+ getType(): string;
62
+ getHandle(): string;
63
+ getProps(): {
64
+ [key: string]: Json;
65
+ };
66
+ getResultHeader(): Promise<string>;
67
+ tfRef(props?: string | string[]): string;
68
+ }
69
+ class File extends Base {
70
+ private fp;
71
+ private content;
72
+ constructor(fp: string, content: string | Buffer);
73
+ getType(): string;
74
+ getResult(): Promise<{
75
+ tf: string;
76
+ files: {
77
+ [x: string]: string | Buffer<ArrayBufferLike>;
78
+ };
79
+ }>;
80
+ tfRef(props?: string | string[]): string;
81
+ }
82
+ }
@@ -0,0 +1,194 @@
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.PetalTerraform = void 0;
7
+ const clearing_1 = require("@gershy/clearing");
8
+ const slashEscape_ts_1 = __importDefault(require("../../util/slashEscape.js"));
9
+ const snakeCase_ts_1 = __importDefault(require("../../util/snakeCase.js"));
10
+ const petal_ts_1 = __importDefault(require("../petal.js"));
11
+ var PetalTerraform;
12
+ (function (PetalTerraform) {
13
+ class Base extends petal_ts_1.default {
14
+ // Defines petals using terraform; subclasses generally map to terraform's different block types
15
+ static terraformEncode = (val) => {
16
+ if (val === null)
17
+ return 'null';
18
+ if ((0, clearing_1.isCls)(val, String))
19
+ return val[hasHead]('| ') ? val.slice('| '.length) : `"${(0, slashEscape_ts_1.default)(val, '"\n')}"`;
20
+ if ((0, clearing_1.isCls)(val, Number))
21
+ return `${val.toString(10)}`;
22
+ if ((0, clearing_1.isCls)(val, Boolean))
23
+ return val ? 'true' : 'false';
24
+ if ((0, clearing_1.isCls)(val, Array)) {
25
+ const vals = val[map](v => this.terraformEncode(v));
26
+ if (vals.some(v => (0, clearing_1.isCls)(v, String) && v[hasHead]('| ')))
27
+ process.exit(0);
28
+ if (vals.length === 0)
29
+ return '[]';
30
+ if (vals.length === 1)
31
+ return `[ ${vals[0]} ]`;
32
+ return `[\n${vals.join(',\n')[indent](' ')}\n]`;
33
+ }
34
+ if ((0, clearing_1.isCls)(val, Object)) {
35
+ // Strings use "| " to avoid any quoting (enabling complex/arbitrary tf)
36
+ // Objects use "$" as a key-prefix to define "nested blocks" instead of "inline maps"
37
+ const keys = Object.keys(val);
38
+ if (keys.length === 1 && keys[0][hasTail]('()')) {
39
+ const vals = val[keys[0]][map](v => this.terraformEncode(v));
40
+ return `${keys[0].slice(0, -'()'.length)}(${vals.join(', ')})`;
41
+ }
42
+ const entryItems = val[toArr]((v, k) => {
43
+ // "special" indicates whether the first char was "$" - it causes objects to be assigned as
44
+ // *nested blocks*
45
+ const [special, pcs] = (() => {
46
+ const ks = k[hasHead]('$');
47
+ const kk = ks ? k.slice(1) : k;
48
+ return [ks, kk.split('.')];
49
+ })();
50
+ const dedup = pcs.length > 1 && /^[0-9]+$/.test(pcs.at(-1));
51
+ if (dedup)
52
+ pcs.pop(); // Remove last item
53
+ // Multi-component keys must pertain to objects
54
+ if (pcs.length > 1 && !(0, clearing_1.isCls)(v, Object))
55
+ throw Error('tf key of this form must correspond to object value')[mod]({ k, v });
56
+ // Resolve to raw string?
57
+ if (special && (0, clearing_1.isCls)(v, String))
58
+ return [(0, snakeCase_ts_1.default)(pcs[0]), ' = ', this.terraformEncode(v[hasHead]('| ') ? v : `| ${v}`)];
59
+ // Resolve to nested block?
60
+ if (special && (0, clearing_1.isCls)(v, Object))
61
+ return [[(0, snakeCase_ts_1.default)(pcs[0]), ...pcs.slice(1)[map](pc => (0, snakeCase_ts_1.default)(pc))].join(' '), ' ', this.terraformEncode(v)];
62
+ // Resolve anything else to typical property - use the key exactly as provided (to support,
63
+ // e.g., aws format for keys in policies, any other specific format, etc.)
64
+ return [(0, snakeCase_ts_1.default)(pcs[0]), ' = ', this.terraformEncode(v)];
65
+ });
66
+ const len = entryItems.length;
67
+ if (len === 0)
68
+ return '{}';
69
+ const entries = entryItems[map](([key, joiner, value]) => [key, joiner, value].join(''));
70
+ // Note single-line terraform definitions are illegal for non-linear values
71
+ // Determine whether the the value is linear based on its terminating character (janky)
72
+ if (len === 1 && !entries[0][has]('\n') && !'}]'[has](entries[0].at(-1)))
73
+ return `{ ${entries[0]} }`;
74
+ return `{\n` + entries.join('\n')[indent](' ') + '\n}';
75
+ }
76
+ throw Error('unexpected val')[mod]({ form: (0, clearing_1.getClsName)(val), val });
77
+ };
78
+ constructor() { super(); }
79
+ getType() { throw Error('not implemented'); }
80
+ getHandle() { throw Error('not implemented'); }
81
+ getProps() { throw Error('not implemented'); }
82
+ tfRef(props = []) {
83
+ if (!(0, clearing_1.isCls)(props, Array))
84
+ props = [props];
85
+ const base = `${(0, snakeCase_ts_1.default)(this.getType())}.${(0, snakeCase_ts_1.default)(this.getHandle())}`;
86
+ return props.length
87
+ ? `${base}.${props[map](v => (0, snakeCase_ts_1.default)(v)).join('.')}`
88
+ : base;
89
+ }
90
+ tfRefp(props = []) {
91
+ // "plain ref" - uses "| " to avoid being quoted within terraform
92
+ return `| ${this.tfRef(props)}`;
93
+ }
94
+ async getResultHeader() { throw Error('not implemented'); }
95
+ async getResult() {
96
+ // Get header and props
97
+ const [header, props] = await Promise.all([
98
+ this.getResultHeader(),
99
+ Base.terraformEncode(this.getProps())
100
+ ]);
101
+ return `${header} ${props}`;
102
+ }
103
+ }
104
+ PetalTerraform.Base = Base;
105
+ ;
106
+ class Terraform extends Base {
107
+ props;
108
+ constructor(props) {
109
+ super();
110
+ this.props = props;
111
+ }
112
+ getProps() { return this.props; }
113
+ async getResultHeader() {
114
+ return `terraform`;
115
+ }
116
+ }
117
+ PetalTerraform.Terraform = Terraform;
118
+ ;
119
+ class Resource extends Base {
120
+ type;
121
+ handle;
122
+ props;
123
+ constructor(type, handle, props) {
124
+ super();
125
+ this.type = type;
126
+ this.handle = handle;
127
+ this.props = props;
128
+ }
129
+ getType() { return this.type; }
130
+ getHandle() { return this.handle; }
131
+ getProps() { return this.props; }
132
+ async getResultHeader() {
133
+ return `resource "${(0, snakeCase_ts_1.default)(this.type)}" "${(0, snakeCase_ts_1.default)(this.handle)}"`;
134
+ }
135
+ }
136
+ PetalTerraform.Resource = Resource;
137
+ ;
138
+ class Provider extends Base {
139
+ name;
140
+ props;
141
+ constructor(name, props) {
142
+ super();
143
+ this.name = name;
144
+ this.props = props;
145
+ }
146
+ getProps() { return this.props; }
147
+ async getResultHeader() {
148
+ return `provider "${(0, snakeCase_ts_1.default)(this.name)}"`;
149
+ }
150
+ }
151
+ PetalTerraform.Provider = Provider;
152
+ ;
153
+ class Data extends Base {
154
+ type;
155
+ handle;
156
+ props;
157
+ constructor(type, handle, props) {
158
+ super();
159
+ this.type = type;
160
+ this.handle = handle;
161
+ this.props = props;
162
+ }
163
+ getType() { return this.type; }
164
+ getHandle() { return this.handle; }
165
+ getProps() { return this.props; }
166
+ async getResultHeader() {
167
+ return `data "${(0, snakeCase_ts_1.default)(this.type)}" "${(0, snakeCase_ts_1.default)(this.handle)}"`;
168
+ }
169
+ tfRef(props = []) {
170
+ return `data.${super.tfRef(props)}`;
171
+ }
172
+ }
173
+ PetalTerraform.Data = Data;
174
+ ;
175
+ class File extends Base {
176
+ fp;
177
+ content;
178
+ constructor(fp, content) {
179
+ super();
180
+ this.fp = fp;
181
+ this.content = content;
182
+ }
183
+ getType() { return '_file'; }
184
+ async getResult() {
185
+ return { tf: '', files: { [this.fp]: this.content } };
186
+ }
187
+ tfRef(props) {
188
+ return this.fp; // `this.fp` should be quoted but not transformed to a tf handle
189
+ }
190
+ }
191
+ PetalTerraform.File = File;
192
+ ;
193
+ })(PetalTerraform || (exports.PetalTerraform = PetalTerraform = {}));
194
+ ;
@@ -0,0 +1,5 @@
1
+ export declare const capitalKeys: (v: any) => any;
2
+ export declare const regions: {
3
+ term: string;
4
+ mini: string;
5
+ }[];