Skip to content

Commit

Permalink
Merge pull request #69 from digitalocean/admission-webhook-changes
Browse files Browse the repository at this point in the history
Introduce basic admission webhook checks to handle error case
  • Loading branch information
nanzhong authored Nov 25, 2019
2 parents d658b20 + 21df8a7 commit eebd18a
Show file tree
Hide file tree
Showing 5 changed files with 492 additions and 23 deletions.
76 changes: 76 additions & 0 deletions checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,82 @@ spec:
## Admission Controller Webhook

- Name: `admission-controller-webhook`
- Groups: `basic`

Admission control webhooks can disrupt normal cluster operations. Specifically, this happens when an admission control webhook:
* targets a service that does not exist,
* targets a service in a namespace that does not exist.

### Example

```yaml
# Error: Configure a webhook pointing at a service that does not exist
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: sample-webhook.example.com
webhooks:
- name: sample-webhook.example.com
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
scope: "Namespaced"
clientConfig:
service:
namespace: webhook
name: missing-webhook-server
path: /pods
admissionReviewVersions:
- v1beta1
timeoutSeconds: 1
failurePolicy: Fail
```

### How to Fix

Point the webhook at the correct service.

```yaml
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: sample-webhook.example.com
webhooks:
- name: sample-webhook.example.com
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
scope: "Namespaced"
clientConfig:
service:
namespace: webhook
name: webhook-server
path: /pods
admissionReviewVersions:
- v1beta1
timeoutSeconds: 1
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: "skip-webhooks"
operator: "DoesNotExist"
```

## Admission Controller Webhook Replacement

- Name: `admission-controller-webhook-replacement`
- Groups: `doks`

Admission control webhooks can disrupt upgrade and node replacement operations by preventing system components from starting. Specifically, this happens when an admission control webhook:
Expand Down
131 changes: 131 additions & 0 deletions checks/basic/admission_controller_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
Copyright 2019 DigitalOcean
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 basic

import (
"fmt"

"github.com/digitalocean/clusterlint/checks"
"github.com/digitalocean/clusterlint/kube"
v1 "k8s.io/api/core/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{"basic"}
}

// Description returns a detailed human-readable description of what this check
// does.
func (w *webhookCheck) Description() string {
return "Check for admission control webhooks"
}

// Run runs this check on a set of Kubernetes objects.
func (w *webhookCheck) Run(objects *kube.Objects) ([]checks.Diagnostic, error) {
const apiserverServiceName = "kubernetes"

var diagnostics []checks.Diagnostic

for _, config := range objects.ValidatingWebhookConfigurations.Items {
for _, wh := range config.Webhooks {
if wh.ClientConfig.Service != nil {
// Ensure that the service (and its namespace) that is configure actually exists.

if !namespaceExists(objects.Namespaces, wh.ClientConfig.Service.Namespace) {
diagnostics = append(diagnostics, checks.Diagnostic{
Severity: checks.Error,
Message: fmt.Sprintf("Validating webhook %s is configured against a service in a namespace that does not exist.", wh.Name),
Kind: checks.ValidatingWebhookConfiguration,
Object: &config.ObjectMeta,
Owners: config.ObjectMeta.GetOwnerReferences(),
})
continue
}

if !serviceExists(objects.Services, wh.ClientConfig.Service.Name, wh.ClientConfig.Service.Namespace) {
diagnostics = append(diagnostics, checks.Diagnostic{
Severity: checks.Error,
Message: fmt.Sprintf("Validating webhook %s is configured against a service that does not exist.", wh.Name),
Kind: checks.ValidatingWebhookConfiguration,
Object: &config.ObjectMeta,
Owners: config.ObjectMeta.GetOwnerReferences(),
})
}
}
}
}

for _, config := range objects.MutatingWebhookConfigurations.Items {
for _, wh := range config.Webhooks {
if wh.ClientConfig.Service != nil {
// Ensure that the service (and its namespace) that is configure actually exists.

if !namespaceExists(objects.Namespaces, wh.ClientConfig.Service.Namespace) {
diagnostics = append(diagnostics, checks.Diagnostic{
Severity: checks.Error,
Message: fmt.Sprintf("Mutating webhook %s is configured against a service in a namespace that does not exist.", wh.Name),
Kind: checks.MutatingWebhookConfiguration,
Object: &config.ObjectMeta,
Owners: config.ObjectMeta.GetOwnerReferences(),
})
continue
}

if !serviceExists(objects.Services, wh.ClientConfig.Service.Name, wh.ClientConfig.Service.Namespace) {
diagnostics = append(diagnostics, checks.Diagnostic{
Severity: checks.Error,
Message: fmt.Sprintf("Mutating webhook %s is configured against a service that does not exist.", wh.Name),
Kind: checks.MutatingWebhookConfiguration,
Object: &config.ObjectMeta,
Owners: config.ObjectMeta.GetOwnerReferences(),
})
}
}
}
}
return diagnostics, nil
}

func namespaceExists(namespaceList *v1.NamespaceList, namespace string) bool {
for _, ns := range namespaceList.Items {
if ns.Name == namespace {
return true
}
}
return false
}

func serviceExists(serviceList *v1.ServiceList, service, namespace string) bool {
for _, svc := range serviceList.Items {
if svc.Name == service && svc.Namespace == namespace {
return true
}
}
return false
}
Loading

0 comments on commit eebd18a

Please sign in to comment.