Guard PackageSource creation on platformSourceURL != "" to avoid dangling SourceRef when no Flux source resource exists. Add tests covering all new and modified functions: - parsePlatformSourceURL (OCI, HTTPS, SSH, empty, unsupported scheme) - parseRefSpec (single/multi values, whitespace, equals in value, missing equals, empty key/value, trailing comma) - validateOCIRef / validateGitRef (valid keys, invalid keys, format) - generateOCIRepository / generateGitRepository (with ref, no ref, invalid ref) - installPlatformPackageSource variant valuesFiles validation - installPlatformPackageSource custom name Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
574 lines
16 KiB
Go
574 lines
16 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"
|
|
"testing"
|
|
|
|
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
|
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"}
|
|
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()
|
|
|
|
existing := &cozyv1alpha1.PackageSource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "cozystack.cozystack-platform",
|
|
ResourceVersion: "1",
|
|
Labels: map[string]string{
|
|
"custom-label": "should-be-preserved",
|
|
},
|
|
},
|
|
Spec: cozyv1alpha1.PackageSourceSpec{
|
|
SourceRef: &cozyv1alpha1.PackageSourceRef{
|
|
Kind: "OCIRepository",
|
|
Name: "old-name",
|
|
Namespace: "cozy-system",
|
|
},
|
|
},
|
|
}
|
|
|
|
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(existing).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 sourceRef was updated
|
|
if ps.Spec.SourceRef.Name != "cozystack-platform" {
|
|
t.Errorf("expected updated sourceRef.name %q, got %q", "cozystack-platform", ps.Spec.SourceRef.Name)
|
|
}
|
|
|
|
// Verify all 4 variants are present after update
|
|
if len(ps.Spec.Variants) != 4 {
|
|
t.Errorf("expected 4 variants after update, got %d", len(ps.Spec.Variants))
|
|
}
|
|
|
|
// Verify that labels set by other controllers are preserved (SSA does not overwrite unmanaged fields)
|
|
if ps.Labels["custom-label"] != "should-be-preserved" {
|
|
t.Errorf("expected custom-label to be preserved, got %q", ps.Labels["custom-label"])
|
|
}
|
|
}
|
|
|
|
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"},
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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 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)
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|