@gershy/lilac 0.0.11 → 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.
package/cmp/mjs/main.js CHANGED
@@ -7,11 +7,19 @@
7
7
  // Can region be dealt with any better??
8
8
  // Support test-mode (Flowers need to be able to do setup, share config, write to volumes, etc)
9
9
  import { PetalTerraform } from "./petal/terraform/terraform.js";
10
- import { regions as awsRegions } from "./util/aws.js";
11
10
  import { isCls, skip } from '@gershy/clearing';
12
11
  import procTerraform from "./util/procTerraform.js";
13
- const { File, Provider, Terraform } = PetalTerraform;
12
+ import tryWithHealing from '@gershy/util-try-with-healing';
13
+ import phrasing from '@gershy/util-phrasing';
14
14
  export class Flower {
15
+ // TODO: The downside of having this static is that different instances may use different
16
+ // services - e.g. api gateway instance may have "useEdge: true", in which case we'd like to
17
+ // include cloudfront and omit it otherwise... but having it on the instance is annoying since
18
+ // we want to enumerate all services *before* instantiating any Flowers... probably better this
19
+ // way? And heirarchical design can probably avoid most unecessary service inclusion...
20
+ // TODO: The naming of these services is coupled to LocalStack - consider using Lilac-scoped
21
+ // naming, and add a translation layer from Lilac->LocalStack in Soil.LocalStack?
22
+ static getAwsServices() { return []; }
15
23
  constructor() { }
16
24
  *getDependencies() {
17
25
  yield this;
@@ -22,10 +30,21 @@ export class Flower {
22
30
  }
23
31
  ;
24
32
  export class Registry {
33
+ // Note that maintaining a duality of classes for each Flower (one for testing, one for remote
34
+ // deploy) is essential to keep test functionality out of deployed code bundles. If a single
35
+ // class supported both test and prod functionality, these two pieces of functionality would
36
+ // always be bundled together.
25
37
  flowers;
26
38
  constructor(flowers) {
27
39
  this.flowers = {}[merge](flowers);
28
40
  }
41
+ getAwsServices() {
42
+ const services = new Set();
43
+ for (const [k, { real }] of this.flowers)
44
+ for (const awsService of real.getAwsServices())
45
+ services.add(awsService);
46
+ return services[toArr](v => v);
47
+ }
29
48
  add(flowers) {
30
49
  return new Registry({ ...this.flowers, ...flowers });
31
50
  }
@@ -37,23 +56,28 @@ export class Registry {
37
56
  export class Garden {
38
57
  // Note this class currently is coupled to terraform logic
39
58
  ctx;
40
- registry;
41
- define;
59
+ reg;
60
+ def;
42
61
  constructor(args) {
43
- const { define, registry, ...ctx } = args;
44
- this.ctx = ctx;
45
- this.registry = registry;
46
- this.define = define;
62
+ const { define, registry, context } = args;
63
+ this.ctx = context;
64
+ this.reg = registry;
65
+ this.def = define;
47
66
  }
48
- async *getPetals(mode) {
67
+ async *getPetals() {
68
+ // TODO: We always use the "real" flowers from the registry - this is part of the shift to
69
+ // localStack; we always generate genuine terraform and apply it to the docker localStack.
70
+ // Eventually may want to support ultra-lightweight dockerless/localStackless js flower mocks;
71
+ // that would be the time to add "fake" flowers alongside each real flower, and start
72
+ // conditionally calling `this.registry.get('fake')`...
49
73
  const seenFlowers = new Set();
50
74
  const seenPetals = new Set();
51
- for await (const topLevelFlower of this.define(this.ctx, this.registry.get(mode))) {
75
+ for await (const topLevelFlower of await this.def(this.ctx, this.reg.get('real'))) {
52
76
  for (const flower of topLevelFlower.getDependencies()) {
53
77
  if (seenFlowers.has(flower))
54
78
  continue;
55
79
  seenFlowers.add(flower);
56
- for await (const petal of flower.getPetals(this.ctx)) {
80
+ for await (const petal of await flower.getPetals(this.ctx)) {
57
81
  if (seenPetals.has(petal))
58
82
  continue;
59
83
  yield petal;
@@ -61,8 +85,9 @@ export class Garden {
61
85
  }
62
86
  }
63
87
  }
64
- async prepare( /* Note this only pertains to "real" mode */) {
65
- return this.ctx.logger.scope('garden.prepare', {}, async (logger) => {
88
+ async genTerraform(deployTarget) {
89
+ const soilTfPetalsPrm = deployTarget.getTerraformPetals(this.ctx);
90
+ return this.ctx.logger.scope('garden.genTerraform', {}, async (logger) => {
66
91
  const setupTfProj = async (args) => args.logger.scope('tf', { proj: this.ctx.name, tf: args.term }, async (logger) => {
67
92
  // Allows a terraform project to be defined in terms of a function which writes to main.tf,
68
93
  // and adds any arbitrary additional files to the terraform project
@@ -81,11 +106,22 @@ export class Garden {
81
106
  // Write new terraform
82
107
  await logger.scope('files.generate', {}, async (logger) => {
83
108
  const stream = await args.fact.kid(['main.tf']).getDataHeadStream();
84
- await args.setup(args.fact, stream);
109
+ await args.setup(args.fact, stream, async (petal) => {
110
+ // Include a utility function the caller can use to easily write petals
111
+ const { tf, files = {} } = await petal.getResult()
112
+ .then(tf => isCls(tf, String) ? { tf } : tf);
113
+ if (tf)
114
+ await stream.write(`${tf}\n`);
115
+ await Promise.all(files[toArr]((data, kfp) => args.fact.kid(kfp.split('/')).setData(data)));
116
+ return petal;
117
+ });
85
118
  await stream.end(); // TODO: @gershy/disk should allow `await headStream.end()`
86
119
  });
87
120
  return args.fact;
88
121
  });
122
+ // Pick names for the s3 and ddb terraform state persistence entities
123
+ const s3Name = `${this.ctx.pfx}-tf-state`;
124
+ const ddbName = `${this.ctx.pfx}-tf-state`;
89
125
  // We generate *two* terraform projects for every logical project - overall we want a
90
126
  // terraform project which saves its state in the cloud; in order to do this we need to first
91
127
  // provision the cloud storage engines to save the terraform state. The "boot" tf project
@@ -95,181 +131,106 @@ export class Garden {
95
131
  term: 'boot',
96
132
  logger,
97
133
  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
- `));
134
+ setup: async (fact, mainWritable, writePetalTfAndFiles) => {
135
+ // Include the soil's infrastructure
136
+ const { boot } = await soilTfPetalsPrm;
137
+ for await (const petal of await boot({ s3Name, ddbName }))
138
+ await writePetalTfAndFiles(petal);
139
+ // Create s3 tf state bucket
140
+ const s3 = await writePetalTfAndFiles(new PetalTerraform.Resource('awsS3Bucket', 'tfState', {
141
+ bucket: s3Name
142
+ }));
143
+ const s3Controls = await writePetalTfAndFiles(new PetalTerraform.Resource('awsS3BucketOwnershipControls', 'tfState', {
144
+ bucket: s3.ref('bucket'),
145
+ $rule: {
146
+ objectOwnership: 'ObjectWriter'
147
+ }
148
+ }));
149
+ await writePetalTfAndFiles(new PetalTerraform.Resource('awsS3BucketAcl', 'tfState', {
150
+ bucket: s3.ref('bucket'),
151
+ acl: 'private',
152
+ dependsOn: [s3Controls.ref()]
153
+ }));
154
+ // Create ddb tf state locking table
155
+ await writePetalTfAndFiles(new PetalTerraform.Resource('awsDynamodbTable', 'tfState', {
156
+ name: ddbName,
157
+ billingMode: phrasing('payPerRequest', 'camel', 'snake')[upper](),
158
+ hashKey: 'LockID',
159
+ $attribute: { name: 'LockID', type: 'S' }
160
+ }));
143
161
  }
144
162
  }),
145
163
  mainFact: setupTfProj({
146
164
  term: 'main',
147
165
  logger,
148
166
  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']);
167
+ setup: async (fact, mainWritable, writePetalTfAndFiles) => {
168
+ // Include the soil's infrastructure
169
+ const { main } = await soilTfPetalsPrm;
170
+ for await (const petal of await main({ s3Name, ddbName }))
171
+ await writePetalTfAndFiles(petal);
172
+ for await (const petal of this.getPetals())
173
+ await writePetalTfAndFiles(petal);
174
+ // Propagate any terraform lock found in version control
175
+ const patioTfHclFact = this.ctx.patioFact.kid(['main', '.terraform.lock.hcl']);
189
176
  const tfHclData = await patioTfHclFact.getData('str');
190
177
  if (tfHclData)
191
178
  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
179
  }
204
- })
180
+ }),
205
181
  });
206
182
  });
207
183
  }
208
184
  terraformInit(fact, args) {
209
- return this.ctx.logger.scope('execTf.init', {}, async (logger) => {
185
+ return this.ctx.logger.scope('execTf.init', { fact: fact.fsp() }, async (logger) => {
210
186
  // Consider if we ever want to pass "-reconfigure" and "-migrate-state" options; these are
211
187
  // useful if we are moving backends (e.g. one aws account to another), and want to move our
212
188
  // 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`);
189
+ // TODO: Some terraform commands fail when offline - can this be covered up? Possibly by
190
+ // checking terraform binaries into the repo? (Cross-platform nightmare though...)
191
+ const result = await procTerraform(fact, `terraform init -input=false`, {
192
+ onData: async (mode, data) => logger.log({ $$: 'notice', mode, data }) ?? null
193
+ });
215
194
  logger.log({ $$: 'result', logFp: result.logDb.toString(), msg: result.output });
216
195
  return result;
217
196
  });
218
197
  }
219
198
  terraformPlan(fact, args) {
220
- return this.ctx.logger.scope('execTf.plan', {}, async (logger) => {
199
+ return this.ctx.logger.scope('execTf.plan', { fact: fact.fsp() }, async (logger) => {
221
200
  const result = await procTerraform(fact, `terraform plan -input=false`);
222
201
  logger.log({ $$: 'result', logFp: result.logDb.toString(), msg: result.output });
223
202
  return result;
224
203
  });
225
204
  }
226
205
  terraformApply(fact, args) {
227
- return this.ctx.logger.scope('execTf.apply', {}, async (logger) => {
206
+ return this.ctx.logger.scope('execTf.apply', { fact: fact.fsp() }, async (logger) => {
228
207
  const result = await procTerraform(fact, `terraform apply -input=false -auto-approve`);
229
208
  logger.log({ $$: 'result', logFp: result.logDb.toString(), msg: result.output });
230
209
  return result;
231
210
  });
232
211
  }
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"'),
212
+ async grow(deploy) {
213
+ if (deploy.type === 'test')
214
+ throw Error('not implemented')[mod]({ type: 'test' }); // TODO: Can be nice to have local service mocks!
215
+ const { bootFact, mainFact } = await this.genTerraform(deploy.soil);
216
+ // Init+apply both "boot" and "main", in optimistic fashion
217
+ const isHealableTerraformApply = err => /run[^a-zA-Z0-9]+terraform init/.test(err.output ?? '');
218
+ await tryWithHealing({
219
+ fn: () => this.terraformApply(mainFact),
220
+ canHeal: isHealableTerraformApply,
221
+ heal: () => tryWithHealing({
222
+ fn: async () => {
223
+ await this.terraformInit(mainFact);
224
+ await this.ctx.patioFact.kid(['main', '.terraform.lock.hcl']).setData(await mainFact.kid(['.terraform.lock.hcl']).getData('str'));
225
+ },
226
+ canHeal: err => true,
255
227
  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
- })
228
+ fn: () => this.terraformApply(bootFact),
229
+ canHeal: isHealableTerraformApply,
230
+ heal: () => this.terraformInit(bootFact)
266
231
  })
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);
232
+ })
233
+ });
273
234
  }
274
235
  }
275
236
  ;
@@ -8,8 +8,8 @@ export declare namespace PetalTerraform {
8
8
  getProps(): {
9
9
  [key: string]: Json;
10
10
  };
11
- tfRef(props?: string | string[]): string;
12
- tfRefp(props?: string | string[]): `| ${string}`;
11
+ refStr(props?: string | string[]): string;
12
+ ref(props?: string | string[]): `| ${string}`;
13
13
  getResultHeader(): Promise<string>;
14
14
  getResult(): Promise<string | {
15
15
  tf: string;
@@ -64,7 +64,7 @@ export declare namespace PetalTerraform {
64
64
  [key: string]: Json;
65
65
  };
66
66
  getResultHeader(): Promise<string>;
67
- tfRef(props?: string | string[]): string;
67
+ refStr(props?: string | string[]): string;
68
68
  }
69
69
  class File extends Base {
70
70
  private fp;
@@ -77,6 +77,6 @@ export declare namespace PetalTerraform {
77
77
  [x: string]: string | Buffer<ArrayBufferLike>;
78
78
  };
79
79
  }>;
80
- tfRef(props?: string | string[]): string;
80
+ refStr(props?: string | string[]): string;
81
81
  }
82
82
  }
@@ -73,7 +73,7 @@ export var PetalTerraform;
73
73
  getType() { throw Error('not implemented'); }
74
74
  getHandle() { throw Error('not implemented'); }
75
75
  getProps() { throw Error('not implemented'); }
76
- tfRef(props = []) {
76
+ refStr(props = []) {
77
77
  if (!isCls(props, Array))
78
78
  props = [props];
79
79
  const base = `${ph(this.getType(), 'camel', 'snake')}.${ph(this.getHandle(), 'camel', 'snake')}`;
@@ -81,9 +81,9 @@ export var PetalTerraform;
81
81
  ? `${base}.${props[map](v => ph(v, 'camel', 'snake')).join('.')}`
82
82
  : base;
83
83
  }
84
- tfRefp(props = []) {
84
+ ref(props = []) {
85
85
  // "plain ref" - uses "| " to avoid being quoted within terraform
86
- return `| ${this.tfRef(props)}`;
86
+ return `| ${this.refStr(props)}`;
87
87
  }
88
88
  async getResultHeader() { throw Error('not implemented'); }
89
89
  async getResult() {
@@ -160,8 +160,8 @@ export var PetalTerraform;
160
160
  async getResultHeader() {
161
161
  return `data "${ph(this.type, 'camel', 'snake')}" "${ph(this.handle, 'camel', 'snake')}"`;
162
162
  }
163
- tfRef(props = []) {
164
- return `data.${super.tfRef(props)}`;
163
+ refStr(props = []) {
164
+ return `data.${super.refStr(props)}`;
165
165
  }
166
166
  }
167
167
  PetalTerraform.Data = Data;
@@ -178,7 +178,7 @@ export var PetalTerraform;
178
178
  async getResult() {
179
179
  return { tf: '', files: { [this.fp]: this.content } };
180
180
  }
181
- tfRef(props) {
181
+ refStr(props) {
182
182
  return this.fp; // `this.fp` should be quoted but not transformed to a tf handle
183
183
  }
184
184
  }
@@ -0,0 +1,79 @@
1
+ import { Context, PetalTerraform, Registry } from '../main.ts';
2
+ import { RegionTerm } from '../util/aws.ts';
3
+ import { NetProc } from '@gershy/util-http';
4
+ import { SuperIterable } from '../util/superIterable.ts';
5
+ import Logger from '@gershy/logger';
6
+ export declare namespace Soil {
7
+ type PetalProjArgs = {
8
+ s3Name: string;
9
+ ddbName: string;
10
+ };
11
+ type PetalProjResult = {
12
+ [K in 'boot' | 'main']: (args: PetalProjArgs) => SuperIterable<PetalTerraform.Base>;
13
+ };
14
+ 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
+ type BaseArgs = {
16
+ registry: Registry<any>;
17
+ };
18
+ class Base {
19
+ protected registry: Registry<any>;
20
+ constructor(args: BaseArgs);
21
+ getTerraformPetals(ctx: Context): Promise<PetalProjResult>;
22
+ }
23
+ type LocalStackArgs = BaseArgs & {
24
+ aws: {
25
+ region: RegionTerm;
26
+ };
27
+ localStackDocker?: {
28
+ image?: `localstack/localstack${':' | ':latest' | '@'}${string}`;
29
+ containerName?: string;
30
+ port?: number;
31
+ };
32
+ };
33
+ class LocalStack extends Base {
34
+ private static localStackInternalPort;
35
+ private aws;
36
+ private localStackDocker;
37
+ private procArgs;
38
+ constructor(args: LocalStackArgs);
39
+ private getAwsServices;
40
+ private getDockerContainers;
41
+ run(args: {
42
+ logger: Logger;
43
+ }): Promise<{
44
+ aws: {
45
+ services: LocalStackAwsService[];
46
+ region: "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
47
+ };
48
+ netProc: NetProc;
49
+ url: string;
50
+ }>;
51
+ end(args?: {
52
+ containers?: Awaited<ReturnType<Soil.LocalStack['getDockerContainers']>>;
53
+ }): Promise<{
54
+ name: string;
55
+ state: "created" | "running" | "paused" | "restarting" | "removing" | "exited" | "dead";
56
+ }[]>;
57
+ getTerraformPetals(ctx: Context): Promise<{
58
+ boot: () => (PetalTerraform.Terraform | PetalTerraform.Provider)[];
59
+ main: (args: any) => Generator<PetalTerraform.Terraform | PetalTerraform.Provider, void, unknown>;
60
+ }>;
61
+ }
62
+ type AwsCloudArgs = BaseArgs & {
63
+ aws: {
64
+ region: RegionTerm;
65
+ accessKey: {
66
+ id: string;
67
+ '!secret': string;
68
+ };
69
+ };
70
+ };
71
+ class AwsCloud extends Base {
72
+ private aws;
73
+ constructor(args: AwsCloudArgs);
74
+ getTerraformPetals(ctx: Context): Promise<{
75
+ boot: () => Generator<PetalTerraform.Terraform | PetalTerraform.Provider | PetalTerraform.File, void, unknown>;
76
+ main: (args: any) => Generator<PetalTerraform.Terraform | PetalTerraform.Provider | PetalTerraform.File, void, unknown>;
77
+ }>;
78
+ }
79
+ }