cozystack/internal/manifestutil/crd_test.go
Aleksei Sviridkin 09805ff382
fix(manifestutil): check apiVersion in CollectCRDNames for consistent GVK matching
CollectCRDNames now requires both apiVersion "apiextensions.k8s.io/v1"
and kind "CustomResourceDefinition", consistent with the validation in
crdinstall.Install.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-18 00:49:57 +03:00

202 lines
6.2 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 manifestutil
import (
"context"
"strings"
"testing"
"time"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
)
func TestCollectCRDNames(t *testing.T) {
objects := []*unstructured.Unstructured{
{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": map[string]interface{}{"name": "test-ns"},
}},
{Object: map[string]interface{}{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": map[string]interface{}{"name": "packages.cozystack.io"},
}},
{Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{"name": "test-deploy"},
}},
{Object: map[string]interface{}{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": map[string]interface{}{"name": "packagesources.cozystack.io"},
}},
}
names := CollectCRDNames(objects)
if len(names) != 2 {
t.Fatalf("CollectCRDNames() returned %d names, want 2", len(names))
}
if names[0] != "packages.cozystack.io" {
t.Errorf("names[0] = %q, want %q", names[0], "packages.cozystack.io")
}
if names[1] != "packagesources.cozystack.io" {
t.Errorf("names[1] = %q, want %q", names[1], "packagesources.cozystack.io")
}
}
func TestCollectCRDNames_ignoresWrongAPIVersion(t *testing.T) {
objects := []*unstructured.Unstructured{
{Object: map[string]interface{}{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": map[string]interface{}{"name": "real.crd.io"},
}},
{Object: map[string]interface{}{
"apiVersion": "apiextensions.k8s.io/v1beta1",
"kind": "CustomResourceDefinition",
"metadata": map[string]interface{}{"name": "legacy.crd.io"},
}},
}
names := CollectCRDNames(objects)
if len(names) != 1 {
t.Fatalf("CollectCRDNames() returned %d names, want 1", len(names))
}
if names[0] != "real.crd.io" {
t.Errorf("names[0] = %q, want %q", names[0], "real.crd.io")
}
}
func TestCollectCRDNames_noCRDs(t *testing.T) {
objects := []*unstructured.Unstructured{
{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": map[string]interface{}{"name": "test"},
}},
}
names := CollectCRDNames(objects)
if len(names) != 0 {
t.Errorf("CollectCRDNames() returned %d names, want 0", len(names))
}
}
func TestWaitForCRDsEstablished_success(t *testing.T) {
log.SetLogger(zap.New(zap.UseDevMode(true)))
scheme := runtime.NewScheme()
if err := apiextensionsv1.AddToScheme(scheme); err != nil {
t.Fatalf("failed to add apiextensions to scheme: %v", err)
}
// Create a CRD object in the fake client
crd := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": map[string]interface{}{"name": "packages.cozystack.io"},
}}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(crd).
WithInterceptorFuncs(interceptor.Funcs{
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
if err := c.Get(ctx, key, obj, opts...); err != nil {
return err
}
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return nil
}
if u.GetKind() == "CustomResourceDefinition" {
_ = unstructured.SetNestedSlice(u.Object, []interface{}{
map[string]interface{}{
"type": "Established",
"status": "True",
},
}, "status", "conditions")
}
return nil
},
}).
Build()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx = log.IntoContext(ctx, log.FromContext(context.Background()))
err := WaitForCRDsEstablished(ctx, fakeClient, []string{"packages.cozystack.io"})
if err != nil {
t.Fatalf("WaitForCRDsEstablished() error = %v", err)
}
}
func TestWaitForCRDsEstablished_timeout(t *testing.T) {
log.SetLogger(zap.New(zap.UseDevMode(true)))
scheme := runtime.NewScheme()
if err := apiextensionsv1.AddToScheme(scheme); err != nil {
t.Fatalf("failed to add apiextensions to scheme: %v", err)
}
// CRD exists but never gets Established condition
crd := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": map[string]interface{}{"name": "packages.cozystack.io"},
}}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(crd).
Build()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
ctx = log.IntoContext(ctx, log.FromContext(context.Background()))
err := WaitForCRDsEstablished(ctx, fakeClient, []string{"packages.cozystack.io"})
if err == nil {
t.Fatal("WaitForCRDsEstablished() expected error on timeout, got nil")
}
if !strings.Contains(err.Error(), "packages.cozystack.io") {
t.Errorf("error should mention stuck CRD name, got: %v", err)
}
}
func TestWaitForCRDsEstablished_empty(t *testing.T) {
scheme := runtime.NewScheme()
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
ctx := context.Background()
err := WaitForCRDsEstablished(ctx, fakeClient, nil)
if err != nil {
t.Fatalf("WaitForCRDsEstablished() with empty names should return nil, got: %v", err)
}
}