Custom validation and defaulting for code generated resources

Reasoning

controller-runtime defines admission.Defaulter and admission.Validator. These interfaces only give you a single Default or ValidateX method, which means that all validation/defaulting needs to be done in that method. I think we’re going to quickly run into situations where we want custom (handcrafted) validations or defaults for a particular resource and we’re not going to want to teach the code generator about these.

Suggestion

Structure our autogenerated webhooks such that there an ability to override the default behavior by implementing an interface.

For defaulting, implement this interface to hook into the autogenerated defaulting process:

type Defaulter interface {
	CustomDefault()
}

For validation, the interface being implemented should allow easy collation of errors, so we expand the controller-runtime Validator interface to return a collection of validation functions whose errors the autogenerated code can aggeregate:

type Validator interface {
	CreateValidations() []func() error
	UpdateValidations() []func(old runtime.Object) error
	DeleteValidations() []func() error
}

Sample Default:

// +kubebuilder:webhook:path=/mutate-microsoft-storage-infra-azure-com-v1alpha1api20190401-storageaccount,mutating=true,sideEffects=None,matchPolicy=Exact,failurePolicy=fail,groups=microsoft.storage.infra.azure.com,resources=storageaccounts,verbs=create;update,versions=v1alpha1api20190401,name=default.v1alpha1api20190401.storageaccounts.microsoft.storage.infra.azure.com,admissionReviewVersions=v1beta1

var _ admission.Defaulter = &StorageAccount{}

// Default defaults the Azure name of the resource to the Kubernetes name
func (storageAccount *StorageAccount) Default() {
	storageAccount.default()
	var temp interface{} = storageAccount
	if runtimeDefaulter, ok := temp.(genruntime.Defaulter); ok {
		runtimeDefaulter.CustomDefault()
	}
}

func (storageAccount *StorageAccount) default() []func() {
	storageAccount.defaultAzureName()
}

func (storageAccount *StorageAccount) defaultAzureName() {
	if storageAccount.Spec.AzureName == "" {
		storageAccount.Spec.AzureName = storageAccount.Name
	}
}

Sample Validate:

// +kubebuilder:webhook:path=/validate-microsoft-storage-infra-azure-com-v1alpha1api20190401-storageaccount,mutating=false,sideEffects=None,matchPolicy=Exact,failurePolicy=fail,groups=microsoft.storage.infra.azure.com,resources=storageaccounts,verbs=create;update,versions=v1alpha1api20190401,name=validate.v1alpha1api20190401.storageaccounts.microsoft.storage.infra.azure.com,admissionReviewVersions=v1beta1

var _ admission.Validator = &StorageAccount{}

// ValidateCreate validates the creation of the resource
func (storageAccount *StorageAccount) ValidateCreate() error {
	validations := storageAccount.createValidations()
	var temp interface{} = storageAccount
	if runtimeValidator, ok := temp.(genruntime.Validator); ok {
		validations = append(validations, runtimeValidator.CreateValidations()...)
	}

	var errs []error
	for _, validation := range validations {
		err := validation()
		if err != nil {
			errs = append(errs, err)
		}
	}

	return kerrors.NewAggregate(errs)
}

func (storageAccount *StorageAccount) createValidations() []func() error {
	return []func() error{
		storageAccount.validateResourceReferences,
	}
}

func (storageAccount *StorageAccount) validateResourceReferences() error {
	refs, err := reflecthelpers.FindResourceReferences(&storageAccount.Spec)
	if err != nil {
		return err
	}
	return genruntime.ValidateResourceReferences(refs)
}

// <other Validator methods elided...>

Open questions

  1. How awkward is var temp runtime.Object = storageAccount + if runtimeValidator, ok := temp.(genruntime.Validator); ok - is there a better way to do this?

Other possibilities

  1. Don’t use admission.Defaulter or admission.Validator, instead register N webhooks. This has significant downsides though because the Defaulter and Validator webhooks are automatically registered by our registerWebhook method in generic_controller (ctrl.NewWebhookManagedBy(mgr).For(obj).Complete())