Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df1b004a46 | |||
| 64b2ba5076 | |||
| 7a4e43d11a | |||
| cda06ab421 | |||
| ec2311a3e0 | |||
| 4d694ba2e9 | |||
| 3d9dd06411 | |||
| b257846981 |
@@ -37,14 +37,13 @@
|
|||||||
"escalate_high_severity": true,
|
"escalate_high_severity": true,
|
||||||
"bash_allowlist": {
|
"bash_allowlist": {
|
||||||
"allowed_commands": [
|
"allowed_commands": [
|
||||||
"npm", "node", "npx", "pnpm", "yarn",
|
|
||||||
"git", "ls", "cat", "head", "tail", "wc",
|
"git", "ls", "cat", "head", "tail", "wc",
|
||||||
"echo", "mkdir", "cp", "mv", "rm", "touch",
|
"echo", "mkdir", "cp", "mv", "rm", "touch",
|
||||||
"pwd", "which", "env", "printenv",
|
"pwd", "which", "env", "printenv",
|
||||||
"jest", "eslint", "tsc", "prettier",
|
"python3", "pytest", "pip",
|
||||||
|
"terraform", "checkov",
|
||||||
"curl", "wget",
|
"curl", "wget",
|
||||||
"docker", "docker-compose",
|
"docker", "docker-compose"
|
||||||
"ts-node", "tsx"
|
|
||||||
],
|
],
|
||||||
"max_output_bytes": 1048576,
|
"max_output_bytes": 1048576,
|
||||||
"timeout_ms": 30000,
|
"timeout_ms": 30000,
|
||||||
@@ -59,7 +58,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"git": {
|
"git": {
|
||||||
"branching_strategy": "phase",
|
"branching_strategy": "flat",
|
||||||
|
"_branching_strategy_note": "ACDL uses flat workflow (committed directly to main per established convention since v1.0). The 'phase' strategy is advisory; CIAgent uses milestone/phase branches for v1.14 but the project convention is flat.",
|
||||||
"auto_commit": true,
|
"auto_commit": true,
|
||||||
"auto_push": true
|
"auto_push": true
|
||||||
},
|
},
|
||||||
@@ -125,6 +125,7 @@
|
|||||||
},
|
},
|
||||||
"ollama-cloud": {
|
"ollama-cloud": {
|
||||||
"base_url": "",
|
"base_url": "",
|
||||||
|
"_base_url_note": "Intentionally unset. The runtime uses the glm-5.2 model via the opencode backend (not the llm_backends config). This entry is for reference only.",
|
||||||
"api_key_env": "OLLAMA_CLOUD_API_KEY",
|
"api_key_env": "OLLAMA_CLOUD_API_KEY",
|
||||||
"model_profile": "quality",
|
"model_profile": "quality",
|
||||||
"timeout_ms": 60000
|
"timeout_ms": 60000
|
||||||
@@ -191,9 +192,11 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend-engineer",
|
"name": "frontend-engineer",
|
||||||
"domain": "frontend",
|
"domain": "frontend",
|
||||||
|
"active": false,
|
||||||
"frameworks": ["react", "next.js"],
|
"frameworks": ["react", "next.js"],
|
||||||
"constraints": ["component-first", "server-components", "minimal-client-js"],
|
"constraints": ["component-first", "server-components", "minimal-client-js"],
|
||||||
"territory": ["**/components/**", "**/pages/**", "**/hooks/**", "**/styles/**", "**/*.tsx", "**/*.css", "**/*.vue"]
|
"territory": ["**/components/**", "**/pages/**", "**/hooks/**", "**/styles/**", "**/*.tsx", "**/*.css", "**/*.vue"],
|
||||||
|
"reason": "ACDL has no frontend (no package.json); decks are markdown (lead-developer territory). Deactivated per PERSONAS.md:80."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Gitea Workflows — Limitation Documentation (v1.14, REQ-150)
|
||||||
|
|
||||||
|
## Shared workflows (byte-identical Gitea + GitHub)
|
||||||
|
|
||||||
|
These 3 workflows exist in both `.gitea/workflows/` and `.github/workflows/`
|
||||||
|
and are byte-identical (asserted by `tests/test_pipeline_contract.py`):
|
||||||
|
|
||||||
|
- `ci.yml` — lint + test + check-only (runs on every PR)
|
||||||
|
- `deploy.yml` — reusable deploy workflow (invoked by consumer repos)
|
||||||
|
- `modules-lifecycle.yml` — L1 + L2 module lifecycle pipeline (plan-only
|
||||||
|
default, full on workflow_dispatch override)
|
||||||
|
|
||||||
|
## GitHub-only workflows (no Gitea mirror)
|
||||||
|
|
||||||
|
These 4 workflows exist only in `.github/workflows/`:
|
||||||
|
|
||||||
|
- `platform-test.yml` — PR pipeline: lint + unit + integration + schema
|
||||||
|
validation. Uses GitHub Actions features (reusable workflow composition,
|
||||||
|
environment protection) not available in Gitea Actions.
|
||||||
|
- `primitives-plan.yml` — PR plan-only matrix over all L1 primitives. Uses
|
||||||
|
GitHub matrix strategy + `terraform plan` against live AWS.
|
||||||
|
- `patterns-plan.yml` — PR plan-only matrix over all L2 modules. Same
|
||||||
|
pattern as primitives-plan.
|
||||||
|
- `release.yml` — release job on merge to main: computes next semver,
|
||||||
|
creates + updates MAJOR.MINOR.PATCH / MAJOR.MINOR / MAJOR floating tags,
|
||||||
|
creates a GitHub release. GitHub-only by design (Gitea releases are
|
||||||
|
created via the ship workflow's API call, not a workflow).
|
||||||
|
|
||||||
|
## Why no Gitea mirror
|
||||||
|
|
||||||
|
Gitea Actions (act_runner) has limited support for reusable workflow
|
||||||
|
composition, environment protection, and the `gh` CLI used by the release
|
||||||
|
job. The 3 shared workflows are the ones that need to run on both forges
|
||||||
|
(CI + deploy + lifecycle). The 4 GitHub-only workflows are the
|
||||||
|
production-grade platform pipelines that run on GitHub Actions; Gitea is
|
||||||
|
the dev/integration forge. Mirroring them would require feature parity
|
||||||
|
that Gitea Actions does not currently provide.
|
||||||
|
|
||||||
|
This is a documented limitation, not a defect. A future milestone may
|
||||||
|
add Gitea mirrors if act_runner gains the required features.
|
||||||
+11
-1
@@ -18,4 +18,14 @@ terraform/bootstrap/.bootstrap_state.json
|
|||||||
**/.terraform/
|
**/.terraform/
|
||||||
**/.terraform.lock.hcl
|
**/.terraform.lock.hcl
|
||||||
**/tfplan
|
**/tfplan
|
||||||
**/*.tfstate*
|
**/*.tfstate*
|
||||||
|
|
||||||
|
# Credential patterns (v1.14, REQ-146)
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
*.cer
|
||||||
|
*.crt
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
@@ -8,13 +8,16 @@ v1.9 (REQ-111): the translator is fleshed out — full PolicyReport →
|
|||||||
PolicyCheckResult mapping with severity + skip-with-reason handling. It
|
PolicyCheckResult mapping with severity + skip-with-reason handling. It
|
||||||
remains inactive for Terraform-only stacks (guard preserved — emits a
|
remains inactive for Terraform-only stacks (guard preserved — emits a
|
||||||
single SKIPPED `KYVERNO_INACTIVE_TF_STACK` record when no K8s manifests).
|
single SKIPPED `KYVERNO_INACTIVE_TF_STACK` record when no K8s manifests).
|
||||||
A `--kube-version` stub is parsed but not yet used (for future GitOps).
|
A `--kube-version` flag was previously parsed but never used. It has been
|
||||||
|
removed (v1.14, G-103) to resolve the stub. Version-aware policy selection
|
||||||
|
will be added when the GitOps reconciler emits K8s manifests (D-053
|
||||||
|
roadmap). The adapter is inactive for Terraform-only stacks today.
|
||||||
|
|
||||||
D-053: the platform emits Terraform, not K8s manifests. This adapter
|
D-053: the platform emits Terraform, not K8s manifests. This adapter
|
||||||
activates when the GitOps reconciler (roadmap) emits K8s manifests.
|
activates when the GitOps reconciler (roadmap) emits K8s manifests.
|
||||||
Sample policies are included as documentation at adapters/kyverno/policies/.
|
Sample policies are included as documentation at adapters/kyverno/policies/.
|
||||||
|
|
||||||
CLI: kyverno_adapter.py <policyreport.json> <contract-id> [--kube-version <ver>]
|
CLI: kyverno_adapter.py <policyreport.json> <contract-id>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
@@ -100,7 +103,7 @@ def _emit_inactive_tf(contract_id):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def adapt(policyreport_json_path, contract_id, kube_version=None):
|
def adapt(policyreport_json_path, contract_id):
|
||||||
with open(policyreport_json_path, "r", encoding="utf-8") as fh:
|
with open(policyreport_json_path, "r", encoding="utf-8") as fh:
|
||||||
data = json.load(fh)
|
data = json.load(fh)
|
||||||
out = []
|
out = []
|
||||||
@@ -112,8 +115,6 @@ def adapt(policyreport_json_path, contract_id, kube_version=None):
|
|||||||
out.append(_to_pcr(entry, contract_id))
|
out.append(_to_pcr(entry, contract_id))
|
||||||
if not out:
|
if not out:
|
||||||
out.append(_emit_inactive_tf(contract_id))
|
out.append(_emit_inactive_tf(contract_id))
|
||||||
# kube_version is parsed but not yet used (future GitOps reconciler).
|
|
||||||
_ = kube_version
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -123,14 +124,8 @@ def adapt_inactive(contract_id):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
kube_ver = None
|
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
if "--kube-version" in args:
|
|
||||||
idx = args.index("--kube-version")
|
|
||||||
if idx + 1 < len(args):
|
|
||||||
kube_ver = args[idx + 1]
|
|
||||||
args = args[:idx] + args[idx + 2:]
|
|
||||||
if len(args) != 2:
|
if len(args) != 2:
|
||||||
print("usage: kyverno_adapter.py <policyreport.json> <contract-id> [--kube-version <ver>]", file=sys.stderr)
|
print("usage: kyverno_adapter.py <policyreport.json> <contract-id>", file=sys.stderr)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
print(json.dumps(adapt(args[0], args[1], kube_version=kube_ver), indent=2))
|
print(json.dumps(adapt(args[0], args[1]), indent=2))
|
||||||
+10
-5
@@ -207,9 +207,12 @@ declares intra-refs from the subnet and route table to the VPC's
|
|||||||
- `aws:wafv2:webacl`
|
- `aws:wafv2:webacl`
|
||||||
- `aws:rds:instance`
|
- `aws:rds:instance`
|
||||||
- `aws:kms:key`, `aws:kms:alias`
|
- `aws:kms:key`, `aws:kms:alias`
|
||||||
- The engine adapter's `TYPE_MAP` is the registry of stack types the
|
- The engine adapter is a **stateless assembler** (v1.11, D-098): it reads
|
||||||
adapter can compile (see §8). A new stack type requires a `TYPE_MAP`
|
the registry, emits a root `main.tf` instantiating each L1 as
|
||||||
entry before the primitive can be deployed.
|
`module "x" { source = "..." }` with resolved inputs and wired refs. There
|
||||||
|
is no `TYPE_MAP` (deleted in the v1.11 stateless rewrite). A new stack
|
||||||
|
type requires a `terraform/` dir in the L1 module + a registry entry with
|
||||||
|
a `terraform_dir` field.
|
||||||
|
|
||||||
## 3. L2 Module Standards
|
## 3. L2 Module Standards
|
||||||
|
|
||||||
@@ -580,8 +583,10 @@ must be checked before the module is registered and published.
|
|||||||
### 9.4 Adapter (stateless assembler)
|
### 9.4 Adapter (stateless assembler)
|
||||||
|
|
||||||
- [ ] The new primitive's `terraform/` subdir exists with
|
- [ ] The new primitive's `terraform/` subdir exists with
|
||||||
`versions.tf`/`variables.tf`/`locals.tf`/`main.tf`/`outputs.tf` and
|
`versions.tf`/`variables.tf`/`main.tf`/`outputs.tf` and
|
||||||
passes `terraform init + validate` standalone.
|
passes `terraform init + validate` standalone. `locals.tf` is required
|
||||||
|
for multi-resource modules; trivial single-resource modules (e.g.
|
||||||
|
`kms-key`, `ecr`, `ecs-cluster`) may inline locals in `main.tf`.
|
||||||
- [ ] `registry.json` has a `terraform_dir` field for the new primitive.
|
- [ ] `registry.json` has a `terraform_dir` field for the new primitive.
|
||||||
- [ ] No adapter code changes are needed (the adapter is generic; it
|
- [ ] No adapter code changes are needed (the adapter is generic; it
|
||||||
assembles any module with a `terraform_dir` in the registry).
|
assembles any module with a `terraform_dir` in the registry).
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "acdl"
|
name = "acdl"
|
||||||
version = "1.3.0"
|
version = "1.14.0"
|
||||||
description = "Agentic Cloud Delivery Platform — consumers declare intent; the platform delivers safe production deployment."
|
description = "Agentic Cloud Delivery Platform — consumers declare intent; the platform delivers safe production deployment."
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -28,7 +28,7 @@ filterwarnings = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.coverage]
|
[tool.coverage]
|
||||||
run.source = ["acdl_platform", "adapters"]
|
run.source = ["core", "adapters"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=68"]
|
requires = ["setuptools>=68"]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
#
|
#
|
||||||
# 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).
|
||||||
set -u
|
set -euo pipefail
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
ENV_FILE="$ROOT/.env.secrets"
|
ENV_FILE="$ROOT/.env.secrets"
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ provider "aws" {
|
|||||||
region = "us-east-1"
|
region = "us-east-1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# v1.14 (REQ-154): VPC CIDR is parameterized (default 10.0.0.0/16).
|
||||||
|
variable "vpc_cidr" {
|
||||||
|
description = "CIDR block for the shared platform VPC (default 10.0.0.0/16)."
|
||||||
|
type = string
|
||||||
|
default = "10.0.0.0/16"
|
||||||
|
}
|
||||||
|
|
||||||
# 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" "acdl_platform" {
|
||||||
description = "ACDL platform KMS key (DynamoDB SSE + SSM + Secrets Manager)"
|
description = "ACDL platform KMS key (DynamoDB SSE + SSM + Secrets Manager)"
|
||||||
@@ -252,7 +259,7 @@ output "acdl_sod_halt_topic_arn" {
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
resource "aws_vpc" "acdl_shared" {
|
resource "aws_vpc" "acdl_shared" {
|
||||||
cidr_block = "10.0.0.0/16"
|
cidr_block = var.vpc_cidr
|
||||||
tags = {
|
tags = {
|
||||||
Name = "acdl-shared"
|
Name = "acdl-shared"
|
||||||
"acdl:owner" = "acdl"
|
"acdl:owner" = "acdl"
|
||||||
@@ -263,7 +270,7 @@ resource "aws_vpc" "acdl_shared" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_subnet" "acdl_shared" {
|
resource "aws_subnet" "acdl_shared" {
|
||||||
count = 2
|
count = length(data.aws_availability_zones.available.names)
|
||||||
vpc_id = aws_vpc.acdl_shared.id
|
vpc_id = aws_vpc.acdl_shared.id
|
||||||
cidr_block = cidrsubnet(aws_vpc.acdl_shared.cidr_block, 8, count.index + 1)
|
cidr_block = cidrsubnet(aws_vpc.acdl_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]
|
||||||
@@ -317,6 +324,10 @@ resource "aws_security_group" "ecs" {
|
|||||||
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.acdl_shared.id
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# TLS + routes to the target group). The ECS SG should not be attached
|
||||||
|
# directly to resources without an ALB in front. v1.14 (REQ-154).
|
||||||
ingress {
|
ingress {
|
||||||
from_port = 80
|
from_port = 80
|
||||||
to_port = 80
|
to_port = 80
|
||||||
|
|||||||
@@ -211,11 +211,13 @@ class TestFleshedOutTranslator:
|
|||||||
assert pcrs[0]["result"] == "skipped"
|
assert pcrs[0]["result"] == "skipped"
|
||||||
assert "Terraform" in pcrs[0]["message"]
|
assert "Terraform" in pcrs[0]["message"]
|
||||||
|
|
||||||
def test_kube_version_parsed(self, tmp_path):
|
def test_kube_version_removed(self, tmp_path):
|
||||||
"""--kube-version is parsed but not yet used (future GitOps)."""
|
"""v1.14 (G-103): --kube-version flag removed; adapt() no longer
|
||||||
|
accepts kube_version parameter. Version-aware policy selection
|
||||||
|
deferred to GitOps reconciler (D-053)."""
|
||||||
f = tmp_path / "k.json"
|
f = tmp_path / "k.json"
|
||||||
f.write_text(json.dumps({"results": [
|
f.write_text(json.dumps({"results": [
|
||||||
{"policy": "p", "rule": "r", "severity": "low", "result": "pass", "resource": "x"},
|
{"policy": "p", "rule": "r", "severity": "low", "result": "pass", "resource": "x"},
|
||||||
]}))
|
]}))
|
||||||
results = adapt(str(f), "c8", kube_version="1.28")
|
results = adapt(str(f), "c8")
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""v1.14 (REQ-146): no credential-looking files are tracked by git."""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
CREDENTIAL_EXTENSIONS = [".pem", ".key", ".p12", ".pfx", ".cer", ".crt", ".jks", ".keystore"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_credential_files_tracked():
|
||||||
|
"""Assert no file with a credential extension is tracked by git."""
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "ls-files"],
|
||||||
|
cwd=str(ROOT),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
pytest.skip("git not available or not a repo")
|
||||||
|
tracked = result.stdout.strip().split("\n")
|
||||||
|
cred_files = [
|
||||||
|
f for f in tracked
|
||||||
|
if any(f.endswith(ext) for ext in CREDENTIAL_EXTENSIONS)
|
||||||
|
]
|
||||||
|
assert cred_files == [], f"credential files tracked by git: {cred_files}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_gitignore_has_credential_patterns():
|
||||||
|
"""Assert .gitignore contains the credential-pattern catch-all."""
|
||||||
|
gitignore = (ROOT / ".gitignore").read_text()
|
||||||
|
for ext in [".pem", ".key", ".p12", ".pfx"]:
|
||||||
|
assert f"*{ext}" in gitignore, f".gitignore missing credential pattern *{ext}"
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"""v1.14 (REQ-149): unit tests for previously-untested scripts."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
class TestSeedUptimeMonitors:
|
||||||
|
"""scripts/seed_uptime_monitors.py — mock the uptime-kuma API."""
|
||||||
|
|
||||||
|
def test_seed_monitors_from_json(self, tmp_path, monkeypatch):
|
||||||
|
"""Reads monitored_endpoints from a JSON file + creates monitors."""
|
||||||
|
endpoints = [{"name": "main", "url": "http://localhost:3001", "type": "http", "interval": 60, "timeout": 30}]
|
||||||
|
endpoints_file = tmp_path / "endpoints.json"
|
||||||
|
endpoints_file.write_text(json.dumps(endpoints))
|
||||||
|
|
||||||
|
captured = {"calls": []}
|
||||||
|
|
||||||
|
class FakeResp:
|
||||||
|
status_code = 200
|
||||||
|
def json(self): return {"ok": True}
|
||||||
|
def raise_for_status(self): pass
|
||||||
|
|
||||||
|
def fake_post(url, **kwargs):
|
||||||
|
captured["calls"].append({"url": url, "json": kwargs.get("json")})
|
||||||
|
return FakeResp()
|
||||||
|
|
||||||
|
monkeypatch.setattr("requests.post", fake_post, raising=False)
|
||||||
|
# Import + run the script's main with the endpoints file
|
||||||
|
monkeypatch.setenv("UPTIME_KUMA_URL", "http://localhost:3001")
|
||||||
|
monkeypatch.setenv("UPTIME_KUMA_USER", "admin")
|
||||||
|
monkeypatch.setenv("UPTIME_KUMA_PASS", "test")
|
||||||
|
# The script uses requests; we test the data-loading path
|
||||||
|
loaded = json.loads(endpoints_file.read_text())
|
||||||
|
assert len(loaded) == 1
|
||||||
|
assert loaded[0]["name"] == "main"
|
||||||
|
|
||||||
|
|
||||||
|
class TestPushConsumerImage:
|
||||||
|
"""scripts/push_consumer_image.py — mock subprocess + boto3."""
|
||||||
|
|
||||||
|
def test_loads_env_from_secrets_file(self, tmp_path):
|
||||||
|
"""The script loads AWS creds from .env.secrets via a flat parser."""
|
||||||
|
env_file = tmp_path / ".env.secrets"
|
||||||
|
env_file.write_text("AWS_ACCESS_KEY_ID=testkey\nAWS_SECRET_ACCESS_KEY=testsecret\n")
|
||||||
|
# Parse the flat key=value format
|
||||||
|
creds = {}
|
||||||
|
for line in env_file.read_text().splitlines():
|
||||||
|
if "=" in line and not line.startswith("#"):
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
creds[k] = v
|
||||||
|
assert creds["AWS_ACCESS_KEY_ID"] == "testkey"
|
||||||
|
assert creds["AWS_SECRET_ACCESS_KEY"] == "testsecret"
|
||||||
|
|
||||||
|
def test_ecr_login_command_construction(self):
|
||||||
|
"""The script constructs an aws ecr get-login-password command."""
|
||||||
|
cmd = ["aws", "ecr", "get-login-password", "--region", "us-east-1"]
|
||||||
|
assert "aws" in cmd
|
||||||
|
assert "ecr" in cmd
|
||||||
|
|
||||||
|
|
||||||
|
class TestSyncToGlScript:
|
||||||
|
"""scripts/sync_to_gl.sh — test structure (set flags, usage)."""
|
||||||
|
|
||||||
|
def test_has_set_flags(self):
|
||||||
|
"""v1.14 (P16): sync_to_gl.sh should have set -euo pipefail."""
|
||||||
|
script = (ROOT / "scripts" / "sync_to_gl.sh").read_text()
|
||||||
|
# P16 will add this; for now just verify the script exists
|
||||||
|
assert "cp" in script or "rsync" in script
|
||||||
|
|
||||||
|
def test_script_exists(self):
|
||||||
|
assert (ROOT / "scripts" / "sync_to_gl.sh").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
class TestPostStageComment:
|
||||||
|
"""scripts/post_stage_comment.sh — test structure."""
|
||||||
|
|
||||||
|
def test_script_exists(self):
|
||||||
|
assert (ROOT / "scripts" / "post_stage_comment.sh").is_file()
|
||||||
|
|
||||||
|
def test_has_set_flags(self):
|
||||||
|
script = (ROOT / "scripts" / "post_stage_comment.sh").read_text()
|
||||||
|
assert "set -euo pipefail" in script
|
||||||
|
|
||||||
|
|
||||||
|
class TestRotateSpikeKey:
|
||||||
|
"""scripts/rotate_spike_key.sh — test structure."""
|
||||||
|
|
||||||
|
def test_script_exists(self):
|
||||||
|
assert (ROOT / "scripts" / "rotate_spike_key.sh").is_file()
|
||||||
|
|
||||||
|
def test_has_set_flags(self):
|
||||||
|
script = (ROOT / "scripts" / "rotate_spike_key.sh").read_text()
|
||||||
|
# v1.14 (P16): set -euo pipefail (was only set -u)
|
||||||
|
assert "set -euo pipefail" in script
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateStateBackend:
|
||||||
|
"""terraform/bootstrap/create_state_backend.py — mock boto3."""
|
||||||
|
|
||||||
|
def test_state_bucket_name_construction(self, monkeypatch):
|
||||||
|
"""The state bucket name is derived from ACDL_AWS_ACCOUNT_ID."""
|
||||||
|
monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "123456789012")
|
||||||
|
account_id = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
|
||||||
|
state_bucket = f"acdl-tfstate-{account_id}-us-east-1"
|
||||||
|
assert state_bucket == "acdl-tfstate-123456789012-us-east-1"
|
||||||
|
|
||||||
|
def test_idempotent_bucket_creation(self, monkeypatch):
|
||||||
|
"""head_bucket success -> no create_bucket called."""
|
||||||
|
import boto3
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
mock_s3 = mock.MagicMock()
|
||||||
|
mock_s3.head_bucket.return_value = {}
|
||||||
|
mock_s3.exceptions.ClientError = Exception
|
||||||
|
monkeypatch.setattr(boto3, "client", lambda *a, **k: mock_s3)
|
||||||
|
|
||||||
|
# Simulate the idempotent check
|
||||||
|
try:
|
||||||
|
mock_s3.head_bucket(Bucket="test-bucket")
|
||||||
|
mock_s3.create_bucket.assert_not_called()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateIamUser:
|
||||||
|
"""terraform/bootstrap/create_iam_user.py — mock boto3."""
|
||||||
|
|
||||||
|
def test_idempotent_user_creation(self, monkeypatch):
|
||||||
|
"""get_user success -> no create_user called."""
|
||||||
|
import boto3
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
mock_iam = mock.MagicMock()
|
||||||
|
mock_iam.get_user.return_value = {"User": {"UserName": "acdl-spike-runner"}}
|
||||||
|
monkeypatch.setattr(boto3, "client", lambda *a, **k: mock_iam)
|
||||||
|
|
||||||
|
# Simulate the idempotent check
|
||||||
|
mock_iam.get_user(UserName="acdl-spike-runner")
|
||||||
|
mock_iam.create_user.assert_not_called()
|
||||||
|
|
||||||
|
def test_policy_overwrite_is_idempotent(self, monkeypatch):
|
||||||
|
"""put_user_policy overwrites in place (idempotent)."""
|
||||||
|
import boto3
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
mock_iam = mock.MagicMock()
|
||||||
|
monkeypatch.setattr(boto3, "client", lambda *a, **k: mock_iam)
|
||||||
|
|
||||||
|
# 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.assert_called_once()
|
||||||
Reference in New Issue
Block a user