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 Mar 1, 2023
1 parent bf98cb3 commit 4508947
Show file tree
Hide file tree
Showing 21 changed files with 609 additions and 12 deletions.
4 changes: 1 addition & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ VERSION ?= 0.3-dev
GOOS ?= $(shell go env GOOS)
GOARCH ?= $(shell go env GOARCH)
SOURCES := $(shell find . -type f -name '*.go')
CRD_PATH ?= "manifests/charts/base/templates"
GIT_COMMIT_HASH ?= $(shell git rev-parse HEAD)
GIT_TREESTATE = "clean"
GIT_DIFF = $(shell git diff --quiet >/dev/null 2>&1; if [ $$? -eq 1 ]; then echo "1"; fi)
Expand Down Expand Up @@ -151,10 +150,9 @@ init-codegen:
.PHONY: gen-api
gen-api: gen-code gen-crd

# make it configurable, read CRD_PATH from env, default path is manifests/charts/base/templates
.PHONY: gen-crd
gen-crd: init-codegen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
CRD_PATH=$(CRD_PATH) hack/update-crdgen.sh
hack/update-crdgen.sh

.PHONY: gen-code
gen-code: init-codegen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
Expand Down
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
}
14 changes: 11 additions & 3 deletions hack/update-crdgen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@ set -o nounset
set -o pipefail

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

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"
kubectl kustomize manifests/charts/ -o "${CRD_PATH}"/infrastructure.cluster.x-k8s.io_customclusters.yaml
kubectl kustomize "${CRD_PATH}" -o "${CRD_PATH}"/infrastructure.cluster.x-k8s.io_customclusters.yaml
mv "${CRD_PATH}"/*.yaml "manifests/charts/base/templates/"

echo "running kustomize to generate the final Webhook"
kubectl kustomize "${WEBHOOK_PATH}" -o "${WEBHOOK_PATH}"/manifests.yaml
mv "${WEBHOOK_PATH}"/manifests.yaml "${OPERATOR_CHART_PATH}"/templates/webhooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ spec:
Machine on different infra.
x-kubernetes-preserve-unknown-fields: true
imageOS:
default: ubuntu
description: ImageOS is the OS of the image to use for the instance.
Defaults to "ubuntu".
type: string
Expand Down Expand Up @@ -195,12 +196,16 @@ spec:
- type
type: object
podCIDRs:
default:
- 192.168.0.0/16
description: PodCIDRs is the CIDR block for pods in this cluster.
Defaults to 192.168.0.0/16.
items:
type: string
type: array
serviceCIDRs:
default:
- 10.96.0.0/12
description: ServiceCIDRs is the CIDR block for services in this
cluster. Defaults to 10.96.0.0/12.
items:
Expand All @@ -210,6 +215,7 @@ spec:
description: VPC is the configuration for the VPC.
properties:
cidrBlock:
default: 10.0.0.0/16
description: CIDRBlock is the CIDR block to be used when the
provider creates a managed VPC. Defaults to 10.0.0.0/16.
type: string
Expand Down Expand Up @@ -250,6 +256,7 @@ spec:
Machine on different infra.
x-kubernetes-preserve-unknown-fields: true
imageOS:
default: ubuntu
description: ImageOS is the OS of the image to use for the instance.
Defaults to "ubuntu".
type: string
Expand Down
27 changes: 27 additions & 0 deletions manifests/charts/cluster-operator/templates/webhooks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
creationTimestamp: null
name: cluster-operator-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:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- cluster
sideEffects: None
6 changes: 0 additions & 6 deletions manifests/charts/kustomization.yaml

This file was deleted.

6 changes: 6 additions & 0 deletions manifests/config/crds/kustomization.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./infrastructure.cluster.x-k8s.io_customclusters.yaml
commonLabels:
cluster.x-k8s.io/v1beta1: v1alpha1
5 changes: 5 additions & 0 deletions manifests/config/webhooks/kustomization.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./manifests.yaml
namePrefix: cluster-operator-
6 changes: 6 additions & 0 deletions pkg/apis/cluster/v1alpha1/cluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ type NetworkConfig struct {
// PodCIDRs is the CIDR block for pods in this cluster.
// Defaults to 192.168.0.0/16.
// +optional
// +kubebuilder:default:={"192.168.0.0/16"}
PodCIDRs CIDRBlocks `json:"podCIDRs,omitempty"`
// ServiceCIDRs is the CIDR block for services in this cluster.
// Defaults to 10.96.0.0/12.
// +optional
// +kubebuilder:default:={"10.96.0.0/12"}
ServiceCIDRs CIDRBlocks `json:"serviceCIDRs,omitempty"`
// CNI is the configuration for the CNI.
CNI CNIConfig `json:"cni"`
Expand All @@ -107,6 +109,7 @@ type VPCConfig struct {
// CIDRBlock is the CIDR block to be used when the provider creates a managed VPC.
// Defaults to 10.0.0.0/16.
// +optional
// +kubebuilder:default:="10.0.0.0/16"
CIDRBlock string `json:"cidrBlock"`
}

Expand Down Expand Up @@ -179,6 +182,7 @@ type MachineConfig struct {
// ImageOS is the OS of the image to use for the instance.
// Defaults to "ubuntu".
// +optional
// +kubebuilder:default:="ubuntu"
ImageOS string `json:"imageOS,omitempty"`
// RootVolume is the root volume to attach to the instance.
// +optional
Expand Down Expand Up @@ -252,6 +256,8 @@ func (c *Cluster) SetConditions(conditions capiv1beta1.Conditions) {
c.Status.Conditions = conditions
}

// +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

// ClusterList contains a list of Cluster.
// +kubebuilder:object:root=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
Expand Down
175 changes: 175 additions & 0 deletions pkg/webhooks/cluster_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
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"
)

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{}).
WithValidator(wh).
Complete()
}

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.InfraType == v1.AWSClusterInfraType && 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 4508947

Please sign in to comment.