diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index 20570b3..08d0ebe 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -36,13 +36,20 @@ jobs: # on a bare runner, which has no macOS or cross-linux compilers. The # binary links Core's tree-sitter grammars (CGO), so releasing it needs # the toolchain this maintained image supplies — see .goreleaser.yaml. + - name: Install Syft for archive SBOMs + id: syft + uses: anchore/sbom-action/download-syft@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2 + with: + syft-version: v1.44.0 - name: Run CGO-capable GoReleaser run: >- docker run --rm -e GITHUB_TOKEN -v "$GITHUB_WORKSPACE:/workspace" + -v "$SYFT_PATH:/usr/local/bin/syft:ro" -w /workspace ghcr.io/goreleaser/goreleaser-cross:v1.27.1-1 release --clean env: GITHUB_TOKEN: ${{ secrets.GH_PAT }} + SYFT_PATH: ${{ steps.syft.outputs.cmd }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 759bc75..2a5355d 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,5 +1,12 @@ version: 2 +sboms: + - id: archives + artifacts: archive + documents: + - "${artifact}.sbom.json" + disable: false + builds: # ARCHITECTURE: Core's authoritative source inspection uses real # tree-sitter grammars, which are CGO. Agent releases therefore require diff --git a/agent.codefly.yaml b/agent.codefly.yaml index 538ebcb..46d2dbd 100644 --- a/agent.codefly.yaml +++ b/agent.codefly.yaml @@ -2,6 +2,9 @@ publisher: codefly.dev kind: codefly:service name: python-fastapi version: 0.0.102 +release: + owner: workflow + workflow: releaser.yml source: directory: . - agent: codefly.dev/go:0.0.51 + agent: codefly.dev/go:0.0.54 diff --git a/builder.go b/builder.go index 2b9ba4f..5eb541c 100644 --- a/builder.go +++ b/builder.go @@ -249,9 +249,7 @@ type Parameters struct { // AutomountServiceAccountToken projects the ServiceAccount token into the // pod. It stays false because the identity path is the metadata server or a // webhook-projected token and the app does not call the API server. Setting - // it true currently renders a manifest core's own manifest conformance - // rejects (codefly-dev/core#602), so the field is the seam, not yet a - // working switch. + // it true requires the pod annotation codefly.dev/api-server-access: required. AutomountServiceAccountToken bool } diff --git a/deployment_test.go b/deployment_test.go index 61d3599..86ca242 100644 --- a/deployment_test.go +++ b/deployment_test.go @@ -201,11 +201,7 @@ func TestDeploymentRendersOptionalConfigMount(t *testing.T) { require.Equal(t, false, *pod.Spec.Containers[0].VolumeMounts[1].ReadOnly) } -// TestDeploymentAutomountServiceAccountTokenRejectsProjection pins the reason -// the parameter defaults to false and cannot yet be flipped: core's manifest -// conformance rejects a projected token for every workload, so a service that -// needs to call the API server is blocked in core, not here. -func TestDeploymentAutomountServiceAccountTokenRejectsProjection(t *testing.T) { +func TestDeploymentAutomountServiceAccountTokenRejectsUndeclaredProjection(t *testing.T) { _, destination := renderDeployment(t, Parameters{}, nil) path := filepath.Join(destination, "base", "deployment.yaml") @@ -219,9 +215,20 @@ func TestDeploymentAutomountServiceAccountTokenRejectsProjection(t *testing.T) { builderv0.KubernetesOutputProfile_KUBERNETES_OUTPUT_PROFILE_EPHEMERAL_LOCAL_APPLY_V1, false, "", "") require.Contains(t, validation.GetViolations(), - "Deployment/example-service must set automountServiceAccountToken: false", - "core no longer rejects a projected token: codefly-dev/core#602 is resolved, so "+ - "AutomountServiceAccountToken is now usable and this test should assert the render instead") + "Deployment/example-service must set automountServiceAccountToken: false, or declare codefly.dev/api-server-access: required") +} + +func TestDeploymentAutomountServiceAccountTokenRendersDeclaredProjection(t *testing.T) { + pod, destination := renderDeployment(t, Parameters{AutomountServiceAccountToken: true}, &services.PodTemplateOverlay{ + PodAnnotations: map[string]string{services.AnnotationAPIServerAccess: services.APIServerAccessRequired}, + }) + require.NotNil(t, pod.Spec.AutomountServiceAccountToken) + require.True(t, *pod.Spec.AutomountServiceAccountToken) + require.Equal(t, services.APIServerAccessRequired, pod.Metadata.Annotations[services.AnnotationAPIServerAccess]) + + validation := services.ValidateKubernetesManifestTree(t.Context(), destination, "test", "codefly-test", + builderv0.KubernetesOutputProfile_KUBERNETES_OUTPUT_PROFILE_EPHEMERAL_LOCAL_APPLY_V1, false, "", "") + require.Empty(t, validation.GetViolations()) } // TestPreparePodOverlayRejectsScratchCollision covers the mounts core's own diff --git a/go.mod b/go.mod index 650f9ac..6107e10 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/codefly-dev/service-python-fastapi go 1.27.0 require ( - github.com/codefly-dev/core v0.3.41 - github.com/codefly-dev/service-python v0.0.62 + github.com/codefly-dev/core v0.4.0 + github.com/codefly-dev/service-python v0.0.64 github.com/stretchr/testify v1.12.1 google.golang.org/grpc v1.83.2 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index eee11b1..9dcba8f 100644 --- a/go.sum +++ b/go.sum @@ -43,12 +43,12 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA= github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c= -github.com/codefly-dev/core v0.3.41 h1:Etae1jOIWEl524U7tnCT3TaDPUtU5WGsf1XhlFALvt8= -github.com/codefly-dev/core v0.3.41/go.mod h1:uG0HQy5iWnkuWUPa1LbbiPfEgzuxi1SzEKwcFPC6eoA= +github.com/codefly-dev/core v0.4.0 h1:xMW9pV+d0mEvxTs4Jqrs5hr4OCuIy1Fvz0aE3/a5LIM= +github.com/codefly-dev/core v0.4.0/go.mod h1:uG0HQy5iWnkuWUPa1LbbiPfEgzuxi1SzEKwcFPC6eoA= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= -github.com/codefly-dev/service-python v0.0.62 h1:quru4JzJi8v5nhiYTzXjl2SswXIasyeYq/iaM6TvN4k= -github.com/codefly-dev/service-python v0.0.62/go.mod h1:LEJP0KA7BRALgQU8YLBwwVxXls5WwQLQ8esbMDG6IOY= +github.com/codefly-dev/service-python v0.0.64 h1:6C0GIu3+c/P3FShvk/lJgGlMkUSIuXxlTCo3RGMHntk= +github.com/codefly-dev/service-python v0.0.64/go.mod h1:s9Se4j0mfGmGo/HPLMtcwDfL31G8c++I8/sDycxMHpI= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= diff --git a/release_test.go b/release_test.go new file mode 100644 index 0000000..0f37481 --- /dev/null +++ b/release_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "gopkg.in/yaml.v3" +) + +func TestReleaseDeclaresOnePublisherAndArchiveSBOMs(t *testing.T) { + read := func(path string, target any) { + t.Helper() + payload, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if err := yaml.Unmarshal(payload, target); err != nil { + t.Fatal(err) + } + } + var manifest struct { + Release struct{ Owner, Workflow string } + } + read("agent.codefly.yaml", &manifest) + if manifest.Release.Owner != "workflow" || manifest.Release.Workflow != "releaser.yml" { + t.Fatal("the tag workflow must be the sole artifact publisher") + } + var config struct { + SBOMs []struct { + Artifacts string + Documents []string + Disable bool + } `yaml:"sboms"` + } + read(".goreleaser.yaml", &config) + if len(config.SBOMs) != 1 || config.SBOMs[0].Artifacts != "archive" || config.SBOMs[0].Disable || len(config.SBOMs[0].Documents) != 1 || config.SBOMs[0].Documents[0] != "${artifact}.sbom.json" { + t.Fatal("each published archive must carry its canonical SBOM") + } + var workflow struct { + Jobs map[string]struct { + Steps []struct { + ID, Uses, Run string + Env map[string]string + } + } + } + read(filepath.Join(".github", "workflows", manifest.Release.Workflow), &workflow) + installed, mounted := false, false + for _, step := range workflow.Jobs["release"].Steps { + if step.ID == "syft" && strings.HasPrefix(step.Uses, "anchore/sbom-action/download-syft@") { + installed = true + } + if strings.Contains(step.Run, `-v "$SYFT_PATH:/usr/local/bin/syft:ro"`) && step.Env["SYFT_PATH"] == "${{ steps.syft.outputs.cmd }}" { + mounted = true + } + } + if !installed || !mounted { + t.Fatal("the canonical cross build must have Syft installed and mounted") + } +}