diff --git a/Makefile b/Makefile index 72e4b0444..f6147d943 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ URL ?= $(REGISTRY)/$(IMG):$(VERSION) # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) CRD_OPTIONS ?= "crd:crdVersions=v1" # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. -ENVTEST_K8S_VERSION = 1.25 +ENVTEST_K8S_VERSION = 1.35.0 # Namespace to deploy resources into NAMESPACE ?= multicluster-engine @@ -248,7 +248,7 @@ podman-catalog-push: ## Push a catalog image. .PHONY: test test: envtest ## Run unit tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --arch=amd64 --bin-dir=$(LOCALBIN) --use-deprecated-gcs=false -p path)" go test `go list ./... | grep -v e2e` -coverprofile cover.out + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --arch=amd64 --bin-dir=$(LOCALBIN) -p path)" go test `go list ./... | grep -v e2e` -coverprofile cover.out integration-tests: ## Run functional/integration tests kubectl apply -f testserver/build/clusters.open-cluster-management.io_managedclusters.yaml @@ -306,7 +306,7 @@ ENVTEST ?= $(LOCALBIN)/setup-envtest ## Tool Versions KUSTOMIZE_VERSION ?= v4.5.7 CONTROLLER_TOOLS_VERSION ?= v0.19.0 -SETUP_ENVTEST_VERSION ?= release-0.17 +SETUP_ENVTEST_VERSION ?= release-0.23 KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" .PHONY: kustomize diff --git a/api/v1/discovery_webhook.go b/api/v1/discovery_webhook.go index 099ef1daa..1418c53e6 100644 --- a/api/v1/discovery_webhook.go +++ b/api/v1/discovery_webhook.go @@ -18,16 +18,16 @@ limitations under the License. package v1 import ( + "context" "fmt" "regexp" admissionregistration "k8s.io/api/admissionregistration/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" cl "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) @@ -89,38 +89,40 @@ func ValidatingWebhook(namespace string) *admissionregistration.ValidatingWebhoo func (r *DiscoveredCluster) SetupWebhookWithManager(mgr ctrl.Manager) error { Client = mgr.GetClient() - return ctrl.NewWebhookManagedBy(mgr). - For(r). + return builder.WebhookManagedBy(mgr, r). + WithDefaulter(r). + WithValidator(r). Complete() } -var _ webhook.Defaulter = &DiscoveredCluster{} +var _ admission.Defaulter[*DiscoveredCluster] = &DiscoveredCluster{} -// Default implements webhook.Defaulter so a webhook will be registered for the type -func (r *DiscoveredCluster) Default() { - discoveredclusterLog.Info("default", "Name", r.Name) +// Default implements admission.Defaulter so a webhook will be registered for the type +func (r *DiscoveredCluster) Default(_ context.Context, obj *DiscoveredCluster) error { + discoveredclusterLog.Info("default", "Name", obj.Name) + return nil } -var _ webhook.Validator = &DiscoveredCluster{} +var _ admission.Validator[*DiscoveredCluster] = &DiscoveredCluster{} -// ValidateCreate implements webhook.Validator so a webhook will be registered for the type -func (r *DiscoveredCluster) ValidateCreate() (admission.Warnings, error) { - discoveredclusterLog.Info("validate create", "Name", r.Name, "Type", r.Spec.Type) +// ValidateCreate implements admission.Validator so a webhook will be registered for the type +func (r *DiscoveredCluster) ValidateCreate(_ context.Context, obj *DiscoveredCluster) (admission.Warnings, error) { + discoveredclusterLog.Info("validate create", "Name", obj.Name, "Type", obj.Spec.Type) // Validate resource - if !IsSupportedClusterType(r.Spec.Type) && r.Spec.ImportAsManagedCluster { + if !IsSupportedClusterType(obj.Spec.Type) && obj.Spec.ImportAsManagedCluster { err := fmt.Errorf( "cannot create DiscoveredCluster '%s': importAsManagedCluster is not allowed for clusters of type '%s'. "+ - "Only ROSA type clusters support auto import", r.Name, r.Spec.Type) + "Only ROSA type clusters support auto import", obj.Name, obj.Spec.Type) discoveredclusterLog.Error(err, "validation failed") return nil, err } - if !IsStringValid(r.Spec.DisplayName) && r.Spec.ImportAsManagedCluster { + if !IsStringValid(obj.Spec.DisplayName) && obj.Spec.ImportAsManagedCluster { err := fmt.Errorf( "cannot update DiscoveredCluster '%s': importAsManagedCluster is not allowed for clusters with an invalid display name '%s'. "+ - "Display name must consist of lowercase alphanumeric characters or '-'", r.Name, r.Spec.DisplayName) + "Display name must consist of lowercase alphanumeric characters or '-'", obj.Name, obj.Spec.DisplayName) discoveredclusterLog.Error(err, "validation failed") return nil, err @@ -129,25 +131,24 @@ func (r *DiscoveredCluster) ValidateCreate() (admission.Warnings, error) { return nil, nil } -// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type -func (r *DiscoveredCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { - discoveredclusterLog.Info("validate update", "Name", r.Name, "Type", r.Spec.Type) +// ValidateUpdate implements admission.Validator so a webhook will be registered for the type +func (r *DiscoveredCluster) ValidateUpdate(_ context.Context, oldObj, newObj *DiscoveredCluster) (admission.Warnings, error) { + discoveredclusterLog.Info("validate update", "Name", newObj.Name, "Type", newObj.Spec.Type) // Validate resource - oldDiscoveredCluster := old.(*DiscoveredCluster) - if !IsSupportedClusterType(oldDiscoveredCluster.Spec.Type) && r.Spec.ImportAsManagedCluster { + if !IsSupportedClusterType(oldObj.Spec.Type) && newObj.Spec.ImportAsManagedCluster { err := fmt.Errorf( "cannot update DiscoveredCluster '%s': importAsManagedCluster is not allowed for clusters of type '%s'. "+ - "Only ROSA type clusters support auto import", r.Name, r.Spec.Type) + "Only ROSA type clusters support auto import", newObj.Name, newObj.Spec.Type) discoveredclusterLog.Error(err, "validation failed") return nil, err } - if !IsStringValid(r.Spec.DisplayName) && r.Spec.ImportAsManagedCluster { + if !IsStringValid(newObj.Spec.DisplayName) && newObj.Spec.ImportAsManagedCluster { err := fmt.Errorf( "cannot update DiscoveredCluster '%s': importAsManagedCluster is not allowed for clusters with an invalid display name '%s'. "+ - "Display name must consist of lowercase alphanumeric characters or '-'", r.Name, r.Spec.DisplayName) + "Display name must consist of lowercase alphanumeric characters or '-'", newObj.Name, newObj.Spec.DisplayName) discoveredclusterLog.Error(err, "validation failed") return nil, err @@ -156,9 +157,9 @@ func (r *DiscoveredCluster) ValidateUpdate(old runtime.Object) (admission.Warnin return nil, nil } -// ValidateDelete implements webhook.Validator so a webhook will be registered for the type -func (r *DiscoveredCluster) ValidateDelete() (admission.Warnings, error) { - discoveredclusterLog.Info("validate delete", "Name", r.Name, "Type", r.Spec.Type) +// ValidateDelete implements admission.Validator so a webhook will be registered for the type +func (r *DiscoveredCluster) ValidateDelete(_ context.Context, obj *DiscoveredCluster) (admission.Warnings, error) { + discoveredclusterLog.Info("validate delete", "Name", obj.Name, "Type", obj.Spec.Type) return nil, nil } diff --git a/api/v1/discovery_webhook_test.go b/api/v1/discovery_webhook_test.go new file mode 100644 index 000000000..339522515 --- /dev/null +++ b/api/v1/discovery_webhook_test.go @@ -0,0 +1,119 @@ +// Copyright Contributors to the Open Cluster Management project + +/* + +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 v1 + +import ( + "context" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func newDiscoveredCluster(clusterType, displayName string, importAsManagedCluster bool) *DiscoveredCluster { + return &DiscoveredCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster", Namespace: "test"}, + Spec: DiscoveredClusterSpec{ + Type: clusterType, + DisplayName: displayName, + ImportAsManagedCluster: importAsManagedCluster, + }, + } +} + +func TestDefault(t *testing.T) { + if err := (&DiscoveredCluster{}).Default(context.TODO(), newDiscoveredCluster("ROSA", "cluster", false)); err != nil { + t.Errorf("Default() error = %v, want nil", err) + } +} + +func TestValidateCreate(t *testing.T) { + tests := []struct { + name string + obj *DiscoveredCluster + wantErr bool + }{ + { + name: "supported type with auto import", + obj: newDiscoveredCluster("ROSA", "rosa-cluster", true), + wantErr: false, + }, + { + name: "unsupported type without auto import", + obj: newDiscoveredCluster("OCP", "ocp-cluster", false), + wantErr: false, + }, + { + name: "unsupported type with auto import", + obj: newDiscoveredCluster("OCP", "ocp-cluster", true), + wantErr: true, + }, + { + name: "invalid display name with auto import", + obj: newDiscoveredCluster("ROSA", "rosa_cluster", true), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := (&DiscoveredCluster{}).ValidateCreate(context.TODO(), tt.obj); (err != nil) != tt.wantErr { + t.Errorf("ValidateCreate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestValidateUpdate(t *testing.T) { + tests := []struct { + name string + oldObj *DiscoveredCluster + newObj *DiscoveredCluster + wantErr bool + }{ + { + name: "enable auto import on supported type", + oldObj: newDiscoveredCluster("MultiClusterEngineHCP", "hcp-cluster", false), + newObj: newDiscoveredCluster("MultiClusterEngineHCP", "hcp-cluster", true), + wantErr: false, + }, + { + name: "enable auto import on unsupported type", + oldObj: newDiscoveredCluster("OCP", "ocp-cluster", false), + newObj: newDiscoveredCluster("OCP", "ocp-cluster", true), + wantErr: true, + }, + { + name: "enable auto import with invalid display name", + oldObj: newDiscoveredCluster("ROSA", "rosa_cluster", false), + newObj: newDiscoveredCluster("ROSA", "rosa_cluster", true), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := (&DiscoveredCluster{}).ValidateUpdate(context.TODO(), tt.oldObj, tt.newObj); (err != nil) != tt.wantErr { + t.Errorf("ValidateUpdate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestValidateDelete(t *testing.T) { + if _, err := (&DiscoveredCluster{}).ValidateDelete(context.TODO(), newDiscoveredCluster("OCP", "ocp-cluster", true)); err != nil { + t.Errorf("ValidateDelete() error = %v, want nil", err) + } +} diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 5bf239a19..86fde51b0 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ limitations under the License. package v1 import ( - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/go.mod b/go.mod index ad7303aa3..95adeb3c8 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( k8s.io/client-go v0.35.3 k8s.io/metrics v0.35.3 open-cluster-management.io/api v0.16.2 - sigs.k8s.io/controller-runtime v0.19.4 + sigs.k8s.io/controller-runtime v0.23.3 sigs.k8s.io/yaml v1.6.0 ) @@ -61,6 +61,7 @@ require ( github.com/go-playground/validator/v10 v10.28.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 // indirect @@ -87,7 +88,6 @@ require ( go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.55.0 // indirect - golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect golang.org/x/mod v0.39.0 // indirect golang.org/x/net v0.58.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect diff --git a/go.sum b/go.sum index 605eba399..4cbd1e770 100644 --- a/go.sum +++ b/go.sum @@ -92,6 +92,8 @@ github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -210,8 +212,6 @@ golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= -golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= -golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= @@ -265,8 +265,8 @@ k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0x k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM= open-cluster-management.io/api v0.16.2 h1:JzpJtgp/qJKjDLEO7o7q5eVLxYkfgxhtagJvWFbaNno= open-cluster-management.io/api v0.16.2/go.mod h1:9erZEWEn4bEqh0nIX2wA7f/s3KCuFycQdBrPrRzi0QM= -sigs.k8s.io/controller-runtime v0.19.4 h1:SUmheabttt0nx8uJtoII4oIP27BVVvAKFvdvGFwV/Qo= -sigs.k8s.io/controller-runtime v0.19.4/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=