Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
55 changes: 28 additions & 27 deletions api/v1/discovery_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
}

Expand Down
119 changes: 119 additions & 0 deletions api/v1/discovery_webhook_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 1 addition & 1 deletion api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
Loading