mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-30 00:27:04 +09:00
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>
107 lines
4.5 KiB
Go
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"])
|
|
}
|
|
}
|