Skip to content

Commit

Permalink
kuma-cp: add a validating admission web hook to verify correctness of…
Browse files Browse the repository at this point in the history
… `<port>.service.kuma.io/protocol` annotations on k8s Service objects
  • Loading branch information
yskopets authored and jakubdyszkiewicz committed Apr 6, 2020
1 parent 33f98ce commit b5b9168
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4500,3 +4500,21 @@ webhooks:
- healthchecks
- meshes
- proxytemplates
- name: service.validator.kuma-admission.kuma.io
failurePolicy: Fail
clientConfig:
caBundle: Q0VSVA==
service:
namespace: kuma-system
name: kuma-control-plane
path: /validate-v1-service
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- services
Original file line number Diff line number Diff line change
Expand Up @@ -4500,4 +4500,22 @@ webhooks:
- dataplanes
- healthchecks
- meshes
- proxytemplates
- proxytemplates
- name: service.validator.kuma-admission.kuma.io
failurePolicy: Fail
clientConfig:
caBundle: QWRtaXNzaW9uQ2VydA==
service:
namespace: kuma
name: kuma-ctrl-plane
path: /validate-v1-service
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- services
18 changes: 18 additions & 0 deletions app/kumactl/data/install/k8s/control-plane/kuma-cp/app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,21 @@ webhooks:
- healthchecks
- meshes
- proxytemplates
- name: service.validator.kuma-admission.kuma.io
failurePolicy: Fail
clientConfig:
caBundle: {{ .AdmissionServerTlsCert | b64enc }}
service:
namespace: {{ .Namespace }}
name: {{ .ControlPlaneServiceName }}
path: /validate-v1-service
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- services
5 changes: 5 additions & 0 deletions pkg/plugins/runtime/k8s/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

kube_schema "k8s.io/apimachinery/pkg/runtime/schema"
kube_ctrl "sigs.k8s.io/controller-runtime"
kuba_webhook "sigs.k8s.io/controller-runtime/pkg/webhook"
)

var (
Expand Down Expand Up @@ -122,5 +123,9 @@ func addValidators(mgr kube_ctrl.Manager, rt core_runtime.Runtime) error {
path := "/validate-kuma-io-v1alpha1"
mgr.GetWebhookServer().Register(path, composite.WebHook())
log.Info("Registering a validation composite webhook", "path", path)

mgr.GetWebhookServer().Register("/validate-v1-service", &kuba_webhook.Admission{Handler: &k8s_webhooks.ServiceValidator{}})
log.Info("Registering a validation webhook for v1/Service", "path", "/validate-v1-service")

return nil
}
55 changes: 55 additions & 0 deletions pkg/plugins/runtime/k8s/webhooks/service_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package webhooks

import (
"context"
"fmt"
"net/http"

kube_core "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

mesh_core "github.com/Kong/kuma/pkg/core/resources/apis/mesh"
"github.com/Kong/kuma/pkg/core/validators"
)

// ServiceValidator validates Kuma-specific annotations on Services.
type ServiceValidator struct {
decoder *admission.Decoder
}

// Handle admits a Service only if Kuma-specific annotations have proper values.
func (v *ServiceValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
svc := &kube_core.Service{}

err := v.decoder.Decode(req, svc)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}

if err := v.validate(svc); err != nil {
if verr, ok := err.(*validators.ValidationError); ok {
return convertValidationErrorOf(verr, svc, svc)
}
return admission.Denied(err.Error())
}

return admission.Allowed("")
}

func (v *ServiceValidator) validate(svc *kube_core.Service) error {
verr := &validators.ValidationError{}
for _, svcPort := range svc.Spec.Ports {
protocolAnnotation := fmt.Sprintf("%d.service.kuma.io/protocol", svcPort.Port)
protocolAnnotationValue, exists := svc.Annotations[protocolAnnotation]
if exists && mesh_core.ParseProtocol(protocolAnnotationValue) == mesh_core.ProtocolUnknown {
verr.AddViolationAt(validators.RootedAt("metadata").Field("annotations").Key(protocolAnnotation),
fmt.Sprintf("value %q is not valid. %s", protocolAnnotationValue, mesh_core.AllowedValuesHint(mesh_core.SupportedProtocols.Strings()...)))
}
}
return verr.OrNil()
}

func (v *ServiceValidator) InjectDecoder(d *admission.Decoder) error {
v.decoder = d
return nil
}
188 changes: 188 additions & 0 deletions pkg/plugins/runtime/k8s/webhooks/service_validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package webhooks_test

import (
"context"

. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"

. "github.com/Kong/kuma/pkg/plugins/runtime/k8s/webhooks"

"github.com/ghodss/yaml"

admissionv1beta1 "k8s.io/api/admission/v1beta1"
kube_core "k8s.io/api/core/v1"
kube_runtime "k8s.io/apimachinery/pkg/runtime"
kube_admission "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

var _ = Describe("ServiceValidator", func() {

var decoder *kube_admission.Decoder

BeforeEach(func() {
scheme := kube_runtime.NewScheme()
// expect
Expect(kube_core.AddToScheme(scheme)).To(Succeed())

var err error
// when
decoder, err = kube_admission.NewDecoder(scheme)
// then
Expect(err).ToNot(HaveOccurred())
})

type testCase struct {
request string
expected string
}

DescribeTable("should make a proper admission verdict",
func(given testCase) {
// setup
validator := &ServiceValidator{}
// when
err := validator.InjectDecoder(decoder)
// then
Expect(err).ToNot(HaveOccurred())

// setup
admissionReview := admissionv1beta1.AdmissionReview{}
// when
err = yaml.Unmarshal([]byte(given.request), &admissionReview)
// then
Expect(err).ToNot(HaveOccurred())

// do
resp := validator.Handle(context.Background(), kube_admission.Request{
AdmissionRequest: *admissionReview.Request,
})

// when
actual, err := yaml.Marshal(resp.AdmissionResponse)
// then
Expect(err).ToNot(HaveOccurred())
// and
Expect(actual).To(MatchYAML(given.expected))
},
Entry("Service w/o Kuma-specific annotation", testCase{
request: `
apiVersion: admission.k8s.io/v1
kind: AdmissionReview
request:
uid: 12345
kind:
group: ""
kind: Service
version: v1
name: backend
namespace: kuma-example
object:
apiVersion: v1
kind: Service
spec:
ports:
- port: 8080
targetPort: 8080
operation: UPDATE
`,
expected: `
allowed: true
status:
code: 200
metadata: {}
uid: ""
`,
}),
Entry("Service w/ valid `<port>.service.kuma.io/protocol` annotations", testCase{
request: `
apiVersion: admission.k8s.io/v1
kind: AdmissionReview
request:
uid: 12345
kind:
group: ""
kind: Service
version: v1
name: backend
namespace: kuma-example
object:
apiVersion: v1
kind: Service
metadata:
annotations:
8080.service.kuma.io/protocol: http
5432.service.kuma.io/protocol: tcp
1234.service.kuma.io/protocol: invalid-value # should be ignored unless this Service actually declares port '1234'
spec:
ports:
- port: 8080
targetPort: 8080
- port: 5432
targetPort: 5432
operation: UPDATE
`,
expected: `
allowed: true
status:
code: 200
metadata: {}
uid: ""
`,
}),
Entry("Service w/ multiple invalid `<port>.service.kuma.io/protocol` annotations", testCase{
request: `
apiVersion: admission.k8s.io/v1
kind: AdmissionReview
request:
uid: 12345
kind:
group: ""
kind: Service
version: v1
name: backend
namespace: kuma-example
object:
apiVersion: v1
kind: Service
metadata:
annotations:
8080.service.kuma.io/protocol: http # valid protocol
8081.service.kuma.io/protocol: "" # invalid empty value
8082.service.kuma.io/protocol: not-yet-supported-protocol # invalid unknown value
spec:
ports:
- port: 8080
targetPort: 8080
- port: 8081
targetPort: 8081
- port: 8082
targetPort: 8082
operation: UPDATE
`,
expected: `
allowed: false
status:
code: 422
details:
causes:
- field: spec.metadata.annotations["8081.service.kuma.io/protocol"]
message: 'value "" is not valid. Allowed values: http, tcp'
reason: FieldValueInvalid
- field: spec.metadata.annotations["8082.service.kuma.io/protocol"]
message: 'value "not-yet-supported-protocol" is not valid. Allowed values: http,
tcp'
reason: FieldValueInvalid
kind: Service
message: 'spec.metadata.annotations["8081.service.kuma.io/protocol"]: value "" is
not valid. Allowed values: http, tcp; spec.metadata.annotations["8082.service.kuma.io/protocol"]:
value "not-yet-supported-protocol" is not valid. Allowed values: http, tcp'
metadata: {}
reason: Invalid
status: Failure
uid: ""
`,
}),
)
})
7 changes: 6 additions & 1 deletion pkg/plugins/runtime/k8s/webhooks/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kube_runtime "k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

core_model "github.com/Kong/kuma/pkg/core/resources/model"
Expand Down Expand Up @@ -72,9 +73,13 @@ func (h *validatingHandler) Supports(admission.Request) bool {
}

func convertValidationError(kumaErr *validators.ValidationError, obj k8s_model.KubernetesObject) admission.Response {
return convertValidationErrorOf(kumaErr, obj, obj.GetObjectMeta())
}

func convertValidationErrorOf(kumaErr *validators.ValidationError, obj kube_runtime.Object, objMeta metav1.Object) admission.Response {
kumaErr = convertFieldNames(kumaErr)
details := &metav1.StatusDetails{
Name: obj.GetObjectMeta().Name,
Name: objMeta.GetName(),
Kind: obj.GetObjectKind().GroupVersionKind().Kind,
}
resp := admission.Response{
Expand Down

0 comments on commit b5b9168

Please sign in to comment.