@quadnix/octo 0.0.30 → 0.1.0
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/LICENSE +661 -0
- package/README.md +25 -3
- package/dist/app.type.d.ts +143 -4
- package/dist/app.type.js +80 -9
- package/dist/app.type.js.map +1 -1
- package/dist/decorators/action.decorator.d.ts +10 -6
- package/dist/decorators/action.decorator.js +19 -6
- package/dist/decorators/action.decorator.js.map +1 -1
- package/dist/decorators/anchor.decorator.d.ts +12 -5
- package/dist/decorators/anchor.decorator.js +12 -5
- package/dist/decorators/anchor.decorator.js.map +1 -1
- package/dist/decorators/enable-hook.decorator.d.ts +1 -1
- package/dist/decorators/enable-hook.decorator.js +1 -1
- package/dist/decorators/factory.decorator.d.ts +18 -34
- package/dist/decorators/factory.decorator.js +18 -34
- package/dist/decorators/factory.decorator.js.map +1 -1
- package/dist/decorators/model.decorator.d.ts +1 -1
- package/dist/decorators/model.decorator.js +1 -1
- package/dist/decorators/module.decorator.d.ts +16 -10
- package/dist/decorators/module.decorator.js +16 -10
- package/dist/decorators/module.decorator.js.map +1 -1
- package/dist/decorators/on-event.decorator.d.ts +5 -0
- package/dist/decorators/on-event.decorator.js +5 -0
- package/dist/decorators/on-event.decorator.js.map +1 -1
- package/dist/decorators/validate.decorator.d.ts +15 -1
- package/dist/decorators/validate.decorator.js +15 -1
- package/dist/decorators/validate.decorator.js.map +1 -1
- package/dist/errors/container.error.d.ts +18 -0
- package/dist/errors/container.error.js +18 -0
- package/dist/errors/container.error.js.map +1 -1
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/errors/index.js.map +1 -1
- package/dist/errors/module.error.d.ts +6 -0
- package/dist/errors/module.error.js +6 -0
- package/dist/errors/module.error.js.map +1 -1
- package/dist/errors/node.error.d.ts +56 -0
- package/dist/errors/node.error.js +56 -0
- package/dist/errors/node.error.js.map +1 -1
- package/dist/errors/schema.error.d.ts +6 -0
- package/dist/errors/schema.error.js +6 -0
- package/dist/errors/schema.error.js.map +1 -1
- package/dist/errors/terraform.error.d.ts +19 -0
- package/dist/errors/terraform.error.js +23 -0
- package/dist/errors/terraform.error.js.map +1 -0
- package/dist/errors/transaction.error.d.ts +61 -0
- package/dist/errors/transaction.error.js +61 -0
- package/dist/errors/transaction.error.js.map +1 -1
- package/dist/events/action.event.d.ts +0 -2
- package/dist/events/action.event.js +0 -2
- package/dist/events/action.event.js.map +1 -1
- package/dist/events/event.model.d.ts +21 -5
- package/dist/events/event.model.js +11 -5
- package/dist/events/event.model.js.map +1 -1
- package/dist/events/hook/commit.hook.event.d.ts +0 -6
- package/dist/events/hook/commit.hook.event.js +0 -6
- package/dist/events/hook/commit.hook.event.js.map +1 -1
- package/dist/events/hook/hook.event.d.ts +0 -2
- package/dist/events/hook/hook.event.js +0 -2
- package/dist/events/hook/hook.event.js.map +1 -1
- package/dist/events/hook/model.hook.event.d.ts +0 -6
- package/dist/events/hook/model.hook.event.js +0 -6
- package/dist/events/hook/model.hook.event.js.map +1 -1
- package/dist/events/hook/resource.hook.event.d.ts +0 -6
- package/dist/events/hook/resource.hook.event.js +0 -6
- package/dist/events/hook/resource.hook.event.js.map +1 -1
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +3 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/mode-result.event.d.ts +57 -0
- package/dist/events/mode-result.event.js +24 -0
- package/dist/events/mode-result.event.js.map +1 -0
- package/dist/events/mode.event.d.ts +57 -0
- package/dist/events/mode.event.js +32 -0
- package/dist/events/mode.event.js.map +1 -0
- package/dist/events/module.event.d.ts +0 -2
- package/dist/events/module.event.js +0 -2
- package/dist/events/module.event.js.map +1 -1
- package/dist/events/registration.event.d.ts +18 -14
- package/dist/events/registration.event.js +18 -14
- package/dist/events/registration.event.js.map +1 -1
- package/dist/events/serialization.event.d.ts +15 -12
- package/dist/events/serialization.event.js +15 -12
- package/dist/events/serialization.event.js.map +1 -1
- package/dist/events/terraform.event.d.ts +42 -0
- package/dist/events/terraform.event.js +31 -0
- package/dist/events/terraform.event.js.map +1 -0
- package/dist/events/transaction.event.d.ts +0 -37
- package/dist/events/transaction.event.js +0 -28
- package/dist/events/transaction.event.js.map +1 -1
- package/dist/functions/container/container.d.ts +54 -13
- package/dist/functions/container/container.js +63 -18
- package/dist/functions/container/container.js.map +1 -1
- package/dist/functions/container/test-container.d.ts +43 -29
- package/dist/functions/container/test-container.js +71 -43
- package/dist/functions/container/test-container.js.map +1 -1
- package/dist/functions/dependency/dependency.d.ts +82 -8
- package/dist/functions/dependency/dependency.js +80 -14
- package/dist/functions/dependency/dependency.js.map +1 -1
- package/dist/functions/diff/aggregate-resource-actions.d.ts +19 -0
- package/dist/functions/diff/aggregate-resource-actions.js +41 -0
- package/dist/functions/diff/aggregate-resource-actions.js.map +1 -0
- package/dist/functions/diff/diff-metadata.d.ts +21 -3
- package/dist/functions/diff/diff-metadata.js +18 -0
- package/dist/functions/diff/diff-metadata.js.map +1 -1
- package/dist/functions/diff/diff.d.ts +55 -1
- package/dist/functions/diff/diff.js +44 -3
- package/dist/functions/diff/diff.js.map +1 -1
- package/dist/functions/diff/diff.utility.d.ts +17 -2
- package/dist/functions/diff/diff.utility.js +53 -38
- package/dist/functions/diff/diff.utility.js.map +1 -1
- package/dist/functions/hook/commit.hook.d.ts +16 -1
- package/dist/functions/hook/commit.hook.js +21 -6
- package/dist/functions/hook/commit.hook.js.map +1 -1
- package/dist/functions/hook/model-action.hook.d.ts +16 -1
- package/dist/functions/hook/model-action.hook.js +21 -6
- package/dist/functions/hook/model-action.hook.js.map +1 -1
- package/dist/functions/hook/resource-action.hook.d.ts +16 -1
- package/dist/functions/hook/resource-action.hook.js +21 -6
- package/dist/functions/hook/resource-action.hook.js.map +1 -1
- package/dist/functions/node/node-action.abstract.d.ts +23 -0
- package/dist/functions/node/node-action.abstract.js +20 -0
- package/dist/functions/node/node-action.abstract.js.map +1 -1
- package/dist/functions/node/node.abstract.d.ts +56 -5
- package/dist/functions/node/node.abstract.js +85 -12
- package/dist/functions/node/node.abstract.js.map +1 -1
- package/dist/functions/node/node.interface.d.ts +1 -0
- package/dist/functions/schema/schema.d.ts +7 -0
- package/dist/functions/schema/schema.js +9 -0
- package/dist/functions/schema/schema.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +244 -14
- package/dist/main.js +361 -72
- package/dist/main.js.map +1 -1
- package/dist/models/account/account.model.d.ts +37 -7
- package/dist/models/account/account.model.js +40 -8
- package/dist/models/account/account.model.js.map +1 -1
- package/dist/models/account/account.schema.d.ts +13 -1
- package/dist/models/account/account.schema.js +12 -0
- package/dist/models/account/account.schema.js.map +1 -1
- package/dist/models/app/app.model.d.ts +16 -1
- package/dist/models/app/app.model.js +19 -4
- package/dist/models/app/app.model.js.map +1 -1
- package/dist/models/app/app.schema.d.ts +6 -0
- package/dist/models/app/app.schema.js +6 -0
- package/dist/models/app/app.schema.js.map +1 -1
- package/dist/models/deployment/deployment.model.d.ts +13 -1
- package/dist/models/deployment/deployment.model.js +16 -4
- package/dist/models/deployment/deployment.model.js.map +1 -1
- package/dist/models/deployment/deployment.schema.d.ts +2 -0
- package/dist/models/deployment/deployment.schema.js +2 -0
- package/dist/models/deployment/deployment.schema.js.map +1 -1
- package/dist/models/environment/environment.model.d.ts +13 -1
- package/dist/models/environment/environment.model.js +20 -8
- package/dist/models/environment/environment.model.js.map +1 -1
- package/dist/models/environment/environment.schema.d.ts +2 -0
- package/dist/models/environment/environment.schema.js +2 -0
- package/dist/models/environment/environment.schema.js.map +1 -1
- package/dist/models/execution/execution.model.d.ts +20 -2
- package/dist/models/execution/execution.model.js +46 -28
- package/dist/models/execution/execution.model.js.map +1 -1
- package/dist/models/execution/execution.schema.d.ts +11 -0
- package/dist/models/execution/execution.schema.js +11 -0
- package/dist/models/execution/execution.schema.js.map +1 -1
- package/dist/models/filesystem/filesystem.model.d.ts +27 -1
- package/dist/models/filesystem/filesystem.model.js +30 -4
- package/dist/models/filesystem/filesystem.model.js.map +1 -1
- package/dist/models/filesystem/filesystem.schema.d.ts +5 -0
- package/dist/models/filesystem/filesystem.schema.js +5 -0
- package/dist/models/filesystem/filesystem.schema.js.map +1 -1
- package/dist/models/image/image.model.d.ts +17 -4
- package/dist/models/image/image.model.js +20 -7
- package/dist/models/image/image.model.js.map +1 -1
- package/dist/models/image/image.schema.d.ts +8 -0
- package/dist/models/image/image.schema.js +8 -0
- package/dist/models/image/image.schema.js.map +1 -1
- package/dist/models/model-action.interface.d.ts +17 -4
- package/dist/models/model.abstract.d.ts +96 -1
- package/dist/models/model.abstract.js +106 -7
- package/dist/models/model.abstract.js.map +1 -1
- package/dist/models/model.interface.d.ts +3 -0
- package/dist/models/pipeline/pipeline.model.d.ts +13 -1
- package/dist/models/pipeline/pipeline.model.js +18 -6
- package/dist/models/pipeline/pipeline.model.js.map +1 -1
- package/dist/models/pipeline/pipeline.schema.d.ts +8 -0
- package/dist/models/pipeline/pipeline.schema.js +8 -0
- package/dist/models/pipeline/pipeline.schema.js.map +1 -1
- package/dist/models/region/region.model.d.ts +16 -1
- package/dist/models/region/region.model.js +19 -4
- package/dist/models/region/region.model.js.map +1 -1
- package/dist/models/region/region.schema.d.ts +6 -0
- package/dist/models/region/region.schema.js +6 -0
- package/dist/models/region/region.schema.js.map +1 -1
- package/dist/models/server/server.model.d.ts +13 -1
- package/dist/models/server/server.model.js +16 -4
- package/dist/models/server/server.model.js.map +1 -1
- package/dist/models/server/server.schema.d.ts +3 -1
- package/dist/models/server/server.schema.js +3 -1
- package/dist/models/server/server.schema.js.map +1 -1
- package/dist/models/service/service.model.d.ts +15 -1
- package/dist/models/service/service.model.js +15 -1
- package/dist/models/service/service.model.js.map +1 -1
- package/dist/models/service/service.schema.d.ts +4 -1
- package/dist/models/service/service.schema.js +4 -1
- package/dist/models/service/service.schema.js.map +1 -1
- package/dist/models/subnet/subnet.model.d.ts +27 -3
- package/dist/models/subnet/subnet.model.js +40 -16
- package/dist/models/subnet/subnet.model.js.map +1 -1
- package/dist/models/subnet/subnet.schema.d.ts +9 -0
- package/dist/models/subnet/subnet.schema.js +9 -0
- package/dist/models/subnet/subnet.schema.js.map +1 -1
- package/dist/modes/apply-transaction.d.ts +23 -0
- package/dist/modes/apply-transaction.js +25 -0
- package/dist/modes/apply-transaction.js.map +1 -0
- package/dist/modes/commit.mode.d.ts +54 -0
- package/dist/modes/commit.mode.js +235 -0
- package/dist/modes/commit.mode.js.map +1 -0
- package/dist/modes/generate.mode.d.ts +29 -0
- package/dist/modes/generate.mode.js +49 -0
- package/dist/modes/generate.mode.js.map +1 -0
- package/dist/modes/run-action.mode.d.ts +30 -0
- package/dist/modes/run-action.mode.js +140 -0
- package/dist/modes/run-action.mode.js.map +1 -0
- package/dist/modes/validate.mode.d.ts +162 -0
- package/dist/modes/validate.mode.js +480 -0
- package/dist/modes/validate.mode.js.map +1 -0
- package/dist/modules/module.abstract.d.ts +51 -0
- package/dist/modules/module.abstract.js +36 -0
- package/dist/modules/module.abstract.js.map +1 -1
- package/dist/modules/module.container.d.ts +36 -0
- package/dist/modules/module.container.js +36 -0
- package/dist/modules/module.container.js.map +1 -1
- package/dist/modules/test-module.container.d.ts +139 -25
- package/dist/modules/test-module.container.js +410 -178
- package/dist/modules/test-module.container.js.map +1 -1
- package/dist/overlays/anchor.abstract.d.ts +32 -1
- package/dist/overlays/anchor.abstract.js +42 -11
- package/dist/overlays/anchor.abstract.js.map +1 -1
- package/dist/overlays/anchor.schema.d.ts +21 -0
- package/dist/overlays/anchor.schema.js +21 -0
- package/dist/overlays/anchor.schema.js.map +1 -1
- package/dist/overlays/overlay-data.repository.d.ts +18 -0
- package/dist/overlays/overlay-data.repository.js +18 -0
- package/dist/overlays/overlay-data.repository.js.map +1 -1
- package/dist/overlays/overlay.abstract.d.ts +56 -1
- package/dist/overlays/overlay.abstract.js +79 -20
- package/dist/overlays/overlay.abstract.js.map +1 -1
- package/dist/overlays/overlay.schema.d.ts +20 -0
- package/dist/overlays/overlay.schema.js +20 -0
- package/dist/overlays/overlay.schema.js.map +1 -1
- package/dist/resources/resource-action.interface.d.ts +26 -2
- package/dist/resources/resource-data.repository.d.ts +63 -1
- package/dist/resources/resource-data.repository.js +94 -4
- package/dist/resources/resource-data.repository.js.map +1 -1
- package/dist/resources/resource.abstract.d.ts +118 -3
- package/dist/resources/resource.abstract.js +166 -33
- package/dist/resources/resource.abstract.js.map +1 -1
- package/dist/resources/resource.schema.d.ts +29 -1
- package/dist/resources/resource.schema.js +28 -0
- package/dist/resources/resource.schema.js.map +1 -1
- package/dist/resources/terraform-resource.abstract.d.ts +44 -0
- package/dist/resources/terraform-resource.abstract.js +45 -0
- package/dist/resources/terraform-resource.abstract.js.map +1 -0
- package/dist/services/event/event.service.d.ts +30 -3
- package/dist/services/event/event.service.js +58 -16
- package/dist/services/event/event.service.js.map +1 -1
- package/dist/services/input/input.service.d.ts +52 -3
- package/dist/services/input/input.service.js +72 -8
- package/dist/services/input/input.service.js.map +1 -1
- package/dist/services/schema-translation/schema-translation.service.d.ts +9 -0
- package/dist/services/schema-translation/schema-translation.service.js +9 -0
- package/dist/services/schema-translation/schema-translation.service.js.map +1 -1
- package/dist/services/serialization/model/model-serialization.service.d.ts +26 -2
- package/dist/services/serialization/model/model-serialization.service.js +110 -62
- package/dist/services/serialization/model/model-serialization.service.js.map +1 -1
- package/dist/services/serialization/resource/resource-serialization.service.d.ts +33 -4
- package/dist/services/serialization/resource/resource-serialization.service.js +64 -28
- package/dist/services/serialization/resource/resource-serialization.service.js.map +1 -1
- package/dist/services/state-management/file-app-lock.utility.d.ts +43 -0
- package/dist/services/state-management/file-app-lock.utility.js +124 -0
- package/dist/services/state-management/file-app-lock.utility.js.map +1 -0
- package/dist/services/state-management/local-encryption.state-provider.d.ts +53 -2
- package/dist/services/state-management/local-encryption.state-provider.js +75 -49
- package/dist/services/state-management/local-encryption.state-provider.js.map +1 -1
- package/dist/services/state-management/local.state-provider.d.ts +49 -2
- package/dist/services/state-management/local.state-provider.js +70 -49
- package/dist/services/state-management/local.state-provider.js.map +1 -1
- package/dist/services/state-management/state-management.service.d.ts +141 -6
- package/dist/services/state-management/state-management.service.js +149 -4
- package/dist/services/state-management/state-management.service.js.map +1 -1
- package/dist/services/state-management/state-provider.interface.d.ts +55 -1
- package/dist/services/state-management/terraform-state.service.d.ts +76 -0
- package/dist/services/state-management/terraform-state.service.js +154 -0
- package/dist/services/state-management/terraform-state.service.js.map +1 -0
- package/dist/services/state-management/test.state-provider.d.ts +38 -1
- package/dist/services/state-management/test.state-provider.js +52 -21
- package/dist/services/state-management/test.state-provider.js.map +1 -1
- package/dist/services/terraform/terraform.service.d.ts +426 -0
- package/dist/services/terraform/terraform.service.js +995 -0
- package/dist/services/terraform/terraform.service.js.map +1 -0
- package/dist/services/terraform/terraform.type.d.ts +387 -0
- package/dist/services/terraform/terraform.type.js +579 -0
- package/dist/services/terraform/terraform.type.js.map +1 -0
- package/dist/services/transaction/transaction.service.d.ts +86 -10
- package/dist/services/transaction/transaction.service.js +387 -232
- package/dist/services/transaction/transaction.service.js.map +1 -1
- package/dist/services/validation/validation.service.d.ts +13 -1
- package/dist/services/validation/validation.service.js +12 -0
- package/dist/services/validation/validation.service.js.map +1 -1
- package/dist/utilities/array/array.utility.d.ts +3 -0
- package/dist/utilities/array/array.utility.js +3 -0
- package/dist/utilities/array/array.utility.js.map +1 -1
- package/dist/utilities/file/file.utility.d.ts +15 -0
- package/dist/utilities/file/file.utility.js +18 -1
- package/dist/utilities/file/file.utility.js.map +1 -1
- package/dist/utilities/hcl/hcl.utility.d.ts +61 -0
- package/dist/utilities/hcl/hcl.utility.js +229 -0
- package/dist/utilities/hcl/hcl.utility.js.map +1 -0
- package/dist/utilities/node/node.utility.d.ts +3 -0
- package/dist/utilities/node/node.utility.js +3 -0
- package/dist/utilities/node/node.utility.js.map +1 -1
- package/dist/utilities/object/object.utility.d.ts +12 -0
- package/dist/utilities/object/object.utility.js +12 -0
- package/dist/utilities/object/object.utility.js.map +1 -1
- package/dist/utilities/project/project.utility.d.ts +28 -0
- package/dist/utilities/project/project.utility.js +35 -0
- package/dist/utilities/project/project.utility.js.map +1 -0
- package/dist/utilities/string/string.utility.d.ts +21 -0
- package/dist/utilities/string/string.utility.js +39 -0
- package/dist/utilities/string/string.utility.js.map +1 -0
- package/dist/utilities/terraform/terraform.utility.d.ts +227 -0
- package/dist/utilities/terraform/terraform.utility.js +434 -0
- package/dist/utilities/terraform/terraform.utility.js.map +1 -0
- package/dist/utilities/test-helpers/test-classes.d.ts +18 -31
- package/dist/utilities/test-helpers/test-classes.js +18 -41
- package/dist/utilities/test-helpers/test-classes.js.map +1 -1
- package/dist/utilities/test-helpers/test-models.js +12 -0
- package/dist/utilities/test-helpers/test-models.js.map +1 -1
- package/dist/utilities/test-helpers/test-modes.d.ts +315 -0
- package/dist/utilities/test-helpers/test-modes.js +463 -0
- package/dist/utilities/test-helpers/test-modes.js.map +1 -0
- package/dist/utilities/test-helpers/test-modules.d.ts +41 -0
- package/dist/utilities/test-helpers/test-modules.js +79 -0
- package/dist/utilities/test-helpers/test-modules.js.map +1 -0
- package/dist/utilities/test-helpers/test-overlays.d.ts +17 -1
- package/dist/utilities/test-helpers/test-overlays.js +49 -5
- package/dist/utilities/test-helpers/test-overlays.js.map +1 -1
- package/dist/utilities/test-helpers/test-resources.d.ts +19 -1
- package/dist/utilities/test-helpers/test-resources.js +61 -3
- package/dist/utilities/test-helpers/test-resources.js.map +1 -1
- package/dist/utilities/validation/validation.utility.d.ts +24 -0
- package/dist/utilities/validation/validation.utility.js +24 -0
- package/dist/utilities/validation/validation.utility.js.map +1 -1
- package/package.json +6 -5
- package/dist/services/capture/capture.service.d.ts +0 -19
- package/dist/services/capture/capture.service.js +0 -43
- package/dist/services/capture/capture.service.js.map +0 -1
- package/dist/utilities/cidr/cidr.utility.d.ts +0 -10
- package/dist/utilities/cidr/cidr.utility.js +0 -51
- package/dist/utilities/cidr/cidr.utility.js.map +0 -1
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { relative, resolve, sep } from 'node:path';
|
|
8
|
+
import { MatchingResource, } from '../../app.type.js';
|
|
9
|
+
import { Factory } from '../../decorators/factory.decorator.js';
|
|
10
|
+
import { getSchemaDefaults } from '../../functions/schema/schema.js';
|
|
11
|
+
import { StringUtility } from '../../utilities/string/string.utility.js';
|
|
12
|
+
import { ExpressionHclNode, HclExpression, InterpolatedStringHclNode, JsonEncodeHclNode, LiteralHclNode, MapHclNode, MapHclNodeRaw, PROJECT_ROOT_VARIABLE_NAME, RefHclNode, TerraformBlock, TerraformData, TerraformOutput, TerraformResource, TerraformVariable, TypeHclNode, toHclNode, } from './terraform.type.js';
|
|
13
|
+
/**
|
|
14
|
+
* Expands an author-provided spec object into attributes and blocks on `target`:
|
|
15
|
+
*/
|
|
16
|
+
function addTerraformResourceSpec(target, spec) {
|
|
17
|
+
for (const [key, value] of Object.entries(spec)) {
|
|
18
|
+
if (value instanceof HclExpression) {
|
|
19
|
+
target.attribute(key, value);
|
|
20
|
+
}
|
|
21
|
+
else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
22
|
+
addTerraformResourceSpec(target.block(key), value);
|
|
23
|
+
}
|
|
24
|
+
else if (Array.isArray(value) &&
|
|
25
|
+
value.every((v) => v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof HclExpression))) {
|
|
26
|
+
for (const item of value) {
|
|
27
|
+
addTerraformResourceSpec(target.block(key), item);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
target.attribute(key, value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Full implementation of {@link OctoTerraformResourceScope}. Holds all the internal state
|
|
37
|
+
* that {@link TerraformService} reads during the wiring and render phases.
|
|
38
|
+
*
|
|
39
|
+
* The `outputs` map is the resource's answer sheet:
|
|
40
|
+
* - key is the resource's response key that other resources use for referencing.
|
|
41
|
+
* - name is the `${resourceId}-${key}` output name it is published under.
|
|
42
|
+
* - value is the terraform expression that produces the value.
|
|
43
|
+
*/
|
|
44
|
+
class OctoTerraformResource {
|
|
45
|
+
/**
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
constructor(resourceId, resourceContext, moduleId, explicitParentResourceIds = [], providerRef, // Fully-qualified provider reference (`aws._111111111-us-east-1`).
|
|
49
|
+
externalResultExpression, // External resource marker with `data.external.<name>.result` value.
|
|
50
|
+
responseDefaults = {}) {
|
|
51
|
+
this.resourceId = resourceId;
|
|
52
|
+
this.resourceContext = resourceContext;
|
|
53
|
+
this.moduleId = moduleId;
|
|
54
|
+
this.explicitParentResourceIds = explicitParentResourceIds;
|
|
55
|
+
this.providerRef = providerRef;
|
|
56
|
+
this.externalResultExpression = externalResultExpression;
|
|
57
|
+
this.responseDefaults = responseDefaults;
|
|
58
|
+
this.outputs = new Map();
|
|
59
|
+
this.terraformResources = [];
|
|
60
|
+
// When an external script, publish the whole-result map as one output, keyed only by resource id, no output key.
|
|
61
|
+
if (externalResultExpression !== undefined) {
|
|
62
|
+
this.outputs.set(resourceId, {
|
|
63
|
+
name: StringUtility.sanitizeForIdentifier(resourceId),
|
|
64
|
+
sensitive: false,
|
|
65
|
+
value: new ExpressionHclNode(externalResultExpression),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
addTerraformResource(type, name, spec = {}) {
|
|
70
|
+
const intraResourceDependencies = this.terraformResources.length === 0 ? [] : [this.terraformResources[this.terraformResources.length - 1].address];
|
|
71
|
+
const terraformResource = new TerraformResource(type, StringUtility.sanitizeForIdentifier(name), intraResourceDependencies);
|
|
72
|
+
if (this.providerRef && !('provider' in spec)) {
|
|
73
|
+
terraformResource.attribute('provider', new ExpressionHclNode(this.providerRef));
|
|
74
|
+
}
|
|
75
|
+
addTerraformResourceSpec(terraformResource, spec);
|
|
76
|
+
this.terraformResources.push(terraformResource);
|
|
77
|
+
return terraformResource;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
collectRefs(into) {
|
|
83
|
+
this.terraformResources.forEach((r) => r.collectRefs(into));
|
|
84
|
+
this.outputs.forEach((o) => o.value.collectRefs(into));
|
|
85
|
+
}
|
|
86
|
+
output(outputs, options = {}) {
|
|
87
|
+
const sensitiveKeys = new Set(options.sensitiveKeys ?? []);
|
|
88
|
+
for (const [key, value] of Object.entries(outputs)) {
|
|
89
|
+
this.outputs.set(key, {
|
|
90
|
+
name: StringUtility.sanitizeForIdentifier(`${this.resourceId}-${key}`),
|
|
91
|
+
sensitive: sensitiveKeys.has(key),
|
|
92
|
+
value: toHclNode(value),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Everything that renders into one terragrunt module folder.
|
|
99
|
+
*/
|
|
100
|
+
class TerraformModule {
|
|
101
|
+
/**
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
constructor(moduleId) {
|
|
105
|
+
this.moduleId = moduleId;
|
|
106
|
+
this.dataSources = [];
|
|
107
|
+
this.hasExternalResources = false; // True when folder has external resources requiring null/external configs.
|
|
108
|
+
this.needsProjectRoot = false; // True when a resource named a local file, requiring the project root variable.
|
|
109
|
+
this.providerKeys = new Set(); // Specific providers this folder's resources require.
|
|
110
|
+
this.resources = [];
|
|
111
|
+
this.variables = [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Terraform providers that take no `region` argument - the hashicorp utility providers, which take
|
|
116
|
+
* no provider arguments at all. Writing one onto their block fails the plan. Regional providers
|
|
117
|
+
* (`aws`, `google`, and the rest) are not listed: they keep the `region` they have always been
|
|
118
|
+
* given, and anything unlisted that does not want one can pass `setRegionAttribute: false`.
|
|
119
|
+
*/
|
|
120
|
+
const REGIONLESS_PROVIDER_TYPES = new Set(['archive', 'external', 'local', 'null', 'random', 'template', 'time']);
|
|
121
|
+
/**
|
|
122
|
+
* Provider types that can be told which account they are allowed to authenticate as. Only `aws`
|
|
123
|
+
* spells it `allowed_account_ids`; the equivalent everywhere else is a different word for a
|
|
124
|
+
* different thing - google scopes to a project, azure to a subscription - so nothing else is listed.
|
|
125
|
+
*/
|
|
126
|
+
const ACCOUNT_GUARDED_PROVIDER_TYPES = new Set(['aws']);
|
|
127
|
+
/**
|
|
128
|
+
* Collects what every resource emitted through {@link TerraformModuleScope}, wires the references
|
|
129
|
+
* between module folders, and renders the terragrunt files {@link Octo.generate} writes.
|
|
130
|
+
*
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
133
|
+
export class TerraformService {
|
|
134
|
+
/**
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
137
|
+
get step() {
|
|
138
|
+
return ' '.repeat(this.indentLength);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* @internal
|
|
142
|
+
*/
|
|
143
|
+
constructor(octoCommandPrefix = 'octo', indentLength = 2) {
|
|
144
|
+
this.octoCommandPrefix = octoCommandPrefix;
|
|
145
|
+
this.indentLength = indentLength;
|
|
146
|
+
this.minTerraformVersion = '1.6.0';
|
|
147
|
+
this.modules = new Map();
|
|
148
|
+
this.pendingExternalResourcesInputWiring = [];
|
|
149
|
+
this.providers = new Map(); // TF provider blocks.
|
|
150
|
+
this.requiredProviders = {}; // TF config block.
|
|
151
|
+
this.resourceRegistry = new Map();
|
|
152
|
+
this.sanitizedResourceIds = new Map(); // Sanitized id -> original id.
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Sets the minimum terraform version and the `required_providers` sources/versions that render
|
|
156
|
+
* into every folder's config block (scoped to the providers each folder actually uses).
|
|
157
|
+
*/
|
|
158
|
+
addTerraformConfig({ minTerraformVersion = '1.6.0', providers = {}, } = {}) {
|
|
159
|
+
this.minTerraformVersion = minTerraformVersion;
|
|
160
|
+
this.requiredProviders = { ...this.requiredProviders, ...providers };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Registers a Terraform provider block for the given provider type, account, and region.
|
|
164
|
+
*
|
|
165
|
+
* An `aws` provider is also given `allowed_account_ids = [accountId]`, so terraform refuses at plan
|
|
166
|
+
* when the credentials it resolves belong to another account. It is omitted when `accountName` is
|
|
167
|
+
* set (the repository has asked for the id not to appear in committed terraform), when `spec`
|
|
168
|
+
* already carries `allowed_account_ids`, and when `spec` sets `skip_requesting_account_id`, which
|
|
169
|
+
* stops the provider learning the account there is to check.
|
|
170
|
+
*
|
|
171
|
+
* @param providerType - The terraform provider type, e.g. `aws`, `google`.
|
|
172
|
+
* @param accountId - The account/project/subscription ID.
|
|
173
|
+
* @param regionId - The region ID.
|
|
174
|
+
* @param spec - Provider arguments beyond `alias` and `region`.
|
|
175
|
+
* - Primitives become attributes (`key = value`).
|
|
176
|
+
* - Plain objects become blocks (`key { }`).
|
|
177
|
+
* - Wrap with `mapAttr()` when the provider schema expects a map attribute (`key = { }`).
|
|
178
|
+
* @param options - optional provider configurations.
|
|
179
|
+
* - options.accountName - What to call the account in the provider alias, in place of its id.
|
|
180
|
+
* `prod` renders `aws.prod-us-east-1` where the id renders `aws._123456789012-us-east-1`. Two
|
|
181
|
+
* accounts may not share a name within one provider type and region, and the generated terraform
|
|
182
|
+
* is committed, so a name is also how a repository keeps its account number out of it. It also
|
|
183
|
+
* drops the `allowed_account_ids` guard above, which would put that id straight back.
|
|
184
|
+
* - options.setRegionAttribute - Whether to write `region = <regionId>` onto the provider block.
|
|
185
|
+
* Defaults to true, except for the utility providers in {@link REGIONLESS_PROVIDER_TYPES}, which
|
|
186
|
+
* accept no arguments at all and fail the plan outright with
|
|
187
|
+
* `An argument named "region" is not expected here`. Set it explicitly to override either way -
|
|
188
|
+
* a regional provider this list does not know about still needs `false` passed by hand.
|
|
189
|
+
*/
|
|
190
|
+
addTerraformProvider(providerType, accountId, regionId, spec = {}, { accountName, setRegionAttribute } = {}) {
|
|
191
|
+
if (!(providerType in this.requiredProviders)) {
|
|
192
|
+
throw new Error(`Provider type "${providerType}" is not configured! Call registerTerraformConfig() with a "${providerType}" entry before registering providers.`);
|
|
193
|
+
}
|
|
194
|
+
// Terraform identifiers (here, the provider alias) must start with a letter or underscore.
|
|
195
|
+
const sanitizedAlias = StringUtility.sanitizeForIdentifier(`${accountName ?? accountId}-${regionId}`);
|
|
196
|
+
const alias = /^[0-9]/.test(sanitizedAlias) ? `_${sanitizedAlias}` : sanitizedAlias;
|
|
197
|
+
const key = this.providerKey(providerType, accountId, regionId);
|
|
198
|
+
if (!this.providers.has(key)) {
|
|
199
|
+
// A name is the caller's to pick, so two accounts can collide on one where two ids cannot.
|
|
200
|
+
const taken = [...this.providers.values()].find((provider) => provider.providerType === providerType && provider.alias === alias);
|
|
201
|
+
if (taken) {
|
|
202
|
+
throw new Error(`Terraform provider alias "${alias}" is already taken by account "${taken.accountId}"! Give one of them a different accountName.`);
|
|
203
|
+
}
|
|
204
|
+
const block = new TerraformBlock(`provider "${providerType}"`);
|
|
205
|
+
block.attribute('alias', alias);
|
|
206
|
+
if (setRegionAttribute ?? !REGIONLESS_PROVIDER_TYPES.has(providerType)) {
|
|
207
|
+
block.attribute('region', regionId);
|
|
208
|
+
}
|
|
209
|
+
// Bind the provider to the account the definition names. `accountId` is otherwise only a label
|
|
210
|
+
// octo builds the alias from: terraform authenticates from the ambient credential chain, so a
|
|
211
|
+
// profile naming another account plans green and applies into it. This is the one form of the
|
|
212
|
+
// check that runs on the chain terraform applies with, and it fails at plan.
|
|
213
|
+
if (ACCOUNT_GUARDED_PROVIDER_TYPES.has(providerType) &&
|
|
214
|
+
accountName === undefined &&
|
|
215
|
+
!('allowed_account_ids' in spec) &&
|
|
216
|
+
!spec['skip_requesting_account_id']) {
|
|
217
|
+
block.attribute('allowed_account_ids', [accountId]);
|
|
218
|
+
}
|
|
219
|
+
addTerraformResourceSpec(block, spec);
|
|
220
|
+
this.providers.set(key, { accountId, alias, block, providerType, regionId });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Returns the folder record of the current sweep: one entry per folder-bearing module — a
|
|
225
|
+
* preloaded shell the sweep left empty is an emptied folder, not folder-bearing, so it drops out
|
|
226
|
+
* of the record. Call after the sweep has contributed every resource (the same point
|
|
227
|
+
* {@link renderAllModules} is valid).
|
|
228
|
+
*/
|
|
229
|
+
getFolderRecords() {
|
|
230
|
+
// Provider blocks are registration-time constants; a resource reference inside one is invalid.
|
|
231
|
+
const context = {
|
|
232
|
+
resolveRef: () => {
|
|
233
|
+
throw new Error('Resource references are not supported in provider blocks!');
|
|
234
|
+
},
|
|
235
|
+
step: this.step,
|
|
236
|
+
};
|
|
237
|
+
return [...this.modules.values()]
|
|
238
|
+
.filter((module) => module.resources.length > 0)
|
|
239
|
+
.map((module) => ({
|
|
240
|
+
hasExternalResources: module.hasExternalResources,
|
|
241
|
+
moduleId: module.moduleId,
|
|
242
|
+
providers: [...module.providerKeys].sort().map((key) => {
|
|
243
|
+
const provider = this.providers.get(key);
|
|
244
|
+
return {
|
|
245
|
+
accountId: provider.accountId,
|
|
246
|
+
blockHcl: provider.block.render('', context),
|
|
247
|
+
providerType: provider.providerType,
|
|
248
|
+
regionId: provider.regionId,
|
|
249
|
+
requiredProvider: this.requiredProviders[provider.providerType],
|
|
250
|
+
};
|
|
251
|
+
}),
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The IDs of the module folders this run rendered.
|
|
256
|
+
*/
|
|
257
|
+
getModuleIds() {
|
|
258
|
+
return [...this.modules.keys()];
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* How each octo resource maps into terraform, which validate and commit read to match a plan
|
|
262
|
+
* entry or an output back to the resource that produced it.
|
|
263
|
+
*/
|
|
264
|
+
getOctoTerraformResourceMappings() {
|
|
265
|
+
return [...this.resourceRegistry.values()].map((r) => {
|
|
266
|
+
const isExternal = r.externalResultExpression !== undefined;
|
|
267
|
+
return {
|
|
268
|
+
entireResponseOutput: isExternal ? r.outputs.get(r.resourceId).name : undefined,
|
|
269
|
+
moduleId: r.moduleId,
|
|
270
|
+
outputMappings: isExternal ? [] : [...r.outputs.entries()].map(([key, o]) => ({ key, outputName: o.name })),
|
|
271
|
+
resourceContext: r.resourceContext,
|
|
272
|
+
resourceId: r.resourceId,
|
|
273
|
+
terraformAddresses: r.terraformResources.map((t) => t.address),
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Resource→resource value-reference edges, keyed by the referenced (producer) resourceId → the set
|
|
279
|
+
* of resourceIds that reference it. Edges follow `getRef` value references only — the same graph
|
|
280
|
+
* terraform cascades replacement along (force-new). `depends_on`/explicit parents are excluded:
|
|
281
|
+
* an ordering edge carries no value and does not force a dependent's replacement.
|
|
282
|
+
*
|
|
283
|
+
* Used by validate to attribute terraform's transitive replacement cascade: when octo replaces a
|
|
284
|
+
* resource, terraform recreates everything that (transitively) references it, so those changes are
|
|
285
|
+
* expected rather than unattributed.
|
|
286
|
+
*/
|
|
287
|
+
getResourceReferrers() {
|
|
288
|
+
const referrers = new Map();
|
|
289
|
+
const addEdge = (referentId, referrerId) => {
|
|
290
|
+
if (referentId === referrerId) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (!referrers.has(referentId)) {
|
|
294
|
+
referrers.set(referentId, new Set());
|
|
295
|
+
}
|
|
296
|
+
referrers.get(referentId).add(referrerId);
|
|
297
|
+
};
|
|
298
|
+
for (const resource of this.resourceRegistry.values()) {
|
|
299
|
+
const refs = [];
|
|
300
|
+
resource.collectRefs(refs);
|
|
301
|
+
for (const ref of refs) {
|
|
302
|
+
addEdge(ref.resourceId, resource.resourceId);
|
|
303
|
+
}
|
|
304
|
+
// An external resource consumes each parent's outputs as inputs — its null_resource triggers
|
|
305
|
+
// embed the parent's values, so a parent replace recreates it. That is a true value edge, even
|
|
306
|
+
// though it is recorded as an explicit parent (and its trigger refs are wired only at render
|
|
307
|
+
// time, so they are not yet in `collectRefs` here).
|
|
308
|
+
if (resource.externalResultExpression !== undefined) {
|
|
309
|
+
for (const parentResourceId of resource.explicitParentResourceIds) {
|
|
310
|
+
addEdge(parentResourceId, resource.resourceId);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return referrers;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Phase 2 of rendering: prints the four files of every folder from the wiring computed in
|
|
318
|
+
* phase 1. Makes no decisions — when a {@link RefHclNode} renders, its answer is looked up in the
|
|
319
|
+
* folder's precomputed `resolvedRefs`. Cross-folder dependency cycles throw (in phase 1).
|
|
320
|
+
*/
|
|
321
|
+
renderAllModules() {
|
|
322
|
+
const wiring = this.computeWiring();
|
|
323
|
+
const result = new Map();
|
|
324
|
+
for (const module of this.modules.values()) {
|
|
325
|
+
const moduleWiring = wiring.get(module.moduleId);
|
|
326
|
+
const context = {
|
|
327
|
+
resolveRef: (ref) => moduleWiring.resolvedRefs.get(`${ref.resourceId}.${ref.key}`),
|
|
328
|
+
step: this.step,
|
|
329
|
+
};
|
|
330
|
+
// main.tf: terraform config, provider blocks, data sources, resources.
|
|
331
|
+
const requiredProviders = {};
|
|
332
|
+
for (const key of module.providerKeys) {
|
|
333
|
+
const providerType = this.providers.get(key).providerType;
|
|
334
|
+
requiredProviders[providerType] = this.requiredProviders[providerType];
|
|
335
|
+
}
|
|
336
|
+
if (module.hasExternalResources) {
|
|
337
|
+
requiredProviders['null'] = this.requiredProviders['null'] ?? { source: 'hashicorp/null' };
|
|
338
|
+
requiredProviders['external'] = this.requiredProviders['external'] ?? { source: 'hashicorp/external' };
|
|
339
|
+
}
|
|
340
|
+
const mainTf = [
|
|
341
|
+
this.renderConfigBlock(requiredProviders, context),
|
|
342
|
+
...[...module.providerKeys].sort().map((key) => this.providers.get(key).block.render('', context)),
|
|
343
|
+
...module.dataSources.map((d) => d.render(context)),
|
|
344
|
+
...module.resources.map((r) => r.terraformResources.map((t) => t.render(context, moduleWiring.parentDependsOn.get(r) ?? [])).join('\n\n')),
|
|
345
|
+
]
|
|
346
|
+
.filter(Boolean)
|
|
347
|
+
.join('\n\n');
|
|
348
|
+
// variables.tf: auto variables from cross-module refs, then user variables. An external
|
|
349
|
+
// producer's variable carries its entire response object (consumed as `var.<name>.<key>`), so it
|
|
350
|
+
// must be typed `map(string)` — terraform's external-data contract — otherwise an un-typed variable
|
|
351
|
+
// coerces the object to a JSON string and `var.<name>.<key>` fails at plan/apply. Per-key scalar
|
|
352
|
+
// variables stay un-typed.
|
|
353
|
+
const variablesTf = [
|
|
354
|
+
...(module.needsProjectRoot
|
|
355
|
+
? [`variable "${PROJECT_ROOT_VARIABLE_NAME}" {\n${this.step}type = string\n}`]
|
|
356
|
+
: []),
|
|
357
|
+
...[...moduleWiring.autoVariables.entries()]
|
|
358
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
359
|
+
.map(([name, v]) => v.objectKeys ? `variable "${name}" {\n${this.step}type = map(string)\n}` : `variable "${name}" {}`),
|
|
360
|
+
...module.variables.map((v) => v.render(context)),
|
|
361
|
+
]
|
|
362
|
+
.filter(Boolean)
|
|
363
|
+
.join('\n\n');
|
|
364
|
+
// outputs.tf: every resource's declared outputs.
|
|
365
|
+
const outputsTf = module.resources
|
|
366
|
+
.map((r) => [...r.outputs.values()]
|
|
367
|
+
.map((o) => new TerraformOutput(o.name, o.value, o.sensitive).render(context))
|
|
368
|
+
.join('\n\n'))
|
|
369
|
+
.filter(Boolean)
|
|
370
|
+
.join('\n\n');
|
|
371
|
+
const terragruntHcl = this.renderTerragrunt(moduleWiring);
|
|
372
|
+
result.set(module.moduleId, {
|
|
373
|
+
mainTf: mainTf + '\n',
|
|
374
|
+
outputsTf: outputsTf ? outputsTf + '\n' : '',
|
|
375
|
+
terragruntHcl: terragruntHcl + '\n',
|
|
376
|
+
variablesTf: variablesTf ? variablesTf + '\n' : '',
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Prints the four files of an **empty** folder from a recorded folder: a `main.tf` with the
|
|
383
|
+
* config block rebuilt from the recorded `required_providers` entries (plus null/external when
|
|
384
|
+
* the folder had external resources) and the recorded provider blocks verbatim; empty
|
|
385
|
+
* variables/outputs; a remote_state-only `terragrunt.hcl`.
|
|
386
|
+
*
|
|
387
|
+
* Reads no module state;
|
|
388
|
+
* the generate mode decides which folders are emptied and which recorded folder each renders from.
|
|
389
|
+
*/
|
|
390
|
+
renderEmptyModule(record) {
|
|
391
|
+
const context = {
|
|
392
|
+
resolveRef: () => {
|
|
393
|
+
throw new Error('Resource references are not supported in emptied folders!');
|
|
394
|
+
},
|
|
395
|
+
step: this.step,
|
|
396
|
+
};
|
|
397
|
+
const requiredProviders = {};
|
|
398
|
+
for (const provider of record.providers) {
|
|
399
|
+
requiredProviders[provider.providerType] = provider.requiredProvider;
|
|
400
|
+
}
|
|
401
|
+
if (record.hasExternalResources) {
|
|
402
|
+
requiredProviders['null'] = this.requiredProviders['null'] ?? { source: 'hashicorp/null' };
|
|
403
|
+
requiredProviders['external'] = this.requiredProviders['external'] ?? { source: 'hashicorp/external' };
|
|
404
|
+
}
|
|
405
|
+
const mainTf = [this.renderConfigBlock(requiredProviders, context), ...record.providers.map((p) => p.blockHcl)]
|
|
406
|
+
.filter(Boolean)
|
|
407
|
+
.join('\n\n');
|
|
408
|
+
return {
|
|
409
|
+
mainTf: mainTf + '\n',
|
|
410
|
+
outputsTf: '',
|
|
411
|
+
terragruntHcl: this.renderTerragrunt({
|
|
412
|
+
autoVariables: new Map(),
|
|
413
|
+
moduleDependencies: new Map(),
|
|
414
|
+
parentDependsOn: new Map(),
|
|
415
|
+
resolvedRefs: new Map(),
|
|
416
|
+
}) + '\n',
|
|
417
|
+
variablesTf: '',
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Drops everything the last run rendered, keeping the registered providers and config.
|
|
422
|
+
*/
|
|
423
|
+
reset() {
|
|
424
|
+
this.minTerraformVersion = '1.6.0';
|
|
425
|
+
this.providers.clear();
|
|
426
|
+
this.requiredProviders = {};
|
|
427
|
+
this.resetTransactionState();
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Clears what a transaction's sweep contributed (modules, resources, wiring), keeping the
|
|
431
|
+
* per-process registrations (terraform config and providers) intact. Test harnesses that run
|
|
432
|
+
* multiple transactions in one process call this between them; a real octo process runs one
|
|
433
|
+
* transaction, so production code never resets.
|
|
434
|
+
*/
|
|
435
|
+
resetTransactionState() {
|
|
436
|
+
this.modules.clear();
|
|
437
|
+
this.pendingExternalResourcesInputWiring.length = 0;
|
|
438
|
+
this.resourceRegistry.clear();
|
|
439
|
+
this.sanitizedResourceIds.clear();
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* The {@link TerraformModuleScope} handed to a resource's `toHCL()`, bound to one module folder.
|
|
443
|
+
*/
|
|
444
|
+
scope(moduleId) {
|
|
445
|
+
return {
|
|
446
|
+
addOctoTerraformExternalResource: (octoResource) => this.addOctoTerraformExternalResource(moduleId, octoResource),
|
|
447
|
+
addOctoTerraformResource: (octoResource, options = {}) => this.addOctoTerraformResource(moduleId, octoResource, options),
|
|
448
|
+
addTerraformData: (type, name, spec = {}) => this.addTerraformData(moduleId, type, name, spec),
|
|
449
|
+
getProviderAliasRef: (accountId, regionId) => this.getProviderAliasRef(moduleId, accountId, regionId),
|
|
450
|
+
getRef: ((resource, key) => this.getRef(resource, key)),
|
|
451
|
+
jsonencode: (subject) => this.jsonencode(subject),
|
|
452
|
+
localPath: (filePath) => this.localPath(moduleId, filePath),
|
|
453
|
+
mapAttr: (value) => this.mapAttr(value),
|
|
454
|
+
moduleId,
|
|
455
|
+
raw: (value) => this.raw(value),
|
|
456
|
+
type: (schema) => this.type(schema),
|
|
457
|
+
variable: (name, typeExpression, options) => this.variable(moduleId, name, typeExpression, options),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Generates the `null_resource` + `data "external"` wrapper for an external resource.
|
|
462
|
+
*
|
|
463
|
+
* Only the parts that depend on no other resource are built here:
|
|
464
|
+
* - the `null_resource` shell (its triggers and `local-exec` provisioners are filled in later),
|
|
465
|
+
* - the `data "external"` read of the response octo recorded, which terraform re-runs on every plan
|
|
466
|
+
* and every apply, and defers to apply time only while the `null_resource` is planned to change,
|
|
467
|
+
* - the single whole-result output (`data.external.<name>.result`) that carries the entire response.
|
|
468
|
+
*
|
|
469
|
+
* The external resource is never enumerated per response key: its response shape is unknown at
|
|
470
|
+
* generation time (the action has not run) and is, by the `data "external"` protocol, a flat map of
|
|
471
|
+
* strings. Refs to it (`getRef`) resolve by indexing this one expression; commit reads the whole map back.
|
|
472
|
+
*
|
|
473
|
+
* The parent-input wiring (triggers + create/destroy commands) reads every parent's outputs, which
|
|
474
|
+
* only all exist once registration is complete. It is therefore deferred to
|
|
475
|
+
* {@link wireExternalResourceInputs} (run at the start of {@link computeWiring}). This deferral is
|
|
476
|
+
* what lets the generate sweep contribute resources in any order, rather than parents-first.
|
|
477
|
+
*/
|
|
478
|
+
addOctoTerraformExternalResource(moduleId, octoResource) {
|
|
479
|
+
const resourceId = octoResource.resourceId;
|
|
480
|
+
const name = StringUtility.sanitizeForIdentifier(resourceId);
|
|
481
|
+
const parents = (octoResource.parents || []).map((p) => p instanceof MatchingResource ? p.getActual() : p);
|
|
482
|
+
const octoTerraformResource = this.addOctoTerraformResource(moduleId, octoResource, {
|
|
483
|
+
explicitParents: parents,
|
|
484
|
+
externalResultExpression: `data.external.${name}.result`,
|
|
485
|
+
});
|
|
486
|
+
this.module(moduleId).hasExternalResources = true;
|
|
487
|
+
// The null_resource shell. Its triggers and provisioner are added by wireExternalResourceInputs(),
|
|
488
|
+
// once every parent's outputs are known.
|
|
489
|
+
const nullResource = octoTerraformResource.addTerraformResource('null_resource', name);
|
|
490
|
+
const dataExternal = this.addTerraformData(moduleId, 'external', name, {
|
|
491
|
+
program: [...this.octoCommandPrefix.split(' '), 'get-response', `--resourceId=${resourceId}`],
|
|
492
|
+
});
|
|
493
|
+
dataExternal.attribute('depends_on', new ExpressionHclNode(`[null_resource.${name}]`));
|
|
494
|
+
this.pendingExternalResourcesInputWiring.push({ nullResource, octoResource, resourceId });
|
|
495
|
+
return octoTerraformResource;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* @internal
|
|
499
|
+
*/
|
|
500
|
+
addOctoTerraformResource(moduleId, octoResource, options = {}) {
|
|
501
|
+
const explicitParentResourceIds = (options.explicitParents || []).map((p) => p instanceof MatchingResource ? p.getSchemaInstanceInResourceAction().resourceId : p.resourceId);
|
|
502
|
+
// Distinct resource ids must stay distinct after sanitization, or else the generated output names,
|
|
503
|
+
// identifiers, and variable names would silently collide. Environment-variable sanitization
|
|
504
|
+
// merges strictly more characters than identifier sanitization, so this one check covers both.
|
|
505
|
+
const sanitizedResourceId = StringUtility.sanitizeForEnvironmentVariable(octoResource.resourceId);
|
|
506
|
+
const existingResourceId = this.sanitizedResourceIds.get(sanitizedResourceId);
|
|
507
|
+
if (existingResourceId !== undefined) {
|
|
508
|
+
if (existingResourceId === octoResource.resourceId) {
|
|
509
|
+
throw new Error(`Resource id "${octoResource.resourceId}" is already registered!`);
|
|
510
|
+
}
|
|
511
|
+
throw new Error(`Resource id "${octoResource.resourceId}" collides with resource id "${existingResourceId}" after sanitization!`);
|
|
512
|
+
}
|
|
513
|
+
this.sanitizedResourceIds.set(sanitizedResourceId, octoResource.resourceId);
|
|
514
|
+
const module = this.module(moduleId);
|
|
515
|
+
let providerRef;
|
|
516
|
+
if (options.provider) {
|
|
517
|
+
const { alias, key, providerType } = this.findProvider(options.provider.accountId, options.provider.regionId);
|
|
518
|
+
providerRef = `${providerType}.${alias}`;
|
|
519
|
+
module.providerKeys.add(key);
|
|
520
|
+
}
|
|
521
|
+
// Seed mock outputs from the producer schema's declared response defaults — never the live
|
|
522
|
+
// response — so a regenerate after a commit cannot inline real (possibly secret) applied values.
|
|
523
|
+
// Test resources carry a non-constructable NODE_SCHEMA; they fall back to synthetic placeholders.
|
|
524
|
+
const schemaClass = octoResource.constructor
|
|
525
|
+
.NODE_SCHEMA;
|
|
526
|
+
const responseDefaults = typeof schemaClass === 'function' ? (getSchemaDefaults(schemaClass).response ?? {}) : {};
|
|
527
|
+
const newResource = new OctoTerraformResource(octoResource.resourceId, octoResource.getContext(), moduleId, explicitParentResourceIds, providerRef, options.externalResultExpression, responseDefaults);
|
|
528
|
+
module.resources.push(newResource);
|
|
529
|
+
this.resourceRegistry.set(octoResource.resourceId, newResource);
|
|
530
|
+
return newResource;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* @internal
|
|
534
|
+
*/
|
|
535
|
+
addTerraformData(moduleId, type, name, spec = {}) {
|
|
536
|
+
const dataSource = new TerraformData(type, StringUtility.sanitizeForIdentifier(name));
|
|
537
|
+
addTerraformResourceSpec(dataSource, spec);
|
|
538
|
+
this.module(moduleId).dataSources.push(dataSource);
|
|
539
|
+
return dataSource;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Phase 1 of rendering: resolve every {@link RefHclNode} and compute each folder's wiring.
|
|
543
|
+
*
|
|
544
|
+
* For every folder: collect all refs made from it, then resolve each one —
|
|
545
|
+
* - producer in the same folder → the inline terraform expression,
|
|
546
|
+
* - producer in another folder → a `var.*` name, plus three recorded consequences: declare the
|
|
547
|
+
* variable, depend on the producer's folder, wire the input in terragrunt.hcl.
|
|
548
|
+
*
|
|
549
|
+
* Explicit parents resolve similarly: same folder → `depends_on` addresses; other folder → a
|
|
550
|
+
* terragrunt dependency. All ref errors and cross-folder cycles surface here — phase 2 cannot
|
|
551
|
+
* fail.
|
|
552
|
+
*/
|
|
553
|
+
computeWiring() {
|
|
554
|
+
// Resolve deferred external-resource input wiring before collecting refs, so the triggers and
|
|
555
|
+
// commands it produces are part of this pass.
|
|
556
|
+
this.wireExternalResourceInputs();
|
|
557
|
+
const wiring = new Map();
|
|
558
|
+
const wiringOf = (moduleId) => {
|
|
559
|
+
if (!wiring.has(moduleId)) {
|
|
560
|
+
wiring.set(moduleId, {
|
|
561
|
+
autoVariables: new Map(),
|
|
562
|
+
moduleDependencies: new Map(),
|
|
563
|
+
parentDependsOn: new Map(),
|
|
564
|
+
resolvedRefs: new Map(),
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
return wiring.get(moduleId);
|
|
568
|
+
};
|
|
569
|
+
const addModuleDependency = (moduleWiring, producerModuleId, reason) => {
|
|
570
|
+
if (!moduleWiring.moduleDependencies.has(producerModuleId)) {
|
|
571
|
+
moduleWiring.moduleDependencies.set(producerModuleId, new Set());
|
|
572
|
+
}
|
|
573
|
+
moduleWiring.moduleDependencies.get(producerModuleId).add(reason);
|
|
574
|
+
};
|
|
575
|
+
// Producer output expressions render without a consumer context; a ref inside an output
|
|
576
|
+
// expression would require recursive resolution and is not supported.
|
|
577
|
+
const renderInlineContext = {
|
|
578
|
+
resolveRef: () => {
|
|
579
|
+
throw new Error('Nested resource references are not supported in output expressions!');
|
|
580
|
+
},
|
|
581
|
+
step: this.step,
|
|
582
|
+
};
|
|
583
|
+
for (const module of this.modules.values()) {
|
|
584
|
+
const moduleWiring = wiringOf(module.moduleId);
|
|
585
|
+
// Collect every reference made from this module.
|
|
586
|
+
const refs = [];
|
|
587
|
+
module.resources.forEach((r) => r.collectRefs(refs));
|
|
588
|
+
module.dataSources.forEach((d) => d.collectRefs(refs));
|
|
589
|
+
module.variables.forEach((v) => v.collectRefs(refs));
|
|
590
|
+
for (const ref of refs) {
|
|
591
|
+
const producer = this.resourceRegistry.get(ref.resourceId);
|
|
592
|
+
if (!producer) {
|
|
593
|
+
throw new Error(`Resource "${ref.resourceId}" not found in Octo Terraform!`);
|
|
594
|
+
}
|
|
595
|
+
if (producer.externalResultExpression !== undefined) {
|
|
596
|
+
if (producer.moduleId === module.moduleId) {
|
|
597
|
+
moduleWiring.resolvedRefs.set(`${ref.resourceId}.${ref.key}`, ref.entireResponse
|
|
598
|
+
? producer.externalResultExpression
|
|
599
|
+
: `${producer.externalResultExpression}.${ref.key}`);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
const variableName = StringUtility.sanitizeForEnvironmentVariable(ref.resourceId);
|
|
603
|
+
const entireResponseOutput = producer.outputs.get(producer.resourceId);
|
|
604
|
+
// The variable holds the producer's entire response object; collect the keys consumed
|
|
605
|
+
// across all refs so the mock can be shaped as an object (see `objectKeys`).
|
|
606
|
+
const objectKeys = moduleWiring.autoVariables.get(variableName)?.objectKeys ?? new Set();
|
|
607
|
+
if (!ref.entireResponse) {
|
|
608
|
+
objectKeys.add(ref.key);
|
|
609
|
+
}
|
|
610
|
+
moduleWiring.autoVariables.set(variableName, {
|
|
611
|
+
objectKeys,
|
|
612
|
+
outputName: entireResponseOutput.name,
|
|
613
|
+
producerModuleId: producer.moduleId,
|
|
614
|
+
producerResourceId: producer.resourceId,
|
|
615
|
+
});
|
|
616
|
+
addModuleDependency(moduleWiring, producer.moduleId, `ref "${ref.resourceId}.${ref.key}"`);
|
|
617
|
+
moduleWiring.resolvedRefs.set(`${ref.resourceId}.${ref.key}`, ref.entireResponse ? `var.${variableName}` : `var.${variableName}.${ref.key}`);
|
|
618
|
+
}
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const output = producer.outputs.get(ref.key);
|
|
622
|
+
if (!output) {
|
|
623
|
+
throw new Error(`Ref "${ref.key}" not registered for resource "${ref.resourceId}" in Octo Terraform!`);
|
|
624
|
+
}
|
|
625
|
+
if (producer.moduleId === module.moduleId) {
|
|
626
|
+
moduleWiring.resolvedRefs.set(`${ref.resourceId}.${ref.key}`, output.value.render('', renderInlineContext));
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
const variableName = StringUtility.sanitizeForEnvironmentVariable(`${ref.resourceId}_${ref.key}`);
|
|
630
|
+
const existingVariable = moduleWiring.autoVariables.get(variableName);
|
|
631
|
+
if (existingVariable && existingVariable.outputName !== output.name) {
|
|
632
|
+
throw new Error(`Variable "${variableName}" maps to both outputs "${existingVariable.outputName}" and ` +
|
|
633
|
+
`"${output.name}"! Rename one of the resources or response keys.`);
|
|
634
|
+
}
|
|
635
|
+
moduleWiring.autoVariables.set(variableName, {
|
|
636
|
+
outputName: output.name,
|
|
637
|
+
producerModuleId: producer.moduleId,
|
|
638
|
+
producerResourceId: producer.resourceId,
|
|
639
|
+
responseKey: ref.key,
|
|
640
|
+
});
|
|
641
|
+
addModuleDependency(moduleWiring, producer.moduleId, `ref "${ref.resourceId}.${ref.key}"`);
|
|
642
|
+
moduleWiring.resolvedRefs.set(`${ref.resourceId}.${ref.key}`, `var.${variableName}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// Explicit parents: same module → depends_on; cross module → terragrunt ordering.
|
|
646
|
+
for (const resource of module.resources) {
|
|
647
|
+
const addresses = [];
|
|
648
|
+
for (const parentResourceId of resource.explicitParentResourceIds) {
|
|
649
|
+
const parent = this.resourceRegistry.get(parentResourceId);
|
|
650
|
+
if (!parent) {
|
|
651
|
+
throw new Error(`Explicit parent "${parentResourceId}" of resource "${resource.resourceId}" is not registered in Terraform!`);
|
|
652
|
+
}
|
|
653
|
+
if (parent.moduleId === module.moduleId) {
|
|
654
|
+
addresses.push(...parent.terraformResources.map((t) => t.address));
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
addModuleDependency(moduleWiring, parent.moduleId, `parent "${parentResourceId}" of "${resource.resourceId}"`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
moduleWiring.parentDependsOn.set(resource, addresses);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
this.detectModuleCycles(wiring);
|
|
664
|
+
return wiring;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Depth-first search over folder dependencies; throws on the first cycle, naming its path and
|
|
668
|
+
* the refs/explicit parents forming each edge.
|
|
669
|
+
*/
|
|
670
|
+
detectModuleCycles(wiring) {
|
|
671
|
+
const visited = new Set();
|
|
672
|
+
const inStack = new Set();
|
|
673
|
+
const visit = (moduleId, path) => {
|
|
674
|
+
if (inStack.has(moduleId)) {
|
|
675
|
+
const cycleStart = path.indexOf(moduleId);
|
|
676
|
+
const cycle = [...path.slice(cycleStart), moduleId];
|
|
677
|
+
const edges = cycle.slice(0, -1).map((from, i) => {
|
|
678
|
+
const to = cycle[i + 1];
|
|
679
|
+
const reasons = [...(wiring.get(from)?.moduleDependencies.get(to) ?? [])];
|
|
680
|
+
return `"${from}" -> "${to}" via ${reasons.join(', ')}`;
|
|
681
|
+
});
|
|
682
|
+
throw new Error(`Found cross-module dependency cycle: ${cycle.join(' -> ')}! Edges: ${edges.join('; ')}.`);
|
|
683
|
+
}
|
|
684
|
+
if (visited.has(moduleId)) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
inStack.add(moduleId);
|
|
688
|
+
for (const dependency of wiring.get(moduleId)?.moduleDependencies.keys() ?? []) {
|
|
689
|
+
visit(dependency, [...path, moduleId]);
|
|
690
|
+
}
|
|
691
|
+
inStack.delete(moduleId);
|
|
692
|
+
visited.add(moduleId);
|
|
693
|
+
};
|
|
694
|
+
for (const moduleId of wiring.keys()) {
|
|
695
|
+
visit(moduleId, []);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Derives the registered provider for a deployment target using only `accountId` (+ `regionId`).
|
|
700
|
+
* The provider type was decided during provider registration.
|
|
701
|
+
*
|
|
702
|
+
* Without a `regionId` (global resources), any provider of the account matches — the first
|
|
703
|
+
* registered wins, since the alias only routes the region.
|
|
704
|
+
*/
|
|
705
|
+
findProvider(accountId, regionId) {
|
|
706
|
+
const matches = [...this.providers.entries()].filter(([, p]) => p.accountId === accountId && (regionId === undefined || p.regionId === regionId));
|
|
707
|
+
const target = regionId === undefined ? `account "${accountId}"` : `account "${accountId}" and region "${regionId}"`;
|
|
708
|
+
if (matches.length === 0) {
|
|
709
|
+
throw new Error(`No provider registered for ${target}!`);
|
|
710
|
+
}
|
|
711
|
+
// A resource names its provider by account and region only, never by provider type, so that pair
|
|
712
|
+
// has to identify one provider. Deliberate: outside aws the account id is expected to be what tells
|
|
713
|
+
// them apart, which keeps the pair unique in practice, and threading a provider type through
|
|
714
|
+
// every resource binding costs more than the ambiguity is worth. Hence a hint rather than a fix.
|
|
715
|
+
const providerTypes = new Set(matches.map(([, p]) => p.providerType));
|
|
716
|
+
if (regionId !== undefined && providerTypes.size > 1) {
|
|
717
|
+
throw new Error(`Multiple providers registered for ${target}! Cannot derive a unique provider. ` +
|
|
718
|
+
`Give each provider type its own accountId, since a resource selects its provider by ` +
|
|
719
|
+
`account and region alone. Registered here: ${[...providerTypes].sort().join(', ')}.`);
|
|
720
|
+
}
|
|
721
|
+
const [key, provider] = matches[0];
|
|
722
|
+
return { alias: provider.alias, key, providerType: provider.providerType };
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* @internal
|
|
726
|
+
*/
|
|
727
|
+
getProviderAliasRef(moduleId, accountId, regionId) {
|
|
728
|
+
const { alias, key, providerType } = this.findProvider(accountId, regionId);
|
|
729
|
+
this.module(moduleId).providerKeys.add(key);
|
|
730
|
+
return new ExpressionHclNode(`${providerType}.${alias}`);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* @internal
|
|
734
|
+
*/
|
|
735
|
+
getRef(resource, key) {
|
|
736
|
+
const resourceId = resource instanceof MatchingResource
|
|
737
|
+
? resource.getSchemaInstanceInResourceAction().resourceId
|
|
738
|
+
: resource.resourceId;
|
|
739
|
+
return new RefHclNode(resourceId, key);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* @internal
|
|
743
|
+
*/
|
|
744
|
+
jsonencode(subject) {
|
|
745
|
+
return new JsonEncodeHclNode(subject);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Renders a local file path as `"${var.OCTO_PROJECT_ROOT}/<path from the project root>"`, and
|
|
749
|
+
* marks this folder as needing that variable declared.
|
|
750
|
+
*
|
|
751
|
+
* Separators are normalized to `/`, so a folder generated on Windows reads the same as one
|
|
752
|
+
* generated anywhere else.
|
|
753
|
+
*/
|
|
754
|
+
localPath(moduleId, filePath) {
|
|
755
|
+
this.module(moduleId).needsProjectRoot = true;
|
|
756
|
+
const pathFromProjectRoot = relative(process.cwd(), resolve(filePath)).split(sep).join('/');
|
|
757
|
+
return `"\${var.${PROJECT_ROOT_VARIABLE_NAME}}/${pathFromProjectRoot}"`;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* @internal
|
|
761
|
+
*/
|
|
762
|
+
mapAttr(value) {
|
|
763
|
+
return new MapHclNode(Object.entries(value).map(([k, v]) => [k, toHclNode(v)]));
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* @internal
|
|
767
|
+
*/
|
|
768
|
+
module(moduleId) {
|
|
769
|
+
if (!this.modules.has(moduleId)) {
|
|
770
|
+
this.modules.set(moduleId, new TerraformModule(moduleId));
|
|
771
|
+
}
|
|
772
|
+
return this.modules.get(moduleId);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* @internal
|
|
776
|
+
*/
|
|
777
|
+
providerKey(providerType, accountId, regionId) {
|
|
778
|
+
return `${providerType}:${accountId}:${regionId}`;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* @internal
|
|
782
|
+
*/
|
|
783
|
+
raw(value) {
|
|
784
|
+
return new ExpressionHclNode(String(value));
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Prints one folder's `terraform { required_version, required_providers }` config block.
|
|
788
|
+
*/
|
|
789
|
+
renderConfigBlock(requiredProviders, context) {
|
|
790
|
+
const configBlock = new TerraformBlock('terraform');
|
|
791
|
+
configBlock.attribute('required_version', `>= ${this.minTerraformVersion}`);
|
|
792
|
+
const providerTypes = Object.keys(requiredProviders).sort();
|
|
793
|
+
if (providerTypes.length > 0) {
|
|
794
|
+
const requiredProvidersBlock = configBlock.block('required_providers');
|
|
795
|
+
for (const providerType of providerTypes) {
|
|
796
|
+
const entry = requiredProviders[providerType];
|
|
797
|
+
const lines = [`source = "${entry.source}"`];
|
|
798
|
+
if (entry.minVersion) {
|
|
799
|
+
lines.push(`version = ">= ${entry.minVersion}"`);
|
|
800
|
+
}
|
|
801
|
+
requiredProvidersBlock.attribute(providerType, new MapHclNodeRaw(lines));
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return configBlock.render('', context);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Prints one folder's `terragrunt.hcl`: a `dependency` block per producer folder (with
|
|
808
|
+
* `mock_outputs`, shallow-merged over any applied state, so `run-all plan` works both before the
|
|
809
|
+
* first apply and when intent renames a producer resource), and the `inputs` map wiring each auto
|
|
810
|
+
* variable to the producer's output.
|
|
811
|
+
*/
|
|
812
|
+
renderTerragrunt(moduleWiring) {
|
|
813
|
+
const step = this.step;
|
|
814
|
+
const sections = [];
|
|
815
|
+
// Pin the (local) backend state to a stable absolute path in the module folder. Terragrunt
|
|
816
|
+
// copies each module into a per-invocation `.terragrunt-cache` working directory; without this,
|
|
817
|
+
// local state lands inside that throwaway copy, so a later `terragrunt output` (run from a fresh
|
|
818
|
+
// copy) cannot see the state a prior `terragrunt apply` wrote. Generating a `backend.tf` that
|
|
819
|
+
// points at `${get_terragrunt_dir()}/terraform.tfstate` keeps the state in one fixed place that
|
|
820
|
+
// every terragrunt command shares. Users who want a remote backend override this block.
|
|
821
|
+
sections.push([
|
|
822
|
+
'remote_state {',
|
|
823
|
+
`${step}backend = "local"`,
|
|
824
|
+
`${step}generate = {`,
|
|
825
|
+
`${step}${step}path = "backend.tf"`,
|
|
826
|
+
`${step}${step}if_exists = "overwrite_terragrunt"`,
|
|
827
|
+
`${step}}`,
|
|
828
|
+
`${step}config = {`,
|
|
829
|
+
`${step}${step}path = "\${get_terragrunt_dir()}/terraform.tfstate"`,
|
|
830
|
+
`${step}}`,
|
|
831
|
+
'}',
|
|
832
|
+
].join('\n'));
|
|
833
|
+
// Dependency blocks, with mock outputs so `run-all plan` works before first apply.
|
|
834
|
+
for (const producerModuleId of [...moduleWiring.moduleDependencies.keys()].sort()) {
|
|
835
|
+
const dependencyName = StringUtility.sanitizeForIdentifier(producerModuleId);
|
|
836
|
+
// Map each consumed output name to its mock shape: a flat string for per-key scalar outputs,
|
|
837
|
+
// or an object (keyed by the accessed fields) for an external producer's entire-response output.
|
|
838
|
+
const mockOutputs = new Map();
|
|
839
|
+
for (const v of moduleWiring.autoVariables.values()) {
|
|
840
|
+
if (v.producerModuleId !== producerModuleId) {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
if (v.objectKeys) {
|
|
844
|
+
const responseKeys = mockOutputs.get(v.outputName)?.responseKeys ?? new Set();
|
|
845
|
+
for (const key of v.objectKeys) {
|
|
846
|
+
responseKeys.add(key);
|
|
847
|
+
}
|
|
848
|
+
mockOutputs.set(v.outputName, { producerResourceId: v.producerResourceId, responseKeys });
|
|
849
|
+
}
|
|
850
|
+
else if (!mockOutputs.has(v.outputName)) {
|
|
851
|
+
mockOutputs.set(v.outputName, { producerResourceId: v.producerResourceId, responseKey: v.responseKey });
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// Prefer the producer schema's declared default value (an author-written, non-secret
|
|
855
|
+
// placeholder, e.g. a syntactically valid ARN) over a synthetic placeholder, falling back to
|
|
856
|
+
// `mock-*` when the schema declares no default for that response key.
|
|
857
|
+
const mockValue = (producerResourceId, responseKey, fallback) => {
|
|
858
|
+
const value = this.resourceRegistry.get(producerResourceId)?.responseDefaults[responseKey];
|
|
859
|
+
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
|
860
|
+
};
|
|
861
|
+
const lines = [`dependency "${dependencyName}" {`, `${step}config_path = "../${producerModuleId}"`];
|
|
862
|
+
if (mockOutputs.size > 0) {
|
|
863
|
+
lines.push('');
|
|
864
|
+
lines.push(`${step}mock_outputs = {`);
|
|
865
|
+
for (const outputName of [...mockOutputs.keys()].sort()) {
|
|
866
|
+
const { producerResourceId, responseKey, responseKeys } = mockOutputs.get(outputName);
|
|
867
|
+
if (responseKeys) {
|
|
868
|
+
const fields = [...responseKeys]
|
|
869
|
+
.sort()
|
|
870
|
+
.map((key) => `${key} = "${mockValue(producerResourceId, key, `mock-${outputName}-${key}`)}"`);
|
|
871
|
+
lines.push(`${step}${step}"${outputName}" = { ${fields.join(', ')} }`);
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
lines.push(`${step}${step}"${outputName}" = "${mockValue(producerResourceId, responseKey, `mock-${outputName}`)}"`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
lines.push(`${step}}`);
|
|
878
|
+
lines.push(`${step}mock_outputs_allowed_terraform_commands = ["init", "plan", "show", "validate"]`);
|
|
879
|
+
lines.push(`${step}mock_outputs_merge_strategy_with_state = "shallow"`);
|
|
880
|
+
}
|
|
881
|
+
lines.push('}');
|
|
882
|
+
sections.push(lines.join('\n'));
|
|
883
|
+
}
|
|
884
|
+
// Inputs wiring.
|
|
885
|
+
const inputEntries = [...moduleWiring.autoVariables.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
886
|
+
if (inputEntries.length > 0) {
|
|
887
|
+
const lines = ['inputs = {'];
|
|
888
|
+
for (const [variableName, { outputName, producerModuleId }] of inputEntries) {
|
|
889
|
+
lines.push(`${step}${variableName} = dependency.${StringUtility.sanitizeForIdentifier(producerModuleId)}.outputs["${outputName}"]`);
|
|
890
|
+
}
|
|
891
|
+
lines.push('}');
|
|
892
|
+
sections.push(lines.join('\n'));
|
|
893
|
+
}
|
|
894
|
+
return sections.join('\n\n');
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* @internal
|
|
898
|
+
*/
|
|
899
|
+
type(schema) {
|
|
900
|
+
return new TypeHclNode(schema);
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* @internal
|
|
904
|
+
*/
|
|
905
|
+
variable(moduleId, name, typeExpression, options) {
|
|
906
|
+
const resolvedType = typeof typeExpression === 'string' && !['any', 'bool', 'number', 'string'].includes(typeExpression)
|
|
907
|
+
? new ExpressionHclNode(typeExpression)
|
|
908
|
+
: new TypeHclNode(typeExpression);
|
|
909
|
+
const variable = new TerraformVariable(name, resolvedType, options);
|
|
910
|
+
this.module(moduleId).variables.push(variable);
|
|
911
|
+
return variable;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Resolves the deferred parent-input wiring for every external resource: for each parent, expands
|
|
915
|
+
* its declared outputs into `--input` arguments on the `local-exec` create/destroy commands and
|
|
916
|
+
* into individual triggers (so the `when = destroy` provisioner can reach the raw values via
|
|
917
|
+
* `self.triggers`). Runs once all resources are registered, so it is independent of the order in
|
|
918
|
+
* which resources were contributed. Idempotent: clears the queue, so a repeat call is a no-op.
|
|
919
|
+
*/
|
|
920
|
+
wireExternalResourceInputs() {
|
|
921
|
+
for (const { nullResource, octoResource, resourceId } of this.pendingExternalResourcesInputWiring) {
|
|
922
|
+
const parents = (octoResource.parents || []).map((p) => p instanceof MatchingResource ? p.getActual() : p);
|
|
923
|
+
// A native parent contributes one input per output key; an external parent its whole result as a single input.
|
|
924
|
+
const inputs = [];
|
|
925
|
+
for (const parent of parents) {
|
|
926
|
+
const parentOctoResource = this.resourceRegistry.get(parent.resourceId);
|
|
927
|
+
if (!parentOctoResource) {
|
|
928
|
+
throw new Error(`Parent resource "${parent.resourceId}" of external resource "${resourceId}" is not registered with Terraform!`);
|
|
929
|
+
}
|
|
930
|
+
if (parentOctoResource.externalResultExpression !== undefined) {
|
|
931
|
+
inputs.push({
|
|
932
|
+
argKey: parent.resourceId,
|
|
933
|
+
ref: new RefHclNode(parent.resourceId, '', true),
|
|
934
|
+
triggerKey: StringUtility.sanitizeForEnvironmentVariable(`input_${parent.resourceId}`),
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
for (const key of parentOctoResource.outputs.keys()) {
|
|
939
|
+
inputs.push({
|
|
940
|
+
argKey: `${parent.resourceId}.${key}`,
|
|
941
|
+
ref: new RefHclNode(parent.resourceId, key),
|
|
942
|
+
triggerKey: StringUtility.sanitizeForEnvironmentVariable(`input_${parent.resourceId}_${key}`),
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const createCommand = [
|
|
948
|
+
`${this.octoCommandPrefix} run-action --resourceId=${resourceId}`,
|
|
949
|
+
];
|
|
950
|
+
const destroyCommand = [
|
|
951
|
+
`${this.octoCommandPrefix} run-action --resourceId=${resourceId}`,
|
|
952
|
+
];
|
|
953
|
+
for (const input of inputs) {
|
|
954
|
+
createCommand.push(` --input ${input.argKey}=`, input.ref);
|
|
955
|
+
destroyCommand.push(` --input ${input.argKey}=\${self.triggers.${input.triggerKey}}`);
|
|
956
|
+
}
|
|
957
|
+
const triggers = [
|
|
958
|
+
[
|
|
959
|
+
'octo_properties_hash',
|
|
960
|
+
new LiteralHclNode(StringUtility.deterministicHash(octoResource.getExternalTriggerProperties())),
|
|
961
|
+
],
|
|
962
|
+
...inputs.map((i) => [i.triggerKey, new InterpolatedStringHclNode([i.ref])]),
|
|
963
|
+
];
|
|
964
|
+
nullResource.attribute('triggers', new MapHclNode(triggers));
|
|
965
|
+
const createProvisioner = nullResource.block('provisioner "local-exec"');
|
|
966
|
+
createProvisioner.attribute('command', new InterpolatedStringHclNode(createCommand));
|
|
967
|
+
const destroyProvisioner = nullResource.block('provisioner "local-exec"');
|
|
968
|
+
destroyProvisioner.attribute('when', new ExpressionHclNode('destroy'));
|
|
969
|
+
destroyProvisioner.attribute('command', new InterpolatedStringHclNode(destroyCommand));
|
|
970
|
+
}
|
|
971
|
+
this.pendingExternalResourcesInputWiring.length = 0;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* @internal
|
|
976
|
+
*/
|
|
977
|
+
let TerraformServiceFactory = class TerraformServiceFactory {
|
|
978
|
+
/**
|
|
979
|
+
* @internal
|
|
980
|
+
*/
|
|
981
|
+
static async create({ octoCommandPrefix = 'octo', terraformFileIndentLength = 2, } = {}, forceNew = false) {
|
|
982
|
+
if (!this.instance) {
|
|
983
|
+
this.instance = new TerraformService(octoCommandPrefix, terraformFileIndentLength);
|
|
984
|
+
}
|
|
985
|
+
if (forceNew) {
|
|
986
|
+
this.instance.reset();
|
|
987
|
+
}
|
|
988
|
+
return this.instance;
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
TerraformServiceFactory = __decorate([
|
|
992
|
+
Factory(TerraformService)
|
|
993
|
+
], TerraformServiceFactory);
|
|
994
|
+
export { TerraformServiceFactory };
|
|
995
|
+
//# sourceMappingURL=terraform.service.js.map
|