Skip to content

Commit

Permalink
feat: add validating webhook for proxmoxmachine
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasl0st committed Dec 18, 2023
1 parent aeabaac commit 324e238
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 0 deletions.
4 changes: 4 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ resources:
kind: ProxmoxMachine
path: github.com/ionos-cloud/cluster-api-provider-proxmox/api/v1alpha1
version: v1alpha1
webhooks:
defaulting: true
validation: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: true
Expand Down
21 changes: 21 additions & 0 deletions config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,24 @@ webhooks:
resources:
- proxmoxclusters
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-infrastructure-cluster-x-k8s-io-v1alpha1-proxmoxmachine
failurePolicy: Fail
matchPolicy: Equivalent
name: validation.proxmoxmachine.infrastructure.cluster.x-k8s.io
rules:
- apiGroups:
- infrastructure.cluster.x-k8s.io
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- proxmoxmachines
sideEffects: None
132 changes: 132 additions & 0 deletions internal/webhook/proxmoxmachine_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
Copyright 2023 IONOS Cloud.
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 webhook contains webhooks for the custom resources.
package webhook

import (
"context"
"fmt"

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

ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

infrav1 "github.com/ionos-cloud/cluster-api-provider-proxmox/api/v1alpha1"
)

// ProxmoxMachine is a type that implements
// the interfaces from the admission package.
type ProxmoxMachine struct{}

// SetupWebhookWithManager sets up the webhook with the
// custom interfaces.
func (p *ProxmoxMachine) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(&infrav1.ProxmoxMachine{}).
WithValidator(p).
Complete()
}

//+kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1alpha1-proxmoxmachine,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=proxmoxmachines,versions=v1alpha1,name=validation.proxmoxmachine.infrastructure.cluster.x-k8s.io,admissionReviewVersions=v1

// ValidateCreate implements the creation validation function.
func (p *ProxmoxMachine) ValidateCreate(_ context.Context, obj runtime.Object) (warnings admission.Warnings, err error) {
machine, ok := obj.(*infrav1.ProxmoxMachine)
if !ok {
return warnings, apierrors.NewBadRequest(fmt.Sprintf("expected a ProxmoxMachine but got %T", obj))
}

err = validateNetworks(machine)
if err != nil {
warnings = append(warnings, fmt.Sprintf("cannot create proxmox machine %s", machine.GetName()))
return warnings, err
}

return warnings, nil
}

// ValidateUpdate implements the update validation function.
func (p *ProxmoxMachine) ValidateUpdate(_ context.Context, _, newObj runtime.Object) (warnings admission.Warnings, err error) {
newMachine, ok := newObj.(*infrav1.ProxmoxMachine)
if !ok {
return warnings, apierrors.NewBadRequest(fmt.Sprintf("expected a ProxmoxMachine but got %T", newObj))
}

err = validateNetworks(newMachine)
if err != nil {
warnings = append(warnings, fmt.Sprintf("cannot update proxmox machine %s", newMachine.GetName()))
return warnings, err
}

return warnings, nil
}

// ValidateDelete implements the deletion validation function.
func (p *ProxmoxMachine) ValidateDelete(_ context.Context, _ runtime.Object) (warnings admission.Warnings, err error) {
return nil, nil
}

func validateNetworks(machine *infrav1.ProxmoxMachine) error {
gk, name := machine.GroupVersionKind().GroupKind(), machine.GetName()

if machine.Spec.Network.Default != nil {
err := validateNetworkDevice(machine.Spec.Network.Default)
if err != nil {
return apierrors.NewInvalid(
gk,
name,
field.ErrorList{
field.Invalid(
field.NewPath("spec", "network", "default", "mtu"), machine.Spec.Network.Default, err.Error()),
})
}
}

for i := range machine.Spec.Network.AdditionalDevices {
err := validateNetworkDevice(&machine.Spec.Network.AdditionalDevices[i].NetworkDevice)
if err != nil {
return apierrors.NewInvalid(
gk,
name,
field.ErrorList{
field.Invalid(
field.NewPath("spec", "network", "additionalDevices", fmt.Sprint(i), "mtu"), machine.Spec.Network.Default, err.Error()),
})
}
}

return nil
}

func validateNetworkDevice(device *infrav1.NetworkDevice) error {
if device.MTU == nil {
return nil
}

if *device.MTU == 1 {
return nil
}

if *device.MTU > 999 {
return nil
}

return fmt.Errorf("mtu must be at least 1000 or 1, but was %d", *device.MTU)
}
105 changes: 105 additions & 0 deletions internal/webhook/proxmoxmachine_webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
Copyright 2023 IONOS Cloud.
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 webhook

import (
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"

infrav1 "github.com/ionos-cloud/cluster-api-provider-proxmox/api/v1alpha1"
)

var _ = Describe("Controller Test", func() {
g := NewWithT(GinkgoT())

Context("create proxmox machine", func() {
It("should disallow invalid network mtu", func() {
machine := invalidProxmoxMachine("test-machine")
g.Expect(k8sClient.Create(testEnv.GetContext(), &machine)).To(MatchError(ContainSubstring("spec.network.default.mtu: Invalid value")))
})

It("should create a valid proxmox machine", func() {
machine := validProxmoxMachine("test-machine")
g.Expect(k8sClient.Create(testEnv.GetContext(), &machine)).To(Succeed())
})
})

Context("update proxmox cluster", func() {
It("should disallow invalid network mtu", func() {
clusterName := "test-cluster"
machine := validProxmoxMachine(clusterName)
g.Expect(k8sClient.Create(testEnv.GetContext(), &machine)).To(Succeed())

g.Expect(k8sClient.Get(testEnv.GetContext(), client.ObjectKeyFromObject(&machine), &machine)).To(Succeed())
machine.Spec.Network.Default.MTU = ptr.To(uint16(50))

g.Expect(k8sClient.Create(testEnv.GetContext(), &machine)).To(MatchError(ContainSubstring("spec.network.default.mtu: Invalid value")))

g.Eventually(func(g Gomega) {
g.Expect(client.IgnoreNotFound(k8sClient.Delete(testEnv.GetContext(), &machine))).To(Succeed())
}).WithTimeout(time.Second * 10).
WithPolling(time.Second).
Should(Succeed())
})
})
})

func validProxmoxMachine(name string) infrav1.ProxmoxMachine {
return infrav1.ProxmoxMachine{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
},
Spec: infrav1.ProxmoxMachineSpec{
VirtualMachineCloneSpec: infrav1.VirtualMachineCloneSpec{
SourceNode: "pve",
},
NumSockets: 1,
NumCores: 1,
MemoryMiB: 1024,
Disks: &infrav1.Storage{
BootVolume: &infrav1.DiskSize{
Disk: "scsi[0]",
SizeGB: 10,
},
},
Network: &infrav1.NetworkSpec{
Default: &infrav1.NetworkDevice{
Bridge: "vmbr1",
Model: ptr.To("virtio"),
MTU: ptr.To(uint16(1500)),
},
},
},
}
}

func invalidProxmoxMachine(name string) infrav1.ProxmoxMachine {
machine := validProxmoxMachine(name)
machine.Spec.Network.Default = &infrav1.NetworkDevice{
Bridge: "vmbr1",
Model: ptr.To("virtio"),
MTU: ptr.To(uint16(50)),
}
return machine
}
3 changes: 3 additions & 0 deletions internal/webhook/webhook_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ var _ = BeforeSuite(func() {
err = (&ProxmoxCluster{}).SetupWebhookWithManager(testEnv.Manager)
Expect(err).NotTo(HaveOccurred())

err = (&ProxmoxMachine{}).SetupWebhookWithManager(testEnv.Manager)
Expect(err).NotTo(HaveOccurred())

//+kubebuilder:scaffold:webhook

go func() {
Expand Down

0 comments on commit 324e238

Please sign in to comment.