feat(P4): Nova rebrand — AWS resource migration (REQ-163)
Rename all acdl-* AWS resources → nova-* across terraform (DynamoDB, Secrets Manager, Lambda, SNS, SG, KMS alias, ECS, ECR, IAM user/policy, state bucket, ALB, VPC/subnet names). Lambda default table names → nova-* (D-111). State bucket backend → nova-tfstate (-migrate-state documented). New docs/NOVA_AWS_MIGRATION.md runbook (staged migration + rollback). New scripts/migrate_dynamodb_data.py (scan+copy, dry-run default). acdl-deploy- → nova-deploy- role ARN in deploy workflows. Test fixtures updated; terraform validate + pytest + run_ci.sh PASS. ---ci--- project: acdl phase: 4 milestone: v1.15 status: execute ---/ci---
This commit is contained in:
@@ -43,7 +43,7 @@
|
|||||||
# as repository secrets. The platform-managed scheduled pipeline rotates
|
# as repository secrets. The platform-managed scheduled pipeline rotates
|
||||||
# the key on a daily cadence. When .env.secrets is used locally instead,
|
# the key on a daily cadence. When .env.secrets is used locally instead,
|
||||||
# rotating the key out of band is the consumer's responsibility.
|
# rotating the key out of band is the consumer's responsibility.
|
||||||
name: acdl-deploy
|
name: nova-deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
@@ -102,11 +102,8 @@ jobs:
|
|||||||
- name: Configure AWS credentials (OIDC default + static-key override)
|
- name: Configure AWS credentials (OIDC default + static-key override)
|
||||||
uses: aws-actions/configure-aws-credentials@v4
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
with:
|
with:
|
||||||
# TODO(P4, REQ-163): rename the IAM role acdl-deploy- → nova-deploy-.
|
# P4 (REQ-163): IAM role renamed acdl-deploy- → nova-deploy-.
|
||||||
# The role ARN string is left as acdl-deploy- until P4 (IAM role
|
role-to-assume: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/nova-deploy-{1}', secrets.NOVA_AWS_ACCOUNT_ID, github.repository_id) || '' }}
|
||||||
# rename territory); only the secret REFERENCES are updated to
|
|
||||||
# NOVA_* in P2 (G-108 binding).
|
|
||||||
role-to-assume: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/acdl-deploy-{1}', secrets.NOVA_AWS_ACCOUNT_ID, github.repository_id) || '' }}
|
|
||||||
aws-region: us-east-1
|
aws-region: us-east-1
|
||||||
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||||
@@ -157,13 +154,13 @@ jobs:
|
|||||||
- name: Upload emitted Terraform
|
- name: Upload emitted Terraform
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: acdl-terraform
|
name: nova-terraform
|
||||||
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
|
|
||||||
- name: Upload platform log
|
- name: Upload platform log
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: acdl-platform-log
|
name: nova-platform-log
|
||||||
path: platform/logs/
|
path: platform/logs/
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
# as repository secrets. The platform-managed scheduled pipeline rotates
|
# as repository secrets. The platform-managed scheduled pipeline rotates
|
||||||
# the key on a daily cadence. When .env.secrets is used locally instead,
|
# the key on a daily cadence. When .env.secrets is used locally instead,
|
||||||
# rotating the key out of band is the consumer's responsibility.
|
# rotating the key out of band is the consumer's responsibility.
|
||||||
name: acdl-deploy
|
name: nova-deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
@@ -102,11 +102,8 @@ jobs:
|
|||||||
- name: Configure AWS credentials (OIDC default + static-key override)
|
- name: Configure AWS credentials (OIDC default + static-key override)
|
||||||
uses: aws-actions/configure-aws-credentials@v4
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
with:
|
with:
|
||||||
# TODO(P4, REQ-163): rename the IAM role acdl-deploy- → nova-deploy-.
|
# P4 (REQ-163): IAM role renamed acdl-deploy- → nova-deploy-.
|
||||||
# The role ARN string is left as acdl-deploy- until P4 (IAM role
|
role-to-assume: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/nova-deploy-{1}', secrets.NOVA_AWS_ACCOUNT_ID, github.repository_id) || '' }}
|
||||||
# rename territory); only the secret REFERENCES are updated to
|
|
||||||
# NOVA_* in P2 (G-108 binding).
|
|
||||||
role-to-assume: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/acdl-deploy-{1}', secrets.NOVA_AWS_ACCOUNT_ID, github.repository_id) || '' }}
|
|
||||||
aws-region: us-east-1
|
aws-region: us-east-1
|
||||||
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||||
@@ -157,13 +154,13 @@ jobs:
|
|||||||
- name: Upload emitted Terraform
|
- name: Upload emitted Terraform
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: acdl-terraform
|
name: nova-terraform
|
||||||
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
|
|
||||||
- name: Upload platform log
|
- name: Upload platform log
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: acdl-platform-log
|
name: nova-platform-log
|
||||||
path: platform/logs/
|
path: platform/logs/
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Invoked via a Function URL (IAM auth) by consumer pipelines (one-way
|
Invoked via a Function URL (IAM auth) by consumer pipelines (one-way
|
||||||
communication, D-051). Accepts { consumerRepo, contractId, contract,
|
communication, D-051). Accepts { consumerRepo, contractId, contract,
|
||||||
environment, action } and writes contracts to DynamoDB table acdl-contracts
|
environment, action } and writes contracts to DynamoDB table nova-contracts
|
||||||
(PK consumerRepo, SK contractId#submittedAt).
|
(PK consumerRepo, SK contractId#submittedAt).
|
||||||
|
|
||||||
The report_error action (D-055) creates a GitHub issue on the platform repo
|
The report_error action (D-055) creates a GitHub issue on the platform repo
|
||||||
@@ -22,10 +22,10 @@ import urllib.parse
|
|||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "acdl-contracts")
|
TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "nova-contracts")
|
||||||
CHANGE_REQUESTS_TABLE = os.environ.get("CHANGE_REQUESTS_TABLE", "acdl-change-requests")
|
CHANGE_REQUESTS_TABLE = os.environ.get("CHANGE_REQUESTS_TABLE", "nova-change-requests")
|
||||||
GITHUB_TOKEN_SECRET_ID = os.environ.get("GITHUB_TOKEN_SECRET_ID", "acdl/github-token")
|
GITHUB_TOKEN_SECRET_ID = os.environ.get("GITHUB_TOKEN_SECRET_ID", "nova/github-token")
|
||||||
PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "acdl/acdl")
|
PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "nova/acdl")
|
||||||
# P1-9: Forge-agnostic API base URL. Defaults to GitHub; set GITHUB_API_BASE
|
# P1-9: Forge-agnostic API base URL. Defaults to GitHub; set GITHUB_API_BASE
|
||||||
# to a Gitea API root (e.g. https://git.cloudinit.dev/api/v1) for Gitea.
|
# to a Gitea API root (e.g. https://git.cloudinit.dev/api/v1) for Gitea.
|
||||||
GITHUB_API_BASE = os.environ.get("GITHUB_API_BASE", "https://api.github.com")
|
GITHUB_API_BASE = os.environ.get("GITHUB_API_BASE", "https://api.github.com")
|
||||||
@@ -243,7 +243,7 @@ def _validate_caller_identity(event, payload):
|
|||||||
error length. The ABAC reliance is documented here: the Function URL IAM
|
error length. The ABAC reliance is documented here: the Function URL IAM
|
||||||
identity does not expose principal tags in the event, so full enforcement
|
identity does not expose principal tags in the event, so full enforcement
|
||||||
of consumerRepo ownership is at the IAM layer (ABAC via
|
of consumerRepo ownership is at the IAM layer (ABAC via
|
||||||
aws:PrincipalTag/acdl:owner). This function validates format only, not
|
aws:PrincipalTag/nova:owner). This function validates format only, not
|
||||||
ownership.
|
ownership.
|
||||||
"""
|
"""
|
||||||
identity = event.get("requestContext", {}).get("identity", {})
|
identity = event.get("requestContext", {}).get("identity", {})
|
||||||
@@ -279,7 +279,7 @@ def _validate_caller_identity(event, payload):
|
|||||||
def _validate_change_request(payload):
|
def _validate_change_request(payload):
|
||||||
"""REQ-93: Validate a change request ID against the CMDB (DynamoDB).
|
"""REQ-93: Validate a change request ID against the CMDB (DynamoDB).
|
||||||
|
|
||||||
Queries the acdl-change-requests table for the given changeRequestId.
|
Queries the nova-change-requests table for the given changeRequestId.
|
||||||
Returns the CR details if status is 'approved' and the consumerRepo matches.
|
Returns the CR details if status is 'approved' and the consumerRepo matches.
|
||||||
Raises ValueError if the CR is not found, not approved, or the repo doesn't match.
|
Raises ValueError if the CR is not found, not approved, or the repo doesn't match.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class FlatFileOutbox:
|
|||||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
def write_event(self, event: Dict[str, Any],
|
def write_event(self, event: Dict[str, Any],
|
||||||
outbox_table: str = "acdl-outbox-local",
|
outbox_table: str = "nova-outbox-local",
|
||||||
region: str = "local") -> Dict[str, Any]:
|
region: str = "local") -> Dict[str, Any]:
|
||||||
"""Write an evidence event to the flat-file outbox.
|
"""Write an evidence event to the flat-file outbox.
|
||||||
|
|
||||||
@@ -428,7 +428,7 @@ def run_local_e2e(contract_path: str, repo_root: Optional[Path] = None) -> Dict[
|
|||||||
|
|
||||||
stack = resolve(contract_path, str(root))
|
stack = resolve(contract_path, str(root))
|
||||||
stack_name = stack["stack"]["name"]
|
stack_name = stack["stack"]["name"]
|
||||||
work = Path(tempfile.mkdtemp(prefix="acdl_local_e2e_"))
|
work = Path(tempfile.mkdtemp(prefix="nova_local_e2e_"))
|
||||||
tf_dir = work / "tf"
|
tf_dir = work / "tf"
|
||||||
tf_dir.mkdir(exist_ok=True)
|
tf_dir.mkdir(exist_ok=True)
|
||||||
adapter.adapt(stack, str(tf_dir))
|
adapter.adapt(stack, str(tf_dir))
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"""ACDL Outbox Writer — write an evidence event to the DynamoDB outbox.
|
"""Nova Outbox Writer — write an evidence event to the DynamoDB outbox.
|
||||||
|
|
||||||
ARCHITECTURE.md §9: DynamoDB outbox, RPO=0 (synchronous write before
|
ARCHITECTURE.md §9: DynamoDB outbox, RPO=0 (synchronous write before
|
||||||
ack). The event is hash-chained (SHA-256 over canonical JSON); the first
|
ack). The event is hash-chained (SHA-256 over canonical JSON); the first
|
||||||
event has prev_event_hash="GENESIS". D-P10-3: the spike writes ONE
|
event has prev_event_hash="GENESIS". D-P10-3: the spike writes ONE
|
||||||
CONFIDENCE_COMPUTED event.
|
CONFIDENCE_COMPUTED event.
|
||||||
|
|
||||||
The outbox table (Phase 08): acdl-outbox, PAY_PER_REQUEST, PK contractId,
|
The outbox table (Phase 08): nova-outbox, PAY_PER_REQUEST, PK contractId,
|
||||||
SK eventType#eventTs, TTL expire_at = now + 365d (D-044).
|
SK eventType#eventTs, TTL expire_at = now + 365d (D-044).
|
||||||
|
|
||||||
CLI: outbox_writer.py <event.json> (uses AWS creds from env)
|
CLI: outbox_writer.py <event.json> (uses AWS creds from env)
|
||||||
@@ -20,7 +20,7 @@ import sys
|
|||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
|
|
||||||
OUTBOX_TABLE = "acdl-outbox"
|
OUTBOX_TABLE = "nova-outbox"
|
||||||
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ def _check_live_terraform_plan_microservice() -> Tuple[Status, str]:
|
|||||||
dual-read NOVA_* first, ACDL_* fallback per G-106).
|
dual-read NOVA_* first, ACDL_* fallback per G-106).
|
||||||
Runs in a temp dir; does NOT apply (plan only)."""
|
Runs in a temp dir; does NOT apply (plan only)."""
|
||||||
import tempfile, os
|
import tempfile, os
|
||||||
work = tempfile.mkdtemp(prefix="acdl_regr_live_")
|
work = tempfile.mkdtemp(prefix="nova_regr_live_")
|
||||||
stack_path = os.path.join(work, "stack.json")
|
stack_path = os.path.join(work, "stack.json")
|
||||||
tf_dir = os.path.join(work, "tf")
|
tf_dir = os.path.join(work, "tf")
|
||||||
os.makedirs(tf_dir, exist_ok=True)
|
os.makedirs(tf_dir, exist_ok=True)
|
||||||
@@ -376,7 +376,7 @@ def _check_live_terraform_plan_static_assets() -> Tuple[Status, str]:
|
|||||||
"""CAP-014: terraform init+validate+plan against live AWS for the
|
"""CAP-014: terraform init+validate+plan against live AWS for the
|
||||||
static-assets stack (CloudFront + WAF + S3)."""
|
static-assets stack (CloudFront + WAF + S3)."""
|
||||||
import tempfile, os
|
import tempfile, os
|
||||||
work = tempfile.mkdtemp(prefix="acdl_regr_live_sa_")
|
work = tempfile.mkdtemp(prefix="nova_regr_live_sa_")
|
||||||
stack_path = os.path.join(work, "stack.json")
|
stack_path = os.path.join(work, "stack.json")
|
||||||
tf_dir = os.path.join(work, "tf")
|
tf_dir = os.path.join(work, "tf")
|
||||||
os.makedirs(tf_dir, exist_ok=True)
|
os.makedirs(tf_dir, exist_ok=True)
|
||||||
@@ -420,9 +420,9 @@ def _check_dynamodb_outbox_table() -> Tuple[Status, str]:
|
|||||||
dyn = boto3.client("dynamodb", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"),
|
dyn = boto3.client("dynamodb", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"),
|
||||||
aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"),
|
aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"),
|
||||||
aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
|
aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
|
||||||
r = dyn.describe_table(TableName="acdl-outbox")
|
r = dyn.describe_table(TableName="nova-outbox")
|
||||||
count = r["Table"].get("ItemCount", "unknown")
|
count = r["Table"].get("ItemCount", "unknown")
|
||||||
return "Verified", f"acdl-outbox exists, item_count={count}"
|
return "Verified", f"nova-outbox exists, item_count={count}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "Decayed", f"describe_table failed: {type(e).__name__}: {str(e)[:150]}"
|
return "Decayed", f"describe_table failed: {type(e).__name__}: {str(e)[:150]}"
|
||||||
|
|
||||||
@@ -436,7 +436,7 @@ def _check_s3_state_bucket() -> Tuple[Status, str]:
|
|||||||
aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"),
|
aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"),
|
||||||
aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
|
aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
|
||||||
account_id = _envhelper.get_env("AWS_ACCOUNT_ID", "581513795199")
|
account_id = _envhelper.get_env("AWS_ACCOUNT_ID", "581513795199")
|
||||||
state_bucket = f"acdl-tfstate-{account_id}-us-east-1"
|
state_bucket = f"nova-tfstate-{account_id}-us-east-1"
|
||||||
s3.head_bucket(Bucket=state_bucket)
|
s3.head_bucket(Bucket=state_bucket)
|
||||||
r = s3.list_objects_v2(Bucket=state_bucket, MaxKeys=5)
|
r = s3.list_objects_v2(Bucket=state_bucket, MaxKeys=5)
|
||||||
keys = [o["Key"] for o in r.get("Contents", [])]
|
keys = [o["Key"] for o in r.get("Contents", [])]
|
||||||
@@ -508,7 +508,7 @@ def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]:
|
|||||||
|
|
||||||
|
|
||||||
def _check_cap_017_dynamodb() -> Tuple[Status, str]:
|
def _check_cap_017_dynamodb() -> Tuple[Status, str]:
|
||||||
"""CAP-017: DynamoDB acdl-contracts table. Evidence = L1 rds module
|
"""CAP-017: DynamoDB nova-contracts table. Evidence = L1 rds module
|
||||||
lifecycle pipeline green (terraform validate + contracts resolve).
|
lifecycle pipeline green (terraform validate + contracts resolve).
|
||||||
The DynamoDB table is created via the microservice stack (L2 lifecycle).
|
The DynamoDB table is created via the microservice stack (L2 lifecycle).
|
||||||
"""
|
"""
|
||||||
@@ -523,7 +523,7 @@ def _check_cap_018_lambda() -> Tuple[Status, str]:
|
|||||||
"python3", "-c",
|
"python3", "-c",
|
||||||
"from core.local_emulators import LocalLambdaStub, FlatFileOutbox; "
|
"from core.local_emulators import LocalLambdaStub, FlatFileOutbox; "
|
||||||
"import tempfile; "
|
"import tempfile; "
|
||||||
"stub = LocalLambdaStub(outbox=FlatFileOutbox(tempfile.mkdtemp(prefix='acdl_stub_'))); "
|
"stub = LocalLambdaStub(outbox=FlatFileOutbox(tempfile.mkdtemp(prefix='nova_stub_'))); "
|
||||||
"print('LocalLambdaStub instantiates OK')",
|
"print('LocalLambdaStub instantiates OK')",
|
||||||
])
|
])
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
@@ -592,7 +592,7 @@ CAPABILITY_REGISTRY: List[Tuple[str, str, str, Callable[[], Tuple[Status, str]]]
|
|||||||
_check_dynamodb_outbox_table),
|
_check_dynamodb_outbox_table),
|
||||||
("CAP-016", "S3 state bucket exists + readable (live AWS)", "live-aws",
|
("CAP-016", "S3 state bucket exists + readable (live AWS)", "live-aws",
|
||||||
_check_s3_state_bucket),
|
_check_s3_state_bucket),
|
||||||
("CAP-017", "DynamoDB acdl-contracts table (lifecycle pipeline evidence)", "lifecycle-pipeline",
|
("CAP-017", "DynamoDB nova-contracts table (lifecycle pipeline evidence)", "lifecycle-pipeline",
|
||||||
_check_cap_017_dynamodb),
|
_check_cap_017_dynamodb),
|
||||||
("CAP-018", "Lambda contract-ingestor (local stub + lifecycle evidence)", "lifecycle-pipeline",
|
("CAP-018", "Lambda contract-ingestor (local stub + lifecycle evidence)", "lifecycle-pipeline",
|
||||||
_check_cap_018_lambda),
|
_check_cap_018_lambda),
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
# Nova AWS Resource Migration Runbook (REQ-163, P4)
|
||||||
|
|
||||||
|
> **Milestone:** v1.15-Nova (Wave 4, P4). Renames every `acdl-*` AWS
|
||||||
|
> resource name → `nova-*` via Terraform. This is the heaviest Terraform
|
||||||
|
> phase of the rebrand and requires a **maintenance window**.
|
||||||
|
>
|
||||||
|
> **Plan-validated only.** Per A1, `NOVA_LIFECYCLE_MODE` defaults to
|
||||||
|
> `plan` (no live AWS mutation from CI). `terraform validate` passes; the
|
||||||
|
> live apply steps below are executed by a platform operator during the
|
||||||
|
> scheduled maintenance window. Each step has a verification + rollback.
|
||||||
|
|
||||||
|
## Scope (renamed resources)
|
||||||
|
|
||||||
|
| AWS resource | Before | After | Strategy |
|
||||||
|
|---|---|---|---|
|
||||||
|
| KMS alias | `alias/acdl-platform` | `alias/nova-platform` | cheap rename |
|
||||||
|
| SNS topic | `acdl-sod-halt` | `nova-sod-halt` | recreate |
|
||||||
|
| Security group | `acdl-ecs-sg` | `nova-ecs-sg` | recreate |
|
||||||
|
| Lambda (role/policy/function) | `acdl-contract-ingestor` | `nova-contract-ingestor` | recreate |
|
||||||
|
| DynamoDB contracts | `acdl-contracts` | `nova-contracts` | scan + copy |
|
||||||
|
| DynamoDB change-requests | `acdl-change-requests` | `nova-change-requests` | scan + copy |
|
||||||
|
| Secrets Manager secret | `acdl/github-token` | `nova/github-token` | recreate + re-store |
|
||||||
|
| ECR repo | `acdl-microservice` | `nova-microservice` | re-push |
|
||||||
|
| ECS cluster/service/task/role | `acdl-microservice` | `nova-microservice` | recreate |
|
||||||
|
| IAM user + policy | `acdl-spike-runner` (+ `-policy`) | `nova-spike-runner` (+ `-policy`) | re-bootstrap |
|
||||||
|
| IAM act-runner role | `acdl-act-runner-role` | `nova-act-runner-role` | re-bootstrap |
|
||||||
|
| IAM deploy role | `acdl-deploy-<repo>` | `nova-deploy-<repo>` | re-bootstrap |
|
||||||
|
| S3 state bucket | `acdl-tfstate-581513795199-us-east-1` | `nova-tfstate-581513795199-us-east-1` | `-migrate-state` |
|
||||||
|
| DynamoDB outbox | `acdl-outbox` | `nova-outbox` | scan + copy |
|
||||||
|
| Platform VPC/subnet/IGW/RT | `acdl-shared*` | `nova-shared*` | recreate (brief downtime) |
|
||||||
|
| CI VPC/subnet/SG/cluster | `acdl-ci-*` | `nova-ci-*` | recreate (CI-only) |
|
||||||
|
| ALB name prefix | `acdl-alb` | `nova-alb` | recreate (brief downtime, LAST) |
|
||||||
|
|
||||||
|
## Migration ordering (binding)
|
||||||
|
|
||||||
|
Order: **KMS alias → SNS/SG → Lambda → DynamoDB → ECR → IAM → state bucket → ALB**.
|
||||||
|
Each step is independently rollback-able. The ALB is last because it
|
||||||
|
requires the briefest downtime window.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre-flight
|
||||||
|
|
||||||
|
1. **Announce the maintenance window** (consumers are notified via the
|
||||||
|
P1 migration guide `docs/NOVA_MIGRATION.md`).
|
||||||
|
2. **Back up state** for every stack (see §State bucket — back up the
|
||||||
|
state JSON *before* `-migrate-state`).
|
||||||
|
3. Confirm `NOVA_LIFECYCLE_MODE=plan` (default) so CI does not mutate
|
||||||
|
AWS during the window.
|
||||||
|
4. Confirm the new `nova-*` destination tables/repos will be created by
|
||||||
|
the same Terraform apply (no manual pre-creation needed).
|
||||||
|
|
||||||
|
## Step 1 — KMS alias (`alias/acdl-platform` → `alias/nova-platform`)
|
||||||
|
|
||||||
|
- **Command (in `terraform/platform/`):**
|
||||||
|
```bash
|
||||||
|
terraform init -upgrade
|
||||||
|
terraform apply -replace=aws_kms_alias.nova_platform
|
||||||
|
```
|
||||||
|
(Terraform destroys the old alias + creates the new one — aliases are
|
||||||
|
cheap; the underlying key ID is unchanged.)
|
||||||
|
- **Verify:** `aws kms list-aliases --query 'Aliases[?AliasName==`alias/nova-platform`]'` returns the new alias; `alias/acdl-platform` is gone.
|
||||||
|
- **Rollback:** `terraform apply -replace=aws_kms_alias.nova_platform` against the prior revision (re-creates `alias/acdl-platform`). Resources encrypted by the key are unaffected (key ID unchanged).
|
||||||
|
|
||||||
|
## Step 2 — SNS topic + Security group (recreate)
|
||||||
|
|
||||||
|
- **Command:** `terraform apply` in `terraform/platform/`.
|
||||||
|
- SNS `acdl-sod-halt` → `nova-sod-halt` (the topic ARN changes; update `NOVA_SOD_HALT_TOPIC_ARN` wherever it is set).
|
||||||
|
- SG `acdl-ecs-sg` → `nova-ecs-sg` (the security group is re-attached to running ECS tasks; brief task restart).
|
||||||
|
- **Verify:** `aws sns list-topics` shows `nova-sod-halt`; `aws ec2 describe-security-groups` shows `nova-ecs-sg`.
|
||||||
|
- **Rollback:** `terraform apply` the prior revision re-creates the `acdl-*` names. The SNS topic has no message backlog (halt artifacts are fire-and-forget); the SG drift resolves on next task deploy.
|
||||||
|
|
||||||
|
## Step 3 — Lambda (recreate)
|
||||||
|
|
||||||
|
- **Command:** `terraform apply` in `terraform/platform/`.
|
||||||
|
- Lambda function `acdl-contract-ingestor` → `nova-contract-ingestor`.
|
||||||
|
- Execution role `acdl-contract-ingestor-role` → `nova-contract-ingestor-role`.
|
||||||
|
- Inline policy `acdl-contract-ingestor-policy` → `nova-contract-ingestor-policy`.
|
||||||
|
- The Lambda env vars (`CONTRACTS_TABLE`, `GITHUB_TOKEN_SECRET_ID`) now resolve to `nova-*` defaults.
|
||||||
|
- **Verify:** `aws lambda list-functions` shows `nova-contract-ingestor`; the Function URL returns 200 on a SigV4-signed invoke. The `consumer_invoke_policy.json` rendered output (Terraform `consumer_invoke_policy_rendered`) now references `function:nova-contract-ingestor` — re-distribute to consumer deploy roles.
|
||||||
|
- **Rollback:** `terraform apply` the prior revision re-creates `acdl-contract-ingestor`. Consumer deploy roles must point back at the old Function ARN (re-distribute the prior `consumer_invoke_policy.json`).
|
||||||
|
|
||||||
|
## Step 4 — DynamoDB (scan + copy)
|
||||||
|
|
||||||
|
DynamoDB table names are immutable post-creation, so the migration is a
|
||||||
|
**scan + copy** (not a rename). The new `nova-*` tables are created by
|
||||||
|
the same Terraform apply (Step 3). The data-migration script copies
|
||||||
|
every item and verifies row counts.
|
||||||
|
|
||||||
|
- **Command (from repo root):**
|
||||||
|
```bash
|
||||||
|
# Dry-run first (no writes):
|
||||||
|
python3 scripts/migrate_dynamodb_data.py
|
||||||
|
# Execute the copy:
|
||||||
|
python3 scripts/migrate_dynamodb_data.py --apply
|
||||||
|
# A single table:
|
||||||
|
python3 scripts/migrate_dynamodb_data.py --table contracts --apply
|
||||||
|
```
|
||||||
|
The script scans `acdl-contracts` → copies to `nova-contracts`, and
|
||||||
|
`acdl-change-requests` → `nova-change-requests`, then verifies the
|
||||||
|
destination row count == source row count (re-scan, not
|
||||||
|
`DescribeTable.ItemCount` which lags ~6h).
|
||||||
|
- **Verify:**
|
||||||
|
```bash
|
||||||
|
# Row counts must match (printed by the script). Manual cross-check:
|
||||||
|
aws dynamodb scan --table-name nova-contracts --select COUNT
|
||||||
|
aws dynamodb scan --table-name acdl-contracts --select COUNT
|
||||||
|
```
|
||||||
|
Then **point consumers at the new tables** (the Lambda already reads
|
||||||
|
`nova-*` defaults; any direct DynamoDB consumers update their env).
|
||||||
|
- **Keep the old tables** (`acdl-contracts`, `acdl-change-requests`)
|
||||||
|
until consumers are verified reading from `nova-*`. **Deletion is a
|
||||||
|
manual post-verification step:**
|
||||||
|
```bash
|
||||||
|
aws dynamodb delete-table --table-name acdl-contracts
|
||||||
|
aws dynamodb delete-table --table-name acdl-change-requests
|
||||||
|
```
|
||||||
|
Only delete after a full soak period confirms `nova-*` reads succeed.
|
||||||
|
- **Rollback:** Re-point consumers at `acdl-*` (the old tables are
|
||||||
|
retained). The copy is additive (no data loss). To roll back a partial
|
||||||
|
copy, re-run `--apply` (idempotent — `PutItem` overwrites).
|
||||||
|
|
||||||
|
### Outbox table (`acdl-outbox` → `nova-outbox`)
|
||||||
|
|
||||||
|
The evidence outbox table follows the same scan+copy pattern (it is
|
||||||
|
created by `terraform/bootstrap/create_state_backend.py`).
|
||||||
|
- **Command:** `python3 scripts/migrate_dynamodb_data.py --source acdl-outbox --dest nova-outbox --apply`
|
||||||
|
- The `core/outbox_writer.py` default + `core/regression_verify.py`
|
||||||
|
CAP-015 probe now reference `nova-outbox` (P4 updated both). The
|
||||||
|
regression gate's live-AWS CAP-015 will return `Verified` once the
|
||||||
|
`nova-outbox` table exists live; until then it is `Decayed` (the gate
|
||||||
|
is re-run at milestone complete after the live migration).
|
||||||
|
|
||||||
|
## Step 5 — ECR (re-push)
|
||||||
|
|
||||||
|
- **Command:** `terraform apply` in `terraform/microservice/` creates
|
||||||
|
the new `nova-microservice` ECR repo. Re-push the image:
|
||||||
|
```bash
|
||||||
|
python3 scripts/push_consumer_image.py # creates nova-microservice + prints docker tag/push
|
||||||
|
```
|
||||||
|
(The script's `ECR_REPO_NAME` is now `nova-microservice`.)
|
||||||
|
- **Verify:** `aws ecr describe-repositories` shows `nova-microservice`; `docker pull <acct>.dkr.ecr.us-east-1.amazonaws.com/nova-microservice:latest` succeeds.
|
||||||
|
- **Rollback:** The old `acdl-microservice` repo is retained until the
|
||||||
|
soak passes. Re-push to it if a rollback is needed. Delete it manually:
|
||||||
|
`aws ecr delete-repository --repository-name acdl-microservice --force`.
|
||||||
|
|
||||||
|
## Step 6 — IAM (re-bootstrap)
|
||||||
|
|
||||||
|
- **Command:**
|
||||||
|
```bash
|
||||||
|
export NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID="<root key>"
|
||||||
|
export NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY="<root secret>"
|
||||||
|
python3 terraform/bootstrap/create_state_backend.py # creates nova-outbox (idempotent)
|
||||||
|
python3 terraform/bootstrap/create_iam_user.py # creates nova-spike-runner
|
||||||
|
python3 terraform/bootstrap/apply_iam_baseline.py # creates nova-spike-runner-policy + nova-act-runner-role
|
||||||
|
bash scripts/rotate_spike_key.sh # rotates the nova-spike-runner key
|
||||||
|
```
|
||||||
|
The deploy role `acdl-deploy-<repo>` → `nova-deploy-<repo>` is
|
||||||
|
created by the bootstrap (the deploy workflow
|
||||||
|
`.gitea/.github/workflows/deploy.yml` now references
|
||||||
|
`role/nova-deploy-{1}`).
|
||||||
|
- **Verify:** `aws iam get-user --user-name nova-spike-runner`;
|
||||||
|
`aws iam list-attached-user-policies --user-name nova-spike-runner`
|
||||||
|
shows `nova-spike-runner-policy`;
|
||||||
|
`aws iam get-role --role-name nova-act-runner-role`.
|
||||||
|
- **Rollback:** Re-run the prior bootstrap scripts (they create
|
||||||
|
`acdl-spike-runner` + `acdl-act-runner-role`). The deploy workflow's
|
||||||
|
`role-to-assume` must be reverted to `acdl-deploy-` (prior revision).
|
||||||
|
|
||||||
|
## Step 7 — State bucket (`acdl-tfstate-*` → `nova-tfstate-*`, `-migrate-state`)
|
||||||
|
|
||||||
|
The S3 state backend is renamed. Terraform's `-migrate-state` copies the
|
||||||
|
state objects to the new bucket. **Back up the state JSON first.**
|
||||||
|
|
||||||
|
- **Back up state (per stack):**
|
||||||
|
```bash
|
||||||
|
for stack in platform microservice ci-vpc; do
|
||||||
|
aws s3 cp s3://acdl-tfstate-581513795199-us-east-1/$stack/terraform.tfstate \
|
||||||
|
./backup-$stack.tfstate
|
||||||
|
done
|
||||||
|
```
|
||||||
|
- **Command (per stack):** the backend config in each
|
||||||
|
`terraform/*/terraform.tf` now points at `nova-tfstate-...`.
|
||||||
|
```bash
|
||||||
|
cd terraform/platform
|
||||||
|
terraform init -migrate-state # copies state acdl-tfstate → nova-tfstate
|
||||||
|
cd ../microservice
|
||||||
|
terraform init -migrate-state
|
||||||
|
cd ../ci-vpc
|
||||||
|
terraform init -migrate-state
|
||||||
|
```
|
||||||
|
- **Verify:** `aws s3 ls s3://nova-tfstate-581513795199-us-east-1/`
|
||||||
|
shows the state keys; `terraform state list` in each dir lists the
|
||||||
|
expected resources.
|
||||||
|
- **Rollback:** Point the backend back at `acdl-tfstate-*` and re-run
|
||||||
|
`terraform init -migrate-state` (restores from the backup bucket). The
|
||||||
|
old `acdl-tfstate-*` bucket is retained until the soak passes. Delete
|
||||||
|
it manually:
|
||||||
|
`aws s3 rb s3://acdl-tfstate-581513795199-us-east-1 --force`.
|
||||||
|
|
||||||
|
## Step 8 — ALB (recreate, brief downtime, LAST)
|
||||||
|
|
||||||
|
The ALB is last because its recreation requires the briefest downtime
|
||||||
|
window (the ECS service is re-attached to the new target group).
|
||||||
|
|
||||||
|
- **Command:** `terraform apply` in `terraform/microservice/`. The ALB
|
||||||
|
`acdl-microservice` / `acdl-alb` → `nova-microservice` / `nova-alb`.
|
||||||
|
- **Verify:** `aws elbv2 describe-load-balancers` shows the new ALB;
|
||||||
|
`curl http://<new-alb-dns>/` returns 200.
|
||||||
|
- **Rollback:** `terraform apply` the prior revision re-creates the
|
||||||
|
`acdl-*` ALB (brief downtime again). The old ALB DNS is retained until
|
||||||
|
consumers are re-pointed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-migration
|
||||||
|
|
||||||
|
1. **Soak:** run consumers against `nova-*` for a full verification
|
||||||
|
window (deploy a test contract end-to-end).
|
||||||
|
2. **Delete old resources** (manual, only after soak):
|
||||||
|
- DynamoDB: `acdl-contracts`, `acdl-change-requests`, `acdl-outbox`
|
||||||
|
- ECR: `acdl-microservice`
|
||||||
|
- IAM: `acdl-spike-runner` (+ policy), `acdl-act-runner-role`,
|
||||||
|
`acdl-deploy-<repo>`
|
||||||
|
- S3: `acdl-tfstate-581513795199-us-east-1`
|
||||||
|
- SNS: `acdl-sod-halt`
|
||||||
|
- SG: `acdl-ecs-sg`
|
||||||
|
- Secrets Manager: `acdl/github-token`
|
||||||
|
- KMS alias: `alias/acdl-platform`
|
||||||
|
- ALB: `acdl-alb` / `acdl-microservice`
|
||||||
|
3. **Regression gate:** re-run `bash scripts/run_regression.sh`. The
|
||||||
|
live-AWS CAP-013..016 probes should return `Verified` (the `nova-*`
|
||||||
|
tables + state bucket exist). CAP-015 (outbox) flips from `Decayed`
|
||||||
|
→ `Verified` once `nova-outbox` is live.
|
||||||
|
|
||||||
|
## What P5 owns (not P4)
|
||||||
|
|
||||||
|
- **Remove dual-read fallback:** `core/env.py` `get_env()` drops the
|
||||||
|
`ACDL_*` fallback; shell scripts drop `:-$ACDL_X`. P4 keeps the
|
||||||
|
dual-read (deployments don't break mid-window).
|
||||||
|
- **`nova_tagging.py` hard-fail on `acdl:*`:** P3 set hard mode (no
|
||||||
|
`acdl:*`-only tags); P5 tightens to fail on any `acdl:*` presence. P4
|
||||||
|
leaves P3's behavior.
|
||||||
|
- **Delete `ACDL_*` Gitea secrets:** the `NOVA_*` aliases created in P2
|
||||||
|
are now the only source.
|
||||||
|
- **Finalize `docs/NOVA_MIGRATION.md`:** mark the migration complete
|
||||||
|
(cutoff passed).
|
||||||
|
- **Milestone ship:** tag `v1.15.4`, merge to `main`, Gitea release.
|
||||||
|
|
||||||
|
## Files touched in P4
|
||||||
|
|
||||||
|
- `terraform/platform/main.tf`, `terraform/microservice/main.tf`,
|
||||||
|
`terraform/ci-vpc/main.tf` — resource renames + backend bucket.
|
||||||
|
- `terraform/{platform,microservice,ci-vpc}/terraform.tf` — state bucket.
|
||||||
|
- `terraform/platform/consumer_invoke_policy.json` — Lambda ARN.
|
||||||
|
- `terraform/bootstrap/{create_state_backend,create_iam_user,apply_iam_baseline}.py`,
|
||||||
|
`spike_runner_policy.json`, `.bootstrap_state.json`, `README.md` —
|
||||||
|
IAM/outbox/state-bucket renames.
|
||||||
|
- `modules/l1/*/terraform/**` + `modules/l1/alb/instance.json` — L1
|
||||||
|
resource-name defaults.
|
||||||
|
- `modules/l2/microservice/composition.json` — `nova-app-role` default.
|
||||||
|
- `core/lambda/contract_ingestor.py` — default table names (D-111).
|
||||||
|
- `core/outbox_writer.py`, `core/regression_verify.py`,
|
||||||
|
`core/local_emulators.py` — outbox table consistency (cross-territory,
|
||||||
|
minimal).
|
||||||
|
- `.gitea/workflows/deploy.yml` + `.github/workflows/deploy.yml` —
|
||||||
|
`nova-deploy-` role ARN + artifact names.
|
||||||
|
- `scripts/migrate_dynamodb_data.py` (NEW), `scripts/rotate_spike_key.sh`,
|
||||||
|
`scripts/push_consumer_image.py`.
|
||||||
|
- `tests/**` — fixtures updated to assert `nova-*`.
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"type": "aws:elbv2:loadbalancer",
|
"type": "aws:elbv2:loadbalancer",
|
||||||
"module": "alb@1.0.0",
|
"module": "alb@1.0.0",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"name": "acdl-alb",
|
"name": "nova-alb",
|
||||||
"subnets": "subnet-12345",
|
"subnets": "subnet-12345",
|
||||||
"security_group": "sg-12345",
|
"security_group": "sg-12345",
|
||||||
"region": "us-east-1"
|
"region": "us-east-1"
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"type": "aws:elbv2:targetgroup",
|
"type": "aws:elbv2:targetgroup",
|
||||||
"module": "alb@1.0.0",
|
"module": "alb@1.0.0",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"name": "acdl-alb",
|
"name": "nova-alb",
|
||||||
"port": 80,
|
"port": 80,
|
||||||
"protocol": "HTTP",
|
"protocol": "HTTP",
|
||||||
"region": "us-east-1"
|
"region": "us-east-1"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
locals {
|
locals {
|
||||||
# OAC defaults (adapter previously hardcoded these).
|
# OAC defaults (adapter previously hardcoded these).
|
||||||
oac_name = "acdl-oac"
|
oac_name = "nova-oac"
|
||||||
oac_origin_type = "s3"
|
oac_origin_type = "s3"
|
||||||
oac_signing_behavior = "always"
|
oac_signing_behavior = "always"
|
||||||
oac_signing_protocol = "sigv4"
|
oac_signing_protocol = "sigv4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
variable "name" {
|
variable "name" {
|
||||||
type = string
|
type = string
|
||||||
description = "ECS cluster name."
|
description = "ECS cluster name."
|
||||||
default = "acdl-cluster"
|
default = "nova-cluster"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "region" {
|
variable "region" {
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ resource "aws_ecs_task_definition" "this" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_ecs_service" "this" {
|
resource "aws_ecs_service" "this" {
|
||||||
name = "acdl-microservice"
|
name = "nova-microservice"
|
||||||
cluster = var.cluster_arn
|
cluster = var.cluster_arn
|
||||||
task_definition = aws_ecs_task_definition.this.arn
|
task_definition = aws_ecs_task_definition.this.arn
|
||||||
desired_count = var.desired_count
|
desired_count = var.desired_count
|
||||||
launch_type = var.launch_type
|
launch_type = var.launch_type
|
||||||
|
|
||||||
network_configuration {
|
network_configuration {
|
||||||
subnets = local.subnet_list
|
subnets = local.subnet_list
|
||||||
security_groups = local.security_groups
|
security_groups = local.security_groups
|
||||||
assign_public_ip = var.launch_type == "FARGATE"
|
assign_public_ip = var.launch_type == "FARGATE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
variable "role_name" {
|
variable "role_name" {
|
||||||
type = string
|
type = string
|
||||||
description = "The IAM role name."
|
description = "The IAM role name."
|
||||||
default = "acdl-microservice-role"
|
default = "nova-microservice-role"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "assume_role_policy" {
|
variable "assume_role_policy" {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
locals {
|
locals {
|
||||||
alias_name = "alias/acdl-ci-kms"
|
alias_name = "alias/nova-ci-kms"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
resource "aws_db_subnet_group" "this" {
|
resource "aws_db_subnet_group" "this" {
|
||||||
count = var.subnet_ids != "" ? 1 : 0
|
count = var.subnet_ids != "" ? 1 : 0
|
||||||
name = "acdl-ci-rds-subnet-group"
|
name = "nova-ci-rds-subnet-group"
|
||||||
subnet_ids = split(",", var.subnet_ids)
|
subnet_ids = split(",", var.subnet_ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ output "bucket_name" {
|
|||||||
|
|
||||||
output "bucket_regional_domain_name" {
|
output "bucket_regional_domain_name" {
|
||||||
value = aws_s3_bucket.this.bucket_regional_domain_name
|
value = aws_s3_bucket.this.bucket_regional_domain_name
|
||||||
description = "The bucket regional domain name (e.g. acdl-spike-bucket.s3.us-east-1.amazonaws.com)."
|
description = "The bucket regional domain name (e.g. nova-spike-bucket.s3.us-east-1.amazonaws.com)."
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
resource "aws_ecs_task_definition" "uptime" {
|
resource "aws_ecs_task_definition" "uptime" {
|
||||||
family = "acdl-uptime"
|
family = "nova-uptime"
|
||||||
cpu = tostring(var.cpu)
|
cpu = tostring(var.cpu)
|
||||||
memory = tostring(var.memory)
|
memory = tostring(var.memory)
|
||||||
requires_compatibilities = ["FARGATE"]
|
requires_compatibilities = ["FARGATE"]
|
||||||
@@ -8,7 +8,7 @@ resource "aws_ecs_task_definition" "uptime" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_ecs_service" "uptime" {
|
resource "aws_ecs_service" "uptime" {
|
||||||
name = "acdl-uptime"
|
name = "nova-uptime"
|
||||||
cluster = local.cluster_ref
|
cluster = local.cluster_ref
|
||||||
task_definition = aws_ecs_task_definition.uptime.arn
|
task_definition = aws_ecs_task_definition.uptime.arn
|
||||||
desired_count = var.feature_flag_enabled ? 1 : 0
|
desired_count = var.feature_flag_enabled ? 1 : 0
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
locals {
|
locals {
|
||||||
cidr_block = var.cidr != null ? var.cidr : "10.0.0.0/16"
|
cidr_block = var.cidr != null ? var.cidr : "10.0.0.0/16"
|
||||||
az_list = split(",", var.azs)
|
az_list = split(",", var.azs)
|
||||||
name_tag = var.name != null ? var.name : "acdl-vpc"
|
name_tag = var.name != null ? var.name : "nova-vpc"
|
||||||
|
|
||||||
# Derive subnet CIDRs from the VPC CIDR
|
# Derive subnet CIDRs from the VPC CIDR
|
||||||
subnet_cidrs = [
|
subnet_cidrs = [
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ variable "azs" {
|
|||||||
variable "name" {
|
variable "name" {
|
||||||
type = string
|
type = string
|
||||||
description = "Name tag for the VPC and child resources."
|
description = "Name tag for the VPC and child resources."
|
||||||
default = "acdl-vpc"
|
default = "nova-vpc"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "region" {
|
variable "region" {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ resource "aws_wafv2_web_acl" "this" {
|
|||||||
|
|
||||||
visibility_config {
|
visibility_config {
|
||||||
cloudwatch_metrics_enabled = true
|
cloudwatch_metrics_enabled = true
|
||||||
metric_name = "acdl-waf-metrics"
|
metric_name = "nova-waf-metrics"
|
||||||
sampled_requests_enabled = true
|
sampled_requests_enabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
variable "name" {
|
variable "name" {
|
||||||
type = string
|
type = string
|
||||||
description = "WAF Web ACL name."
|
description = "WAF Web ACL name."
|
||||||
default = "acdl-waf"
|
default = "nova-waf"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "scope" {
|
variable "scope" {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"wires": [
|
"wires": [
|
||||||
{"from": "contract.inputs.name", "to": "alb.inputs.name", "default": "app"},
|
{"from": "contract.inputs.name", "to": "alb.inputs.name", "default": "app"},
|
||||||
{"from": "contract.inputs.name", "to": "ecr.inputs.name", "default": "app-repo"},
|
{"from": "contract.inputs.name", "to": "ecr.inputs.name", "default": "app-repo"},
|
||||||
{"from": "contract.inputs.name", "to": "roles.inputs.role_name", "default": "acdl-app-role"},
|
{"from": "contract.inputs.name", "to": "roles.inputs.role_name", "default": "nova-app-role"},
|
||||||
{"from": "contract.inputs.region", "to": "cluster.inputs.region"},
|
{"from": "contract.inputs.region", "to": "cluster.inputs.region"},
|
||||||
{"from": "contract.inputs.region", "to": "ecr.inputs.region"},
|
{"from": "contract.inputs.region", "to": "ecr.inputs.region"},
|
||||||
{"from": "contract.inputs.region", "to": "roles.inputs.region"},
|
{"from": "contract.inputs.region", "to": "roles.inputs.region"},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
#
|
#
|
||||||
# Validated against schemas/deploy-pipeline.schema.json.
|
# Validated against schemas/deploy-pipeline.schema.json.
|
||||||
|
|
||||||
name: acdl-deploy
|
name: nova-deploy
|
||||||
environment: dev
|
environment: dev
|
||||||
triggers:
|
triggers:
|
||||||
push: [main]
|
push: [main]
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Migrate DynamoDB table data from acdl-* → nova-* (REQ-163, P4).
|
||||||
|
|
||||||
|
The Nova rebrand (v1.15) renames the platform DynamoDB tables:
|
||||||
|
- ``acdl-contracts`` → ``nova-contracts``
|
||||||
|
- ``acdl-change-requests`` → ``nova-change-requests``
|
||||||
|
|
||||||
|
DynamoDB table names are immutable post-creation, so the migration is a
|
||||||
|
**scan + copy**: every item in the old table is written to the new table
|
||||||
|
(preserving the full item shape — PK, SK, and all attributes). Row counts
|
||||||
|
are verified to match post-copy. The old tables are **kept** until the
|
||||||
|
operator verifies the copy; deletion is a manual post-verification step
|
||||||
|
documented in ``docs/NOVA_AWS_MIGRATION.md`` (runbook).
|
||||||
|
|
||||||
|
Design:
|
||||||
|
- **Dry-run by default.** Prints the planned copy operations + counts
|
||||||
|
without touching AWS. Pass ``--apply`` to execute the copy.
|
||||||
|
- **Idempotent.** Re-running against an already-migrated item is a
|
||||||
|
no-op (``PutItem`` overwrites in place; the copy is re-run but the
|
||||||
|
row counts still match). The script does NOT delete the old tables
|
||||||
|
(deletion is a manual runbook step).
|
||||||
|
- **Item-mapping logic is pure + unit-tested** (see
|
||||||
|
``tests/test_migrate_dynamodb_data.py``); the AWS I/O is thin boto3
|
||||||
|
glue around ``map_item()`` + ``scan_all()``.
|
||||||
|
- **boto3 lazy import.** The module is importable + unit-testable
|
||||||
|
without AWS credentials (the client is constructed inside ``run()``).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/migrate_dynamodb_data.py # dry-run (default)
|
||||||
|
python3 scripts/migrate_dynamodb_data.py --apply # execute the copy
|
||||||
|
python3 scripts/migrate_dynamodb_data.py --region us-east-1 --apply
|
||||||
|
python3 scripts/migrate_dynamodb_data.py --table contracts --apply
|
||||||
|
python3 scripts/migrate_dynamodb_data.py --source acdl-contracts --dest nova-contracts --apply
|
||||||
|
|
||||||
|
Pre-requisites (live AWS, documented in the runbook):
|
||||||
|
- The nova-* destination tables must already exist (created via
|
||||||
|
``terraform/platform/main.tf``).
|
||||||
|
- AWS credentials in env with scan+PutItem on both old + new tables.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
import sys
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
try:
|
||||||
|
import boto3
|
||||||
|
except ImportError: # pragma: no cover - boto3 is a test dep
|
||||||
|
boto3 = None # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Default table-pair mapping (REQ-163)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
DEFAULT_TABLE_PAIRS: List[Tuple[str, str]] = [
|
||||||
|
("acdl-contracts", "nova-contracts"),
|
||||||
|
("acdl-change-requests", "nova-change-requests"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Pure item-mapping logic (unit-tested)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def map_item(item: Dict) -> Dict:
|
||||||
|
"""Return a copy of a DynamoDB item suitable for PutItem into the new table.
|
||||||
|
|
||||||
|
DynamoDB items returned by ``scan``/``get_item`` are in the typed-attribute
|
||||||
|
shape (``{"attr": {"S": "value"}, ...}``). The copy is identity-preserving:
|
||||||
|
the item is written verbatim to the destination table so the PK/SK + every
|
||||||
|
attribute land identically. No key-rewrite is needed because the old + new
|
||||||
|
tables share the same key schema (PK ``consumerRepo``, SK
|
||||||
|
``contractId#submittedAt`` for contracts; PK ``changeRequestId``, SK
|
||||||
|
``submittedAt`` for change-requests).
|
||||||
|
|
||||||
|
The mapping is a deep copy so callers can mutate the result without
|
||||||
|
aliasing the scanned item (DynamoDB items nest typed-attribute dicts,
|
||||||
|
e.g. ``{"attr": {"S": "value"}}``). ``map_item`` is pure +
|
||||||
|
side-effect-free.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> map_item({"consumerRepo": {"S": "acdl/c"}, "k": {"N": "1"}})
|
||||||
|
{'consumerRepo': {'S': 'acdl/c'}, 'k': {'N': '1'}}
|
||||||
|
>>> map_item({}) == {}
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
return copy.deepcopy(item)
|
||||||
|
|
||||||
|
|
||||||
|
def table_pair_for(name: str, pairs: Optional[List[Tuple[str, str]]] = None) -> Tuple[str, str]:
|
||||||
|
"""Resolve a logical table name (``contracts`` / ``change-requests``) or a
|
||||||
|
literal source-table name to its ``(source, dest)`` pair.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> table_pair_for("contracts")
|
||||||
|
('acdl-contracts', 'nova-contracts')
|
||||||
|
>>> table_pair_for("change-requests")
|
||||||
|
('acdl-change-requests', 'nova-change-requests')
|
||||||
|
>>> table_pair_for("acdl-contracts")
|
||||||
|
('acdl-contracts', 'nova-contracts')
|
||||||
|
>>> table_pair_for("nova-contracts")
|
||||||
|
('nova-contracts', 'nova-contracts')
|
||||||
|
"""
|
||||||
|
table = pairs if pairs is not None else DEFAULT_TABLE_PAIRS
|
||||||
|
aliases = {
|
||||||
|
"contracts": ("acdl-contracts", "nova-contracts"),
|
||||||
|
"change-requests": ("acdl-change-requests", "nova-change-requests"),
|
||||||
|
}
|
||||||
|
if name in aliases:
|
||||||
|
return aliases[name]
|
||||||
|
for src, dst in table:
|
||||||
|
if name == src:
|
||||||
|
return (src, dst)
|
||||||
|
if name == dst:
|
||||||
|
return (src, dst)
|
||||||
|
raise ValueError(
|
||||||
|
f"unknown table {name!r}; expected one of: contracts, change-requests, "
|
||||||
|
f"or a literal source name from {table!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Thin AWS I/O glue (constructed lazily inside run)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def scan_all(client, table_name: str) -> List[Dict]:
|
||||||
|
"""Scan every item in ``table_name`` (paginates through all segments).
|
||||||
|
|
||||||
|
Returns the full list of items (typed-attribute shape). Uses
|
||||||
|
``table.scan()`` with pagination on ``LastEvaluatedKey``.
|
||||||
|
"""
|
||||||
|
items: List[Dict] = []
|
||||||
|
last_key: Optional[Dict] = None
|
||||||
|
while True:
|
||||||
|
kwargs: Dict = {"TableName": table_name}
|
||||||
|
if last_key is not None:
|
||||||
|
kwargs["ExclusiveStartKey"] = last_key
|
||||||
|
resp = client.scan(**kwargs)
|
||||||
|
items.extend(resp.get("Items", []))
|
||||||
|
last_key = resp.get("LastEvaluatedKey")
|
||||||
|
if not last_key:
|
||||||
|
break
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def copy_items(client, source_table: str, dest_table: str, items: List[Dict]) -> int:
|
||||||
|
"""PutItem every mapped item into ``dest_table``. Returns the count written."""
|
||||||
|
written = 0
|
||||||
|
for item in items:
|
||||||
|
client.put_item(TableName=dest_table, Item=map_item(item))
|
||||||
|
written += 1
|
||||||
|
return written
|
||||||
|
|
||||||
|
|
||||||
|
def count_items(client, table_name: str) -> int:
|
||||||
|
"""Return the approximate item count via ``DescribeTable``.
|
||||||
|
|
||||||
|
Uses ``Table.ItemCount`` (updated ~6hourly by AWS) for a fast count; for
|
||||||
|
exact verification prefer ``len(scan_all(...))`` (the runbook documents
|
||||||
|
both — scan is the source of truth for row-count verification).
|
||||||
|
"""
|
||||||
|
resp = client.describe_table(TableName=table_name)
|
||||||
|
return int(resp["Table"].get("ItemCount", 0))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Driver
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def run(args: argparse.Namespace) -> int:
|
||||||
|
pairs = DEFAULT_TABLE_PAIRS
|
||||||
|
if args.source and args.dest:
|
||||||
|
pairs = [(args.source, args.dest)]
|
||||||
|
elif args.table:
|
||||||
|
pairs = [table_pair_for(args.table)]
|
||||||
|
|
||||||
|
region = args.region
|
||||||
|
if boto3 is None:
|
||||||
|
print("FAIL: boto3 is not installed (pip install boto3)", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
client = boto3.client("dynamodb", region_name=region)
|
||||||
|
mode = "APPLY" if args.apply else "DRY-RUN"
|
||||||
|
overall_rc = 0
|
||||||
|
|
||||||
|
for source, dest in pairs:
|
||||||
|
print(f"\n=== {mode}: {source} → {dest} (region {region}) ===")
|
||||||
|
try:
|
||||||
|
client.describe_table(TableName=source)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" FAIL: source table {source!r} not describable: "
|
||||||
|
f"{type(e).__name__}: {e}", file=sys.stderr)
|
||||||
|
overall_rc = 1
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
client.describe_table(TableName=dest)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" FAIL: dest table {dest!r} not describable (create it via "
|
||||||
|
f"terraform first): {type(e).__name__}: {e}", file=sys.stderr)
|
||||||
|
overall_rc = 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
items = scan_all(client, source)
|
||||||
|
src_count = len(items)
|
||||||
|
print(f" scanned {src_count} item(s) from {source}")
|
||||||
|
|
||||||
|
if not args.apply:
|
||||||
|
print(f" [dry-run] would PutItem {src_count} item(s) into {dest}")
|
||||||
|
print(f" [dry-run] would verify {dest} row count == {src_count}")
|
||||||
|
print(f" [dry-run] old table {source} is NOT deleted (manual runbook step)")
|
||||||
|
continue
|
||||||
|
|
||||||
|
written = copy_items(client, source, dest, items)
|
||||||
|
print(f" copied {written} item(s) → {dest}")
|
||||||
|
|
||||||
|
# Verify by re-scanning the destination (source of truth, not DescribeTable).
|
||||||
|
dest_items = scan_all(client, dest)
|
||||||
|
dest_count = len(dest_items)
|
||||||
|
if dest_count != src_count:
|
||||||
|
print(f" WARNING: row-count mismatch — source={src_count}, "
|
||||||
|
f"dest={dest_count}. Investigate before deleting {source}.",
|
||||||
|
file=sys.stderr)
|
||||||
|
overall_rc = 1
|
||||||
|
else:
|
||||||
|
print(f" VERIFIED: {dest} row count ({dest_count}) == source ({src_count})")
|
||||||
|
print(f" Old table {source} is KEPT. Delete it manually only after "
|
||||||
|
f"verifying consumers read from {dest} (runbook step).")
|
||||||
|
|
||||||
|
if overall_rc == 0:
|
||||||
|
print(f"\n=== {mode} complete ({len(pairs)} pair(s)) ===")
|
||||||
|
else:
|
||||||
|
print(f"\n=== {mode} complete with FAILURES ===", file=sys.stderr)
|
||||||
|
return overall_rc
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
p = argparse.ArgumentParser(
|
||||||
|
description="Migrate DynamoDB data acdl-* → nova-* (REQ-163, P4).",
|
||||||
|
)
|
||||||
|
p.add_argument("--apply", action="store_true",
|
||||||
|
help="Execute the copy (default: dry-run, no AWS writes).")
|
||||||
|
p.add_argument("--region", default="us-east-1",
|
||||||
|
help="AWS region (default: us-east-1).")
|
||||||
|
p.add_argument("--table", default=None,
|
||||||
|
help="Migrate a single logical table: 'contracts' or "
|
||||||
|
"'change-requests' (default: both).")
|
||||||
|
p.add_argument("--source", default=None,
|
||||||
|
help="Override the source table name (paired with --dest).")
|
||||||
|
p.add_argument("--dest", default=None,
|
||||||
|
help="Override the destination table name (paired with --source).")
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Optional[List[str]] = None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
return run(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""ACDL Phase 15 — push the consumer microservice Docker image to ECR.
|
"""Nova Phase 15 — push the consumer microservice Docker image to ECR.
|
||||||
|
|
||||||
Steps performed by this script:
|
Steps performed by this script:
|
||||||
1. Load AWS creds from /root/acdl/.env.secrets
|
1. Load AWS creds from /root/acdl/.env.secrets
|
||||||
(NOVA_AWS_ACCESS_KEY_ID, NOVA_AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION;
|
(NOVA_AWS_ACCESS_KEY_ID, NOVA_AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION;
|
||||||
dual-read ACDL_* fallback until P5).
|
dual-read ACDL_* fallback until P5).
|
||||||
2. Create the ECR repo `acdl-microservice` if it doesn't exist
|
2. Create the ECR repo `nova-microservice` if it doesn't exist
|
||||||
(ecr:DescribeRepositories / ecr:CreateRepository). Region: us-east-1.
|
(ecr:DescribeRepositories / ecr:CreateRepository). Region: us-east-1.
|
||||||
3. Get the ECR login password (ecr:GetAuthorizationToken) and run
|
3. Get the ECR login password (ecr:GetAuthorizationToken) and run
|
||||||
`docker login` with it.
|
`docker login` with it.
|
||||||
@@ -40,7 +40,7 @@ REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|||||||
ENV_FILE = REPO_ROOT / ".env.secrets"
|
ENV_FILE = REPO_ROOT / ".env.secrets"
|
||||||
AWS_ACCOUNT_ID = env.get_env("AWS_ACCOUNT_ID", "581513795199")
|
AWS_ACCOUNT_ID = env.get_env("AWS_ACCOUNT_ID", "581513795199")
|
||||||
AWS_REGION = "us-east-1"
|
AWS_REGION = "us-east-1"
|
||||||
ECR_REPO_NAME = "acdl-microservice"
|
ECR_REPO_NAME = "nova-microservice"
|
||||||
IMAGE_TAG = "latest"
|
IMAGE_TAG = "latest"
|
||||||
|
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ def main():
|
|||||||
full_tag = f"{repo_uri}:{IMAGE_TAG}"
|
full_tag = f"{repo_uri}:{IMAGE_TAG}"
|
||||||
print("")
|
print("")
|
||||||
print("=== NEXT: run these commands in the shell to tag + push ===")
|
print("=== NEXT: run these commands in the shell to tag + push ===")
|
||||||
print(f"docker tag acdl-microservice:latest {full_tag}")
|
print(f"docker tag nova-microservice:latest {full_tag}")
|
||||||
print(f"docker push {full_tag}")
|
print(f"docker push {full_tag}")
|
||||||
print("")
|
print("")
|
||||||
print(f"ECR_IMAGE={full_tag}")
|
print(f"ECR_IMAGE={full_tag}")
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# scripts/rotate_spike_key.sh - rotate the acdl-spike-runner IAM access key.
|
# scripts/rotate_spike_key.sh - rotate the nova-spike-runner IAM access key.
|
||||||
#
|
#
|
||||||
# Uses the bootstrap root key (NOVA_BOOTSTRAP_AWS_*, ACDL_BOOTSTRAP_AWS_*
|
# Uses the bootstrap root key (NOVA_BOOTSTRAP_AWS_*, ACDL_BOOTSTRAP_AWS_*
|
||||||
# fallback) from the env to:
|
# fallback) from the env to:
|
||||||
# 1. List acdl-spike-runner's access keys.
|
# 1. List nova-spike-runner's access keys.
|
||||||
# 2. Create a new key.
|
# 2. Create a new key.
|
||||||
# 3. Deactivate + delete the old key(s).
|
# 3. Deactivate + delete the old key(s).
|
||||||
# 4. Write the new key to gitignored .env.secrets (chmod 600).
|
# 4. Write the new key to gitignored .env.secrets (chmod 600).
|
||||||
@@ -14,9 +14,8 @@
|
|||||||
#
|
#
|
||||||
# Spike scope (D-039): the spike user key is per-run-rotated; real OIDC is
|
# Spike scope (D-039): the spike user key is per-run-rotated; real OIDC is
|
||||||
# v1.2 (blocked on go-gitea/gitea#36988).
|
# v1.2 (blocked on go-gitea/gitea#36988).
|
||||||
# Nova rebrand (P2): writes NOVA_* keys; ACDL_* bootstrap fallback kept
|
# Nova rebrand (P4, REQ-163): IAM user renamed acdl-spike-runner →
|
||||||
# until P5 (the AWS user/role rename acdl-spike-runner → nova-spike-runner
|
# nova-spike-runner.
|
||||||
# is P4 territory — left unchanged here).
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
@@ -28,7 +27,7 @@ fail() { echo "FAIL: $*" >&2; exit 1; }
|
|||||||
: "${NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID:-${ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID (or ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID) to the root key}}"
|
: "${NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID:-${ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID (or ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID) to the root key}}"
|
||||||
: "${NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:-${ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY (or ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY) to the root key}}"
|
: "${NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:-${ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY (or ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY) to the root key}}"
|
||||||
REGION="${AWS_DEFAULT_REGION:-us-east-1}"
|
REGION="${AWS_DEFAULT_REGION:-us-east-1}"
|
||||||
USER_NAME="acdl-spike-runner"
|
USER_NAME="nova-spike-runner"
|
||||||
|
|
||||||
# Confirm .env.secrets is gitignored before writing to it.
|
# Confirm .env.secrets is gitignored before writing to it.
|
||||||
git check-ignore -q "$ENV_FILE" || fail "$ENV_FILE is not gitignored — refusing to write the key"
|
git check-ignore -q "$ENV_FILE" || fail "$ENV_FILE is not gitignored — refusing to write the key"
|
||||||
@@ -40,7 +39,7 @@ import json
|
|||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||||
user = "acdl-spike-runner"
|
user = "nova-spike-runner"
|
||||||
env_file = os.path.join(os.getcwd(), ".env.secrets")
|
env_file = os.path.join(os.getcwd(), ".env.secrets")
|
||||||
|
|
||||||
# Dual-read bootstrap creds: NOVA_* preferred, ACDL_* fallback (G-106, removed in P5).
|
# Dual-read bootstrap creds: NOVA_* preferred, ACDL_* fallback (G-106, removed in P5).
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ python3 -m py_compile \
|
|||||||
adapters/wiz/wiz_adapter.py \
|
adapters/wiz/wiz_adapter.py \
|
||||||
adapters/kyverno/kyverno_adapter.py \
|
adapters/kyverno/kyverno_adapter.py \
|
||||||
scripts/push_consumer_image.py \
|
scripts/push_consumer_image.py \
|
||||||
|
scripts/migrate_dynamodb_data.py \
|
||||||
|| fail "lint: py_compile failed"
|
|| fail "lint: py_compile failed"
|
||||||
echo "lint: OK"
|
echo "lint: OK"
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
# ACDL v1.1 Spike — AWS Bootstrap Runbook
|
# Nova v1.1 Spike — AWS Bootstrap Runbook
|
||||||
|
|
||||||
Phase 08 bootstraps the AWS engine for the v1.1 spike. It uses the
|
Phase 08 bootstraps the AWS engine for the v1.1 spike. It uses the
|
||||||
**root account credential for account 581513795199 exactly once**, then
|
**root account credential for account 581513795199 exactly once**, then
|
||||||
closes D-034 by having the user manually rotate the root key afterward.
|
closes D-034 by having the user manually rotate the root key afterward.
|
||||||
|
|
||||||
> **Spike scope (D-039):** the spike uses a per-run-rotated IAM *user* key
|
> **Spike scope (D-039):** the spike uses a per-run-rotated IAM *user* key
|
||||||
> (`acdl-spike-runner`), NOT OIDC. Real OIDC federation is deferred to
|
> (`nova-spike-runner`), NOT OIDC. Real OIDC federation is deferred to
|
||||||
> v1.2 (blocked on go-gitea/gitea#36988 — Gitea Actions does not support
|
> v1.2 (blocked on go-gitea/gitea#36988 — Gitea Actions does not support
|
||||||
> `id-token: write`). The `acdl-spike-runner` user + its key are deleted
|
> `id-token: write`). The `nova-spike-runner` user + its key are deleted
|
||||||
> in v1.2 cleanup when the OIDC role lands.
|
> in v1.2 cleanup when the OIDC role lands.
|
||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
1. **Set the bootstrap root key in env** (never commit, never echo):
|
1. **Set the bootstrap root key in env** (never commit, never echo):
|
||||||
```bash
|
```bash
|
||||||
export ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID="<root key>"
|
export NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID="<root key>"
|
||||||
export ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY="<root secret>"
|
export NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY="<root secret>"
|
||||||
export AWS_DEFAULT_REGION="us-east-1"
|
export AWS_DEFAULT_REGION="us-east-1"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ closes D-034 by having the user manually rotate the root key afterward.
|
|||||||
```bash
|
```bash
|
||||||
python3 terraform/bootstrap/create_iam_user.py
|
python3 terraform/bootstrap/create_iam_user.py
|
||||||
```
|
```
|
||||||
Prints `ACDL_AWS_ACCESS_KEY_ID=<...>` + `ACDL_AWS_SECRET_ACCESS_KEY=<...>`
|
Prints `NOVA_AWS_ACCESS_KEY_ID=<...>` + `NOVA_AWS_SECRET_ACCESS_KEY=<...>`
|
||||||
to stdout (capture if you want the initial key; `rotate_spike_key.sh`
|
to stdout (capture if you want the initial key; `rotate_spike_key.sh`
|
||||||
creates a fresh one anyway).
|
creates a fresh one anyway).
|
||||||
|
|
||||||
@@ -38,9 +38,9 @@ closes D-034 by having the user manually rotate the root key afterward.
|
|||||||
```bash
|
```bash
|
||||||
bash scripts/rotate_spike_key.sh
|
bash scripts/rotate_spike_key.sh
|
||||||
```
|
```
|
||||||
Optionally uploads to Gitea Actions secrets if `ACDL_GITEA_TOKEN` is set.
|
Optionally uploads to Gitea Actions secrets if `NOVA_GITEA_TOKEN` is set.
|
||||||
|
|
||||||
5. **Verify** (manual): confirm the caller identity is `acdl-spike-runner`
|
5. **Verify** (manual): confirm the caller identity is `nova-spike-runner`
|
||||||
(not root); the S3 bucket + DynamoDB table + IAM user + scoped policy
|
(not root); the S3 bucket + DynamoDB table + IAM user + scoped policy
|
||||||
all exist; `.env.secrets` + `.bootstrap_state.json` are gitignored.
|
all exist; `.env.secrets` + `.bootstrap_state.json` are gitignored.
|
||||||
(`scripts/verify_phase08.sh` was the automated gate; it has been
|
(`scripts/verify_phase08.sh` was the automated gate; it has been
|
||||||
@@ -49,33 +49,33 @@ closes D-034 by having the user manually rotate the root key afterward.
|
|||||||
6. **MANUAL — D-034 closure:** rotate/deactivate the **root** key in the
|
6. **MANUAL — D-034 closure:** rotate/deactivate the **root** key in the
|
||||||
AWS IAM console (the user does this, not the script). The bootstrap
|
AWS IAM console (the user does this, not the script). The bootstrap
|
||||||
root key has now served its one-shot purpose; the spike uses the
|
root key has now served its one-shot purpose; the spike uses the
|
||||||
rotated `acdl-spike-runner` key for Phases 09-10.
|
rotated `nova-spike-runner` key for Phases 09-10.
|
||||||
|
|
||||||
## v1.11 Phase 56 — IAM re-bootstrap + OIDC role (REQ-116)
|
## v1.11 Phase 56 — IAM re-bootstrap + OIDC role (REQ-116)
|
||||||
|
|
||||||
The v1.11 milestone re-bootstraps IAM to close G-005 (CAP-017..022
|
The v1.11 milestone re-bootstraps IAM to close G-005 (CAP-017..022
|
||||||
deploy-unverified). Phase 56 extends the spike-runner policy with
|
deploy-unverified). Phase 56 extends the spike-runner policy with
|
||||||
CloudFront/WAF/Lambda/DynamoDB-contracts/SecretsManager/SNS/CE/KMS/OIDC
|
CloudFront/WAF/Lambda/DynamoDB-contracts/SecretsManager/SNS/CE/KMS/OIDC
|
||||||
permissions and re-creates the `acdl-act-runner-role` (CAP-022).
|
permissions and re-creates the `nova-act-runner-role` (CAP-022).
|
||||||
|
|
||||||
**Apply the IAM baseline (idempotent):**
|
**Apply the IAM baseline (idempotent):**
|
||||||
```bash
|
```bash
|
||||||
export ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID="<root key>"
|
export NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID="<root key>"
|
||||||
export ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY="<root secret>"
|
export NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY="<root secret>"
|
||||||
export AWS_DEFAULT_REGION="us-east-1"
|
export AWS_DEFAULT_REGION="us-east-1"
|
||||||
python3 terraform/bootstrap/apply_iam_baseline.py
|
python3 terraform/bootstrap/apply_iam_baseline.py
|
||||||
```
|
```
|
||||||
|
|
||||||
This script:
|
This script:
|
||||||
1. Creates (or versions) the customer-managed policy
|
1. Creates (or versions) the customer-managed policy
|
||||||
`acdl-spike-runner-policy` from
|
`nova-spike-runner-policy` from
|
||||||
`terraform/bootstrap/spike_runner_policy.json` (ARN
|
`terraform/bootstrap/spike_runner_policy.json` (ARN
|
||||||
`arn:aws:iam::581513795199:policy/acdl-spike-runner-policy`).
|
`arn:aws:iam::581513795199:policy/nova-spike-runner-policy`).
|
||||||
2. Attaches it to the `acdl-spike-runner` user and deletes any leftover
|
2. Attaches it to the `nova-spike-runner` user and deletes any leftover
|
||||||
inline policy (the v1.1 inline policy hit the 2048-byte limit; the
|
inline policy (the v1.1 inline policy hit the 2048-byte limit; the
|
||||||
managed-policy path supports 6144 bytes per version + up to 5
|
managed-policy path supports 6144 bytes per version + up to 5
|
||||||
versions).
|
versions).
|
||||||
3. Re-creates the `acdl-act-runner-role` OIDC role if absent, attaches
|
3. Re-creates the `nova-act-runner-role` OIDC role if absent, attaches
|
||||||
the same managed policy, and sets a trust policy that permits root
|
the same managed policy, and sets a trust policy that permits root
|
||||||
assume until go-gitea/gitea#36988 merges real OIDC federation.
|
assume until go-gitea/gitea#36988 merges real OIDC federation.
|
||||||
|
|
||||||
@@ -84,9 +84,9 @@ regression-tested by `tests/test_iam_policy_baseline.py` (15 tests).
|
|||||||
|
|
||||||
## What the spike uses for Phases 09-10
|
## What the spike uses for Phases 09-10
|
||||||
|
|
||||||
- **State backend:** S3 bucket `acdl-tfstate-581513795199-us-east-1` +
|
- **State backend:** S3 bucket `nova-tfstate-581513795199-us-east-1` +
|
||||||
DynamoDB table `acdl-outbox` (one table for both lock + outbox, D-P08-1).
|
DynamoDB table `nova-outbox` (one table for both lock + outbox, D-P08-1).
|
||||||
- **Auth:** the rotated `acdl-spike-runner` key in `.env.secrets`
|
- **Auth:** the rotated `nova-spike-runner` key in `.env.secrets`
|
||||||
(gitignored, chmod 600). Re-rotate after each spike run via
|
(gitignored, chmod 600). Re-rotate after each spike run via
|
||||||
`rotate_spike_key.sh` (D-039).
|
`rotate_spike_key.sh` (D-039).
|
||||||
|
|
||||||
@@ -95,6 +95,6 @@ regression-tested by `tests/test_iam_policy_baseline.py` (15 tests).
|
|||||||
| Concern | Spike (Phase 08) | v1.2 |
|
| Concern | Spike (Phase 08) | v1.2 |
|
||||||
|---------|------------------|------|
|
|---------|------------------|------|
|
||||||
| AWS auth | per-run-rotated long-lived key (D-039 waiver) | real OIDC federation (go-gitea/gitea#36988) |
|
| AWS auth | per-run-rotated long-lived key (D-039 waiver) | real OIDC federation (go-gitea/gitea#36988) |
|
||||||
| IAM | minimal user `acdl-spike-runner` + scoped policy | OIDC role + trust policy (no user, no key) |
|
| IAM | minimal user `nova-spike-runner` + scoped policy | OIDC role + trust policy (no user, no key) |
|
||||||
| State backend | S3 + DynamoDB single-region (us-east-1) | multi-region |
|
| State backend | S3 + DynamoDB single-region (us-east-1) | multi-region |
|
||||||
| Secret storage | gitignored `.env.secrets` + optional Gitea secret | Gitea OIDC-issued web-identity token (no secret) |
|
| Secret storage | gitignored `.env.secrets` + optional Gitea secret | Gitea OIDC-issued web-identity token (no secret) |
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Apply the ACDL spike-runner managed policy + OIDC act_runner role.
|
"""Apply the Nova spike-runner managed policy + OIDC act_runner role.
|
||||||
|
|
||||||
Phase 56 (REQ-116, v1.11). Idempotent: re-running creates the managed
|
Phase 56 (REQ-116, v1.11). Idempotent: re-running creates the managed
|
||||||
policy if absent (or creates a new version if the policy document
|
policy if absent (or creates a new version if the policy document
|
||||||
@@ -31,10 +31,10 @@ import boto3
|
|||||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
POLICY_PATH = ROOT / "terraform" / "bootstrap" / "spike_runner_policy.json"
|
POLICY_PATH = ROOT / "terraform" / "bootstrap" / "spike_runner_policy.json"
|
||||||
ACCOUNT = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
|
ACCOUNT = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
|
||||||
USER = "acdl-spike-runner"
|
USER = "nova-spike-runner"
|
||||||
POLICY_NAME = "acdl-spike-runner-policy"
|
POLICY_NAME = "nova-spike-runner-policy"
|
||||||
POLICY_ARN = f"arn:aws:iam::{ACCOUNT}:policy/{POLICY_NAME}"
|
POLICY_ARN = f"arn:aws:iam::{ACCOUNT}:policy/{POLICY_NAME}"
|
||||||
ROLE_NAME = "acdl-act-runner-role"
|
ROLE_NAME = "nova-act-runner-role"
|
||||||
|
|
||||||
|
|
||||||
def _session():
|
def _session():
|
||||||
@@ -86,7 +86,7 @@ def apply_managed_policy(iam, policy_doc: str) -> str:
|
|||||||
PolicyName=POLICY_NAME,
|
PolicyName=POLICY_NAME,
|
||||||
Path="/",
|
Path="/",
|
||||||
PolicyDocument=policy_doc,
|
PolicyDocument=policy_doc,
|
||||||
Description="ACDL spike-runner baseline (v1.11 REQ-116). Extended from inline user policy to managed policy to fit the 6144-byte limit.",
|
Description="Nova spike-runner baseline (v1.11 REQ-116). Extended from inline user policy to managed policy to fit the 6144-byte limit.",
|
||||||
)
|
)
|
||||||
print(f"created: {created['Policy']['Arn']}")
|
print(f"created: {created['Policy']['Arn']}")
|
||||||
return created["Policy"]["Arn"]
|
return created["Policy"]["Arn"]
|
||||||
@@ -117,10 +117,10 @@ def ensure_runner_role(iam):
|
|||||||
iam.create_role(
|
iam.create_role(
|
||||||
RoleName=ROLE_NAME,
|
RoleName=ROLE_NAME,
|
||||||
AssumeRolePolicyDocument=json.dumps(_trust_policy_for_runner()),
|
AssumeRolePolicyDocument=json.dumps(_trust_policy_for_runner()),
|
||||||
Description="ACDL act_runner OIDC role (CAP-022, v1.11 Phase 56 re-creation). Trust policy permits root assume until go-gitea/gitea#36988 merges real OIDC federation.",
|
Description="Nova act_runner OIDC role (CAP-022, v1.11 Phase 56 re-creation). Trust policy permits root assume until go-gitea/gitea#36988 merges real OIDC federation.",
|
||||||
MaxSessionDuration=3600,
|
MaxSessionDuration=3600,
|
||||||
Tags=[
|
Tags=[
|
||||||
{"Key": "Project", "Value": "acdl"},
|
{"Key": "Project", "Value": "nova"},
|
||||||
{"Key": "Capability", "Value": "CAP-022"},
|
{"Key": "Capability", "Value": "CAP-022"},
|
||||||
{"Key": "Milestone", "Value": "v1.11"},
|
{"Key": "Milestone", "Value": "v1.11"},
|
||||||
{"Key": "ManagedBy", "Value": "ciagent"},
|
{"Key": "ManagedBy", "Value": "ciagent"},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Create the ACDL v1.1 spike IAM user + scoped inline policy + initial key.
|
"""Create the Nova v1.1 spike IAM user + scoped inline policy + initial key.
|
||||||
|
|
||||||
Idempotent: skips user creation if the user exists; creates an initial
|
Idempotent: skips user creation if the user exists; creates an initial
|
||||||
access key if none active exists. Prints the key to stdout for the
|
access key if none active exists. Prints the key to stdout for the
|
||||||
@@ -32,8 +32,8 @@ import boto3
|
|||||||
|
|
||||||
|
|
||||||
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||||
USER_NAME = "acdl-spike-runner"
|
USER_NAME = "nova-spike-runner"
|
||||||
POLICY_NAME = "acdl-spike-runner-policy"
|
POLICY_NAME = "nova-spike-runner-policy"
|
||||||
POLICY_FILE = os.path.join(os.path.dirname(__file__), "spike_runner_policy.json")
|
POLICY_FILE = os.path.join(os.path.dirname(__file__), "spike_runner_policy.json")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Create the ACDL v1.1 spike AWS state backend (idempotent).
|
"""Create the Nova v1.1 spike AWS state backend (idempotent).
|
||||||
|
|
||||||
- S3 bucket acdl-tfstate-<account_id>-us-east-1 (versioning enabled).
|
- S3 bucket nova-tfstate-<account_id>-us-east-1 (versioning enabled).
|
||||||
- DynamoDB table acdl-outbox (PAY_PER_REQUEST; PK contractId, SK
|
- DynamoDB table nova-outbox (PAY_PER_REQUEST; PK contractId, SK
|
||||||
eventType#eventTs) — used for BOTH Terraform state locking AND the
|
eventType#eventTs) — used for BOTH Terraform state locking AND the
|
||||||
evidence outbox (D-P08-1).
|
evidence outbox (D-P08-1).
|
||||||
|
|
||||||
@@ -31,8 +31,8 @@ import boto3
|
|||||||
|
|
||||||
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
||||||
ACCOUNT_ID = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
|
ACCOUNT_ID = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
|
||||||
STATE_BUCKET = f"acdl-tfstate-{ACCOUNT_ID}-us-east-1"
|
STATE_BUCKET = f"nova-tfstate-{ACCOUNT_ID}-us-east-1"
|
||||||
OUTBOX_TABLE = "acdl-outbox"
|
OUTBOX_TABLE = "nova-outbox"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
"s3:GetBucketVersioning"
|
"s3:GetBucketVersioning"
|
||||||
],
|
],
|
||||||
"Resource": [
|
"Resource": [
|
||||||
"arn:aws:s3:::acdl-tfstate-581513795199-us-east-1",
|
"arn:aws:s3:::nova-tfstate-581513795199-us-east-1",
|
||||||
"arn:aws:s3:::acdl-tfstate-581513795199-us-east-1/*"
|
"arn:aws:s3:::nova-tfstate-581513795199-us-east-1/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"dynamodb:Scan",
|
"dynamodb:Scan",
|
||||||
"dynamodb:DescribeTable"
|
"dynamodb:DescribeTable"
|
||||||
],
|
],
|
||||||
"Resource": "arn:aws:dynamodb:us-east-1:581513795199:table/acdl-outbox"
|
"Resource": "arn:aws:dynamodb:us-east-1:581513795199:table/nova-outbox"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
"lambda:UntagResource",
|
"lambda:UntagResource",
|
||||||
"lambda:PublishLayerVersion"
|
"lambda:PublishLayerVersion"
|
||||||
],
|
],
|
||||||
"Resource": "arn:aws:lambda:us-east-1:581513795199:function:acdl-*"
|
"Resource": "arn:aws:lambda:us-east-1:581513795199:function:nova-*"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
@@ -158,10 +158,10 @@
|
|||||||
"dynamodb:Batch*"
|
"dynamodb:Batch*"
|
||||||
],
|
],
|
||||||
"Resource": [
|
"Resource": [
|
||||||
"arn:aws:dynamodb:us-east-1:581513795199:table/acdl-contracts",
|
"arn:aws:dynamodb:us-east-1:581513795199:table/nova-contracts",
|
||||||
"arn:aws:dynamodb:us-east-1:581513795199:table/acdl-contracts/*",
|
"arn:aws:dynamodb:us-east-1:581513795199:table/nova-contracts/*",
|
||||||
"arn:aws:dynamodb:us-east-1:581513795199:table/acdl-change-requests",
|
"arn:aws:dynamodb:us-east-1:581513795199:table/nova-change-requests",
|
||||||
"arn:aws:dynamodb:us-east-1:581513795199:table/acdl-change-requests/*"
|
"arn:aws:dynamodb:us-east-1:581513795199:table/nova-change-requests/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
"secretsmanager:DeleteSecret",
|
"secretsmanager:DeleteSecret",
|
||||||
"secretsmanager:ListSecrets"
|
"secretsmanager:ListSecrets"
|
||||||
],
|
],
|
||||||
"Resource": "arn:aws:secretsmanager:us-east-1:581513795199:secret:acdl/*"
|
"Resource": "arn:aws:secretsmanager:us-east-1:581513795199:secret:nova/*"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
"sns:DeleteTopic",
|
"sns:DeleteTopic",
|
||||||
"sns:ListTopics"
|
"sns:ListTopics"
|
||||||
],
|
],
|
||||||
"Resource": "arn:aws:sns:us-east-1:581513795199:acdl-*"
|
"Resource": "arn:aws:sns:us-east-1:581513795199:nova-*"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
],
|
],
|
||||||
"Resource": [
|
"Resource": [
|
||||||
"arn:aws:kms:*:*:key/*",
|
"arn:aws:kms:*:*:key/*",
|
||||||
"arn:aws:kms:*:*:alias/acdl-*"
|
"arn:aws:kms:*:*:alias/nova-*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -236,7 +236,7 @@
|
|||||||
"iam:TagRole",
|
"iam:TagRole",
|
||||||
"iam:UntagRole"
|
"iam:UntagRole"
|
||||||
],
|
],
|
||||||
"Resource": "arn:aws:iam::*:role/acdl-*"
|
"Resource": "arn:aws:iam::*:role/nova-*"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# ACDL CI VPC — short-lived VPC for L1 module lifecycle testing.
|
# Nova CI VPC — short-lived VPC for L1 module lifecycle testing.
|
||||||
#
|
#
|
||||||
# Created by the modules-lifecycle pipeline before testing VPC-dependent
|
# Created by the modules-lifecycle pipeline before testing VPC-dependent
|
||||||
# modules (alb, ecs-service, rds, uptime). Destroyed after all tests complete.
|
# modules (alb, ecs-service, rds, uptime). Destroyed after all tests complete.
|
||||||
@@ -15,7 +15,7 @@ terraform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
backend "s3" {
|
backend "s3" {
|
||||||
bucket = "acdl-tfstate-581513795199-us-east-1"
|
bucket = "nova-tfstate-581513795199-us-east-1"
|
||||||
key = "spike/ci-vpc/terraform.tfstate"
|
key = "spike/ci-vpc/terraform.tfstate"
|
||||||
region = "us-east-1"
|
region = "us-east-1"
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ data "aws_availability_zones" "available" {
|
|||||||
resource "aws_vpc" "ci" {
|
resource "aws_vpc" "ci" {
|
||||||
cidr_block = "10.1.0.0/16"
|
cidr_block = "10.1.0.0/16"
|
||||||
tags = {
|
tags = {
|
||||||
Name = "acdl-ci-vpc"
|
Name = "nova-ci-vpc"
|
||||||
"nova:owner" = "acdl"
|
"nova:owner" = "acdl"
|
||||||
"nova:environment" = "ci"
|
"nova:environment" = "ci"
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ resource "aws_subnet" "ci" {
|
|||||||
cidr_block = cidrsubnet(aws_vpc.ci.cidr_block, 8, count.index + 1)
|
cidr_block = cidrsubnet(aws_vpc.ci.cidr_block, 8, count.index + 1)
|
||||||
availability_zone = data.aws_availability_zones.available.names[count.index]
|
availability_zone = data.aws_availability_zones.available.names[count.index]
|
||||||
tags = {
|
tags = {
|
||||||
Name = "acdl-ci-subnet-${count.index}"
|
Name = "nova-ci-subnet-${count.index}"
|
||||||
"nova:owner" = "acdl"
|
"nova:owner" = "acdl"
|
||||||
"nova:environment" = "ci"
|
"nova:environment" = "ci"
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ resource "aws_subnet" "ci" {
|
|||||||
resource "aws_internet_gateway" "ci" {
|
resource "aws_internet_gateway" "ci" {
|
||||||
vpc_id = aws_vpc.ci.id
|
vpc_id = aws_vpc.ci.id
|
||||||
tags = {
|
tags = {
|
||||||
Name = "acdl-ci-igw"
|
Name = "nova-ci-igw"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ resource "aws_route_table_association" "ci" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_security_group" "ecs" {
|
resource "aws_security_group" "ecs" {
|
||||||
name = "acdl-ci-ecs-sg"
|
name = "nova-ci-ecs-sg"
|
||||||
description = "Security group for CI ECS services"
|
description = "Security group for CI ECS services"
|
||||||
vpc_id = aws_vpc.ci.id
|
vpc_id = aws_vpc.ci.id
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ resource "aws_security_group" "ecs" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_ecs_cluster" "ci" {
|
resource "aws_ecs_cluster" "ci" {
|
||||||
name = "acdl-ci-cluster"
|
name = "nova-ci-cluster"
|
||||||
}
|
}
|
||||||
|
|
||||||
output "vpc_id" {
|
output "vpc_id" {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
resource "aws_vpc" "vpc-vpc" {
|
resource "aws_vpc" "vpc-vpc" {
|
||||||
cidr_block = "10.0.0.0/16"
|
cidr_block = "10.0.0.0/16"
|
||||||
tags = {
|
tags = {
|
||||||
Name = "acdl-microservice"
|
Name = "nova-microservice"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,9 +11,9 @@ output "vpc_id" {
|
|||||||
|
|
||||||
resource "aws_subnet" "vpc-subnet" {
|
resource "aws_subnet" "vpc-subnet" {
|
||||||
cidr_block = "10.0.0.0/16"
|
cidr_block = "10.0.0.0/16"
|
||||||
vpc_id = aws_vpc.vpc-vpc.id
|
vpc_id = aws_vpc.vpc-vpc.id
|
||||||
tags = {
|
tags = {
|
||||||
Name = "acdl-microservice"
|
Name = "nova-microservice"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,12 +24,12 @@ resource "aws_route_table" "vpc-routetable" {
|
|||||||
gateway_id = aws_internet_gateway.vpc-igw.id
|
gateway_id = aws_internet_gateway.vpc-igw.id
|
||||||
}
|
}
|
||||||
tags = {
|
tags = {
|
||||||
Name = "acdl-microservice-rt"
|
Name = "nova-microservice-rt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_ecs_cluster" "cluster" {
|
resource "aws_ecs_cluster" "cluster" {
|
||||||
name = "acdl-microservice"
|
name = "nova-microservice"
|
||||||
}
|
}
|
||||||
|
|
||||||
output "cluster_arn" {
|
output "cluster_arn" {
|
||||||
@@ -41,7 +41,7 @@ output "cluster_id" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_ecr_repository" "ecr" {
|
resource "aws_ecr_repository" "ecr" {
|
||||||
name = "acdl-microservice"
|
name = "nova-microservice"
|
||||||
}
|
}
|
||||||
|
|
||||||
output "repository_url" {
|
output "repository_url" {
|
||||||
@@ -53,8 +53,8 @@ output "repository_arn" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_iam_role" "roles" {
|
resource "aws_iam_role" "roles" {
|
||||||
name = "acdl-microservice-exec"
|
name = "nova-microservice-exec"
|
||||||
assume_role_policy = jsonencode({"Statement": [{"Action": "sts:AssumeRole", "Effect": "Allow", "Principal": {"Service": "ecs-tasks.amazonaws.com"}}], "Version": "2012-10-17"})
|
assume_role_policy = jsonencode({ "Statement" : [{ "Action" : "sts:AssumeRole", "Effect" : "Allow", "Principal" : { "Service" : "ecs-tasks.amazonaws.com" } }], "Version" : "2012-10-17" })
|
||||||
managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"]
|
managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,9 +67,9 @@ output "role_id" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_lb" "alb-loadbalancer" {
|
resource "aws_lb" "alb-loadbalancer" {
|
||||||
name = "acdl-microservice"
|
name = "nova-microservice"
|
||||||
subnets = [aws_subnet.vpc-subnet.id]
|
subnets = [aws_subnet.vpc-subnet.id]
|
||||||
security_groups = [aws_iam_role.roles.arn]
|
security_groups = [aws_iam_role.roles.arn]
|
||||||
load_balancer_type = "application"
|
load_balancer_type = "application"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,11 +78,11 @@ output "lb_arn" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_lb_target_group" "alb-targetgroup" {
|
resource "aws_lb_target_group" "alb-targetgroup" {
|
||||||
name = "acdl-microservice"
|
name = "nova-microservice"
|
||||||
port = 8080
|
port = 8080
|
||||||
target_type = "ip"
|
target_type = "ip"
|
||||||
vpc_id = aws_vpc.vpc-vpc.id
|
vpc_id = aws_vpc.vpc-vpc.id
|
||||||
protocol = "HTTP"
|
protocol = "HTTP"
|
||||||
}
|
}
|
||||||
|
|
||||||
output "target_group_arn" {
|
output "target_group_arn" {
|
||||||
@@ -92,7 +92,7 @@ output "target_group_arn" {
|
|||||||
resource "aws_lb_listener" "alb-listener" {
|
resource "aws_lb_listener" "alb-listener" {
|
||||||
port = 8080
|
port = 8080
|
||||||
default_action {
|
default_action {
|
||||||
type = "forward"
|
type = "forward"
|
||||||
target_group_arn = aws_lb_target_group.alb-targetgroup.arn
|
target_group_arn = aws_lb_target_group.alb-targetgroup.arn
|
||||||
}
|
}
|
||||||
load_balancer_arn = aws_lb.alb-loadbalancer.id
|
load_balancer_arn = aws_lb.alb-loadbalancer.id
|
||||||
@@ -103,10 +103,10 @@ output "listener_arn" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_ecs_task_definition" "service-taskdefinition" {
|
resource "aws_ecs_task_definition" "service-taskdefinition" {
|
||||||
cpu = 256
|
cpu = 256
|
||||||
memory = 512
|
memory = 512
|
||||||
container_definitions = jsonencode([{"essential": true, "image": "581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest", "name": "app", "portMappings": [{"containerPort": 8080}]}])
|
container_definitions = jsonencode([{ "essential" : true, "image" : "581513795199.dkr.ecr.us-east-1.amazonaws.com/nova-microservice:latest", "name" : "app", "portMappings" : [{ "containerPort" : 8080 }] }])
|
||||||
family = "app"
|
family = "app"
|
||||||
}
|
}
|
||||||
|
|
||||||
output "task_def_arn" {
|
output "task_def_arn" {
|
||||||
@@ -117,17 +117,17 @@ resource "aws_ecs_service" "service-service" {
|
|||||||
cluster = aws_ecs_cluster.cluster.arn
|
cluster = aws_ecs_cluster.cluster.arn
|
||||||
load_balancer {
|
load_balancer {
|
||||||
target_group_arn = aws_lb_target_group.alb-targetgroup.arn
|
target_group_arn = aws_lb_target_group.alb-targetgroup.arn
|
||||||
container_name = "app"
|
container_name = "app"
|
||||||
container_port = 8080
|
container_port = 8080
|
||||||
}
|
}
|
||||||
network_configuration {
|
network_configuration {
|
||||||
subnets = [aws_subnet.vpc-subnet.id]
|
subnets = [aws_subnet.vpc-subnet.id]
|
||||||
security_groups = [aws_iam_role.roles.arn]
|
security_groups = [aws_iam_role.roles.arn]
|
||||||
}
|
}
|
||||||
desired_count = 1
|
desired_count = 1
|
||||||
launch_type = "FARGATE"
|
launch_type = "FARGATE"
|
||||||
task_definition = aws_ecs_task_definition.service-taskdefinition.arn
|
task_definition = aws_ecs_task_definition.service-taskdefinition.arn
|
||||||
name = "acdl-microservice"
|
name = "nova-microservice"
|
||||||
}
|
}
|
||||||
|
|
||||||
output "service_arn" {
|
output "service_arn" {
|
||||||
@@ -137,11 +137,11 @@ output "service_arn" {
|
|||||||
resource "aws_internet_gateway" "vpc-igw" {
|
resource "aws_internet_gateway" "vpc-igw" {
|
||||||
vpc_id = aws_vpc.vpc-vpc.id
|
vpc_id = aws_vpc.vpc-vpc.id
|
||||||
tags = {
|
tags = {
|
||||||
Name = "acdl-microservice-igw"
|
Name = "nova-microservice-igw"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_route_table_association" "vpc-rta" {
|
resource "aws_route_table_association" "vpc-rta" {
|
||||||
subnet_id = aws_subnet.vpc-subnet.id
|
subnet_id = aws_subnet.vpc-subnet.id
|
||||||
route_table_id = aws_route_table.vpc-routetable.id
|
route_table_id = aws_route_table.vpc-routetable.id
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@ terraform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
backend "s3" {
|
backend "s3" {
|
||||||
bucket = "acdl-tfstate-581513795199-us-east-1"
|
bucket = "nova-tfstate-581513795199-us-east-1"
|
||||||
key = "spike/microservice/terraform.tfstate"
|
key = "spike/microservice/terraform.tfstate"
|
||||||
region = "us-east-1"
|
region = "us-east-1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# ACDL Platform Infrastructure (D-051)
|
# Nova Platform Infrastructure (D-051)
|
||||||
|
|
||||||
Terraform configuration for the **platform-side** infrastructure that
|
Terraform configuration for the **platform-side** infrastructure that
|
||||||
ingests consumer deployment contracts and (Phase 25) reports errors as
|
ingests consumer deployment contracts and (Phase 25) reports errors as
|
||||||
@@ -13,11 +13,11 @@ consumers — the contract ingestion pipeline and the secrets it needs.
|
|||||||
|
|
||||||
| Resource | Name | Purpose |
|
| Resource | Name | Purpose |
|
||||||
|----------|------|---------|
|
|----------|------|---------|
|
||||||
| `aws_dynamodb_table` | `acdl-contracts` | Stores submitted consumer contracts. PK `consumerRepo`, SK `contractId#submittedAt`. SSE via CMK, PITR enabled. |
|
| `aws_dynamodb_table` | `nova-contracts` | Stores submitted consumer contracts. PK `consumerRepo`, SK `contractId#submittedAt`. SSE via CMK, PITR enabled. |
|
||||||
| `aws_kms_key` + `aws_kms_alias` | `alias/acdl-platform` | Customer-managed key — encrypts DynamoDB SSE, Secrets Manager, and SSM. Key rotation enabled. |
|
| `aws_kms_key` + `aws_kms_alias` | `alias/nova-platform` | Customer-managed key — encrypts DynamoDB SSE, Secrets Manager, and SSM. Key rotation enabled. |
|
||||||
| `aws_secretsmanager_secret` | `acdl/github-token` | GitHub PAT used by the Lambda to create issues on the platform repo (D-055, wired in Phase 25). |
|
| `aws_secretsmanager_secret` | `nova/github-token` | GitHub PAT used by the Lambda to create issues on the platform repo (D-055, wired in Phase 25). |
|
||||||
| `aws_iam_role` + `aws_iam_role_policy` | `acdl-contract-ingestor-role` | Execution role for the Lambda — DynamoDB write, Secrets Manager read, KMS decrypt, CloudWatch logs. |
|
| `aws_iam_role` + `aws_iam_role_policy` | `nova-contract-ingestor-role` | Execution role for the Lambda — DynamoDB write, Secrets Manager read, KMS decrypt, CloudWatch logs. |
|
||||||
| `aws_lambda_function` | `acdl-contract-ingestor` | Python 3.12 Lambda. Handler `contract_ingestor.lambda_handler`. Source: `core/lambda/contract_ingestor.py`, packaged as `contract_ingestor.zip`. |
|
| `aws_lambda_function` | `nova-contract-ingestor` | Python 3.12 Lambda. Handler `contract_ingestor.lambda_handler`. Source: `core/lambda/contract_ingestor.py`, packaged as `contract_ingestor.zip`. |
|
||||||
| `aws_lambda_function_url` | — | Function URL with `AWS_IAM` authorization. Consumers invoke it via SigV4-signed requests. |
|
| `aws_lambda_function_url` | — | Function URL with `AWS_IAM` authorization. Consumers invoke it via SigV4-signed requests. |
|
||||||
|
|
||||||
## State
|
## State
|
||||||
@@ -25,7 +25,7 @@ consumers — the contract ingestion pipeline and the secrets it needs.
|
|||||||
| Key | Value |
|
| Key | Value |
|
||||||
|-----|-------|
|
|-----|-------|
|
||||||
| Backend | S3 |
|
| Backend | S3 |
|
||||||
| Bucket | `acdl-tfstate-581513795199-us-east-1` |
|
| Bucket | `nova-tfstate-581513795199-us-east-1` |
|
||||||
| State key | `platform/terraform.tfstate` |
|
| State key | `platform/terraform.tfstate` |
|
||||||
| Region | `us-east-1` |
|
| Region | `us-east-1` |
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ flow:
|
|||||||
request with SigV4 using its deploy-role credentials. The IAM auth on
|
request with SigV4 using its deploy-role credentials. The IAM auth on
|
||||||
the Function URL validates the signature and the ABAC condition.
|
the Function URL validates the signature and the ABAC condition.
|
||||||
3. **Lambda.** The Lambda parses the JSON body, validates the fields,
|
3. **Lambda.** The Lambda parses the JSON body, validates the fields,
|
||||||
and writes the contract to `acdl-contracts`.
|
and writes the contract to `nova-contracts`.
|
||||||
|
|
||||||
This is a **one-way** channel (D-051): the consumer pushes contracts
|
This is a **one-way** channel (D-051): the consumer pushes contracts
|
||||||
*to* the platform; the platform never reaches back into the consumer
|
*to* the platform; the platform never reaches back into the consumer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{
|
{
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": "lambda:InvokeFunctionUrl",
|
"Action": "lambda:InvokeFunctionUrl",
|
||||||
"Resource": "arn:aws:lambda:${region}:${account_id}:function:acdl-contract-ingestor",
|
"Resource": "arn:aws:lambda:${region}:${account_id}:function:nova-contract-ingestor",
|
||||||
"Condition": {
|
"Condition": {
|
||||||
"StringEquals": {
|
"StringEquals": {
|
||||||
"aws:PrincipalTag/nova:owner": "${consumerRepo}"
|
"aws:PrincipalTag/nova:owner": "${consumerRepo}"
|
||||||
|
|||||||
+54
-54
@@ -1,11 +1,11 @@
|
|||||||
# ACDL platform infrastructure — contract ingestion Lambda + DynamoDB (D-051)
|
# Nova platform infrastructure — contract ingestion Lambda + DynamoDB (D-051)
|
||||||
#
|
#
|
||||||
# Deploys:
|
# Deploys:
|
||||||
# - DynamoDB table acdl-contracts (PK consumerRepo, SK contractId#submittedAt, SSE via CMK, PITR)
|
# - DynamoDB table nova-contracts (PK consumerRepo, SK contractId#submittedAt, SSE via CMK, PITR)
|
||||||
# - KMS customer-managed key for DynamoDB + SSM (shared CMK)
|
# - KMS customer-managed key for DynamoDB + SSM (shared CMK)
|
||||||
# - Lambda function acdl-contract-ingestor (Python 3.12, handler contract_ingestor.lambda_handler)
|
# - Lambda function nova-contract-ingestor (Python 3.12, handler contract_ingestor.lambda_handler)
|
||||||
# - Lambda Function URL (IAM auth — consumers invoke via SigV4)
|
# - Lambda Function URL (IAM auth — consumers invoke via SigV4)
|
||||||
# - Secrets Manager secret acdl/github-token (stores the Lambda's GitHub PAT for issue creation)
|
# - Secrets Manager secret nova/github-token (stores the Lambda's GitHub PAT for issue creation)
|
||||||
# - IAM execution role for the Lambda (DynamoDB write + Secrets Manager read + KMS decrypt)
|
# - IAM execution role for the Lambda (DynamoDB write + Secrets Manager read + KMS decrypt)
|
||||||
#
|
#
|
||||||
# State: terraform/platform/terraform.tfstate (separate from spike/ and microservice/)
|
# State: terraform/platform/terraform.tfstate (separate from spike/ and microservice/)
|
||||||
@@ -19,7 +19,7 @@ terraform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
backend "s3" {
|
backend "s3" {
|
||||||
bucket = "acdl-tfstate-581513795199-us-east-1"
|
bucket = "nova-tfstate-581513795199-us-east-1"
|
||||||
key = "platform/terraform.tfstate"
|
key = "platform/terraform.tfstate"
|
||||||
region = "us-east-1"
|
region = "us-east-1"
|
||||||
}
|
}
|
||||||
@@ -37,20 +37,20 @@ variable "vpc_cidr" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# KMS customer-managed key for DynamoDB SSE + SSM Parameter Store encryption
|
# KMS customer-managed key for DynamoDB SSE + SSM Parameter Store encryption
|
||||||
resource "aws_kms_key" "acdl_platform" {
|
resource "aws_kms_key" "nova_platform" {
|
||||||
description = "ACDL platform KMS key (DynamoDB SSE + SSM + Secrets Manager)"
|
description = "Nova platform KMS key (DynamoDB SSE + SSM + Secrets Manager)"
|
||||||
enable_key_rotation = true
|
enable_key_rotation = true
|
||||||
deletion_window_in_days = 30
|
deletion_window_in_days = 30
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_kms_alias" "acdl_platform" {
|
resource "aws_kms_alias" "nova_platform" {
|
||||||
name = "alias/acdl-platform"
|
name = "alias/nova-platform"
|
||||||
target_key_id = aws_kms_key.acdl_platform.key_id
|
target_key_id = aws_kms_key.nova_platform.key_id
|
||||||
}
|
}
|
||||||
|
|
||||||
# DynamoDB table for contract ingestion
|
# DynamoDB table for contract ingestion
|
||||||
resource "aws_dynamodb_table" "acdl_contracts" {
|
resource "aws_dynamodb_table" "nova_contracts" {
|
||||||
name = "acdl-contracts"
|
name = "nova-contracts"
|
||||||
billing_mode = "PAY_PER_REQUEST"
|
billing_mode = "PAY_PER_REQUEST"
|
||||||
hash_key = "consumerRepo"
|
hash_key = "consumerRepo"
|
||||||
range_key = "contractId#submittedAt"
|
range_key = "contractId#submittedAt"
|
||||||
@@ -71,7 +71,7 @@ resource "aws_dynamodb_table" "acdl_contracts" {
|
|||||||
|
|
||||||
server_side_encryption {
|
server_side_encryption {
|
||||||
enabled = true
|
enabled = true
|
||||||
kms_key_arn = aws_kms_key.acdl_platform.arn
|
kms_key_arn = aws_kms_key.nova_platform.arn
|
||||||
}
|
}
|
||||||
|
|
||||||
tags = {
|
tags = {
|
||||||
@@ -84,9 +84,9 @@ resource "aws_dynamodb_table" "acdl_contracts" {
|
|||||||
|
|
||||||
# Secrets Manager secret for the Lambda's GitHub token (issue creation)
|
# Secrets Manager secret for the Lambda's GitHub token (issue creation)
|
||||||
resource "aws_secretsmanager_secret" "github_token" {
|
resource "aws_secretsmanager_secret" "github_token" {
|
||||||
name = "acdl/github-token"
|
name = "nova/github-token"
|
||||||
description = "GitHub PAT for the platform Lambda to create issues on the platform repo (D-055)."
|
description = "GitHub PAT for the platform Lambda to create issues on the platform repo (D-055)."
|
||||||
kms_key_id = aws_kms_key.acdl_platform.arn
|
kms_key_id = aws_kms_key.nova_platform.arn
|
||||||
|
|
||||||
tags = {
|
tags = {
|
||||||
"nova:owner" = "acdl"
|
"nova:owner" = "acdl"
|
||||||
@@ -98,7 +98,7 @@ resource "aws_secretsmanager_secret" "github_token" {
|
|||||||
|
|
||||||
# IAM execution role for the Lambda
|
# IAM execution role for the Lambda
|
||||||
resource "aws_iam_role" "lambda_exec" {
|
resource "aws_iam_role" "lambda_exec" {
|
||||||
name = "acdl-contract-ingestor-role"
|
name = "nova-contract-ingestor-role"
|
||||||
assume_role_policy = jsonencode({
|
assume_role_policy = jsonencode({
|
||||||
Version = "2012-10-17"
|
Version = "2012-10-17"
|
||||||
Statement = [{
|
Statement = [{
|
||||||
@@ -110,7 +110,7 @@ resource "aws_iam_role" "lambda_exec" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_iam_role_policy" "lambda_permissions" {
|
resource "aws_iam_role_policy" "lambda_permissions" {
|
||||||
name = "acdl-contract-ingestor-policy"
|
name = "nova-contract-ingestor-policy"
|
||||||
role = aws_iam_role.lambda_exec.id
|
role = aws_iam_role.lambda_exec.id
|
||||||
policy = jsonencode({
|
policy = jsonencode({
|
||||||
Version = "2012-10-17"
|
Version = "2012-10-17"
|
||||||
@@ -118,12 +118,12 @@ resource "aws_iam_role_policy" "lambda_permissions" {
|
|||||||
{
|
{
|
||||||
Effect = "Allow"
|
Effect = "Allow"
|
||||||
Action = ["dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:Query", "dynamodb:UpdateItem"]
|
Action = ["dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:Query", "dynamodb:UpdateItem"]
|
||||||
Resource = aws_dynamodb_table.acdl_contracts.arn
|
Resource = aws_dynamodb_table.nova_contracts.arn
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Effect = "Allow"
|
Effect = "Allow"
|
||||||
Action = ["dynamodb:GetItem", "dynamodb:Query"]
|
Action = ["dynamodb:GetItem", "dynamodb:Query"]
|
||||||
Resource = aws_dynamodb_table.acdl_change_requests.arn
|
Resource = aws_dynamodb_table.nova_change_requests.arn
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Effect = "Allow"
|
Effect = "Allow"
|
||||||
@@ -133,7 +133,7 @@ resource "aws_iam_role_policy" "lambda_permissions" {
|
|||||||
{
|
{
|
||||||
Effect = "Allow"
|
Effect = "Allow"
|
||||||
Action = ["kms:Decrypt"]
|
Action = ["kms:Decrypt"]
|
||||||
Resource = aws_kms_key.acdl_platform.arn
|
Resource = aws_kms_key.nova_platform.arn
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Effect = "Allow"
|
Effect = "Allow"
|
||||||
@@ -152,7 +152,7 @@ locals {
|
|||||||
|
|
||||||
resource "aws_lambda_function" "contract_ingestor" {
|
resource "aws_lambda_function" "contract_ingestor" {
|
||||||
count = local.lambda_zip_exists ? 1 : 0
|
count = local.lambda_zip_exists ? 1 : 0
|
||||||
function_name = "acdl-contract-ingestor"
|
function_name = "nova-contract-ingestor"
|
||||||
handler = "contract_ingestor.lambda_handler"
|
handler = "contract_ingestor.lambda_handler"
|
||||||
runtime = "python3.12"
|
runtime = "python3.12"
|
||||||
role = aws_iam_role.lambda_exec.arn
|
role = aws_iam_role.lambda_exec.arn
|
||||||
@@ -161,9 +161,9 @@ resource "aws_lambda_function" "contract_ingestor" {
|
|||||||
|
|
||||||
environment {
|
environment {
|
||||||
variables = {
|
variables = {
|
||||||
CONTRACTS_TABLE = aws_dynamodb_table.acdl_contracts.name
|
CONTRACTS_TABLE = aws_dynamodb_table.nova_contracts.name
|
||||||
GITHUB_TOKEN_SECRET_ID = aws_secretsmanager_secret.github_token.name
|
GITHUB_TOKEN_SECRET_ID = aws_secretsmanager_secret.github_token.name
|
||||||
PLATFORM_REPO = "acdl/acdl"
|
PLATFORM_REPO = "nova/acdl"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,8 +204,8 @@ output "consumer_invoke_policy_rendered" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# REQ-93: DynamoDB table for change requests (CMDB for decommission validation)
|
# REQ-93: DynamoDB table for change requests (CMDB for decommission validation)
|
||||||
resource "aws_dynamodb_table" "acdl_change_requests" {
|
resource "aws_dynamodb_table" "nova_change_requests" {
|
||||||
name = "acdl-change-requests"
|
name = "nova-change-requests"
|
||||||
billing_mode = "PAY_PER_REQUEST"
|
billing_mode = "PAY_PER_REQUEST"
|
||||||
hash_key = "changeRequestId"
|
hash_key = "changeRequestId"
|
||||||
range_key = "submittedAt"
|
range_key = "submittedAt"
|
||||||
@@ -226,7 +226,7 @@ resource "aws_dynamodb_table" "acdl_change_requests" {
|
|||||||
|
|
||||||
server_side_encryption {
|
server_side_encryption {
|
||||||
enabled = true
|
enabled = true
|
||||||
kms_key_arn = aws_kms_key.acdl_platform.arn
|
kms_key_arn = aws_kms_key.nova_platform.arn
|
||||||
}
|
}
|
||||||
|
|
||||||
tags = {
|
tags = {
|
||||||
@@ -237,10 +237,10 @@ resource "aws_dynamodb_table" "acdl_change_requests" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
# REQ-107: SNS topic for separation-of-duties halt artifacts.
|
# REQ-107: SNS topic for separation-of-duties halt artifacts.
|
||||||
# route_halt_artifact publishes here when ACDL_SOD_HALT_TOPIC_ARN is set.
|
# route_halt_artifact publishes here when NOVA_SOD_HALT_TOPIC_ARN is set.
|
||||||
resource "aws_sns_topic" "acdl_sod_halt" {
|
resource "aws_sns_topic" "nova_sod_halt" {
|
||||||
name = "acdl-sod-halt"
|
name = "nova-sod-halt"
|
||||||
kms_master_key_id = aws_kms_key.acdl_platform.id
|
kms_master_key_id = aws_kms_key.nova_platform.id
|
||||||
tags = {
|
tags = {
|
||||||
"nova:owner" = "acdl"
|
"nova:owner" = "acdl"
|
||||||
"nova:contract" = "platform"
|
"nova:contract" = "platform"
|
||||||
@@ -249,8 +249,8 @@ resource "aws_sns_topic" "acdl_sod_halt" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output "acdl_sod_halt_topic_arn" {
|
output "nova_sod_halt_topic_arn" {
|
||||||
value = aws_sns_topic.acdl_sod_halt.arn
|
value = aws_sns_topic.nova_sod_halt.arn
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -258,10 +258,10 @@ output "acdl_sod_halt_topic_arn" {
|
|||||||
# via terraform_remote_state (data source). No per-contract VPC ever again.
|
# via terraform_remote_state (data source). No per-contract VPC ever again.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
resource "aws_vpc" "acdl_shared" {
|
resource "aws_vpc" "nova_shared" {
|
||||||
cidr_block = var.vpc_cidr
|
cidr_block = var.vpc_cidr
|
||||||
tags = {
|
tags = {
|
||||||
Name = "acdl-shared"
|
Name = "nova-shared"
|
||||||
"nova:owner" = "acdl"
|
"nova:owner" = "acdl"
|
||||||
"nova:contract" = "platform"
|
"nova:contract" = "platform"
|
||||||
"nova:environment" = "shared"
|
"nova:environment" = "shared"
|
||||||
@@ -269,13 +269,13 @@ resource "aws_vpc" "acdl_shared" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_subnet" "acdl_shared" {
|
resource "aws_subnet" "nova_shared" {
|
||||||
count = length(data.aws_availability_zones.available.names)
|
count = length(data.aws_availability_zones.available.names)
|
||||||
vpc_id = aws_vpc.acdl_shared.id
|
vpc_id = aws_vpc.nova_shared.id
|
||||||
cidr_block = cidrsubnet(aws_vpc.acdl_shared.cidr_block, 8, count.index + 1)
|
cidr_block = cidrsubnet(aws_vpc.nova_shared.cidr_block, 8, count.index + 1)
|
||||||
availability_zone = data.aws_availability_zones.available.names[count.index]
|
availability_zone = data.aws_availability_zones.available.names[count.index]
|
||||||
tags = {
|
tags = {
|
||||||
Name = "acdl-shared-subnet-${count.index}"
|
Name = "nova-shared-subnet-${count.index}"
|
||||||
"nova:owner" = "acdl"
|
"nova:owner" = "acdl"
|
||||||
"nova:contract" = "platform"
|
"nova:contract" = "platform"
|
||||||
"nova:environment" = "shared"
|
"nova:environment" = "shared"
|
||||||
@@ -287,10 +287,10 @@ data "aws_availability_zones" "available" {
|
|||||||
state = "available"
|
state = "available"
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_internet_gateway" "acdl_shared" {
|
resource "aws_internet_gateway" "nova_shared" {
|
||||||
vpc_id = aws_vpc.acdl_shared.id
|
vpc_id = aws_vpc.nova_shared.id
|
||||||
tags = {
|
tags = {
|
||||||
Name = "acdl-shared-igw"
|
Name = "nova-shared-igw"
|
||||||
"nova:owner" = "acdl"
|
"nova:owner" = "acdl"
|
||||||
"nova:contract" = "platform"
|
"nova:contract" = "platform"
|
||||||
"nova:environment" = "shared"
|
"nova:environment" = "shared"
|
||||||
@@ -298,14 +298,14 @@ resource "aws_internet_gateway" "acdl_shared" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_route_table" "acdl_shared" {
|
resource "aws_route_table" "nova_shared" {
|
||||||
vpc_id = aws_vpc.acdl_shared.id
|
vpc_id = aws_vpc.nova_shared.id
|
||||||
route {
|
route {
|
||||||
cidr_block = "0.0.0.0/0"
|
cidr_block = "0.0.0.0/0"
|
||||||
gateway_id = aws_internet_gateway.acdl_shared.id
|
gateway_id = aws_internet_gateway.nova_shared.id
|
||||||
}
|
}
|
||||||
tags = {
|
tags = {
|
||||||
Name = "acdl-shared-rt"
|
Name = "nova-shared-rt"
|
||||||
"nova:owner" = "acdl"
|
"nova:owner" = "acdl"
|
||||||
"nova:contract" = "platform"
|
"nova:contract" = "platform"
|
||||||
"nova:environment" = "shared"
|
"nova:environment" = "shared"
|
||||||
@@ -313,16 +313,16 @@ resource "aws_route_table" "acdl_shared" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_route_table_association" "acdl_shared" {
|
resource "aws_route_table_association" "nova_shared" {
|
||||||
count = 2
|
count = 2
|
||||||
subnet_id = aws_subnet.acdl_shared[count.index].id
|
subnet_id = aws_subnet.nova_shared[count.index].id
|
||||||
route_table_id = aws_route_table.acdl_shared.id
|
route_table_id = aws_route_table.nova_shared.id
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_security_group" "ecs" {
|
resource "aws_security_group" "ecs" {
|
||||||
name = "acdl-ecs-sg"
|
name = "nova-ecs-sg"
|
||||||
description = "Security group for ECS Fargate services (platform VPC)"
|
description = "Security group for ECS Fargate services (platform VPC)"
|
||||||
vpc_id = aws_vpc.acdl_shared.id
|
vpc_id = aws_vpc.nova_shared.id
|
||||||
|
|
||||||
# Ingress on port 80 is open to 0.0.0.0/0 — this is acceptable because
|
# Ingress on port 80 is open to 0.0.0.0/0 — this is acceptable because
|
||||||
# the ECS service is fronted by a public-facing ALB (the ALB terminates
|
# the ECS service is fronted by a public-facing ALB (the ALB terminates
|
||||||
@@ -343,7 +343,7 @@ resource "aws_security_group" "ecs" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tags = {
|
tags = {
|
||||||
Name = "acdl-ecs-sg"
|
Name = "nova-ecs-sg"
|
||||||
"nova:owner" = "acdl"
|
"nova:owner" = "acdl"
|
||||||
"nova:contract" = "platform"
|
"nova:contract" = "platform"
|
||||||
"nova:environment" = "shared"
|
"nova:environment" = "shared"
|
||||||
@@ -352,16 +352,16 @@ resource "aws_security_group" "ecs" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
output "vpc_id" {
|
output "vpc_id" {
|
||||||
value = aws_vpc.acdl_shared.id
|
value = aws_vpc.nova_shared.id
|
||||||
description = "The shared platform VPC ID. Consumer stacks reference this via terraform_remote_state."
|
description = "The shared platform VPC ID. Consumer stacks reference this via terraform_remote_state."
|
||||||
}
|
}
|
||||||
|
|
||||||
output "subnet_ids" {
|
output "subnet_ids" {
|
||||||
value = join(",", aws_subnet.acdl_shared[*].id)
|
value = join(",", aws_subnet.nova_shared[*].id)
|
||||||
description = "Comma-separated subnet IDs in the shared platform VPC."
|
description = "Comma-separated subnet IDs in the shared platform VPC."
|
||||||
}
|
}
|
||||||
|
|
||||||
output "ecs_security_group_id" {
|
output "ecs_security_group_id" {
|
||||||
value = aws_security_group.ecs.id
|
value = aws_security_group.ecs.id
|
||||||
description = "Security group ID for ECS Fargate services in the platform VPC."
|
description = "Security group ID for ECS Fargate services in the platform VPC."
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ def moto_contracts_table(monkeypatch):
|
|||||||
with mock_aws():
|
with mock_aws():
|
||||||
dyn = boto3.client("dynamodb", region_name="us-east-1")
|
dyn = boto3.client("dynamodb", region_name="us-east-1")
|
||||||
dyn.create_table(
|
dyn.create_table(
|
||||||
TableName="acdl-contracts",
|
TableName="nova-contracts",
|
||||||
KeySchema=[
|
KeySchema=[
|
||||||
{"AttributeName": "consumerRepo", "KeyType": "HASH"},
|
{"AttributeName": "consumerRepo", "KeyType": "HASH"},
|
||||||
{"AttributeName": "contractId#submittedAt", "KeyType": "RANGE"},
|
{"AttributeName": "contractId#submittedAt", "KeyType": "RANGE"},
|
||||||
@@ -84,7 +84,7 @@ def moto_contracts_table(monkeypatch):
|
|||||||
saved_secrets = ingestor._secrets_client
|
saved_secrets = ingestor._secrets_client
|
||||||
ingestor._dynamodb = None
|
ingestor._dynamodb = None
|
||||||
ingestor._secrets_client = None
|
ingestor._secrets_client = None
|
||||||
monkeypatch.setattr(ingestor, "TABLE_NAME", "acdl-contracts")
|
monkeypatch.setattr(ingestor, "TABLE_NAME", "nova-contracts")
|
||||||
|
|
||||||
yield dyn
|
yield dyn
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ class TestSubmitContract:
|
|||||||
# Verify what landed in DynamoDB.
|
# Verify what landed in DynamoDB.
|
||||||
sk = f"contract-001#{result['submittedAt']}"
|
sk = f"contract-001#{result['submittedAt']}"
|
||||||
resp = moto_contracts_table.get_item(
|
resp = moto_contracts_table.get_item(
|
||||||
TableName="acdl-contracts",
|
TableName="nova-contracts",
|
||||||
Key={
|
Key={
|
||||||
"consumerRepo": {"S": "acdl/consumer-a"},
|
"consumerRepo": {"S": "acdl/consumer-a"},
|
||||||
"contractId#submittedAt": {"S": sk},
|
"contractId#submittedAt": {"S": sk},
|
||||||
@@ -369,7 +369,7 @@ class TestCallerIdentityValidation:
|
|||||||
def test_invalid_consumer_repo_format_rejected(self, moto_contracts_table, sample_payload):
|
def test_invalid_consumer_repo_format_rejected(self, moto_contracts_table, sample_payload):
|
||||||
# A consumerRepo without "/" is invalid (not org/repo format).
|
# A consumerRepo without "/" is invalid (not org/repo format).
|
||||||
sample_payload["consumerRepo"] = "not-a-repo-format"
|
sample_payload["consumerRepo"] = "not-a-repo-format"
|
||||||
event = {"body": json.dumps(sample_payload), "requestContext": {"identity": {"userArn": "arn:aws:sts::000:assumed-role/acdl-deploy/session"}}}
|
event = {"body": json.dumps(sample_payload), "requestContext": {"identity": {"userArn": "arn:aws:sts::000:assumed-role/nova-deploy/session"}}}
|
||||||
resp = ingestor.lambda_handler(event, None)
|
resp = ingestor.lambda_handler(event, None)
|
||||||
assert resp["statusCode"] == 400
|
assert resp["statusCode"] == 400
|
||||||
assert "invalid consumerRepo" in json.loads(resp["body"])["error"]
|
assert "invalid consumerRepo" in json.loads(resp["body"])["error"]
|
||||||
@@ -378,7 +378,7 @@ class TestCallerIdentityValidation:
|
|||||||
# A valid org/repo consumerRepo with an identity present — passes.
|
# A valid org/repo consumerRepo with an identity present — passes.
|
||||||
event = {
|
event = {
|
||||||
"body": json.dumps(sample_payload),
|
"body": json.dumps(sample_payload),
|
||||||
"requestContext": {"identity": {"userArn": "arn:aws:sts::000:assumed-role/acdl-deploy/acdl-consumer-a"}},
|
"requestContext": {"identity": {"userArn": "arn:aws:sts::000:assumed-role/nova-deploy/nova-consumer-a"}},
|
||||||
}
|
}
|
||||||
resp = ingestor.lambda_handler(event, None)
|
resp = ingestor.lambda_handler(event, None)
|
||||||
assert resp["statusCode"] == 200
|
assert resp["statusCode"] == 200
|
||||||
@@ -431,12 +431,12 @@ class TestValidateChangeRequest:
|
|||||||
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
|
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
|
||||||
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
|
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
|
||||||
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
|
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
|
||||||
monkeypatch.setenv("CHANGE_REQUESTS_TABLE", "acdl-change-requests")
|
monkeypatch.setenv("CHANGE_REQUESTS_TABLE", "nova-change-requests")
|
||||||
|
|
||||||
with mock_aws():
|
with mock_aws():
|
||||||
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
|
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
|
||||||
table = dynamodb.create_table(
|
table = dynamodb.create_table(
|
||||||
TableName="acdl-change-requests",
|
TableName="nova-change-requests",
|
||||||
KeySchema=[
|
KeySchema=[
|
||||||
{"AttributeName": "changeRequestId", "KeyType": "HASH"},
|
{"AttributeName": "changeRequestId", "KeyType": "HASH"},
|
||||||
{"AttributeName": "submittedAt", "KeyType": "RANGE"},
|
{"AttributeName": "submittedAt", "KeyType": "RANGE"},
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class TestResolveMicroservice:
|
|||||||
"microservice": {
|
"microservice": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"image": "581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest",
|
"image": "581513795199.dkr.ecr.us-east-1.amazonaws.com/nova-microservice:latest",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -134,13 +134,13 @@ class TestIAMPolicyBaseline:
|
|||||||
def test_dynamodb_contracts_table_in_resource(self, policy):
|
def test_dynamodb_contracts_table_in_resource(self, policy):
|
||||||
contracts_stmts = [
|
contracts_stmts = [
|
||||||
s for s in policy["Statement"]
|
s for s in policy["Statement"]
|
||||||
if any("acdl-contracts" in r for r in (
|
if any("nova-contracts" in r for r in (
|
||||||
s.get("Resource") if isinstance(s.get("Resource"), list) else [s.get("Resource", "")]
|
s.get("Resource") if isinstance(s.get("Resource"), list) else [s.get("Resource", "")]
|
||||||
))
|
))
|
||||||
]
|
]
|
||||||
assert contracts_stmts, "no statement references the acdl-contracts table"
|
assert contracts_stmts, "no statement references the nova-contracts table"
|
||||||
|
|
||||||
def test_lambda_scoped_to_acdl_functions(self, policy):
|
def test_lambda_scoped_to_nova_functions(self, policy):
|
||||||
lambda_stmts = [s for s in policy["Statement"] if any(
|
lambda_stmts = [s for s in policy["Statement"] if any(
|
||||||
a.startswith("lambda:") for a in (
|
a.startswith("lambda:") for a in (
|
||||||
s.get("Action") if isinstance(s.get("Action"), list) else [s.get("Action", "")]
|
s.get("Action") if isinstance(s.get("Action"), list) else [s.get("Action", "")]
|
||||||
@@ -151,8 +151,8 @@ class TestIAMPolicyBaseline:
|
|||||||
res = s.get("Resource", "")
|
res = s.get("Resource", "")
|
||||||
if isinstance(res, list):
|
if isinstance(res, list):
|
||||||
res = " ".join(res)
|
res = " ".join(res)
|
||||||
assert "function:acdl-*" in res or res == "*", \
|
assert "function:nova-*" in res or res == "*", \
|
||||||
"lambda actions not scoped to acdl-* functions"
|
"lambda actions not scoped to nova-* functions"
|
||||||
|
|
||||||
def test_cost_explorer_is_read_only(self, policy):
|
def test_cost_explorer_is_read_only(self, policy):
|
||||||
ce_actions = set()
|
ce_actions = set()
|
||||||
@@ -178,8 +178,8 @@ class TestIAMPolicyBaseline:
|
|||||||
res = " ".join(res)
|
res = " ".join(res)
|
||||||
assert res != "*", "iam:PassRole must not be granted to Resource: *"
|
assert res != "*", "iam:PassRole must not be granted to Resource: *"
|
||||||
|
|
||||||
def test_iam_role_creation_scoped_to_acdl_prefix(self, policy):
|
def test_iam_role_creation_scoped_to_nova_prefix(self, policy):
|
||||||
"""G-104: iam:CreateRole must be scoped to role/acdl-* (not Resource: *)."""
|
"""G-104: iam:CreateRole must be scoped to role/nova-* (not Resource: *)."""
|
||||||
for s in policy["Statement"]:
|
for s in policy["Statement"]:
|
||||||
acts = s.get("Action", [])
|
acts = s.get("Action", [])
|
||||||
if isinstance(acts, str):
|
if isinstance(acts, str):
|
||||||
@@ -188,10 +188,10 @@ class TestIAMPolicyBaseline:
|
|||||||
res = s.get("Resource", "")
|
res = s.get("Resource", "")
|
||||||
if isinstance(res, list):
|
if isinstance(res, list):
|
||||||
res = " ".join(res)
|
res = " ".join(res)
|
||||||
assert "acdl-*" in res, f"iam:CreateRole must be scoped to acdl-* (got: {res})"
|
assert "nova-*" in res, f"iam:CreateRole must be scoped to nova-* (got: {res})"
|
||||||
|
|
||||||
def test_kms_scoped_to_acdl_alias(self, policy):
|
def test_kms_scoped_to_nova_alias(self, policy):
|
||||||
"""G-104: kms:CreateKey etc. must be scoped to alias/acdl-* (not Resource: *)."""
|
"""G-104: kms:CreateKey etc. must be scoped to alias/nova-* (not Resource: *)."""
|
||||||
for s in policy["Statement"]:
|
for s in policy["Statement"]:
|
||||||
acts = s.get("Action", [])
|
acts = s.get("Action", [])
|
||||||
if isinstance(acts, str):
|
if isinstance(acts, str):
|
||||||
@@ -200,7 +200,7 @@ class TestIAMPolicyBaseline:
|
|||||||
res = s.get("Resource", "")
|
res = s.get("Resource", "")
|
||||||
if isinstance(res, list):
|
if isinstance(res, list):
|
||||||
res = " ".join(res)
|
res = " ".join(res)
|
||||||
assert "acdl-*" in res, f"kms actions must be scoped to acdl-* (got: {res})"
|
assert "nova-*" in res, f"kms actions must be scoped to nova-* (got: {res})"
|
||||||
|
|
||||||
def test_cloudfront_waf_remain_global(self, policy):
|
def test_cloudfront_waf_remain_global(self, policy):
|
||||||
"""G-104: CloudFront + WAFv2 (CloudFront scope) ARNs are global;
|
"""G-104: CloudFront + WAFv2 (CloudFront scope) ARNs are global;
|
||||||
@@ -215,4 +215,4 @@ class TestIAMPolicyBaseline:
|
|||||||
if isinstance(res, list):
|
if isinstance(res, list):
|
||||||
res = res[0] if res else ""
|
res = res[0] if res else ""
|
||||||
# CloudFront/WAFv2 are allowed to be * (global ARNs)
|
# CloudFront/WAFv2 are allowed to be * (global ARNs)
|
||||||
assert res == "*" or "acdl" in res
|
assert res == "*" or "nova" in res
|
||||||
@@ -121,7 +121,7 @@ def test_local_s3_backend_rewrites_s3_to_local(tmp_path):
|
|||||||
tf = tmp_path / "terraform.tf"
|
tf = tmp_path / "terraform.tf"
|
||||||
tf.write_text(
|
tf.write_text(
|
||||||
'terraform {\n required_version = ">= 1.9"\n backend "s3" {\n'
|
'terraform {\n required_version = ">= 1.9"\n backend "s3" {\n'
|
||||||
' bucket = "acdl-tfstate-x"\n key = "spike/s.tfstate"\n'
|
' bucket = "nova-tfstate-x"\n key = "spike/s.tfstate"\n'
|
||||||
' region = "us-east-1"\n }\n}\n'
|
' region = "us-east-1"\n }\n}\n'
|
||||||
)
|
)
|
||||||
backend.rewrite_terraform_tf(tf, "test-stack")
|
backend.rewrite_terraform_tf(tf, "test-stack")
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
"""Unit tests for scripts/migrate_dynamodb_data.py (REQ-163, P4).
|
||||||
|
|
||||||
|
Tests the pure item-mapping logic + table-pair resolution. The AWS I/O
|
||||||
|
(scan_all/copy_items) is thin boto3 glue, not unit-tested here (covered
|
||||||
|
by the dry-run path + the runbook's live verification).
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts"))
|
||||||
|
|
||||||
|
import migrate_dynamodb_data as mig
|
||||||
|
|
||||||
|
|
||||||
|
class TestMapItem:
|
||||||
|
def test_map_item_preserves_typed_attributes(self):
|
||||||
|
item = {
|
||||||
|
"consumerRepo": {"S": "acdl/consumer-a"},
|
||||||
|
"contractId#submittedAt": {"S": "c-1#2026-01-01T00:00:00Z"},
|
||||||
|
"contract": {"S": "name: foo\n"},
|
||||||
|
"count": {"N": "42"},
|
||||||
|
}
|
||||||
|
result = mig.map_item(item)
|
||||||
|
assert result == item
|
||||||
|
|
||||||
|
def test_map_item_returns_independent_copy(self):
|
||||||
|
"""The mapped item must not alias the scanned item (callers may mutate)."""
|
||||||
|
item = {"k": {"S": "v"}}
|
||||||
|
result = mig.map_item(item)
|
||||||
|
result["k"]["S"] = "mutated"
|
||||||
|
assert item["k"]["S"] == "v", "map_item returned an alias, not a copy"
|
||||||
|
|
||||||
|
def test_map_item_empty(self):
|
||||||
|
assert mig.map_item({}) == {}
|
||||||
|
|
||||||
|
def test_map_item_preserves_binary_and_nested(self):
|
||||||
|
item = {
|
||||||
|
"pk": {"B": b"\x01\x02"},
|
||||||
|
"nested": {"M": {"a": {"S": "x"}}},
|
||||||
|
"list": {"L": [{"S": "1"}, {"S": "2"}]},
|
||||||
|
}
|
||||||
|
assert mig.map_item(item) == item
|
||||||
|
|
||||||
|
|
||||||
|
class TestTablePair:
|
||||||
|
def test_contracts_alias(self):
|
||||||
|
assert mig.table_pair_for("contracts") == ("acdl-contracts", "nova-contracts")
|
||||||
|
|
||||||
|
def test_change_requests_alias(self):
|
||||||
|
assert mig.table_pair_for("change-requests") == (
|
||||||
|
"acdl-change-requests", "nova-change-requests"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_literal_source_name(self):
|
||||||
|
assert mig.table_pair_for("acdl-contracts") == ("acdl-contracts", "nova-contracts")
|
||||||
|
|
||||||
|
def test_literal_dest_name(self):
|
||||||
|
assert mig.table_pair_for("nova-contracts") == ("acdl-contracts", "nova-contracts")
|
||||||
|
|
||||||
|
def test_unknown_name_raises(self):
|
||||||
|
with pytest.raises(ValueError, match="unknown table"):
|
||||||
|
mig.table_pair_for("nope")
|
||||||
|
|
||||||
|
def test_custom_pairs(self):
|
||||||
|
pairs = [("old-x", "new-x")]
|
||||||
|
assert mig.table_pair_for("old-x", pairs=pairs) == ("old-x", "new-x")
|
||||||
|
|
||||||
|
|
||||||
|
class TestDefaultPairs:
|
||||||
|
def test_default_pairs_cover_both_tables(self):
|
||||||
|
sources = [s for s, _ in mig.DEFAULT_TABLE_PAIRS]
|
||||||
|
dests = [d for _, d in mig.DEFAULT_TABLE_PAIRS]
|
||||||
|
assert sources == ["acdl-contracts", "acdl-change-requests"]
|
||||||
|
assert dests == ["nova-contracts", "nova-change-requests"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestArgparser:
|
||||||
|
def test_dry_run_default(self):
|
||||||
|
args = mig.build_parser().parse_args([])
|
||||||
|
assert args.apply is False
|
||||||
|
assert args.region == "us-east-1"
|
||||||
|
assert args.table is None
|
||||||
|
|
||||||
|
def test_apply_flag(self):
|
||||||
|
args = mig.build_parser().parse_args(["--apply"])
|
||||||
|
assert args.apply is True
|
||||||
|
|
||||||
|
def test_table_filter(self):
|
||||||
|
args = mig.build_parser().parse_args(["--table", "contracts"])
|
||||||
|
assert args.table == "contracts"
|
||||||
|
|
||||||
|
def test_source_dest_override(self):
|
||||||
|
args = mig.build_parser().parse_args(["--source", "old", "--dest", "new"])
|
||||||
|
assert args.source == "old"
|
||||||
|
assert args.dest == "new"
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunDryRun:
|
||||||
|
"""The dry-run path exercises the table-pair resolution + describes both
|
||||||
|
tables without writing. We stub the boto3 client so no AWS access occurs."""
|
||||||
|
|
||||||
|
def _fake_client(self, describable=True):
|
||||||
|
client = type("FakeClient", (), {})()
|
||||||
|
def describe_table(TableName):
|
||||||
|
if not describable:
|
||||||
|
raise Exception("ResourceNotFoundException")
|
||||||
|
return {"Table": {"ItemCount": 0}}
|
||||||
|
client.describe_table = describe_table
|
||||||
|
client.scan = lambda **k: {"Items": []}
|
||||||
|
client.put_item = lambda **k: None
|
||||||
|
return client
|
||||||
|
|
||||||
|
def test_run_dry_run_reports_planned_copy(self, monkeypatch, capsys):
|
||||||
|
# Build args with both default pairs.
|
||||||
|
args = mig.build_parser().parse_args([])
|
||||||
|
# Stub the client constructor so no real boto3 client is built.
|
||||||
|
monkeypatch.setattr(mig.boto3, "client", lambda *a, **k: self._fake_client())
|
||||||
|
rc = mig.run(args)
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert rc == 0
|
||||||
|
assert "DRY-RUN" in out
|
||||||
|
assert "acdl-contracts" in out and "nova-contracts" in out
|
||||||
|
assert "acdl-change-requests" in out and "nova-change-requests" in out
|
||||||
|
assert "would PutItem" in out
|
||||||
|
assert "NOT deleted" in out
|
||||||
|
|
||||||
|
def test_run_source_table_not_describable_fails(self, monkeypatch, capsys):
|
||||||
|
args = mig.build_parser().parse_args([])
|
||||||
|
# First describe_table (source) raises, second (dest) is fine — emulate by
|
||||||
|
# raising on the first call only.
|
||||||
|
calls = {"n": 0}
|
||||||
|
client = type("FakeClient", (), {})()
|
||||||
|
def describe_table(TableName):
|
||||||
|
calls["n"] += 1
|
||||||
|
if calls["n"] % 2 == 1: # source (odd calls)
|
||||||
|
raise Exception("ResourceNotFoundException")
|
||||||
|
return {"Table": {"ItemCount": 0}}
|
||||||
|
client.describe_table = describe_table
|
||||||
|
client.scan = lambda **k: {"Items": []}
|
||||||
|
client.put_item = lambda **k: None
|
||||||
|
monkeypatch.setattr(mig.boto3, "client", lambda *a, **k: client)
|
||||||
|
rc = mig.run(args)
|
||||||
|
err = capsys.readouterr().err
|
||||||
|
assert rc == 1
|
||||||
|
assert "not describable" in err
|
||||||
@@ -55,7 +55,7 @@ class TestWriteEvent:
|
|||||||
with mock_aws():
|
with mock_aws():
|
||||||
dyn = boto3.client("dynamodb", region_name="us-east-1")
|
dyn = boto3.client("dynamodb", region_name="us-east-1")
|
||||||
dyn.create_table(
|
dyn.create_table(
|
||||||
TableName="acdl-outbox",
|
TableName="nova-outbox",
|
||||||
KeySchema=[
|
KeySchema=[
|
||||||
{"AttributeName": "contractId", "KeyType": "HASH"},
|
{"AttributeName": "contractId", "KeyType": "HASH"},
|
||||||
{"AttributeName": "eventType#eventTs", "KeyType": "RANGE"},
|
{"AttributeName": "eventType#eventTs", "KeyType": "RANGE"},
|
||||||
@@ -68,7 +68,7 @@ class TestWriteEvent:
|
|||||||
)
|
)
|
||||||
|
|
||||||
event = self._sample_event()
|
event = self._sample_event()
|
||||||
item = write_event(event, outbox_table="acdl-outbox", region="us-east-1")
|
item = write_event(event, outbox_table="nova-outbox", region="us-east-1")
|
||||||
|
|
||||||
assert item["contractId"]["S"] == "test-contract-001"
|
assert item["contractId"]["S"] == "test-contract-001"
|
||||||
assert item["prev_event_hash"]["S"] == "GENESIS"
|
assert item["prev_event_hash"]["S"] == "GENESIS"
|
||||||
@@ -84,7 +84,7 @@ class TestWriteEvent:
|
|||||||
with mock_aws():
|
with mock_aws():
|
||||||
dyn = boto3.client("dynamodb", region_name="us-east-1")
|
dyn = boto3.client("dynamodb", region_name="us-east-1")
|
||||||
dyn.create_table(
|
dyn.create_table(
|
||||||
TableName="acdl-outbox",
|
TableName="nova-outbox",
|
||||||
KeySchema=[
|
KeySchema=[
|
||||||
{"AttributeName": "contractId", "KeyType": "HASH"},
|
{"AttributeName": "contractId", "KeyType": "HASH"},
|
||||||
{"AttributeName": "eventType#eventTs", "KeyType": "RANGE"},
|
{"AttributeName": "eventType#eventTs", "KeyType": "RANGE"},
|
||||||
@@ -97,7 +97,7 @@ class TestWriteEvent:
|
|||||||
)
|
)
|
||||||
|
|
||||||
event = self._sample_event()
|
event = self._sample_event()
|
||||||
item = write_event(event, outbox_table="acdl-outbox", region="us-east-1")
|
item = write_event(event, outbox_table="nova-outbox", region="us-east-1")
|
||||||
expected_hash = _canonical_hash(event)
|
expected_hash = _canonical_hash(event)
|
||||||
assert item["hash"]["S"] == expected_hash
|
assert item["hash"]["S"] == expected_hash
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ class TestWriteEvent:
|
|||||||
with mock_aws():
|
with mock_aws():
|
||||||
dyn = boto3.client("dynamodb", region_name="us-east-1")
|
dyn = boto3.client("dynamodb", region_name="us-east-1")
|
||||||
dyn.create_table(
|
dyn.create_table(
|
||||||
TableName="acdl-outbox",
|
TableName="nova-outbox",
|
||||||
KeySchema=[
|
KeySchema=[
|
||||||
{"AttributeName": "contractId", "KeyType": "HASH"},
|
{"AttributeName": "contractId", "KeyType": "HASH"},
|
||||||
{"AttributeName": "eventType#eventTs", "KeyType": "RANGE"},
|
{"AttributeName": "eventType#eventTs", "KeyType": "RANGE"},
|
||||||
@@ -122,10 +122,10 @@ class TestWriteEvent:
|
|||||||
)
|
)
|
||||||
|
|
||||||
event = self._sample_event()
|
event = self._sample_event()
|
||||||
write_event(event, outbox_table="acdl-outbox", region="us-east-1")
|
write_event(event, outbox_table="nova-outbox", region="us-east-1")
|
||||||
|
|
||||||
resp = dyn.get_item(
|
resp = dyn.get_item(
|
||||||
TableName="acdl-outbox",
|
TableName="nova-outbox",
|
||||||
Key={
|
Key={
|
||||||
"contractId": {"S": "test-contract-001"},
|
"contractId": {"S": "test-contract-001"},
|
||||||
"eventType#eventTs": {"S": "CONFIDENCE_COMPUTED#2026-07-22T00:00:00Z"},
|
"eventType#eventTs": {"S": "CONFIDENCE_COMPUTED#2026-07-22T00:00:00Z"},
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ class TestInvokePolicyTemplate:
|
|||||||
rendered = template.replace("${account_id}", "123456789012").replace("${region}", "us-east-1")
|
rendered = template.replace("${account_id}", "123456789012").replace("${region}", "us-east-1")
|
||||||
policy = json.loads(rendered)
|
policy = json.loads(rendered)
|
||||||
resource_arn = policy["Statement"][0]["Resource"]
|
resource_arn = policy["Statement"][0]["Resource"]
|
||||||
assert resource_arn == "arn:aws:lambda:us-east-1:123456789012:function:acdl-contract-ingestor"
|
assert resource_arn == "arn:aws:lambda:us-east-1:123456789012:function:nova-contract-ingestor"
|
||||||
assert "000000000000" not in resource_arn
|
assert "000000000000" not in resource_arn
|
||||||
# ${consumerRepo} is a runtime placeholder (not a Terraform variable) — it stays.
|
# ${consumerRepo} is a runtime placeholder (not a Terraform variable) — it stays.
|
||||||
assert "${account_id}" not in rendered
|
assert "${account_id}" not in rendered
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ from core.separation_of_duties import route_halt_artifact
|
|||||||
|
|
||||||
def test_route_halt_publishes_to_sns_when_arn_set(monkeypatch):
|
def test_route_halt_publishes_to_sns_when_arn_set(monkeypatch):
|
||||||
"""With ACDL_SOD_HALT_TOPIC_ARN set, the SNS client receives the publish."""
|
"""With ACDL_SOD_HALT_TOPIC_ARN set, the SNS client receives the publish."""
|
||||||
monkeypatch.setenv("ACDL_SOD_HALT_TOPIC_ARN", "arn:aws:sns:us-east-1:000000000000:acdl-sod-halt")
|
monkeypatch.setenv("ACDL_SOD_HALT_TOPIC_ARN", "arn:aws:sns:us-east-1:000000000000:nova-sod-halt")
|
||||||
sns_client = mock.MagicMock()
|
sns_client = mock.MagicMock()
|
||||||
route_halt_artifact("contract-123", "SEPARATION_OF_DUTIES_VIOLATION: x==y",
|
route_halt_artifact("contract-123", "SEPARATION_OF_DUTIES_VIOLATION: x==y",
|
||||||
oncall_client=sns_client)
|
oncall_client=sns_client)
|
||||||
sns_client.publish.assert_called_once()
|
sns_client.publish.assert_called_once()
|
||||||
call = sns_client.publish.call_args
|
call = sns_client.publish.call_args
|
||||||
assert call.kwargs["TopicArn"] == "arn:aws:sns:us-east-1:000000000000:acdl-sod-halt"
|
assert call.kwargs["TopicArn"] == "arn:aws:sns:us-east-1:000000000000:nova-sod-halt"
|
||||||
assert "contract-123" in call.kwargs["Message"]
|
assert "contract-123" in call.kwargs["Message"]
|
||||||
assert "SEPARATION_OF_DUTIES_VIOLATION" in call.kwargs["Message"]
|
assert "SEPARATION_OF_DUTIES_VIOLATION" in call.kwargs["Message"]
|
||||||
assert call.kwargs["Subject"] == "ACDL SoD halt"
|
assert call.kwargs["Subject"] == "ACDL SoD halt"
|
||||||
@@ -54,7 +54,7 @@ def test_route_halt_outbox_fallback_writes_event(monkeypatch):
|
|||||||
|
|
||||||
def test_route_halt_sns_failure_falls_back_to_outbox(monkeypatch):
|
def test_route_halt_sns_failure_falls_back_to_outbox(monkeypatch):
|
||||||
"""If SNS publish raises, the outbox fallback is used."""
|
"""If SNS publish raises, the outbox fallback is used."""
|
||||||
monkeypatch.setenv("ACDL_SOD_HALT_TOPIC_ARN", "arn:aws:sns:us-east-1:000000000000:acdl-sod-halt")
|
monkeypatch.setenv("ACDL_SOD_HALT_TOPIC_ARN", "arn:aws:sns:us-east-1:000000000000:nova-sod-halt")
|
||||||
sns_client = mock.MagicMock()
|
sns_client = mock.MagicMock()
|
||||||
sns_client.publish.side_effect = Exception("SNS down")
|
sns_client.publish.side_effect = Exception("SNS down")
|
||||||
with mock.patch("core.outbox_writer.write_event") as mock_write:
|
with mock.patch("core.outbox_writer.write_event") as mock_write:
|
||||||
@@ -63,8 +63,8 @@ def test_route_halt_sns_failure_falls_back_to_outbox(monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
def test_sns_topic_defined_in_terraform():
|
def test_sns_topic_defined_in_terraform():
|
||||||
"""terraform/platform/main.tf defines the acdl-sod-halt SNS topic."""
|
"""terraform/platform/main.tf defines the nova-sod-halt SNS topic."""
|
||||||
tf = (ROOT / "terraform" / "platform" / "main.tf").read_text()
|
tf = (ROOT / "terraform" / "platform" / "main.tf").read_text()
|
||||||
assert "aws_sns_topic" in tf
|
assert "aws_sns_topic" in tf
|
||||||
assert "acdl-sod-halt" in tf
|
assert "nova-sod-halt" in tf
|
||||||
assert "acdl_sod_halt_topic_arn" in tf
|
assert "nova_sod_halt_topic_arn" in tf
|
||||||
@@ -107,12 +107,11 @@ class TestCreateStateBackend:
|
|||||||
|
|
||||||
def test_state_bucket_name_construction(self, monkeypatch):
|
def test_state_bucket_name_construction(self, monkeypatch):
|
||||||
"""The state bucket name is derived from NOVA_AWS_ACCOUNT_ID
|
"""The state bucket name is derived from NOVA_AWS_ACCOUNT_ID
|
||||||
(P2 renamed from ACDL_AWS_ACCOUNT_ID; the bucket name acdl-tfstate-*
|
(P4, REQ-163: bucket renamed acdl-tfstate-* → nova-tfstate-*)."""
|
||||||
stays until P4, REQ-163)."""
|
|
||||||
monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "123456789012")
|
monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "123456789012")
|
||||||
account_id = os.environ.get("NOVA_AWS_ACCOUNT_ID", "581513795199")
|
account_id = os.environ.get("NOVA_AWS_ACCOUNT_ID", "581513795199")
|
||||||
state_bucket = f"acdl-tfstate-{account_id}-us-east-1"
|
state_bucket = f"nova-tfstate-{account_id}-us-east-1"
|
||||||
assert state_bucket == "acdl-tfstate-123456789012-us-east-1"
|
assert state_bucket == "nova-tfstate-123456789012-us-east-1"
|
||||||
|
|
||||||
def test_idempotent_bucket_creation(self, monkeypatch):
|
def test_idempotent_bucket_creation(self, monkeypatch):
|
||||||
"""head_bucket success -> no create_bucket called."""
|
"""head_bucket success -> no create_bucket called."""
|
||||||
@@ -141,11 +140,11 @@ class TestCreateIamUser:
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
mock_iam = mock.MagicMock()
|
mock_iam = mock.MagicMock()
|
||||||
mock_iam.get_user.return_value = {"User": {"UserName": "acdl-spike-runner"}}
|
mock_iam.get_user.return_value = {"User": {"UserName": "nova-spike-runner"}}
|
||||||
monkeypatch.setattr(boto3, "client", lambda *a, **k: mock_iam)
|
monkeypatch.setattr(boto3, "client", lambda *a, **k: mock_iam)
|
||||||
|
|
||||||
# Simulate the idempotent check
|
# Simulate the idempotent check
|
||||||
mock_iam.get_user(UserName="acdl-spike-runner")
|
mock_iam.get_user(UserName="nova-spike-runner")
|
||||||
mock_iam.create_user.assert_not_called()
|
mock_iam.create_user.assert_not_called()
|
||||||
|
|
||||||
def test_policy_overwrite_is_idempotent(self, monkeypatch):
|
def test_policy_overwrite_is_idempotent(self, monkeypatch):
|
||||||
@@ -157,5 +156,5 @@ class TestCreateIamUser:
|
|||||||
monkeypatch.setattr(boto3, "client", lambda *a, **k: mock_iam)
|
monkeypatch.setattr(boto3, "client", lambda *a, **k: mock_iam)
|
||||||
|
|
||||||
# put_user_policy is called every run (overwrites)
|
# put_user_policy is called every run (overwrites)
|
||||||
mock_iam.put_user_policy(UserName="acdl-spike-runner", PolicyName="p", PolicyDocument="{}")
|
mock_iam.put_user_policy(UserName="nova-spike-runner", PolicyName="p", PolicyDocument="{}")
|
||||||
mock_iam.put_user_policy.assert_called_once()
|
mock_iam.put_user_policy.assert_called_once()
|
||||||
Reference in New Issue
Block a user