Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c19a5d66f2 | |||
| e8effef415 |
@@ -126,20 +126,46 @@ engine-specific code. `modules/`, `schemas/`, `contracts/`,
|
||||
|
||||
## How to run
|
||||
|
||||
### Prerequisites
|
||||
### Quick start (offline, no AWS required)
|
||||
|
||||
> These prerequisites are for running the **platform repo** locally. A
|
||||
> consumer does not need any of these — see the
|
||||
> [Consumer guide](docs/consumer-guide.md) for the consumer happy path.
|
||||
The fastest way to verify the platform works — no AWS credentials, no
|
||||
bootstrap, no cost. See the [Consumer guide](docs/consumer-guide.md)
|
||||
for the consumer happy path (a consumer owns only a contract + app code).
|
||||
|
||||
- A platform-managed environment (see [docs/environments/](docs/environments/)).
|
||||
For local testing, `core/environments/dev.json` is provided as the sample.
|
||||
- AWS credentials for the dev environment (in `.env.secrets`, gitignored;
|
||||
see [Credentials & zero-trust](#credentials--zero-trust)).
|
||||
- `terraform` (pin `1.9.*`), `checkov` (pin `>=3.2,<4`), `python3` + `boto3`
|
||||
+ `jsonschema`.
|
||||
```bash
|
||||
# Install test dependencies
|
||||
pip install -r requirements-test.txt
|
||||
|
||||
### Run the platform pipeline end-to-end
|
||||
# 1. Run the test suite (all offline — uses moto for DynamoDB mocking)
|
||||
python3 -m pytest tests/ -v
|
||||
|
||||
# 2. Run the platform in check-only mode (offline — contract -> resolver ->
|
||||
# adapter -> structure validation). Uses the default sample contract
|
||||
# (contracts/static-assets.yaml) + sample dev environment.
|
||||
bash scripts/run_platform.sh --check-only
|
||||
# Expected: "=== PLATFORM CHECK OK ==="
|
||||
|
||||
# 3. Run the headline E2E against the local emulating tier (emulates ECS,
|
||||
# outbox, S3 state, Lambda in-process; D-092).
|
||||
bash scripts/run_platform.sh --local
|
||||
# Expected: "=== LOCAL E2E OK ==="
|
||||
|
||||
# 4. Reproduce the full CI pipeline locally (lint -> test -> check-only)
|
||||
bash scripts/run_ci.sh
|
||||
# Expected: "=== CI PIPELINE OK ==="
|
||||
|
||||
# Show all run_platform.sh flags:
|
||||
bash scripts/run_platform.sh --help
|
||||
```
|
||||
|
||||
### Run against live AWS (requires credentials + bootstrap)
|
||||
|
||||
> Prerequisites: a platform-managed environment (see
|
||||
> [docs/environments/](docs/environments/); `core/environments/dev.json`
|
||||
> is the sample), AWS credentials for dev (in `.env.secrets`, gitignored;
|
||||
> see [Credentials & zero-trust](#credentials--zero-trust)), `terraform`
|
||||
> (pin `1.9.*`), `checkov` (pin `>=3.2,<4`), `python3` + `boto3` +
|
||||
> `jsonschema`.
|
||||
|
||||
```bash
|
||||
# 1. Bootstrap the AWS state backend + runner IAM user (one-time, idempotent)
|
||||
@@ -168,34 +194,6 @@ bash scripts/run_platform.sh --plan-only contracts/static-assets.yaml
|
||||
bash scripts/run_platform.sh --quiet contracts/static-assets.yaml
|
||||
```
|
||||
|
||||
### Test the platform (offline, no AWS required)
|
||||
|
||||
```bash
|
||||
# Install test dependencies
|
||||
pip install -r requirements-test.txt
|
||||
|
||||
# Run the test suite (all offline — uses moto for DynamoDB mocking)
|
||||
python3 -m pytest tests/ -v
|
||||
|
||||
# Run the platform in check-only mode (offline — no AWS, no policy checks,
|
||||
# no outbox). Uses the default sample contract (contracts/static-assets.yaml)
|
||||
# and the sample dev environment (core/environments/dev.json).
|
||||
bash scripts/run_platform.sh --check-only
|
||||
# Expected: "=== PLATFORM CHECK OK ==="
|
||||
|
||||
# Run the headline E2E against the local emulating tier (no AWS credentials
|
||||
# needed — emulates ECS, outbox, S3 state, Lambda in-process; D-092).
|
||||
bash scripts/run_platform.sh --local
|
||||
# Expected: "=== LOCAL E2E OK ==="
|
||||
|
||||
# Reproduce the full CI pipeline locally (lint -> test -> check-only)
|
||||
bash scripts/run_ci.sh
|
||||
# Expected: "=== CI PIPELINE OK ==="
|
||||
|
||||
# Show all run_platform.sh flags + a one-line description each.
|
||||
bash scripts/run_platform.sh --help
|
||||
```
|
||||
|
||||
### CI/CD pipelines
|
||||
|
||||
The CI/CD pipeline is defined by a **central pipeline contract** — a
|
||||
|
||||
@@ -398,6 +398,65 @@ def _validate_change_request(payload):
|
||||
}
|
||||
|
||||
|
||||
def _onboard_consumer(payload):
|
||||
"""P18 (REQ-182): accept a self-service onboarding request.
|
||||
|
||||
Validates the payload against schemas/onboarding.schema.json, then
|
||||
writes a 'pending' row to nova-contracts (D-119). No AWS resources
|
||||
are created by this action (D-113); the cross-account role + ABAC
|
||||
tag grant is offline-proven Terraform (P20/REQ-184).
|
||||
"""
|
||||
import jsonschema
|
||||
schema_path = os.path.join(os.path.dirname(os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(__file__)))),
|
||||
"schemas", "onboarding.schema.json")
|
||||
try:
|
||||
with open(schema_path) as f:
|
||||
schema = json.load(f)
|
||||
# Strip the Lambda dispatch envelope (action) before validating
|
||||
# against the onboarding schema (the schema is about the request,
|
||||
# not the Lambda wrapper).
|
||||
onboarding_payload = {k: v for k, v in payload.items() if k != "action"}
|
||||
jsonschema.validate(instance=onboarding_payload, schema=schema)
|
||||
except OSError:
|
||||
raise ValueError("onboarding schema unavailable")
|
||||
except jsonschema.ValidationError as e:
|
||||
raise ValueError(f"onboarding payload invalid: {e.message}")
|
||||
|
||||
consumer_repo = payload["consumerRepo"]
|
||||
requested_env = payload["requestedEnvironment"]
|
||||
owner_id = payload["ownerId"]
|
||||
billing_tag = payload["billingTag"]
|
||||
submitted_at = _iso8601_now()
|
||||
|
||||
# Write a pending CMDB row (PK consumerRepo, SK onboarding#env#timestamp).
|
||||
table = _get_dynamodb().Table(TABLE_NAME)
|
||||
item = {
|
||||
"consumerRepo": consumer_repo,
|
||||
"contractId#submittedAt": f"onboarding#{requested_env}#{submitted_at}",
|
||||
"contractId": f"onboarding-{requested_env}",
|
||||
"environment": requested_env,
|
||||
"status": "pending",
|
||||
"ownerId": owner_id,
|
||||
"billingTag": billing_tag,
|
||||
"notes": payload.get("notes", ""),
|
||||
"submittedAt": submitted_at,
|
||||
}
|
||||
table.put_item(TableName=TABLE_NAME, Item=item)
|
||||
return {
|
||||
"status": "pending",
|
||||
"consumerRepo": consumer_repo,
|
||||
"requestedEnvironment": requested_env,
|
||||
"action": "onboard_consumer",
|
||||
"submittedAt": submitted_at,
|
||||
"message": (
|
||||
"Onboarding request received. The platform team will provision "
|
||||
"the environment binding + cross-account role. Track the status "
|
||||
"via the nova-contracts table (status=pending → granted)."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def lambda_handler(event, context):
|
||||
"""AWS Lambda handler entry point.
|
||||
|
||||
@@ -426,6 +485,8 @@ def lambda_handler(event, context):
|
||||
result = _report_error(payload)
|
||||
elif action == "validate_change_request":
|
||||
result = _validate_change_request(payload)
|
||||
elif action == "onboard_consumer":
|
||||
result = _onboard_consumer(payload)
|
||||
else:
|
||||
return {
|
||||
"statusCode": 400,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://nova.cloudinit.dev/schemas/onboarding.schema.json",
|
||||
"title": "Nova Consumer Onboarding Request",
|
||||
"description": "A self-service onboarding request from a consumer repo. Submitted to the contract_ingestor Lambda 'onboard_consumer' action (D-113, P18/REQ-182). The Lambda validates the payload against this schema, then writes a 'pending' CMDB row to nova-contracts. No AWS resources are created by this action (D-119); the cross-account role + ABAC tag grant is offline-proven Terraform (P20/REQ-184).",
|
||||
"type": "object",
|
||||
"required": ["consumerRepo", "requestedEnvironment", "ownerId", "billingTag"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"consumerRepo": {
|
||||
"type": "string",
|
||||
"description": "The consumer repository in org/repo format.",
|
||||
"pattern": "^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$",
|
||||
"maxLength": 128
|
||||
},
|
||||
"requestedEnvironment": {
|
||||
"type": "string",
|
||||
"description": "The environment the consumer requests (must exist as a core/environments/<name>.json).",
|
||||
"enum": ["dev", "qa", "prod", "dr"]
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "string",
|
||||
"description": "The owning team or individual (for ABAC nova:owner tag + CMDB).",
|
||||
"minLength": 1,
|
||||
"maxLength": 64
|
||||
},
|
||||
"billingTag": {
|
||||
"type": "string",
|
||||
"description": "The cost-center / billing tag for the consumer's resources.",
|
||||
"minLength": 1,
|
||||
"maxLength": 64
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Optional free-form notes for the platform team.",
|
||||
"maxLength": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -593,4 +593,52 @@ class TestV14IdentityValidation:
|
||||
"""The _validate_caller_identity docstring documents the ABAC reliance."""
|
||||
docstring = ingestor._validate_caller_identity.__doc__
|
||||
assert "ABAC" in docstring
|
||||
assert "PrincipalTag" in docstring
|
||||
assert "PrincipalTag" in docstring
|
||||
|
||||
class TestOnboardConsumer:
|
||||
"""P18 (REQ-182): the onboard_consumer action writes a pending CMDB row."""
|
||||
|
||||
_ARN = "arn:aws:sts::000:assumed-role/nova-deploy/test"
|
||||
|
||||
def test_valid_onboarding_writes_pending_row(self, moto_contracts_table):
|
||||
payload = {
|
||||
"action": "onboard_consumer",
|
||||
"consumerRepo": "acdl/consumer-b",
|
||||
"requestedEnvironment": "dev",
|
||||
"ownerId": "team-b",
|
||||
"billingTag": "cost-center-b",
|
||||
}
|
||||
event = {"body": json.dumps(payload), "requestContext": {"identity": {"userArn": self._ARN}}}
|
||||
resp = ingestor.lambda_handler(event, None)
|
||||
assert resp["statusCode"] == 200
|
||||
body = json.loads(resp["body"])
|
||||
assert body["status"] == "pending"
|
||||
assert body["action"] == "onboard_consumer"
|
||||
assert body["requestedEnvironment"] == "dev"
|
||||
|
||||
def test_invalid_onboarding_rejected(self, moto_contracts_table):
|
||||
# An invalid consumerRepo (no /) fails the identity format check
|
||||
# (which runs for all actions) before the onboarding schema.
|
||||
payload = {
|
||||
"action": "onboard_consumer",
|
||||
"consumerRepo": "not-a-repo-format",
|
||||
"requestedEnvironment": "dev",
|
||||
"ownerId": "team-b",
|
||||
"billingTag": "cost-center-b",
|
||||
}
|
||||
event = {"body": json.dumps(payload), "requestContext": {"identity": {"userArn": self._ARN}}}
|
||||
resp = ingestor.lambda_handler(event, None)
|
||||
assert resp["statusCode"] == 400
|
||||
assert "invalid consumerRepo" in json.loads(resp["body"])["error"]
|
||||
|
||||
def test_missing_onboarding_field_rejected(self, moto_contracts_table):
|
||||
payload = {
|
||||
"action": "onboard_consumer",
|
||||
"consumerRepo": "acdl/consumer-b",
|
||||
"requestedEnvironment": "dev",
|
||||
# ownerId + billingTag missing
|
||||
}
|
||||
event = {"body": json.dumps(payload), "requestContext": {"identity": {"userArn": self._ARN}}}
|
||||
resp = ingestor.lambda_handler(event, None)
|
||||
assert resp["statusCode"] == 400
|
||||
assert "onboarding payload invalid" in json.loads(resp["body"])["error"]
|
||||
|
||||
Reference in New Issue
Block a user