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 0ed251200c..8ffab403c6 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 { @@ -294,7 +295,19 @@ type Config struct { OpenPolicyAgentMaxRequestBodySize int64 `yaml:"open-policy-agent-max-request-body-size"` OpenPolicyAgentMaxMemoryBodyParsing int64 `yaml:"open-policy-agent-max-memory-body-parsing"` - PassiveHealthCheck mapFlags `yaml:"passive-health-check"` + PassiveHealthCheck mapFlags `yaml:"passive-health-check"` + EnableOpenPolicyAgent bool `yaml:"enable-open-policy-agent"` + OpenPolicyAgentConfigTemplate string `yaml:"open-policy-agent-config-template"` + 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 ( @@ -595,6 +608,13 @@ func NewConfig() *Config { flag.Var(&cfg.PassiveHealthCheck, "passive-health-check", "sets the parameters for passive health check feature") cfg.Flags = flag + 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 } @@ -944,6 +964,12 @@ func (c *Config) ToOptions() skipper.Options { OpenPolicyAgentMaxMemoryBodyParsing: c.OpenPolicyAgentMaxMemoryBodyParsing, PassiveHealthCheck: c.PassiveHealthCheck.values, + // 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/config/config_test.go b/config/config_test.go index bad7f480ec..4b5e2baafd 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -164,6 +164,8 @@ func defaultConfig(with func(*Config)) *Config { OpenPolicyAgentMaxRequestBodySize: openpolicyagent.DefaultMaxRequestBodySize, OpenPolicyAgentMaxMemoryBodyParsing: openpolicyagent.DefaultMaxMemoryBodyParsing, OpenPolicyAgentRequestBodyBufferSize: openpolicyagent.DefaultRequestBodyBufferSize, + ValidationWebhookAddr: ":9443", + ValidationWebhookLogLevel: "info", } with(cfg) return cfg diff --git a/dataclients/kubernetes/clusterclient.go b/dataclients/kubernetes/clusterclient.go index dbd64e00a7..1fba168ea7 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 53de8b485a..3b62d6237b 100644 --- a/dataclients/kubernetes/definitions/ingressvalidator.go +++ b/dataclients/kubernetes/definitions/ingressvalidator.go @@ -5,9 +5,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 @@ -20,14 +23,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 { @@ -42,12 +50,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 a42baa8d9e..05de3c6189 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 errors.Join(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 523b71fdd8..fbf5f358a2 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 550d020bb0..608cb54e3b 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) @@ -27,9 +27,6 @@ skipper: eskip: GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOARM) CGO_ENABLED=$(CGO_ENABLED) go build $(BUILD_FLAGS) -o eskip ../cmd/eskip/*.go -webhook: - GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOARM) CGO_ENABLED=$(CGO_ENABLED) go build $(BUILD_FLAGS) -o webhook ../cmd/webhook/*.go - routesrv: GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOARM) CGO_ENABLED=$(CGO_ENABLED) go build $(BUILD_FLAGS) -o routesrv ../cmd/routesrv/*.go diff --git a/skipper.go b/skipper.go index 19e24f027f..36798fe17a 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 ( @@ -947,6 +948,21 @@ type Options struct { OpenPolicyAgentMaxMemoryBodyParsing int64 PassiveHealthCheck map[string]string + // 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 { @@ -2058,6 +2074,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 118c5ba380..ae6baca231 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 4: 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()