Files
cozystack/cmd/cozystack-operator/main_test.go
T
Kirill Ilin dfd7cbed36 fix(operator): reject a Git cert secret before installing CRDs and Flux
CRDs and Flux are installed before the platform source, so refusing a
cert secret on a Git source only when the source is written exits the
operator after a partial install. The check now runs right after flag
parsing, and the source install reuses it.

The guard runs on every start, so a Git source without a cert secret is
now covered over https and ssh: a guard that misfired there would
crash-loop the operator for every Git platform source. The update test
also renames the cert Secret, as a CA rotation does.

Assisted-by: LLM
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-11-06 18:28:45 +05:00

914 lines
30 KiB
Go

/*
Copyright 2025 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 main
import (
"context"
"strings"
"testing"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
"github.com/cozystack/cozystack/internal/marketplace/tapconst"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func newTestScheme() *runtime.Scheme {
s := runtime.NewScheme()
_ = cozyv1alpha1.AddToScheme(s)
return s
}
func TestInstallPlatformPackageSource_Creates(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
// Verify name
if ps.Name != "cozystack.cozystack-platform" {
t.Errorf("expected name %q, got %q", "cozystack.cozystack-platform", ps.Name)
}
// Verify annotation
if ps.Annotations["operator.cozystack.io/skip-cozystack-values"] != "true" {
t.Errorf("expected skip-cozystack-values annotation to be 'true', got %q", ps.Annotations["operator.cozystack.io/skip-cozystack-values"])
}
// Verify sourceRef
if ps.Spec.SourceRef == nil {
t.Fatal("expected SourceRef to be set")
}
if ps.Spec.SourceRef.Kind != "OCIRepository" {
t.Errorf("expected sourceRef.kind %q, got %q", "OCIRepository", ps.Spec.SourceRef.Kind)
}
if ps.Spec.SourceRef.Name != "cozystack-platform" {
t.Errorf("expected sourceRef.name %q, got %q", "cozystack-platform", ps.Spec.SourceRef.Name)
}
if ps.Spec.SourceRef.Namespace != "cozy-system" {
t.Errorf("expected sourceRef.namespace %q, got %q", "cozy-system", ps.Spec.SourceRef.Namespace)
}
if ps.Spec.SourceRef.Path != "/" {
t.Errorf("expected sourceRef.path %q, got %q", "/", ps.Spec.SourceRef.Path)
}
// Verify variants
expectedVariants := []string{"default", "isp-full", "isp-hosted", "isp-full-generic", "isp-slim", "isp-slim-generic", "isp-hosted-slim"}
if len(ps.Spec.Variants) != len(expectedVariants) {
t.Fatalf("expected %d variants, got %d", len(expectedVariants), len(ps.Spec.Variants))
}
for i, name := range expectedVariants {
if ps.Spec.Variants[i].Name != name {
t.Errorf("expected variant[%d].name %q, got %q", i, name, ps.Spec.Variants[i].Name)
}
if len(ps.Spec.Variants[i].Components) != 1 {
t.Errorf("expected variant[%d] to have 1 component, got %d", i, len(ps.Spec.Variants[i].Components))
}
}
}
func TestInstallPlatformPackageSource_Updates(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
ctx := context.Background()
// The first apply establishes the operator as the owner of the object.
if err := installPlatformPackageSource(ctx, k8sClient, "cozystack-platform", "OCIRepository"); err != nil {
t.Fatalf("first apply: %v", err)
}
// Re-applying as the same owner is a legitimate update, so it must not
// conflict even though Force is no longer set. (A conflict would only arise
// against a different manager, which the refusal tests cover.)
if err := installPlatformPackageSource(ctx, k8sClient, "cozystack-platform", "OCIRepository"); err != nil {
t.Fatalf("re-apply for the operator's own object must not conflict: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(ctx, client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
if ps.Spec.SourceRef.Name != "cozystack-platform" {
t.Errorf("expected sourceRef.name %q, got %q", "cozystack-platform", ps.Spec.SourceRef.Name)
}
if len(ps.Spec.Variants) != 7 {
t.Errorf("expected 7 variants, got %d", len(ps.Spec.Variants))
}
}
// TestInstallPlatformPackageSource_RefusesTapOwned asserts that when a
// marketplace tap has materialized a PackageSource under a name the platform
// later ships, the platform refuses to overwrite it, names the conflict, and
// leaves the object untouched.
func TestInstallPlatformPackageSource_RefusesTapOwned(t *testing.T) {
s := newTestScheme()
tapOwned := &cozyv1alpha1.PackageSource{
ObjectMeta: metav1.ObjectMeta{
Name: "cozystack.cozystack-platform",
Labels: map[string]string{tapconst.Label: "true"},
Annotations: map[string]string{tapconst.SourceAnnotation: "tap-acme-platform"},
},
Spec: cozyv1alpha1.PackageSourceSpec{
SourceRef: &cozyv1alpha1.PackageSourceRef{Kind: "OCIRepository", Name: "tap-acme-platform", Namespace: "cozy-system"},
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(tapOwned).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
if err == nil || !strings.Contains(err.Error(), "owned by a marketplace tap") {
t.Fatalf("expected refusal naming the tap owner, got %v", err)
}
// The tapped object is left exactly as it was.
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatal(err)
}
if ps.Spec.SourceRef.Name != "tap-acme-platform" {
t.Errorf("the tapped PackageSource must be left untouched, sourceRef=%q", ps.Spec.SourceRef.Name)
}
}
// TestInstallPlatformPackageSource_RefusesForeignOwner asserts that a
// PackageSource owned by any other field manager (no tap label) is not
// force-overwritten: the server-side apply conflict surfaces as an error and the
// object is left untouched.
func TestInstallPlatformPackageSource_RefusesForeignOwner(t *testing.T) {
s := newTestScheme()
// WithObjects seeds an object owned by a foreign manager (before-first-apply)
// with a conflicting sourceRef and no tap label.
foreign := &cozyv1alpha1.PackageSource{
ObjectMeta: metav1.ObjectMeta{Name: "cozystack.cozystack-platform"},
Spec: cozyv1alpha1.PackageSourceSpec{
SourceRef: &cozyv1alpha1.PackageSourceRef{Kind: "OCIRepository", Name: "old-name", Namespace: "cozy-system"},
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(foreign).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
if err == nil || !strings.Contains(err.Error(), "conflict") {
t.Fatalf("expected a field-ownership conflict error, got %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatal(err)
}
if ps.Spec.SourceRef.Name != "old-name" {
t.Errorf("a foreign-owned PackageSource must be left untouched, sourceRef=%q", ps.Spec.SourceRef.Name)
}
}
func TestParsePlatformSourceURL(t *testing.T) {
tests := []struct {
name string
url string
wantType string
wantURL string
wantErr bool
}{
{
name: "OCI URL",
url: "oci://ghcr.io/cozystack/cozystack/cozystack-packages",
wantType: "oci",
wantURL: "oci://ghcr.io/cozystack/cozystack/cozystack-packages",
},
{
name: "HTTPS URL",
url: "https://github.com/cozystack/cozystack",
wantType: "git",
wantURL: "https://github.com/cozystack/cozystack",
},
{
name: "SSH URL",
url: "ssh://git@github.com/cozystack/cozystack",
wantType: "git",
wantURL: "ssh://git@github.com/cozystack/cozystack",
},
{
name: "empty URL",
url: "",
wantErr: true,
},
{
name: "unsupported scheme",
url: "ftp://example.com/repo",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sourceType, repoURL, err := parsePlatformSourceURL(tt.url)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error for URL %q, got nil", tt.url)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if sourceType != tt.wantType {
t.Errorf("expected type %q, got %q", tt.wantType, sourceType)
}
if repoURL != tt.wantURL {
t.Errorf("expected URL %q, got %q", tt.wantURL, repoURL)
}
})
}
}
func TestInstallPlatformPackageSource_VariantValuesFiles(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
expectedValuesFiles := map[string][]string{
"default": {"values.yaml"},
"isp-full": {"values.yaml", "values-isp-full.yaml"},
"isp-hosted": {"values.yaml", "values-isp-hosted.yaml"},
"isp-full-generic": {"values.yaml", "values-isp-full-generic.yaml"},
"isp-slim": {"values.yaml", "values-isp-slim.yaml"},
"isp-slim-generic": {"values.yaml", "values-isp-slim-generic.yaml"},
"isp-hosted-slim": {"values.yaml", "values-isp-hosted-slim.yaml"},
}
for _, v := range ps.Spec.Variants {
expected, ok := expectedValuesFiles[v.Name]
if !ok {
t.Errorf("unexpected variant %q", v.Name)
continue
}
if len(v.Components) != 1 {
t.Errorf("variant %q: expected 1 component, got %d", v.Name, len(v.Components))
continue
}
comp := v.Components[0]
if comp.Name != "platform" {
t.Errorf("variant %q: expected component name %q, got %q", v.Name, "platform", comp.Name)
}
if comp.Path != "core/platform" {
t.Errorf("variant %q: expected component path %q, got %q", v.Name, "core/platform", comp.Path)
}
if comp.Install == nil {
t.Errorf("variant %q: expected Install to be set", v.Name)
} else {
if comp.Install.Namespace != "cozy-system" {
t.Errorf("variant %q: expected install namespace %q, got %q", v.Name, "cozy-system", comp.Install.Namespace)
}
if comp.Install.ReleaseName != "cozystack-platform" {
t.Errorf("variant %q: expected install releaseName %q, got %q", v.Name, "cozystack-platform", comp.Install.ReleaseName)
}
}
if len(comp.ValuesFiles) != len(expected) {
t.Errorf("variant %q: expected %d valuesFiles, got %d", v.Name, len(expected), len(comp.ValuesFiles))
continue
}
for i, f := range expected {
if comp.ValuesFiles[i] != f {
t.Errorf("variant %q: expected valuesFiles[%d] %q, got %q", v.Name, i, f, comp.ValuesFiles[i])
}
}
}
}
func TestInstallPlatformPackageSource_CustomName(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "custom-source", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.custom-source"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
if ps.Name != "cozystack.custom-source" {
t.Errorf("expected name %q, got %q", "cozystack.custom-source", ps.Name)
}
if ps.Spec.SourceRef.Name != "custom-source" {
t.Errorf("expected sourceRef.name %q, got %q", "custom-source", ps.Spec.SourceRef.Name)
}
}
func TestInstallPlatformPackageSource_GitRepository(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "my-source", "GitRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.my-source"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
if ps.Spec.SourceRef.Kind != "GitRepository" {
t.Errorf("expected sourceRef.kind %q, got %q", "GitRepository", ps.Spec.SourceRef.Kind)
}
if ps.Spec.SourceRef.Name != "my-source" {
t.Errorf("expected sourceRef.name %q, got %q", "my-source", ps.Spec.SourceRef.Name)
}
}
func TestParseRefSpec(t *testing.T) {
tests := []struct {
name string
input string
want map[string]string
wantErr bool
}{
{
name: "empty string",
input: "",
want: map[string]string{},
},
{
name: "single key-value",
input: "tag=v1.0",
want: map[string]string{"tag": "v1.0"},
},
{
name: "multiple key-values",
input: "digest=sha256:abc123,tag=v1.0",
want: map[string]string{"digest": "sha256:abc123", "tag": "v1.0"},
},
{
name: "whitespace around pairs",
input: " tag=v1.0 , branch=main ",
want: map[string]string{"tag": "v1.0", "branch": "main"},
},
{
name: "equals sign in value",
input: "digest=sha256:abc=123",
want: map[string]string{"digest": "sha256:abc=123"},
},
{
name: "missing equals sign",
input: "tag",
wantErr: true,
},
{
name: "empty key",
input: "=value",
wantErr: true,
},
{
name: "empty value",
input: "tag=",
wantErr: true,
},
{
name: "trailing comma",
input: "tag=v1.0,",
want: map[string]string{"tag": "v1.0"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseRefSpec(tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error for input %q, got nil", tt.input)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(got) != len(tt.want) {
t.Fatalf("expected %d entries, got %d: %v", len(tt.want), len(got), got)
}
for k, v := range tt.want {
if got[k] != v {
t.Errorf("expected %q=%q, got %q=%q", k, v, k, got[k])
}
}
})
}
}
func TestValidateOCIRef(t *testing.T) {
tests := []struct {
name string
refMap map[string]string
wantErr bool
}{
{
name: "valid tag",
refMap: map[string]string{"tag": "v1.0"},
},
{
name: "valid digest",
refMap: map[string]string{"digest": "sha256:abc123def456"},
},
{
name: "valid semver",
refMap: map[string]string{"semver": ">=1.0.0"},
},
{
name: "multiple valid keys",
refMap: map[string]string{"tag": "v1.0", "digest": "sha256:abc"},
},
{
name: "empty map",
refMap: map[string]string{},
},
{
name: "invalid key",
refMap: map[string]string{"branch": "main"},
wantErr: true,
},
{
name: "invalid digest format",
refMap: map[string]string{"digest": "md5:abc"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateOCIRef(tt.refMap)
if tt.wantErr && err == nil {
t.Fatal("expected error, got nil")
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestValidateGitRef(t *testing.T) {
tests := []struct {
name string
refMap map[string]string
wantErr bool
}{
{
name: "valid branch",
refMap: map[string]string{"branch": "main"},
},
{
name: "valid commit",
refMap: map[string]string{"commit": "abc1234"},
},
{
name: "valid tag and branch",
refMap: map[string]string{"tag": "v1.0", "branch": "release"},
},
{
name: "empty map",
refMap: map[string]string{},
},
{
name: "invalid key",
refMap: map[string]string{"digest": "sha256:abc"},
wantErr: true,
},
{
name: "commit too short",
refMap: map[string]string{"commit": "abc"},
wantErr: true,
},
{
name: "commit not hex",
refMap: map[string]string{"commit": "zzzzzzz"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateGitRef(tt.refMap)
if tt.wantErr && err == nil {
t.Fatal("expected error, got nil")
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestGenerateOCIRepository(t *testing.T) {
refMap := map[string]string{"tag": "v1.0", "digest": "sha256:abc123"}
obj, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", refMap, "", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Name != "my-repo" {
t.Errorf("expected name %q, got %q", "my-repo", obj.Name)
}
if obj.Namespace != "cozy-system" {
t.Errorf("expected namespace %q, got %q", "cozy-system", obj.Namespace)
}
if obj.Spec.URL != "oci://registry.example.com/repo" {
t.Errorf("expected URL %q, got %q", "oci://registry.example.com/repo", obj.Spec.URL)
}
if obj.Spec.Reference == nil {
t.Fatal("expected Reference to be set")
}
if obj.Spec.Reference.Tag != "v1.0" {
t.Errorf("expected tag %q, got %q", "v1.0", obj.Spec.Reference.Tag)
}
if obj.Spec.Reference.Digest != "sha256:abc123" {
t.Errorf("expected digest %q, got %q", "sha256:abc123", obj.Spec.Reference.Digest)
}
if obj.Spec.SecretRef != nil {
t.Errorf("expected SecretRef to be nil when secretName is empty, got %+v", obj.Spec.SecretRef)
}
if obj.Spec.CertSecretRef != nil {
t.Errorf("expected CertSecretRef to be nil when certSecretName is empty, got %+v", obj.Spec.CertSecretRef)
}
}
func TestGenerateOCIRepository_NoRef(t *testing.T) {
obj, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", map[string]string{}, "", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Spec.Reference != nil {
t.Error("expected Reference to be nil for empty refMap")
}
}
func TestGenerateOCIRepository_InvalidRef(t *testing.T) {
_, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", map[string]string{"branch": "main"}, "", "")
if err == nil {
t.Fatal("expected error for invalid OCI ref key, got nil")
}
}
func TestGenerateOCIRepository_WithSecret(t *testing.T) {
obj, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", map[string]string{"tag": "v1.0"}, "my-registry-creds", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Spec.SecretRef == nil {
t.Fatal("expected SecretRef to be set when secretName is non-empty")
}
if obj.Spec.SecretRef.Name != "my-registry-creds" {
t.Errorf("expected SecretRef.Name %q, got %q", "my-registry-creds", obj.Spec.SecretRef.Name)
}
}
func TestGenerateOCIRepository_WithCertSecret(t *testing.T) {
obj, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", map[string]string{"tag": "v1.0"}, "", "registry-ca")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Spec.CertSecretRef == nil {
t.Fatal("expected CertSecretRef to be set when certSecretName is non-empty")
}
if obj.Spec.CertSecretRef.Name != "registry-ca" {
t.Errorf("expected CertSecretRef.Name %q, got %q", "registry-ca", obj.Spec.CertSecretRef.Name)
}
if obj.Spec.SecretRef != nil {
t.Errorf("expected SecretRef to stay nil when only certSecretName is set, got %+v", obj.Spec.SecretRef)
}
}
func newSourceTestClient(t *testing.T, objs ...client.Object) client.Client {
t.Helper()
scheme := newTestScheme()
if err := sourcev1.AddToScheme(scheme); err != nil {
t.Fatalf("add source scheme: %v", err)
}
return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build()
}
// The operator rewrites the whole platform OCIRepository on every start, so a
// certSecretRef patched onto the live object by hand is lost on the next
// restart and source-controller then fails TLS against a registry behind a
// private CA. The CA has to come from the operator's own arguments and survive
// that update path, including a rename of the Secret when the CA is rotated.
func TestInstallPlatformSourceResource_UpdateKeepsCertSecret(t *testing.T) {
existing := &sourcev1.OCIRepository{
ObjectMeta: metav1.ObjectMeta{Name: "cozystack-platform", Namespace: "cozy-system"},
Spec: sourcev1.OCIRepositorySpec{URL: "oci://ghcr.io/cozystack/cozystack/cozystack-packages"},
}
k8sClient := newSourceTestClient(t, existing)
for _, certSecret := range []string{"registry-ca", "registry-ca-next"} {
if err := installPlatformSourceResource(context.Background(), k8sClient,
"oci://registry.internal:8083/cozystack/cozystack/cozystack-packages", "cozystack-platform",
"digest=sha256:abc123", "registry-creds", certSecret); err != nil {
t.Fatalf("installPlatformSourceResource: %v", err)
}
}
got := &sourcev1.OCIRepository{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Namespace: "cozy-system", Name: "cozystack-platform"}, got); err != nil {
t.Fatalf("get OCIRepository: %v", err)
}
if got.Spec.CertSecretRef == nil || got.Spec.CertSecretRef.Name != "registry-ca-next" {
t.Errorf("expected CertSecretRef registry-ca-next after update, got %+v", got.Spec.CertSecretRef)
}
if got.Spec.SecretRef == nil || got.Spec.SecretRef.Name != "registry-creds" {
t.Errorf("expected SecretRef registry-creds after update, got %+v", got.Spec.SecretRef)
}
if got.Spec.URL != "oci://registry.internal:8083/cozystack/cozystack/cozystack-packages" {
t.Errorf("expected URL to be repointed, got %q", got.Spec.URL)
}
}
// GitRepository has no certSecretRef: Flux reads a Git CA from the ca.crt key
// of the secretRef Secret. Accepting the flag for a Git source would silently
// drop the CA, so it must fail before anything is written.
func TestInstallPlatformSourceResource_GitRejectsCertSecret(t *testing.T) {
k8sClient := newSourceTestClient(t)
err := installPlatformSourceResource(context.Background(), k8sClient,
"https://git.internal/cozystack/cozystack", "cozystack-platform", "branch=main", "git-creds", "git-ca")
if err == nil {
t.Fatal("expected an error for a cert secret on a Git platform source, got nil")
}
if !strings.Contains(err.Error(), "ca.crt") {
t.Errorf("expected the error to point at the ca.crt key of the source secret, got: %v", err)
}
list := &sourcev1.GitRepositoryList{}
if err := k8sClient.List(context.Background(), list); err != nil {
t.Fatalf("list GitRepositories: %v", err)
}
if len(list.Items) != 0 {
t.Errorf("expected no GitRepository to be written, got %d", len(list.Items))
}
}
// The Git guard runs on every start; one that misfires without a cert secret
// would crash-loop the operator for every Git platform source.
func TestInstallPlatformSourceResource_GitWithoutCertSecret(t *testing.T) {
for _, sourceURL := range []string{
"https://git.internal/cozystack/cozystack",
"ssh://git@git.internal/cozystack/cozystack",
} {
t.Run(sourceURL, func(t *testing.T) {
k8sClient := newSourceTestClient(t)
if err := installPlatformSourceResource(context.Background(), k8sClient,
sourceURL, "cozystack-platform", "branch=main", "git-creds", ""); err != nil {
t.Fatalf("installPlatformSourceResource: %v", err)
}
got := &sourcev1.GitRepository{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Namespace: "cozy-system", Name: "cozystack-platform"}, got); err != nil {
t.Fatalf("get GitRepository: %v", err)
}
if got.Spec.URL != sourceURL {
t.Errorf("expected URL %q, got %q", sourceURL, got.Spec.URL)
}
if got.Spec.SecretRef == nil || got.Spec.SecretRef.Name != "git-creds" {
t.Errorf("expected SecretRef git-creds, got %+v", got.Spec.SecretRef)
}
})
}
}
// Checked right after flag parsing: CRDs and Flux are installed before the
// platform source, so a combination rejected only there would exit after a
// partial install.
func TestValidatePlatformSourceCertSecret(t *testing.T) {
tests := []struct {
name string
sourceURL string
certSecret string
wantErr bool
}{
{name: "oci with cert secret", sourceURL: "oci://registry.internal:8083/cozystack-packages", certSecret: "registry-ca"},
{name: "oci without cert secret", sourceURL: "oci://registry.internal:8083/cozystack-packages"},
{name: "https git without cert secret", sourceURL: "https://git.internal/cozystack/cozystack"},
{name: "ssh git without cert secret", sourceURL: "ssh://git@git.internal/cozystack/cozystack"},
{name: "no source url", certSecret: "registry-ca"},
{name: "https git with cert secret", sourceURL: "https://git.internal/cozystack/cozystack", certSecret: "git-ca", wantErr: true},
{name: "ssh git with cert secret", sourceURL: "ssh://git@git.internal/cozystack/cozystack", certSecret: "git-ca", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validatePlatformSourceCertSecret(tt.sourceURL, tt.certSecret)
if (err != nil) != tt.wantErr {
t.Errorf("validatePlatformSourceCertSecret(%q, %q) error = %v, wantErr %v", tt.sourceURL, tt.certSecret, err, tt.wantErr)
}
})
}
}
func TestGenerateGitRepository(t *testing.T) {
refMap := map[string]string{"branch": "main", "commit": "abc1234def5678"}
obj, err := generateGitRepository("my-repo", "https://github.com/user/repo", refMap, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Name != "my-repo" {
t.Errorf("expected name %q, got %q", "my-repo", obj.Name)
}
if obj.Namespace != "cozy-system" {
t.Errorf("expected namespace %q, got %q", "cozy-system", obj.Namespace)
}
if obj.Spec.URL != "https://github.com/user/repo" {
t.Errorf("expected URL %q, got %q", "https://github.com/user/repo", obj.Spec.URL)
}
if obj.Spec.Reference == nil {
t.Fatal("expected Reference to be set")
}
if obj.Spec.Reference.Branch != "main" {
t.Errorf("expected branch %q, got %q", "main", obj.Spec.Reference.Branch)
}
if obj.Spec.Reference.Commit != "abc1234def5678" {
t.Errorf("expected commit %q, got %q", "abc1234def5678", obj.Spec.Reference.Commit)
}
if obj.Spec.SecretRef != nil {
t.Errorf("expected SecretRef to be nil when secretName is empty, got %+v", obj.Spec.SecretRef)
}
}
func TestGenerateGitRepository_NoRef(t *testing.T) {
obj, err := generateGitRepository("my-repo", "https://github.com/user/repo", map[string]string{}, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Spec.Reference != nil {
t.Error("expected Reference to be nil for empty refMap")
}
}
func TestGenerateGitRepository_InvalidRef(t *testing.T) {
_, err := generateGitRepository("my-repo", "https://github.com/user/repo", map[string]string{"digest": "sha256:abc"}, "")
if err == nil {
t.Fatal("expected error for invalid Git ref key, got nil")
}
}
func TestGenerateGitRepository_WithSecret(t *testing.T) {
obj, err := generateGitRepository("my-repo", "https://github.com/user/repo", map[string]string{"branch": "main"}, "my-git-creds")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Spec.SecretRef == nil {
t.Fatal("expected SecretRef to be set when secretName is non-empty")
}
if obj.Spec.SecretRef.Name != "my-git-creds" {
t.Errorf("expected SecretRef.Name %q, got %q", "my-git-creds", obj.Spec.SecretRef.Name)
}
}
// The system namespace memory flags are the only ones on this binary whose bad values do
// not surface here at all: a limit and a request that parse but contradict each other
// produce a LimitRange the API server rejects, once per system namespace, on every Package
// reconcile, with nothing in the error pointing back at a flag. Each branch below is a way
// that has happened or could.
func TestParseSystemNamespaceMemory(t *testing.T) {
tests := []struct {
name string
rawLimit string
rawRequest string
wantErr bool
wantLimit string
wantRequest string
}{
{
// Inputs follow the constants registered as flag defaults; literal
// expectations pin the shipped contract so changing either constant
// requires an intentional test update.
name: "defaults",
rawLimit: DefaultSystemNamespaceMemoryLimit,
rawRequest: DefaultSystemNamespaceMemoryRequest,
wantLimit: "32Gi",
wantRequest: "32Mi",
},
{
// Empty is the documented off switch, not a missing value: the chart
// leaves both keys empty unless an operator sets them.
name: "empty disables both",
rawLimit: "",
rawRequest: "",
wantLimit: "0",
wantRequest: "0",
},
{
// An explicit 0 limit is the other way to disable, and it must not
// trip the pair check against a request that is still set.
name: "zero limit disables and ignores the request",
rawLimit: "0",
rawRequest: "32Mi",
wantLimit: "0",
wantRequest: "32Mi",
},
{
// Supported on purpose: the container still gets memory.max, the
// scheduler reserves nothing. See the flag help.
name: "zero request against a real limit is accepted",
rawLimit: "4Gi",
rawRequest: "0",
wantLimit: "4Gi",
wantRequest: "0",
},
{
name: "malformed limit",
rawLimit: "4GB",
wantErr: true,
},
{
name: "malformed request",
rawLimit: "4Gi",
rawRequest: "thirty-two",
wantErr: true,
},
{
// Parses as a Quantity and is meaningless as memory.
name: "negative limit",
rawLimit: "-1Gi",
wantErr: true,
},
{
name: "negative request",
rawLimit: "4Gi",
rawRequest: "-32Mi",
wantErr: true,
},
{
// The one the API server would otherwise reject per namespace, per
// reconcile, forever.
name: "request above limit",
rawLimit: "1Gi",
rawRequest: "2Gi",
wantErr: true,
},
{
// Equal is the boundary and is legal: defaultRequest may equal default.
name: "request equal to limit",
rawLimit: "1Gi",
rawRequest: "1Gi",
wantLimit: "1Gi",
wantRequest: "1Gi",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
limit, request, err := parseSystemNamespaceMemory(tt.rawLimit, tt.rawRequest)
if tt.wantErr {
if err == nil {
t.Fatalf("parseSystemNamespaceMemory(%q, %q) = %s/%s, want an error",
tt.rawLimit, tt.rawRequest, limit.String(), request.String())
}
return
}
if err != nil {
t.Fatalf("parseSystemNamespaceMemory(%q, %q): %v", tt.rawLimit, tt.rawRequest, err)
}
if want := resource.MustParse(tt.wantLimit); limit.Cmp(want) != 0 {
t.Errorf("limit = %s, want %s", limit.String(), want.String())
}
if want := resource.MustParse(tt.wantRequest); request.Cmp(want) != 0 {
t.Errorf("request = %s, want %s", request.String(), want.String())
}
})
}
}