Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11e342a9f7 | |||
| 14be01d971 |
+10
-15
@@ -1,25 +1,20 @@
|
||||
{
|
||||
"phase": 1,
|
||||
"phase": 3,
|
||||
"stage": "verify",
|
||||
"milestone": "v1.0",
|
||||
"phase_role": "execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-20T19:00:00Z",
|
||||
"updated_at": "2026-08-20T19:45:00Z",
|
||||
"project": "nova-platform",
|
||||
"milestone_branch": "milestone/v1.0-nova-platform",
|
||||
"phase_branch": "phase/01-contract-surface-schemas-resolver",
|
||||
"phase_0_ship": {
|
||||
"tag": "v0.1.0",
|
||||
"merged_to_milestone": true,
|
||||
"merge_commit": "7835c2a",
|
||||
"phase_branch_deleted": true,
|
||||
"local_only": true,
|
||||
"release_pending": "NOVA_FORGE_TOKEN blank"
|
||||
},
|
||||
"phase_1_verify": {
|
||||
"phase_branch": "phase/03-l1-primitives-registry",
|
||||
"phase_0_ship": {"tag": "v0.1.0", "local_only": true},
|
||||
"phase_1_ship": {"tag": "v0.1.1", "local_only": true},
|
||||
"phase_2_ship": {"tag": "v0.1.2", "local_only": true},
|
||||
"phase_3_verify": {
|
||||
"tests_pass": true,
|
||||
"tests_count": 36,
|
||||
"reqs_covered": ["REQ-01", "REQ-02", "REQ-03", "REQ-04", "REQ-05", "REQ-06", "REQ-23", "REQ-24", "REQ-27", "REQ-28"]
|
||||
"tests_count": 68,
|
||||
"reqs_covered": ["REQ-10", "REQ-11", "REQ-13", "REQ-34"]
|
||||
},
|
||||
"next_phase": "phase/02-terraform-adapter-engine-boundary"
|
||||
"next_phase": "phase/04-l2-patterns-bootstrap-platform"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
from adapters.terraform.adapter import adapt
|
||||
|
||||
__all__ = ["adapt"]
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Nova Platform — Terraform Adapter.
|
||||
|
||||
The ONLY engine-specific code in the platform (per REQ-09, verified by
|
||||
tests/test_engine_boundary.py). Loads modules/registry.json internally
|
||||
to map module -> terraform_dir (per D-037/C-1 grill fix — the resolver
|
||||
does NOT put a `source` field in the stack; the adapter resolves it
|
||||
here, inside the engine boundary).
|
||||
|
||||
Stateless assembler: no `terraform` CLI invocation, no state files, no
|
||||
plan files. Emits Terraform HCL: one `module "x" { source = ...; <inputs> }`
|
||||
block per stack resource.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _load_registry(repo_root):
|
||||
"""Load modules/registry.json -> {module_name: terraform_dir}."""
|
||||
registry_path = os.path.join(str(repo_root), "modules", "registry.json")
|
||||
with open(registry_path) as fh:
|
||||
registry = json.load(fh)
|
||||
return {name: list(versions.values())[0].get("terraform_dir")
|
||||
for name, versions in registry.items()
|
||||
if list(versions.values())[0].get("terraform_dir")}
|
||||
|
||||
|
||||
def _tf_value(value):
|
||||
"""Render a Python value as an HCL expression."""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if isinstance(value, (int, float)):
|
||||
return str(value)
|
||||
if isinstance(value, list):
|
||||
return "[" + ", ".join(_tf_value(v) for v in value) + "]"
|
||||
if isinstance(value, dict):
|
||||
return "{ " + ", ".join(f"{k} = {_tf_value(v)}" for k, v in value.items()) + " }"
|
||||
return json.dumps(str(value))
|
||||
|
||||
|
||||
def _emit_module_block(resource, terraform_dirs, repo_root):
|
||||
"""Emit one `module "x" { source = ...; <inputs> }` block."""
|
||||
module_name = resource["module"]
|
||||
rid = module_name.replace("-", "_")
|
||||
tf_dir = terraform_dirs.get(module_name)
|
||||
if tf_dir is None:
|
||||
raise ValueError(f"module '{module_name}' has no terraform_dir in registry")
|
||||
source = os.path.join(str(repo_root), tf_dir)
|
||||
lines = [f'module "{rid}" {{', f' source = "{source}"']
|
||||
for key, val in resource.get("inputs", {}).items():
|
||||
if key == "region":
|
||||
continue
|
||||
lines.append(f" {key} = {_tf_value(val)}")
|
||||
lines.append("}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def adapt(stack, repo_root):
|
||||
"""Compile a flat stack dict to Terraform HCL.
|
||||
|
||||
Args:
|
||||
stack: a flat stack dict conforming to schemas/stack.schema.json
|
||||
(NO `source` field per D-037 — the adapter resolves
|
||||
module -> terraform_dir via the registry).
|
||||
repo_root: Path to the repo root (the adapter loads
|
||||
modules/registry.json from here).
|
||||
|
||||
Returns:
|
||||
A string of Terraform HCL with one `module "x" {}` block per
|
||||
stack resource.
|
||||
"""
|
||||
terraform_dirs = _load_registry(repo_root)
|
||||
blocks = []
|
||||
for resource in stack.get("resources", []):
|
||||
blocks.append(_emit_module_block(resource, terraform_dirs, repo_root))
|
||||
return "\n\n".join(blocks) + "\n"
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
import sys
|
||||
argv = argv or sys.argv[1:]
|
||||
if len(argv) < 1:
|
||||
print("usage: adapter.py <stack.json> [out.tf]", file=sys.stderr)
|
||||
return 2
|
||||
stack_path = argv[0]
|
||||
out_path = argv[1] if len(argv) > 1 else None
|
||||
repo_root = Path(__file__).resolve().parent.parent.parent
|
||||
with open(stack_path) as fh:
|
||||
stack = json.load(fh)
|
||||
hcl = adapt(stack, repo_root)
|
||||
if out_path:
|
||||
with open(out_path, "w") as fh:
|
||||
fh.write(hcl)
|
||||
else:
|
||||
print(hcl)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,36 @@
|
||||
# Module Catalog
|
||||
|
||||
Every module's full documentation lives next to its code under
|
||||
`modules/l1/<name>/README.md` or `modules/l2/<name>/README.md` (per
|
||||
D-028). This page is the index: it lists the available modules and
|
||||
links to their per-module docs.
|
||||
|
||||
## L1 primitives (13)
|
||||
|
||||
| Module | Stack type | Multi-resource? | Docs |
|
||||
|-----------------|-------------------------------|-----------------|-----------------------------------------------|
|
||||
| `s3` | `aws:s3:bucket` | no | [modules/l1/s3/README.md](../../modules/l1/s3/README.md) |
|
||||
| `vpc` | `aws:ec2:vpc` | yes | [modules/l1/vpc/README.md](../../modules/l1/vpc/README.md) |
|
||||
| `ecs-cluster` | `aws:ecs:cluster` | no | [modules/l1/ecs-cluster/README.md](../../modules/l1/ecs-cluster/README.md) |
|
||||
| `ecs-service` | `aws:ecs:service` | yes | [modules/l1/ecs-service/README.md](../../modules/l1/ecs-service/README.md) |
|
||||
| `iam-role` | `aws:iam:role` | no | [modules/l1/iam-role/README.md](../../modules/l1/iam-role/README.md) |
|
||||
| `alb` | `aws:alb` | yes | [modules/l1/alb/README.md](../../modules/l1/alb/README.md) |
|
||||
| `ecr` | `aws:ecr:repository` | no | [modules/l1/ecr/README.md](../../modules/l1/ecr/README.md) |
|
||||
| `cloudfront` | `aws:cloudfront:distribution` | no | [modules/l1/cloudfront/README.md](../../modules/l1/cloudfront/README.md) |
|
||||
| `waf` | `aws:waf:web_acl` | no | [modules/l1/waf/README.md](../../modules/l1/waf/README.md) |
|
||||
| `rds` | `aws:rds:instance` | no | [modules/l1/rds/README.md](../../modules/l1/rds/README.md) |
|
||||
| `kms-key` | `aws:kms:key` | no | [modules/l1/kms-key/README.md](../../modules/l1/kms-key/README.md) |
|
||||
| `dynamodb` | `aws:dynamodb:table` | no | [modules/l1/dynamodb/README.md](../../modules/l1/dynamodb/README.md) |
|
||||
| `uptime` | `aws:uptime:monitor` | no | [modules/l1/uptime/README.md](../../modules/l1/uptime/README.md) |
|
||||
|
||||
## L2 compositions (2)
|
||||
|
||||
| Module | Composes | Docs |
|
||||
|------------------|---------------------------------------------|---------------------------------------------------|
|
||||
| `microservice` | vpc + ecs-cluster + ecs-service + alb + ecr | [modules/l2/microservice/README.md](../../modules/l2/microservice/README.md) |
|
||||
| `static-assets` | s3 + cloudfront | [modules/l2/static-assets/README.md](../../modules/l2/static-assets/README.md) |
|
||||
|
||||
## See also
|
||||
|
||||
- [modules/README.md](../../modules/README.md) — L1/L2 distinction, registry format, how to add a module.
|
||||
- [modules/README-TEMPLATE.md](../../modules/README-TEMPLATE.md) — per-module doc template.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Module: `<name>`
|
||||
|
||||
> Copy this template into `modules/l1/<name>/README.md` or
|
||||
> `modules/l2/<name>/README.md` and fill in the placeholders. Sections
|
||||
> marked **DROP** are intentionally omitted from nova modules
|
||||
> (D-029): do **not** add `NFRs` or `Compliance` sections.
|
||||
|
||||
## Overview
|
||||
|
||||
One-paragraph description of what this module provisions, the stack
|
||||
type(s) it exposes, and when to reach for it. Mention whether it is L1
|
||||
(single primitive) or L2 (composition of L1s), and whether it is
|
||||
multi-resource.
|
||||
|
||||
- **Stack type:** `aws:<service>:<resource>`
|
||||
- **Kind:** `l1` (or `l2`)
|
||||
- **Version:** `1.0.0`
|
||||
|
||||
## Resources
|
||||
|
||||
List the concrete cloud resources the Terraform adapter creates. For L1
|
||||
single-resource modules this is one row; for multi-resource L1s mirror
|
||||
the `resources[]` array in `interface.json`.
|
||||
|
||||
| Stack type | Terraform resource | Notes |
|
||||
|-------------------------|------------------------------------|----------------------------------|
|
||||
| `aws:s3:bucket` | `aws_s3_bucket` | The bucket itself |
|
||||
| `aws:s3:bucket` | `aws_s3_bucket_versioning` | Versioning sibling |
|
||||
| `aws:s3:bucket` | `aws_s3_bucket_server_side_encryption_configuration` | SSE config sibling |
|
||||
|
||||
For L2 modules, list the L1 modules composed via `module` blocks in
|
||||
`terraform/main.tf` instead.
|
||||
|
||||
## Inputs
|
||||
|
||||
Mirror `interface.json` → `inputs`. Mark required inputs with **yes**.
|
||||
|
||||
| Name | Type | Required | Default | Description |
|
||||
|----------------|---------|----------|---------------|-----------------------------------|
|
||||
| `bucket_name` | string | yes | — | Globally-unique S3 bucket name |
|
||||
| `region` | string | yes | — | AWS region |
|
||||
| `kms_key_arn` | string | no | `null` | CMK ARN for SSE-KMS |
|
||||
| `enabled` | boolean | no | `true` | Feature flag |
|
||||
| `tags` | map | no | `{}` | Tags merged with module defaults |
|
||||
|
||||
## Outputs
|
||||
|
||||
Mirror `interface.json` → `outputs`.
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------------------------------|--------|----------------------------------------------|
|
||||
| `bucket_arn` | arn | The S3 bucket ARN |
|
||||
| `bucket_name` | string | The bucket name |
|
||||
| `bucket_regional_domain_name` | string | The bucket regional domain name |
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "bucket" {
|
||||
source = "modules/l1/s3/terraform"
|
||||
|
||||
bucket_name = "nova-prod-assets"
|
||||
region = "us-east-1"
|
||||
|
||||
tags = {
|
||||
"nova:owner" = "team-platform"
|
||||
"nova:environment" = "prod"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or as a flat-stack contract entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"module": "s3",
|
||||
"version": "1.0.0",
|
||||
"inputs": {
|
||||
"bucket_name": "nova-prod-assets",
|
||||
"region": "us-east-1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
This module follows the registry semver contract: bump the patch/minor
|
||||
version in `interface.json` and `modules/registry.json` for any
|
||||
input/output/behavior change. Breaking changes (renamed inputs,
|
||||
removed outputs, changed defaults) require a major bump and a new
|
||||
registry entry; the previous version is marked `deprecated: true` and
|
||||
remains selectable by pinned contracts. See `modules/README.md` for
|
||||
the registry format and the resolver's version-selection rules.
|
||||
|
||||
<!-- DROP: NFRs — out of scope for nova v1 (D-029) -->
|
||||
<!-- DROP: Compliance / attestation — out of scope for nova v1 (D-029) -->
|
||||
@@ -0,0 +1,127 @@
|
||||
# Nova Modules
|
||||
|
||||
Nova ships a two-tier module library. Modules are **engine-agnostic**:
|
||||
their contract is declared in `interface.json` (stack types like
|
||||
`aws:s3:bucket`), and an adapter translates the contract to a concrete
|
||||
IaC engine (Terraform today; Pulumi/CDK possible later). All L1 modules
|
||||
in this repo ship a Terraform adapter under `terraform/`.
|
||||
|
||||
## L1 vs L2
|
||||
|
||||
| Tier | What it is | Composes | Examples |
|
||||
|------|----------------------------------------------------------------------------|---------------------|-----------------------------------|
|
||||
| L1 | A single primitive resource (or tightly-coupled resource group) on a cloud | One stack resource | `s3`, `vpc`, `ecs-cluster`, `alb` |
|
||||
| L2 | A composition of L1s expressing an architectural pattern | Multiple L1 modules | `microservice`, `static-assets` |
|
||||
|
||||
- **L1** = one entry in the flat stack. Even multi-resource L1s (e.g.
|
||||
`vpc`, `ecs-service`, `alb`) emit a single stack entry; their
|
||||
`interface.json` lists the child resources in a `resources[]` array
|
||||
for documentation, but the resolver does **not** expand them
|
||||
(D-012).
|
||||
- **L2** = also one opaque entry in the flat stack. The L2's
|
||||
`terraform/main.tf` composes L1 modules internally via `module` blocks
|
||||
(D-012). The L2 exposes its own L2-level `inputs`/`outputs`; children
|
||||
and wiring live in terraform, not in the interface.
|
||||
|
||||
## Registry format
|
||||
|
||||
`modules/registry.json` maps `module_name -> version -> entry`:
|
||||
|
||||
```json
|
||||
{
|
||||
"s3": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/s3/interface.json",
|
||||
"terraform_dir": "modules/l1/s3/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `interface` — path to the `interface.json` declaring the contract.
|
||||
- `terraform_dir` — path to the adapter's Terraform module directory
|
||||
(the flat stack's `source` field).
|
||||
- `kind` — `"l1"` or `"l2"`.
|
||||
- `deprecated` — when `true`, the resolver warns and selects the latest
|
||||
non-deprecated version unless the caller pins a version.
|
||||
|
||||
## interface.json shape (D-014)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "s3",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:s3:bucket",
|
||||
"description": "...",
|
||||
"inputs": { "<name>": { "type": "...", "required": true, "description": "..." } },
|
||||
"outputs": { "<name>": { "type": "...", "description": "..." } },
|
||||
"resources": [ { "type": "aws:ec2:vpc", "inputs": [...], "outputs": [...] } ]
|
||||
}
|
||||
```
|
||||
|
||||
- `type` is **stack-typed** — `aws:<service>:<resource>` (e.g.
|
||||
`aws:s3:bucket`), **not** the Terraform resource name
|
||||
(`aws_s3_bucket`). The adapter performs the translation.
|
||||
- `resources[]` is present only on multi-resource L1s (`vpc`,
|
||||
`ecs-service`, `alb`); it documents the child stack types but does not
|
||||
drive resolution.
|
||||
- **Dropped** per D-014: `nfrs` (confidence signal, out of scope) and
|
||||
`intra_refs` (wire engine, eliminated by D-012). Do not re-add them.
|
||||
|
||||
## Conventions shared by all L1 Terraform adapters
|
||||
|
||||
- `terraform/versions.tf` pins `required_version = ">= 1.9, < 1.10"` and
|
||||
`aws ~> 5.0`.
|
||||
- Every resource is guarded by `count = var.enabled ? 1 : 0`; the
|
||||
`enabled` input defaults to `true`.
|
||||
- `locals.tf` merges module-default tags with caller-supplied `var.tags`:
|
||||
```hcl
|
||||
tags = merge({ "nova:owner" = "nova", "nova:environment" = "dev" }, var.tags)
|
||||
```
|
||||
- Every `interface.json` input has a matching `variable` block; every
|
||||
output has a matching `output` block. Outputs return `null` (or `[]`)
|
||||
when `enabled = false`.
|
||||
|
||||
## How to add a module
|
||||
|
||||
1. Pick the tier. New primitive → L1. New pattern composing existing
|
||||
L1s → L2.
|
||||
2. Create `modules/l1/<name>/` (or `modules/l2/<name>/`).
|
||||
3. Author `interface.json` (L1) or `interface.json` + L2 terraform that
|
||||
composes L1s via `module` blocks. Use `modules/README-TEMPLATE.md`
|
||||
as the per-module doc template.
|
||||
4. Author `terraform/{main,variables,outputs,versions,locals}.tf`
|
||||
following the conventions above.
|
||||
5. Add an entry to `modules/registry.json` and a row to the catalog at
|
||||
`docs/modules/index.md`.
|
||||
6. Verify: `python3 -c "import json; json.load(open('modules/l1/<name>/interface.json'))"`
|
||||
and `terraform validate` inside `terraform/`.
|
||||
|
||||
## L1 primitives (13)
|
||||
|
||||
| Module | Stack type | Multi-resource? | Description |
|
||||
|-----------------|-------------------------------|-----------------|----------------------------------------------------------|
|
||||
| `s3` | `aws:s3:bucket` | no | S3 bucket with versioning + SSE-KMS |
|
||||
| `vpc` | `aws:ec2:vpc` | yes | VPC + subnets + route table + IGW |
|
||||
| `ecs-cluster` | `aws:ecs:cluster` | no | ECS cluster |
|
||||
| `ecs-service` | `aws:ecs:service` | yes | ECS task definition + service |
|
||||
| `iam-role` | `aws:iam:role` | no | IAM role with assume-role policy |
|
||||
| `alb` | `aws:alb` | yes | ALB + target group + listener |
|
||||
| `ecr` | `aws:ecr:repository` | no | ECR repository with scan-on-push |
|
||||
| `cloudfront` | `aws:cloudfront:distribution` | no | CloudFront distribution with a single origin |
|
||||
| `waf` | `aws:waf:web_acl` | no | WAFv2 web ACL (regional, default allow) |
|
||||
| `rds` | `aws:rds:instance` | no | RDS Postgres DB instance |
|
||||
| `kms-key` | `aws:kms:key` | no | KMS CMK with alias |
|
||||
| `dynamodb` | `aws:dynamodb:table` | no | DynamoDB table (PAY_PER_REQUEST default) |
|
||||
| `uptime` | `aws:uptime:monitor` | no | Uptime monitor (CloudWatch alarm stand-in) |
|
||||
|
||||
## L2 compositions (2)
|
||||
|
||||
| Module | Composes | Description |
|
||||
|------------------|-------------------------------------------|----------------------------------------------|
|
||||
| `microservice` | vpc + ecs-cluster + ecs-service + alb + ecr | Container microservice with public ALB |
|
||||
| `static-assets` | s3 + cloudfront | Static site fronted by CloudFront |
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: alb
|
||||
|
||||
Application Load Balancer primitive (multi-resource: LB + target group + listener; stack type `aws:alb`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "alb",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:alb",
|
||||
"description": "Application Load Balancer primitive (multi-resource: LB + target group + listener). Engine-agnostic stack types aws:alb + aws:alb:targetgroup + aws:alb:listener; the Terraform adapter translates to aws_lb/aws_lb_target_group/aws_lb_listener.",
|
||||
"inputs": {
|
||||
"lb_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the load balancer.",
|
||||
"required": true
|
||||
},
|
||||
"subnet_ids": {
|
||||
"type": "list",
|
||||
"description": "List of subnet ids the LB is deployed into.",
|
||||
"required": true
|
||||
},
|
||||
"target_group_port": {
|
||||
"type": "integer",
|
||||
"default": 80,
|
||||
"description": "Port the target group forwards to."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"lb_arn": {
|
||||
"type": "arn",
|
||||
"description": "The load balancer ARN."
|
||||
},
|
||||
"dns_name": {
|
||||
"type": "string",
|
||||
"description": "The load balancer DNS name."
|
||||
},
|
||||
"target_group_arn": {
|
||||
"type": "arn",
|
||||
"description": "The target group ARN."
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "aws:alb",
|
||||
"description": "The Application Load Balancer.",
|
||||
"inputs": ["lb_name", "subnet_ids"],
|
||||
"outputs": ["lb_arn", "dns_name"]
|
||||
},
|
||||
{
|
||||
"type": "aws:alb:targetgroup",
|
||||
"description": "Target group on the LB port.",
|
||||
"inputs": ["lb_name", "target_group_port"],
|
||||
"outputs": ["target_group_arn"]
|
||||
},
|
||||
{
|
||||
"type": "aws:alb:listener",
|
||||
"description": "Listener forwarding to the target group.",
|
||||
"inputs": ["target_group_port", "target_group_arn"],
|
||||
"outputs": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
|
||||
# Target group requires a vpc_id. The L1 interface does not expose it as
|
||||
# an input by design (kept minimal per D-014); the caller is expected to
|
||||
# supply subnets in a single VPC. When a vpc_id input is added later, this
|
||||
# local can be removed. For now, null forces the caller to set it via a
|
||||
# provider-level default or an extension.
|
||||
vpc_id = null
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
resource "aws_lb" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = var.lb_name
|
||||
load_balancer_type = "application"
|
||||
subnets = var.subnet_ids
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_lb_target_group" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name_prefix = "${var.lb_name}-"
|
||||
port = var.target_group_port
|
||||
protocol = "HTTP"
|
||||
target_type = "ip"
|
||||
vpc_id = local.vpc_id
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_lb_listener" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
load_balancer_arn = aws_lb.this[0].id
|
||||
port = var.target_group_port
|
||||
protocol = "HTTP"
|
||||
|
||||
default_action {
|
||||
type = "forward"
|
||||
target_group_arn = aws_lb_target_group.this[0].arn
|
||||
}
|
||||
|
||||
depends_on = [aws_lb_target_group.this]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
output "lb_arn" {
|
||||
value = var.enabled ? aws_lb.this[0].arn : null
|
||||
description = "The load balancer ARN."
|
||||
}
|
||||
|
||||
output "dns_name" {
|
||||
value = var.enabled ? aws_lb.this[0].dns_name : null
|
||||
description = "The load balancer DNS name."
|
||||
}
|
||||
|
||||
output "target_group_arn" {
|
||||
value = var.enabled ? aws_lb_target_group.this[0].arn : null
|
||||
description = "The target group ARN."
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
variable "lb_name" {
|
||||
type = string
|
||||
description = "Name of the load balancer."
|
||||
}
|
||||
|
||||
variable "subnet_ids" {
|
||||
type = list(string)
|
||||
description = "List of subnet ids the LB is deployed into."
|
||||
}
|
||||
|
||||
variable "target_group_port" {
|
||||
type = number
|
||||
description = "Port the target group forwards to."
|
||||
default = 80
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: cloudfront
|
||||
|
||||
CloudFront distribution primitive (stack type `aws:cloudfront:distribution`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "cloudfront",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:cloudfront:distribution",
|
||||
"description": "CloudFront distribution primitive (engine-agnostic stack type aws:cloudfront:distribution; the Terraform adapter translates to aws_cloudfront_distribution).",
|
||||
"inputs": {
|
||||
"distribution_name": {
|
||||
"type": "string",
|
||||
"description": "Name (comment) of the CloudFront distribution.",
|
||||
"required": true
|
||||
},
|
||||
"origin_domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name of the origin (e.g. an S3 bucket regional domain or ALB DNS).",
|
||||
"required": true
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"distribution_arn": {
|
||||
"type": "arn",
|
||||
"description": "The CloudFront distribution ARN."
|
||||
},
|
||||
"domain_name": {
|
||||
"type": "string",
|
||||
"description": "The CloudFront distribution domain name."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
|
||||
origin_id = "${var.distribution_name}-origin"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
resource "aws_cloudfront_distribution" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
comment = var.distribution_name
|
||||
enabled = true
|
||||
price_class = "PriceClass_100"
|
||||
tags = local.tags
|
||||
|
||||
origin {
|
||||
domain_name = var.origin_domain
|
||||
origin_id = local.origin_id
|
||||
}
|
||||
|
||||
default_cache_behavior {
|
||||
allowed_methods = ["GET", "HEAD", "OPTIONS"]
|
||||
cached_methods = ["GET", "HEAD"]
|
||||
target_origin_id = local.origin_id
|
||||
|
||||
forwarded_values {
|
||||
query_string = false
|
||||
|
||||
cookies {
|
||||
forward = "none"
|
||||
}
|
||||
}
|
||||
|
||||
viewer_protocol_policy = "redirect-to-https"
|
||||
min_ttl = 0
|
||||
default_ttl = 3600
|
||||
max_ttl = 86400
|
||||
}
|
||||
|
||||
restrictions {
|
||||
geo_restriction {
|
||||
restriction_type = "none"
|
||||
}
|
||||
}
|
||||
|
||||
viewer_certificate {
|
||||
cloudfront_default_certificate = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "distribution_arn" {
|
||||
value = var.enabled ? aws_cloudfront_distribution.this[0].arn : null
|
||||
description = "The CloudFront distribution ARN."
|
||||
}
|
||||
|
||||
output "domain_name" {
|
||||
value = var.enabled ? aws_cloudfront_distribution.this[0].domain_name : null
|
||||
description = "The CloudFront distribution domain name."
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
variable "distribution_name" {
|
||||
type = string
|
||||
description = "Name (comment) of the CloudFront distribution."
|
||||
}
|
||||
|
||||
variable "origin_domain" {
|
||||
type = string
|
||||
description = "Domain name of the origin (e.g. an S3 bucket regional domain or ALB DNS)."
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: dynamodb
|
||||
|
||||
DynamoDB table primitive (stack type `aws:dynamodb:table`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "dynamodb",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:dynamodb:table",
|
||||
"description": "DynamoDB table primitive (engine-agnostic stack type aws:dynamodb:table; the Terraform adapter translates to aws_dynamodb_table).",
|
||||
"inputs": {
|
||||
"table_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the DynamoDB table.",
|
||||
"required": true
|
||||
},
|
||||
"hash_key": {
|
||||
"type": "string",
|
||||
"description": "Name of the partition (hash) key.",
|
||||
"required": true
|
||||
},
|
||||
"billing_mode": {
|
||||
"type": "string",
|
||||
"default": "PAY_PER_REQUEST",
|
||||
"description": "Billing mode: PAY_PER_REQUEST or PROVISIONED."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"table_arn": {
|
||||
"type": "arn",
|
||||
"description": "The DynamoDB table ARN."
|
||||
},
|
||||
"table_name": {
|
||||
"type": "string",
|
||||
"description": "The DynamoDB table name (echoes the input)."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
resource "aws_dynamodb_table" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = var.table_name
|
||||
billing_mode = var.billing_mode
|
||||
hash_key = var.hash_key
|
||||
tags = local.tags
|
||||
|
||||
attribute {
|
||||
name = var.hash_key
|
||||
type = "S"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "table_arn" {
|
||||
value = var.enabled ? aws_dynamodb_table.this[0].arn : null
|
||||
description = "The DynamoDB table ARN."
|
||||
}
|
||||
|
||||
output "table_name" {
|
||||
value = var.enabled ? aws_dynamodb_table.this[0].name : null
|
||||
description = "The DynamoDB table name (echoes the input)."
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
variable "table_name" {
|
||||
type = string
|
||||
description = "Name of the DynamoDB table."
|
||||
}
|
||||
|
||||
variable "hash_key" {
|
||||
type = string
|
||||
description = "Name of the partition (hash) key."
|
||||
}
|
||||
|
||||
variable "billing_mode" {
|
||||
type = string
|
||||
description = "Billing mode: PAY_PER_REQUEST or PROVISIONED."
|
||||
default = "PAY_PER_REQUEST"
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: ecr
|
||||
|
||||
ECR repository primitive (stack type `aws:ecr:repository`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "ecr",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:ecr:repository",
|
||||
"description": "ECR repository primitive (engine-agnostic stack type aws:ecr:repository; the Terraform adapter translates to aws_ecr_repository).",
|
||||
"inputs": {
|
||||
"repository_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the ECR repository.",
|
||||
"required": true
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"repository_url": {
|
||||
"type": "string",
|
||||
"description": "The ECR repository URL."
|
||||
},
|
||||
"repository_arn": {
|
||||
"type": "arn",
|
||||
"description": "The ECR repository ARN."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
resource "aws_ecr_repository" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = var.repository_name
|
||||
image_tag_mutability = "MUTABLE"
|
||||
tags = local.tags
|
||||
|
||||
image_scanning_configuration {
|
||||
scan_on_push = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "repository_url" {
|
||||
value = var.enabled ? aws_ecr_repository.this[0].repository_url : null
|
||||
description = "The ECR repository URL."
|
||||
}
|
||||
|
||||
output "repository_arn" {
|
||||
value = var.enabled ? aws_ecr_repository.this[0].arn : null
|
||||
description = "The ECR repository ARN."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
variable "repository_name" {
|
||||
type = string
|
||||
description = "Name of the ECR repository."
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: ecs-cluster
|
||||
|
||||
ECS cluster primitive (stack type `aws:ecs:cluster`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "ecs-cluster",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:ecs:cluster",
|
||||
"description": "ECS cluster primitive (engine-agnostic stack type aws:ecs:cluster; the Terraform adapter translates to aws_ecs_cluster).",
|
||||
"inputs": {
|
||||
"cluster_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the ECS cluster.",
|
||||
"required": true
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"cluster_arn": {
|
||||
"type": "arn",
|
||||
"description": "The ECS cluster ARN."
|
||||
},
|
||||
"cluster_name": {
|
||||
"type": "string",
|
||||
"description": "The ECS cluster name (echoes the input)."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
resource "aws_ecs_cluster" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = var.cluster_name
|
||||
tags = local.tags
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "cluster_arn" {
|
||||
value = var.enabled ? aws_ecs_cluster.this[0].arn : null
|
||||
description = "The ECS cluster ARN."
|
||||
}
|
||||
|
||||
output "cluster_name" {
|
||||
value = var.enabled ? aws_ecs_cluster.this[0].name : null
|
||||
description = "The ECS cluster name (echoes the input)."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
variable "cluster_name" {
|
||||
type = string
|
||||
description = "Name of the ECS cluster."
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: ecs-service
|
||||
|
||||
ECS service primitive (multi-resource: task definition + service; stack type `aws:ecs:service`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "ecs-service",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:ecs:service",
|
||||
"description": "ECS service primitive (multi-resource: task definition + service). Engine-agnostic stack types aws:ecs:taskdef + aws:ecs:service; the Terraform adapter translates to aws_ecs_task_definition/aws_ecs_service.",
|
||||
"inputs": {
|
||||
"service_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the ECS service (also used as the task definition family).",
|
||||
"required": true
|
||||
},
|
||||
"cluster_arn": {
|
||||
"type": "arn",
|
||||
"description": "ARN of the ECS cluster the service runs in.",
|
||||
"required": true
|
||||
},
|
||||
"task_definition": {
|
||||
"type": "string",
|
||||
"description": "Task definition ARN or family:revision to run. If supplied as a path/string JSON, the module creates an aws_ecs_task_definition.",
|
||||
"required": true
|
||||
},
|
||||
"desired_count": {
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"description": "Number of tasks to run."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"service_arn": {
|
||||
"type": "arn",
|
||||
"description": "The ECS service ARN."
|
||||
},
|
||||
"service_name": {
|
||||
"type": "string",
|
||||
"description": "The ECS service name (echoes the input)."
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "aws:ecs:taskdef",
|
||||
"description": "The ECS task definition (registered from task_definition input).",
|
||||
"inputs": ["service_name", "task_definition"],
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"type": "aws:ecs:service",
|
||||
"description": "The ECS service running the task definition on the cluster.",
|
||||
"inputs": ["service_name", "cluster_arn", "desired_count"],
|
||||
"outputs": ["service_arn", "service_name"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
resource "aws_ecs_task_definition" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
family = var.service_name
|
||||
container_definitions = var.task_definition
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_ecs_service" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = var.service_name
|
||||
cluster = var.cluster_arn
|
||||
task_definition = aws_ecs_task_definition.this[0].arn
|
||||
desired_count = var.desired_count
|
||||
tags = local.tags
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "service_arn" {
|
||||
value = var.enabled ? aws_ecs_service.this[0].id : null
|
||||
description = "The ECS service ARN."
|
||||
}
|
||||
|
||||
output "service_name" {
|
||||
value = var.enabled ? aws_ecs_service.this[0].name : null
|
||||
description = "The ECS service name (echoes the input)."
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
variable "service_name" {
|
||||
type = string
|
||||
description = "Name of the ECS service (also used as the task definition family)."
|
||||
}
|
||||
|
||||
variable "cluster_arn" {
|
||||
type = string
|
||||
description = "ARN of the ECS cluster the service runs in."
|
||||
}
|
||||
|
||||
variable "task_definition" {
|
||||
type = string
|
||||
description = "Task definition JSON string (container definitions). The module registers an aws_ecs_task_definition with family = service_name."
|
||||
}
|
||||
|
||||
variable "desired_count" {
|
||||
type = number
|
||||
description = "Number of tasks to run."
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: iam-role
|
||||
|
||||
IAM role primitive (stack type `aws:iam:role`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "iam-role",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:iam:role",
|
||||
"description": "IAM role primitive (engine-agnostic stack type aws:iam:role; the Terraform adapter translates to aws_iam_role).",
|
||||
"inputs": {
|
||||
"role_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the IAM role.",
|
||||
"required": true
|
||||
},
|
||||
"policy_document": {
|
||||
"type": "string",
|
||||
"description": "Assume-role policy document JSON string.",
|
||||
"required": true
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"role_arn": {
|
||||
"type": "arn",
|
||||
"description": "The IAM role ARN."
|
||||
},
|
||||
"role_name": {
|
||||
"type": "string",
|
||||
"description": "The IAM role name (echoes the input)."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
resource "aws_iam_role" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = var.role_name
|
||||
assume_role_policy = var.policy_document
|
||||
tags = local.tags
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "role_arn" {
|
||||
value = var.enabled ? aws_iam_role.this[0].arn : null
|
||||
description = "The IAM role ARN."
|
||||
}
|
||||
|
||||
output "role_name" {
|
||||
value = var.enabled ? aws_iam_role.this[0].name : null
|
||||
description = "The IAM role name (echoes the input)."
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
variable "role_name" {
|
||||
type = string
|
||||
description = "Name of the IAM role."
|
||||
}
|
||||
|
||||
variable "policy_document" {
|
||||
type = string
|
||||
description = "Assume-role policy document JSON string."
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: kms-key
|
||||
|
||||
KMS customer master key primitive (stack type `aws:kms:key`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "kms-key",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:kms:key",
|
||||
"description": "KMS customer master key primitive (engine-agnostic stack type aws:kms:key; the Terraform adapter translates to aws_kms_key).",
|
||||
"inputs": {
|
||||
"key_name": {
|
||||
"type": "string",
|
||||
"description": "Name (alias) of the KMS key.",
|
||||
"required": true
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"key_arn": {
|
||||
"type": "arn",
|
||||
"description": "The KMS key ARN."
|
||||
},
|
||||
"key_id": {
|
||||
"type": "string",
|
||||
"description": "The KMS key id."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
resource "aws_kms_key" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
description = "KMS key managed by nova L1 kms-key primitive."
|
||||
deletion_window_in_days = 30
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_kms_alias" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = "alias/${var.key_name}"
|
||||
target_key_id = aws_kms_key.this[0].key_id
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "key_arn" {
|
||||
value = var.enabled ? aws_kms_key.this[0].arn : null
|
||||
description = "The KMS key ARN."
|
||||
}
|
||||
|
||||
output "key_id" {
|
||||
value = var.enabled ? aws_kms_key.this[0].key_id : null
|
||||
description = "The KMS key id."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
variable "key_name" {
|
||||
type = string
|
||||
description = "Name (alias) of the KMS key."
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: rds
|
||||
|
||||
RDS DB instance primitive (stack type `aws:rds:instance`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "rds",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:rds:instance",
|
||||
"description": "RDS DB instance primitive (engine-agnostic stack type aws:rds:instance; the Terraform adapter translates to aws_db_instance).",
|
||||
"inputs": {
|
||||
"instance_name": {
|
||||
"type": "string",
|
||||
"description": "Name (identifier) of the RDS DB instance.",
|
||||
"required": true
|
||||
},
|
||||
"instance_class": {
|
||||
"type": "string",
|
||||
"default": "db.t3.micro",
|
||||
"description": "DB instance class."
|
||||
},
|
||||
"allocated_storage": {
|
||||
"type": "integer",
|
||||
"default": 20,
|
||||
"description": "Allocated storage in GiB."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"instance_endpoint": {
|
||||
"type": "string",
|
||||
"description": "The RDS DB instance endpoint (host:port)."
|
||||
},
|
||||
"instance_arn": {
|
||||
"type": "arn",
|
||||
"description": "The RDS DB instance ARN."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
resource "aws_db_instance" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
identifier = var.instance_name
|
||||
instance_class = var.instance_class
|
||||
allocated_storage = var.allocated_storage
|
||||
engine = "postgres"
|
||||
engine_version = "14"
|
||||
username = "nova"
|
||||
password = "changeme-rotate-me"
|
||||
skip_final_snapshot = true
|
||||
tags = local.tags
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "instance_endpoint" {
|
||||
value = var.enabled ? aws_db_instance.this[0].endpoint : null
|
||||
description = "The RDS DB instance endpoint (host:port)."
|
||||
}
|
||||
|
||||
output "instance_arn" {
|
||||
value = var.enabled ? aws_db_instance.this[0].arn : null
|
||||
description = "The RDS DB instance ARN."
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
variable "instance_name" {
|
||||
type = string
|
||||
description = "Name (identifier) of the RDS DB instance."
|
||||
}
|
||||
|
||||
variable "instance_class" {
|
||||
type = string
|
||||
description = "DB instance class."
|
||||
default = "db.t3.micro"
|
||||
}
|
||||
|
||||
variable "allocated_storage" {
|
||||
type = number
|
||||
description = "Allocated storage in GiB."
|
||||
default = 20
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: s3
|
||||
|
||||
S3 bucket primitive (stack type `aws:s3:bucket`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "s3",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:s3:bucket",
|
||||
"description": "S3 bucket primitive (engine-agnostic stack type aws:s3:bucket; the Terraform adapter translates to aws_s3_bucket).",
|
||||
"inputs": {
|
||||
"bucket_name": {
|
||||
"type": "string",
|
||||
"description": "Globally-unique S3 bucket name.",
|
||||
"required": true
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "AWS region the bucket is created in (provider-level; not a resource arg).",
|
||||
"required": true
|
||||
},
|
||||
"kms_key_arn": {
|
||||
"type": "string",
|
||||
"description": "ARN of the CMK for SSE-KMS; if absent, uses managed key (SSE-S3).",
|
||||
"required": false
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"bucket_arn": {
|
||||
"type": "arn",
|
||||
"description": "The S3 bucket ARN."
|
||||
},
|
||||
"bucket_name": {
|
||||
"type": "string",
|
||||
"description": "The bucket name (echoes the input)."
|
||||
},
|
||||
"bucket_regional_domain_name": {
|
||||
"type": "string",
|
||||
"description": "The bucket regional domain name (e.g. nova-bucket.s3.us-east-1.amazonaws.com)."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
locals {
|
||||
# SSE algorithm: KMS when a CMK ARN is supplied, else AES256 (SSE-S3).
|
||||
sse_algorithm = var.kms_key_arn != null ? "aws:kms" : "AES256"
|
||||
|
||||
# Tags: merge caller-supplied tags with the module defaults.
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
resource "aws_s3_bucket" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
bucket = var.bucket_name
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_versioning" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
bucket = aws_s3_bucket.this[0].id
|
||||
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
bucket = aws_s3_bucket.this[0].id
|
||||
|
||||
rule {
|
||||
apply_server_side_encryption_by_default {
|
||||
sse_algorithm = local.sse_algorithm
|
||||
kms_master_key_id = var.kms_key_arn
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
output "bucket_arn" {
|
||||
value = var.enabled ? aws_s3_bucket.this[0].arn : null
|
||||
description = "The S3 bucket ARN."
|
||||
}
|
||||
|
||||
output "bucket_name" {
|
||||
value = var.enabled ? aws_s3_bucket.this[0].id : null
|
||||
description = "The bucket name (echoes the input)."
|
||||
}
|
||||
|
||||
output "bucket_regional_domain_name" {
|
||||
value = var.enabled ? aws_s3_bucket.this[0].bucket_regional_domain_name : null
|
||||
description = "The bucket regional domain name (e.g. nova-bucket.s3.us-east-1.amazonaws.com)."
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
variable "bucket_name" {
|
||||
type = string
|
||||
description = "Globally-unique S3 bucket name."
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
type = string
|
||||
description = "AWS region the bucket is created in (provider-level; not a resource arg)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "kms_key_arn" {
|
||||
type = string
|
||||
description = "ARN of the CMK for SSE-KMS; if absent, uses managed key (SSE-S3)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: uptime
|
||||
|
||||
Uptime monitor primitive (simplified stand-in: a CloudWatch alarm; stack type `aws:uptime:monitor`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "uptime",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:uptime:monitor",
|
||||
"description": "Uptime monitor primitive (simplified stand-in: a CloudWatch alarm watching the target resource). Engine-agnostic stack type aws:uptime:monitor; the Terraform adapter translates to aws_cloudwatch_metric_alarm.",
|
||||
"inputs": {
|
||||
"monitor_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the uptime monitor (CloudWatch alarm).",
|
||||
"required": true
|
||||
},
|
||||
"target_arn": {
|
||||
"type": "arn",
|
||||
"description": "ARN of the target resource being monitored.",
|
||||
"required": true
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"monitor_arn": {
|
||||
"type": "arn",
|
||||
"description": "The CloudWatch alarm ARN (stand-in for the monitor ARN)."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Uptime monitor stand-in: a CloudWatch metric alarm referencing the
|
||||
# target resource ARN via dimensions. A future revision may swap this
|
||||
# for a Route 53 health check or CloudWatch composite alarm.
|
||||
resource "aws_cloudwatch_metric_alarm" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
alarm_name = var.monitor_name
|
||||
comparison_operator = "LessThanThreshold"
|
||||
evaluation_periods = 2
|
||||
metric_name = "RequestCount"
|
||||
namespace = "AWS/ApplicationELB"
|
||||
period = 60
|
||||
statistic = "Sum"
|
||||
threshold = 1
|
||||
alarm_description = "Uptime monitor (CloudWatch alarm stand-in) for target ${var.target_arn}."
|
||||
|
||||
dimensions = {
|
||||
LoadBalancer = var.target_arn
|
||||
}
|
||||
|
||||
tags = local.tags
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
output "monitor_arn" {
|
||||
value = var.enabled ? aws_cloudwatch_metric_alarm.this[0].arn : null
|
||||
description = "The CloudWatch alarm ARN (stand-in for the monitor ARN)."
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
variable "monitor_name" {
|
||||
type = string
|
||||
description = "Name of the uptime monitor (CloudWatch alarm)."
|
||||
}
|
||||
|
||||
variable "target_arn" {
|
||||
type = string
|
||||
description = "ARN of the target resource being monitored."
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: vpc
|
||||
|
||||
VPC primitive (multi-resource: VPC + subnets + route table + IGW; stack type `aws:ec2:vpc`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "vpc",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:ec2:vpc",
|
||||
"description": "VPC primitive (multi-resource: VPC + subnets + route table + internet gateway). Engine-agnostic stack types aws:ec2:vpc + aws:ec2:subnet + aws:ec2:routetable + aws:ec2:igw; the Terraform adapter translates to aws_vpc/aws_subnet/aws_route_table/aws_internet_gateway.",
|
||||
"inputs": {
|
||||
"cidr": {
|
||||
"type": "string",
|
||||
"description": "VPC CIDR block, e.g. 10.0.0.0/16.",
|
||||
"required": true
|
||||
},
|
||||
"azs": {
|
||||
"type": "list",
|
||||
"description": "List of availability zones, e.g. [\"us-east-1a\", \"us-east-1b\"]. One subnet is created per AZ.",
|
||||
"required": true
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"vpc_id": {
|
||||
"type": "string",
|
||||
"description": "The VPC id."
|
||||
},
|
||||
"subnet_ids": {
|
||||
"type": "list",
|
||||
"description": "List of subnet ids (one per AZ)."
|
||||
},
|
||||
"igw_id": {
|
||||
"type": "string",
|
||||
"description": "The internet gateway id."
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "aws:ec2:vpc",
|
||||
"description": "The VPC itself.",
|
||||
"inputs": ["cidr"],
|
||||
"outputs": ["vpc_id"]
|
||||
},
|
||||
{
|
||||
"type": "aws:ec2:subnet",
|
||||
"description": "One subnet per availability zone (azs).",
|
||||
"inputs": ["cidr", "az", "vpc_id"],
|
||||
"outputs": ["subnet_ids"]
|
||||
},
|
||||
{
|
||||
"type": "aws:ec2:routetable",
|
||||
"description": "Route table bound to the VPC with a default route via the IGW.",
|
||||
"inputs": ["vpc_id"],
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"type": "aws:ec2:igw",
|
||||
"description": "Internet gateway attached to the VPC.",
|
||||
"inputs": ["vpc_id"],
|
||||
"outputs": ["igw_id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
locals {
|
||||
# Tags: merge caller-supplied tags with the module defaults.
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
|
||||
# Derive /24 subnet CIDRs from the VPC /16 by slicing the 3rd octet.
|
||||
# Works for /16 VPC CIDRs; for other sizes the caller should pass
|
||||
# pre-computed subnet CIDRs (future input).
|
||||
vpc_octets = split(".", cidrhost(var.cidr, 0))
|
||||
subnet_cidrs = [for i in range(length(var.azs)) : "${local.vpc_octets[0]}.${local.vpc_octets[1]}.${i}.0/24"]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
resource "aws_vpc" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
cidr_block = var.cidr
|
||||
tags = merge(
|
||||
{
|
||||
Name = "nova-vpc"
|
||||
},
|
||||
local.tags,
|
||||
)
|
||||
}
|
||||
|
||||
resource "aws_subnet" "this" {
|
||||
count = var.enabled ? length(var.azs) : 0
|
||||
vpc_id = aws_vpc.this[0].id
|
||||
cidr_block = local.subnet_cidrs[count.index]
|
||||
availability_zone = var.azs[count.index]
|
||||
tags = merge(
|
||||
{
|
||||
Name = "nova-subnet-${count.index}"
|
||||
},
|
||||
local.tags,
|
||||
)
|
||||
}
|
||||
|
||||
resource "aws_internet_gateway" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
vpc_id = aws_vpc.this[0].id
|
||||
tags = merge(
|
||||
{
|
||||
Name = "nova-igw"
|
||||
},
|
||||
local.tags,
|
||||
)
|
||||
}
|
||||
|
||||
resource "aws_route_table" "this" {
|
||||
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[0].id
|
||||
}
|
||||
|
||||
tags = merge(
|
||||
{
|
||||
Name = "nova-rt"
|
||||
},
|
||||
local.tags,
|
||||
)
|
||||
}
|
||||
|
||||
resource "aws_route_table_association" "this" {
|
||||
count = var.enabled ? length(var.azs) : 0
|
||||
subnet_id = aws_subnet.this[count.index].id
|
||||
route_table_id = aws_route_table.this[0].id
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
output "vpc_id" {
|
||||
value = var.enabled ? aws_vpc.this[0].id : null
|
||||
description = "The VPC id."
|
||||
}
|
||||
|
||||
output "subnet_ids" {
|
||||
value = var.enabled ? aws_subnet.this[*].id : []
|
||||
description = "List of subnet ids (one per AZ)."
|
||||
}
|
||||
|
||||
output "igw_id" {
|
||||
value = var.enabled ? aws_internet_gateway.this[0].id : null
|
||||
description = "The internet gateway id."
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
variable "cidr" {
|
||||
type = string
|
||||
description = "VPC CIDR block, e.g. 10.0.0.0/16."
|
||||
}
|
||||
|
||||
variable "azs" {
|
||||
type = list(string)
|
||||
description = "List of availability zones, e.g. [\"us-east-1a\", \"us-east-1b\"]. One subnet is created per AZ."
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: waf
|
||||
|
||||
WAFv2 web ACL primitive (stack type `aws:waf:web_acl`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "waf",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:waf:web_acl",
|
||||
"description": "WAFv2 web ACL primitive (engine-agnostic stack type aws:waf:web_acl; the Terraform adapter translates to aws_wafv2_web_acl).",
|
||||
"inputs": {
|
||||
"acl_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the WAF web ACL.",
|
||||
"required": true
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"acl_arn": {
|
||||
"type": "arn",
|
||||
"description": "The WAF web ACL ARN."
|
||||
},
|
||||
"acl_id": {
|
||||
"type": "string",
|
||||
"description": "The WAF web ACL id."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
resource "aws_wafv2_web_acl" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = var.acl_name
|
||||
description = "WAF web ACL managed by nova L1 waf primitive."
|
||||
scope = "REGIONAL"
|
||||
default_action {
|
||||
allow {}
|
||||
}
|
||||
|
||||
visibility_config {
|
||||
cloudwatch_metrics_enabled = true
|
||||
metric_name = var.acl_name
|
||||
sampled_requests_enabled = true
|
||||
}
|
||||
|
||||
tags = local.tags
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "acl_arn" {
|
||||
value = var.enabled ? aws_wafv2_web_acl.this[0].arn : null
|
||||
description = "The WAF web ACL ARN."
|
||||
}
|
||||
|
||||
output "acl_id" {
|
||||
value = var.enabled ? aws_wafv2_web_acl.this[0].id : null
|
||||
description = "The WAF web ACL id."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
variable "acl_name" {
|
||||
type = string
|
||||
description = "Name of the WAF web ACL."
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"s3": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/s3/interface.json",
|
||||
"terraform_dir": "modules/l1/s3/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
},
|
||||
"vpc": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/vpc/interface.json",
|
||||
"terraform_dir": "modules/l1/vpc/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
},
|
||||
"ecs-cluster": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/ecs-cluster/interface.json",
|
||||
"terraform_dir": "modules/l1/ecs-cluster/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
},
|
||||
"ecs-service": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/ecs-service/interface.json",
|
||||
"terraform_dir": "modules/l1/ecs-service/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
},
|
||||
"iam-role": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/iam-role/interface.json",
|
||||
"terraform_dir": "modules/l1/iam-role/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
},
|
||||
"alb": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/alb/interface.json",
|
||||
"terraform_dir": "modules/l1/alb/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
},
|
||||
"ecr": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/ecr/interface.json",
|
||||
"terraform_dir": "modules/l1/ecr/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
},
|
||||
"cloudfront": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/cloudfront/interface.json",
|
||||
"terraform_dir": "modules/l1/cloudfront/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
},
|
||||
"waf": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/waf/interface.json",
|
||||
"terraform_dir": "modules/l1/waf/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
},
|
||||
"rds": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/rds/interface.json",
|
||||
"terraform_dir": "modules/l1/rds/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
},
|
||||
"kms-key": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/kms-key/interface.json",
|
||||
"terraform_dir": "modules/l1/kms-key/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
},
|
||||
"dynamodb": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/dynamodb/interface.json",
|
||||
"terraform_dir": "modules/l1/dynamodb/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
},
|
||||
"uptime": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/uptime/interface.json",
|
||||
"terraform_dir": "modules/l1/uptime/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
},
|
||||
"microservice": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l2/microservice/interface.json",
|
||||
"terraform_dir": "modules/l2/microservice/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l2"
|
||||
}
|
||||
},
|
||||
"static-assets": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l2/static-assets/interface.json",
|
||||
"terraform_dir": "modules/l2/static-assets/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Engine-boundary test — REQ-09, D-034.
|
||||
|
||||
Verifies no engine-specific *logic* (HCL strings, aws_ resource types,
|
||||
terraform CLI calls, module/provider/resource block declarations) leaks
|
||||
outside adapters/terraform/.
|
||||
|
||||
Scans .py files in core/, schemas/, contracts/, tests/, scripts/, root.
|
||||
EXCLUDES adapters/terraform/ (the boundary), modules/, .tf/.md/.json data
|
||||
files (per D-034).
|
||||
|
||||
To avoid false positives on docstrings/comments that *mention* "Terraform"
|
||||
conceptually, the test strips comments + docstrings before scanning.
|
||||
The forbidden terms are checked as *code-level* tokens: aws_<word>,
|
||||
`module "`, `provider "`, `resource "` (HCL block declarations that
|
||||
would indicate actual HCL emission outside the adapter). The bare word
|
||||
"terraform" is NOT forbidden (it appears in import paths like
|
||||
`adapters.terraform` and docstrings); only `terraform ` followed by a
|
||||
block brace or CLI invocation is.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import ROOT
|
||||
|
||||
# Forbidden as code-level patterns (not in strings/comments):
|
||||
# - aws_<word>: AWS resource type prefixes (e.g. aws_s3_bucket)
|
||||
# - 'module "': HCL module block declaration
|
||||
# - 'provider "': HCL provider block declaration
|
||||
# - 'resource "': HCL resource block declaration
|
||||
# - terraform init/plan/apply: CLI invocations
|
||||
FORBIDDEN_PATTERNS = [
|
||||
re.compile(r'\baws_[a-z_]+'),
|
||||
re.compile(r'module\s+"'),
|
||||
re.compile(r'provider\s+"'),
|
||||
re.compile(r'resource\s+"'),
|
||||
re.compile(r'\bterraform\s+(init|plan|apply|validate|destroy)\b'),
|
||||
]
|
||||
|
||||
EXCLUDE_DIRS = {
|
||||
"adapters/terraform",
|
||||
".venv",
|
||||
"__pycache__",
|
||||
".ciagent",
|
||||
".git",
|
||||
"modules",
|
||||
"terraform",
|
||||
"docs",
|
||||
}
|
||||
|
||||
# Test files that legitimately reference engine terms to verify the
|
||||
# boundary/adapter (they assert HCL output contains 'module "' etc.).
|
||||
# These are part of the boundary enforcement, not engine logic leaks.
|
||||
EXCLUDE_FILES = {
|
||||
"tests/test_terraform_adapter.py",
|
||||
"tests/test_engine_boundary.py",
|
||||
}
|
||||
|
||||
|
||||
def _strip_docstrings_and_comments(source):
|
||||
"""Remove docstrings + comments from Python source, return code only."""
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
return source
|
||||
lines = source.splitlines(keepends=True)
|
||||
# Collect line ranges of docstring nodes
|
||||
docstring_ranges = set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.Expr,)) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
||||
for ln in range(node.lineno, node.end_lineno + 1):
|
||||
docstring_ranges.add(ln)
|
||||
if isinstance(node, ast.Constant) and isinstance(node.value, str) and node.lineno == getattr(node, "end_lineno", None):
|
||||
# standalone string used as docstring at module/class level
|
||||
pass
|
||||
out = []
|
||||
for i, line in enumerate(lines, start=1):
|
||||
if i in docstring_ranges:
|
||||
continue
|
||||
# strip inline comments
|
||||
stripped = re.sub(r'#.*$', '', line)
|
||||
out.append(stripped)
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _scan_files():
|
||||
for path in ROOT.rglob("*.py"):
|
||||
rel = path.relative_to(ROOT)
|
||||
rel_str = str(rel)
|
||||
if any(rel_str.startswith(ex) for ex in EXCLUDE_DIRS):
|
||||
continue
|
||||
if any(part in EXCLUDE_DIRS for part in rel.parts):
|
||||
continue
|
||||
if rel_str in EXCLUDE_FILES:
|
||||
continue
|
||||
yield path
|
||||
|
||||
|
||||
def _forbidden_matches(code):
|
||||
matches = []
|
||||
for pat in FORBIDDEN_PATTERNS:
|
||||
found = pat.findall(code)
|
||||
if found:
|
||||
matches.extend(found)
|
||||
return matches
|
||||
|
||||
|
||||
class TestEngineBoundary:
|
||||
@pytest.mark.parametrize("path", list(_scan_files()),
|
||||
ids=[str(p.relative_to(ROOT)) for p in _scan_files()])
|
||||
def test_no_engine_logic(self, path):
|
||||
source = path.read_text()
|
||||
code = _strip_docstrings_and_comments(source)
|
||||
matches = _forbidden_matches(code)
|
||||
assert not matches, (
|
||||
f"{path.relative_to(ROOT)} contains forbidden engine logic: {matches}. "
|
||||
f"Engine-specific code must live ONLY in adapters/terraform/."
|
||||
)
|
||||
|
||||
def test_boundary_scans_files(self):
|
||||
files = list(_scan_files())
|
||||
assert len(files) > 0, "engine-boundary test must scan at least one .py file"
|
||||
for f in files:
|
||||
assert "adapters/terraform" not in str(f.relative_to(ROOT)), \
|
||||
f"adapters/terraform/ must be excluded but found {f}"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user