Skip to content
Permalink
Browse files

Add Validation webhooks to cluster api manager

Performs validation for Machines, MachineSets and MachineDeployments.
For Machines it validates that Bootstrap has either ConfigRef or Data
set.
For MachineSets and MachineDeployments it verifies selectors match
template labels.

Signed-off-by: Warren Fernandes <wfernandes@pivotal.io>
  • Loading branch information
wfernandes committed Nov 14, 2019
1 parent 08b2959 commit 52b75a05aa38533653b14f9bd534171257fdef04
@@ -150,6 +150,7 @@ generate-manifests: $(CONTROLLER_GEN) ## Generate manifests e.g. CRD, RBAC etc.
crd \
rbac:roleName=manager-role \
output:crd:dir=./config/crd/bases \
output:webhook:dir=./config/webhook \
webhook
$(CONTROLLER_GEN) \
paths=./bootstrap/kubeadm/api/... \
@@ -0,0 +1,59 @@
/*
Copyright 2019 The Kubernetes Authors.
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 v1alpha3

import (
"github.com/pkg/errors"
runtime "k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

func (m *Machine) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(m).
Complete()
}

// +kubebuilder:webhook:verbs=create;update,path=/validate-cluster-x-k8s-io-v1alpha3-machine,mutating=false,failurePolicy=fail,groups=cluster.x-k8s.io,resources=machines,versions=v1alpha3,name=validation.machine.cluster.x-k8s.io

var _ webhook.Validator = &Machine{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (m *Machine) ValidateCreate() error {
return m.validateMachine()
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (m *Machine) ValidateUpdate(old runtime.Object) error {
return m.validateMachine()
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (m *Machine) ValidateDelete() error {
return nil
}

func (m *Machine) validateMachine() error {
if m.Spec.Bootstrap.ConfigRef == nil && m.Spec.Bootstrap.Data == nil {
return errors.Errorf(
"Expected at least one of `Bootstrap.ConfigRef` or `Bootstrap.Data` to be populated for Machine %q in namespace %q",
m.Name, m.Namespace,
)
}
return nil
}
@@ -0,0 +1,87 @@
/*
Copyright 2019 The Kubernetes Authors.
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 v1alpha3

import (
"testing"

. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
)

func TestMachineValidation(t *testing.T) {
t.Run("should validate machine bootstrap", func(t *testing.T) {
data := "some bootstrap data"
tests := []struct {
name string
f func(m *Machine) error
bootstrap Bootstrap
expectErr bool
}{
{
name: "return error if configref and data are nil on create",
f: func(m *Machine) error { return m.ValidateCreate() },
bootstrap: Bootstrap{ConfigRef: nil, Data: nil},
expectErr: true,
},
{
name: "return error if configref and data are nil on update",
f: func(m *Machine) error { return m.ValidateUpdate(nil) },
bootstrap: Bootstrap{ConfigRef: nil, Data: nil},
expectErr: true,
},
{
name: "not return error if data is set on create",
f: func(m *Machine) error { return m.ValidateCreate() },
bootstrap: Bootstrap{ConfigRef: nil, Data: &data},
expectErr: false,
},
{
name: "not return error if data is set on update",
f: func(m *Machine) error { return m.ValidateUpdate(nil) },
bootstrap: Bootstrap{ConfigRef: nil, Data: &data},
expectErr: false,
},
{
name: "not return error if config ref is set on create",
f: func(m *Machine) error { return m.ValidateCreate() },
bootstrap: Bootstrap{ConfigRef: &corev1.ObjectReference{}, Data: nil},
expectErr: false,
},
{
name: "not return error if config ref is set on update",
f: func(m *Machine) error { return m.ValidateUpdate(nil) },
bootstrap: Bootstrap{ConfigRef: &corev1.ObjectReference{}, Data: nil},
expectErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
RegisterTestingT(t)
m := &Machine{
Spec: MachineSpec{Bootstrap: tt.bootstrap},
}
if tt.expectErr {
Expect(tt.f(m)).To(HaveOccurred())
} else {
Expect(tt.f(m)).ToNot(HaveOccurred())
}
})
}
})
}
@@ -0,0 +1,66 @@
/*
Copyright 2019 The Kubernetes Authors.
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 v1alpha3

import (
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
runtime "k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

func (m *MachineDeployment) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(m).
Complete()
}

// +kubebuilder:webhook:verbs=create;update,path=/validate-cluster-x-k8s-io-v1alpha3-machinedeployment,mutating=false,failurePolicy=fail,groups=cluster.x-k8s.io,resources=machinedeployments,versions=v1alpha3,name=validation.machinedeployment.cluster.x-k8s.io

var _ webhook.Validator = &MachineDeployment{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (m *MachineDeployment) ValidateCreate() error {
return m.validateMachineDeployment()
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (m *MachineDeployment) ValidateUpdate(old runtime.Object) error {
return m.validateMachineDeployment()
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (m *MachineDeployment) ValidateDelete() error {
return nil
}

func (m *MachineDeployment) validateMachineDeployment() error {
selector, err := metav1.LabelSelectorAsSelector(&m.Spec.Selector)
if err != nil {
return errors.Wrapf(err, "failed to parse MachineDeployment %q label selector", m.Name)
}

if !selector.Matches(labels.Set(m.Spec.Template.Labels)) {
return errors.Errorf(
"failed validation on MachineDeployment %q label selector, cannot match Machine template labels",
m.Name,
)
}
return nil
}
@@ -0,0 +1,124 @@
/*
Copyright 2019 The Kubernetes Authors.
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 v1alpha3

import (
"testing"

. "github.com/onsi/gomega"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestMachineDeploymentValidation(t *testing.T) {
t.Run("should validate labels and selectors match", func(t *testing.T) {
tests := []struct {
name string
f func(m *MachineDeployment) error
selectors map[string]string
labels map[string]string
expectErr bool
}{
{
name: "return error on mismatch during create",
f: func(m *MachineDeployment) error { return m.ValidateCreate() },
selectors: map[string]string{"foo": "bar"},
labels: map[string]string{"foo": "baz"},
expectErr: true,
},
{
name: "return error on mistmatch during update",
f: func(m *MachineDeployment) error { return m.ValidateUpdate(nil) },
selectors: map[string]string{"foo": "bar"},
labels: map[string]string{"foo": "baz"},
expectErr: true,
},
{
name: "return error on missing labels during create",
f: func(m *MachineDeployment) error { return m.ValidateCreate() },
selectors: map[string]string{"foo": "bar"},
labels: map[string]string{"": ""},
expectErr: true,
},
{
name: "return error on missing labels during update",
f: func(m *MachineDeployment) error { return m.ValidateUpdate(nil) },
selectors: map[string]string{"foo": "bar"},
labels: map[string]string{"": ""},
expectErr: true,
},
{
name: "return error if all selectors don't match during create",
f: func(m *MachineDeployment) error { return m.ValidateCreate() },
selectors: map[string]string{"foo": "bar", "hello": "world"},
labels: map[string]string{"foo": "bar"},
expectErr: true,
},
{
name: "return error on if all selectors don't match during update",
f: func(m *MachineDeployment) error { return m.ValidateUpdate(nil) },
selectors: map[string]string{"foo": "bar", "hello": "world"},
labels: map[string]string{"foo": "bar"},
expectErr: true,
},
{
name: "not return error on match during create",
f: func(m *MachineDeployment) error { return m.ValidateCreate() },
selectors: map[string]string{"foo": "bar"},
labels: map[string]string{"foo": "bar"},
expectErr: false,
},
{
name: "not return error on match during update",
f: func(m *MachineDeployment) error { return m.ValidateUpdate(nil) },
selectors: map[string]string{"foo": "bar"},
labels: map[string]string{"foo": "bar"},
expectErr: false,
},
{
name: "return error for invalid selector",
f: func(m *MachineDeployment) error { return m.ValidateUpdate(nil) },
selectors: map[string]string{"-123-foo": "bar"},
labels: map[string]string{"-123-foo": "bar"},
expectErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
RegisterTestingT(t)
md := &MachineDeployment{
Spec: MachineDeploymentSpec{
Selector: v1.LabelSelector{
MatchLabels: tt.selectors,
},
Template: MachineTemplateSpec{
ObjectMeta: ObjectMeta{
Labels: tt.labels,
},
},
},
}
if tt.expectErr {
Expect(tt.f(md)).To(HaveOccurred())
} else {
Expect(tt.f(md)).ToNot(HaveOccurred())
}
})
}
})

}

0 comments on commit 52b75a0

Please sign in to comment.
You can’t perform that action at this time.