@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,276 @@
1
+ // TODO:
2
+ // A more generic (beyond just tf) provider is very hard to support due to the multiplicity of
3
+ // provider/petal combos - e.g. "api" flower would need to support ,api.getCloudformationPetals,
4
+ // api.getTerraformPetals, etc... supporting just terraform for now
5
+ // Watch out when working lilac into an npm dependency, need to allow the user a way to declare
6
+ // their absolute repo path (so "<repo>/..." filenames work in any setup!)
7
+ // Can region be dealt with any better??
8
+ // Support test-mode (Flowers need to be able to do setup, share config, write to volumes, etc)
9
+ import { PetalTerraform } from "./petal/terraform/terraform.js";
10
+ import { regions as awsRegions } from "./util/aws.js";
11
+ import { isCls, skip } from '@gershy/clearing';
12
+ import procTerraform from "./util/procTerraform.js";
13
+ const { File, Provider, Terraform } = PetalTerraform;
14
+ export class Flower {
15
+ constructor() { }
16
+ *getDependencies() {
17
+ yield this;
18
+ }
19
+ getPetals(ctx) {
20
+ throw Error('not implemented');
21
+ }
22
+ }
23
+ ;
24
+ export class Registry {
25
+ flowers;
26
+ constructor(flowers) {
27
+ this.flowers = {}[merge](flowers);
28
+ }
29
+ add(flowers) {
30
+ return new Registry({ ...this.flowers, ...flowers });
31
+ }
32
+ get(mode) {
33
+ return this.flowers[map]((v) => v[mode]);
34
+ }
35
+ }
36
+ ;
37
+ export class Garden {
38
+ // Note this class currently is coupled to terraform logic
39
+ ctx;
40
+ registry;
41
+ define;
42
+ constructor(args) {
43
+ const { define, registry, ...ctx } = args;
44
+ this.ctx = ctx;
45
+ this.registry = registry;
46
+ this.define = define;
47
+ }
48
+ async *getPetals(mode) {
49
+ const seenFlowers = new Set();
50
+ const seenPetals = new Set();
51
+ for await (const topLevelFlower of this.define(this.ctx, this.registry.get(mode))) {
52
+ for (const flower of topLevelFlower.getDependencies()) {
53
+ if (seenFlowers.has(flower))
54
+ continue;
55
+ seenFlowers.add(flower);
56
+ for await (const petal of flower.getPetals(this.ctx)) {
57
+ if (seenPetals.has(petal))
58
+ continue;
59
+ yield petal;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ async prepare( /* Note this only pertains to "real" mode */) {
65
+ return this.ctx.logger.scope('garden.prepare', {}, async (logger) => {
66
+ const setupTfProj = async (args) => args.logger.scope('tf', { proj: this.ctx.name, tf: args.term }, async (logger) => {
67
+ // Allows a terraform project to be defined in terms of a function which writes to main.tf,
68
+ // and adds any arbitrary additional files to the terraform project
69
+ // Clean up previous terraform
70
+ await logger.scope('files.reset', {}, async (logger) => {
71
+ // We only want to update iac - need to preserve terraform state management
72
+ const tfFilesToPreserve = new Set([
73
+ '.terraform',
74
+ '.terraform.lock.hcl',
75
+ 'terraform.tfstate',
76
+ 'terraform.tfstate.backup'
77
+ ]);
78
+ const kids = await args.fact.getKids();
79
+ await Promise.all(kids[toArr]((kid, k) => tfFilesToPreserve.has(k) ? skip : kid.rem()));
80
+ });
81
+ // Write new terraform
82
+ await logger.scope('files.generate', {}, async (logger) => {
83
+ const stream = await args.fact.kid(['main.tf']).getDataHeadStream();
84
+ await args.setup(args.fact, stream);
85
+ await stream.end(); // TODO: @gershy/disk should allow `await headStream.end()`
86
+ });
87
+ return args.fact;
88
+ });
89
+ // We generate *two* terraform projects for every logical project - overall we want a
90
+ // terraform project which saves its state in the cloud; in order to do this we need to first
91
+ // provision the cloud storage engines to save the terraform state. The "boot" tf project
92
+ // takes care of this, and "main" uses the storage engine provisioned by "boot"!
93
+ return Promise[allObj]({
94
+ bootFact: setupTfProj({
95
+ term: 'boot',
96
+ logger,
97
+ fact: this.ctx.fact.kid(['boot']),
98
+ setup: async (fact, mainWritable) => {
99
+ await mainWritable.write(String[baseline](`
100
+ | terraform {
101
+ | required_providers {
102
+ | aws = {
103
+ | source = "hashicorp/aws"
104
+ | version = "~> 5.0"
105
+ | }
106
+ | }
107
+ | }
108
+ | provider "aws" {
109
+ | shared_credentials_files = [ "creds.ini" ]
110
+ | profile = "default"
111
+ | region = "ca-central-1"
112
+ | }
113
+ | resource "aws_s3_bucket" "tf_state" {
114
+ | bucket = "${this.ctx.pfx}-tf-state"
115
+ | }
116
+ | resource "aws_s3_bucket_ownership_controls" "tf_state" {
117
+ | bucket = aws_s3_bucket.tf_state.bucket
118
+ | rule {
119
+ | object_ownership = "ObjectWriter"
120
+ | }
121
+ | }
122
+ | resource "aws_s3_bucket_acl" "tf_state" {
123
+ | bucket = aws_s3_bucket.tf_state.bucket
124
+ | acl = "private"
125
+ | depends_on = [ aws_s3_bucket_ownership_controls.tf_state ]
126
+ | }
127
+ | resource "aws_dynamodb_table" "tf_state" {
128
+ | name = "${this.ctx.pfx}-tf-state"
129
+ | billing_mode = "PAY_PER_REQUEST"
130
+ | hash_key = "LockID"
131
+ | attribute {
132
+ | name = "LockID"
133
+ | type = "S"
134
+ | }
135
+ | }
136
+ `));
137
+ await fact.kid(['creds.ini']).setData(String[baseline](`
138
+ | [default]
139
+ | aws_region = ${this.ctx.aws.region}
140
+ | aws_access_key_id = ${this.ctx.aws.accessKey.id}
141
+ | aws_secret_access_key = ${this.ctx.aws.accessKey['!secret']}
142
+ `));
143
+ }
144
+ }),
145
+ mainFact: setupTfProj({
146
+ term: 'main',
147
+ logger,
148
+ fact: this.ctx.fact.kid(['main']),
149
+ setup: async (fact, mainWritable) => {
150
+ const garden = this;
151
+ const iteratePetals = async function* () {
152
+ const tfAwsCredsFile = new File('creds.ini', String[baseline](`
153
+ | [default]
154
+ | aws_region = ${garden.ctx.aws.region}
155
+ | aws_access_key_id = ${garden.ctx.aws.accessKey.id}
156
+ | aws_secret_access_key = ${garden.ctx.aws.accessKey['!secret']}
157
+ `));
158
+ yield tfAwsCredsFile;
159
+ const terraform = new Terraform({
160
+ $requiredProviders: {
161
+ aws: {
162
+ source: 'hashicorp/aws',
163
+ version: `~> 5.0` // Consider parameterizing??
164
+ }
165
+ },
166
+ '$backend.s3': {
167
+ region: garden.ctx.aws.region,
168
+ encrypt: true,
169
+ // Note references not allowed in terraform.backend!!
170
+ bucket: `${garden.ctx.pfx}-tf-state`,
171
+ key: `tf`,
172
+ dynamodbTable: `${garden.ctx.pfx}-tf-state`, // Dynamodb table is aws-account-wide
173
+ sharedCredentialsFiles: [tfAwsCredsFile.tfRef()],
174
+ profile: 'default', // References a section within the credentials file
175
+ }
176
+ });
177
+ yield terraform;
178
+ for (const { term } of awsRegions)
179
+ yield new Provider('aws', {
180
+ sharedCredentialsFiles: [tfAwsCredsFile.tfRef()],
181
+ profile: 'default', // References a section within the credentials file
182
+ region: term,
183
+ // Omit the alias for the default provider!
184
+ ...(term !== garden.ctx.aws.region && { alias: term.split('-').join('_') })
185
+ });
186
+ yield* garden.getPetals('real');
187
+ };
188
+ const patioTfHclFact = garden.ctx.patioFact.kid(['main', '.terraform.lock.hcl']);
189
+ const tfHclData = await patioTfHclFact.getData('str');
190
+ if (tfHclData)
191
+ await fact.kid(['.terraform.lock.hcl']).setData(tfHclData);
192
+ for await (const petal of iteratePetals()) {
193
+ const result = await (async () => {
194
+ const result = await petal.getResult();
195
+ if (!isCls(result, Object))
196
+ return { tf: result, files: {} };
197
+ return { files: {}, ...result };
198
+ })();
199
+ if (result.tf)
200
+ await mainWritable.write(`${result.tf}\n`);
201
+ await Promise.all(result.files[toArr]((data, kfp) => fact.kid(kfp.split('/')).setData(data)));
202
+ }
203
+ }
204
+ })
205
+ });
206
+ });
207
+ }
208
+ terraformInit(fact, args) {
209
+ return this.ctx.logger.scope('execTf.init', {}, async (logger) => {
210
+ // Consider if we ever want to pass "-reconfigure" and "-migrate-state" options; these are
211
+ // useful if we are moving backends (e.g. one aws account to another), and want to move our
212
+ // full iac definition too
213
+ // TODO: Some terraform commands fail when offline - can this be covered up?
214
+ const result = await procTerraform(fact, `terraform init -input=false`);
215
+ logger.log({ $$: 'result', logFp: result.logDb.toString(), msg: result.output });
216
+ return result;
217
+ });
218
+ }
219
+ terraformPlan(fact, args) {
220
+ return this.ctx.logger.scope('execTf.plan', {}, async (logger) => {
221
+ const result = await procTerraform(fact, `terraform plan -input=false`);
222
+ logger.log({ $$: 'result', logFp: result.logDb.toString(), msg: result.output });
223
+ return result;
224
+ });
225
+ }
226
+ terraformApply(fact, args) {
227
+ return this.ctx.logger.scope('execTf.apply', {}, async (logger) => {
228
+ const result = await procTerraform(fact, `terraform apply -input=false -auto-approve`);
229
+ logger.log({ $$: 'result', logFp: result.logDb.toString(), msg: result.output });
230
+ return result;
231
+ });
232
+ }
233
+ async grow(mode) {
234
+ // - Avoid tf preventing deletions on populated dbs - e.g. s3 tf needs "force_destroy = true"
235
+ // - Note `terraform init` generates a ".terraform.lock.hcl" file - this should be checked into
236
+ // consumer's source control! May need consumers to provide an optional `repoFact`, and if
237
+ // present after `terraform init` we can copy the ".terraform.lock.hcl" file
238
+ // Note that test mode does not involve any iac - it all runs locally
239
+ if (mode === 'test')
240
+ throw Error('test mode not implemented yet');
241
+ const { bootFact, mainFact } = await this.prepare();
242
+ const tryWithHealing = async (args) => {
243
+ const { fn, heal, canHeal } = args;
244
+ return fn().catch(async (err) => {
245
+ if (!canHeal(err))
246
+ throw err;
247
+ await heal();
248
+ return fn();
249
+ });
250
+ };
251
+ const logicalApply = (args) => {
252
+ return tryWithHealing({
253
+ fn: () => this.terraformApply(args.mainFact),
254
+ canHeal: err => (err.output ?? '')[has]('please run "terraform init"'),
255
+ heal: () => tryWithHealing({
256
+ fn: async () => {
257
+ await this.terraformInit(args.mainFact);
258
+ await this.ctx.patioFact.kid(['main', '.terraform.lock.hcl']).setData(await args.mainFact.kid(['.terraform.lock.hcl']).getData('str'));
259
+ },
260
+ canHeal: err => true,
261
+ heal: () => tryWithHealing({
262
+ fn: () => this.terraformApply(args.bootFact),
263
+ canHeal: err => (err.output ?? '')[has]('please run "terraform init"'),
264
+ heal: () => this.terraformInit(args.bootFact)
265
+ })
266
+ })
267
+ });
268
+ };
269
+ // TODO: HEEERE do `logicalApply` instead!
270
+ await this.terraformInit(bootFact);
271
+ // const result = await logicalApply({ mainFact, bootFact });
272
+ //const result = await execTf.init(bootFact);
273
+ }
274
+ }
275
+ ;
276
+ export * from "./petal/terraform/terraform.js";
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,3 @@
1
+ export default class Petal {
2
+ constructor();
3
+ }
@@ -0,0 +1,3 @@
1
+ export default class Petal {
2
+ constructor() { }
3
+ }
@@ -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,188 @@
1
+ import { getClsName, isCls } from '@gershy/clearing';
2
+ import slashEscape from "../../util/slashEscape.js";
3
+ import snakeCase from "../../util/snakeCase.js";
4
+ import Petal from "../petal.js";
5
+ export var PetalTerraform;
6
+ (function (PetalTerraform) {
7
+ class Base extends Petal {
8
+ // Defines petals using terraform; subclasses generally map to terraform's different block types
9
+ static terraformEncode = (val) => {
10
+ if (val === null)
11
+ return 'null';
12
+ if (isCls(val, String))
13
+ return val[hasHead]('| ') ? val.slice('| '.length) : `"${slashEscape(val, '"\n')}"`;
14
+ if (isCls(val, Number))
15
+ return `${val.toString(10)}`;
16
+ if (isCls(val, Boolean))
17
+ return val ? 'true' : 'false';
18
+ if (isCls(val, Array)) {
19
+ const vals = val[map](v => this.terraformEncode(v));
20
+ if (vals.some(v => isCls(v, String) && v[hasHead]('| ')))
21
+ process.exit(0);
22
+ if (vals.length === 0)
23
+ return '[]';
24
+ if (vals.length === 1)
25
+ return `[ ${vals[0]} ]`;
26
+ return `[\n${vals.join(',\n')[indent](' ')}\n]`;
27
+ }
28
+ if (isCls(val, Object)) {
29
+ // Strings use "| " to avoid any quoting (enabling complex/arbitrary tf)
30
+ // Objects use "$" as a key-prefix to define "nested blocks" instead of "inline maps"
31
+ const keys = Object.keys(val);
32
+ if (keys.length === 1 && keys[0][hasTail]('()')) {
33
+ const vals = val[keys[0]][map](v => this.terraformEncode(v));
34
+ return `${keys[0].slice(0, -'()'.length)}(${vals.join(', ')})`;
35
+ }
36
+ const entryItems = val[toArr]((v, k) => {
37
+ // "special" indicates whether the first char was "$" - it causes objects to be assigned as
38
+ // *nested blocks*
39
+ const [special, pcs] = (() => {
40
+ const ks = k[hasHead]('$');
41
+ const kk = ks ? k.slice(1) : k;
42
+ return [ks, kk.split('.')];
43
+ })();
44
+ const dedup = pcs.length > 1 && /^[0-9]+$/.test(pcs.at(-1));
45
+ if (dedup)
46
+ pcs.pop(); // Remove last item
47
+ // Multi-component keys must pertain to objects
48
+ if (pcs.length > 1 && !isCls(v, Object))
49
+ throw Error('tf key of this form must correspond to object value')[mod]({ k, v });
50
+ // Resolve to raw string?
51
+ if (special && isCls(v, String))
52
+ return [snakeCase(pcs[0]), ' = ', this.terraformEncode(v[hasHead]('| ') ? v : `| ${v}`)];
53
+ // Resolve to nested block?
54
+ if (special && isCls(v, Object))
55
+ return [[snakeCase(pcs[0]), ...pcs.slice(1)[map](pc => snakeCase(pc))].join(' '), ' ', this.terraformEncode(v)];
56
+ // Resolve anything else to typical property - use the key exactly as provided (to support,
57
+ // e.g., aws format for keys in policies, any other specific format, etc.)
58
+ return [snakeCase(pcs[0]), ' = ', this.terraformEncode(v)];
59
+ });
60
+ const len = entryItems.length;
61
+ if (len === 0)
62
+ return '{}';
63
+ const entries = entryItems[map](([key, joiner, value]) => [key, joiner, value].join(''));
64
+ // Note single-line terraform definitions are illegal for non-linear values
65
+ // Determine whether the the value is linear based on its terminating character (janky)
66
+ if (len === 1 && !entries[0][has]('\n') && !'}]'[has](entries[0].at(-1)))
67
+ return `{ ${entries[0]} }`;
68
+ return `{\n` + entries.join('\n')[indent](' ') + '\n}';
69
+ }
70
+ throw Error('unexpected val')[mod]({ form: getClsName(val), val });
71
+ };
72
+ constructor() { super(); }
73
+ getType() { throw Error('not implemented'); }
74
+ getHandle() { throw Error('not implemented'); }
75
+ getProps() { throw Error('not implemented'); }
76
+ tfRef(props = []) {
77
+ if (!isCls(props, Array))
78
+ props = [props];
79
+ const base = `${snakeCase(this.getType())}.${snakeCase(this.getHandle())}`;
80
+ return props.length
81
+ ? `${base}.${props[map](v => snakeCase(v)).join('.')}`
82
+ : base;
83
+ }
84
+ tfRefp(props = []) {
85
+ // "plain ref" - uses "| " to avoid being quoted within terraform
86
+ return `| ${this.tfRef(props)}`;
87
+ }
88
+ async getResultHeader() { throw Error('not implemented'); }
89
+ async getResult() {
90
+ // Get header and props
91
+ const [header, props] = await Promise.all([
92
+ this.getResultHeader(),
93
+ Base.terraformEncode(this.getProps())
94
+ ]);
95
+ return `${header} ${props}`;
96
+ }
97
+ }
98
+ PetalTerraform.Base = Base;
99
+ ;
100
+ class Terraform extends Base {
101
+ props;
102
+ constructor(props) {
103
+ super();
104
+ this.props = props;
105
+ }
106
+ getProps() { return this.props; }
107
+ async getResultHeader() {
108
+ return `terraform`;
109
+ }
110
+ }
111
+ PetalTerraform.Terraform = Terraform;
112
+ ;
113
+ class Resource extends Base {
114
+ type;
115
+ handle;
116
+ props;
117
+ constructor(type, handle, props) {
118
+ super();
119
+ this.type = type;
120
+ this.handle = handle;
121
+ this.props = props;
122
+ }
123
+ getType() { return this.type; }
124
+ getHandle() { return this.handle; }
125
+ getProps() { return this.props; }
126
+ async getResultHeader() {
127
+ return `resource "${snakeCase(this.type)}" "${snakeCase(this.handle)}"`;
128
+ }
129
+ }
130
+ PetalTerraform.Resource = Resource;
131
+ ;
132
+ class Provider extends Base {
133
+ name;
134
+ props;
135
+ constructor(name, props) {
136
+ super();
137
+ this.name = name;
138
+ this.props = props;
139
+ }
140
+ getProps() { return this.props; }
141
+ async getResultHeader() {
142
+ return `provider "${snakeCase(this.name)}"`;
143
+ }
144
+ }
145
+ PetalTerraform.Provider = Provider;
146
+ ;
147
+ class Data extends Base {
148
+ type;
149
+ handle;
150
+ props;
151
+ constructor(type, handle, props) {
152
+ super();
153
+ this.type = type;
154
+ this.handle = handle;
155
+ this.props = props;
156
+ }
157
+ getType() { return this.type; }
158
+ getHandle() { return this.handle; }
159
+ getProps() { return this.props; }
160
+ async getResultHeader() {
161
+ return `data "${snakeCase(this.type)}" "${snakeCase(this.handle)}"`;
162
+ }
163
+ tfRef(props = []) {
164
+ return `data.${super.tfRef(props)}`;
165
+ }
166
+ }
167
+ PetalTerraform.Data = Data;
168
+ ;
169
+ class File extends Base {
170
+ fp;
171
+ content;
172
+ constructor(fp, content) {
173
+ super();
174
+ this.fp = fp;
175
+ this.content = content;
176
+ }
177
+ getType() { return '_file'; }
178
+ async getResult() {
179
+ return { tf: '', files: { [this.fp]: this.content } };
180
+ }
181
+ tfRef(props) {
182
+ return this.fp; // `this.fp` should be quoted but not transformed to a tf handle
183
+ }
184
+ }
185
+ PetalTerraform.File = File;
186
+ ;
187
+ })(PetalTerraform || (PetalTerraform = {}));
188
+ ;
@@ -0,0 +1,5 @@
1
+ export declare const capitalKeys: (v: any) => any;
2
+ export declare const regions: {
3
+ term: string;
4
+ mini: string;
5
+ }[];
@@ -0,0 +1,54 @@
1
+ import { isCls } from '@gershy/clearing';
2
+ import capitalize from "./capitalize.js";
3
+ export const capitalKeys = (v) => {
4
+ if (isCls(v, Array))
5
+ return v[map](v => capitalKeys(v));
6
+ if (isCls(v, Object))
7
+ return v[mapk]((val, key) => [capitalize(key), capitalKeys(val)]);
8
+ return v;
9
+ };
10
+ export const regions = [
11
+ 'ca-central-1',
12
+ 'us-east-1',
13
+ 'us-east-2',
14
+ 'us-west-1',
15
+ 'us-west-2',
16
+ // TODO: What's the exact purpose of this definition? Exhaustive list of regions??
17
+ // 'af-south-1',
18
+ // 'ap-east-1',
19
+ // 'ap-east-2',
20
+ // 'ap-northeast-1',
21
+ // 'ap-northeast-2',
22
+ // 'ap-northeast-3',
23
+ // 'ap-south-1',
24
+ // 'ap-south-2',
25
+ // 'ap-southeast-1',
26
+ // 'ap-southeast-2',
27
+ // 'ap-southeast-3',
28
+ // 'ap-southeast-4',
29
+ // 'ap-southeast-5',
30
+ // 'ap-southeast-7',
31
+ // 'ca-west-1',
32
+ // 'cn-north-1',
33
+ // 'cn-northwest-1',
34
+ // 'eu-central-1',
35
+ // 'eu-central-2',
36
+ // 'eu-north-1',
37
+ // 'eu-south-1',
38
+ // 'eu-south-2',
39
+ // 'eu-west-1',
40
+ // 'eu-west-2',
41
+ // 'eu-west-3',
42
+ // 'il-central-1',
43
+ // 'me-central-1',
44
+ // 'me-south-1',
45
+ // 'mx-central-1',
46
+ // 'sa-east-1',
47
+ // Ignoring these for now...
48
+ // 'us-gov-east-1',
49
+ // 'us-gov-west-1',
50
+ ][map](region => {
51
+ const [c, z, num] = region.split('-');
52
+ const [dir0, dir1 = dir0] = z.match(/central|north|south|east|west/g);
53
+ return { term: region, mini: [c, dir0[0], dir1[0], num].join('') };
54
+ });
@@ -0,0 +1,3 @@
1
+ export declare const provider: (defaultAwsRegion: string, targetAwsRegion: string) => {
2
+ $provider: string;
3
+ };
@@ -0,0 +1,5 @@
1
+ export const provider = (defaultAwsRegion, targetAwsRegion) => {
2
+ return defaultAwsRegion !== targetAwsRegion
3
+ ? { $provider: `aws.${targetAwsRegion.split('-').join('_')}` }
4
+ : {};
5
+ };
@@ -0,0 +1,2 @@
1
+ declare const capitalize: (v: string | string[]) => string;
2
+ export default capitalize;
@@ -0,0 +1,10 @@
1
+ import { isCls } from '@gershy/clearing';
2
+ const capitalize = (v) => {
3
+ // capitalize('hello') -> "Hello"
4
+ // capitalize('heLLo') -> "HeLLo"
5
+ // capitalize([ 'my', 'name', 'is', 'bOb' ]) -> "myNameIsBOb"
6
+ if (isCls(v, String))
7
+ return v[0][upper]() + v.slice(1);
8
+ return [v[0], ...v.slice(1)[map](v => v[0][upper]() + v.slice(1))].join('');
9
+ };
10
+ export default capitalize;
@@ -0,0 +1,2 @@
1
+ declare const _default: (str: string | Buffer | Iterable<string | Buffer>, targetBase?: string) => string;
2
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { isCls } from '@gershy/clearing';
3
+ export default (str, targetBase = String[base62]) => {
4
+ // Consider moving this under the "node" directory - relies on "node:crypto'
5
+ if (isCls(str, String) || isCls(str, Buffer))
6
+ str = [str];
7
+ const hash = createHash('sha256');
8
+ for (const val of str)
9
+ hash.update(val);
10
+ return hash
11
+ .digest('base64url')[toNum](String[base64Url])[toStr](targetBase);
12
+ };