From de558232312f0bd5e4b0795e58cc33147e41b132 Mon Sep 17 00:00:00 2001 From: Hannah Braswell Date: Thu, 4 Jun 2026 16:16:28 -0400 Subject: [PATCH] fix(registry): seed valid ampel complypack content in mock registry Replace the dummy ampel complypack payload in the mock OCI registry with valid granular policy JSON that the ampel provider's LoadGranularPolicies() accepts. The dummy content lacked the required 'id' field, causing cross-repo integration test failures when the ampel provider consumes ComplypackContentPath (complytime-providers PR #52). Changes: - Add testdata/ampel-complypack/block-force-push.json with valid AmpelPolicy content (copied from cross-repo test fixture) - Add //go:embed directive for ampel complypack testdata - Update seedDefaults() to use buildTarGzFromFS instead of buildDummyTarGz for the complypacks/ampel-bp artifact - Add TestBuildTarGzFromFS_AmpelFS verifying archive structure and JSON content validity - Extend TestSeedDefaults_AllReposSeeded with ampel complypack content blob verification (manifest -> layer -> gzip -> tar -> JSON -> id field) Follows the OPA complypack pattern established in commit 74fbae8. Ref: complytime/complytime-providers#52 --- cmd/mock-oci-registry/main.go | 6 +- cmd/mock-oci-registry/main_test.go | 86 +++++++++++++++++ .../ampel-complypack/block-force-push.json | 27 ++++++ .../ampel-complypack-content/.openspec.yaml | 2 + .../ampel-complypack-content/design.md | 93 +++++++++++++++++++ .../ampel-complypack-content/proposal.md | 54 +++++++++++ .../specs/ampel-complypack-seed/spec.md | 62 +++++++++++++ .../changes/ampel-complypack-content/tasks.md | 21 +++++ 8 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 cmd/mock-oci-registry/testdata/ampel-complypack/block-force-push.json create mode 100644 openspec/changes/ampel-complypack-content/.openspec.yaml create mode 100644 openspec/changes/ampel-complypack-content/design.md create mode 100644 openspec/changes/ampel-complypack-content/proposal.md create mode 100644 openspec/changes/ampel-complypack-content/specs/ampel-complypack-seed/spec.md create mode 100644 openspec/changes/ampel-complypack-content/tasks.md diff --git a/cmd/mock-oci-registry/main.go b/cmd/mock-oci-registry/main.go index b3d30c62..ed1f8641 100644 --- a/cmd/mock-oci-registry/main.go +++ b/cmd/mock-oci-registry/main.go @@ -35,6 +35,9 @@ var seedData embed.FS //go:embed testdata/opa-complypack/* var opaComplypackData embed.FS +//go:embed testdata/ampel-complypack/* +var ampelComplypackData embed.FS + const defaultPort = "8765" const ( @@ -490,10 +493,11 @@ func (s *contentStore) seedDefaults() { []string{"v1.0.0", "latest"}) // complypacks/ampel-bp — ComplyPack artifact for AMPEL branch protection evaluator + // Contains granular policy JSON from testdata/ampel-complypack/ s.addComplypackArtifact("complypacks/ampel-bp", []string{"v1.0.0", "latest"}, complypackDef{ evaluatorID: "ampel", version: "1.0.0", - content: buildDummyTarGz("policy.json", []byte(`{"name":"ampel-branch-protection","version":"1.0.0"}`)), + content: buildTarGzFromFS(ampelComplypackData, "testdata/ampel-complypack"), }) // policies/test-opa-policy — OPA container security controls diff --git a/cmd/mock-oci-registry/main_test.go b/cmd/mock-oci-registry/main_test.go index c8e29e61..92013501 100644 --- a/cmd/mock-oci-registry/main_test.go +++ b/cmd/mock-oci-registry/main_test.go @@ -6,6 +6,7 @@ import ( "archive/tar" "bytes" "compress/gzip" + "encoding/json" "io" "os" "path/filepath" @@ -65,6 +66,43 @@ func TestBuildTarGzFromFS_ContentReadable(t *testing.T) { } } +func TestBuildTarGzFromFS_AmpelFS(t *testing.T) { + data := buildTarGzFromFS(ampelComplypackData, "testdata/ampel-complypack") + + gr, err := gzip.NewReader(bytes.NewReader(data)) + require.NoError(t, err) + defer gr.Close() + + tr := tar.NewReader(gr) + files := make(map[string]bool) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + require.NoError(t, err) + files[hdr.Name] = true + assert.Equal(t, int64(0o644), hdr.Mode) + assert.Greater(t, hdr.Size, int64(0)) + + // Verify each file is valid JSON with a non-empty "id" field. + content := make([]byte, hdr.Size) + _, readErr := io.ReadFull(tr, content) + require.NoError(t, readErr) + + var policy struct { + ID string `json:"id"` + } + require.NoError(t, json.Unmarshal(content, &policy), + "file %s should be valid JSON", hdr.Name) + assert.NotEmpty(t, policy.ID, + "file %s should have a non-empty id field", hdr.Name) + } + + assert.Contains(t, files, "block-force-push.json") + assert.Len(t, files, 1, "expected exactly 1 file in tar archive") +} + func TestBuildDummyTarGz_SingleFile(t *testing.T) { content := []byte(`{"test": true}`) data := buildDummyTarGz("test.json", content) @@ -115,6 +153,54 @@ func TestSeedDefaults_AllReposSeeded(t *testing.T) { require.NotNil(t, complypackRepo) _, hasLatest = complypackRepo.tags["latest"] assert.True(t, hasLatest, "OPA complypack should have 'latest' tag") + + // Verify ampel complypack has expected tags and valid content + ampelRepo := store.repos["complypacks/ampel-bp"] + require.NotNil(t, ampelRepo) + _, hasLatest = ampelRepo.tags["latest"] + assert.True(t, hasLatest, "ampel complypack should have 'latest' tag") + _, hasV1 = ampelRepo.tags["v1.0.0"] + assert.True(t, hasV1, "ampel complypack should have 'v1.0.0' tag") + + // Verify the ampel complypack content blob contains valid granular policy JSON. + ampelArt := ampelRepo.tags["latest"] + require.NotNil(t, ampelArt) + var ampelManifest ociManifest + require.NoError(t, json.Unmarshal(ampelArt.manifestBytes, &elManifest)) + require.NotEmpty(t, ampelManifest.Layers, "ampel complypack should have at least one layer") + + contentDigest := ampelManifest.Layers[0].Digest + contentBlob, ok := ampelRepo.blobs[contentDigest] + require.True(t, ok, "content blob should exist for digest %s", contentDigest) + + gr, err := gzip.NewReader(bytes.NewReader(contentBlob.data)) + require.NoError(t, err) + defer gr.Close() + + tr := tar.NewReader(gr) + fileCount := 0 + for { + hdr, tarErr := tr.Next() + if tarErr == io.EOF { + break + } + require.NoError(t, tarErr) + fileCount++ + + data := make([]byte, hdr.Size) + _, readErr := io.ReadFull(tr, data) + require.NoError(t, readErr) + + var policy struct { + ID string `json:"id"` + } + require.NoError(t, json.Unmarshal(data, &policy), + "ampel complypack file %s should be valid JSON", hdr.Name) + assert.NotEmpty(t, policy.ID, + "ampel complypack file %s should have a non-empty id field", hdr.Name) + } + assert.GreaterOrEqual(t, fileCount, 1, + "ampel complypack should contain at least one file") } func TestResolveContentDir_Default(t *testing.T) { diff --git a/cmd/mock-oci-registry/testdata/ampel-complypack/block-force-push.json b/cmd/mock-oci-registry/testdata/ampel-complypack/block-force-push.json new file mode 100644 index 00000000..fe521a09 --- /dev/null +++ b/cmd/mock-oci-registry/testdata/ampel-complypack/block-force-push.json @@ -0,0 +1,27 @@ +{ + "id": "block-force-push", + "meta": { + "description": "Validate force push blocking is enabled on protected branches via the GitHub API", + "controls": [ + { "framework": "test-branch-protection", "class": "source-code", "id": "force-push-protection" } + ] + }, + "tenets": [ + { + "id": "01", + "code": "(has(predicates[0].data.values) && type(predicates[0].data.values) == list && predicates[0].data.values.exists(rule, rule.type == \"non_fast_forward\")) || (has(predicates[0].data.values) && type(predicates[0].data.values) != list && has(predicates[0].data.values.allow_force_push) && predicates[0].data.values.allow_force_push == false)", + "predicates": { + "types": [ + "http://github.com/carabiner-dev/snappy/specs/github/branch-rules.yaml" + ] + }, + "assessment": { + "message": "Force pushing is disabled in protected branch." + }, + "error": { + "message": "Force pushes can be sent to protected branch", + "guidance": "Create a branch ruleset and enable 'Block force pushes' in the GitHub repository settings." + } + } + ] +} diff --git a/openspec/changes/ampel-complypack-content/.openspec.yaml b/openspec/changes/ampel-complypack-content/.openspec.yaml new file mode 100644 index 00000000..f617bd18 --- /dev/null +++ b/openspec/changes/ampel-complypack-content/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-04 diff --git a/openspec/changes/ampel-complypack-content/design.md b/openspec/changes/ampel-complypack-content/design.md new file mode 100644 index 00000000..7d0d54fc --- /dev/null +++ b/openspec/changes/ampel-complypack-content/design.md @@ -0,0 +1,93 @@ +## Context + +The mock OCI registry (`cmd/mock-oci-registry/main.go`) seeds test +content for integration testing. It currently seeds the ampel complypack +(`complypacks/ampel-bp`) with a dummy payload via `buildDummyTarGz()`: + +```go +content: buildDummyTarGz("policy.json", + []byte(`{"name":"ampel-branch-protection","version":"1.0.0"}`)) +``` + +This dummy JSON lacks the `id` field that `LoadGranularPolicies()` +requires. The OPA complypack was already migrated to the +`buildTarGzFromFS()` pattern in commit `74fbae8`, where real policy +files are embedded from `testdata/opa-complypack/` via `//go:embed`. + +The cross-repo integration test script (`cross_repo_integration_test.sh`) +currently works around the dummy content by manually copying +`block-force-push.json` into the workspace default directory. With the +ampel provider now consuming `ComplypackContentPath`, this workaround +is insufficient — the complypack content takes precedence. + +## Goals / Non-Goals + +**Goals:** +- Seed the ampel complypack with valid granular policy content that the + provider accepts +- Follow the established OPA complypack pattern (embed + `buildTarGzFromFS`) +- Remove the manual granular policy pre-staging from the integration test +- Maintain backward compatibility with the pre-PR#52 ampel provider + +**Non-Goals:** +- Adding new ampel policies or expanding test coverage beyond what exists +- Modifying the ampel provider code (that is `complytime-providers` PR #52) +- Changing the complypack OCI artifact format or media types +- Adding OPA-side changes (already handled by `opa-devcontainer-content`) + +## Decisions + +### D1: Reuse existing `block-force-push.json` content + +**Decision**: Copy the content from +`tests/cross-repo/testdata/granular-policies/block-force-push.json` +into `cmd/mock-oci-registry/testdata/ampel-complypack/block-force-push.json`. + +**Rationale**: This is the same fixture the integration test already +validates against. Using identical content ensures the test assertions +(e.g., checking for `block-force-push` policy ID in results) continue +to pass without modification. + +**Alternatives considered**: +- Create minimal stub content: Rejected — would require updating test + assertions and diverges from the real policy format the test validates. + +### D2: Remove pre-staged granular policies from integration test + +**Decision**: Remove the `mkdir -p` and `cp` lines that pre-stage +`block-force-push.json` into `.complytime/ampel/granular-policies/` in +`cross_repo_integration_test.sh`. + +**Rationale**: With `ComplypackContentPath` taking precedence in PR #52's +provider, the pre-staged directory is never consulted. Keeping it creates +a false safety net — the test would pass even if complypack delivery +broke, defeating the purpose of the integration test. + +**Alternatives considered**: +- Keep pre-staged content as fallback: Rejected — masks complypack + delivery failures and makes the test less meaningful. + +### D3: Add `//go:embed` directive alongside the existing OPA one + +**Decision**: Add a new `//go:embed testdata/ampel-complypack/*` var +declaration directly below the existing OPA embed directive. + +**Rationale**: Follows the established pattern. Each complypack provider +gets its own embedded filesystem variable and testdata subdirectory. + +## Risks / Trade-offs + +- **[Risk] Merge ordering**: If this PR merges before + `complytime-providers` PR #52, the cross-repo CI will build the old + ampel provider from `main`, which ignores `ComplypackContentPath` + and falls back to the default directory. Since we're removing the + pre-staged content, the test would fail. + → **Mitigation**: Keep the pre-staged content removal as a separate, + final task. If needed, split the change: merge the complypack content + seeding first (backward-compatible), then remove pre-staging after + PR #52 lands on `complytime-providers` main. + +- **[Risk] Content drift**: The embedded `block-force-push.json` could + diverge from the provider's expected format over time. + → **Mitigation**: This is test content for integration validation — + if the format changes, the integration test will catch it (by design). diff --git a/openspec/changes/ampel-complypack-content/proposal.md b/openspec/changes/ampel-complypack-content/proposal.md new file mode 100644 index 00000000..e6cd875d --- /dev/null +++ b/openspec/changes/ampel-complypack-content/proposal.md @@ -0,0 +1,54 @@ +## Why + +The mock OCI registry seeds the `complypacks/ampel-bp` artifact with a +dummy `policy.json` containing `{"name":"...","version":"..."}` — a +placeholder that lacks the `id` field required by the ampel provider's +`LoadGranularPolicies()`. Before `complytime-providers` PR #52, the ampel +provider ignored `ComplypackContentPath` and fell back to pre-staged +granular policies, so the dummy content was never parsed. Now that the +provider consumes complypack content, the cross-repo integration test +fails because the dummy payload is rejected. This change replaces the +dummy content with a valid ampel granular policy, mirroring the pattern +already established for the OPA complypack (commit `74fbae8`). + +## What Changes + +- Replace the dummy `buildDummyTarGz("policy.json", ...)` call in + `seedDefaults()` with `buildTarGzFromFS()` using embedded ampel + complypack test content +- Add `testdata/ampel-complypack/block-force-push.json` containing a + valid `AmpelPolicy` structure (matching the existing cross-repo test + fixture at `tests/cross-repo/testdata/granular-policies/`) +- Add `//go:embed testdata/ampel-complypack/*` directive to embed the + ampel complypack content +- Update `TestSeedDefaults_AllReposSeeded` to verify the ampel + complypack contains the expected file count and content structure +- Remove the now-unused `block-force-push.json` from + `tests/cross-repo/testdata/granular-policies/` and the corresponding + `cp` / `mkdir` lines from `cross_repo_integration_test.sh`, since + the provider will consume complypack content directly instead of the + pre-staged fallback directory + +## Capabilities + +### New Capabilities +- `ampel-complypack-seed`: Embedded ampel complypack test content in the + mock OCI registry, producing a valid tar.gz payload that the ampel + provider's `LoadGranularPolicies()` accepts + +### Modified Capabilities + +## Impact + +- `cmd/mock-oci-registry/main.go`: New embed directive, updated + `seedDefaults()` call for `complypacks/ampel-bp` +- `cmd/mock-oci-registry/testdata/ampel-complypack/`: New directory with + valid granular policy JSON +- `cmd/mock-oci-registry/main_test.go`: Updated test assertions for + ampel complypack content +- `tests/cross-repo/testdata/granular-policies/`: Removed (content + migrated to embedded complypack) +- `tests/cross-repo/cross_repo_integration_test.sh`: Simplified setup + (no manual granular policy staging) +- Backward-compatible: the old ampel provider ignores + `ComplypackContentPath` and is unaffected by serving valid content diff --git a/openspec/changes/ampel-complypack-content/specs/ampel-complypack-seed/spec.md b/openspec/changes/ampel-complypack-content/specs/ampel-complypack-seed/spec.md new file mode 100644 index 00000000..b86a7b71 --- /dev/null +++ b/openspec/changes/ampel-complypack-content/specs/ampel-complypack-seed/spec.md @@ -0,0 +1,62 @@ +## ADDED Requirements + +### Requirement: Valid ampel complypack content in mock registry +The mock OCI registry SHALL seed the `complypacks/ampel-bp` artifact +with a tar.gz payload containing valid ampel granular policy JSON that +the ampel provider's `LoadGranularPolicies()` accepts without error. +Each embedded JSON file SHALL contain an `id` field, a `meta` object +with `description` and `controls`, and a `tenets` array with at least +one entry. + +#### Scenario: Mock registry serves valid ampel complypack +- **WHEN** the mock registry starts with default seed data +- **THEN** the `complypacks/ampel-bp` artifact SHALL contain a tar.gz + layer with at least one `.json` file that conforms to the `AmpelPolicy` + schema (non-empty `id`, `meta.controls`, and `tenets` fields) + +#### Scenario: Ampel provider parses complypack content without error +- **WHEN** the ampel provider's `Generate()` receives a + `ComplypackContentPath` pointing to the extracted complypack content +- **THEN** `LoadGranularPolicies()` SHALL parse all JSON files + successfully and return a non-empty policy map + +### Requirement: Ampel complypack embedded via filesystem +The ampel complypack content SHALL be embedded using `//go:embed` and +`buildTarGzFromFS()`, matching the pattern established by the OPA +complypack. The content SHALL NOT use `buildDummyTarGz()` with inline +string literals. + +#### Scenario: Embedded testdata directory structure +- **WHEN** the mock registry binary is compiled +- **THEN** the `testdata/ampel-complypack/` directory SHALL be embedded + via `//go:embed` and its contents packaged by `buildTarGzFromFS()` + +### Requirement: Cross-repo integration test uses complypack flow +The cross-repo integration test SHALL exercise the complypack content +path for the ampel provider. Manual pre-staging of granular policy files +into the workspace default directory SHALL be removed so the test +validates the complypack-delivered content path. + +#### Scenario: Integration test passes without pre-staged granular policies +- **WHEN** the cross-repo integration test runs `complyctl generate` +- **THEN** the ampel provider SHALL load granular policies from the + complypack content path (delivered by `complyctl get`) instead of from + a manually pre-staged `.complytime/ampel/granular-policies/` directory + +#### Scenario: Backward compatibility with old provider +- **WHEN** the mock registry serves valid ampel complypack content +- **AND** the ampel provider binary does NOT support `ComplypackContentPath` + (pre-PR#52 version) +- **THEN** the provider SHALL ignore the complypack content and fall back + to the pre-staged granular policies directory without error + +### Requirement: Unit test coverage for ampel complypack seeding +The mock registry's unit tests SHALL verify that the ampel complypack +artifact is seeded with valid, readable content containing the expected +files. + +#### Scenario: TestSeedDefaults verifies ampel complypack content +- **WHEN** `TestSeedDefaults_AllReposSeeded` runs +- **THEN** it SHALL verify that `complypacks/ampel-bp` contains a content + blob that decompresses to at least one `.json` file with a non-empty + `id` field diff --git a/openspec/changes/ampel-complypack-content/tasks.md b/openspec/changes/ampel-complypack-content/tasks.md new file mode 100644 index 00000000..b52149bd --- /dev/null +++ b/openspec/changes/ampel-complypack-content/tasks.md @@ -0,0 +1,21 @@ +## 1. Ampel Complypack Test Content + +- [x] 1.1 [P] Create `cmd/mock-oci-registry/testdata/ampel-complypack/block-force-push.json` with valid `AmpelPolicy` content (copy from `tests/cross-repo/testdata/granular-policies/block-force-push.json`) +- [x] 1.2 Add `//go:embed testdata/ampel-complypack/*` directive and `ampelComplypackData` variable to `cmd/mock-oci-registry/main.go`, below the existing OPA embed directive +- [x] 1.3 Update `seedDefaults()` in `cmd/mock-oci-registry/main.go` to replace `buildDummyTarGz("policy.json", ...)` with `buildTarGzFromFS(ampelComplypackData, "testdata/ampel-complypack")` for the `complypacks/ampel-bp` artifact + +## 2. Unit Tests + +- [x] 2.1 Add `TestBuildTarGzFromFS_AmpelFS` test in `cmd/mock-oci-registry/main_test.go` to verify the ampel complypack archive contains `block-force-push.json` with valid content +- [x] 2.2 Update `TestSeedDefaults_AllReposSeeded` to verify the ampel complypack content blob decompresses to at least one `.json` file with a non-empty `id` field + +## 3. Integration Test Cleanup + +- [ ] 3.1 DEFERRED (merge ordering) Remove the `mkdir -p "${WORK_DIR}/.complytime/ampel/granular-policies"` and `cp` lines from `tests/cross-repo/cross_repo_integration_test.sh` (lines 176-179) +- [ ] 3.2 DEFERRED (merge ordering) Remove `tests/cross-repo/testdata/granular-policies/block-force-push.json` and its parent directory (content now embedded in mock registry) + +## 4. Validation + +- [x] 4.1 Verify `make build` compiles with the new embedded testdata +- [x] 4.2 Verify `make test-unit` passes (mock registry tests) +- [x] 4.3 Verify `make lint` passes with zero issues