Files
cozystack/internal/apigate/parse_test.go
Timofei Larkin 108b51df3d feat(api): serve credentials with mint and revoke, and seed accounts
The Credential view lists the account Secrets of the namespace by label
and skips any no live HelmRelease owns, so no kind is served until its
definition carries a credentials block. A mint replaces the stored value
in one conditional write keyed on the Secret's resourceVersion and
returns the plaintext once; a revoke discards it. Both record an Event
on the application, which needs create on events.

The application registry seeds an account Secret per declared account
before it writes the HelmRelease, so the chart can reference them from
the first render, prunes the Secrets of removed accounts after the
write, and refuses to delete an application whose release is suspended,
since Flux skips the uninstall and would leave the database running
with the Secrets gone. The ApplicationDefinition reconciler computes
the per-account valuesFrom entries with the function the API uses, so a
release that predates the block gets them on its next reconcile. Schema
defaulting moves to a package both can import.

A definition declaring a format or an account source that is not built
leaves its kind unserved rather than served with passwords the API
cannot issue. Tenant roles gain get and list on credentials at view and
create on mint and revoke at admin and for the tenant ServiceAccount.
Attribution comes from the kube-apiserver audit log, so the operations
docs get a policy for distributions that ship none, and
monitoring-agents a value for an extra fluent-bit output so a SIEM can
receive the log without dropping the VictoriaLogs outputs.

Assisted-by: LLM
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-10-14 12:29:46 +02:00

563 lines
19 KiB
Go

/*
Copyright 2026 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apigate
import (
"os"
"path/filepath"
"strings"
"testing"
)
const sampleCozyRD = `apiVersion: cozystack.io/v1alpha1
kind: ApplicationDefinition
metadata:
name: postgres
spec:
application:
kind: Postgres
singular: postgres
plural: postgreses
openAPISchema: |-
{"type":"object","properties":{"replicas":{"type":"integer","default":2}}}
`
const sampleCRD = `apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: widgets.example.cozystack.io
spec:
group: example.cozystack.io
names:
kind: Widget
plural: widgets
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
size:
type: string
`
func TestParseCozyRD(t *testing.T) {
r, ok, err := ParseCozyRD("postgres.yaml", []byte(sampleCozyRD))
if err != nil || !ok {
t.Fatalf("ParseCozyRD failed: ok=%v err=%v", ok, err)
}
if r.Group != "apps.cozystack.io" || r.Kind != "Postgres" || r.Plural != "postgreses" {
t.Fatalf("unexpected identity: %+v", r)
}
if _, ok := r.Versions["v1alpha1"]; !ok {
t.Fatalf("expected v1alpha1 schema, got versions %v", r.Versions)
}
}
func TestParseCRDs(t *testing.T) {
rs, err := ParseCRDs("widget.yaml", []byte(sampleCRD))
if err != nil {
t.Fatalf("ParseCRDs failed: %v", err)
}
if len(rs) != 1 {
t.Fatalf("expected 1 CRD resource, got %d", len(rs))
}
r := rs[0]
if r.Group != "example.cozystack.io" || r.Kind != "Widget" || r.Plural != "widgets" {
t.Fatalf("unexpected identity: %+v", r)
}
if _, ok := r.Versions["v1alpha1"]; !ok {
t.Fatalf("expected v1alpha1 schema")
}
}
// TestParseCRDsSkipsUnservedVersions covers B7(a): a version flipped to
// served:false is not live API surface and must be excluded, so the standard
// CRD deprecation step surfaces later as a removed served version.
func TestParseCRDsSkipsUnservedVersions(t *testing.T) {
const crd = `apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: widgets.example.cozystack.io
spec:
group: example.cozystack.io
names: {kind: Widget, plural: widgets}
versions:
- name: v1alpha1
served: false
schema: {openAPIV3Schema: {type: object}}
- name: v1beta1
served: true
schema: {openAPIV3Schema: {type: object}}
`
rs, err := ParseCRDs("widget.yaml", []byte(crd))
if err != nil {
t.Fatal(err)
}
if len(rs) != 1 {
t.Fatalf("expected 1 resource, got %d", len(rs))
}
if _, ok := rs[0].Versions["v1alpha1"]; ok {
t.Fatalf("unserved v1alpha1 must be excluded, got versions %v", rs[0].Versions)
}
if _, ok := rs[0].Versions["v1beta1"]; !ok {
t.Fatalf("served v1beta1 must be present, got versions %v", rs[0].Versions)
}
}
// TestParseCRDsToleratesTemplatedNeighborDocument reproduces the cozyplane
// bug: a real CRD living in a chart's templates/ dir, guarded by a
// `{{- if }}` block that is not valid YAML on its own. The malformed
// neighbor document must not take the real CRD down with it.
func TestParseCRDsToleratesTemplatedNeighborDocument(t *testing.T) {
const doc = `{{- /* CRDs are installed only in CRD mode */ -}}
{{- if not .Values.apiserver.enabled }}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: vpcs.sdn.cozystack.io
spec:
group: sdn.cozystack.io
names: {kind: VPC, plural: vpcs}
versions:
- name: v1alpha1
served: true
schema: {openAPIV3Schema: {type: object}}
{{- end }}
`
rs, err := ParseCRDs("templates/crds.yaml", []byte(doc))
if err != nil {
t.Fatalf("ParseCRDs failed: %v", err)
}
if len(rs) != 1 || rs[0].Plural != "vpcs" {
t.Fatalf("expected the CRD sharing the file with templated markup to survive, got %v", rs)
}
}
// TestParseAPIServices covers the new APIService source: a first-party group
// registration is surfaced as a schema-less resource, and a non-APIService or
// incomplete document is ignored.
func TestParseAPIServices(t *testing.T) {
const doc = `apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1alpha1.sdn.cozystack.io
spec:
group: sdn.cozystack.io
version: v1alpha1
groupPriorityMinimum: 1000
versionPriority: 10
---
apiVersion: v1
kind: Service
metadata:
name: cozyplane-apiserver
`
rs := ParseAPIServices("apiserver.yaml", []byte(doc))
if len(rs) != 1 {
t.Fatalf("expected exactly 1 APIService resource, got %v", rs)
}
r := rs[0]
if r.Group != "sdn.cozystack.io" || r.Source != SourceAPIService || r.Plural != apiServicePlural {
t.Fatalf("unexpected APIService resource: %+v", r)
}
if _, ok := r.Versions["v1alpha1"]; !ok {
t.Fatalf("expected v1alpha1 in versions, got %v", r.Versions)
}
}
func TestParseAPIServerStorages(t *testing.T) {
src := `
coreV1alpha1Storage["tenantsecrets"] = cozyregistry.RESTInPeace(x)
coreV1alpha1Storage["credentials/mint"] = credentialstorage.NewMintREST(w)
sdnV1alpha1Storage["securitygroups"] = cozyregistry.RESTInPeace(y)
appsV1alpha1Storage[resConfig.Application.Plural] = cozyregistry.RESTInPeace(z)
`
rs := ParseAPIServerStorages("apiserver.go", []byte(src))
got := map[string]string{}
for _, r := range rs {
got[r.Plural] = r.Group
}
if got["tenantsecrets"] != "core.cozystack.io" {
t.Fatalf("expected core tenantsecrets, got %v", got)
}
if got["securitygroups"] != "sdn.cozystack.io" {
t.Fatalf("expected sdn securitygroups, got %v", got)
}
if got["credentials/mint"] != "core.cozystack.io" {
t.Fatalf("expected the subresource registration to be gated, got %v", got)
}
if _, leaked := got["Plural"]; leaked {
t.Fatalf("apps dynamic registration must not be captured: %v", got)
}
}
// TestParseAPIServerStoragesSurfacesUnknownPrefix covers the drift guard: a new
// aggregated group registered under an unlisted *Storage variable must not be
// silently dropped, or it would bypass the gate entirely.
func TestParseAPIServerStoragesSurfacesUnknownPrefix(t *testing.T) {
src := `newgroupV1alpha1Storage["foos"] = cozyregistry.RESTInPeace(x)`
rs := ParseAPIServerStorages("apiserver.go", []byte(src))
if len(rs) != 1 {
t.Fatalf("expected the unknown-prefix resource to be surfaced, got %v", rs)
}
if !strings.HasPrefix(rs[0].Group, unmappedStoragePrefix) {
t.Fatalf("unknown prefix must surface under the unmapped group, got %q", rs[0].Group)
}
}
// TestLoadSnapshotDiscoversCRDsUnderChartsCrds covers B6: a first-party CRD that
// lives in a chart's crds/ directory (rather than a definitions/ dir) is still
// discovered.
func TestLoadSnapshotDiscoversCRDsUnderChartsCrds(t *testing.T) {
root := t.TempDir()
write := func(rel, content string) {
p := filepath.Join(root, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
write("packages/system/foo/charts/foo/crds/example.cozystack.io_widgets.yaml", sampleCRD)
snap, err := LoadSnapshot(root)
if err != nil {
t.Fatal(err)
}
if _, ok := snap[resourceKey{Group: "example.cozystack.io", Plural: "widgets"}]; !ok {
t.Fatalf("CRD under charts/*/crds/ was not discovered: %v", snap)
}
}
// TestLoadSnapshotDiscoversCRDUnderTemplatesDir reproduces the real gap found
// in the cozyplane PR: a raw CRD document guarded by `{{- if }}`, sitting in a
// chart's templates/ dir (not crds/), must still be discovered — directory
// name is no longer the discovery signal, file content is.
func TestLoadSnapshotDiscoversCRDUnderTemplatesDir(t *testing.T) {
root := t.TempDir()
write := func(rel, content string) {
p := filepath.Join(root, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
write("packages/system/cozyplane/templates/crds.yaml", `{{- if not .Values.apiserver.enabled }}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: vpcs.sdn.cozystack.io
spec:
group: sdn.cozystack.io
names: {kind: VPC, plural: vpcs}
versions:
- name: v1alpha1
served: true
schema: {openAPIV3Schema: {type: object}}
{{- end }}
`)
snap, err := LoadSnapshot(root)
if err != nil {
t.Fatal(err)
}
if _, ok := snap[resourceKey{Group: "sdn.cozystack.io", Plural: "vpcs"}]; !ok {
t.Fatalf("CRD under templates/ was not discovered: %v", snap)
}
}
// TestLoadSnapshotDiscoversAPIServiceAndTripsNewGroup covers the other half of
// the cozyplane gap: a group served only by an aggregated apiserver (no CRD,
// no cozyrd, no apiserver.go entry) — its schema lives entirely outside this
// repo, but the group's first appearance must still require review. Versions
// registered via separate sibling APIService objects (one per version, the
// real-world shape) must merge onto the same resource.
func TestLoadSnapshotDiscoversAPIServiceAndTripsNewGroup(t *testing.T) {
root := t.TempDir()
write := func(rel, content string) {
p := filepath.Join(root, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
write("packages/system/cozyplane/templates/apiserver-v1alpha1.yaml", `apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata: {name: v1alpha1.sdn.cozystack.io}
spec: {group: sdn.cozystack.io, version: v1alpha1}
`)
write("packages/system/cozyplane/templates/apiserver-v1beta1.yaml", `apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata: {name: v1beta1.sdn.cozystack.io}
spec: {group: sdn.cozystack.io, version: v1beta1}
`)
// A minimal but non-empty base, unrelated to sdn.cozystack.io.
baseRoot := t.TempDir()
writeBase := func(rel, content string) {
p := filepath.Join(baseRoot, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
writeBase("packages/system/postgres-rd/cozyrds/postgres.yaml", sampleCozyRD)
base, err := LoadSnapshot(baseRoot)
if err != nil {
t.Fatal(err)
}
head, err := LoadSnapshot(root)
if err != nil {
t.Fatal(err)
}
key := resourceKey{Group: "sdn.cozystack.io", Plural: apiServicePlural}
res, ok := head[key]
if !ok {
t.Fatalf("APIService group was not discovered: %v", head)
}
if _, ok := res.Versions["v1alpha1"]; !ok {
t.Fatalf("expected v1alpha1 merged in, got %v", res.Versions)
}
if _, ok := res.Versions["v1beta1"]; !ok {
t.Fatalf("expected v1beta1 from the sibling APIService object merged in, got %v", res.Versions)
}
if countCategory(Classify(base, head), NewGroup) != 1 {
t.Fatalf("expected the APIService-only group to trip NewGroup, got %v", Classify(base, head))
}
}
// TestLoadSnapshotIgnoresThirdPartyManifestsUnderTemplates guards against the
// content-based walk becoming too permissive: a vendored CRD and APIService
// for a non-cozystack.io group, sitting in the exact same templates/ shape,
// must still be filtered out.
func TestLoadSnapshotIgnoresThirdPartyManifestsUnderTemplates(t *testing.T) {
root := t.TempDir()
write := func(rel, content string) {
p := filepath.Join(root, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
write("packages/system/cert-manager/templates/crds.yaml", `apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata: {name: certificates.cert-manager.io}
spec:
group: cert-manager.io
names: {kind: Certificate, plural: certificates}
versions:
- name: v1
served: true
schema: {openAPIV3Schema: {type: object}}
`)
write("packages/system/metrics/templates/apiservice.yaml", `apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata: {name: v1beta1.metrics.k8s.io}
spec: {group: metrics.k8s.io, version: v1beta1}
`)
// Give the checkout a first-party resource so LoadSnapshot's
// empty-snapshot guard doesn't fire for an unrelated reason.
write("packages/system/postgres-rd/cozyrds/postgres.yaml", sampleCozyRD)
snap, err := LoadSnapshot(root)
if err != nil {
t.Fatal(err)
}
if _, ok := snap[resourceKey{Group: "cert-manager.io", Plural: "certificates"}]; ok {
t.Fatalf("vendored third-party CRD must not be discovered: %v", snap)
}
if _, ok := snap[resourceKey{Group: "metrics.k8s.io", Plural: apiServicePlural}]; ok {
t.Fatalf("vendored third-party APIService must not be discovered: %v", snap)
}
}
// TestLoadSnapshotSkipsOversizedFiles guards the size cap added alongside
// content-based discovery: since every .yaml/.yml file in the tree is now a
// candidate (no directory allowlist), an untrusted PR head checkout could
// otherwise force an unbounded read/parse of a pathologically large file. A
// first-party CRD past maxYAMLFileSize must be excluded from discovery.
func TestLoadSnapshotSkipsOversizedFiles(t *testing.T) {
root := t.TempDir()
write := func(rel, content string) {
p := filepath.Join(root, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
padding := strings.Repeat("# padding\n", (maxYAMLFileSize/len("# padding\n"))+1)
write("packages/system/oversized/crds/widgets.yaml", padding+sampleCRD)
// A small, legitimate resource so the empty-snapshot guard doesn't fire
// for an unrelated reason.
write("packages/system/postgres-rd/cozyrds/postgres.yaml", sampleCozyRD)
snap, err := LoadSnapshot(root)
if err != nil {
t.Fatal(err)
}
if _, ok := snap[resourceKey{Group: "example.cozystack.io", Plural: "widgets"}]; ok {
t.Fatalf("CRD in a file past maxYAMLFileSize must be excluded: %v", snap)
}
}
// TestLoadSnapshotMergesAPIServiceOriginAcrossFiles covers the traceability
// fix: when a group's versions are registered via several sibling APIService
// objects in different files (the real-world shape — one object per
// version), the merged resource's Origin must mention every contributing
// file, not just the first one encountered.
func TestLoadSnapshotMergesAPIServiceOriginAcrossFiles(t *testing.T) {
root := t.TempDir()
write := func(rel, content string) {
p := filepath.Join(root, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
write("packages/system/cozyplane/templates/apiserver-v1alpha1.yaml", `apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata: {name: v1alpha1.sdn.cozystack.io}
spec: {group: sdn.cozystack.io, version: v1alpha1}
`)
write("packages/system/cozyplane/templates/apiserver-v1beta1.yaml", `apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata: {name: v1beta1.sdn.cozystack.io}
spec: {group: sdn.cozystack.io, version: v1beta1}
`)
snap, err := LoadSnapshot(root)
if err != nil {
t.Fatal(err)
}
res, ok := snap[resourceKey{Group: "sdn.cozystack.io", Plural: apiServicePlural}]
if !ok {
t.Fatalf("APIService group was not discovered: %v", snap)
}
for _, want := range []string{
"packages/system/cozyplane/templates/apiserver-v1alpha1.yaml",
"packages/system/cozyplane/templates/apiserver-v1beta1.yaml",
} {
if !strings.Contains(res.Origin, want) {
t.Fatalf("expected Origin to mention %q, got %q", want, res.Origin)
}
}
}
// BenchmarkLoadSnapshotRealTree tracks LoadSnapshot's wall-clock cost against
// this repository's own checkout — the actual worst case the CI gate runs
// against on every PR. Content-based discovery (see discoverYAMLFiles) scans
// every .yaml/.yml file under packages/ and internal/, not a curated
// directory allowlist, so this is meant to catch a future change that
// reintroduces redundant per-file work (e.g. splitting or parsing the same
// file's documents more than once). Run with:
//
// go test ./internal/apigate/ -run '^$' -bench LoadSnapshotRealTree
func BenchmarkLoadSnapshotRealTree(b *testing.B) {
for b.Loop() {
if _, err := LoadSnapshot("../.."); err != nil {
b.Fatal(err)
}
}
}
// TestLoadSnapshotEmptyErrors covers the silent-bypass guard: an empty result
// (e.g. wrong directory) must be an error, not a clean pass.
func TestLoadSnapshotEmptyErrors(t *testing.T) {
if _, err := LoadSnapshot(t.TempDir()); err == nil {
t.Fatal("expected an error for a directory with no API resources")
}
}
// TestIsFirstPartyGroup covers the subdomain check: only cozystack.io and its
// dot-delimited subdomains are first-party; a lookalike suffix is not.
func TestIsFirstPartyGroup(t *testing.T) {
cases := map[string]bool{
"cozystack.io": true,
"apps.cozystack.io": true,
"strategy.backups.cozystack.io": true,
"fakecozystack.io": false,
"cozystack.io.evil.com": false,
"cert-manager.io": false,
}
for group, want := range cases {
if got := isFirstPartyGroup(group); got != want {
t.Errorf("isFirstPartyGroup(%q) = %v, want %v", group, got, want)
}
}
}
// TestLoadSnapshotEndToEnd builds two minimal checkouts on disk and verifies
// the loader + classifier wire together across all three sources.
func TestLoadSnapshotEndToEnd(t *testing.T) {
base := t.TempDir()
head := t.TempDir()
write := func(root, rel, content string) {
p := filepath.Join(root, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
// Base has just Postgres.
write(base, "packages/system/postgres-rd/cozyrds/postgres.yaml", sampleCozyRD)
// Head keeps Postgres and adds a brand-new CRD group.
write(head, "packages/system/postgres-rd/cozyrds/postgres.yaml", sampleCozyRD)
write(head, "internal/crdinstall/manifests/example.yaml", sampleCRD)
baseSnap, err := LoadSnapshot(base)
if err != nil {
t.Fatal(err)
}
headSnap, err := LoadSnapshot(head)
if err != nil {
t.Fatal(err)
}
findings := Classify(baseSnap, headSnap)
if countCategory(findings, NewGroup) != 1 {
t.Fatalf("expected 1 new-group finding, got %v", findings)
}
}