Admission webhook check: Add a doks specific error is webhook applies to objects in kube-system namespace

varsha/versions
Varsha Varadarajan 2019-06-28 13:00:32 -04:00
parent dc2b0df5c3
commit 92e0e9dfe2
4 changed files with 373 additions and 12 deletions

View File

@ -65,4 +65,8 @@ const (
ServiceAccount Kind = "service account"
// PersistentVolume identifies Kubernetes objects of kind `persistent volume`
PersistentVolume Kind = "persistent volume"
// ValidatingWebhookConfiguration identifies Kubernetes objects of kind `validating webhook configuration`
ValidatingWebhookConfiguration Kind = "validating webhook configuration"
// MutatingWebhookConfiguration identifies Kubernetes objects of kind `validating webhook configuration`
MutatingWebhookConfiguration Kind = "mutating webhook configuration"
)

View File

@ -0,0 +1,123 @@
package doks
import (
"github.com/digitalocean/clusterlint/checks"
"github.com/digitalocean/clusterlint/kube"
ar "k8s.io/api/admissionregistration/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func init() {
checks.Register(&webhookCheck{})
}
type webhookCheck struct{}
// Name returns a unique name for this check.
func (w *webhookCheck) Name() string {
return "admission-controller-webhook"
}
// Groups returns a list of group names this check should be part of.
func (w *webhookCheck) Groups() []string {
return []string{"doks"}
}
// Description returns a detailed human-readable description of what this check
// does.
func (w *webhookCheck) Description() string {
return "Check for admission controllers that could prevent managed components from starting"
}
// Run runs this check on a set of Kubernetes objects. It can return warnings
// (low-priority problems) and errors (high-priority problems) as well as an
// error value indicating that the check failed to run.
func (w *webhookCheck) Run(objects *kube.Objects) ([]checks.Diagnostic, error) {
var diagnostics []checks.Diagnostic
for _, config := range objects.ValidatingWebhookConfigurations.Items {
for _, validatingWebhook := range config.Webhooks {
if *validatingWebhook.FailurePolicy == ar.Fail && doesSelectorIncludeKubeSystem(validatingWebhook.NamespaceSelector, objects.SystemNamespace) {
d := checks.Diagnostic{
Severity: checks.Error,
Message: "Webhook matches objects in the kube-system namespace. This can cause problems when upgrading the cluster.",
Kind: checks.ValidatingWebhookConfiguration,
Object: &config.ObjectMeta,
Owners: config.ObjectMeta.GetOwnerReferences(),
}
diagnostics = append(diagnostics, d)
}
}
}
for _, config := range objects.MutatingWebhookConfigurations.Items {
for _, mutatingWebhook := range config.Webhooks {
if *mutatingWebhook.FailurePolicy == ar.Fail && doesSelectorIncludeKubeSystem(mutatingWebhook.NamespaceSelector, objects.SystemNamespace) {
d := checks.Diagnostic{
Severity: checks.Error,
Message: "Webhook matches objects in the kube-system namespace. This can cause problems when upgrading the cluster.",
Kind: checks.MutatingWebhookConfiguration,
Object: &config.ObjectMeta,
Owners: config.ObjectMeta.GetOwnerReferences(),
}
diagnostics = append(diagnostics, d)
}
}
}
return diagnostics, nil
}
func doesSelectorIncludeKubeSystem(selector *metav1.LabelSelector, namespace *corev1.Namespace) bool {
if selector.Size() == 0 {
return true
}
labels := namespace.GetLabels()
for key, value := range selector.MatchLabels {
if v, ok := labels[key]; ok && v == value {
continue
}
return false
}
for _, lbr := range selector.MatchExpressions {
if !match(labels, lbr) {
return false
}
}
return true
}
func match(labels map[string]string, lbr metav1.LabelSelectorRequirement) bool {
switch lbr.Operator {
case metav1.LabelSelectorOpExists:
if _, ok := labels[lbr.Key]; ok {
return true
}
return false
case metav1.LabelSelectorOpDoesNotExist:
if _, ok := labels[lbr.Key]; !ok {
return true
}
return false
case metav1.LabelSelectorOpIn:
if v, ok := labels[lbr.Key]; ok && contains(lbr.Values, v) {
return true
}
return false
case metav1.LabelSelectorOpNotIn:
if v, ok := labels[lbr.Key]; !ok || !contains(lbr.Values, v) {
return true
}
return false
}
return false
}
func contains(list []string, name string) bool {
for _, l := range list {
if l == name {
return true
}
}
return false
}

View File

@ -0,0 +1,217 @@
package doks
import (
"testing"
"github.com/digitalocean/clusterlint/checks"
"github.com/digitalocean/clusterlint/kube"
"github.com/stretchr/testify/assert"
ar "k8s.io/api/admissionregistration/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestWebhookCheckMeta(t *testing.T) {
webhookCheck := webhookCheck{}
assert.Equal(t, "admission-controller-webhook", webhookCheck.Name())
assert.Equal(t, []string{"doks"}, webhookCheck.Groups())
assert.NotEmpty(t, webhookCheck.Description())
}
func TestWebhookCheckRegistration(t *testing.T) {
webhookCheck := &webhookCheck{}
check, err := checks.Get("admission-controller-webhook")
assert.NoError(t, err)
assert.Equal(t, check, webhookCheck)
}
func TestWebhookError(t *testing.T) {
tests := []struct {
name string
objs *kube.Objects
expected []checks.Diagnostic
}{
{
name: "no webhook configurations",
objs: &kube.Objects{
MutatingWebhookConfigurations: &ar.MutatingWebhookConfigurationList{},
ValidatingWebhookConfigurations: &ar.ValidatingWebhookConfigurationList{},
SystemNamespace: &corev1.Namespace{},
},
expected: nil,
},
{
name: "failure policy is ignore",
objs: initObjects(ar.Ignore),
expected: nil,
},
{
name: "namespace selector is empty",
objs: initObjects(ar.Fail),
expected: webhookErrors(),
},
{
name: "namespace selector matches label",
objs: label(map[string]string{"doks_key": "bar"}),
expected: webhookErrors(),
},
{
name: "namespace selector does not match label",
objs: label(map[string]string{"non-existent-label-on-namespace": "bar"}),
expected: nil,
},
{
name: "namespace selector matches OpExists expression",
objs: expr("doks_key", []string{}, metav1.LabelSelectorOpExists),
expected: webhookErrors(),
},
{
name: "namespace selector matches OpDoesNotExist expression",
objs: expr("random", []string{}, metav1.LabelSelectorOpDoesNotExist),
expected: webhookErrors(),
},
{
name: "namespace selector matches OpIn expression",
objs: expr("doks_key", []string{"bar"}, metav1.LabelSelectorOpIn),
expected: webhookErrors(),
},
{
name: "namespace selector matches OpNotIn expression",
objs: expr("doks_key", []string{"non-existent"}, metav1.LabelSelectorOpNotIn),
expected: webhookErrors(),
},
{
name: "namespace selector does not match OpExists expression",
objs: expr("non-existent", []string{}, metav1.LabelSelectorOpExists),
expected: nil,
},
{
name: "namespace selector does not match OpDoesNotExist expression",
objs: expr("doks_key", []string{}, metav1.LabelSelectorOpDoesNotExist),
expected: nil,
},
{
name: "namespace selector does not match OpIn expression",
objs: expr("doks_key", []string{"non-existent"}, metav1.LabelSelectorOpIn),
expected: nil,
},
{
name: "namespace selector does not match OpNotIn expression",
objs: expr("doks_key", []string{"bar"}, metav1.LabelSelectorOpNotIn),
expected: nil,
},
}
webhookCheck := webhookCheck{}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
d, err := webhookCheck.Run(test.objs)
assert.NoError(t, err)
assert.ElementsMatch(t, test.expected, d)
})
}
}
func initObjects(failurePolicyType ar.FailurePolicyType) *kube.Objects {
objs := &kube.Objects{
SystemNamespace: &corev1.Namespace{
TypeMeta: metav1.TypeMeta{Kind: "Namespace", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{
Name: "kube-system",
Labels: map[string]string{"doks_key": "bar"}},
},
MutatingWebhookConfigurations: &ar.MutatingWebhookConfigurationList{
Items: []ar.MutatingWebhookConfiguration{
{
TypeMeta: metav1.TypeMeta{Kind: "MutatingWebhookConfiguration", APIVersion: "v1beta1"},
ObjectMeta: metav1.ObjectMeta{
Name: "mwc_foo",
},
Webhooks: []ar.MutatingWebhook{
{
Name: "mw_foo",
FailurePolicy: &failurePolicyType,
NamespaceSelector: &metav1.LabelSelector{},
},
},
},
},
},
ValidatingWebhookConfigurations: &ar.ValidatingWebhookConfigurationList{
Items: []ar.ValidatingWebhookConfiguration{
{
TypeMeta: metav1.TypeMeta{Kind: "ValidatingWebhookConfiguration", APIVersion: "v1beta1"},
ObjectMeta: metav1.ObjectMeta{
Name: "vwc_foo",
},
Webhooks: []ar.ValidatingWebhook{
{
Name: "vw_foo",
FailurePolicy: &failurePolicyType,
NamespaceSelector: &metav1.LabelSelector{},
},
},
},
},
},
}
return objs
}
func label(label map[string]string) *kube.Objects {
objs := initObjects(ar.Fail)
objs.ValidatingWebhookConfigurations.Items[0].Webhooks[0].NamespaceSelector = &metav1.LabelSelector{
MatchLabels: label,
}
objs.MutatingWebhookConfigurations.Items[0].Webhooks[0].NamespaceSelector = &metav1.LabelSelector{
MatchLabels: label,
}
return objs
}
func expr(key string, values []string, labelOperator metav1.LabelSelectorOperator) *kube.Objects {
objs := initObjects(ar.Fail)
objs.ValidatingWebhookConfigurations.Items[0].Webhooks[0].NamespaceSelector = &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: key,
Operator: labelOperator,
Values: values,
},
},
}
objs.MutatingWebhookConfigurations.Items[0].Webhooks[0].NamespaceSelector = &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: key,
Operator: labelOperator,
Values: values,
},
},
}
return objs
}
func webhookErrors() []checks.Diagnostic {
objs := initObjects(ar.Fail)
validatingConfig := objs.ValidatingWebhookConfigurations.Items[0]
mutatingConfig := objs.MutatingWebhookConfigurations.Items[0]
diagnostics := []checks.Diagnostic{
{
Severity: checks.Error,
Message: "Webhook matches objects in the kube-system namespace. This can cause problems when upgrading the cluster.",
Kind: checks.ValidatingWebhookConfiguration,
Object: &validatingConfig.ObjectMeta,
Owners: validatingConfig.ObjectMeta.GetOwnerReferences(),
},
{
Severity: checks.Error,
Message: "Webhook matches objects in the kube-system namespace. This can cause problems when upgrading the cluster.",
Kind: checks.MutatingWebhookConfiguration,
Object: &mutatingConfig.ObjectMeta,
Owners: mutatingConfig.ObjectMeta.GetOwnerReferences(),
},
}
return diagnostics
}

View File

@ -18,6 +18,7 @@ package kube
import (
"golang.org/x/sync/errgroup"
ar "k8s.io/api/admissionregistration/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
@ -36,6 +37,7 @@ type Objects struct {
Nodes *corev1.NodeList
PersistentVolumes *corev1.PersistentVolumeList
ComponentStatuses *corev1.ComponentStatusList
SystemNamespace *corev1.Namespace
Pods *corev1.PodList
PodTemplates *corev1.PodTemplateList
PersistentVolumeClaims *corev1.PersistentVolumeClaimList
@ -45,6 +47,8 @@ type Objects struct {
ServiceAccounts *corev1.ServiceAccountList
ResourceQuotas *corev1.ResourceQuotaList
LimitRanges *corev1.LimitRangeList
MutatingWebhookConfigurations *ar.MutatingWebhookConfigurationList
ValidatingWebhookConfigurations *ar.ValidatingWebhookConfigurationList
}
// Client encapsulates a client for a Kubernetes cluster.
@ -55,6 +59,7 @@ type Client struct {
// FetchObjects returns the objects from a Kubernetes cluster.
func (c *Client) FetchObjects() (*Objects, error) {
client := c.kubeClient.CoreV1()
admissionControllerClient := c.kubeClient.AdmissionregistrationV1beta1()
opts := metav1.ListOptions{}
objects := &Objects{}
@ -108,6 +113,18 @@ func (c *Client) FetchObjects() (*Objects, error) {
objects.LimitRanges, err = client.LimitRanges(corev1.NamespaceAll).List(opts)
return
})
g.Go(func() (err error) {
objects.SystemNamespace, err = client.Namespaces().Get(metav1.NamespaceSystem, metav1.GetOptions{})
return
})
g.Go(func() (err error) {
objects.MutatingWebhookConfigurations, err = admissionControllerClient.MutatingWebhookConfigurations().List(opts)
return
})
g.Go(func() (err error) {
objects.ValidatingWebhookConfigurations, err = admissionControllerClient.ValidatingWebhookConfigurations().List(opts)
return
})
err := g.Wait()
if err != nil {