Skip to content

Commit

Permalink
separate webhooks from the api types package (#672)
Browse files Browse the repository at this point in the history
* separate webhooks from the api types package

coupling api/ type packages with webhook by implementing
webhook.Validator interface is, in hindsight, a bad design since api
types are frequently imported in other projects and should therefore be
as minimal as possible. The issue is discussed in kubernetes-sigs/controller-runtime#2596.

B/c of that, webhook.Validator and webhook.Defaulter interfaces are
deprecated from controller-runtime and removed in v0.20

Moving webhooks to a separate package allows repos that import
api/v1beta1 to upgrade controller-runtime to v0.20, and also prepares
capdo for controller-runtime v0.20 upgrade

Signed-off-by: dkomsa <[email protected]>

* fix linting issues

---------

Signed-off-by: dkomsa <[email protected]>
  • Loading branch information
d-honeybadger authored Feb 4, 2025
1 parent 59eae28 commit 06b7296
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 145 deletions.
82 changes: 0 additions & 82 deletions api/v1beta1/domachinetemplate_webhook.go

This file was deleted.

2 changes: 1 addition & 1 deletion api/v1beta1/zz_generated.deepcopy.go

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

Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package v1beta1
// Package webhooks contains implementations of validating and mutating webhooks for infrastructure.cluster.x-k8s.io resources.
package webhooks

import (
"context"
"fmt"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"

"sigs.k8s.io/cluster-api-provider-digitalocean/api/v1beta1"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
Expand All @@ -35,46 +38,58 @@ var _ = logf.Log.WithName("docluster-resource")
// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-docluster,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=doclusters,versions=v1beta1,name=validation.docluster.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-docluster,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=doclusters,versions=v1beta1,name=default.docluster.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1

// DOClusterWebhook is a collection of webhooks for DOCluster objects.
type DOClusterWebhook struct{}

var (
_ webhook.Defaulter = &DOCluster{}
_ webhook.Validator = &DOCluster{}
_ webhook.CustomDefaulter = &DOClusterWebhook{}
_ webhook.CustomValidator = &DOClusterWebhook{}
)

func (r *DOCluster) SetupWebhookWithManager(mgr ctrl.Manager) error {
func (w *DOClusterWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
For(&v1beta1.DOCluster{}).
WithDefaulter(&DOClusterWebhook{}).
WithValidator(&DOClusterWebhook{}).
Complete()
}

// Default implements webhook.Defaulter so a webhook will be registered for the type.
func (r *DOCluster) Default() {}
// Default implements webhook.CustomDefaulter so a webhook will be registered for the type.
func (w *DOClusterWebhook) Default(context.Context, runtime.Object) error {
return nil
}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
func (r *DOCluster) ValidateCreate() (admission.Warnings, error) {
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type.
func (w *DOClusterWebhook) ValidateCreate(context.Context, runtime.Object) (admission.Warnings, error) {
return nil, nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
func (r *DOCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type.
func (w *DOClusterWebhook) ValidateUpdate(_ context.Context, objOld, objNew runtime.Object) (admission.Warnings, error) {
var allErrs field.ErrorList

oldDOCluster, ok := old.(*DOCluster)
oldDOCluster, ok := objOld.(*v1beta1.DOCluster)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an DOCluster old object but got a %T", objOld))
}

newDOCluster, ok := objNew.(*v1beta1.DOCluster)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an DOCluster but got a %T", old))
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an DOCluster new object but got a %T", objNew))
}

if r.Spec.Region != oldDOCluster.Spec.Region {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "region"), r.Spec.Region, "field is immutable"))
if newDOCluster.Spec.Region != oldDOCluster.Spec.Region {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "region"), newDOCluster.Spec.Region, "field is immutable"))
}

if len(allErrs) == 0 {
return nil, nil
}

return nil, apierrors.NewInvalid(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
return nil, apierrors.NewInvalid(newDOCluster.GroupVersionKind().GroupKind(), newDOCluster.Name, allErrs)
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
func (r *DOCluster) ValidateDelete() (admission.Warnings, error) {
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type.
func (w *DOClusterWebhook) ValidateDelete(context.Context, runtime.Object) (admission.Warnings, error) {
return nil, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,63 +14,73 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package v1beta1
package webhooks

import (
"context"
"fmt"
"reflect"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/cluster-api-provider-digitalocean/api/v1beta1"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// log is for logging in this package.
var doclustertemplatelog = logf.Log.WithName("doclustertemplate-resource")
var _ = logf.Log.WithName("doclustertemplate-resource")

func (r *DOClusterTemplate) SetupWebhookWithManager(mgr ctrl.Manager) error {
func (w *DOClusterTemplateWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
For(&v1beta1.DOClusterTemplate{}).
WithDefaulter(&DOClusterTemplateWebhook{}).
WithValidator(&DOClusterTemplateWebhook{}).
Complete()
}

//+kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-doclustertemplate,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=doclustertemplates,versions=v1beta1,name=default.doclustertemplate.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
//+kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-doclustertemplate,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=doclustertemplates,versions=v1beta1,name=validation.doclustertemplate.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1

var _ webhook.Defaulter = &DOClusterTemplate{}
// DOClusterTemplateWebhook is a collection of webhooks for DOClusterTemplate objects.
type DOClusterTemplateWebhook struct{}

// Default implements webhook.Defaulter so a webhook will be registered for the type.
func (r *DOClusterTemplate) Default() {
doclustertemplatelog.Info("default", "name", r.Name)
}

var _ webhook.Validator = &DOClusterTemplate{}
var (
_ webhook.CustomDefaulter = &DOClusterTemplateWebhook{}
_ webhook.CustomValidator = &DOClusterTemplateWebhook{}
)

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
func (r *DOClusterTemplate) ValidateCreate() (admission.Warnings, error) {
doclustertemplatelog.Info("validate create", "name", r.Name)
// Default implements webhook.CustomDefaulter so a webhook will be registered for the type.
func (w *DOClusterTemplateWebhook) Default(context.Context, runtime.Object) error {
return nil
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type.
func (w *DOClusterTemplateWebhook) ValidateCreate(context.Context, runtime.Object) (admission.Warnings, error) {
return nil, nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
func (r *DOClusterTemplate) ValidateUpdate(oldRaw runtime.Object) (admission.Warnings, error) {
old, ok := oldRaw.(*DOClusterTemplate)
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type.
func (w *DOClusterTemplateWebhook) ValidateUpdate(_ context.Context, objOld, objNew runtime.Object) (admission.Warnings, error) {
oldTemplate, ok := objOld.(*v1beta1.DOClusterTemplate)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an DOClusterTemplate old object but got a %T", objOld))
}

newTemplate, ok := objNew.(*v1beta1.DOClusterTemplate)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an DOClusterTemplate but got a %T", oldRaw))
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an DOClusterTemplate new object but got a %T", objNew))
}

if !reflect.DeepEqual(r.Spec, old.Spec) {
if !reflect.DeepEqual(newTemplate.Spec, oldTemplate.Spec) {
return nil, apierrors.NewBadRequest("DOClusterTemplate.Spec is immutable")
}
return nil, nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
func (r *DOClusterTemplate) ValidateDelete() (admission.Warnings, error) {
doclustertemplatelog.Info("validate delete", "name", r.Name)
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type.
func (w *DOClusterTemplateWebhook) ValidateDelete(context.Context, runtime.Object) (admission.Warnings, error) {
return nil, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package v1beta1
package webhooks

import (
"context"
"fmt"
"reflect"

"github.com/pkg/errors"
Expand All @@ -25,6 +27,7 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"

"sigs.k8s.io/cluster-api-provider-digitalocean/api/v1beta1"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
Expand All @@ -37,41 +40,53 @@ var _ = logf.Log.WithName("domachine-resource")
// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-domachine,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=domachines,versions=v1beta1,name=validation.domachine.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1
// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-domachine,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=domachines,versions=v1beta1,name=default.domachine.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1

// DOMachineWebhook is a collection of webhooks for DOMachine objects.
type DOMachineWebhook struct{}

var (
_ webhook.Defaulter = &DOMachine{}
_ webhook.Validator = &DOMachine{}
_ webhook.CustomDefaulter = &DOMachineWebhook{}
_ webhook.CustomValidator = &DOMachineWebhook{}
)

func (r *DOMachine) SetupWebhookWithManager(mgr ctrl.Manager) error {
func (w *DOMachineWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
For(&v1beta1.DOMachine{}).
WithDefaulter(&DOMachineWebhook{}).
WithValidator(&DOMachineWebhook{}).
Complete()
}

// Default implements webhook.Defaulter so a webhook will be registered for the type.
func (r *DOMachine) Default() {}
// Default implements webhook.CustomDefaulter so a webhook will be registered for the type.
func (w *DOMachineWebhook) Default(context.Context, runtime.Object) error {
return nil
}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
func (r *DOMachine) ValidateCreate() (admission.Warnings, error) {
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type.
func (w *DOMachineWebhook) ValidateCreate(context.Context, runtime.Object) (admission.Warnings, error) {
return nil, nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
func (r *DOMachine) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type.
func (w *DOMachineWebhook) ValidateUpdate(_ context.Context, objOld, objNew runtime.Object) (admission.Warnings, error) {
var allErrs field.ErrorList

newDOMachine, err := runtime.DefaultUnstructuredConverter.ToUnstructured(r)
newDOMachine, ok := objNew.(*v1beta1.DOMachine)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an DOMachine new object but got a %T", objNew))
}

newDOMachineUnstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(objNew)
if err != nil {
return nil, apierrors.NewInternalError(errors.Wrap(err, "failed to convert new DOMachine to unstructured object"))
}

oldDOMachine, err := runtime.DefaultUnstructuredConverter.ToUnstructured(old)
oldDOMachineUnstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(objOld)
if err != nil {
return nil, apierrors.NewInternalError(errors.Wrap(err, "failed to convert old DOMachine to unstructured object"))
}

newDOMachineSpec := newDOMachine["spec"].(map[string]interface{})
oldDOMachineSpec := oldDOMachine["spec"].(map[string]interface{})
newDOMachineSpec := newDOMachineUnstr["spec"].(map[string]interface{})
oldDOMachineSpec := oldDOMachineUnstr["spec"].(map[string]interface{})

// allow changes to providerID
delete(oldDOMachineSpec, "providerID")
Expand All @@ -89,10 +104,10 @@ func (r *DOMachine) ValidateUpdate(old runtime.Object) (admission.Warnings, erro
return nil, nil
}

return nil, apierrors.NewInvalid(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
return nil, apierrors.NewInvalid(newDOMachine.GroupVersionKind().GroupKind(), newDOMachine.Name, allErrs)
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
func (r *DOMachine) ValidateDelete() (admission.Warnings, error) {
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type.
func (w *DOMachineWebhook) ValidateDelete(context.Context, runtime.Object) (admission.Warnings, error) {
return nil, nil
}
Loading

0 comments on commit 06b7296

Please sign in to comment.