From c29b68b654c9768ae4e92bbba1f49d999a0146b6 Mon Sep 17 00:00:00 2001 From: Cindy Bang Date: Mon, 29 Apr 2024 09:00:00 -0400 Subject: [PATCH] api: linodemachine: add create validation --- api/v1alpha1/linodemachine_webhook.go | 142 ++++++++++++++++++++++++-- config/webhook/manifests.yaml | 1 - 2 files changed, 136 insertions(+), 7 deletions(-) diff --git a/api/v1alpha1/linodemachine_webhook.go b/api/v1alpha1/linodemachine_webhook.go index af8740573..dbb5a71e7 100644 --- a/api/v1alpha1/linodemachine_webhook.go +++ b/api/v1alpha1/linodemachine_webhook.go @@ -17,13 +17,29 @@ limitations under the License. package v1alpha1 import ( + "context" + "fmt" + "net/http" + "slices" + + "github.com/linode/linodego" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" 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" ) +var ( + // The list of valid SCSI device paths that user-defined data disks may attach to + // NOTE: sda is reserved for the OS disk + LinodeMachineSCSIPaths = []string{"sdb", "sdc", "sdd", "sde", "sdh"} +) + // log is for logging in this package. var linodemachinelog = logf.Log.WithName("linodemachine-resource") @@ -34,10 +50,7 @@ func (r *LinodeMachine) SetupWebhookWithManager(mgr ctrl.Manager) error { Complete() } -// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! - -// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. -//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1alpha1-linodemachine,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=linodemachines,verbs=create;update,versions=v1alpha1,name=vlinodemachine.kb.io,admissionReviewVersions=v1 +//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1alpha1-linodemachine,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=linodemachines,verbs=create,versions=v1alpha1,name=vlinodemachine.kb.io,admissionReviewVersions=v1 var _ webhook.Validator = &LinodeMachine{} @@ -45,8 +58,7 @@ var _ webhook.Validator = &LinodeMachine{} func (r *LinodeMachine) ValidateCreate() (admission.Warnings, error) { linodemachinelog.Info("validate create", "name", r.Name) - // TODO(user): fill in your validation logic upon object creation. - return nil, nil + return nil, r.validateLinodeMachine() } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type @@ -64,3 +76,121 @@ func (r *LinodeMachine) ValidateDelete() (admission.Warnings, error) { // TODO(user): fill in your validation logic upon object deletion. return nil, nil } + +func (r *LinodeMachine) validateLinodeMachine() error { + var errs field.ErrorList + if err := r.validateLinodeMachineSpec(); err != nil { + errs = slices.Concat(errs, err) + } + + if len(errs) == 0 { + return nil + } + return apierrors.NewInvalid( + schema.GroupKind{Group: "infrastructure.cluster.x-k8s.io", Kind: "LinodeMachine"}, + r.Name, errs) +} + +func (r *LinodeMachine) validateLinodeMachineSpec() field.ErrorList { + var ( + errs field.ErrorList + client = linodego.NewClient(http.DefaultClient) + ctx = context.TODO() + ) + if err := validateRegion(ctx, client, r.Spec.Region, field.NewPath("spec").Child("region")); err != nil { + errs = append(errs, err) + } + plan, err := validateLinodeType(ctx, client, r.Spec.Type, field.NewPath("spec").Child("type")) + if err != nil { + errs = append(errs, err) + } + if err := r.validateLinodeMachineDisks(plan); err != nil { + errs = append(errs, err) + } + + if len(errs) == 0 { + return nil + } + return errs +} + +func validateRegion(ctx context.Context, client linodego.Client, region string, path *field.Path) *field.Error { + if _, err := client.GetRegion(ctx, region); err != nil { + return field.NotFound(path, region) + } + + return nil +} + +func validateLinodeType(ctx context.Context, client linodego.Client, plan string, path *field.Path) (*linodego.LinodeType, *field.Error) { + linodeType, err := client.GetType(ctx, plan) + if err != nil { + return nil, field.NotFound(path, plan) + } + + return linodeType, nil +} + +func (r *LinodeMachine) validateLinodeMachineDisks(plan *linodego.LinodeType) *field.Error { + // The Linode plan information is required to perform disk validation + if plan == nil { + return nil + } + + var ( + // The Linode API represents storage sizes in megabytes (MB) + // See: https://www.linode.com/docs/api/linode-types/#type-view + planSize = resource.MustParse(fmt.Sprintf("%d%s", plan.Disk, "M")) + remainSize = &planSize + err *field.Error + ) + + if remainSize, err = validateDisk(r.Spec.OSDisk, field.NewPath("spec").Child("osDisk"), remainSize); err != nil { + return err + } + if _, err := validateDataDisks(r.Spec.DataDisks, field.NewPath("spec").Child("dataDisks"), remainSize); err != nil { + return err + } + + return nil +} + +func validateDataDisks(disks map[string]*InstanceDisk, path *field.Path, availSize *resource.Quantity) (*resource.Quantity, *field.Error) { + var ( + devs []string + remainSize = availSize + ) + + for dev, disk := range disks { + if !slices.Contains(LinodeMachineSCSIPaths, dev) { + return nil, field.Forbidden(path.Child(dev), fmt.Sprintf("allowed device paths: %v", LinodeMachineSCSIPaths)) + } + if slices.Contains(devs, dev) { + return nil, field.Duplicate(path.Child(dev), "duplicate device path") + } + devs = append(devs, dev) + + var err *field.Error + if remainSize, err = validateDisk(disk, path.Child(dev), remainSize); err != nil { + return nil, err + } + } + return remainSize, nil +} + +func validateDisk(disk *InstanceDisk, path *field.Path, availSize *resource.Quantity) (*resource.Quantity, *field.Error) { + if disk == nil { + return availSize, nil + } + + if disk.Size.Sign() == -1 { + return nil, field.Invalid(path, disk.Size.String(), "cannot be negative") + } + if availSize.Cmp(disk.Size) == -1 { + return nil, field.Invalid(path, disk.Size.String(), "sum of disk sizes cannot exceed plan storage") + } + + // Decrement the remaining amount of space available + availSize.Sub(disk.Size) + return availSize, nil +} diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index f07c9a056..6f148c7fb 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -20,7 +20,6 @@ webhooks: - v1alpha1 operations: - CREATE - - UPDATE resources: - linodemachines sideEffects: None