Files
cozystack/pkg/cmd/server/start_owner_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

107 lines
4.5 KiB
Go

package server
import (
"testing"
"time"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/cozystack/cozystack/api/v1alpha1"
)
// TestOwnedDefinitionsDropsKindDuplicate pins that cozystack-api registers one
// resource per kind: a definition declaring a kind an older definition already
// owns is left out rather than registered as a second resource for one GVK, and
// is reported so the operator can see why its kind is missing.
func TestOwnedDefinitionsDropsKindDuplicate(t *testing.T) {
base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
mk := func(name, kind string, at time.Time) v1alpha1.ApplicationDefinition {
d := v1alpha1.ApplicationDefinition{ObjectMeta: metav1.ObjectMeta{Name: name, CreationTimestamp: metav1.NewTime(at)}}
d.Spec.Application.Kind = kind
return d
}
kept, skipped := ownedDefinitions([]v1alpha1.ApplicationDefinition{
mk("a-redis-shadow", "Redis", base.Add(time.Hour)),
mk("redis", "Redis", base),
mk("postgres", "Postgres", base),
mk("no-kind", "", base),
})
names := map[string]bool{}
for _, d := range kept {
names[d.Name] = true
}
// A definition with no kind claims nothing, so it is not anyone's duplicate.
if len(kept) != 3 || !names["redis"] || !names["postgres"] || !names["no-kind"] {
t.Fatalf("kept = %v, want redis, postgres and no-kind", names)
}
if len(skipped) != 1 || skipped[0] != `ApplicationDefinition "a-redis-shadow" declares kind Redis already owned by "redis"` {
t.Fatalf("skipped = %q", skipped)
}
}
// TestResourceConfigFromRegistersOwnerOnly pins that the resource config
// cozystack-api serves is built from the owners only: one resource per kind,
// carrying the owner's chart.
func TestResourceConfigFromRegistersOwnerOnly(t *testing.T) {
base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
mk := func(name, artifact string, at time.Time) v1alpha1.ApplicationDefinition {
d := v1alpha1.ApplicationDefinition{ObjectMeta: metav1.ObjectMeta{Name: name, CreationTimestamp: metav1.NewTime(at)}}
d.Spec.Application.Kind = "Redis"
d.Spec.Release.ChartRef = &helmv2.CrossNamespaceSourceReference{Kind: "ExternalArtifact", Name: artifact, Namespace: "cozy-system"}
return d
}
rc, err := resourceConfigFrom([]v1alpha1.ApplicationDefinition{
mk("a-redis-shadow", "shadow-artifact", base.Add(time.Hour)),
mk("redis", "redis-artifact", base),
}, helmReleaseFlagValues{})
if err != nil {
t.Fatal(err)
}
if len(rc.Resources) != 1 || rc.Resources[0].Release.ChartRef.Name != "redis-artifact" {
t.Fatalf("resources = %+v, want the owner's redis-artifact only", rc.Resources)
}
}
// TestResourceConfigFromSkipsUnservableCredentials pins that a definition
// whose credentials block the API cannot honour leaves its kind unserved,
// while a block it can honour reaches the resource config.
func TestResourceConfigFromSkipsUnservableCredentials(t *testing.T) {
mk := func(name, kind string, block *v1alpha1.ApplicationDefinitionCredentials) v1alpha1.ApplicationDefinition {
d := v1alpha1.ApplicationDefinition{ObjectMeta: metav1.ObjectMeta{Name: name}}
d.Spec.Application.Kind = kind
d.Spec.Release.ChartRef = &helmv2.CrossNamespaceSourceReference{Kind: "ExternalArtifact", Name: name, Namespace: "cozy-system"}
d.Spec.Credentials = block
return d
}
served := &v1alpha1.ApplicationDefinitionCredentials{
Format: v1alpha1.CredentialFormat{Name: "scram-sha-256"},
Accounts: []v1alpha1.CredentialAccountSource{{Kind: v1alpha1.CredentialAccountsValuesMap, Path: "users"}},
}
unbuilt := &v1alpha1.ApplicationDefinitionCredentials{
Format: v1alpha1.CredentialFormat{Name: "apr1", Kind: v1alpha1.CredentialFormatWebhook},
Accounts: []v1alpha1.CredentialAccountSource{{Kind: v1alpha1.CredentialAccountsValuesMap, Path: "users"}},
}
rc, err := resourceConfigFrom([]v1alpha1.ApplicationDefinition{
mk("postgres", "Postgres", served),
mk("foo", "Foo", unbuilt),
mk("redis", "Redis", nil),
}, helmReleaseFlagValues{})
if err != nil {
t.Fatal(err)
}
kinds := map[string]*v1alpha1.ApplicationDefinitionCredentials{}
for _, r := range rc.Resources {
kinds[r.Application.Kind] = r.Credentials
}
if len(kinds) != 2 || kinds["Postgres"] == nil || kinds["Postgres"].Format.Name != "scram-sha-256" {
t.Fatalf("resources = %v, want Postgres with its block and Redis without", kinds)
}
if _, ok := kinds["Foo"]; ok {
t.Fatal("a kind with an unbuilt format must not be served")
}
if _, ok := kinds["Redis"]; !ok || kinds["Redis"] != nil {
t.Fatalf("Redis = %v, want served with no block", kinds["Redis"])
}
}