From 0ca383dae66e476c6038a2ceb1c3efde3272b6d3 Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Fri, 7 Aug 2026 18:49:56 +0000 Subject: [PATCH] feat(P4): transparent terraform + feature flags + run_platform.sh split (REQ-233..238) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create run_codegen.sh (pre-TF: env check, validate, resolve, adapt). Create run_postapply.sh (post-TF: Checkov, confidence, HITL, outbox, SSM, uptime). Add variable 'enabled' (bool, default true) + count=var.enabled?1:0 to all 12 L1 modules (alb, cloudfront, ecr, ecs-cluster, ecs-service, iam-role, kms-key, rds, s3, uptime, vpc, waf). Fix all cross-resource references with [0] indexing. Update interface.json for all modules to declare 'enabled' input. Fix stale artifact path /tmp/acdl_platform_run_v18 → /tmp/nova_platform_run (REQ-238). run_platform.sh remains as backward-compat shim for local-dev usage. ---ci--- project: acdl phase: 4 milestone: v1.20 status: execute requirements: [REQ-233, REQ-234, REQ-235, REQ-236, REQ-237, REQ-238] ---/ci--- --- .gitea/workflows/deploy.yml | 2 +- .github/workflows/deploy.yml | 2 +- modules/l1/alb/interface.json | 41 +++- modules/l1/alb/terraform/main.tf | 7 +- modules/l1/alb/terraform/outputs.tf | 6 +- modules/l1/alb/terraform/variables.tf | 6 + modules/l1/cloudfront/interface.json | 37 +++- modules/l1/cloudfront/terraform/main.tf | 4 +- modules/l1/cloudfront/terraform/outputs.tf | 6 +- modules/l1/cloudfront/terraform/variables.tf | 6 + modules/l1/ecr/interface.json | 7 +- modules/l1/ecr/terraform/main.tf | 1 + modules/l1/ecr/terraform/outputs.tf | 4 +- modules/l1/ecr/terraform/variables.tf | 8 +- modules/l1/ecs-cluster/interface.json | 7 +- modules/l1/ecs-cluster/terraform/main.tf | 1 + modules/l1/ecs-cluster/terraform/outputs.tf | 4 +- modules/l1/ecs-cluster/terraform/variables.tf | 6 + modules/l1/ecs-service/interface.json | 33 ++- modules/l1/ecs-service/terraform/main.tf | 4 +- modules/l1/ecs-service/terraform/outputs.tf | 4 +- modules/l1/ecs-service/terraform/variables.tf | 6 + modules/l1/iam-role/interface.json | 7 +- modules/l1/iam-role/terraform/main.tf | 5 +- modules/l1/iam-role/terraform/outputs.tf | 4 +- modules/l1/iam-role/terraform/variables.tf | 6 + modules/l1/kms-key/interface.json | 7 +- modules/l1/kms-key/terraform/main.tf | 4 +- modules/l1/kms-key/terraform/outputs.tf | 4 +- modules/l1/kms-key/terraform/variables.tf | 8 +- modules/l1/rds/interface.json | 5 + modules/l1/rds/terraform/main.tf | 1 + modules/l1/rds/terraform/outputs.tf | 4 +- modules/l1/rds/terraform/variables.tf | 6 + modules/l1/s3/interface.json | 7 +- modules/l1/s3/terraform/main.tf | 7 +- modules/l1/s3/terraform/outputs.tf | 6 +- modules/l1/s3/terraform/variables.tf | 8 +- modules/l1/uptime/interface.json | 7 +- modules/l1/uptime/terraform/main.tf | 2 +- modules/l1/uptime/terraform/variables.tf | 6 + modules/l1/vpc/interface.json | 41 +++- modules/l1/vpc/terraform/main.tf | 13 +- modules/l1/vpc/terraform/outputs.tf | 2 +- modules/l1/vpc/terraform/variables.tf | 6 + modules/l1/waf/interface.json | 18 +- modules/l1/waf/terraform/main.tf | 1 + modules/l1/waf/terraform/outputs.tf | 2 +- modules/l1/waf/terraform/variables.tf | 6 + scripts/run_codegen.sh | 147 +++++++++++++ scripts/run_postapply.sh | 202 ++++++++++++++++++ workflows-src/deploy.yml | 2 +- 52 files changed, 667 insertions(+), 79 deletions(-) create mode 100755 scripts/run_codegen.sh create mode 100755 scripts/run_postapply.sh diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index d9906dc..219b4d4 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -155,7 +155,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: nova-terraform - path: /tmp/acdl_platform_run_v18/tf/*.tf + path: /tmp/nova_platform_run/tf/*.tf if-no-files-found: warn - name: Upload platform log diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d9906dc..219b4d4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -155,7 +155,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: nova-terraform - path: /tmp/acdl_platform_run_v18/tf/*.tf + path: /tmp/nova_platform_run/tf/*.tf if-no-files-found: warn - name: Upload platform log diff --git a/modules/l1/alb/interface.json b/modules/l1/alb/interface.json index 57af3a5..5e3c41b 100644 --- a/modules/l1/alb/interface.json +++ b/modules/l1/alb/interface.json @@ -48,6 +48,11 @@ "description": "Target group target type (ip or instance).", "required": false, "default": "ip" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Feature flag: enable/disable this module. Set to false to skip resource creation." } }, "outputs": { @@ -85,20 +90,42 @@ { "type": "aws:elbv2:loadbalancer", "description": "Application load balancer in the VPC subnets.", - "inputs": ["name", "subnets", "security_group", "load_balancer_type"], - "outputs": ["lb_arn"] + "inputs": [ + "name", + "subnets", + "security_group", + "load_balancer_type" + ], + "outputs": [ + "lb_arn" + ] }, { "type": "aws:elbv2:targetgroup", "description": "Target group for the ECS service tasks.", - "inputs": ["name", "port", "protocol", "vpc_id", "target_type"], - "outputs": ["target_group_arn"] + "inputs": [ + "name", + "port", + "protocol", + "vpc_id", + "target_type" + ], + "outputs": [ + "target_group_arn" + ] }, { "type": "aws:elbv2:listener", "description": "Listener forwarding the LB port to the target group.", - "inputs": ["lb_arn", "port", "protocol", "target_group_arn"], - "outputs": ["listener_arn"] + "inputs": [ + "lb_arn", + "port", + "protocol", + "target_group_arn" + ], + "outputs": [ + "listener_arn" + ] } ] -} \ No newline at end of file +} diff --git a/modules/l1/alb/terraform/main.tf b/modules/l1/alb/terraform/main.tf index a5215ef..f46992d 100644 --- a/modules/l1/alb/terraform/main.tf +++ b/modules/l1/alb/terraform/main.tf @@ -1,4 +1,5 @@ resource "aws_lb" "this" { + count = var.enabled ? 1 : 0 name = var.name load_balancer_type = var.load_balancer_type subnets = local.subnet_list @@ -6,6 +7,7 @@ resource "aws_lb" "this" { } resource "aws_lb_target_group" "this" { + count = var.enabled ? 1 : 0 name_prefix = "${var.name}-" port = var.port protocol = var.protocol @@ -18,13 +20,14 @@ resource "aws_lb_target_group" "this" { } resource "aws_lb_listener" "this" { - load_balancer_arn = aws_lb.this.id + count = var.enabled ? 1 : 0 + load_balancer_arn = aws_lb.this[0].id port = var.port protocol = var.protocol default_action { type = "forward" - target_group_arn = aws_lb_target_group.this.arn + target_group_arn = aws_lb_target_group.this[0].arn } depends_on = [aws_lb_target_group.this] diff --git a/modules/l1/alb/terraform/outputs.tf b/modules/l1/alb/terraform/outputs.tf index f44ace4..16b84f0 100644 --- a/modules/l1/alb/terraform/outputs.tf +++ b/modules/l1/alb/terraform/outputs.tf @@ -1,14 +1,14 @@ output "lb_arn" { - value = aws_lb.this.id + value = aws_lb.this[0].id description = "The load balancer ARN." } output "listener_arn" { - value = aws_lb_listener.this.arn + value = aws_lb_listener.this[0].arn description = "The listener ARN." } output "target_group_arn" { - value = aws_lb_target_group.this.arn + value = aws_lb_target_group.this[0].arn description = "The target group ARN." } diff --git a/modules/l1/alb/terraform/variables.tf b/modules/l1/alb/terraform/variables.tf index 8ca89bb..cee5b32 100644 --- a/modules/l1/alb/terraform/variables.tf +++ b/modules/l1/alb/terraform/variables.tf @@ -50,3 +50,9 @@ variable "vpc_id" { description = "VPC ID for the target group (ref to vpc or platform VPC)." default = null } + +variable "enabled" { + type = bool + description = "Feature flag: enable/disable this module. Set to false to skip resource creation." + default = true +} diff --git a/modules/l1/cloudfront/interface.json b/modules/l1/cloudfront/interface.json index 6497855..e60ab11 100644 --- a/modules/l1/cloudfront/interface.json +++ b/modules/l1/cloudfront/interface.json @@ -43,6 +43,11 @@ "type": "string", "description": "AWS region (CloudFront is global but the provider region is used for the OAC).", "required": true + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Feature flag: enable/disable this module. Set to false to skip resource creation." } }, "outputs": { @@ -75,17 +80,37 @@ { "type": "aws:cloudfront:distribution", "description": "CloudFront distribution with S3 origin via OAC.", - "inputs": ["bucket_regional_domain_name", "price_class", "viewer_protocol_policy", "default_ttl", "max_ttl", "waf_web_acl_arn", "oac_id"], - "outputs": ["distribution_arn", "distribution_domain_name"] + "inputs": [ + "bucket_regional_domain_name", + "price_class", + "viewer_protocol_policy", + "default_ttl", + "max_ttl", + "waf_web_acl_arn", + "oac_id" + ], + "outputs": [ + "distribution_arn", + "distribution_domain_name" + ] }, { "type": "aws:cloudfront:originaccesscontrol", "description": "Origin Access Control for the S3 origin.", - "inputs": ["name", "origin_type", "signing_behavior"], - "outputs": ["oac_id"] + "inputs": [ + "name", + "origin_type", + "signing_behavior" + ], + "outputs": [ + "oac_id" + ] } ], "intra_refs": [ - {"from": "aws:cloudfront:distribution.oac_id", "to": "aws:cloudfront:originaccesscontrol.oac_id"} + { + "from": "aws:cloudfront:distribution.oac_id", + "to": "aws:cloudfront:originaccesscontrol.oac_id" + } ] -} \ No newline at end of file +} diff --git a/modules/l1/cloudfront/terraform/main.tf b/modules/l1/cloudfront/terraform/main.tf index 7ffc8cc..3b380c5 100644 --- a/modules/l1/cloudfront/terraform/main.tf +++ b/modules/l1/cloudfront/terraform/main.tf @@ -1,4 +1,5 @@ resource "aws_cloudfront_origin_access_control" "this" { + count = var.enabled ? 1 : 0 name = local.oac_name origin_access_control_origin_type = local.oac_origin_type signing_behavior = local.oac_signing_behavior @@ -6,10 +7,11 @@ resource "aws_cloudfront_origin_access_control" "this" { } resource "aws_cloudfront_distribution" "this" { + count = var.enabled ? 1 : 0 origin { origin_id = "s3-origin" domain_name = var.bucket_regional_domain_name - origin_access_control_id = aws_cloudfront_origin_access_control.this.id + origin_access_control_id = aws_cloudfront_origin_access_control.this[0].id s3_origin_config { origin_access_identity = "" } diff --git a/modules/l1/cloudfront/terraform/outputs.tf b/modules/l1/cloudfront/terraform/outputs.tf index 949afff..a5d4f4d 100644 --- a/modules/l1/cloudfront/terraform/outputs.tf +++ b/modules/l1/cloudfront/terraform/outputs.tf @@ -1,14 +1,14 @@ output "distribution_arn" { - value = aws_cloudfront_distribution.this.arn + value = aws_cloudfront_distribution.this[0].arn description = "The CloudFront distribution ARN." } output "distribution_domain_name" { - value = aws_cloudfront_distribution.this.domain_name + value = aws_cloudfront_distribution.this[0].domain_name description = "The CloudFront distribution domain name." } output "oac_id" { - value = aws_cloudfront_origin_access_control.this.id + value = aws_cloudfront_origin_access_control.this[0].id description = "The Origin Access Control ID." } diff --git a/modules/l1/cloudfront/terraform/variables.tf b/modules/l1/cloudfront/terraform/variables.tf index 4819355..7dd8d8c 100644 --- a/modules/l1/cloudfront/terraform/variables.tf +++ b/modules/l1/cloudfront/terraform/variables.tf @@ -38,3 +38,9 @@ variable "region" { description = "AWS region (CloudFront is global but the provider region is used for the OAC)." default = null } + +variable "enabled" { + type = bool + description = "Feature flag: enable/disable this module. Set to false to skip resource creation." + default = true +} diff --git a/modules/l1/ecr/interface.json b/modules/l1/ecr/interface.json index 798c66d..79a7994 100644 --- a/modules/l1/ecr/interface.json +++ b/modules/l1/ecr/interface.json @@ -19,6 +19,11 @@ "type": "string", "description": "ARN of the CMK for repository encryption; if absent, uses AWS-managed key.", "required": false + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Feature flag: enable/disable this module. Set to false to skip resource creation." } }, "outputs": { @@ -48,4 +53,4 @@ "default": true } } -} \ No newline at end of file +} diff --git a/modules/l1/ecr/terraform/main.tf b/modules/l1/ecr/terraform/main.tf index 7b8b8c0..3db6da8 100644 --- a/modules/l1/ecr/terraform/main.tf +++ b/modules/l1/ecr/terraform/main.tf @@ -6,6 +6,7 @@ locals { } resource "aws_ecr_repository" "this" { + count = var.enabled ? 1 : 0 name = var.name image_tag_mutability = "MUTABLE" image_scanning_configuration { diff --git a/modules/l1/ecr/terraform/outputs.tf b/modules/l1/ecr/terraform/outputs.tf index a686adf..e105671 100644 --- a/modules/l1/ecr/terraform/outputs.tf +++ b/modules/l1/ecr/terraform/outputs.tf @@ -1,9 +1,9 @@ output "repository_url" { - value = aws_ecr_repository.this.repository_url + value = aws_ecr_repository.this[0].repository_url description = "The ECR repository URL." } output "repository_arn" { - value = aws_ecr_repository.this.arn + value = aws_ecr_repository.this[0].arn description = "The ECR repository ARN." } \ No newline at end of file diff --git a/modules/l1/ecr/terraform/variables.tf b/modules/l1/ecr/terraform/variables.tf index 7e86c44..a4ae3d3 100644 --- a/modules/l1/ecr/terraform/variables.tf +++ b/modules/l1/ecr/terraform/variables.tf @@ -13,4 +13,10 @@ variable "kms_key_arn" { type = string description = "ARN of the CMK for ECR encryption; if absent, uses managed key." default = null -} \ No newline at end of file +} + +variable "enabled" { + type = bool + description = "Feature flag: enable/disable this module. Set to false to skip resource creation." + default = true +} diff --git a/modules/l1/ecs-cluster/interface.json b/modules/l1/ecs-cluster/interface.json index c732d8e..056ec9b 100644 --- a/modules/l1/ecs-cluster/interface.json +++ b/modules/l1/ecs-cluster/interface.json @@ -19,6 +19,11 @@ "type": "string", "description": "ARN of the CMK for CloudWatch log group encryption; if absent, uses managed key.", "required": false + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Feature flag: enable/disable this module. Set to false to skip resource creation." } }, "outputs": { @@ -43,4 +48,4 @@ "default": true } } -} \ No newline at end of file +} diff --git a/modules/l1/ecs-cluster/terraform/main.tf b/modules/l1/ecs-cluster/terraform/main.tf index 96c5264..d398e61 100644 --- a/modules/l1/ecs-cluster/terraform/main.tf +++ b/modules/l1/ecs-cluster/terraform/main.tf @@ -1,3 +1,4 @@ resource "aws_ecs_cluster" "this" { + count = var.enabled ? 1 : 0 name = var.name } diff --git a/modules/l1/ecs-cluster/terraform/outputs.tf b/modules/l1/ecs-cluster/terraform/outputs.tf index 7fc3625..e59ed57 100644 --- a/modules/l1/ecs-cluster/terraform/outputs.tf +++ b/modules/l1/ecs-cluster/terraform/outputs.tf @@ -1,9 +1,9 @@ output "cluster_arn" { - value = aws_ecs_cluster.this.arn + value = aws_ecs_cluster.this[0].arn description = "The ECS cluster ARN." } output "cluster_id" { - value = aws_ecs_cluster.this.id + value = aws_ecs_cluster.this[0].id description = "The ECS cluster ID." } diff --git a/modules/l1/ecs-cluster/terraform/variables.tf b/modules/l1/ecs-cluster/terraform/variables.tf index addc305..ad86808 100644 --- a/modules/l1/ecs-cluster/terraform/variables.tf +++ b/modules/l1/ecs-cluster/terraform/variables.tf @@ -15,3 +15,9 @@ variable "kms_key_arn" { description = "ARN of the CMK for CloudWatch log group encryption; if absent, uses managed key." default = null } + +variable "enabled" { + type = bool + description = "Feature flag: enable/disable this module. Set to false to skip resource creation." + default = true +} diff --git a/modules/l1/ecs-service/interface.json b/modules/l1/ecs-service/interface.json index df5a853..2adc6ff 100644 --- a/modules/l1/ecs-service/interface.json +++ b/modules/l1/ecs-service/interface.json @@ -79,6 +79,11 @@ "description": "ECS task definition family name.", "required": false, "default": "app" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Feature flag: enable/disable this module. Set to false to skip resource creation." } }, "outputs": { @@ -107,14 +112,32 @@ { "type": "aws:ecs:task_definition", "description": "Fargate task definition; the adapter jsonencodes image/port/env into container_definitions.", - "inputs": ["image", "port", "cpu", "memory", "env", "family"], - "outputs": ["task_def_arn"] + "inputs": [ + "image", + "port", + "cpu", + "memory", + "env", + "family" + ], + "outputs": [ + "task_def_arn" + ] }, { "type": "aws:ecs:service", "description": "Fargate service running the task definition in the cluster + subnets.", - "inputs": ["cluster_arn", "subnets", "security_group", "lb_target_group_arn", "desired_count", "launch_type"], - "outputs": ["service_arn"] + "inputs": [ + "cluster_arn", + "subnets", + "security_group", + "lb_target_group_arn", + "desired_count", + "launch_type" + ], + "outputs": [ + "service_arn" + ] } ] -} \ No newline at end of file +} diff --git a/modules/l1/ecs-service/terraform/main.tf b/modules/l1/ecs-service/terraform/main.tf index 0940024..3b81a09 100644 --- a/modules/l1/ecs-service/terraform/main.tf +++ b/modules/l1/ecs-service/terraform/main.tf @@ -1,4 +1,5 @@ resource "aws_ecs_task_definition" "this" { + count = var.enabled ? 1 : 0 family = var.family cpu = tostring(var.cpu) memory = tostring(var.memory) @@ -8,9 +9,10 @@ resource "aws_ecs_task_definition" "this" { } resource "aws_ecs_service" "this" { + count = var.enabled ? 1 : 0 name = "nova-microservice" cluster = var.cluster_arn - task_definition = aws_ecs_task_definition.this.arn + task_definition = aws_ecs_task_definition.this[0].arn desired_count = var.desired_count launch_type = var.launch_type diff --git a/modules/l1/ecs-service/terraform/outputs.tf b/modules/l1/ecs-service/terraform/outputs.tf index 15d3b8a..68ef6a9 100644 --- a/modules/l1/ecs-service/terraform/outputs.tf +++ b/modules/l1/ecs-service/terraform/outputs.tf @@ -1,9 +1,9 @@ output "service_arn" { - value = aws_ecs_service.this.id + value = aws_ecs_service.this[0].id description = "The ECS service ARN." } output "task_def_arn" { - value = aws_ecs_task_definition.this.arn + value = aws_ecs_task_definition.this[0].arn description = "The ECS task definition ARN." } diff --git a/modules/l1/ecs-service/terraform/variables.tf b/modules/l1/ecs-service/terraform/variables.tf index b0ce66f..b781862 100644 --- a/modules/l1/ecs-service/terraform/variables.tf +++ b/modules/l1/ecs-service/terraform/variables.tf @@ -78,3 +78,9 @@ variable "family" { description = "ECS task definition family name." default = "app" } + +variable "enabled" { + type = bool + description = "Feature flag: enable/disable this module. Set to false to skip resource creation." + default = true +} diff --git a/modules/l1/iam-role/interface.json b/modules/l1/iam-role/interface.json index 6d3d4c6..d732ece 100644 --- a/modules/l1/iam-role/interface.json +++ b/modules/l1/iam-role/interface.json @@ -24,6 +24,11 @@ "type": "string", "description": "AWS region the role is created in.", "required": true + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Feature flag: enable/disable this module. Set to false to skip resource creation." } }, "outputs": { @@ -48,4 +53,4 @@ "default": true } } -} \ No newline at end of file +} diff --git a/modules/l1/iam-role/terraform/main.tf b/modules/l1/iam-role/terraform/main.tf index de4daf7..f6c21ef 100644 --- a/modules/l1/iam-role/terraform/main.tf +++ b/modules/l1/iam-role/terraform/main.tf @@ -1,11 +1,12 @@ resource "aws_iam_role" "this" { + count = var.enabled ? 1 : 0 name = var.role_name assume_role_policy = local.assume_role_policy } resource "aws_iam_role_policy" "ecr_logs" { - count = local.inline_policy != null ? 1 : 0 + count = (local.inline_policy != null && var.enabled) ? 1 : 0 name = local.inline_policy.name - role = aws_iam_role.this.id + role = aws_iam_role.this[0].id policy = local.inline_policy.policy } diff --git a/modules/l1/iam-role/terraform/outputs.tf b/modules/l1/iam-role/terraform/outputs.tf index 371acfa..18bc174 100644 --- a/modules/l1/iam-role/terraform/outputs.tf +++ b/modules/l1/iam-role/terraform/outputs.tf @@ -1,9 +1,9 @@ output "role_arn" { - value = aws_iam_role.this.arn + value = aws_iam_role.this[0].arn description = "The IAM role ARN." } output "role_id" { - value = aws_iam_role.this.id + value = aws_iam_role.this[0].id description = "The IAM role ID." } diff --git a/modules/l1/iam-role/terraform/variables.tf b/modules/l1/iam-role/terraform/variables.tf index afb3684..64253b5 100644 --- a/modules/l1/iam-role/terraform/variables.tf +++ b/modules/l1/iam-role/terraform/variables.tf @@ -21,3 +21,9 @@ variable "region" { description = "AWS region (provider-level; not a resource arg)." default = null } + +variable "enabled" { + type = bool + description = "Feature flag: enable/disable this module. Set to false to skip resource creation." + default = true +} diff --git a/modules/l1/kms-key/interface.json b/modules/l1/kms-key/interface.json index 44644ef..da4bd79 100644 --- a/modules/l1/kms-key/interface.json +++ b/modules/l1/kms-key/interface.json @@ -20,6 +20,11 @@ "description": "Number of days before the key is deleted after deletion is requested (default 30).", "required": false, "default": 30 + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Feature flag: enable/disable this module. Set to false to skip resource creation." } }, "outputs": { @@ -49,4 +54,4 @@ "default": true } } -} \ No newline at end of file +} diff --git a/modules/l1/kms-key/terraform/main.tf b/modules/l1/kms-key/terraform/main.tf index 665f3f1..9ccde47 100644 --- a/modules/l1/kms-key/terraform/main.tf +++ b/modules/l1/kms-key/terraform/main.tf @@ -1,10 +1,12 @@ resource "aws_kms_key" "this" { + count = var.enabled ? 1 : 0 description = var.description enable_key_rotation = true deletion_window_in_days = var.deletion_window_days } resource "aws_kms_alias" "this" { + count = var.enabled ? 1 : 0 name = local.alias_name - target_key_id = aws_kms_key.this.key_id + target_key_id = aws_kms_key.this[0].key_id } \ No newline at end of file diff --git a/modules/l1/kms-key/terraform/outputs.tf b/modules/l1/kms-key/terraform/outputs.tf index 8828df8..bd75e38 100644 --- a/modules/l1/kms-key/terraform/outputs.tf +++ b/modules/l1/kms-key/terraform/outputs.tf @@ -1,9 +1,9 @@ output "kms_key_arn" { - value = aws_kms_key.this.arn + value = aws_kms_key.this[0].arn description = "The KMS key ARN." } output "kms_key_id" { - value = aws_kms_key.this.key_id + value = aws_kms_key.this[0].key_id description = "The KMS key ID." } \ No newline at end of file diff --git a/modules/l1/kms-key/terraform/variables.tf b/modules/l1/kms-key/terraform/variables.tf index 33ebfd6..1b15172 100644 --- a/modules/l1/kms-key/terraform/variables.tf +++ b/modules/l1/kms-key/terraform/variables.tf @@ -14,4 +14,10 @@ variable "deletion_window_days" { type = number description = "Deletion window in days (7-30)." default = 30 -} \ No newline at end of file +} + +variable "enabled" { + type = bool + description = "Feature flag: enable/disable this module. Set to false to skip resource creation." + default = true +} diff --git a/modules/l1/rds/interface.json b/modules/l1/rds/interface.json index 6ea322b..09522b2 100644 --- a/modules/l1/rds/interface.json +++ b/modules/l1/rds/interface.json @@ -79,6 +79,11 @@ "description": "Database admin password", "required": false, "default": "ACdlcI2026!" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Feature flag: enable/disable this module. Set to false to skip resource creation." } }, "outputs": { diff --git a/modules/l1/rds/terraform/main.tf b/modules/l1/rds/terraform/main.tf index 3b9a572..7654962 100644 --- a/modules/l1/rds/terraform/main.tf +++ b/modules/l1/rds/terraform/main.tf @@ -5,6 +5,7 @@ resource "aws_db_subnet_group" "this" { } resource "aws_db_instance" "this" { + count = var.enabled ? 1 : 0 engine = var.engine engine_version = var.engine_version instance_class = var.instance_class diff --git a/modules/l1/rds/terraform/outputs.tf b/modules/l1/rds/terraform/outputs.tf index cc0696b..b824805 100644 --- a/modules/l1/rds/terraform/outputs.tf +++ b/modules/l1/rds/terraform/outputs.tf @@ -1,9 +1,9 @@ output "db_endpoint" { - value = aws_db_instance.this.endpoint + value = aws_db_instance.this[0].endpoint description = "The RDS instance endpoint." } output "db_arn" { - value = aws_db_instance.this.arn + value = aws_db_instance.this[0].arn description = "The RDS instance ARN." } diff --git a/modules/l1/rds/terraform/variables.tf b/modules/l1/rds/terraform/variables.tf index 0ed990f..47a1d69 100644 --- a/modules/l1/rds/terraform/variables.tf +++ b/modules/l1/rds/terraform/variables.tf @@ -64,3 +64,9 @@ variable "subnet_ids" { description = "Comma-separated subnet IDs for the DB subnet group (VPC-dependent)." default = "" } + +variable "enabled" { + type = bool + description = "Feature flag: enable/disable this module. Set to false to skip resource creation." + default = true +} diff --git a/modules/l1/s3/interface.json b/modules/l1/s3/interface.json index b3cc025..cb22315 100644 --- a/modules/l1/s3/interface.json +++ b/modules/l1/s3/interface.json @@ -19,6 +19,11 @@ "type": "string", "description": "ARN of the CMK for SSE-KMS; if absent, uses managed key.", "required": false + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Feature flag: enable/disable this module. Set to false to skip resource creation." } }, "outputs": { @@ -57,4 +62,4 @@ "default": true } } -} \ No newline at end of file +} diff --git a/modules/l1/s3/terraform/main.tf b/modules/l1/s3/terraform/main.tf index 5eea94f..3e2784f 100644 --- a/modules/l1/s3/terraform/main.tf +++ b/modules/l1/s3/terraform/main.tf @@ -1,10 +1,12 @@ resource "aws_s3_bucket" "this" { + count = var.enabled ? 1 : 0 bucket = var.bucket_name tags = local.tags } resource "aws_s3_bucket_versioning" "this" { - bucket = aws_s3_bucket.this.id + count = var.enabled ? 1 : 0 + bucket = aws_s3_bucket.this[0].id versioning_configuration { status = "Enabled" @@ -12,7 +14,8 @@ resource "aws_s3_bucket_versioning" "this" { } resource "aws_s3_bucket_server_side_encryption_configuration" "this" { - bucket = aws_s3_bucket.this.id + count = var.enabled ? 1 : 0 + bucket = aws_s3_bucket.this[0].id rule { apply_server_side_encryption_by_default { diff --git a/modules/l1/s3/terraform/outputs.tf b/modules/l1/s3/terraform/outputs.tf index 96b1fca..f55c437 100644 --- a/modules/l1/s3/terraform/outputs.tf +++ b/modules/l1/s3/terraform/outputs.tf @@ -1,14 +1,14 @@ output "bucket_arn" { - value = aws_s3_bucket.this.arn + value = aws_s3_bucket.this[0].arn description = "The S3 bucket ARN." } output "bucket_name" { - value = aws_s3_bucket.this.id + value = aws_s3_bucket.this[0].id description = "The bucket name (echoes the input)." } output "bucket_regional_domain_name" { - value = aws_s3_bucket.this.bucket_regional_domain_name + value = aws_s3_bucket.this[0].bucket_regional_domain_name description = "The bucket regional domain name (e.g. nova-spike-bucket.s3.us-east-1.amazonaws.com)." } \ No newline at end of file diff --git a/modules/l1/s3/terraform/variables.tf b/modules/l1/s3/terraform/variables.tf index f9fbb3f..5a2cc6a 100644 --- a/modules/l1/s3/terraform/variables.tf +++ b/modules/l1/s3/terraform/variables.tf @@ -19,4 +19,10 @@ variable "tags" { type = map(string) description = "Additional tags to merge with the module defaults." default = {} -} \ No newline at end of file +} + +variable "enabled" { + type = bool + description = "Feature flag: enable/disable this module. Set to false to skip resource creation." + default = true +} diff --git a/modules/l1/uptime/interface.json b/modules/l1/uptime/interface.json index c3de5f9..a392308 100644 --- a/modules/l1/uptime/interface.json +++ b/modules/l1/uptime/interface.json @@ -71,6 +71,11 @@ "type": "string", "description": "ECS cluster ARN to deploy the service into", "required": false + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Feature flag: enable/disable this module. Set to false to skip resource creation." } }, "outputs": { @@ -99,4 +104,4 @@ "default": true } } -} \ No newline at end of file +} diff --git a/modules/l1/uptime/terraform/main.tf b/modules/l1/uptime/terraform/main.tf index 4eb0229..10a3059 100644 --- a/modules/l1/uptime/terraform/main.tf +++ b/modules/l1/uptime/terraform/main.tf @@ -11,7 +11,7 @@ resource "aws_ecs_service" "uptime" { name = "nova-uptime" cluster = local.cluster_ref task_definition = aws_ecs_task_definition.uptime.arn - desired_count = var.feature_flag_enabled ? 1 : 0 + desired_count = var.enabled ? (var.feature_flag_enabled ? 1 : 0) : 0 launch_type = "FARGATE" dynamic "network_configuration" { diff --git a/modules/l1/uptime/terraform/variables.tf b/modules/l1/uptime/terraform/variables.tf index f20f949..882a248 100644 --- a/modules/l1/uptime/terraform/variables.tf +++ b/modules/l1/uptime/terraform/variables.tf @@ -69,3 +69,9 @@ variable "cluster_arn" { description = "ECS cluster ARN to deploy the service into." default = "" } + +variable "enabled" { + type = bool + description = "Feature flag: enable/disable this module. Set to false to skip resource creation." + default = true +} diff --git a/modules/l1/vpc/interface.json b/modules/l1/vpc/interface.json index 24d027f..fb550dc 100644 --- a/modules/l1/vpc/interface.json +++ b/modules/l1/vpc/interface.json @@ -24,6 +24,11 @@ "type": "string", "description": "AWS region the VPC is created in.", "required": true + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Feature flag: enable/disable this module. Set to false to skip resource creation." } }, "outputs": { @@ -57,24 +62,44 @@ { "type": "aws:ec2:vpc", "description": "The VPC itself.", - "inputs": ["cidr", "name"], - "outputs": ["vpc_id"] + "inputs": [ + "cidr", + "name" + ], + "outputs": [ + "vpc_id" + ] }, { "type": "aws:ec2:subnet", "description": "One subnet per availability zone (azs split on comma).", - "inputs": ["cidr", "az", "vpc_id", "name"], - "outputs": ["subnet_ids"] + "inputs": [ + "cidr", + "az", + "vpc_id", + "name" + ], + "outputs": [ + "subnet_ids" + ] }, { "type": "aws:ec2:routetable", "description": "Route table bound to the VPC with an internet gateway + default route.", - "inputs": ["vpc_id"], + "inputs": [ + "vpc_id" + ], "outputs": [] } ], "intra_refs": [ - {"from": "aws:ec2:subnet.vpc_id", "to": "aws:ec2:vpc.vpc_id"}, - {"from": "aws:ec2:routetable.vpc_id", "to": "aws:ec2:vpc.vpc_id"} + { + "from": "aws:ec2:subnet.vpc_id", + "to": "aws:ec2:vpc.vpc_id" + }, + { + "from": "aws:ec2:routetable.vpc_id", + "to": "aws:ec2:vpc.vpc_id" + } ] -} \ No newline at end of file +} diff --git a/modules/l1/vpc/terraform/main.tf b/modules/l1/vpc/terraform/main.tf index de17778..6b0c328 100644 --- a/modules/l1/vpc/terraform/main.tf +++ b/modules/l1/vpc/terraform/main.tf @@ -1,4 +1,5 @@ resource "aws_vpc" "this" { + count = var.enabled ? 1 : 0 cidr_block = local.cidr_block tags = { Name = local.name_tag @@ -11,7 +12,7 @@ resource "aws_vpc" "this" { resource "aws_subnet" "this" { count = length(local.az_list) - vpc_id = aws_vpc.this.id + vpc_id = aws_vpc.this[0].id cidr_block = local.subnet_cidrs[count.index] availability_zone = local.az_list[count.index] tags = { @@ -20,17 +21,19 @@ resource "aws_subnet" "this" { } resource "aws_internet_gateway" "this" { - vpc_id = aws_vpc.this.id + count = var.enabled ? 1 : 0 + vpc_id = aws_vpc.this[0].id tags = { Name = "${local.name_tag}-igw" } } resource "aws_route_table" "this" { - vpc_id = aws_vpc.this.id + count = var.enabled ? 1 : 0 + vpc_id = aws_vpc.this[0].id route { cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.this.id + gateway_id = aws_internet_gateway.this[0].id } tags = { Name = "${local.name_tag}-rt" @@ -40,5 +43,5 @@ resource "aws_route_table" "this" { resource "aws_route_table_association" "this" { count = length(local.az_list) subnet_id = aws_subnet.this[count.index].id - route_table_id = aws_route_table.this.id + route_table_id = aws_route_table.this[0].id } diff --git a/modules/l1/vpc/terraform/outputs.tf b/modules/l1/vpc/terraform/outputs.tf index afabd44..ec22059 100644 --- a/modules/l1/vpc/terraform/outputs.tf +++ b/modules/l1/vpc/terraform/outputs.tf @@ -1,5 +1,5 @@ output "vpc_id" { - value = aws_vpc.this.id + value = aws_vpc.this[0].id description = "The VPC id." } diff --git a/modules/l1/vpc/terraform/variables.tf b/modules/l1/vpc/terraform/variables.tf index 5c05b84..dcf82f7 100644 --- a/modules/l1/vpc/terraform/variables.tf +++ b/modules/l1/vpc/terraform/variables.tf @@ -21,3 +21,9 @@ variable "region" { description = "AWS region (provider-level; not a resource arg)." default = null } + +variable "enabled" { + type = bool + description = "Feature flag: enable/disable this module. Set to false to skip resource creation." + default = true +} diff --git a/modules/l1/waf/interface.json b/modules/l1/waf/interface.json index 3ad2769..b34c8ee 100644 --- a/modules/l1/waf/interface.json +++ b/modules/l1/waf/interface.json @@ -31,6 +31,11 @@ "type": "string", "description": "AWS region (CloudFront-scoped WAF is always us-east-1; the adapter ignores this for cloudfront scope).", "required": true + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Feature flag: enable/disable this module. Set to false to skip resource creation." } }, "outputs": { @@ -60,8 +65,15 @@ { "type": "aws:wafv2:webacl", "description": "WAFv2 Web ACL with managed rules.", - "inputs": ["name", "scope", "default_action", "rules"], - "outputs": ["web_acl_arn"] + "inputs": [ + "name", + "scope", + "default_action", + "rules" + ], + "outputs": [ + "web_acl_arn" + ] } ] -} \ No newline at end of file +} diff --git a/modules/l1/waf/terraform/main.tf b/modules/l1/waf/terraform/main.tf index d61cf19..9256b70 100644 --- a/modules/l1/waf/terraform/main.tf +++ b/modules/l1/waf/terraform/main.tf @@ -1,4 +1,5 @@ resource "aws_wafv2_web_acl" "this" { + count = var.enabled ? 1 : 0 name = var.name scope = local.scope diff --git a/modules/l1/waf/terraform/outputs.tf b/modules/l1/waf/terraform/outputs.tf index b8552ca..d8e05bd 100644 --- a/modules/l1/waf/terraform/outputs.tf +++ b/modules/l1/waf/terraform/outputs.tf @@ -1,4 +1,4 @@ output "web_acl_arn" { - value = aws_wafv2_web_acl.this.arn + value = aws_wafv2_web_acl.this[0].arn description = "The WAF Web ACL ARN." } diff --git a/modules/l1/waf/terraform/variables.tf b/modules/l1/waf/terraform/variables.tf index 68d871e..2a61f79 100644 --- a/modules/l1/waf/terraform/variables.tf +++ b/modules/l1/waf/terraform/variables.tf @@ -27,3 +27,9 @@ variable "region" { description = "AWS region (provider-level; not a resource arg)." default = null } + +variable "enabled" { + type = bool + description = "Feature flag: enable/disable this module. Set to false to skip resource creation." + default = true +} diff --git a/scripts/run_codegen.sh b/scripts/run_codegen.sh new file mode 100755 index 0000000..46fdafc --- /dev/null +++ b/scripts/run_codegen.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# scripts/run_codegen.sh — pre-Terraform codegen for the Nova platform pipeline. +# +# Performs steps 0–3b of run_platform.sh: +# 0. Environment onboarding check +# 1. Validate contract against contract.schema.json +# 2. Resolve contract → Target Stack instance (contract_resolver.py) +# 3. Adapter compiles stack → Terraform (adapter.py) +# 3b. Structural validation of emitted TF (offline) +# +# Emits Terraform files to $NOVA_WORK_DIR/tf/{main.tf,terraform.tf,providers.tf} +# and prints the work dir path for the caller (workflow or run_platform.sh) +# to use for native terraform init/validate/plan/apply steps. +# +# Usage: +# run_codegen.sh [--environment ] [--check-only] +# +# When --check-only is passed, exits 0 after structural validation (no AWS). +# Otherwise, loads AWS credentials from .env.secrets if not already set, +# and exits 0 with the work dir ready for terraform. +# +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CALLER_CWD="$(pwd)" +cd "$ROOT" + +CHECK_ONLY=0 +ENVIRONMENT_OVERRIDE="" +CONTRACT="" + +while [ $# -gt 0 ]; do + case "$1" in + --check-only) CHECK_ONLY=1; shift ;; + --environment) shift; ENVIRONMENT_OVERRIDE="$1"; shift ;; + --environment=*) ENVIRONMENT_OVERRIDE="${1#--environment=}"; shift ;; + -h|--help) + echo "Usage: run_codegen.sh [--environment ] [--check-only]" + exit 0 ;; + *) CONTRACT="$1"; shift ;; + esac +done + +[ -n "$CONTRACT" ] || { echo "FAIL: no contract file specified" >&2; exit 1; } + +# Resolve relative contract path against caller's CWD +if ! [[ "$CONTRACT" = /* ]]; then + CONTRACT="$CALLER_CWD/$CONTRACT" +fi +[ -f "$CONTRACT" ] || { echo "FAIL: contract not found: $CONTRACT" >&2; exit 1; } + +if [ -n "$ENVIRONMENT_OVERRIDE" ]; then + export NOVA_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE" +fi + +CONTRACT_ID="${NOVA_CONTRACT_ID:-11111111-1111-1111-1111-111111111111}" +WORK="${NOVA_WORK_DIR:-/tmp/nova_platform_run}" +TF_DIR="$WORK/tf" +rm -rf "$WORK"; mkdir -p "$TF_DIR" + +echo "=== Step 0: environment onboarding check ===" +if [ -n "$ENVIRONMENT_OVERRIDE" ]; then + python3 core/environment_check.py --env="$ENVIRONMENT_OVERRIDE" || { + echo "FAIL: environment onboarding check failed for $ENVIRONMENT_OVERRIDE" >&2 + exit 1 + } +else + python3 core/environment_check.py || true +fi + +echo "" +echo "=== Step 1: validate contract against contract.schema.json ===" +python3 -c " +import json, sys, yaml +from jsonschema import validate +schema = json.load(open('schemas/contract.schema.json')) +doc = yaml.safe_load(open('$CONTRACT')) +validate(instance=doc, schema=schema) +print(f'contract valid: {doc.get(\"name\", \"unnamed\")} (env={doc.get(\"environment\",\"dev\")})') +" + +echo "" +echo "=== Step 2: resolve contract → Target Stack instance ===" +python3 -c " +import json, sys +sys.path.insert(0, '$ROOT') +from core.contract_resolver import resolve +stack = resolve('$CONTRACT', environment_override='${ENVIRONMENT_OVERRIDE}' or None) +json.dump(stack, open('$WORK/stack.json', 'w'), indent=2) +print(f'stack resolved: {stack[\"stack\"][\"name\"]} ({len(stack[\"resources\"])} resource(s))') +" + +echo "" +echo "=== Step 3: adapter compiles stack → $TF_DIR/*.tf ===" +python3 -c " +import json, sys +sys.path.insert(0, '$ROOT') +from adapters.terraform.adapter import TerraformAdapter +stack = json.load(open('$WORK/stack.json')) +adapter = TerraformAdapter() +adapter.compile(stack, '$TF_DIR') +print('adapter: main.tf + terraform.tf + providers.tf written') +" + +if [ "$CHECK_ONLY" = "1" ]; then + echo "" + echo "=== Step 3b: validate adapter output structure (offline) ===" + python3 -c " +import json, os +d = json.load(open('$WORK/stack.json')) +assert d['stack']['name'], 'stack name missing' +assert len(d['resources']) >= 1, 'expected at least 1 resource' +tf_dir = '$TF_DIR' +for f in ('main.tf', 'terraform.tf', 'providers.tf'): + assert os.path.isfile(os.path.join(tf_dir, f)), f'{f} missing' +main = open(os.path.join(tf_dir, 'main.tf')).read() +assert len(main) > 0, 'main.tf is empty' +tf = open(os.path.join(tf_dir, 'terraform.tf')).read() +assert 'backend' in tf +assert 'required_version' in tf +prov = open(os.path.join(tf_dir, 'providers.tf')).read() +assert 'provider \"aws\"' in prov +print(f\"adapter output: OK ({d['stack']['name']}, {len(d['resources'])} resource(s))\") +" + echo "" + echo "=== CODEGEN CHECK OK ===" + echo "contract → resolver → stack → adapter → structure validated (offline, no AWS)" + echo "WORK_DIR=$WORK" + exit 0 +fi + +# Load AWS credentials if not already set (for non-check-only modes) +if [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then + ENV_FILE="$ROOT/.env.secrets" + if [ -f "$ENV_FILE" ]; then + set -a + . "$ENV_FILE" + set +a + export AWS_ACCESS_KEY_ID="$NOVA_AWS_ACCESS_KEY_ID" + export AWS_SECRET_ACCESS_KEY="$NOVA_AWS_SECRET_ACCESS_KEY" + export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION" + fi +fi + +echo "" +echo "=== CODEGEN OK ===" +echo "Terraform files ready in: $TF_DIR" +echo "WORK_DIR=$WORK" diff --git a/scripts/run_postapply.sh b/scripts/run_postapply.sh new file mode 100755 index 0000000..121fc57 --- /dev/null +++ b/scripts/run_postapply.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# scripts/run_postapply.sh — post-Terraform steps for the Nova platform pipeline. +# +# Performs steps 5–9 of run_platform.sh (after terraform apply/destroy): +# 5. Checkov policy scan on the emitted main.tf +# 6. Checkov adapter → PolicyCheckResult (compliance details) +# 7. Confidence signal compute +# 7b. HITL attestation gate (qa/prod/dr only) +# 8. Write evidence event to DynamoDB outbox +# 9. Publish outputs to SSM + GitHub PR comment +# 9b. Uptime monitoring sub-deploy (sourced from run_uptime.sh) +# +# Expects the work dir ($NOVA_WORK_DIR) to already contain: +# - tf/*.tf (from run_codegen.sh) +# - stack.json (from run_codegen.sh) +# And terraform to have already run (init/validate/plan/apply) in $WORK/tf/. +# +# Usage: +# run_postapply.sh [--environment ] [--quiet] [--deploy-uptime] +# +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CALLER_CWD="$(pwd)" +cd "$ROOT" + +QUIET=0 +DEPLOY_UPTIME=0 +ENVIRONMENT_OVERRIDE="" +CONTRACT="" + +while [ $# -gt 0 ]; do + case "$1" in + --quiet) QUIET=1; shift ;; + --deploy-uptime) DEPLOY_UPTIME=1; shift ;; + --environment) shift; ENVIRONMENT_OVERRIDE="$1"; shift ;; + --environment=*) ENVIRONMENT_OVERRIDE="${1#--environment=}"; shift ;; + -h|--help) + echo "Usage: run_postapply.sh [--environment ] [--quiet] [--deploy-uptime]" + exit 0 ;; + *) CONTRACT="$1"; shift ;; + esac +done + +[ -n "$CONTRACT" ] || { echo "FAIL: no contract file specified" >&2; exit 1; } +if ! [[ "$CONTRACT" = /* ]]; then + CONTRACT="$CALLER_CWD/$CONTRACT" +fi +[ -f "$CONTRACT" ] || { echo "FAIL: contract not found: $CONTRACT" >&2; exit 1; } + +if [ -n "$ENVIRONMENT_OVERRIDE" ]; then + export NOVA_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE" +fi + +CONTRACT_ID="${NOVA_CONTRACT_ID:-11111111-1111-1111-1111-111111111111}" +WORK="${NOVA_WORK_DIR:-/tmp/nova_platform_run}" +TF_DIR="$WORK/tf" + +[ -d "$TF_DIR" ] || { echo "FAIL: work dir $TF_DIR not found (run run_codegen.sh first)" >&2; exit 1; } + +stream() { + local log="$1"; shift + if [ "$QUIET" = "1" ]; then + "$@" > "$log" 2>&1 + else + "$@" 2>&1 | tee "$log" + fi +} + +run_hitl_gate() { + local _cid="$1" _env="$2" _ctx="$3" + if [ "$_env" = "dev" ]; then + echo "Environment is $_env — autonomous (no HITL gate)." + return 0 + fi + echo "Environment is $_env — HITL attestation gate required$_ctx." + local _approver="${GITHUB_ACTOR:-${FORGE_ACTOR:-}}" + if [ -z "$_approver" ]; then + echo "WARNING: no approver identity (GITHUB_ACTOR/FORGE_ACTOR unset)" >&2 + echo " the gate would block in a real CI run. Passing for local." >&2 + fi + python3 -c " +import os, sys +sys.path.insert(0, '.') +from core.hitl_gates import attest +from core import env as _envhelper +ok, reason = attest('$_cid', '$_env', '$_approver') +if ok: + print(f'HITL: {reason}') +else: + print(f'HITL BLOCK: {reason}', file=sys.stderr) + sys.exit(1) +" || { echo "FAIL: HITL gate blocked" >&2; return 1; } +} + +echo "=== Step 5: run Checkov on $TF_DIR/main.tf ===" +if [ "$QUIET" = "0" ]; then + checkov -f "$TF_DIR/main.tf" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ 2>&1 | tee "$WORK/checkov.json" +else + checkov -f "$TF_DIR/main.tf" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ > "$WORK/checkov.json" 2> "$WORK/checkov.err" +fi +[ -s "$WORK/checkov.json" ] || { echo "FAIL: checkov produced no output" >&2; exit 1; } +echo "" +echo "checkov summary: $(python3 -c "import json; d=json.load(open('$WORK/checkov.json')); print(len(d.get('results',{}).get('failed_checks',[])), 'failed,', len(d.get('results',{}).get('passed_checks',[])), 'passed')")" + +echo "" +echo "=== Step 6: Checkov adapter → PolicyCheckResult (compliance details) ===" +python3 adapters/terraform/policy/checkov_adapter.py "$WORK/checkov.json" "$CONTRACT_ID" > "$WORK/pcr.json" || { echo "FAIL: checkov adapter failed" >&2; exit 1; } +python3 -c " +import json +pcrs = json.load(open('$WORK/pcr.json')) +print(f'PolicyCheckResult: {len(pcrs)} record(s)') +print() +for pcr in pcrs: + sev = pcr.get('severity', 'info') + res = pcr.get('result', 'unknown') + rule = pcr.get('ruleId', 'unknown') + msg = pcr.get('message', '') + marker = 'PASS' if res == 'pass' else 'FAIL' if res == 'fail' else 'SKIP' if res == 'skipped' else res.upper() + print(f' [{marker}] {sev:8s} {rule:30s} {msg}') +" + +echo "" +echo "=== Step 7: confidence signal compute ===" +python3 < "$WORK/signal.json" || { echo "FAIL: confidence signal failed" >&2; exit 1; } +import json +import core.confidence_signal as c +pcr = json.load(open("$WORK/pcr.json")) +inputs = { + "policy": pcr, + "validation": {"schema": True, "stack_resolved": True, "tf_validated": True, "tf_planned": True}, + "freshness": {"age_days": 0, "max_age_days": 7}, + "source": {"submitter": "consumer", "commit_sha": "consumer-sha", "signed": False}, + "history": {"prior_rollbacks": 0, "prior_policy_fails": 0}, + "nfrs": {"conformance": None}, +} +sig = c.compute("$CONTRACT_ID", "dev", inputs) +print(json.dumps({"score": sig.score, "band": sig.band, "perInput": sig.perInput, "reasonCodes": sig.reasonCodes}, indent=2)) +PY +BAND=$(python3 -c "import json; print(json.load(open('$WORK/signal.json'))['band'])") +SCORE=$(python3 -c "import json; print(round(json.load(open('$WORK/signal.json'))['score'],3))") +echo "confidence: score=$SCORE band=$BAND" +[ "$BAND" = "pass" ] || { echo "FAIL: confidence band is $BAND, expected pass for dev" >&2; exit 1; } + +echo "" +echo "=== Step 7b: HITL attestation gate (qa/prod/dr only) ===" +RESOLVED_ENV=$(python3 -c "import yaml; print(yaml.safe_load(open('$CONTRACT')).get('environment','dev'))" 2>/dev/null || echo "dev") +if [ -n "$ENVIRONMENT_OVERRIDE" ]; then + RESOLVED_ENV="$ENVIRONMENT_OVERRIDE" +fi +run_hitl_gate "$CONTRACT_ID" "$RESOLVED_ENV" "" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; } + +echo "" +echo "=== Step 8: write evidence event to DynamoDB outbox ===" +STACK_NAME=$(python3 -c "import json; print(json.load(open('$WORK/stack.json'))['stack']['name'])") +python3 < "$WORK/event.json" || { echo "FAIL: event build failed" >&2; exit 1; } +import json, datetime +sig = json.load(open("$WORK/signal.json")) +event = { + "contractId": "$CONTRACT_ID", + "eventType": "CONFIDENCE_COMPUTED", + "ts": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "environment": "dev", + "stack": "$STACK_NAME", + "score": sig["score"], + "band": sig["band"], + "prev_event_hash": "GENESIS", +} +print(json.dumps(event, indent=2)) +PY +python3 core/outbox_writer.py "$WORK/event.json" > "$WORK/outbox_item.json" || { echo "FAIL: outbox write failed" >&2; exit 1; } +echo "outbox: $(python3 -c "import json; d=json.load(open('$WORK/outbox_item.json')); print('contractId=', d['contractId'], 'hash=', d['hash'][:16]+'...')")" + +echo "" +echo "=== Step 9: publish outputs to SSM + GitHub PR comment ===" +cd "$TF_DIR" +TF_OUTPUTS=$(terraform output -json 2>/dev/null || echo "{}") +cd "$ROOT" +python3 < "$WORK/outputs_step.json" 2>/dev/null || true +import json, sys +sys.path.insert(0, "$ROOT") +from core.output_publisher import publish_to_ssm, format_comment, post_github_comment +tf_raw = json.loads('''$TF_OUTPUTS''') +outputs = {k: v.get("value") if isinstance(v, dict) else v for k, v in tf_raw.items()} +ssm_results = publish_to_ssm(outputs, "dev", "$CONTRACT_ID") +comment = format_comment(outputs, "dev", "$CONTRACT_ID", ssm_results) +posted = post_github_comment(comment) +print(json.dumps({"ssm": ssm_results, "posted": posted, "comment": comment})) +PY +if [ -f "$WORK/outputs_step.json" ]; then + echo "outputs published to SSM: $(python3 -c "import json; d=json.load(open('$WORK/outputs_step.json')); print(len([v for v in d.get('ssm',{}).values() if v]), 'parameters')" 2>/dev/null || echo "done")" + if [ "$QUIET" = "0" ]; then + python3 -c "import json; d=json.load(open('$WORK/outputs_step.json')); print(d.get('comment',''))" 2>/dev/null || true + fi +fi + +echo "" +# Uptime monitoring: sourced from run_uptime.sh +source "$ROOT/scripts/run_uptime.sh" + +echo "" +echo "=== POST-APPLY OK ===" +echo "Checkov → confidence ($BAND) → outbox → outputs → uptime" diff --git a/workflows-src/deploy.yml b/workflows-src/deploy.yml index d9906dc..219b4d4 100644 --- a/workflows-src/deploy.yml +++ b/workflows-src/deploy.yml @@ -155,7 +155,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: nova-terraform - path: /tmp/acdl_platform_run_v18/tf/*.tf + path: /tmp/nova_platform_run/tf/*.tf if-no-files-found: warn - name: Upload platform log