Skip to content

Commit

Permalink
Add Validating webhook
Browse files Browse the repository at this point in the history
Signed-off-by: Yury Kulazhenkov <ykulazhenkov@nvidia.com>
  • Loading branch information
ykulazhenkov committed Sep 25, 2023
1 parent 9095af7 commit bf17352
Show file tree
Hide file tree
Showing 9 changed files with 422 additions and 35 deletions.
128 changes: 128 additions & 0 deletions api/v1alpha1/ippool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
Copyright 2023, NVIDIA CORPORATION & AFFILIATES
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 v1alpha1_test

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

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/Mellanox/nvidia-k8s-ipam/api/v1alpha1"
)

var _ = Describe("Validate", func() {
It("Valid", func() {
ipPool := v1alpha1.IPPool{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: v1alpha1.IPPoolSpec{
Subnet: "192.168.0.0/16",
PerNodeBlockSize: 128,
Gateway: "192.168.0.1",
NodeSelector: &corev1.NodeSelector{
NodeSelectorTerms: []corev1.NodeSelectorTerm{{
MatchExpressions: []corev1.NodeSelectorRequirement{{
Key: "foo.bar",
Operator: corev1.NodeSelectorOpExists,
}},
}},
},
},
}
Expect(ipPool.Validate()).To(BeEmpty())
})
It("Valid - ipv6", func() {
ipPool := v1alpha1.IPPool{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: v1alpha1.IPPoolSpec{
Subnet: "2001:db8:3333:4444::0/64",
PerNodeBlockSize: 1000,
Gateway: "2001:db8:3333:4444::1",
},
}
Expect(ipPool.Validate()).To(BeEmpty())
})
It("Valid - no NodeSelector", func() {
ipPool := v1alpha1.IPPool{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: v1alpha1.IPPoolSpec{
Subnet: "192.168.0.0/16",
PerNodeBlockSize: 128,
Gateway: "192.168.0.1",
},
}
Expect(ipPool.Validate()).To(BeEmpty())
})
It("Empty object", func() {
ipPool := v1alpha1.IPPool{}
Expect(ipPool.Validate().ToAggregate().Error()).
To(And(
ContainSubstring("metadata.name"),
ContainSubstring("spec.subnet"),
ContainSubstring("spec.perNodeBlockSize"),
ContainSubstring("gateway"),
))
})
It("Invalid - perNodeBlockSize is too large", func() {
ipPool := v1alpha1.IPPool{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: v1alpha1.IPPoolSpec{
Subnet: "192.168.0.0/24",
PerNodeBlockSize: 300,
Gateway: "192.168.0.1",
},
}
Expect(ipPool.Validate().ToAggregate().Error()).
To(
ContainSubstring("spec.perNodeBlockSize"),
)
})
It("Invalid - gateway outside of the subnet", func() {
ipPool := v1alpha1.IPPool{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: v1alpha1.IPPoolSpec{
Subnet: "192.168.0.0/16",
PerNodeBlockSize: 128,
Gateway: "10.0.0.1",
},
}
Expect(ipPool.Validate().ToAggregate().Error()).
To(
ContainSubstring("spec.gateway"),
)
})
It("Invalid - invalid NodeSelector", func() {
ipPool := v1alpha1.IPPool{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: v1alpha1.IPPoolSpec{
Subnet: "192.168.0.0/16",
PerNodeBlockSize: 128,
Gateway: "192.168.0.1",
NodeSelector: &corev1.NodeSelector{
NodeSelectorTerms: []corev1.NodeSelectorTerm{{
MatchExpressions: []corev1.NodeSelectorRequirement{{
Key: "foo.bar",
Operator: "unknown",
}},
}},
},
},
}
Expect(ipPool.Validate().ToAggregate().Error()).
To(
ContainSubstring("spec.nodeSelector"),
)
})
})
72 changes: 72 additions & 0 deletions api/v1alpha1/ippool_validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
Copyright 2023, NVIDIA CORPORATION & AFFILIATES
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 v1alpha1

import (
"math"
"net"

cniUtils "github.com/containernetworking/cni/pkg/utils"
"k8s.io/apimachinery/pkg/util/validation/field"
)

// Validate contains validation for the object fields
func (r *IPPool) Validate() field.ErrorList {
errList := field.ErrorList{}
if err := cniUtils.ValidateNetworkName(r.Name); err != nil {
errList = append(errList, field.Invalid(
field.NewPath("metadata", "name"), r.Name,
"invalid IP pool name, should be compatible with CNI network name"))
}
_, network, err := net.ParseCIDR(r.Spec.Subnet)
if err != nil {
errList = append(errList, field.Invalid(
field.NewPath("spec", "subnet"), r.Spec.Subnet, "is invalid subnet"))
}

if r.Spec.PerNodeBlockSize < 2 {
errList = append(errList, field.Invalid(
field.NewPath("spec", "perNodeBlockSize"),
r.Spec.PerNodeBlockSize, "must be at least 2"))
}

if network != nil && r.Spec.PerNodeBlockSize >= 2 {
setBits, bitsTotal := network.Mask.Size()
// possibleIPs = net size - network address - broadcast
possibleIPs := int(math.Pow(2, float64(bitsTotal-setBits))) - 2
if possibleIPs < r.Spec.PerNodeBlockSize {
// config is not valid even if only one node exist in the cluster
errList = append(errList, field.Invalid(
field.NewPath("spec", "perNodeBlockSize"), r.Spec.PerNodeBlockSize,
"is larger then amount of IPs available in the subnet"))
}
}
parsedGW := net.ParseIP(r.Spec.Gateway)
if len(parsedGW) == 0 {
errList = append(errList, field.Invalid(
field.NewPath("spec", "gateway"), r.Spec.Gateway,
"is invalid IP address"))
}

if network != nil && len(parsedGW) != 0 && !network.Contains(parsedGW) {
errList = append(errList, field.Invalid(
field.NewPath("spec", "gateway"), r.Spec.Gateway,
"is not part of the subnet"))
}

if r.Spec.NodeSelector != nil {
errList = append(errList, validateNodeSelector(r.Spec.NodeSelector, field.NewPath("spec"))...)
}
return errList
}
60 changes: 60 additions & 0 deletions api/v1alpha1/ippool_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
Copyright 2023, NVIDIA CORPORATION & AFFILIATES
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 v1alpha1

import (
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
logPkg "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

var logger = logPkg.Log.WithName("IPPool-validator")

// SetupWebhookWithManager registers webhook handler in the manager
func (r *IPPool) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
Complete()
}

var _ webhook.Validator = &IPPool{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *IPPool) ValidateCreate() error {
logger.V(1).Info("validate create", "name", r.Name)
return r.validate()
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *IPPool) ValidateUpdate(_ runtime.Object) error {
logger.V(1).Info("validate update", "name", r.Name)
return r.validate()
}

func (r *IPPool) validate() error {
errList := r.Validate()
if len(errList) == 0 {
logger.V(1).Info("validation succeed")
return nil
}
err := errList.ToAggregate()
logger.V(1).Info("validation failed", "reason", err.Error())
return err
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *IPPool) ValidateDelete() error {
return nil
}
13 changes: 13 additions & 0 deletions api/v1alpha1/v1alpha1_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package v1alpha1_test

import (
"testing"

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

func TestV1alpha1(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "V1alpha1 Suite")
}
Loading

0 comments on commit bf17352

Please sign in to comment.