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
- 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
- Don’t use
admission.Defaulter
oradmission.Validator
, instead register N webhooks. This has significant downsides though because theDefaulter
andValidator
webhooks are automatically registered by ourregisterWebhook
method ingeneric_controller
(ctrl.NewWebhookManagedBy(mgr).For(obj).Complete()
)