Skip to content

Commit

Permalink
cluster api:init webhook
Browse files Browse the repository at this point in the history
Signed-off-by: hejianpeng <hejianpeng2@huawei.com>
  • Loading branch information
zirain committed Feb 27, 2023
1 parent 755e09c commit 02357a5
Show file tree
Hide file tree
Showing 16 changed files with 677 additions and 1 deletion.
8 changes: 8 additions & 0 deletions cmd/cluster-operator/infra/infra.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ package infra

import (
"context"
"fmt"
"time"

ctrl "sigs.k8s.io/controller-runtime"

"kurator.dev/kurator/pkg/controllers/infra"
"kurator.dev/kurator/pkg/webhooks"
)

var log = ctrl.Log.WithName("infra cluster")
Expand All @@ -37,5 +39,11 @@ func InitControllers(ctx context.Context, mgr ctrl.Manager) error {
return err
}

if err := (&webhooks.ClusterWebhook{
Client: mgr.GetClient(),
}).SetupWebhookWithManager(mgr); err != nil {
return fmt.Errorf("unable to create Cluster webhook, %w", err)
}

return nil
}
4 changes: 3 additions & 1 deletion hack/update-crdgen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ set -o pipefail

CONTROLLER_GEN=${CONTROLLER_GEN:-"$(go env GOPATH)/bin/controller-gen"}
CRD_PATH=${CRD_PATH:-"manifests/charts/base/templates"}
WEBHOOK_PATH=${WEBHOOK_PATH:-"manifests/charts/cluster-operator/templates"}
APIS_PATHS=("./pkg/apis/cluster/..." "./pkg/apis/infra/...")

for APIS_PATH in "${APIS_PATHS[@]}"
do
echo "Generating CRD for ${APIS_PATH}"
${CONTROLLER_GEN} crd paths="${APIS_PATH}" output:crd:dir="${CRD_PATH}"
${CONTROLLER_GEN} crd paths="${APIS_PATH}" output:crd:dir="${CRD_PATH}" \
webhook output:webhook:dir="${WEBHOOK_PATH}"
done

echo "running kustomize to generate the final CRD"
Expand Down
56 changes: 56 additions & 0 deletions manifests/charts/base/templates/manifests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
creationTimestamp: null
name: mutating-webhook-configuration
webhooks:
- admissionReviewVersions:
- v1alpha1
clientConfig:
service:
name: webhook-service
namespace: system
path: /mutate-cluster-kurator-dev-v1alpha1-cluster
failurePolicy: Fail
matchPolicy: Equivalent
name: default.cluster.cluster.kurator.dev
rules:
- apiGroups:
- cluster.kurator.dev
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- machines
sideEffects: None
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
creationTimestamp: null
name: validating-webhook-configuration
webhooks:
- admissionReviewVersions:
- v1alpha1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-cluster-kurator-dev-v1alpha1-cluster
failurePolicy: Fail
matchPolicy: Equivalent
name: validation.cluster.cluster.kurator.dev
rules:
- apiGroups:
- cluster.kurator.dev
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- machines
sideEffects: None
2 changes: 2 additions & 0 deletions pkg/apis/cluster/v1alpha1/cluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import (
// +kubebuilder:printcolumn:name="InfraType",type="string",JSONPath=".spec.infraType",description="Infra type of the cluster"
// +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".spec.version",description="Kubernetes version of the cluster"
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Phase of the cluster"
// +kubebuilder:webhook:verbs=create;update,path=/validate-cluster-kurator-dev-v1alpha1-cluster,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.kurator.dev,resources=cluster,versions=v1alpha1,name=validation.cluster.cluster.kurator.dev,sideEffects=None,admissionReviewVersions=v1alpha1
// +kubebuilder:webhook:verbs=create;update,path=/mutate-cluster-kurator-dev-v1alpha1-cluster,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.kurator.dev,resources=cluster,versions=v1alpha1,name=default.cluster.cluster.kurator.dev,sideEffects=None,admissionReviewVersions=v1alpha1
type Cluster struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Expand Down
204 changes: 204 additions & 0 deletions pkg/webhooks/cluster_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
Copyright Kurator 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 webhooks

import (
"context"
"fmt"
"net"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/cluster-api/util/version"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook"

v1 "kurator.dev/kurator/pkg/apis/cluster/v1alpha1"
)

const (
defaultPodCIDR = "192.168.0.0/16"
defaultServiceCIDR = "10.96.0.0/12"
defaultVpcCIDR = "10.0.0.0/16"
)

var _ webhook.CustomDefaulter = &ClusterWebhook{}
var _ webhook.CustomValidator = &ClusterWebhook{}

type ClusterWebhook struct {
Client client.Reader
}

func (wh *ClusterWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(&v1.Cluster{}).
WithDefaulter(wh).
WithValidator(wh).
Complete()
}

func (wh *ClusterWebhook) Default(_ context.Context, obj runtime.Object) error {
in, ok := obj.(*v1.Cluster)
if !ok {
return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", obj))
}

if len(in.Spec.Network.PodCIDRs) == 0 {
in.Spec.Network.PodCIDRs = []string{defaultPodCIDR}
}

if len(in.Spec.Network.ServiceCIDRs) == 0 {
in.Spec.Network.ServiceCIDRs = []string{defaultServiceCIDR}
}

if in.Spec.Network.VPC.CIDRBlock == "" {
in.Spec.Network.VPC.CIDRBlock = defaultVpcCIDR
}

return nil
}

func (wh *ClusterWebhook) ValidateCreate(_ context.Context, obj runtime.Object) error {
in, ok := obj.(*v1.Cluster)
if !ok {
return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", obj))
}

return wh.validate(in)
}

func validateInfra(in *v1.Cluster) field.ErrorList {
var allErrs field.ErrorList
if in.Spec.InfraType == "" {
allErrs = append(allErrs, field.Required(field.NewPath("spec", "infraType"), "must be set"))
}

if in.Spec.Region == "" {
allErrs = append(allErrs, field.Required(field.NewPath("spec", "region"), "must be set"))
}

if in.Spec.Credential == nil || in.Spec.Credential.SecretRef == "" {
allErrs = append(allErrs, field.Required(field.NewPath("spec", "credential", "secretRef"), "must be set"))
}

return allErrs
}

func validateVersion(in *v1.Cluster) field.ErrorList {
var allErrs field.ErrorList
if in.Spec.Version == "" {
allErrs = append(allErrs, field.Required(field.NewPath("spec", "version"), "must be set"))
} else {
if !version.KubeSemver.MatchString(in.Spec.Version) {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "version"), in.Spec.Version, "must be a valid Kubernetes version"))
}
}

return allErrs
}

func validateNetwork(network v1.NetworkConfig, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, validateCIDR(network.VPC.CIDRBlock, fldPath.Child("vpc", "cidrBlock"))...)
allErrs = append(allErrs, validateCIDRBlocks(network.PodCIDRs, fldPath.Child("podCIDRs"))...)
allErrs = append(allErrs, validateCIDRBlocks(network.ServiceCIDRs, fldPath.Child("serviceCIDRs"))...)

return allErrs
}

func validateCIDR(cidr string, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if _, _, err := net.ParseCIDR(cidr); err != nil {
allErrs = append(allErrs, field.Invalid(fldPath, cidr, err.Error()))
}
return allErrs
}

func validateCIDRBlocks(cidrBlocks []string, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList

for i, cidr := range cidrBlocks {
if _, _, err := net.ParseCIDR(cidr); err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), cidr, err.Error()))
}
}

return allErrs
}

func validateAdditionalResource(in []v1.ResourceRef, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if len(in) == 0 {
return allErrs
}

keys := sets.NewString()
for i, resource := range in {
if resource.Name == "" {
allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("name"), "must be set"))
}
if resource.Kind != "Secret" && resource.Kind != "ConfigMap" {
allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("type"), "must be either Secret or ConfigMap"))
}
key := fmt.Sprintf("%s/%s", resource.Kind, resource.Name)
if keys.Has(key) {
allErrs = append(allErrs, field.Duplicate(fldPath.Index(i), key))
} else {
keys.Insert(key)
}
}

return allErrs
}

func (wh *ClusterWebhook) validate(in *v1.Cluster) error {
var allErrs field.ErrorList

// The Cluste name is used as a label value, so it must be a valid label value.
if errs := validation.IsValidLabelValue(in.Name); len(errs) > 0 {
for _, err := range errs {
allErrs = append(allErrs, field.Invalid(field.NewPath("metadata", "name"), in.Name, fmt.Sprintf("must be a valid label value: %s", err)))
}
}

allErrs = append(allErrs, validateInfra(in)...)
allErrs = append(allErrs, validateVersion(in)...)
allErrs = append(allErrs, validateNetwork(in.Spec.Network, field.NewPath("spec", "network"))...)
allErrs = append(allErrs, validateAdditionalResource(in.Spec.AdditionalResources, field.NewPath("spec", "additionalResources"))...)

if len(allErrs) > 0 {
return apierrors.NewInvalid(v1.SchemeGroupVersion.WithKind("Cluster").GroupKind(), in.Name, allErrs)
}
return nil
}

func (wh *ClusterWebhook) ValidateUpdate(_ context.Context, obj runtime.Object, _ runtime.Object) error {
in, ok := obj.(*v1.Cluster)
if !ok {
return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", obj))
}

return wh.validate(in)
}

func (wh *ClusterWebhook) ValidateDelete(_ context.Context, obj runtime.Object) error {
return nil
}

0 comments on commit 02357a5

Please sign in to comment.