Skip to content

Commit 70cdfa9

Browse files
authored
Merge pull request #163 from SovereignCloudStack/kr/add-webhooks
✨ add webhooks for resources
2 parents 1073f7d + a09fb6e commit 70cdfa9

27 files changed

+2239
-74
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ test-integration: test-integration-workloadcluster test-integration-github
310310
.PHONY: test-unit
311311
test-unit: $(SETUP_ENVTEST) $(GOTESTSUM) $(HELM) ## Run unit
312312
@mkdir -p $(shell pwd)/.coverage
313-
CREATE_KIND_CLUSTER=true KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" $(GOTESTSUM) --junitfile=.coverage/junit.xml --format testname -- -mod=vendor \
313+
CREATE_KIND_CLUSTER=false KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" $(GOTESTSUM) --junitfile=.coverage/junit.xml --format testname -- -mod=vendor \
314314
-covermode=atomic -coverprofile=.coverage/cover.out -p=4 ./internal/controller/...
315315

316316
.PHONY: test-integration-workloadcluster

Tiltfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ def deploy_cso():
207207
"cso-serving-cert:certificate",
208208
"cso-cluster-stack-variables:secret",
209209
"cso-selfsigned-issuer:issuer",
210-
#"cso-validating-webhook-configuration:validatingwebhookconfiguration",
210+
"cso-validating-webhook-configuration:validatingwebhookconfiguration",
211211
],
212212
new_name = "cso-misc",
213213
labels = ["CSO"],

api/v1alpha1/cluster_webhook.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha1
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
"github.com/SovereignCloudStack/cluster-stack-operator/pkg/clusterstack"
24+
"github.com/SovereignCloudStack/cluster-stack-operator/pkg/release"
25+
apierrors "k8s.io/apimachinery/pkg/api/errors"
26+
"k8s.io/apimachinery/pkg/runtime"
27+
"k8s.io/apimachinery/pkg/types"
28+
"k8s.io/apimachinery/pkg/util/validation/field"
29+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
30+
ctrl "sigs.k8s.io/controller-runtime"
31+
"sigs.k8s.io/controller-runtime/pkg/client"
32+
"sigs.k8s.io/controller-runtime/pkg/webhook"
33+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
34+
)
35+
36+
// Cluster implements a validating and defaulting webhook for Cluster.
37+
// +k8s:deepcopy-gen=false
38+
type Cluster struct {
39+
Client client.Client
40+
}
41+
42+
// SetupWebhookWithManager initializes webhook manager for ClusterStack.
43+
func (r *Cluster) SetupWebhookWithManager(mgr ctrl.Manager) error {
44+
r.Client = mgr.GetClient()
45+
return ctrl.NewWebhookManagedBy(mgr).
46+
For(&clusterv1.Cluster{}).
47+
WithValidator(r).
48+
Complete()
49+
}
50+
51+
//+kubebuilder:webhook:path=/validate-cluster-x-k8s-io-v1beta1-cluster,mutating=false,failurePolicy=fail,sideEffects=None,groups=cluster.x-k8s.io,resources=clusters,verbs=create;update,versions=v1beta1,name=validation.cluster.cluster.x-k8s.io,admissionReviewVersions={v1,v1beta1}
52+
53+
var _ webhook.CustomValidator = &Cluster{}
54+
55+
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
56+
func (r *Cluster) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
57+
cluster, ok := obj.(*clusterv1.Cluster)
58+
if !ok {
59+
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", obj))
60+
}
61+
62+
return r.isVersionCorrect(ctx, cluster)
63+
}
64+
65+
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
66+
func (r *Cluster) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
67+
oldCluster, ok := oldObj.(*clusterv1.Cluster)
68+
if !ok {
69+
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an Cluster but got a %T", oldCluster))
70+
}
71+
72+
newCluster, ok := newObj.(*clusterv1.Cluster)
73+
if !ok {
74+
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an Cluster but got a %T", newCluster))
75+
}
76+
77+
var allErrs field.ErrorList
78+
79+
warnings, err := r.isVersionCorrect(ctx, newCluster)
80+
if len(warnings) > 0 || err != nil {
81+
return warnings, err
82+
}
83+
84+
csOld, err := clusterstack.NewFromClusterClassProperties(oldCluster.Spec.Topology.Class)
85+
if err != nil {
86+
return nil, fmt.Errorf("expected a clusterclass of form <provider>-<clusterStackName>-<kubernetesVersion>-<clusterStackVersion> but got %s: %w", oldCluster.Spec.Topology.Class, err)
87+
}
88+
89+
csNew, err := clusterstack.NewFromClusterClassProperties(newCluster.Spec.Topology.Class)
90+
if err != nil {
91+
return nil, fmt.Errorf("expected a clusterclass of form <provider>-<clusterStackName>-<kubernetesVersion>-<clusterStackVersion> but got %s: %w", newCluster.Spec.Topology.Class, err)
92+
}
93+
94+
// provider must not change
95+
if csOld.Provider != csNew.Provider {
96+
allErrs = append(allErrs,
97+
field.Invalid(field.NewPath("spec", "topology", "class"), newCluster.Spec.Topology.Class, fmt.Sprintf("provider name must not change. Got %s, want %s", csNew.Provider, csOld.Provider)))
98+
}
99+
100+
// cluster stack name must not change
101+
if csOld.Name != csNew.Name {
102+
allErrs = append(allErrs,
103+
field.Invalid(field.NewPath("spec", "topology", "class"), newCluster.Spec.Topology.Class, fmt.Sprintf("cluster stack name must not change. Got %s, want %s", csNew.Name, csOld.Name)))
104+
}
105+
106+
// kubernetes version must be the same or higher by one
107+
if csNew.KubernetesVersion.Minor-csOld.KubernetesVersion.Minor != 1 && csNew.KubernetesVersion.Minor-csOld.KubernetesVersion.Minor != 0 {
108+
allErrs = append(allErrs,
109+
field.Invalid(field.NewPath("spec", "topology", "class"), newCluster.Spec.Topology.Class, fmt.Sprintf("kubernetes version must be the same or higher by one. Got %s, want %s or 1-%d", csNew.KubernetesVersion, csOld.KubernetesVersion, csOld.KubernetesVersion.Minor+1)))
110+
}
111+
112+
return nil, aggregateObjErrors(oldCluster.GroupVersionKind().GroupKind(), oldCluster.Name, allErrs)
113+
}
114+
115+
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
116+
func (*Cluster) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
117+
return nil, nil
118+
}
119+
120+
func (r *Cluster) isVersionCorrect(ctx context.Context, cluster *clusterv1.Cluster) (admission.Warnings, error) {
121+
if cluster.Spec.Topology == nil {
122+
return nil, field.Invalid(field.NewPath("spec", "topology"), cluster.Spec.Topology, "topology field cannot be empty")
123+
}
124+
125+
if cluster.Spec.Topology.Class == "" {
126+
return nil, field.Invalid(field.NewPath("spec", "topology", "class"), cluster.Spec.Topology.Class, "class field cannot be empty")
127+
}
128+
129+
wantKubernetesVersion, err := r.getClusterStackReleaseVersion(ctx, release.ConvertFromClusterClassToClusterStackFormat(cluster.Spec.Topology.Class), cluster.Namespace)
130+
if err != nil {
131+
return admission.Warnings{fmt.Sprintf("cannot validate clusterClass and Kubernetes version. Getting clusterStackRelease object failed: %s", err.Error())}, nil
132+
}
133+
134+
if wantKubernetesVersion == "" {
135+
return admission.Warnings{fmt.Sprintf("no Kubernetes version set in status of clusterStackRelease object. Cannot validate Kubernetes version. Check out the ClusterStackReleaseObject %s/%s manually", cluster.Namespace, cluster.Spec.Topology.Class)}, nil
136+
}
137+
138+
if cluster.Spec.Topology.Version != wantKubernetesVersion {
139+
return nil, field.Invalid(field.NewPath("spec", "topology", "version"), cluster.Spec.Topology.Version, fmt.Sprintf("clusterClass %s expects Kubernetes version %s, but got %s", cluster.Spec.Topology.Class, wantKubernetesVersion, cluster.Spec.Topology.Version))
140+
}
141+
return nil, nil
142+
}
143+
144+
func (r *Cluster) getClusterStackReleaseVersion(ctx context.Context, name, namespace string) (string, error) {
145+
clusterStackRelCR := &ClusterStackRelease{}
146+
namespacedName := types.NamespacedName{Name: name, Namespace: namespace}
147+
148+
if err := r.Client.Get(ctx, namespacedName, clusterStackRelCR); apierrors.IsNotFound(err) {
149+
return "", fmt.Errorf("clusterclass does not exist: %w", err)
150+
} else if err != nil {
151+
return "", fmt.Errorf("failed to get ClusterStackRelease - cannot validate Kubernetes version: %w", err)
152+
}
153+
return clusterStackRelCR.Status.KubernetesVersion, nil
154+
}

api/v1alpha1/clusteraddon_webhook.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha1
18+
19+
import (
20+
"fmt"
21+
22+
apierrors "k8s.io/apimachinery/pkg/api/errors"
23+
"k8s.io/apimachinery/pkg/runtime"
24+
"k8s.io/apimachinery/pkg/util/validation/field"
25+
ctrl "sigs.k8s.io/controller-runtime"
26+
"sigs.k8s.io/controller-runtime/pkg/webhook"
27+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
28+
)
29+
30+
// SetupWebhookWithManager initializes webhook manager for ClusterAddon.
31+
func (r *ClusterAddon) SetupWebhookWithManager(mgr ctrl.Manager) error {
32+
return ctrl.NewWebhookManagedBy(mgr).
33+
For(r).
34+
Complete()
35+
}
36+
37+
// SetupWebhookWithManager initializes webhook manager for ClusterAddonList.
38+
func (r *ClusterAddonList) SetupWebhookWithManager(mgr ctrl.Manager) error {
39+
return ctrl.NewWebhookManagedBy(mgr).
40+
For(r).
41+
Complete()
42+
}
43+
44+
//+kubebuilder:webhook:path=/validate-clusterstack-x-k8s-io-v1alpha1-clusteraddon,mutating=false,failurePolicy=fail,sideEffects=None,groups=clusterstack.x-k8s.io,resources=clusteraddons,verbs=create;update,versions=v1alpha1,name=validation.clusteraddon.clusterstack.x-k8s.io,admissionReviewVersions={v1,v1alpha1}
45+
46+
var _ webhook.Validator = &ClusterAddon{}
47+
48+
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
49+
func (r *ClusterAddon) ValidateCreate() (admission.Warnings, error) {
50+
var allErrs field.ErrorList
51+
52+
if r.Spec.ClusterRef == nil {
53+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "clusterRef"), r.Spec.ClusterRef, "must not be empty"))
54+
} else if r.Spec.ClusterRef.Kind != "Cluster" {
55+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "clusterRef", "kind"), r.Spec.ClusterRef.Kind, "kind must be cluster"))
56+
}
57+
58+
return nil, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
59+
}
60+
61+
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
62+
func (r *ClusterAddon) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
63+
oldM, ok := old.(*ClusterAddon)
64+
if !ok {
65+
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an ClusterAddon but got a %T", old))
66+
}
67+
68+
var allErrs field.ErrorList
69+
70+
if r.Spec.ClusterRef == nil {
71+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "clusterRef"), r.Spec.ClusterRef, "must not be empty"))
72+
return nil, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
73+
}
74+
75+
// clusterRef.Name is immutable
76+
if oldM.Spec.ClusterRef.Name != r.Spec.ClusterRef.Name {
77+
allErrs = append(allErrs,
78+
field.Invalid(field.NewPath("spec", "clusterRef", "name"), r.Spec.ClusterRef.Name, "field is immutable"),
79+
)
80+
}
81+
82+
// namespace needs to always be the same for clusterAddon and cluster
83+
if r.Spec.ClusterRef.Namespace != r.Namespace {
84+
allErrs = append(allErrs,
85+
field.Invalid(field.NewPath("spec", "clusterRef", "namespace"), r.Spec.ClusterRef.Namespace, "cluster and clusterAddon need to be in same namespace"),
86+
)
87+
}
88+
89+
// clusterRef.kind is immutable
90+
if oldM.Spec.ClusterRef.Kind != r.Spec.ClusterRef.Kind {
91+
allErrs = append(allErrs,
92+
field.Invalid(field.NewPath("spec", "clusterRef", "kind"), r.Spec.ClusterRef.Kind, "field is immutable"),
93+
)
94+
}
95+
96+
return nil, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
97+
}
98+
99+
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
100+
func (*ClusterAddon) ValidateDelete() (admission.Warnings, error) {
101+
return nil, nil
102+
}

0 commit comments

Comments
 (0)