diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 73c27ec..8a442d2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,14 +8,22 @@ # - go-version-file: go.mod instead of hardcoded version # - Removed Force Remote Dependency step — replace directives must not be # committed to go.mod; local replace is development-only -# - Removed -mod=vendor — controller fetches deps via SSH + GOPRIVATE at +# - Removed -mod=vendor — controller fetches deps via HTTPS + GOPRIVATE at # build time; vendor directory is not committed in this repo # - golangci-lint replaces staticcheck — consistent with library CI # - go vet added before lint # - GONOSUMDB + GOPRIVATE + GOPROXY=direct set consistently # +# Auth: +# Private-module git config (environments, environments-api, +# environments-contract) uses the BlanketOps-Environments GitHub App (via +# actions/create-github-app-token) instead of a personal-account PAT — +# installation tokens are minted fresh per job and expire in an hour, +# instead of a PAT's expiry silently lapsing. +# # Secrets required: -# GH_PAT — private module access +# APP_ID, APP_PRIVATE_KEY — GitHub App credentials, exchanged for a +# short-lived installation token per job (private-module access only) # ============================================================================= name: CI @@ -36,6 +44,15 @@ jobs: build: runs-on: ubuntu-latest steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: blanketops + repositories: environments,environments-api,environments-contract + - name: Checkout uses: actions/checkout@v4 @@ -47,7 +64,7 @@ jobs: - name: Configure Git for private modules run: | - git config --global url."https://${{ secrets.GH_PAT }}@github.com/".insteadOf "https://github.com/" + git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" go env -w GOPRIVATE=github.com/blanketops/* go env -w GONOSUMDB=github.com/blanketops/* go env -w GOPROXY=direct @@ -58,6 +75,18 @@ jobs: - name: Vet run: go vet ./... + - name: Test + run: go test ./... -coverprofile=coverage.out -covermode=atomic + + - name: Coverage summary + if: always() + run: | + TOTAL=$(go tool cover -func=coverage.out 2>/dev/null | grep "^total" | awk '{print $3}' || echo "N/A") + cat >> "$GITHUB_STEP_SUMMARY" < 0)' gosec-report.json >/dev/null 2>&1; then - echo "| Severity | Confidence | Rule | Location | Details |" - echo "|---|---|---|---|---|" - jq -r '.Issues[] | "| \(.severity) | \(.confidence) | \(.rule_id) | \(.file):\(.line) | \(.details | gsub("\\|"; "\\\\|")) |"' gosec-report.json - else - echo "No findings." - fi - echo - echo "
Raw report (JSON)" - echo - echo '```json' - head -c 60000 gosec-report.json 2>/dev/null - echo '```' - echo "
" - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/ko.yml b/.github/workflows/ko.yml index 64980c9..7c680a4 100644 --- a/.github/workflows/ko.yml +++ b/.github/workflows/ko.yml @@ -20,8 +20,20 @@ # tag push → {version} + latest # manual → {ref_name} only # +# Auth: +# Private-module git config uses the BlanketOps-Environments GitHub App +# (via actions/create-github-app-token) instead of a personal-account PAT +# — installation tokens are minted fresh per job and expire in an hour, +# instead of a PAT's expiry silently lapsing. +# +# GHCR login uses GITHUB_TOKEN, not the App token — the App installation +# token was denied "Write organization package" against this GHCR package +# regardless of what permission was granted. GITHUB_TOKEN is scoped to +# this repo only, which is exactly what a same-repo package push needs. +# # Secrets required: -# GH_PAT — GHCR push + private module git access (HTTPS) +# APP_ID, APP_PRIVATE_KEY — GitHub App credentials, exchanged for a +# short-lived installation token per job (private-module access only) # ============================================================================= name: Ko Image @@ -51,6 +63,15 @@ jobs: build-and-publish: runs-on: ubuntu-latest steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: blanketops + repositories: environments,environments-api,environments-contract + - uses: actions/checkout@v4 - name: Setup Go @@ -61,7 +82,7 @@ jobs: - name: Configure git for private modules run: | - git config --global url."https://${{ secrets.GH_PAT }}@github.com/".insteadOf "https://github.com/" + git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" go env -w GOPRIVATE=github.com/blanketops/* go env -w GONOSUMDB=github.com/blanketops/* go env -w GOPROXY=direct @@ -81,10 +102,10 @@ jobs: - name: Login to GHCR run: | - echo "${{ secrets.GH_PAT }}" | ko login ghcr.io \ + echo "${{ secrets.GITHUB_TOKEN }}" | ko login ghcr.io \ --username "${{ github.actor }}" \ --password-stdin - echo "${{ secrets.GH_PAT }}" | oras login ghcr.io \ + echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io \ --username "${{ github.actor }}" \ --password-stdin diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 162a5fa..a093a58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,7 +53,7 @@ jobs: with: registry: ghcr.io username: ${{ github.actor }} - password: ${{ secrets.GH_PAT }} + password: ${{ secrets.GITHUB_TOKEN }} # ko.yml builds and pushes this same tag concurrently (both fire on # the same tag push) — poll rather than assume it's already there. @@ -93,7 +93,7 @@ jobs: with: body_path: RELEASE_NOTES.md env: - GITHUB_TOKEN: ${{ secrets.GH_PAT }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Release Summary run: | diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..d37afe5 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,268 @@ +# ============================================================================= +# security.yml — Security & Vulnerability Scanning +# +# Three jobs (merges the former gosec.yml and vulnscan.yml), each keeping its +# original trigger scope via a job-level `if:` gate against the combined +# top-level trigger: +# +# gosec — Go static analysis (hardcoded creds, SQL injection, unsafe +# crypto, etc.). Was: push to main + pull_request. +# govulncheck — Go module + stdlib reachable-vulnerability scan. Was: push +# to main + pull_request + weekly Monday 06:00 UTC. +# trivy-image — OS + library CVE scan of the published GHCR image. Only +# meaningful once an image exists, so it runs on tag push +# (same trigger as ko.yml) + the weekly schedule, scanning +# :latest on the scheduled run. +# +# All three are report-only — none fail the build. This repo is private, so +# SARIF upload to the Security tab would need GitHub Advanced Security; +# findings render into each job's summary instead. Once the repo goes +# public, wire up SARIF upload for gosec/govulncheck to get free +# Security-tab integration. +# +# Auth: +# Private-module git config (gosec, govulncheck) uses the +# BlanketOps-Environments GitHub App (via actions/create-github-app-token) +# instead of a personal-account PAT — installation tokens are minted fresh +# per job and expire in an hour, instead of a PAT's expiry silently +# lapsing. +# +# trivy-image's GHCR login uses GITHUB_TOKEN, not the App token — the App +# installation token was denied package pulls against this org-scoped +# package regardless of what permission was granted; GITHUB_TOKEN is +# scoped to this repo, which is exactly what a same-repo pull needs. +# +# Secrets required: +# APP_ID, APP_PRIVATE_KEY — GitHub App credentials, exchanged for a +# short-lived installation token per job (private-module access only) +# ============================================================================= + +name: Security + +on: + pull_request: + push: + branches: + - main + tags: + - "v*" + schedule: + - cron: "0 6 * * 1" # govulncheck + trivy-image (:latest) — Monday 06:00 UTC + +permissions: + contents: read + packages: read + +env: + GO_VERSION_FILE: go.mod + IMAGE_REPO: ghcr.io/blanketops/environments-controller + +jobs: + + # ─────────────────────────────────────────────────────────────────────────── + # Job 1: gosec (from gosec.yml) + # Original scope: push to main, pull_request (any). + # ─────────────────────────────────────────────────────────────────────────── + gosec: + name: Gosec + runs-on: ubuntu-latest + if: > + github.event_name == 'pull_request' || + (github.event_name == 'push' && github.ref == 'refs/heads/main') + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: blanketops + repositories: environments,environments-api,environments-contract + + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: ${{ env.GO_VERSION_FILE }} + cache: true + + - name: Configure Git for private modules + run: | + git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" + go env -w GOPRIVATE=github.com/blanketops/* + go env -w GONOSUMDB=github.com/blanketops/* + go env -w GOPROXY=direct + + - name: Install gosec + run: go install github.com/securego/gosec/v2/cmd/gosec@latest + + - name: Run gosec + id: gosec + continue-on-error: true + run: gosec -fmt=json -out=gosec-report.json ./... || true + + - name: Gosec Summary + if: always() + run: | + { + echo "# 🔒 Gosec Static Analysis" + echo + echo "**Branch:** \`${{ github.ref_name }}\`" + echo "**Trigger:** \`${{ github.event_name }}\`" + echo + echo "Report-only — this job does not fail the build." + echo + if jq -e '.Issues and (.Issues | length > 0)' gosec-report.json >/dev/null 2>&1; then + echo "| Severity | Confidence | Rule | Location | Details |" + echo "|---|---|---|---|---|" + jq -r '.Issues[] | "| \(.severity) | \(.confidence) | \(.rule_id) | \(.file):\(.line) | \(.details | gsub("\\|"; "\\\\|")) |"' gosec-report.json + else + echo "No findings." + fi + echo + echo "
Raw report (JSON)" + echo + echo '```json' + head -c 60000 gosec-report.json 2>/dev/null + echo '```' + echo "
" + } >> "$GITHUB_STEP_SUMMARY" + + # ─────────────────────────────────────────────────────────────────────────── + # Job 2: govulncheck (from vulnscan.yml) + # Original scope: push to main, pull_request (any), weekly Monday 06:00 UTC. + # ─────────────────────────────────────────────────────────────────────────── + govulncheck: + name: govulncheck + runs-on: ubuntu-latest + if: > + github.event_name == 'pull_request' || + (github.event_name == 'push' && github.ref == 'refs/heads/main') || + github.event_name == 'schedule' + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: blanketops + repositories: environments,environments-api,environments-contract + + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: ${{ env.GO_VERSION_FILE }} + cache: true + + - name: Configure Git for private modules + run: | + git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/" + go env -w GOPRIVATE=github.com/blanketops/* + go env -w GONOSUMDB=github.com/blanketops/* + go env -w GOPROXY=direct + + - name: Run govulncheck + id: govulncheck + continue-on-error: true + run: | + go run golang.org/x/vuln/cmd/govulncheck@latest -json ./... > govulncheck-report.json 2>govulncheck-stderr.txt || true + + - name: Govulncheck Summary + if: always() + run: | + { + echo "# 🛡️ govulncheck" + echo + echo "**Branch:** \`${{ github.ref_name }}\`" + echo "**Trigger:** \`${{ github.event_name }}\`" + echo + echo "Report-only — this job does not fail the build." + echo + COUNT=$(jq -s '[.[] | select(.finding != null)] | length' govulncheck-report.json 2>/dev/null || echo 0) + if [[ "$COUNT" -gt 0 ]]; then + echo "| OSV ID | Module | Version | Fixed | Function |" + echo "|---|---|---|---|---|" + jq -s -r '.[] | select(.finding != null) | .finding | + { + osv: .osv, + module: ([.trace[]? | select(.module) | .module] | first // "-"), + version: ([.trace[]? | select(.version) | .version] | first // "-"), + fixed: (.fixed_version // "-"), + function: ([.trace[]? | select(.function) | .function] | first // "-") + } | "| \(.osv) | \(.module) | \(.version) | \(.fixed) | \(.function) |"' \ + govulncheck-report.json | sort -u + else + echo "No known vulnerabilities found." + fi + echo + echo "
Raw report (JSON)" + echo + echo '```json' + head -c 60000 govulncheck-report.json 2>/dev/null + echo '```' + echo "
" + } >> "$GITHUB_STEP_SUMMARY" + + # ─────────────────────────────────────────────────────────────────────────── + # Job 3: trivy-image (from vulnscan.yml) + # Original scope: weekly schedule (scanning :latest), or tag push. + # ─────────────────────────────────────────────────────────────────────────── + trivy-image: + name: Trivy Image Scan + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/v') + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Compute image ref + id: image + run: | + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + echo "ref=${{ env.IMAGE_REPO }}:${{ github.ref_name }}" >> "$GITHUB_OUTPUT" + else + echo "ref=${{ env.IMAGE_REPO }}:latest" >> "$GITHUB_OUTPUT" + fi + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Trivy image scan + id: trivy + uses: aquasecurity/trivy-action@v0.36.0 + continue-on-error: true + with: + image-ref: ${{ steps.image.outputs.ref }} + format: template + template: "@/contrib/markdown.tpl" + severity: CRITICAL,HIGH,MEDIUM + exit-code: "0" + output: trivy-report.md + + - name: Trivy Summary + if: always() + run: | + { + echo "# 🐳 Trivy Image Scan" + echo + echo "**Image:** \`${{ steps.image.outputs.ref }}\`" + echo "**Trigger:** \`${{ github.event_name }}\`" + echo + echo "Report-only — this job does not fail the build." + echo + if [[ -s trivy-report.md ]]; then + head -c 100000 trivy-report.md + else + echo "No findings." + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/vulnscan.yml b/.github/workflows/vulnscan.yml deleted file mode 100644 index b2ebae5..0000000 --- a/.github/workflows/vulnscan.yml +++ /dev/null @@ -1,155 +0,0 @@ -# ============================================================================= -# vulnscan.yml — Vulnerability Scanning -# -# Two independent jobs: -# govulncheck — Go module + stdlib reachable-vulnerability scan. Runs on -# push to main, PRs, and a weekly schedule (CVE databases -# change even when the code doesn't). -# trivy-image — OS + library CVE scan of the published GHCR image (the -# ko.yml build). Only meaningful once an image exists, so it -# runs on tag push (same trigger as ko.yml) and the weekly -# schedule, scanning :latest on the scheduled run. -# -# Report-only for now: this repo is private, so SARIF upload to the Security -# tab would need GitHub Advanced Security. Findings surface via job logs and -# the step summary. Once the repo goes public, wire up SARIF upload for both -# jobs to get free Security-tab integration. -# -# Secrets required: -# GH_PAT — private module access (govulncheck) + GHCR pull (trivy-image) -# ============================================================================= - -name: Vulnerability Scan - -on: - pull_request: - push: - branches: - - main - tags: - - "v*" - schedule: - - cron: "0 6 * * 1" # every Monday 06:00 UTC - -permissions: - contents: read - packages: read - -env: - GO_VERSION_FILE: go.mod - IMAGE_REPO: ghcr.io/blanketops/environments-controller - -jobs: - govulncheck: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version-file: ${{ env.GO_VERSION_FILE }} - cache: true - - - name: Configure Git for private modules - run: | - git config --global url."https://${{ secrets.GH_PAT }}@github.com/".insteadOf "https://github.com/" - go env -w GOPRIVATE=github.com/blanketops/* - go env -w GONOSUMDB=github.com/blanketops/* - go env -w GOPROXY=direct - - - name: Run govulncheck - id: govulncheck - continue-on-error: true - run: | - go run golang.org/x/vuln/cmd/govulncheck@latest -json ./... > govulncheck-report.json 2>govulncheck-stderr.txt || true - - - name: Govulncheck Summary - if: always() - run: | - { - echo "# 🛡️ govulncheck" - echo - echo "**Branch:** \`${{ github.ref_name }}\`" - echo "**Trigger:** \`${{ github.event_name }}\`" - echo - echo "Report-only — this job does not fail the build." - echo - COUNT=$(jq -s '[.[] | select(.finding != null)] | length' govulncheck-report.json 2>/dev/null || echo 0) - if [[ "$COUNT" -gt 0 ]]; then - echo "| OSV ID | Module | Version | Fixed | Function |" - echo "|---|---|---|---|---|" - jq -s -r '.[] | select(.finding != null) | .finding | - { - osv: .osv, - module: ([.trace[]? | select(.module) | .module] | first // "-"), - version: ([.trace[]? | select(.version) | .version] | first // "-"), - fixed: (.fixed_version // "-"), - function: ([.trace[]? | select(.function) | .function] | first // "-") - } | "| \(.osv) | \(.module) | \(.version) | \(.fixed) | \(.function) |"' \ - govulncheck-report.json | sort -u - else - echo "No known vulnerabilities found." - fi - echo - echo "
Raw report (JSON)" - echo - echo '```json' - head -c 60000 govulncheck-report.json 2>/dev/null - echo '```' - echo "
" - } >> "$GITHUB_STEP_SUMMARY" - - trivy-image: - if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Compute image ref - id: image - run: | - if [[ "${{ github.ref }}" == refs/tags/* ]]; then - echo "ref=${{ env.IMAGE_REPO }}:${{ github.ref_name }}" >> "$GITHUB_OUTPUT" - else - echo "ref=${{ env.IMAGE_REPO }}:latest" >> "$GITHUB_OUTPUT" - fi - - - name: Login to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GH_PAT }} - - - name: Run Trivy image scan - id: trivy - uses: aquasecurity/trivy-action@v0.36.0 - continue-on-error: true - with: - image-ref: ${{ steps.image.outputs.ref }} - format: template - template: "@/contrib/markdown.tpl" - severity: CRITICAL,HIGH,MEDIUM - exit-code: "0" - output: trivy-report.md - - - name: Trivy Summary - if: always() - run: | - { - echo "# 🐳 Trivy Image Scan" - echo - echo "**Image:** \`${{ steps.image.outputs.ref }}\`" - echo "**Trigger:** \`${{ github.event_name }}\`" - echo - echo "Report-only — this job does not fail the build." - echo - if [[ -s trivy-report.md ]]; then - head -c 100000 trivy-report.md - else - echo "No findings." - fi - } >> "$GITHUB_STEP_SUMMARY" diff --git a/internal/controller/observers/build/build.go b/internal/controller/observers/build/build.go index 4292e45..c565f2f 100644 --- a/internal/controller/observers/build/build.go +++ b/internal/controller/observers/build/build.go @@ -127,20 +127,20 @@ func (r *Reconciler) applyTriggers(ctx context.Context, build *buildv1.Build, re // Deliberately no client.InNamespace(...) — GitHubEvents live in a // different namespace (argo-events) than Build CRs. Correlate by label // only. See package doc. - var events eventsv1alpha1.GitHubEventList - if err := r.List(ctx, &events, + var githubEvents eventsv1alpha1.GitHubEventList + if err := r.List(ctx, &githubEvents, client.MatchingLabels{"environments.blanketops.dev/name": appName}, ); err != nil { return err } - log.Info("trigger scan: candidates found", "appName", appName, "count", len(events.Items)) + log.Info("trigger scan: candidates found", "appName", appName, "count", len(githubEvents.Items)) var latestEvent *eventsv1alpha1.GitHubEvent var latestResolved *githubeventresolution.ResolvedGitHubEvent - for i := range events.Items { - ev := &events.Items[i] + for i := range githubEvents.Items { + ev := &githubEvents.Items[i] ghResolved, err := githubeventresolution.ResolveGitHubEvent(ev) if err != nil { log.Info("trigger scan: resolve failed", "event", ev.Name, "error", err.Error()) diff --git a/internal/domains/build/build.go b/internal/domains/build/build.go index 3fbbd1b..d0e3c8b 100644 --- a/internal/domains/build/build.go +++ b/internal/domains/build/build.go @@ -66,12 +66,12 @@ type BuildDomain struct { } // New returns a new BuildDomain instance configured with the necessary dependencies. -func New(buildMediator *build.Mediator, buildService *application.BuildService, cache *cache.Cache, events *events.EventRecorder, log logr.Logger) *BuildDomain { +func New(buildMediator *build.Mediator, buildService *application.BuildService, domainCache *cache.Cache, eventRecorder *events.EventRecorder, log logr.Logger) *BuildDomain { return &BuildDomain{ buildMediator: buildMediator, buildService: buildService, - buildCache: libbuild.NewBuildCache(cache), - events: events, + buildCache: libbuild.NewBuildCache(domainCache), + events: eventRecorder, log: log, } } diff --git a/internal/domains/build/build_test.go b/internal/domains/build/build_test.go index 1cf4d40..93fd6ef 100644 --- a/internal/domains/build/build_test.go +++ b/internal/domains/build/build_test.go @@ -32,6 +32,14 @@ import ( const testAppName = "app-sample" +// Contract map keys repeated across fixtures below — named to satisfy +// goconst rather than to document meaning (the keys are self-explanatory). +const ( + keyImage = "image" + keySource = "source" + keyURL = "url" +) + func newEnvironment() *environmentsv1alpha1.Environment { return &environmentsv1alpha1.Environment{ ObjectMeta: metav1.ObjectMeta{ @@ -72,9 +80,9 @@ func newBuildCR(contract map[string]any) *environmentsv1alpha1.Build { func validBuildContract() map[string]any { return map[string]any{ - "image": "ghcr.io/blanketops/app:latest", - "source": map[string]any{ - "url": "https://github.com/blanketops/app.git", + keyImage: "ghcr.io/blanketops/app:latest", + keySource: map[string]any{ + keyURL: "https://github.com/blanketops/app.git", "cloneSecret": "app-git-ssh", }, "strategy": map[string]any{ @@ -90,7 +98,7 @@ func validBuildContract() map[string]any { // fake client can satisfy (no real Shipwright controller runs BuildRuns to // completion, so success here means "dispatched correctly", not "the image // built"). -func newTestDomain(t *testing.T, objs ...client.Object) (*BuildDomain, client.Client) { +func newTestDomain(t *testing.T, objs ...client.Object) *BuildDomain { t.Helper() c := testsupport.NewFakeClient(objs...) log := logr.Discard() @@ -113,8 +121,7 @@ func newTestDomain(t *testing.T, objs ...client.Object) (*BuildDomain, client.Cl // (ObjectCache.PublishResolved/Invalidate) never touches. Only External // is read, so a real manager would be pure unused ceremony in this test. cache := &corecache.Cache{External: corecache.NoopExternalCache{}} - d := New(med, service, cache, testsupport.NoopRecorder(), log) - return d, c + return New(med, service, cache, testsupport.NoopRecorder(), log) } func conditionStatus(conds []metav1.Condition, condType string) (metav1.ConditionStatus, bool) { @@ -168,8 +175,8 @@ func TestBuildDomain_CanUpdate(t *testing.T) { }{ { name: "spec changed", - oldObj: newBuildCR(map[string]any{"image": "old", "source": map[string]any{"url": "x"}}), - newObj: newBuildCR(map[string]any{"image": "new", "source": map[string]any{"url": "x"}}), + oldObj: newBuildCR(map[string]any{keyImage: "old", keySource: map[string]any{keyURL: "x"}}), + newObj: newBuildCR(map[string]any{keyImage: "new", keySource: map[string]any{keyURL: "x"}}), want: true, }, { @@ -216,7 +223,7 @@ func TestBuildDomain_Handle_InvalidObject(t *testing.T) { func TestBuildDomain_Handle_Create_ResolutionFailure(t *testing.T) { buildCR := newBuildCR(nil) // no contract -> resolution fails - d, _ := newTestDomain(t, buildCR) + d := newTestDomain(t, buildCR) err := d.Handle(context.Background(), command.Command{Type: command.CmdCreate, Obj: buildCR}) if err == nil { @@ -236,7 +243,7 @@ func TestBuildDomain_Handle_Create_MissingEnvironment(t *testing.T) { // Environment referenced by the label doesn't exist in the fake client — // the mediator's EnsurePrerequisites must fail at the query.Lookup gate. buildCR := newBuildCR(validBuildContract()) - d, _ := newTestDomain(t, buildCR) + d := newTestDomain(t, buildCR) err := d.Handle(context.Background(), command.Command{Type: command.CmdCreate, Obj: buildCR}) if err == nil { @@ -260,7 +267,7 @@ func TestBuildDomain_Handle_Create_MissingEnvironment(t *testing.T) { func TestBuildDomain_Handle_Create_PrerequisitesSucceed(t *testing.T) { env := newEnvironment() buildCR := newBuildCR(validBuildContract()) - d, _ := newTestDomain(t, env, buildCR) + d := newTestDomain(t, env, buildCR) // Prerequisites (git SSH secret, registry secret, ServiceAccount) should // all provision successfully against the fake client once the owning @@ -279,7 +286,7 @@ func TestBuildDomain_Handle_Create_PrerequisitesSucceed(t *testing.T) { func TestBuildDomain_Handle_Delete_ResolutionFailure(t *testing.T) { buildCR := newBuildCR(nil) - d, _ := newTestDomain(t, buildCR) + d := newTestDomain(t, buildCR) err := d.Handle(context.Background(), command.Command{Type: command.CmdDelete, Obj: buildCR}) if err == nil { @@ -293,7 +300,7 @@ func TestBuildDomain_Handle_Delete_ResolutionFailure(t *testing.T) { func TestBuildDomain_Handle_Delete_MissingEnvironment(t *testing.T) { buildCR := newBuildCR(validBuildContract()) - d, _ := newTestDomain(t, buildCR) + d := newTestDomain(t, buildCR) err := d.Handle(context.Background(), command.Command{Type: command.CmdDelete, Obj: buildCR}) if err == nil { @@ -308,7 +315,7 @@ func TestBuildDomain_Handle_Delete_MissingEnvironment(t *testing.T) { func TestBuildDomain_Handle_Delete_Succeeds(t *testing.T) { env := newEnvironment() buildCR := newBuildCR(validBuildContract()) - d, _ := newTestDomain(t, env, buildCR) + d := newTestDomain(t, env, buildCR) if err := d.Handle(context.Background(), command.Command{Type: command.CmdDelete, Obj: buildCR}); err != nil { t.Fatalf("Handle() delete = %v, want nil", err) diff --git a/internal/domains/deployment/deployment.go b/internal/domains/deployment/deployment.go index 3a91265..7acb88a 100644 --- a/internal/domains/deployment/deployment.go +++ b/internal/domains/deployment/deployment.go @@ -70,12 +70,12 @@ type DeployDomain struct { } // New returns a new DeployDomain instance configured with the necessary dependencies. -func New(deploymentMediatorIn *deploymentMediator.Mediator, deploymentServiceIn *deployapp.DeploymentService, cache *cache.Cache, reader client.Reader, events *events.EventRecorder, log logr.Logger) *DeployDomain { +func New(deploymentMediatorIn *deploymentMediator.Mediator, deploymentServiceIn *deployapp.DeploymentService, domainCache *cache.Cache, reader client.Reader, eventRecorder *events.EventRecorder, log logr.Logger) *DeployDomain { return &DeployDomain{ deploymentMediator: deploymentMediatorIn, deployService: deploymentServiceIn, - deploymentCache: libdeployment.NewDeploymentCache(cache), - events: events, + deploymentCache: libdeployment.NewDeploymentCache(domainCache), + events: eventRecorder, log: log, reader: reader, } diff --git a/internal/domains/deployment/deployment_test.go b/internal/domains/deployment/deployment_test.go index 53f97c9..74b2b9c 100644 --- a/internal/domains/deployment/deployment_test.go +++ b/internal/domains/deployment/deployment_test.go @@ -30,13 +30,19 @@ import ( const testAppName = "app-sample" +// Repeated across fixtures below — named to satisfy goconst. +const ( + testNamespace = "default" + labelEnvironmentName = "environments.blanketops.dev/name" +) + func newEnvironment() *environmentv1.Environment { return &environmentv1.Environment{ ObjectMeta: metav1.ObjectMeta{ Name: testAppName, - Namespace: "default", + Namespace: testNamespace, Labels: map[string]string{ - "environments.blanketops.dev/name": testAppName, + labelEnvironmentName: testAppName, "environments.blanketops.dev/type": "dev", }, }, @@ -56,9 +62,9 @@ func newDeploymentCR(contract map[string]any) *environmentv1.Deployment { d := &environmentv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-sample", - Namespace: "default", + Namespace: testNamespace, Labels: map[string]string{ - "environments.blanketops.dev/name": testAppName, + labelEnvironmentName: testAppName, }, }, } @@ -87,9 +93,9 @@ func newServiceUnit(name string) *environmentv1.ServiceUnit { return &environmentv1.ServiceUnit{ ObjectMeta: metav1.ObjectMeta{ Name: name, - Namespace: "default", + Namespace: testNamespace, Labels: map[string]string{ - "environments.blanketops.dev/name": testAppName, + labelEnvironmentName: testAppName, }, }, Spec: environmentv1.ServiceUnitSpec{ @@ -108,7 +114,7 @@ func newServiceUnit(name string) *environmentv1.ServiceUnit { // on the domain's own orchestration (resolution, prerequisites, cross-CR // ServiceUnit resolution) rather than re-testing the deployment application // layer's reconciliation-executor/Flux Kustomize machinery. -func newTestDomain(t *testing.T, objs ...client.Object) (*DeployDomain, client.Client) { +func newTestDomain(t *testing.T, objs ...client.Object) *DeployDomain { t.Helper() c := testsupport.NewFakeClient(objs...) log := logr.Discard() @@ -116,8 +122,7 @@ func newTestDomain(t *testing.T, objs ...client.Object) (*DeployDomain, client.C med := deploymentmediator.New(c, testsupport.NewScheme(), log, testsupport.NoopRawRecorder()) cache := &corecache.Cache{External: corecache.NoopExternalCache{}} - d := New(med, nil, cache, c, testsupport.NoopRecorder(), log) - return d, c + return New(med, nil, cache, c, testsupport.NoopRecorder(), log) } func conditionStatus(conds []metav1.Condition, condType string) (metav1.ConditionStatus, bool) { @@ -203,7 +208,7 @@ func TestDeployDomain_Handle_InvalidObject(t *testing.T) { func TestDeployDomain_Handle_Create_ResolutionFailure(t *testing.T) { depl := newDeploymentCR(nil) - d, _ := newTestDomain(t, depl) + d := newTestDomain(t, depl) err := d.Handle(context.Background(), command.Command{Type: command.CmdCreate, Obj: depl}) if err == nil { @@ -216,7 +221,7 @@ func TestDeployDomain_Handle_Create_ResolutionFailure(t *testing.T) { func TestDeployDomain_Handle_Create_MissingEnvironment(t *testing.T) { depl := newDeploymentCR(validDeploymentContract()) - d, _ := newTestDomain(t, depl) + d := newTestDomain(t, depl) err := d.Handle(context.Background(), command.Command{Type: command.CmdCreate, Obj: depl}) if err == nil { @@ -230,7 +235,7 @@ func TestDeployDomain_Handle_Create_MissingEnvironment(t *testing.T) { func TestDeployDomain_Handle_Create_ServiceUnitMissing(t *testing.T) { env := newEnvironment() depl := newDeploymentCR(validDeploymentContract("su-missing")) - d, _ := newTestDomain(t, env, depl) + d := newTestDomain(t, env, depl) err := d.Handle(context.Background(), command.Command{Type: command.CmdCreate, Obj: depl}) if err == nil { @@ -249,7 +254,7 @@ func TestDeployDomain_Handle_Create_Succeeds(t *testing.T) { env := newEnvironment() su := newServiceUnit("su-sample") depl := newDeploymentCR(validDeploymentContract("su-sample")) - d, _ := newTestDomain(t, env, su, depl) + d := newTestDomain(t, env, su, depl) if err := d.Handle(context.Background(), command.Command{Type: command.CmdCreate, Obj: depl}); err != nil { t.Fatalf("Handle() = %v, want nil", err) @@ -265,7 +270,7 @@ func TestDeployDomain_Handle_Create_Succeeds(t *testing.T) { func TestDeployDomain_Handle_Delete_ResolutionFailure(t *testing.T) { depl := newDeploymentCR(nil) - d, _ := newTestDomain(t, depl) + d := newTestDomain(t, depl) err := d.Handle(context.Background(), command.Command{Type: command.CmdDelete, Obj: depl}) if err == nil { @@ -279,7 +284,7 @@ func TestDeployDomain_Handle_Delete_ResolutionFailure(t *testing.T) { func TestDeployDomain_Handle_Delete_Succeeds(t *testing.T) { env := newEnvironment() depl := newDeploymentCR(validDeploymentContract()) - d, _ := newTestDomain(t, env, depl) + d := newTestDomain(t, env, depl) if err := d.Handle(context.Background(), command.Command{Type: command.CmdDelete, Obj: depl}); err != nil { t.Fatalf("Handle() delete = %v, want nil", err) diff --git a/internal/domains/environment/environment.go b/internal/domains/environment/environment.go index 44b0921..ebbec0c 100644 --- a/internal/domains/environment/environment.go +++ b/internal/domains/environment/environment.go @@ -71,15 +71,15 @@ type EnvironmentDomain struct { func New( c client.Client, scheme *runtime.Scheme, - cache *cache.Cache, - events *events.EventRecorder, + domainCache *cache.Cache, + eventRecorder *events.EventRecorder, log logr.Logger, ) *EnvironmentDomain { return &EnvironmentDomain{ client: c, scheme: scheme, - environmentCache: libenvironment.NewEnvironmentCache(cache), - events: events, + environmentCache: libenvironment.NewEnvironmentCache(domainCache), + events: eventRecorder, log: log, } } diff --git a/internal/domains/environment/environment_test.go b/internal/domains/environment/environment_test.go index 70d66e1..5c7eb10 100644 --- a/internal/domains/environment/environment_test.go +++ b/internal/domains/environment/environment_test.go @@ -27,6 +27,9 @@ import ( "github.com/blanketops/environments-controller/internal/testsupport" ) +// Repeated across fixtures below — named to satisfy goconst. +const keyApplicationName = "applicationName" + func newEnvironmentCR(contract map[string]any) *environmentsv1alpha1.Environment { e := &environmentsv1alpha1.Environment{ ObjectMeta: metav1.ObjectMeta{ @@ -42,11 +45,11 @@ func newEnvironmentCR(contract map[string]any) *environmentsv1alpha1.Environment func validEnvironmentContract() map[string]any { return map[string]any{ - "applicationName": "env-sample", - "branch": "main", - "gitOwner": "blanketops", - "environmentType": "dev", - "version": "v1", + keyApplicationName: "env-sample", + "branch": "main", + "gitOwner": "blanketops", + "environmentType": "dev", + "version": "v1", } } @@ -102,8 +105,8 @@ func TestEnvironmentDomain_CanUpdate(t *testing.T) { }{ { name: "spec changed", - oldObj: newEnvironmentCR(map[string]any{"applicationName": "a"}), - newObj: newEnvironmentCR(map[string]any{"applicationName": "b"}), + oldObj: newEnvironmentCR(map[string]any{keyApplicationName: "a"}), + newObj: newEnvironmentCR(map[string]any{keyApplicationName: "b"}), want: true, }, { diff --git a/internal/domains/githubevent/githubevent.go b/internal/domains/githubevent/githubevent.go index c47d928..065136b 100644 --- a/internal/domains/githubevent/githubevent.go +++ b/internal/domains/githubevent/githubevent.go @@ -61,12 +61,12 @@ type GitHubEventDomain struct { } // New constructs a new GitHubEventDomain instance. -func New(githubEventServiceIn *application.GitHubEventService, githubEventMediatorIn *githubEventMediator.Mediator, events *events.EventRecorder, cache *cache.Cache, log logr.Logger) *GitHubEventDomain { +func New(githubEventServiceIn *application.GitHubEventService, githubEventMediatorIn *githubEventMediator.Mediator, eventRecorder *events.EventRecorder, domainCache *cache.Cache, log logr.Logger) *GitHubEventDomain { return &GitHubEventDomain{ githubEventMediator: githubEventMediatorIn, githubEventService: githubEventServiceIn, - githubEventCache: libgithubevent.NewGitHubEventCache(cache), - events: events, + githubEventCache: libgithubevent.NewGitHubEventCache(domainCache), + events: eventRecorder, log: log, } } diff --git a/internal/domains/githubevent/githubevent_test.go b/internal/domains/githubevent/githubevent_test.go index af30be3..d91e796 100644 --- a/internal/domains/githubevent/githubevent_test.go +++ b/internal/domains/githubevent/githubevent_test.go @@ -33,6 +33,13 @@ import ( const testAppName = "app-sample" +// Repeated across fixtures below — named to satisfy goconst. +const ( + keyRepository = "repository" + keyEventType = "eventType" + valPush = "push" +) + func newEnvironment() *environmentsv1alpha1.Environment { return &environmentsv1alpha1.Environment{ ObjectMeta: metav1.ObjectMeta{ @@ -73,9 +80,9 @@ func newGitHubEventCR(contract map[string]any) *eventsv1alpha1.GitHubEvent { func validGitHubEventContract() map[string]any { return map[string]any{ - "repository": "blanketops/app", - "eventType": "push", - "eventId": "delivery-123", + keyRepository: "blanketops/app", + keyEventType: valPush, + "eventId": "delivery-123", "webhook": map[string]any{ "secretRef": map[string]any{ "name": "app-webhook-secret", @@ -146,8 +153,8 @@ func TestGitHubEventDomain_CanUpdate(t *testing.T) { }{ { name: "spec changed", - oldObj: newGitHubEventCR(map[string]any{"repository": "a", "eventType": "push"}), - newObj: newGitHubEventCR(map[string]any{"repository": "b", "eventType": "push"}), + oldObj: newGitHubEventCR(map[string]any{keyRepository: "a", keyEventType: valPush}), + newObj: newGitHubEventCR(map[string]any{keyRepository: "b", keyEventType: valPush}), want: true, }, { diff --git a/internal/domains/gitrepository/gitrepository.go b/internal/domains/gitrepository/gitrepository.go index 8e2b137..30206c6 100644 --- a/internal/domains/gitrepository/gitrepository.go +++ b/internal/domains/gitrepository/gitrepository.go @@ -66,12 +66,12 @@ type GitRepositoryDomain struct { } // New returns a new GitRepositoryDomain instance configured with the necessary dependencies. -func New(gitRepositoryMediator *gitrepository.Mediator, gitRepositoryService *application.GitRepositoryService, cache *cache.Cache, events *events.EventRecorder, log logr.Logger) *GitRepositoryDomain { +func New(gitRepositoryMediator *gitrepository.Mediator, gitRepositoryService *application.GitRepositoryService, domainCache *cache.Cache, eventRecorder *events.EventRecorder, log logr.Logger) *GitRepositoryDomain { return &GitRepositoryDomain{ gitRepositoryMediator: gitRepositoryMediator, gitRepositoryService: gitRepositoryService, - gitRepositoryCache: libgitrepository.NewGitRepositoryCache(cache), - events: events, + gitRepositoryCache: libgitrepository.NewGitRepositoryCache(domainCache), + events: eventRecorder, log: log, } } diff --git a/internal/domains/gitrepository/gitrepository_test.go b/internal/domains/gitrepository/gitrepository_test.go index f4f93d8..e114cb0 100644 --- a/internal/domains/gitrepository/gitrepository_test.go +++ b/internal/domains/gitrepository/gitrepository_test.go @@ -33,6 +33,9 @@ import ( const testAppName = "app-sample" +// Repeated across fixtures below — named to satisfy goconst. +const keyProvider = "provider" + func newEnvironment() *environmentsv1alpha1.Environment { return &environmentsv1alpha1.Environment{ ObjectMeta: metav1.ObjectMeta{ @@ -73,8 +76,8 @@ func newGitRepositoryCR(contract map[string]any) *sourcesv1alpha1.GitRepository func validGitRepositoryContract() map[string]any { return map[string]any{ - "provider": "github", - "hookUrl": "https://events.blanketops.dev/hooks/app-sample", + keyProvider: "github", + "hookUrl": "https://events.blanketops.dev/hooks/app-sample", "repository": map[string]any{ "owner": "blanketops", "name": "app", @@ -144,8 +147,8 @@ func TestGitRepositoryDomain_CanUpdate(t *testing.T) { }{ { name: "spec changed", - oldObj: newGitRepositoryCR(map[string]any{"provider": "a"}), - newObj: newGitRepositoryCR(map[string]any{"provider": "b"}), + oldObj: newGitRepositoryCR(map[string]any{keyProvider: "a"}), + newObj: newGitRepositoryCR(map[string]any{keyProvider: "b"}), want: true, }, { diff --git a/internal/domains/packages/package.go b/internal/domains/packages/package.go index f8e2c94..34b3625 100644 --- a/internal/domains/packages/package.go +++ b/internal/domains/packages/package.go @@ -66,13 +66,13 @@ type PackageDomain struct { } // New returns a new PackageDomain instance configured with the necessary dependencies. -func New(packageMediator *pkgMediator.Mediator, packageService *pkgApplication.PackageService, cache *cache.Cache, events *events.EventRecorder, log logr.Logger, +func New(packageMediator *pkgMediator.Mediator, packageService *pkgApplication.PackageService, domainCache *cache.Cache, eventRecorder *events.EventRecorder, log logr.Logger, ) *PackageDomain { return &PackageDomain{ packageMediator: packageMediator, packageService: packageService, - packageCache: libpackages.NewPackageCache(cache), - events: events, + packageCache: libpackages.NewPackageCache(domainCache), + events: eventRecorder, log: log, } } diff --git a/internal/domains/packages/package_test.go b/internal/domains/packages/package_test.go index ef181f1..31252d2 100644 --- a/internal/domains/packages/package_test.go +++ b/internal/domains/packages/package_test.go @@ -32,6 +32,9 @@ import ( const testAppName = "app-sample" +// Repeated across fixtures below — named to satisfy goconst. +const keyPackageName = "packageName" + func newEnvironment() *environmentv1.Environment { return &environmentv1.Environment{ ObjectMeta: metav1.ObjectMeta{ @@ -72,7 +75,7 @@ func newPackageCR(contract map[string]any) *environmentv1.Package { func validPackageContract() map[string]any { return map[string]any{ - "packageName": "app", + keyPackageName: "app", "packageVersion": "1.0.0", "packageRepository": map[string]any{ "url": "oci://ghcr.io/blanketops/packages/app", @@ -154,8 +157,8 @@ func TestPackageDomain_CanUpdate(t *testing.T) { }{ { name: "spec changed", - oldObj: newPackageCR(map[string]any{"packageName": "a"}), - newObj: newPackageCR(map[string]any{"packageName": "b"}), + oldObj: newPackageCR(map[string]any{keyPackageName: "a"}), + newObj: newPackageCR(map[string]any{keyPackageName: "b"}), want: true, }, { diff --git a/internal/domains/serviceunit/serviceunit.go b/internal/domains/serviceunit/serviceunit.go index 0024e4d..3cd52f8 100644 --- a/internal/domains/serviceunit/serviceunit.go +++ b/internal/domains/serviceunit/serviceunit.go @@ -55,11 +55,11 @@ type ServiceUnitDomain struct { log logr.Logger } -func New(mediator *serviceunit.Mediator, cache *cache.Cache, events *events.EventRecorder, log logr.Logger) *ServiceUnitDomain { +func New(mediator *serviceunit.Mediator, domainCache *cache.Cache, eventRecorder *events.EventRecorder, log logr.Logger) *ServiceUnitDomain { return &ServiceUnitDomain{ serviceUnitMediator: mediator, - serviceUnitCache: libserviceunit.NewServiceUnitCache(cache), - events: events, + serviceUnitCache: libserviceunit.NewServiceUnitCache(domainCache), + events: eventRecorder, log: log, } } diff --git a/internal/domains/serviceunit/serviceunit_test.go b/internal/domains/serviceunit/serviceunit_test.go index b566646..c7c2aa1 100644 --- a/internal/domains/serviceunit/serviceunit_test.go +++ b/internal/domains/serviceunit/serviceunit_test.go @@ -28,6 +28,13 @@ import ( "github.com/blanketops/environments-controller/internal/testsupport" ) +// Repeated across fixtures below — named to satisfy goconst. +const ( + keyImage = "image" + keyType = "type" + valTypeStatic = "static" +) + func newServiceUnitCR(contract map[string]any) *environmentsv1alpha1.ServiceUnit { su := &environmentsv1alpha1.ServiceUnit{ ObjectMeta: metav1.ObjectMeta{ @@ -43,8 +50,8 @@ func newServiceUnitCR(contract map[string]any) *environmentsv1alpha1.ServiceUnit func validServiceUnitContract() map[string]any { return map[string]any{ - "type": "static", - "image": "ghcr.io/blanketops/app:latest", + keyType: valTypeStatic, + keyImage: "ghcr.io/blanketops/app:latest", } } @@ -102,8 +109,8 @@ func TestServiceUnitDomain_CanUpdate(t *testing.T) { }{ { name: "spec changed", - oldObj: newServiceUnitCR(map[string]any{"type": "static", "image": "a"}), - newObj: newServiceUnitCR(map[string]any{"type": "static", "image": "b"}), + oldObj: newServiceUnitCR(map[string]any{keyType: valTypeStatic, keyImage: "a"}), + newObj: newServiceUnitCR(map[string]any{keyType: valTypeStatic, keyImage: "b"}), want: true, }, { diff --git a/internal/mediators/deployment/deployment_test.go b/internal/mediators/deployment/deployment_test.go index 97b011d..96cbf06 100644 --- a/internal/mediators/deployment/deployment_test.go +++ b/internal/mediators/deployment/deployment_test.go @@ -34,11 +34,17 @@ import ( "github.com/blanketops/environments-controller/internal/testsupport" ) +// Repeated across fixtures below — named to satisfy goconst. +const ( + testNamespace = "default" + manifestsRepoURL = "https://github.com/blanketops/app-manifests.git" +) + func newResolvedDeployment(manifestsRepo *deploymentResolution.ResolvedManifestsRepo) *deploymentResolution.ResolvedDeployment { depl := &environmentsv1alpha1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-sample", - Namespace: "default", + Namespace: testNamespace, }, } return &deploymentResolution.ResolvedDeployment{ @@ -83,11 +89,11 @@ func TestTeardownManifestsRepo_NoManifestsRepo_NoOp(t *testing.T) { } func TestExtractPublicKey_PreStoredIdentityPub(t *testing.T) { - resolved := newResolvedDeployment(&deploymentResolution.ResolvedManifestsRepo{URL: "https://github.com/blanketops/app-manifests.git"}) + resolved := newResolvedDeployment(&deploymentResolution.ResolvedManifestsRepo{URL: manifestsRepoURL}) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-sample-flux-ssh", - Namespace: "default", + Namespace: testNamespace, }, Data: map[string][]byte{ "identity.pub": []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINPreStoredKey test@blanketops"), @@ -105,7 +111,7 @@ func TestExtractPublicKey_PreStoredIdentityPub(t *testing.T) { } func TestExtractPublicKey_SecretNotFound(t *testing.T) { - resolved := newResolvedDeployment(&deploymentResolution.ResolvedManifestsRepo{URL: "https://github.com/blanketops/app-manifests.git"}) + resolved := newResolvedDeployment(&deploymentResolution.ResolvedManifestsRepo{URL: manifestsRepoURL}) m := New(testsupport.NewFakeClient(), testsupport.NewScheme(), logr.Discard(), testsupport.NoopRawRecorder()) if _, err := m.extractPublicKey(context.Background(), resolved); err == nil { @@ -114,11 +120,11 @@ func TestExtractPublicKey_SecretNotFound(t *testing.T) { } func TestWriteSSHKeyToDisk_WritesAndCleansUp(t *testing.T) { - resolved := newResolvedDeployment(&deploymentResolution.ResolvedManifestsRepo{URL: "https://github.com/blanketops/app-manifests.git"}) + resolved := newResolvedDeployment(&deploymentResolution.ResolvedManifestsRepo{URL: manifestsRepoURL}) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment-sample-flux-ssh", - Namespace: "default", + Namespace: testNamespace, }, Data: map[string][]byte{ "identity": []byte("-----BEGIN OPENSSH PRIVATE KEY-----\nfake-key-material\n-----END OPENSSH PRIVATE KEY-----\n"), @@ -154,7 +160,7 @@ func TestWriteSSHKeyToDisk_WritesAndCleansUp(t *testing.T) { } func TestWriteSSHKeyToDisk_SecretNotFound(t *testing.T) { - resolved := newResolvedDeployment(&deploymentResolution.ResolvedManifestsRepo{URL: "https://github.com/blanketops/app-manifests.git"}) + resolved := newResolvedDeployment(&deploymentResolution.ResolvedManifestsRepo{URL: manifestsRepoURL}) m := New(testsupport.NewFakeClient(), testsupport.NewScheme(), logr.Discard(), testsupport.NoopRawRecorder()) if _, _, err := m.writeSSHKeyToDisk(context.Background(), resolved); err == nil { diff --git a/internal/mediators/environment_test.go b/internal/mediators/environment_test.go index 17e51b4..8bd7859 100644 --- a/internal/mediators/environment_test.go +++ b/internal/mediators/environment_test.go @@ -27,11 +27,20 @@ import ( "github.com/blanketops/environments-controller/internal/testsupport" ) +// Repeated across fixtures below — named to satisfy goconst. +const ( + testBuildName = "build-sample" + testNamespace = "default" + testAppSampleName = "app-sample" + keyApplicationName = "applicationName" + testAppNewName = "app-new" +) + func newScopedObject(envName, envType string) client.Object { return &env1alpha1.Build{ ObjectMeta: metav1.ObjectMeta{ - Name: "build-sample", - Namespace: "default", + Name: testBuildName, + Namespace: testNamespace, Labels: map[string]string{ "environments.blanketops.dev/name": envName, "environments.blanketops.dev/type": envType, @@ -42,7 +51,7 @@ func newScopedObject(envName, envType string) client.Object { func TestEnsureEnvironment_NotEnvironmentScoped(t *testing.T) { c := testsupport.NewFakeClient() - obj := &env1alpha1.Build{ObjectMeta: metav1.ObjectMeta{Name: "build-sample", Namespace: "default"}} + obj := &env1alpha1.Build{ObjectMeta: metav1.ObjectMeta{Name: testBuildName, Namespace: testNamespace}} env, err := EnsureEnvironment(context.Background(), c, obj, runtime.RawExtension{}) if err != nil { @@ -55,25 +64,25 @@ func TestEnsureEnvironment_NotEnvironmentScoped(t *testing.T) { func TestEnsureEnvironment_ReturnsExisting(t *testing.T) { existing := &env1alpha1.Environment{ - ObjectMeta: metav1.ObjectMeta{Name: "app-sample", Namespace: "default"}, - Spec: env1alpha1.EnvironmentSpec{Contract: testsupport.RawContract(map[string]any{"applicationName": "app-sample"})}, + ObjectMeta: metav1.ObjectMeta{Name: testAppSampleName, Namespace: testNamespace}, + Spec: env1alpha1.EnvironmentSpec{Contract: testsupport.RawContract(map[string]any{keyApplicationName: testAppSampleName})}, } c := testsupport.NewFakeClient(existing) - obj := newScopedObject("app-sample", "dev") + obj := newScopedObject(testAppSampleName, "dev") env, err := EnsureEnvironment(context.Background(), c, obj, runtime.RawExtension{}) if err != nil { t.Fatalf("EnsureEnvironment() = %v, want nil", err) } - if env == nil || env.Name != "app-sample" { + if env == nil || env.Name != testAppSampleName { t.Errorf("EnsureEnvironment() = %+v, want the existing Environment returned as-is", env) } } func TestEnsureEnvironment_CreatesWhenMissing(t *testing.T) { c := testsupport.NewFakeClient() - obj := newScopedObject("app-new", "dev") - contract := testsupport.RawContract(map[string]any{"applicationName": "app-new"}) + obj := newScopedObject(testAppNewName, "dev") + contract := testsupport.RawContract(map[string]any{keyApplicationName: testAppNewName}) env, err := EnsureEnvironment(context.Background(), c, obj, contract) if err != nil { @@ -82,13 +91,13 @@ func TestEnsureEnvironment_CreatesWhenMissing(t *testing.T) { if env == nil { t.Fatal("EnsureEnvironment() = nil, want a newly created Environment") } - if env.Labels["environments.blanketops.dev/name"] != "app-new" || env.Labels["environments.blanketops.dev/type"] != "dev" { + if env.Labels["environments.blanketops.dev/name"] != testAppNewName || env.Labels["environments.blanketops.dev/type"] != "dev" { t.Errorf("created Environment labels = %v, want name=app-new type=dev", env.Labels) } // Verify it was actually persisted, not just returned in-memory. var fetched env1alpha1.Environment - if err := c.Get(context.Background(), client.ObjectKey{Name: "app-new", Namespace: "default"}, &fetched); err != nil { + if err := c.Get(context.Background(), client.ObjectKey{Name: testAppNewName, Namespace: testNamespace}, &fetched); err != nil { t.Fatalf("created Environment not found in client: %v", err) } } @@ -96,7 +105,7 @@ func TestEnsureEnvironment_CreatesWhenMissing(t *testing.T) { func TestPatchEnvironmentAggregate_ResolutionFailure(t *testing.T) { c := testsupport.NewFakeClient() env := &env1alpha1.Environment{ - ObjectMeta: metav1.ObjectMeta{Name: "app-sample", Namespace: "default"}, + ObjectMeta: metav1.ObjectMeta{Name: testAppSampleName, Namespace: testNamespace}, // No contract set — ResolveEnvironment requires spec.contract. } @@ -108,21 +117,21 @@ func TestPatchEnvironmentAggregate_ResolutionFailure(t *testing.T) { func TestPatchEnvironmentAggregate_AppliesMutation(t *testing.T) { env := &env1alpha1.Environment{ - ObjectMeta: metav1.ObjectMeta{Name: "app-sample", Namespace: "default"}, + ObjectMeta: metav1.ObjectMeta{Name: testAppSampleName, Namespace: testNamespace}, Spec: env1alpha1.EnvironmentSpec{ Contract: testsupport.RawContract(map[string]any{ - "applicationName": "app-sample", - "branch": "main", - "gitOwner": "blanketops", - "environmentType": "dev", - "version": "v1", + keyApplicationName: testAppSampleName, + "branch": "main", + "gitOwner": "blanketops", + "environmentType": "dev", + "version": "v1", }), }, } c := testsupport.NewFakeClient(env) err := PatchEnvironmentAggregate(context.Background(), c, env, func(spec *environmentResolution.ResolvedEnvironmentSpec) { - spec.Build = "build-sample" + spec.Build = testBuildName }) if err != nil { t.Fatalf("PatchEnvironmentAggregate() = %v, want nil", err) @@ -132,21 +141,21 @@ func TestPatchEnvironmentAggregate_AppliesMutation(t *testing.T) { if err := json.Unmarshal(env.Spec.Contract.Raw, &patched); err != nil { t.Fatalf("failed to decode patched contract: %v", err) } - if patched["Build"] != "build-sample" { - t.Errorf("patched contract Build = %v, want %q", patched["Build"], "build-sample") + if patched["Build"] != testBuildName { + t.Errorf("patched contract Build = %v, want %q", patched["Build"], testBuildName) } // Also verify the update was actually persisted via the client, not // just mutated on the in-memory object passed in. var fetched env1alpha1.Environment - if err := c.Get(context.Background(), client.ObjectKey{Name: "app-sample", Namespace: "default"}, &fetched); err != nil { + if err := c.Get(context.Background(), client.ObjectKey{Name: testAppSampleName, Namespace: testNamespace}, &fetched); err != nil { t.Fatalf("get after patch: %v", err) } var fetchedContract map[string]any if err := json.Unmarshal(fetched.Spec.Contract.Raw, &fetchedContract); err != nil { t.Fatalf("failed to decode fetched contract: %v", err) } - if fetchedContract["Build"] != "build-sample" { - t.Errorf("persisted contract Build = %v, want %q", fetchedContract["Build"], "build-sample") + if fetchedContract["Build"] != testBuildName { + t.Errorf("persisted contract Build = %v, want %q", fetchedContract["Build"], testBuildName) } } diff --git a/internal/mediators/packages/package_test.go b/internal/mediators/packages/package_test.go index 8aae0e4..82be5a7 100644 --- a/internal/mediators/packages/package_test.go +++ b/internal/mediators/packages/package_test.go @@ -35,6 +35,12 @@ import ( const testAppName = "app-sample" +// Repeated across fixtures below — named to satisfy goconst. +const ( + stateRepoURL = "https://github.com/blanketops/app-state.git" + stateRepoCloneSecret = "app-state-git-ssh" +) + func newEnvironment() *environmentsv1alpha1.Environment { return &environmentsv1alpha1.Environment{ ObjectMeta: metav1.ObjectMeta{ @@ -113,8 +119,8 @@ func TestMediator_EnsurePrerequisites_NoStateRepository_NoPanic(t *testing.T) { func TestMediator_EnsurePrerequisites_WithStateRepository(t *testing.T) { env := newEnvironment() resolved := newResolvedPackage(&packageResolution.ResolvedStateRepository{ - URL: "https://github.com/blanketops/app-state.git", - CloneSecret: "app-state-git-ssh", + URL: stateRepoURL, + CloneSecret: stateRepoCloneSecret, }, "") c := testsupport.NewFakeClient(env, resolved.Package) m := New(c, testsupport.NewScheme(), logr.Discard(), testsupport.NoopRawRecorder()) @@ -127,8 +133,8 @@ func TestMediator_EnsurePrerequisites_WithStateRepository(t *testing.T) { func TestMediator_EnsurePrerequisites_Idempotent(t *testing.T) { env := newEnvironment() resolved := newResolvedPackage(&packageResolution.ResolvedStateRepository{ - URL: "https://github.com/blanketops/app-state.git", - CloneSecret: "app-state-git-ssh", + URL: stateRepoURL, + CloneSecret: stateRepoCloneSecret, }, "app-registry-creds") c := testsupport.NewFakeClient(env, resolved.Package) m := New(c, testsupport.NewScheme(), logr.Discard(), testsupport.NoopRawRecorder()) @@ -156,8 +162,8 @@ func TestMediator_CleanupPrerequisites_NoStateRepository_NoPanic(t *testing.T) { func TestMediator_CleanupPrerequisites_AfterEnsure(t *testing.T) { env := newEnvironment() resolved := newResolvedPackage(&packageResolution.ResolvedStateRepository{ - URL: "https://github.com/blanketops/app-state.git", - CloneSecret: "app-state-git-ssh", + URL: stateRepoURL, + CloneSecret: stateRepoCloneSecret, }, "app-registry-creds") c := testsupport.NewFakeClient(env, resolved.Package) m := New(c, testsupport.NewScheme(), logr.Discard(), testsupport.NoopRawRecorder()) diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index 94ce2cc..12aa8ad 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -35,17 +35,17 @@ type Runtime struct { func New(mgr ctrl.Manager) *Runtime { log := ctrl.Log.WithName("environments-runtime") - cache := cache.NewCache(mgr, nil) - registry := registry.NewRegistry() + objCache := cache.NewCache(mgr, nil) + reg := registry.NewRegistry() - engine := engine.NewEngine(registry, ctrl.Log.WithName("environments-engine")) - events := events.NewEventRecorder(mgr.GetEventRecorder("environments-runtime")) + eng := engine.NewEngine(reg, ctrl.Log.WithName("environments-engine")) + eventRecorder := events.NewEventRecorder(mgr.GetEventRecorder("environments-runtime")) return &Runtime{ - Cache: cache, - Registry: registry, - Engine: engine, - Events: events, + Cache: objCache, + Registry: reg, + Engine: eng, + Events: eventRecorder, Log: log, } }