@aws/nx-plugin 0.109.0 → 0.110.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/package.json +1 -1
- package/src/preset/__snapshots__/generator.spec.ts.snap +11 -1
- package/src/preset/generator.js +12 -3
- package/src/preset/generator.js.map +1 -1
- package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +184 -106
- package/src/py/lambda-function/__snapshots__/generator.spec.ts.snap +74 -18
- package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +11 -9
- package/src/py/strands-agent/__snapshots__/generator.spec.ts.snap +11 -9
- package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +114 -62
- package/src/terraform/project/files/application/scripts/aws-config.ts.template +41 -0
- package/src/terraform/project/files/application/scripts/bootstrap.ts.template +91 -0
- package/src/terraform/project/files/application/scripts/init.ts.template +44 -0
- package/src/terraform/project/generator.js +16 -11
- package/src/terraform/project/generator.js.map +1 -1
- package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +175 -97
- package/src/ts/lambda-function/__snapshots__/generator.spec.ts.snap +74 -18
- package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +11 -9
- package/src/ts/react-website/agui/generator.js +7 -0
- package/src/ts/react-website/agui/generator.js.map +1 -1
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +7 -0
- package/src/ts/strands-agent/__snapshots__/generator.spec.ts.snap +11 -9
- package/src/utils/agent-core-constructs/files/terraform/app/agent-core/__nameKebabCase__/__nameKebabCase__.tf.template +14 -12
- package/src/utils/api-constructs/files/terraform/app/apis/http/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +30 -17
- package/src/utils/api-constructs/files/terraform/app/apis/rest/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +32 -19
- package/src/utils/api-constructs/files/terraform/core/api/http/http-api/http-api.tf.template +2 -2
- package/src/utils/files/terraform/scripts/reset-runtime-config.ts.template +25 -0
- package/src/utils/files/terraform/src/core/asset-bucket/asset-bucket.tf.template +203 -0
- package/src/utils/files/terraform/src/core/runtime-config/appconfig/appconfig.tf.template +42 -39
- package/src/utils/files/terraform/src/core/runtime-config/appconfig-deployment/appconfig-deployment.tf.template +138 -0
- package/src/utils/files/terraform/src/core/runtime-config/entry/entry.tf.template +26 -90
- package/src/utils/files/terraform/src/core/runtime-config/read/read.tf.template +61 -5
- package/src/utils/function-constructs/files/terraform/app/lambda-functions/__functionNameKebabCase__/__functionNameKebabCase__.tf.template +36 -8
- package/src/utils/identity-constructs/files/terraform/core/user-identity/identity/identity.tf.template +6 -6
- package/src/utils/pnpm-workspace.d.ts +22 -0
- package/src/utils/pnpm-workspace.js +58 -0
- package/src/utils/pnpm-workspace.js.map +1 -0
- package/src/utils/shared-constructs.js +4 -2
- package/src/utils/shared-constructs.js.map +1 -1
- package/src/utils/versions.d.ts +4 -1
- package/src/utils/versions.js +3 -0
- package/src/utils/versions.js.map +1 -1
- package/src/utils/website-constructs/files/terraform/core/static-website/static-website.tf.template +7 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clears the runtime-config dist dir before every build so that removed
|
|
3
|
+
* modules don't leave behind stale entries in `connection.json` /
|
|
4
|
+
* `agentcore.json` / per-entry leaf files.
|
|
5
|
+
*
|
|
6
|
+
* Invoked by the `reset-runtime-config` nx target, which the `build`
|
|
7
|
+
* target depends on.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
const distDir = process.env.DIST_DIR;
|
|
13
|
+
if (!distDir) {
|
|
14
|
+
throw new Error('DIST_DIR environment variable is required');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// `runtime-config.json` starts as `{}` so the reader Terraform module can
|
|
18
|
+
// assume the file exists.
|
|
19
|
+
mkdirSync(distDir, { recursive: true });
|
|
20
|
+
writeFileSync(join(distDir, 'runtime-config.json'), '{}');
|
|
21
|
+
|
|
22
|
+
// Clear and recreate the per-entry leaf-file tree.
|
|
23
|
+
const runtimeConfigDir = join(distDir, 'runtime-config');
|
|
24
|
+
rmSync(runtimeConfigDir, { recursive: true, force: true });
|
|
25
|
+
mkdirSync(join(runtimeConfigDir, 'entries'), { recursive: true });
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
terraform {
|
|
2
|
+
required_version = ">= 1.0"
|
|
3
|
+
|
|
4
|
+
required_providers {
|
|
5
|
+
aws = {
|
|
6
|
+
source = "hashicorp/aws"
|
|
7
|
+
version = "~> 6.0"
|
|
8
|
+
}
|
|
9
|
+
random = {
|
|
10
|
+
source = "hashicorp/random"
|
|
11
|
+
version = "~> 3.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
variable "bucket_name_prefix" {
|
|
17
|
+
description = "Optional prefix to apply to the generated bucket name. Useful when you want a stable, human-recognisable identifier in addition to the account/region/random suffix."
|
|
18
|
+
type = string
|
|
19
|
+
default = "assets"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
variable "tags" {
|
|
23
|
+
description = "Tags to apply to all resources"
|
|
24
|
+
type = map(string)
|
|
25
|
+
default = {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
data "aws_caller_identity" "current" {}
|
|
29
|
+
data "aws_region" "current" {}
|
|
30
|
+
|
|
31
|
+
resource "random_string" "suffix" {
|
|
32
|
+
length = 8
|
|
33
|
+
special = false
|
|
34
|
+
upper = false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Access logs bucket for the asset bucket.
|
|
38
|
+
resource "aws_s3_bucket" "access_logs" {
|
|
39
|
+
#checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for access log bucket
|
|
40
|
+
#checkov:skip=CKV_AWS_144:Cross-region replication not required for access log bucket
|
|
41
|
+
#checkov:skip=CKV2_AWS_62:Event notifications not required for access log bucket
|
|
42
|
+
#checkov:skip=CKV_AWS_18:Access logging the access log bucket would create a cycle
|
|
43
|
+
#checkov:skip=CKV_AWS_145:AES256 (S3-managed) encryption is sufficient for access logs
|
|
44
|
+
bucket = "${var.bucket_name_prefix}-logs-${data.aws_caller_identity.current.account_id}-${data.aws_region.current.region}-${random_string.suffix.result}"
|
|
45
|
+
force_destroy = true
|
|
46
|
+
|
|
47
|
+
tags = var.tags
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
resource "aws_s3_bucket_versioning" "access_logs" {
|
|
51
|
+
bucket = aws_s3_bucket.access_logs.id
|
|
52
|
+
versioning_configuration {
|
|
53
|
+
status = "Enabled"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
resource "aws_s3_bucket_server_side_encryption_configuration" "access_logs" {
|
|
58
|
+
bucket = aws_s3_bucket.access_logs.id
|
|
59
|
+
|
|
60
|
+
rule {
|
|
61
|
+
apply_server_side_encryption_by_default {
|
|
62
|
+
sse_algorithm = "AES256"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
resource "aws_s3_bucket_public_access_block" "access_logs" {
|
|
68
|
+
bucket = aws_s3_bucket.access_logs.id
|
|
69
|
+
|
|
70
|
+
block_public_acls = true
|
|
71
|
+
block_public_policy = true
|
|
72
|
+
ignore_public_acls = true
|
|
73
|
+
restrict_public_buckets = true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resource "aws_s3_bucket_policy" "access_logs" {
|
|
77
|
+
bucket = aws_s3_bucket.access_logs.id
|
|
78
|
+
|
|
79
|
+
policy = jsonencode({
|
|
80
|
+
Version = "2012-10-17"
|
|
81
|
+
Statement = [
|
|
82
|
+
{
|
|
83
|
+
Sid = "DenyInsecureConnections"
|
|
84
|
+
Effect = "Deny"
|
|
85
|
+
Principal = "*"
|
|
86
|
+
Action = "s3:*"
|
|
87
|
+
Resource = [
|
|
88
|
+
aws_s3_bucket.access_logs.arn,
|
|
89
|
+
"${aws_s3_bucket.access_logs.arn}/*"
|
|
90
|
+
]
|
|
91
|
+
Condition = {
|
|
92
|
+
Bool = {
|
|
93
|
+
"aws:SecureTransport" = "false"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
Sid = "AllowS3LogDelivery"
|
|
99
|
+
Effect = "Allow"
|
|
100
|
+
Principal = {
|
|
101
|
+
Service = "logging.s3.amazonaws.com"
|
|
102
|
+
}
|
|
103
|
+
Action = "s3:PutObject"
|
|
104
|
+
Resource = "${aws_s3_bucket.access_logs.arn}/*"
|
|
105
|
+
Condition = {
|
|
106
|
+
StringEquals = {
|
|
107
|
+
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Shared asset bucket
|
|
116
|
+
resource "aws_s3_bucket" "assets" {
|
|
117
|
+
#checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for transient build artefacts
|
|
118
|
+
#checkov:skip=CKV_AWS_144:Cross-region replication not required for asset bucket
|
|
119
|
+
#checkov:skip=CKV2_AWS_62:Event notifications not required for asset bucket
|
|
120
|
+
#checkov:skip=CKV_AWS_145:AES256 (S3-managed) encryption is sufficient for build artefacts
|
|
121
|
+
bucket = "${var.bucket_name_prefix}-${data.aws_caller_identity.current.account_id}-${data.aws_region.current.region}-${random_string.suffix.result}"
|
|
122
|
+
force_destroy = true
|
|
123
|
+
|
|
124
|
+
tags = var.tags
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
resource "aws_s3_bucket_versioning" "assets" {
|
|
128
|
+
bucket = aws_s3_bucket.assets.id
|
|
129
|
+
versioning_configuration {
|
|
130
|
+
status = "Enabled"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
resource "aws_s3_bucket_server_side_encryption_configuration" "assets" {
|
|
135
|
+
bucket = aws_s3_bucket.assets.id
|
|
136
|
+
|
|
137
|
+
rule {
|
|
138
|
+
apply_server_side_encryption_by_default {
|
|
139
|
+
sse_algorithm = "AES256"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
resource "aws_s3_bucket_public_access_block" "assets" {
|
|
145
|
+
bucket = aws_s3_bucket.assets.id
|
|
146
|
+
|
|
147
|
+
block_public_acls = true
|
|
148
|
+
block_public_policy = true
|
|
149
|
+
ignore_public_acls = true
|
|
150
|
+
restrict_public_buckets = true
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
resource "aws_s3_bucket_logging" "assets" {
|
|
154
|
+
bucket = aws_s3_bucket.assets.id
|
|
155
|
+
|
|
156
|
+
target_bucket = aws_s3_bucket.access_logs.id
|
|
157
|
+
target_prefix = "s3-access-logs/"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resource "aws_s3_bucket_policy" "assets" {
|
|
161
|
+
bucket = aws_s3_bucket.assets.id
|
|
162
|
+
|
|
163
|
+
policy = jsonencode({
|
|
164
|
+
Version = "2012-10-17"
|
|
165
|
+
Statement = [
|
|
166
|
+
{
|
|
167
|
+
Sid = "DenyInsecureConnections"
|
|
168
|
+
Effect = "Deny"
|
|
169
|
+
Principal = "*"
|
|
170
|
+
Action = "s3:*"
|
|
171
|
+
Resource = [
|
|
172
|
+
aws_s3_bucket.assets.arn,
|
|
173
|
+
"${aws_s3_bucket.assets.arn}/*"
|
|
174
|
+
]
|
|
175
|
+
Condition = {
|
|
176
|
+
Bool = {
|
|
177
|
+
"aws:SecureTransport" = "false"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
output "bucket_name" {
|
|
186
|
+
description = "Name of the shared asset bucket — pass to app modules' `asset_bucket_name` input."
|
|
187
|
+
value = aws_s3_bucket.assets.id
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
output "bucket_arn" {
|
|
191
|
+
description = "ARN of the shared asset bucket"
|
|
192
|
+
value = aws_s3_bucket.assets.arn
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
output "access_logs_bucket_name" {
|
|
196
|
+
description = "Name of the access-logs bucket that receives S3 access logs for the asset bucket."
|
|
197
|
+
value = aws_s3_bucket.access_logs.id
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
output "access_logs_bucket_arn" {
|
|
201
|
+
description = "ARN of the access-logs bucket"
|
|
202
|
+
value = aws_s3_bucket.access_logs.arn
|
|
203
|
+
}
|
|
@@ -6,36 +6,48 @@ terraform {
|
|
|
6
6
|
source = "hashicorp/aws"
|
|
7
7
|
version = ">= 5.0"
|
|
8
8
|
}
|
|
9
|
-
local = {
|
|
10
|
-
source = "hashicorp/local"
|
|
11
|
-
version = "~> 2.0"
|
|
12
|
-
}
|
|
13
9
|
}
|
|
14
10
|
}
|
|
15
11
|
|
|
12
|
+
# Shared AppConfig application — call this module once per deployment and
|
|
13
|
+
# thread its outputs into every API / agent / MCP / lambda module that
|
|
14
|
+
# contributes a runtime-config entry. Aggregation and deployment of the
|
|
15
|
+
# collected entries happens in the sibling `appconfig-deployment` module,
|
|
16
|
+
# which you call once at the end of the root module with `depends_on`
|
|
17
|
+
# covering every entry-contributing module.
|
|
18
|
+
|
|
16
19
|
variable "application_name" {
|
|
17
20
|
description = "Name of the AppConfig application"
|
|
18
21
|
type = string
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
variable "namespaces" {
|
|
25
|
+
description = "List of runtime-config namespaces this AppConfig application should expose (one Configuration Profile is created per namespace)."
|
|
26
|
+
type = list(string)
|
|
27
|
+
default = ["connection", "agentcore"]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
variable "deployment_strategy_id" {
|
|
31
|
+
description = "Optional AppConfig Deployment Strategy ID to use. When null (the default), this module provisions a zero-wait strategy so deployed config is visible to consumers as soon as terraform apply completes. Pass an existing strategy ID (e.g. `AppConfig.AllAtOnce` or a custom one) to reuse it across multiple AppConfig applications."
|
|
32
|
+
type = string
|
|
33
|
+
default = null
|
|
23
34
|
}
|
|
24
35
|
|
|
25
|
-
# AppConfig Application
|
|
26
36
|
resource "aws_appconfig_application" "runtime_config" {
|
|
27
37
|
name = var.application_name
|
|
28
38
|
description = "Runtime configuration for ${var.application_name}"
|
|
29
39
|
}
|
|
30
40
|
|
|
31
|
-
# AppConfig Environment
|
|
32
41
|
resource "aws_appconfig_environment" "default" {
|
|
33
42
|
name = "default"
|
|
34
43
|
application_id = aws_appconfig_application.runtime_config.id
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
# Instant deployment strategy
|
|
46
|
+
# Instant (zero-wait) deployment strategy — only provisioned when the
|
|
47
|
+
# caller doesn't pass a shared `deployment_strategy_id`.
|
|
38
48
|
resource "aws_appconfig_deployment_strategy" "instant" {
|
|
49
|
+
count = var.deployment_strategy_id == null ? 1 : 0
|
|
50
|
+
|
|
39
51
|
name = "${var.application_name}-instant"
|
|
40
52
|
deployment_duration_in_minutes = 0
|
|
41
53
|
growth_factor = 100
|
|
@@ -43,18 +55,9 @@ resource "aws_appconfig_deployment_strategy" "instant" {
|
|
|
43
55
|
final_bake_time_in_minutes = 0
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
# Read all namespace JSON files from the config directory
|
|
47
|
-
locals {
|
|
48
|
-
namespace_files = fileset(local.config_dir, "*.json")
|
|
49
|
-
namespaces = {
|
|
50
|
-
for f in local.namespace_files :
|
|
51
|
-
trimsuffix(f, ".json") => jsondecode(file("${local.config_dir}/${f}"))
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
58
|
# Configuration Profile per namespace
|
|
56
59
|
resource "aws_appconfig_configuration_profile" "namespace" {
|
|
57
|
-
for_each =
|
|
60
|
+
for_each = toset(var.namespaces)
|
|
58
61
|
|
|
59
62
|
application_id = aws_appconfig_application.runtime_config.id
|
|
60
63
|
name = each.key
|
|
@@ -62,28 +65,13 @@ resource "aws_appconfig_configuration_profile" "namespace" {
|
|
|
62
65
|
type = "AWS.Freeform"
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
configuration_profile_id = aws_appconfig_configuration_profile.namespace[each.key].configuration_profile_id
|
|
71
|
-
content_type = "application/json"
|
|
72
|
-
content = jsonencode(each.value)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
# Deployment per namespace
|
|
76
|
-
resource "aws_appconfig_deployment" "namespace" {
|
|
77
|
-
for_each = local.namespaces
|
|
78
|
-
|
|
79
|
-
application_id = aws_appconfig_application.runtime_config.id
|
|
80
|
-
environment_id = aws_appconfig_environment.default.environment_id
|
|
81
|
-
configuration_profile_id = aws_appconfig_configuration_profile.namespace[each.key].configuration_profile_id
|
|
82
|
-
configuration_version = aws_appconfig_hosted_configuration_version.namespace[each.key].version_number
|
|
83
|
-
deployment_strategy_id = aws_appconfig_deployment_strategy.instant.id
|
|
68
|
+
locals {
|
|
69
|
+
resolved_deployment_strategy_id = coalesce(
|
|
70
|
+
var.deployment_strategy_id,
|
|
71
|
+
try(aws_appconfig_deployment_strategy.instant[0].id, null),
|
|
72
|
+
)
|
|
84
73
|
}
|
|
85
74
|
|
|
86
|
-
# Outputs
|
|
87
75
|
output "application_id" {
|
|
88
76
|
description = "AppConfig Application ID"
|
|
89
77
|
value = aws_appconfig_application.runtime_config.id
|
|
@@ -98,3 +86,18 @@ output "environment_id" {
|
|
|
98
86
|
description = "AppConfig Environment ID"
|
|
99
87
|
value = aws_appconfig_environment.default.environment_id
|
|
100
88
|
}
|
|
89
|
+
|
|
90
|
+
output "deployment_strategy_id" {
|
|
91
|
+
description = "AppConfig Deployment Strategy ID — either the one supplied via `var.deployment_strategy_id` or the zero-wait strategy provisioned by this module."
|
|
92
|
+
value = local.resolved_deployment_strategy_id
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
output "configuration_profile_ids" {
|
|
96
|
+
description = "Map of namespace to Configuration Profile ID."
|
|
97
|
+
value = { for k, p in aws_appconfig_configuration_profile.namespace : k => p.configuration_profile_id }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
output "namespaces" {
|
|
101
|
+
description = "Passthrough of the namespaces configured on this application."
|
|
102
|
+
value = var.namespaces
|
|
103
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
terraform {
|
|
2
|
+
required_version = ">= 1.0"
|
|
3
|
+
|
|
4
|
+
required_providers {
|
|
5
|
+
aws = {
|
|
6
|
+
source = "hashicorp/aws"
|
|
7
|
+
version = ">= 5.0"
|
|
8
|
+
}
|
|
9
|
+
local = {
|
|
10
|
+
source = "hashicorp/local"
|
|
11
|
+
version = "~> 2.0"
|
|
12
|
+
}
|
|
13
|
+
null = {
|
|
14
|
+
source = "hashicorp/null"
|
|
15
|
+
version = ">= 3.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
variable "application_id" {
|
|
21
|
+
description = "AppConfig application ID (from `core/runtime-config/appconfig.application_id`)."
|
|
22
|
+
type = string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
variable "environment_id" {
|
|
26
|
+
description = "AppConfig environment ID (from `core/runtime-config/appconfig.environment_id`)."
|
|
27
|
+
type = string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
variable "deployment_strategy_id" {
|
|
31
|
+
description = "AppConfig deployment strategy ID (from `core/runtime-config/appconfig.deployment_strategy_id`)."
|
|
32
|
+
type = string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
variable "configuration_profile_ids" {
|
|
36
|
+
description = "Map of namespace to Configuration Profile ID (from `core/runtime-config/appconfig.configuration_profile_ids`)."
|
|
37
|
+
type = map(string)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
variable "namespaces" {
|
|
41
|
+
description = "List of namespaces to aggregate + deploy. Must match the keys of `configuration_profile_ids`."
|
|
42
|
+
type = list(string)
|
|
43
|
+
default = ["connection", "agentcore"]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
locals {
|
|
47
|
+
config_dir = "${path.module}/../../../../../../../dist/packages/common/terraform/runtime-config"
|
|
48
|
+
entries_dir = "${local.config_dir}/entries"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Aggregate the per-entry leaf JSON files into a single JSON file per
|
|
52
|
+
# namespace. Runs at apply time so every contributor's entry file is
|
|
53
|
+
# guaranteed to be on disk when aggregation happens.
|
|
54
|
+
resource "null_resource" "aggregate_namespace" {
|
|
55
|
+
for_each = toset(var.namespaces)
|
|
56
|
+
|
|
57
|
+
triggers = {
|
|
58
|
+
namespace = each.key
|
|
59
|
+
# Re-run the aggregation every apply so newly-written entries are
|
|
60
|
+
# always picked up. The local-exec is idempotent.
|
|
61
|
+
always = timestamp()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
provisioner "local-exec" {
|
|
65
|
+
command = <<-EOT
|
|
66
|
+
uv run python -c "
|
|
67
|
+
import json
|
|
68
|
+
import pathlib
|
|
69
|
+
import sys
|
|
70
|
+
|
|
71
|
+
namespace = '${each.key}'
|
|
72
|
+
entries_dir = pathlib.Path('${local.entries_dir}') / namespace
|
|
73
|
+
output_file = pathlib.Path('${local.config_dir}') / f'{namespace}.json'
|
|
74
|
+
|
|
75
|
+
def deep_merge(target: dict, source: dict) -> dict:
|
|
76
|
+
for k, v in source.items():
|
|
77
|
+
if k in target and isinstance(target[k], dict) and isinstance(v, dict):
|
|
78
|
+
deep_merge(target[k], v)
|
|
79
|
+
else:
|
|
80
|
+
target[k] = v
|
|
81
|
+
return target
|
|
82
|
+
|
|
83
|
+
# Each entry file is named '<key>-<sha>.json' — callers that share the
|
|
84
|
+
# same (namespace, key) pair hash their value into a distinct sha suffix,
|
|
85
|
+
# and their contributions are deep-merged back together under 'key' here.
|
|
86
|
+
merged: dict = {}
|
|
87
|
+
if entries_dir.is_dir():
|
|
88
|
+
for entry_path in sorted(entries_dir.glob('*.json')):
|
|
89
|
+
section = entry_path.stem.rsplit('-', 1)[0]
|
|
90
|
+
try:
|
|
91
|
+
contribution = json.loads(entry_path.read_text())
|
|
92
|
+
except json.JSONDecodeError as e:
|
|
93
|
+
print(f'Skipping malformed entry {entry_path}: {e}', file=sys.stderr)
|
|
94
|
+
continue
|
|
95
|
+
existing = merged.get(section)
|
|
96
|
+
if isinstance(existing, dict) and isinstance(contribution, dict):
|
|
97
|
+
deep_merge(existing, contribution)
|
|
98
|
+
else:
|
|
99
|
+
merged[section] = contribution
|
|
100
|
+
|
|
101
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
output_file.write_text(json.dumps(merged, indent=2))
|
|
103
|
+
print(f'Aggregated {namespace}.json with {len(merged)} section(s)')
|
|
104
|
+
"
|
|
105
|
+
EOT
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
data "local_file" "namespace_content" {
|
|
110
|
+
for_each = toset(var.namespaces)
|
|
111
|
+
|
|
112
|
+
filename = "${local.config_dir}/${each.key}.json"
|
|
113
|
+
depends_on = [null_resource.aggregate_namespace]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
resource "aws_appconfig_hosted_configuration_version" "namespace" {
|
|
117
|
+
for_each = toset(var.namespaces)
|
|
118
|
+
|
|
119
|
+
application_id = var.application_id
|
|
120
|
+
configuration_profile_id = var.configuration_profile_ids[each.key]
|
|
121
|
+
content_type = "application/json"
|
|
122
|
+
content = data.local_file.namespace_content[each.key].content
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
resource "aws_appconfig_deployment" "namespace" {
|
|
126
|
+
for_each = toset(var.namespaces)
|
|
127
|
+
|
|
128
|
+
application_id = var.application_id
|
|
129
|
+
environment_id = var.environment_id
|
|
130
|
+
configuration_profile_id = var.configuration_profile_ids[each.key]
|
|
131
|
+
configuration_version = aws_appconfig_hosted_configuration_version.namespace[each.key].version_number
|
|
132
|
+
deployment_strategy_id = var.deployment_strategy_id
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
output "hosted_configuration_version_numbers" {
|
|
136
|
+
description = "Map of namespace to hosted configuration version number."
|
|
137
|
+
value = { for k, v in aws_appconfig_hosted_configuration_version.namespace : k => v.version_number }
|
|
138
|
+
}
|
|
@@ -15,107 +15,43 @@ variable "namespace" {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
variable "key" {
|
|
18
|
-
description = "
|
|
18
|
+
description = "Section name within the namespace (e.g., 'apis', 'cognitoProps'). Multiple `entry` modules may share the same key — the `appconfig` / `read` aggregation deep-merges their contributions under that key."
|
|
19
19
|
type = string
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
variable "value" {
|
|
23
|
-
description = "Value to set at the key"
|
|
23
|
+
description = "Value to set at the key. When the value is an object, its fields are deep-merged with any other `entry` module that targets the same (namespace, key) pair."
|
|
24
24
|
type = any
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
locals {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
entries_dir = "${path.module}/../../../../../../../dist/packages/common/terraform/runtime-config/entries"
|
|
29
|
+
# A per-entry SHA256 so two modules writing to the same (namespace, key)
|
|
30
|
+
# produce distinct leaf files — the `appconfig-deployment` / `read`
|
|
31
|
+
# modules deep-merge them back together at apply time.
|
|
32
|
+
entry_id = sha256(jsonencode({ key = var.key, value = var.value }))
|
|
33
|
+
entry_file_path = "${local.entries_dir}/${var.namespace}/${var.key}-${local.entry_id}.json"
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
import json
|
|
39
|
-
import sys
|
|
40
|
-
import os
|
|
41
|
-
import time
|
|
42
|
-
import fcntl
|
|
43
|
-
from pathlib import Path
|
|
44
|
-
|
|
45
|
-
# Read input from Terraform
|
|
46
|
-
input_data = json.load(sys.stdin)
|
|
47
|
-
config_file = input_data['config_file']
|
|
48
|
-
key = input_data['key']
|
|
49
|
-
value = json.loads(input_data['value'])
|
|
50
|
-
|
|
51
|
-
# Create lock file path
|
|
52
|
-
lock_file = config_file + '.lock'
|
|
53
|
-
|
|
54
|
-
# Ensure directory exists
|
|
55
|
-
Path(config_file).parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
-
|
|
57
|
-
# Acquire exclusive lock with retry mechanism
|
|
58
|
-
max_retries = 30
|
|
59
|
-
retry_delay = 1.0
|
|
60
|
-
|
|
61
|
-
for attempt in range(max_retries):
|
|
62
|
-
try:
|
|
63
|
-
# Open lock file for exclusive access
|
|
64
|
-
with open(lock_file, 'w') as lock_fd:
|
|
65
|
-
# Try to acquire exclusive lock (non-blocking)
|
|
66
|
-
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
67
|
-
|
|
68
|
-
# Critical section - file operations under lock
|
|
69
|
-
try:
|
|
70
|
-
# Create empty base config if file doesn't exist
|
|
71
|
-
if not os.path.exists(config_file):
|
|
72
|
-
base_config = {}
|
|
73
|
-
with open(config_file, 'w') as f:
|
|
74
|
-
json.dump(base_config, f, indent=2)
|
|
75
|
-
|
|
76
|
-
# Read current config
|
|
77
|
-
with open(config_file, 'r') as f:
|
|
78
|
-
config = json.load(f)
|
|
79
|
-
|
|
80
|
-
# Set the key directly in the namespace file
|
|
81
|
-
config[key] = value
|
|
82
|
-
|
|
83
|
-
# Write updated config back to file atomically
|
|
84
|
-
temp_file = config_file + '.tmp'
|
|
85
|
-
with open(temp_file, 'w') as f:
|
|
86
|
-
json.dump(config, f, indent=2)
|
|
87
|
-
os.rename(temp_file, config_file)
|
|
88
|
-
|
|
89
|
-
# Output the updated config for Terraform
|
|
90
|
-
print(json.dumps({"updated_json": json.dumps(config)}))
|
|
91
|
-
|
|
92
|
-
# Success - break out of retry loop
|
|
93
|
-
break
|
|
94
|
-
|
|
95
|
-
finally:
|
|
96
|
-
# Lock is automatically released when file is closed
|
|
97
|
-
pass
|
|
36
|
+
# Write this entry's contribution as a standalone JSON file.
|
|
37
|
+
resource "local_file" "entry" {
|
|
38
|
+
filename = local.entry_file_path
|
|
39
|
+
content = jsonencode(var.value)
|
|
40
|
+
file_permission = "0644"
|
|
41
|
+
}
|
|
98
42
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
else:
|
|
105
|
-
# Final attempt failed
|
|
106
|
-
raise Exception(f"Failed to acquire lock after {max_retries} attempts: {e}")
|
|
43
|
+
# Outputs
|
|
44
|
+
output "entry_file_path" {
|
|
45
|
+
description = "Absolute path to the JSON file that stores this entry's value"
|
|
46
|
+
value = local_file.entry.filename
|
|
47
|
+
}
|
|
107
48
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
pass
|
|
113
|
-
EOT
|
|
114
|
-
]
|
|
49
|
+
output "namespace" {
|
|
50
|
+
description = "Namespace this entry was written under"
|
|
51
|
+
value = var.namespace
|
|
52
|
+
}
|
|
115
53
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
value = jsonencode(var.value)
|
|
120
|
-
}
|
|
54
|
+
output "key" {
|
|
55
|
+
description = "Key this entry was written as"
|
|
56
|
+
value = var.key
|
|
121
57
|
}
|