From fb588a79b3c6eb22a9d8fbd1227ef73da76781cd Mon Sep 17 00:00:00 2001 From: Mustafa Abdelrahman Date: Fri, 21 Jul 2023 19:42:10 +0200 Subject: [PATCH] [experimental] Move admission webhook into skipper for better validation Signed-off-by: Mustafa Abdelrahman --- Makefile | 6 +- config/config.go | 20 +++++ dataclients/kubernetes/clusterclient.go | 4 +- .../definitions/ingressvalidator.go | 40 ++++++++-- .../definitions/routegroupvalidator.go | 16 +++- dataclients/kubernetes/kube.go | 3 + packaging/Makefile | 5 +- skipper.go | 27 +++++++ .../admission/admission.go | 0 .../admission/admission_test.go | 51 +++++++++++-- .../admission/definitions.go | 0 {cmd/webhook => webhook}/admission/ingress.go | 0 .../admission/routegroup.go | 0 .../testdata/ingress/invalid-filter-name.json | 39 ++++++++++ .../invalid-filters-and-predicates.json | 0 .../testdata/ingress/invalid-filters.json | 0 .../invalid-ingress-with-duplicate-hosts.json | 0 .../testdata/ingress/invalid-predicates.json | 0 .../testdata/ingress/invalid-routes.json | 0 .../valid-ingress-with-annotations.json | 0 .../valid-ingress-without-annotations.json | 0 .../admission/testdata/rg/invalid-rg.json | 0 .../testdata/rg/rg-with-duplicate-hosts.json | 0 .../rg/rg-with-invalid-backend-path.json | 0 ...-invalid-eskip-filters-and-predicates.json | 0 .../rg/rg-with-invalid-eskip-filters.json | 0 .../rg/rg-with-invalid-eskip-predicates.json | 0 .../testdata/rg/rg-with-multiple-filters.json | 0 .../rg/rg-with-multiple-predicates.json | 0 ...valid-eskip-filters-but-not-supported.json | 48 ++++++++++++ .../rg/rg-with-valid-eskip-filters.json | 0 .../rg/rg-with-valid-eskip-predicates.json | 0 .../admission/testdata/rg/valid-rg.json | 0 {cmd/webhook => webhook}/main.go | 76 +++++++++++-------- 34 files changed, 276 insertions(+), 59 deletions(-) rename {cmd/webhook => webhook}/admission/admission.go (100%) rename {cmd/webhook => webhook}/admission/admission_test.go (85%) rename {cmd/webhook => webhook}/admission/definitions.go (100%) rename {cmd/webhook => webhook}/admission/ingress.go (100%) rename {cmd/webhook => webhook}/admission/routegroup.go (100%) create mode 100644 webhook/admission/testdata/ingress/invalid-filter-name.json rename {cmd/webhook => webhook}/admission/testdata/ingress/invalid-filters-and-predicates.json (100%) rename {cmd/webhook => webhook}/admission/testdata/ingress/invalid-filters.json (100%) rename {cmd/webhook => webhook}/admission/testdata/ingress/invalid-ingress-with-duplicate-hosts.json (100%) rename {cmd/webhook => webhook}/admission/testdata/ingress/invalid-predicates.json (100%) rename {cmd/webhook => webhook}/admission/testdata/ingress/invalid-routes.json (100%) rename {cmd/webhook => webhook}/admission/testdata/ingress/valid-ingress-with-annotations.json (100%) rename {cmd/webhook => webhook}/admission/testdata/ingress/valid-ingress-without-annotations.json (100%) rename {cmd/webhook => webhook}/admission/testdata/rg/invalid-rg.json (100%) rename {cmd/webhook => webhook}/admission/testdata/rg/rg-with-duplicate-hosts.json (100%) rename {cmd/webhook => webhook}/admission/testdata/rg/rg-with-invalid-backend-path.json (100%) rename {cmd/webhook => webhook}/admission/testdata/rg/rg-with-invalid-eskip-filters-and-predicates.json (100%) rename {cmd/webhook => webhook}/admission/testdata/rg/rg-with-invalid-eskip-filters.json (100%) rename {cmd/webhook => webhook}/admission/testdata/rg/rg-with-invalid-eskip-predicates.json (100%) rename {cmd/webhook => webhook}/admission/testdata/rg/rg-with-multiple-filters.json (100%) rename {cmd/webhook => webhook}/admission/testdata/rg/rg-with-multiple-predicates.json (100%) create mode 100644 webhook/admission/testdata/rg/rg-with-valid-eskip-filters-but-not-supported.json rename {cmd/webhook => webhook}/admission/testdata/rg/rg-with-valid-eskip-filters.json (100%) rename {cmd/webhook => webhook}/admission/testdata/rg/rg-with-valid-eskip-predicates.json (100%) rename {cmd/webhook => webhook}/admission/testdata/rg/valid-rg.json (100%) rename {cmd/webhook => webhook}/main.go (52%) diff --git a/Makefile b/Makefile index 1b0f9b64eb..9f6d91632c 100644 --- a/Makefile +++ b/Makefile @@ -31,10 +31,6 @@ skipper: $(SOURCES) ## build skipper binary eskip: $(SOURCES) ## build eskip binary go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT_HASH)" -o bin/eskip ./cmd/eskip -.PHONY: webhook -webhook: $(SOURCES) ## build webhook binary - go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT_HASH)" -o bin/webhook ./cmd/webhook - .PHONY: routesrv routesrv: $(SOURCES) ## build routesrv binary go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT_HASH)" -o bin/routesrv ./cmd/routesrv @@ -46,7 +42,7 @@ ifeq (LIMIT_FDS, 256) endif .PHONY: build -build: $(SOURCES) lib skipper eskip webhook routesrv ## build library and all binaries +build: $(SOURCES) lib skipper eskip routesrv ## build library and all binaries build.linux.static: ## build static linux binary for amd64 GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o bin/skipper -ldflags "-extldflags=-static -X main.version=$(VERSION) -X main.commit=$(COMMIT_HASH)" ./cmd/skipper diff --git a/config/config.go b/config/config.go index 63db7d2977..544fd8a5a1 100644 --- a/config/config.go +++ b/config/config.go @@ -23,6 +23,7 @@ import ( "github.com/zalando/skipper/net" "github.com/zalando/skipper/proxy" "github.com/zalando/skipper/swarm" + "github.com/zalando/skipper/webhook" ) type Config struct { @@ -283,6 +284,13 @@ type Config struct { OpenPolicyAgentEnvoyMetadata string `yaml:"open-policy-agent-envoy-metadata"` OpenPolicyAgentCleanerInterval time.Duration `yaml:"open-policy-agent-cleaner-interval"` OpenPolicyAgentStartupTimeout time.Duration `yaml:"open-policy-agent-startup-timeout"` + // admission webhook + // validation addmission webhook + EnableValidationWebhook bool `yaml:"enable-validation-webhook"` + ValidationWebhookTLSCertFile string `yaml:"validation-webhook-tls-cert-file"` + ValidationWebhookTLSKeyFile string `yaml:"validation-webhook-tls-key-file"` + ValidationWebhookAddr string `yaml:"validation-webhook-address"` + ValidationWebhookLogLevel string `yaml:"validation-webhook-log-level"` } const ( @@ -567,6 +575,12 @@ func NewConfig() *Config { flag.Var(cfg.LuaModules, "lua-modules", "comma separated list of lua filter modules. Use . to selectively enable module symbols, for example: package,base._G,base.print,json") flag.Var(cfg.LuaSources, "lua-sources", `comma separated list of lua input types for the lua() filter. Valid sources "", "file", "inline", "file,inline" and "none". Use "file" to only allow lua file references in lua filter. Default "" is the same as "file","inline". Use "none" to disable lua filters.`) + flag.BoolVar(&cfg.EnableValidationWebhook, "enable-validation-webhook", false, "enables the validation admission webhook for RouteGroup CRD, *IMPORTANT* This mode runs only the validation webhook server and does not start the proxy") + flag.StringVar(&cfg.ValidationWebhookTLSCertFile, "validation-webhook-tls-cert-file", os.Getenv("CERT_FILE"), "File containing the certificate for HTTPS") + flag.StringVar(&cfg.ValidationWebhookTLSKeyFile, "validation-webhook-tls-key-file", os.Getenv("KEY_FILE"), "File containing the private key for HTTPS") + flag.StringVar(&cfg.ValidationWebhookAddr, "validation-webhook-address", webhook.DefaultHTTPSAddress, "The address to listen on") + flag.StringVar(&cfg.ValidationWebhookLogLevel, "validation-webhook-log-level", webhook.DefaultLogLevel, "Log level for validation webhook server") + cfg.flags = flag return cfg } @@ -906,6 +920,12 @@ func (c *Config) ToOptions() skipper.Options { OpenPolicyAgentEnvoyMetadata: c.OpenPolicyAgentEnvoyMetadata, OpenPolicyAgentCleanerInterval: c.OpenPolicyAgentCleanerInterval, OpenPolicyAgentStartupTimeout: c.OpenPolicyAgentStartupTimeout, + // Admission Webhook: + EnableValidationWebhook: c.EnableValidationWebhook, + ValidationWebhookTLSCertFile: c.ValidationWebhookTLSCertFile, + ValidationWebhookTLSKeyFile: c.ValidationWebhookTLSKeyFile, + ValidationWebhookAddr: c.ValidationWebhookAddr, + ValidationWebhookLogLevel: c.ValidationWebhookLogLevel, } for _, rcci := range c.CloneRoute { eskipClone := eskip.NewClone(rcci.Reg, rcci.Repl) diff --git a/dataclients/kubernetes/clusterclient.go b/dataclients/kubernetes/clusterclient.go index f09684910d..f637a28ae3 100644 --- a/dataclients/kubernetes/clusterclient.go +++ b/dataclients/kubernetes/clusterclient.go @@ -173,8 +173,8 @@ func newClusterClient(o Options, apiURL, ingCls, rgCls string, quit <-chan struc httpClient: httpClient, apiURL: apiURL, certificateRegistry: o.CertificateRegistry, - routeGroupValidator: &definitions.RouteGroupValidator{}, - ingressValidator: &definitions.IngressV1Validator{}, + ingressValidator: &definitions.IngressV1Validator{FiltersRegistry: o.FiltersRegistry}, + routeGroupValidator: &definitions.RouteGroupValidator{FiltersRegistry: o.FiltersRegistry}, enableEndpointSlices: o.KubernetesEnableEndpointslices, } diff --git a/dataclients/kubernetes/definitions/ingressvalidator.go b/dataclients/kubernetes/definitions/ingressvalidator.go index 1a7e2499d8..dd2213d2d4 100644 --- a/dataclients/kubernetes/definitions/ingressvalidator.go +++ b/dataclients/kubernetes/definitions/ingressvalidator.go @@ -4,9 +4,12 @@ import ( "fmt" "github.com/zalando/skipper/eskip" + "github.com/zalando/skipper/filters" ) -type IngressV1Validator struct{} +type IngressV1Validator struct { + FiltersRegistry filters.Registry +} func (igv *IngressV1Validator) Validate(item *IngressV1Item) error { var errs []error @@ -19,14 +22,19 @@ func (igv *IngressV1Validator) Validate(item *IngressV1Item) error { } func (igv *IngressV1Validator) validateFilterAnnotation(annotations map[string]string) error { + var errs []error if filters, ok := annotations[IngressFilterAnnotation]; ok { - _, err := eskip.ParseFilters(filters) + parsedFilters, err := eskip.ParseFilters(filters) if err != nil { - err = fmt.Errorf("invalid \"%s\" annotation: %w", IngressFilterAnnotation, err) + errs = append(errs, fmt.Errorf("invalid \"%s\" annotation: %w", IngressFilterAnnotation, err)) + } + + if igv.FiltersRegistry != nil { + errs = append(errs, igv.validateFiltersNames(parsedFilters)) } - return err } - return nil + + return errorsJoin(errs...) } func (igv *IngressV1Validator) validatePredicateAnnotation(annotations map[string]string) error { @@ -41,12 +49,28 @@ func (igv *IngressV1Validator) validatePredicateAnnotation(annotations map[strin } func (igv *IngressV1Validator) validateRoutesAnnotation(annotations map[string]string) error { + var errs []error if routes, ok := annotations[IngressRoutesAnnotation]; ok { - _, err := eskip.Parse(routes) + parsedRoutes, err := eskip.Parse(routes) if err != nil { - err = fmt.Errorf("invalid \"%s\" annotation: %w", IngressRoutesAnnotation, err) + errs = append(errs, fmt.Errorf("invalid \"%s\" annotation: %w", IngressRoutesAnnotation, err)) + } + + if igv.FiltersRegistry != nil { + for _, r := range parsedRoutes { + errs = append(errs, igv.validateFiltersNames(r.Filters)) + } + } + } + + return errorsJoin(errs...) +} + +func (igv *IngressV1Validator) validateFiltersNames(filters []*eskip.Filter) error { + for _, f := range filters { + if _, ok := igv.FiltersRegistry[f.Name]; !ok { + return fmt.Errorf("filter \"%s\" is not supported", f.Name) } - return err } return nil } diff --git a/dataclients/kubernetes/definitions/routegroupvalidator.go b/dataclients/kubernetes/definitions/routegroupvalidator.go index 3f96167194..e85467f3fa 100644 --- a/dataclients/kubernetes/definitions/routegroupvalidator.go +++ b/dataclients/kubernetes/definitions/routegroupvalidator.go @@ -6,9 +6,12 @@ import ( "net/url" "github.com/zalando/skipper/eskip" + "github.com/zalando/skipper/filters" ) -type RouteGroupValidator struct{} +type RouteGroupValidator struct { + FiltersRegistry filters.Registry +} var ( errSingleFilterExpected = errors.New("single filter expected") @@ -72,6 +75,8 @@ func (rgv *RouteGroupValidator) validateFilters(item *RouteGroupItem) error { errs = append(errs, err) } else if len(filters) != 1 { errs = append(errs, fmt.Errorf("%w at %q", errSingleFilterExpected, f)) + } else if rgv.FiltersRegistry != nil { + errs = append(errs, rgv.validateFiltersNames(filters)) } } } @@ -79,6 +84,15 @@ func (rgv *RouteGroupValidator) validateFilters(item *RouteGroupItem) error { return errorsJoin(errs...) } +func (rgv *RouteGroupValidator) validateFiltersNames(filters []*eskip.Filter) error { + for _, f := range filters { + if _, ok := rgv.FiltersRegistry[f.Name]; !ok { + return fmt.Errorf("filter \"%s\" is not supported", f.Name) + } + } + return nil +} + func (rgv *RouteGroupValidator) validatePredicates(item *RouteGroupItem) error { var errs []error for _, r := range item.Spec.Routes { diff --git a/dataclients/kubernetes/kube.go b/dataclients/kubernetes/kube.go index 3500c71de0..753bbacab3 100644 --- a/dataclients/kubernetes/kube.go +++ b/dataclients/kubernetes/kube.go @@ -240,6 +240,9 @@ type Options struct { // DefaultLoadBalancerAlgorithm sets the default algorithm to be used for load balancing between backend endpoints, // available options: roundRobin, consistentHash, random, powerOfRandomNChoices DefaultLoadBalancerAlgorithm string + + // FiltersRegistry is used to validate filters names in RouteGroups/Ingresses + FiltersRegistry filters.Registry } // Client is a Skipper DataClient implementation used to create routes based on Kubernetes Ingress settings. diff --git a/packaging/Makefile b/packaging/Makefile index e140665128..8d93826e02 100644 --- a/packaging/Makefile +++ b/packaging/Makefile @@ -2,7 +2,7 @@ VERSION ?= $(shell git rev-parse HEAD) REGISTRY ?= registry-write.opensource.zalan.do/teapot -BINARIES ?= skipper webhook eskip routesrv +BINARIES ?= skipper eskip routesrv IMAGE ?= $(REGISTRY)/skipper:$(VERSION) ARM64_IMAGE ?= $(REGISTRY)/skipper-arm64:$(VERSION) ARM_IMAGE ?= $(REGISTRY)/skipper-armv7:$(VERSION) @@ -25,9 +25,6 @@ skipper: eskip: GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOARM) CGO_ENABLED=$(CGO_ENABLED) go build -o eskip ../cmd/eskip/*.go -webhook: - GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOARM) CGO_ENABLED=$(CGO_ENABLED) go build -o webhook ../cmd/webhook/*.go - routesrv: GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOARM) CGO_ENABLED=$(CGO_ENABLED) go build -o routesrv ../cmd/routesrv/*.go diff --git a/skipper.go b/skipper.go index 3a6dc0344c..832e49835b 100644 --- a/skipper.go +++ b/skipper.go @@ -69,6 +69,7 @@ import ( "github.com/zalando/skipper/secrets/certregistry" "github.com/zalando/skipper/swarm" "github.com/zalando/skipper/tracing" + "github.com/zalando/skipper/webhook" ) const ( @@ -917,6 +918,22 @@ type Options struct { OpenPolicyAgentEnvoyMetadata string OpenPolicyAgentCleanerInterval time.Duration OpenPolicyAgentStartupTimeout time.Duration + + // EnableValidationWebhook runs skipper in admission webhook mode + // *IMPORTANT* This mode runs only the validation webhook server and does not start the proxy + EnableValidationWebhook bool + + // ValidationWebhookTLSCertFile is the path to the certificate file for the admission webhook server + ValidationWebhookTLSCertFile string + + // ValidationWebhookTLSKeyFile is the path to the private key file for the admission webhook server + ValidationWebhookTLSKeyFile string + + // ValidationWebhookAddr is the address to listen on for the admission webhook server + ValidationWebhookAddr string + + // ValidationWebhookLogLevel is the log level for the admission webhook server + ValidationWebhookLogLevel string } func (o *Options) KubernetesDataClientOptions() kubernetes.Options { @@ -1959,6 +1976,16 @@ func run(o Options, sig chan os.Signal, idleConnsCH chan struct{}) error { routing := routing.New(ro) defer routing.Close() + if o.EnableValidationWebhook { + webhook.Run( + o.ValidationWebhookLogLevel, + o.ValidationWebhookAddr, + o.ValidationWebhookTLSCertFile, + o.ValidationWebhookTLSKeyFile, + o.filterRegistry(), + ) + } + proxyFlags := proxy.Flags(o.ProxyOptions) | o.ProxyFlags proxyParams := proxy.Params{ Routing: routing, diff --git a/cmd/webhook/admission/admission.go b/webhook/admission/admission.go similarity index 100% rename from cmd/webhook/admission/admission.go rename to webhook/admission/admission.go diff --git a/cmd/webhook/admission/admission_test.go b/webhook/admission/admission_test.go similarity index 85% rename from cmd/webhook/admission/admission_test.go rename to webhook/admission/admission_test.go index d878cc8f3d..0605de9204 100644 --- a/cmd/webhook/admission/admission_test.go +++ b/webhook/admission/admission_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zalando/skipper/dataclients/kubernetes/definitions" + "github.com/zalando/skipper/filters/builtin" ) const ( @@ -87,9 +88,10 @@ func TestUnsupportedContentType(t *testing.T) { func TestRouteGroupAdmitter(t *testing.T) { for _, tc := range []struct { - name string - inputFile string - message string + name string + inputFile string + message string + withFilterRegistry bool }{ { name: "allowed", @@ -104,6 +106,12 @@ func TestRouteGroupAdmitter(t *testing.T) { name: "valid eskip filters", inputFile: "rg-with-valid-eskip-filters.json", }, + { + name: "valid eskip filters but not supported", + inputFile: "rg-with-valid-eskip-filters-but-not-supported.json", + message: `filter \"test\" is not supported`, + withFilterRegistry: true, + }, { name: "invalid eskip filters", inputFile: "rg-with-invalid-eskip-filters.json", @@ -157,7 +165,15 @@ func TestRouteGroupAdmitter(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - rgAdm := &RouteGroupAdmitter{RouteGroupValidator: &definitions.RouteGroupValidator{}} + + var rgValidator *definitions.RouteGroupValidator + if tc.withFilterRegistry { + rgValidator = &definitions.RouteGroupValidator{FiltersRegistry: builtin.MakeRegistry()} + } else { + rgValidator = &definitions.RouteGroupValidator{} + } + + rgAdm := &RouteGroupAdmitter{RouteGroupValidator: rgValidator} h := Handler(rgAdm) h(w, req) @@ -175,9 +191,10 @@ func TestRouteGroupAdmitter(t *testing.T) { func TestIngressAdmitter(t *testing.T) { for _, tc := range []struct { - name string - inputFile string - message string + name string + inputFile string + message string + withFilterRegistry bool }{ { name: "allowed without annotations", @@ -192,6 +209,16 @@ func TestIngressAdmitter(t *testing.T) { inputFile: "invalid-filters.json", message: `invalid \"zalando.org/skipper-filter\" annotation: parse failed after token this, position 9: syntax error`, }, + { + name: "Filter not found in filter registry", + inputFile: "invalid-filter-name.json", + message: `filter \"play\" is not supported`, + withFilterRegistry: true, + }, + { + name: "Filter not found in filter registry but valid eskip filters", + inputFile: "invalid-filter-name.json", + }, { name: "invalid eskip predicates", inputFile: "invalid-predicates.json", @@ -221,7 +248,15 @@ func TestIngressAdmitter(t *testing.T) { req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - ingressAdm := &IngressAdmitter{IngressValidator: &definitions.IngressV1Validator{}} + + var ingressValidator *definitions.IngressV1Validator + if tc.withFilterRegistry { + ingressValidator = &definitions.IngressV1Validator{FiltersRegistry: builtin.MakeRegistry()} + } else { + ingressValidator = &definitions.IngressV1Validator{} + } + + ingressAdm := &IngressAdmitter{IngressValidator: ingressValidator} h := Handler(ingressAdm) h(w, req) diff --git a/cmd/webhook/admission/definitions.go b/webhook/admission/definitions.go similarity index 100% rename from cmd/webhook/admission/definitions.go rename to webhook/admission/definitions.go diff --git a/cmd/webhook/admission/ingress.go b/webhook/admission/ingress.go similarity index 100% rename from cmd/webhook/admission/ingress.go rename to webhook/admission/ingress.go diff --git a/cmd/webhook/admission/routegroup.go b/webhook/admission/routegroup.go similarity index 100% rename from cmd/webhook/admission/routegroup.go rename to webhook/admission/routegroup.go diff --git a/webhook/admission/testdata/ingress/invalid-filter-name.json b/webhook/admission/testdata/ingress/invalid-filter-name.json new file mode 100644 index 0000000000..6eb9902a24 --- /dev/null +++ b/webhook/admission/testdata/ingress/invalid-filter-name.json @@ -0,0 +1,39 @@ +{ + "request": { + "uid": "req-uid", + "name": "req1", + "namespace": "n1", + "object": { + "metadata": { + "name": "ing1", + "namespace": "ing1", + "annotations": { + "zalando.org/skipper-filter": "play(10) -> inlineContent(\"This should't work\")" + } + }, + "spec": { + "rules": [ + { + "host": "example.com", + "http": { + "paths": [ + { + "backend": { + "service": { + "name": "example-service", + "port": { + "number": 80 + } + } + }, + "path": "/", + "pathType": "Prefix" + } + ] + } + } + ] + } + } + } +} diff --git a/cmd/webhook/admission/testdata/ingress/invalid-filters-and-predicates.json b/webhook/admission/testdata/ingress/invalid-filters-and-predicates.json similarity index 100% rename from cmd/webhook/admission/testdata/ingress/invalid-filters-and-predicates.json rename to webhook/admission/testdata/ingress/invalid-filters-and-predicates.json diff --git a/cmd/webhook/admission/testdata/ingress/invalid-filters.json b/webhook/admission/testdata/ingress/invalid-filters.json similarity index 100% rename from cmd/webhook/admission/testdata/ingress/invalid-filters.json rename to webhook/admission/testdata/ingress/invalid-filters.json diff --git a/cmd/webhook/admission/testdata/ingress/invalid-ingress-with-duplicate-hosts.json b/webhook/admission/testdata/ingress/invalid-ingress-with-duplicate-hosts.json similarity index 100% rename from cmd/webhook/admission/testdata/ingress/invalid-ingress-with-duplicate-hosts.json rename to webhook/admission/testdata/ingress/invalid-ingress-with-duplicate-hosts.json diff --git a/cmd/webhook/admission/testdata/ingress/invalid-predicates.json b/webhook/admission/testdata/ingress/invalid-predicates.json similarity index 100% rename from cmd/webhook/admission/testdata/ingress/invalid-predicates.json rename to webhook/admission/testdata/ingress/invalid-predicates.json diff --git a/cmd/webhook/admission/testdata/ingress/invalid-routes.json b/webhook/admission/testdata/ingress/invalid-routes.json similarity index 100% rename from cmd/webhook/admission/testdata/ingress/invalid-routes.json rename to webhook/admission/testdata/ingress/invalid-routes.json diff --git a/cmd/webhook/admission/testdata/ingress/valid-ingress-with-annotations.json b/webhook/admission/testdata/ingress/valid-ingress-with-annotations.json similarity index 100% rename from cmd/webhook/admission/testdata/ingress/valid-ingress-with-annotations.json rename to webhook/admission/testdata/ingress/valid-ingress-with-annotations.json diff --git a/cmd/webhook/admission/testdata/ingress/valid-ingress-without-annotations.json b/webhook/admission/testdata/ingress/valid-ingress-without-annotations.json similarity index 100% rename from cmd/webhook/admission/testdata/ingress/valid-ingress-without-annotations.json rename to webhook/admission/testdata/ingress/valid-ingress-without-annotations.json diff --git a/cmd/webhook/admission/testdata/rg/invalid-rg.json b/webhook/admission/testdata/rg/invalid-rg.json similarity index 100% rename from cmd/webhook/admission/testdata/rg/invalid-rg.json rename to webhook/admission/testdata/rg/invalid-rg.json diff --git a/cmd/webhook/admission/testdata/rg/rg-with-duplicate-hosts.json b/webhook/admission/testdata/rg/rg-with-duplicate-hosts.json similarity index 100% rename from cmd/webhook/admission/testdata/rg/rg-with-duplicate-hosts.json rename to webhook/admission/testdata/rg/rg-with-duplicate-hosts.json diff --git a/cmd/webhook/admission/testdata/rg/rg-with-invalid-backend-path.json b/webhook/admission/testdata/rg/rg-with-invalid-backend-path.json similarity index 100% rename from cmd/webhook/admission/testdata/rg/rg-with-invalid-backend-path.json rename to webhook/admission/testdata/rg/rg-with-invalid-backend-path.json diff --git a/cmd/webhook/admission/testdata/rg/rg-with-invalid-eskip-filters-and-predicates.json b/webhook/admission/testdata/rg/rg-with-invalid-eskip-filters-and-predicates.json similarity index 100% rename from cmd/webhook/admission/testdata/rg/rg-with-invalid-eskip-filters-and-predicates.json rename to webhook/admission/testdata/rg/rg-with-invalid-eskip-filters-and-predicates.json diff --git a/cmd/webhook/admission/testdata/rg/rg-with-invalid-eskip-filters.json b/webhook/admission/testdata/rg/rg-with-invalid-eskip-filters.json similarity index 100% rename from cmd/webhook/admission/testdata/rg/rg-with-invalid-eskip-filters.json rename to webhook/admission/testdata/rg/rg-with-invalid-eskip-filters.json diff --git a/cmd/webhook/admission/testdata/rg/rg-with-invalid-eskip-predicates.json b/webhook/admission/testdata/rg/rg-with-invalid-eskip-predicates.json similarity index 100% rename from cmd/webhook/admission/testdata/rg/rg-with-invalid-eskip-predicates.json rename to webhook/admission/testdata/rg/rg-with-invalid-eskip-predicates.json diff --git a/cmd/webhook/admission/testdata/rg/rg-with-multiple-filters.json b/webhook/admission/testdata/rg/rg-with-multiple-filters.json similarity index 100% rename from cmd/webhook/admission/testdata/rg/rg-with-multiple-filters.json rename to webhook/admission/testdata/rg/rg-with-multiple-filters.json diff --git a/cmd/webhook/admission/testdata/rg/rg-with-multiple-predicates.json b/webhook/admission/testdata/rg/rg-with-multiple-predicates.json similarity index 100% rename from cmd/webhook/admission/testdata/rg/rg-with-multiple-predicates.json rename to webhook/admission/testdata/rg/rg-with-multiple-predicates.json diff --git a/webhook/admission/testdata/rg/rg-with-valid-eskip-filters-but-not-supported.json b/webhook/admission/testdata/rg/rg-with-valid-eskip-filters-but-not-supported.json new file mode 100644 index 0000000000..4e139d78d6 --- /dev/null +++ b/webhook/admission/testdata/rg/rg-with-valid-eskip-filters-but-not-supported.json @@ -0,0 +1,48 @@ +{ + "request": { + "uid": "req-uid", + "name": "req1", + "operation": "create", + "kind": { + "group": "zalando", + "version": "v1", + "kind": "RouteGroup" + }, + "namespace": "n1", + "object": { + "metadata": { + "name": "rg1", + "namespace": "n1" + }, + "spec": { + "backends": [ + { + "name": "backend", + "type": "shunt" + } + ], + "defaultBackends": [ + { + "backendName": "backend" + } + ], + "routes": [ + { + "backends": [ + { + "backendName": "backend" + } + ], + "filters": [ + "test(201)" + ], + "path": "/", + "predicates": [ + "Method(\"GET\")" + ] + } + ] + } + } + } +} diff --git a/cmd/webhook/admission/testdata/rg/rg-with-valid-eskip-filters.json b/webhook/admission/testdata/rg/rg-with-valid-eskip-filters.json similarity index 100% rename from cmd/webhook/admission/testdata/rg/rg-with-valid-eskip-filters.json rename to webhook/admission/testdata/rg/rg-with-valid-eskip-filters.json diff --git a/cmd/webhook/admission/testdata/rg/rg-with-valid-eskip-predicates.json b/webhook/admission/testdata/rg/rg-with-valid-eskip-predicates.json similarity index 100% rename from cmd/webhook/admission/testdata/rg/rg-with-valid-eskip-predicates.json rename to webhook/admission/testdata/rg/rg-with-valid-eskip-predicates.json diff --git a/cmd/webhook/admission/testdata/rg/valid-rg.json b/webhook/admission/testdata/rg/valid-rg.json similarity index 100% rename from cmd/webhook/admission/testdata/rg/valid-rg.json rename to webhook/admission/testdata/rg/valid-rg.json diff --git a/cmd/webhook/main.go b/webhook/main.go similarity index 52% rename from cmd/webhook/main.go rename to webhook/main.go index 82a67b303f..66331135a1 100644 --- a/cmd/webhook/main.go +++ b/webhook/main.go @@ -1,8 +1,7 @@ -package main +package webhook import ( "context" - "flag" "net/http" "os" "os/signal" @@ -11,50 +10,65 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" log "github.com/sirupsen/logrus" - "github.com/zalando/skipper/cmd/webhook/admission" "github.com/zalando/skipper/dataclients/kubernetes/definitions" + "github.com/zalando/skipper/filters" + "github.com/zalando/skipper/webhook/admission" ) const ( - defaultHTTPSAddress = ":9443" + DefaultHTTPSAddress = ":9443" defaultHTTPAddress = ":9080" ) -type config struct { - debug bool - certFile string - keyFile string - address string +var DefaultLogLevel = log.InfoLevel.String() + +type options struct { + loglevel string + certFile string + keyFile string + address string + filterRegistry filters.Registry } -func (c *config) parse() { - flag.BoolVar(&c.debug, "debug", false, "Enable debug logging") - flag.StringVar(&c.certFile, "tls-cert-file", os.Getenv("CERT_FILE"), "File containing the certificate for HTTPS") - flag.StringVar(&c.keyFile, "tls-key-file", os.Getenv("KEY_FILE"), "File containing the private key for HTTPS") - flag.StringVar(&c.address, "address", defaultHTTPSAddress, "The address to listen on") - flag.Parse() +func (opts *options) parse() { + + if opts.loglevel != "" { + loglevel, err := log.ParseLevel(opts.loglevel) + if err != nil { + log.Error("Config parse error: ", err) + log.SetLevel(log.InfoLevel) + } + log.SetLevel(loglevel) + } - if (c.certFile != "" || c.keyFile != "") && !(c.certFile != "" && c.keyFile != "") { + if (opts.certFile != "" || opts.keyFile != "") && !(opts.certFile != "" && opts.keyFile != "") { log.Fatal("Config parse error: both of TLS cert & key must be provided or neither (for testing )") return } // support non-HTTPS for local testing - if (c.certFile == "" && c.keyFile == "") && c.address == defaultHTTPSAddress { - c.address = defaultHTTPAddress + if (opts.certFile == "" && opts.keyFile == "") && opts.address == DefaultHTTPSAddress { + opts.address = defaultHTTPAddress } - if c.debug { - log.SetLevel(log.DebugLevel) +} + +func Run(loglevel, address, certFile, keyFile string, filterRegistry filters.Registry) { + opts := &options{ + loglevel: loglevel, + address: address, + certFile: certFile, + keyFile: keyFile, + filterRegistry: filterRegistry, } + run(opts) } -func main() { - var cfg = &config{} - cfg.parse() +func run(opts *options) { + opts.parse() - rgAdmitter := &admission.RouteGroupAdmitter{RouteGroupValidator: &definitions.RouteGroupValidator{}} - ingressAdmitter := &admission.IngressAdmitter{IngressValidator: &definitions.IngressV1Validator{}} + rgAdmitter := &admission.RouteGroupAdmitter{RouteGroupValidator: &definitions.RouteGroupValidator{FiltersRegistry: opts.filterRegistry}} + ingressAdmitter := &admission.IngressAdmitter{IngressValidator: &definitions.IngressV1Validator{FiltersRegistry: opts.filterRegistry}} handler := http.NewServeMux() handler.Handle("/routegroups", admission.Handler(rgAdmitter)) handler.Handle("/ingresses", admission.Handler(ingressAdmitter)) @@ -63,7 +77,7 @@ func main() { // One can use generate_cert.go in https://golang.org/pkg/crypto/tls // to generate cert.pem and key.pem. - serve(cfg, handler) + serve(opts, handler) } func healthCheck(writer http.ResponseWriter, _ *http.Request) { @@ -74,15 +88,15 @@ func healthCheck(writer http.ResponseWriter, _ *http.Request) { } -func serve(cfg *config, handler http.Handler) { +func serve(opts *options, handler http.Handler) { server := &http.Server{ - Addr: cfg.address, + Addr: opts.address, Handler: handler, ReadTimeout: 1 * time.Minute, ReadHeaderTimeout: 1 * time.Minute, } - log.Infof("Starting server on %s", cfg.address) + log.Infof("Starting server on %s", opts.address) sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGTERM) @@ -93,8 +107,8 @@ func serve(cfg *config, handler http.Handler) { }() var err error - if cfg.certFile != "" && cfg.keyFile != "" { - err = server.ListenAndServeTLS(cfg.certFile, cfg.keyFile) + if opts.certFile != "" && opts.keyFile != "" { + err = server.ListenAndServeTLS(opts.certFile, opts.keyFile) } else { // support non-HTTPS for local testing err = server.ListenAndServe()