From 679e06467f736246dd8ef431346a7938e42eee63 Mon Sep 17 00:00:00 2001 From: Mustafa Saber Date: Thu, 10 Aug 2023 15:35:36 +0300 Subject: [PATCH] webhook: validate skipper annotations in ingress (#2493) Introduce skipper ingress annotations eskip validation to `AdmissionValidationWebhook` and our webhook binary Signed-off-by: Mustafa Abdelrahman --- cmd/webhook/admission/admission.go | 42 ---------- cmd/webhook/admission/admission_test.go | 76 +++++++++++++++++-- cmd/webhook/admission/ingress.go | 34 +++++++++ cmd/webhook/admission/routegroup.go | 34 +++++++++ .../invalid-filters-and-predicates.json | 40 ++++++++++ .../testdata/ingress/invalid-filters.json | 39 ++++++++++ .../testdata/ingress/invalid-predicates.json | 39 ++++++++++ .../testdata/ingress/invalid-routes.json | 39 ++++++++++ .../valid-ingress-with-annotations.json | 41 ++++++++++ .../valid-ingress-without-annotations.json | 36 +++++++++ .../testdata/{ => rg}/invalid-rg.json | 0 ...-invalid-eskip-filters-and-predicates.json | 0 .../rg-with-invalid-eskip-filters.json | 0 .../rg-with-invalid-eskip-predicates.json | 0 .../{ => rg}/rg-with-valid-eskip-filters.json | 0 .../rg-with-valid-eskip-predicates.json | 0 .../admission/testdata/{ => rg}/valid-rg.json | 0 cmd/webhook/main.go | 5 +- dataclients/kubernetes/clusterclient.go | 4 +- .../kubernetes/definitions/ingressv1.go | 55 +++++--------- .../definitions/ingressvalidator.go | 52 +++++++++++++ dataclients/kubernetes/ingress.go | 11 +-- dataclients/kubernetes/kube_test.go | 6 +- dataclients/kubernetes/path_test.go | 2 +- dataclients/kubernetes/redirect.go | 2 +- 25 files changed, 459 insertions(+), 98 deletions(-) create mode 100644 cmd/webhook/admission/ingress.go create mode 100644 cmd/webhook/admission/routegroup.go create mode 100644 cmd/webhook/admission/testdata/ingress/invalid-filters-and-predicates.json create mode 100644 cmd/webhook/admission/testdata/ingress/invalid-filters.json create mode 100644 cmd/webhook/admission/testdata/ingress/invalid-predicates.json create mode 100644 cmd/webhook/admission/testdata/ingress/invalid-routes.json create mode 100644 cmd/webhook/admission/testdata/ingress/valid-ingress-with-annotations.json create mode 100644 cmd/webhook/admission/testdata/ingress/valid-ingress-without-annotations.json rename cmd/webhook/admission/testdata/{ => rg}/invalid-rg.json (100%) rename cmd/webhook/admission/testdata/{ => rg}/rg-with-invalid-eskip-filters-and-predicates.json (100%) rename cmd/webhook/admission/testdata/{ => rg}/rg-with-invalid-eskip-filters.json (100%) rename cmd/webhook/admission/testdata/{ => rg}/rg-with-invalid-eskip-predicates.json (100%) rename cmd/webhook/admission/testdata/{ => rg}/rg-with-valid-eskip-filters.json (100%) rename cmd/webhook/admission/testdata/{ => rg}/rg-with-valid-eskip-predicates.json (100%) rename cmd/webhook/admission/testdata/{ => rg}/valid-rg.json (100%) create mode 100644 dataclients/kubernetes/definitions/ingressvalidator.go diff --git a/cmd/webhook/admission/admission.go b/cmd/webhook/admission/admission.go index a13dfe3437..a83b0d7c2c 100644 --- a/cmd/webhook/admission/admission.go +++ b/cmd/webhook/admission/admission.go @@ -9,7 +9,6 @@ import ( "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" - "github.com/zalando/skipper/dataclients/kubernetes/definitions" ) const ( @@ -58,51 +57,10 @@ type admitter interface { admit(req *admissionRequest) (*admissionResponse, error) } -type RouteGroupAdmitter struct { -} - func init() { prometheus.MustRegister(totalRequests, invalidRequests, rejectedRequests, approvedRequests, admissionDuration) } -func (RouteGroupAdmitter) name() string { - return "routegroup" -} - -func (RouteGroupAdmitter) admit(req *admissionRequest) (*admissionResponse, error) { - rgItem := definitions.RouteGroupItem{} - err := json.Unmarshal(req.Object, &rgItem) - if err != nil { - emsg := fmt.Sprintf("could not parse RouteGroup, %v", err) - log.Error(emsg) - return &admissionResponse{ - UID: req.UID, - Allowed: false, - Result: &status{ - Message: emsg, - }, - }, nil - } - - err = definitions.ValidateRouteGroup(&rgItem) - if err != nil { - emsg := fmt.Sprintf("could not validate RouteGroup, %v", err) - log.Error(emsg) - return &admissionResponse{ - UID: req.UID, - Allowed: false, - Result: &status{ - Message: emsg, - }, - }, nil - } - - return &admissionResponse{ - UID: req.UID, - Allowed: true, - }, nil -} - func Handler(admitter admitter) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { admitterName := admitter.name() diff --git a/cmd/webhook/admission/admission_test.go b/cmd/webhook/admission/admission_test.go index b32b315f48..e00dde1cf1 100644 --- a/cmd/webhook/admission/admission_test.go +++ b/cmd/webhook/admission/admission_test.go @@ -96,7 +96,7 @@ func TestRouteGroupAdmitter(t *testing.T) { { name: "not allowed", inputFile: "invalid-rg.json", - message: "could not validate RouteGroup, error in route group n1/rg1: route group without spec", + message: "error in route group n1/rg1: route group without spec", }, { name: "valid eskip filters", @@ -105,7 +105,7 @@ func TestRouteGroupAdmitter(t *testing.T) { { name: "invalid eskip filters", inputFile: "rg-with-invalid-eskip-filters.json", - message: "could not validate RouteGroup, parse failed after token status, last route id: , position 11: syntax error", + message: "parse failed after token status, last route id: , position 11: syntax error", }, { name: "valid eskip predicates", @@ -114,12 +114,12 @@ func TestRouteGroupAdmitter(t *testing.T) { { name: "invalid eskip predicates", inputFile: "rg-with-invalid-eskip-predicates.json", - message: "could not validate RouteGroup, parse failed after token Method, last route id: Method, position 6: syntax error", + message: "parse failed after token Method, last route id: Method, position 6: syntax error", }, { name: "invalid eskip filters and predicates", inputFile: "rg-with-invalid-eskip-filters-and-predicates.json", - message: "could not validate RouteGroup, parse failed after token status, last route id: , position 11: syntax error\\nparse failed after token Method, last route id: Method, position 6: syntax error", + message: "parse failed after token status, last route id: , position 11: syntax error\\nparse failed after token Method, last route id: Method, position 6: syntax error", }, } { t.Run(tc.name, func(t *testing.T) { @@ -128,14 +128,14 @@ func TestRouteGroupAdmitter(t *testing.T) { expectedResponse = fmt.Sprintf(responseNotAllowedFmt, tc.message) } - input, err := os.ReadFile("testdata/" + tc.inputFile) + input, err := os.ReadFile("testdata/rg/" + tc.inputFile) require.NoError(t, err) req := httptest.NewRequest("POST", "http://example.com/foo", bytes.NewBuffer(input)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - rgAdm := RouteGroupAdmitter{} + rgAdm := &RouteGroupAdmitter{} h := Handler(rgAdm) h(w, req) @@ -150,3 +150,67 @@ func TestRouteGroupAdmitter(t *testing.T) { }) } } + +func TestIngressAdmitter(t *testing.T) { + for _, tc := range []struct { + name string + inputFile string + message string + }{ + { + name: "allowed without annotations", + inputFile: "valid-ingress-without-annotations.json", + }, + { + name: "allowed with valid annotations", + inputFile: "valid-ingress-with-annotations.json", + }, + { + name: "invalid eskip filters", + inputFile: "invalid-filters.json", + message: `invalid \"zalando.org/skipper-filter\" annotation: parse failed after token this, last route id: , position 9: syntax error`, + }, + { + name: "invalid eskip predicates", + inputFile: "invalid-predicates.json", + message: `invalid \"zalando.org/skipper-predicate\" annotation: parse failed after token ), last route id: , position 15: syntax error`, + }, + { + name: "invalid eskip routes", + inputFile: "invalid-routes.json", + message: `invalid \"zalando.org/skipper-routes\" annotation: invalid predicate count arg`, + }, + { + name: "invalid eskip filters and predicates", + inputFile: "invalid-filters-and-predicates.json", + message: `invalid \"zalando.org/skipper-filter\" annotation: parse failed after token this, last route id: , position 9: syntax error\ninvalid \"zalando.org/skipper-predicate\" annotation: parse failed after token ), last route id: , position 15: syntax error`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + expectedResponse := responseAllowedFmt + if len(tc.message) > 0 { + expectedResponse = fmt.Sprintf(responseNotAllowedFmt, tc.message) + } + + input, err := os.ReadFile("testdata/ingress/" + tc.inputFile) + require.NoError(t, err) + + req := httptest.NewRequest("POST", "http://example.com/foo", bytes.NewBuffer(input)) + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + ingressAdm := &IngressAdmitter{} + + h := Handler(ingressAdm) + h(w, req) + resp := w.Result() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + rb, err := io.ReadAll(resp.Body) + require.NoError(t, err) + resp.Body.Close() + + assert.JSONEq(t, expectedResponse, string(rb)) + }) + } +} diff --git a/cmd/webhook/admission/ingress.go b/cmd/webhook/admission/ingress.go new file mode 100644 index 0000000000..f4313da106 --- /dev/null +++ b/cmd/webhook/admission/ingress.go @@ -0,0 +1,34 @@ +package admission + +import ( + "encoding/json" + + "github.com/zalando/skipper/dataclients/kubernetes/definitions" +) + +type IngressAdmitter struct { + IngressValidator *definitions.IngressV1Validator +} + +func (iga *IngressAdmitter) name() string { + return "ingress" +} + +func (iga *IngressAdmitter) admit(req *admissionRequest) (*admissionResponse, error) { + + ingressItem := definitions.IngressV1Item{} + err := json.Unmarshal(req.Object, &ingressItem) + if err != nil { + return nil, err + } + + err = iga.IngressValidator.Validate(&ingressItem) + if err != nil { + return nil, err + } + + return &admissionResponse{ + UID: req.UID, + Allowed: true, + }, nil +} diff --git a/cmd/webhook/admission/routegroup.go b/cmd/webhook/admission/routegroup.go new file mode 100644 index 0000000000..42c70810d7 --- /dev/null +++ b/cmd/webhook/admission/routegroup.go @@ -0,0 +1,34 @@ +package admission + +import ( + "encoding/json" + + "github.com/zalando/skipper/dataclients/kubernetes/definitions" +) + +type RouteGroupAdmitter struct { + RouteGroupValidator *definitions.RouteGroupValidator +} + +func (rga *RouteGroupAdmitter) name() string { + return "routegroup" +} + +func (rga *RouteGroupAdmitter) admit(req *admissionRequest) (*admissionResponse, error) { + + rgItem := definitions.RouteGroupItem{} + err := json.Unmarshal(req.Object, &rgItem) + if err != nil { + return nil, err + } + + err = rga.RouteGroupValidator.Validate(&rgItem) + if err != nil { + return nil, err + } + + return &admissionResponse{ + UID: req.UID, + Allowed: true, + }, nil +} diff --git a/cmd/webhook/admission/testdata/ingress/invalid-filters-and-predicates.json b/cmd/webhook/admission/testdata/ingress/invalid-filters-and-predicates.json new file mode 100644 index 0000000000..e5aec6dd57 --- /dev/null +++ b/cmd/webhook/admission/testdata/ingress/invalid-filters-and-predicates.json @@ -0,0 +1,40 @@ +{ + "request": { + "uid": "req-uid", + "name": "req1", + "namespace": "n1", + "object": { + "metadata": { + "name": "ing1", + "namespace": "ing1", + "annotations": { + "zalando.org/skipper-filter": "this-is-invalid(10) -> inlineContent(\"This should work\")", + "zalando.org/skipper-predicate": "Header(\"test\") & Path(\"/login\")" + } + }, + "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.json b/cmd/webhook/admission/testdata/ingress/invalid-filters.json new file mode 100644 index 0000000000..d9f3d5ed92 --- /dev/null +++ b/cmd/webhook/admission/testdata/ingress/invalid-filters.json @@ -0,0 +1,39 @@ +{ + "request": { + "uid": "req-uid", + "name": "req1", + "namespace": "n1", + "object": { + "metadata": { + "name": "ing1", + "namespace": "ing1", + "annotations": { + "zalando.org/skipper-filter": "this-is-invalid(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-predicates.json b/cmd/webhook/admission/testdata/ingress/invalid-predicates.json new file mode 100644 index 0000000000..2b7b37bfc1 --- /dev/null +++ b/cmd/webhook/admission/testdata/ingress/invalid-predicates.json @@ -0,0 +1,39 @@ +{ + "request": { + "uid": "req-uid", + "name": "req1", + "namespace": "n1", + "object": { + "metadata": { + "name": "ing1", + "namespace": "ing1", + "annotations": { + "zalando.org/skipper-predicate": "Header(\"test\") & Path(\"/login\")" + } + }, + "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-routes.json b/cmd/webhook/admission/testdata/ingress/invalid-routes.json new file mode 100644 index 0000000000..348fdaa469 --- /dev/null +++ b/cmd/webhook/admission/testdata/ingress/invalid-routes.json @@ -0,0 +1,39 @@ +{ + "request": { + "uid": "req-uid", + "name": "req1", + "namespace": "n1", + "object": { + "metadata": { + "name": "ing1", + "namespace": "ing1", + "annotations": { + "zalando.org/skipper-routes": "r1: Header(\"test\") && Path(\"/login\") -> status(200) -> \"http://foo.test\"" + } + }, + "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/valid-ingress-with-annotations.json b/cmd/webhook/admission/testdata/ingress/valid-ingress-with-annotations.json new file mode 100644 index 0000000000..7fe8aa0e56 --- /dev/null +++ b/cmd/webhook/admission/testdata/ingress/valid-ingress-with-annotations.json @@ -0,0 +1,41 @@ +{ + "request": { + "uid": "req-uid", + "name": "req1", + "namespace": "n1", + "object": { + "metadata": { + "name": "ing1", + "namespace": "ing1", + "annotations": { + "zalando.org/skipper-filter": "status(200) -> inlineContent(\"This should work\")", + "zalando.org/skipper-predicate": "Header(\"test\", \"test\") && Path(\"/login\")", + "zalando.org/skipper-routes": "r1: Header(\"test2\", \"test2\") && Path(\"/login\") -> status(200) -> \"http://foo.test\"" + } + }, + "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/valid-ingress-without-annotations.json b/cmd/webhook/admission/testdata/ingress/valid-ingress-without-annotations.json new file mode 100644 index 0000000000..e87b4420cb --- /dev/null +++ b/cmd/webhook/admission/testdata/ingress/valid-ingress-without-annotations.json @@ -0,0 +1,36 @@ +{ + "request": { + "uid": "req-uid", + "name": "req1", + "namespace": "n1", + "object": { + "metadata": { + "name": "ing1", + "namespace": "ing1" + }, + "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/invalid-rg.json b/cmd/webhook/admission/testdata/rg/invalid-rg.json similarity index 100% rename from cmd/webhook/admission/testdata/invalid-rg.json rename to cmd/webhook/admission/testdata/rg/invalid-rg.json diff --git a/cmd/webhook/admission/testdata/rg-with-invalid-eskip-filters-and-predicates.json b/cmd/webhook/admission/testdata/rg/rg-with-invalid-eskip-filters-and-predicates.json similarity index 100% rename from cmd/webhook/admission/testdata/rg-with-invalid-eskip-filters-and-predicates.json rename to cmd/webhook/admission/testdata/rg/rg-with-invalid-eskip-filters-and-predicates.json diff --git a/cmd/webhook/admission/testdata/rg-with-invalid-eskip-filters.json b/cmd/webhook/admission/testdata/rg/rg-with-invalid-eskip-filters.json similarity index 100% rename from cmd/webhook/admission/testdata/rg-with-invalid-eskip-filters.json rename to cmd/webhook/admission/testdata/rg/rg-with-invalid-eskip-filters.json diff --git a/cmd/webhook/admission/testdata/rg-with-invalid-eskip-predicates.json b/cmd/webhook/admission/testdata/rg/rg-with-invalid-eskip-predicates.json similarity index 100% rename from cmd/webhook/admission/testdata/rg-with-invalid-eskip-predicates.json rename to cmd/webhook/admission/testdata/rg/rg-with-invalid-eskip-predicates.json diff --git a/cmd/webhook/admission/testdata/rg-with-valid-eskip-filters.json b/cmd/webhook/admission/testdata/rg/rg-with-valid-eskip-filters.json similarity index 100% rename from cmd/webhook/admission/testdata/rg-with-valid-eskip-filters.json rename to cmd/webhook/admission/testdata/rg/rg-with-valid-eskip-filters.json diff --git a/cmd/webhook/admission/testdata/rg-with-valid-eskip-predicates.json b/cmd/webhook/admission/testdata/rg/rg-with-valid-eskip-predicates.json similarity index 100% rename from cmd/webhook/admission/testdata/rg-with-valid-eskip-predicates.json rename to cmd/webhook/admission/testdata/rg/rg-with-valid-eskip-predicates.json diff --git a/cmd/webhook/admission/testdata/valid-rg.json b/cmd/webhook/admission/testdata/rg/valid-rg.json similarity index 100% rename from cmd/webhook/admission/testdata/valid-rg.json rename to cmd/webhook/admission/testdata/rg/valid-rg.json diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index e250318bff..82a67b303f 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -12,6 +12,7 @@ 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" ) const ( @@ -52,9 +53,11 @@ func main() { var cfg = &config{} cfg.parse() - rgAdmitter := admission.RouteGroupAdmitter{} + rgAdmitter := &admission.RouteGroupAdmitter{RouteGroupValidator: &definitions.RouteGroupValidator{}} + ingressAdmitter := &admission.IngressAdmitter{IngressValidator: &definitions.IngressV1Validator{}} handler := http.NewServeMux() handler.Handle("/routegroups", admission.Handler(rgAdmitter)) + handler.Handle("/ingresses", admission.Handler(ingressAdmitter)) handler.Handle("/metrics", promhttp.Handler()) handler.HandleFunc("/healthz", healthCheck) diff --git a/dataclients/kubernetes/clusterclient.go b/dataclients/kubernetes/clusterclient.go index 1b16fa4a3b..e55aae2cf5 100644 --- a/dataclients/kubernetes/clusterclient.go +++ b/dataclients/kubernetes/clusterclient.go @@ -71,6 +71,7 @@ type clusterClient struct { routeGroupsLabelSelectors string loggedMissingRouteGroups bool + routeGroupValidator *definitions.RouteGroupValidator } var ( @@ -162,6 +163,7 @@ func newClusterClient(o Options, apiURL, ingCls, rgCls string, quit <-chan struc httpClient: httpClient, apiURL: apiURL, certificateRegistry: o.CertificateRegistry, + routeGroupValidator: &definitions.RouteGroupValidator{}, } if o.KubernetesInCluster { @@ -361,7 +363,7 @@ func (c *clusterClient) LoadRouteGroups() ([]*definitions.RouteGroupItem, error) rgs := make([]*definitions.RouteGroupItem, 0, len(rgl.Items)) for _, i := range rgl.Items { // Validate RouteGroup item. - if err := definitions.ValidateRouteGroup(i); err != nil { + if err := c.routeGroupValidator.Validate(i); err != nil { log.Errorf("[routegroup] %v", err) continue } diff --git a/dataclients/kubernetes/definitions/ingressv1.go b/dataclients/kubernetes/definitions/ingressv1.go index 7c92974606..ac5e6975f8 100644 --- a/dataclients/kubernetes/definitions/ingressv1.go +++ b/dataclients/kubernetes/definitions/ingressv1.go @@ -3,10 +3,17 @@ package definitions import ( "encoding/json" "errors" - "fmt" "strconv" ) +const ( + IngressFilterAnnotation = "zalando.org/skipper-filter" + IngressPredicateAnnotation = "zalando.org/skipper-predicate" + IngressRoutesAnnotation = "zalando.org/skipper-routes" +) + +var errInvalidPortType = errors.New("invalid port type") + type IngressV1List struct { Items []*IngressV1Item `json:"items"` } @@ -69,6 +76,17 @@ type TLSV1 struct { SecretName string `json:"secretName"` } +// ResourceID is a stripped down version of Metadata used to identify resources in a cache map +type ResourceID struct { + Namespace string + Name string +} + +// BackendPort is used for TargetPort similar to Kubernetes intOrString type +type BackendPort struct { + Value interface{} +} + // ParseIngressV1JSON parse JSON into an IngressV1List func ParseIngressV1JSON(d []byte) (IngressV1List, error) { var il IngressV1List @@ -76,26 +94,6 @@ func ParseIngressV1JSON(d []byte) (IngressV1List, error) { return il, err } -// TODO: implement once IngressItem has a validate method -// ValidateIngressV1 is a no-op -func ValidateIngressV1(_ *IngressV1Item) error { - return nil -} - -func ValidateIngressesV1(ingressList IngressV1List) error { - var errs []error - // discover all errors to avoid the user having to repeatedly validate - for _, i := range ingressList.Items { - nerr := ValidateIngressV1(i) - if nerr != nil { - name := i.Metadata.Name - namespace := i.Metadata.Namespace - errs = append(errs, fmt.Errorf("%s/%s: %w", name, namespace, nerr)) - } - } - return errorsJoin(errs...) -} - func GetHostsFromIngressRulesV1(ing *IngressV1Item) []string { hostList := make([]string, 0) for _, i := range ing.Spec.Rules { @@ -104,19 +102,6 @@ func GetHostsFromIngressRulesV1(ing *IngressV1Item) []string { return hostList } -// ResourceID is a stripped down version of Metadata used to identify resources in a cache map -type ResourceID struct { - Namespace string - Name string -} - -/* required from v1beta1 */ - -// BackendPort is used for TargetPort similar to Kubernetes intOrString type -type BackendPort struct { - Value interface{} -} - // String converts BackendPort to string func (p BackendPort) String() string { switch v := p.Value.(type) { @@ -162,5 +147,3 @@ func (p BackendPort) MarshalJSON() ([]byte, error) { return nil, errInvalidPortType } } - -var errInvalidPortType = errors.New("invalid port type") diff --git a/dataclients/kubernetes/definitions/ingressvalidator.go b/dataclients/kubernetes/definitions/ingressvalidator.go new file mode 100644 index 0000000000..1a7e2499d8 --- /dev/null +++ b/dataclients/kubernetes/definitions/ingressvalidator.go @@ -0,0 +1,52 @@ +package definitions + +import ( + "fmt" + + "github.com/zalando/skipper/eskip" +) + +type IngressV1Validator struct{} + +func (igv *IngressV1Validator) Validate(item *IngressV1Item) error { + var errs []error + + errs = append(errs, igv.validateFilterAnnotation(item.Metadata.Annotations)) + errs = append(errs, igv.validatePredicateAnnotation(item.Metadata.Annotations)) + errs = append(errs, igv.validateRoutesAnnotation(item.Metadata.Annotations)) + + return errorsJoin(errs...) +} + +func (igv *IngressV1Validator) validateFilterAnnotation(annotations map[string]string) error { + if filters, ok := annotations[IngressFilterAnnotation]; ok { + _, err := eskip.ParseFilters(filters) + if err != nil { + err = fmt.Errorf("invalid \"%s\" annotation: %w", IngressFilterAnnotation, err) + } + return err + } + return nil +} + +func (igv *IngressV1Validator) validatePredicateAnnotation(annotations map[string]string) error { + if predicates, ok := annotations[IngressPredicateAnnotation]; ok { + _, err := eskip.ParsePredicates(predicates) + if err != nil { + err = fmt.Errorf("invalid \"%s\" annotation: %w", IngressPredicateAnnotation, err) + } + return err + } + return nil +} + +func (igv *IngressV1Validator) validateRoutesAnnotation(annotations map[string]string) error { + if routes, ok := annotations[IngressRoutesAnnotation]; ok { + _, err := eskip.Parse(routes) + if err != nil { + err = fmt.Errorf("invalid \"%s\" annotation: %w", IngressRoutesAnnotation, err) + } + return err + } + return nil +} diff --git a/dataclients/kubernetes/ingress.go b/dataclients/kubernetes/ingress.go index 0a385de782..aaec03360b 100644 --- a/dataclients/kubernetes/ingress.go +++ b/dataclients/kubernetes/ingress.go @@ -20,9 +20,6 @@ const ( ingressRouteIDPrefix = "kube" backendWeightsAnnotationKey = "zalando.org/backend-weights" ratelimitAnnotationKey = "zalando.org/ratelimit" - skipperfilterAnnotationKey = "zalando.org/skipper-filter" - skipperpredicateAnnotationKey = "zalando.org/skipper-predicate" - skipperRoutesAnnotationKey = "zalando.org/skipper-routes" skipperLoadBalancerAnnotationKey = "zalando.org/skipper-loadbalancer" skipperBackendProtocolAnnotationKey = "zalando.org/skipper-backend-protocol" pathModeAnnotationKey = "zalando.org/skipper-ingress-path-mode" @@ -229,7 +226,7 @@ func annotationFilter(m *definitions.Metadata, logger *logger) []*eskip.Filter { if ratelimitAnnotationValue, ok := m.Annotations[ratelimitAnnotationKey]; ok { annotationFilter = ratelimitAnnotationValue } - if val, ok := m.Annotations[skipperfilterAnnotationKey]; ok { + if val, ok := m.Annotations[definitions.IngressFilterAnnotation]; ok { if annotationFilter != "" { annotationFilter += " -> " } @@ -249,7 +246,7 @@ func annotationFilter(m *definitions.Metadata, logger *logger) []*eskip.Filter { // parse predicate annotation func annotationPredicate(m *definitions.Metadata) string { var annotationPredicate string - if val, ok := m.Annotations[skipperpredicateAnnotationKey]; ok { + if val, ok := m.Annotations[definitions.IngressPredicateAnnotation]; ok { annotationPredicate = val } return annotationPredicate @@ -258,12 +255,12 @@ func annotationPredicate(m *definitions.Metadata) string { // parse routes annotation func extraRoutes(m *definitions.Metadata, logger *logger) []*eskip.Route { var extraRoutes []*eskip.Route - annotationRoutes := m.Annotations[skipperRoutesAnnotationKey] + annotationRoutes := m.Annotations[definitions.IngressRoutesAnnotation] if annotationRoutes != "" { var err error extraRoutes, err = eskip.Parse(annotationRoutes) if err != nil { - logger.Errorf("Failed to parse routes from %s, skipping: %v", skipperRoutesAnnotationKey, err) + logger.Errorf("Failed to parse routes from %s, skipping: %v", definitions.IngressRoutesAnnotation, err) } } return extraRoutes diff --git a/dataclients/kubernetes/kube_test.go b/dataclients/kubernetes/kube_test.go index 28bcd23349..a5e958e167 100644 --- a/dataclients/kubernetes/kube_test.go +++ b/dataclients/kubernetes/kube_test.go @@ -201,13 +201,13 @@ func testIngress(ns, name, defaultService, ratelimitCfg, filterString, predicate setAnnotation(i, ratelimitAnnotationKey, ratelimitCfg) } if filterString != "" { - setAnnotation(i, skipperfilterAnnotationKey, filterString) + setAnnotation(i, definitions.IngressFilterAnnotation, filterString) } if predicateString != "" { - setAnnotation(i, skipperpredicateAnnotationKey, predicateString) + setAnnotation(i, definitions.IngressPredicateAnnotation, predicateString) } if routesString != "" { - setAnnotation(i, skipperRoutesAnnotationKey, routesString) + setAnnotation(i, definitions.IngressRoutesAnnotation, routesString) } if pathModeString != "" { setAnnotation(i, pathModeAnnotationKey, pathModeString) diff --git a/dataclients/kubernetes/path_test.go b/dataclients/kubernetes/path_test.go index 85287d7264..6f74c9ccf3 100644 --- a/dataclients/kubernetes/path_test.go +++ b/dataclients/kubernetes/path_test.go @@ -86,7 +86,7 @@ func TestPathMatchingModes(t *testing.T) { annotation := strings.Join(annotations, " && ") if len(annotations) > 0 { - i.Metadata.Annotations[skipperpredicateAnnotationKey] = annotation + i.Metadata.Annotations[definitions.IngressPredicateAnnotation] = annotation } api.ingresses.Items = []*definitions.IngressV1Item{i} diff --git a/dataclients/kubernetes/redirect.go b/dataclients/kubernetes/redirect.go index d574477e20..227e8f1d81 100644 --- a/dataclients/kubernetes/redirect.go +++ b/dataclients/kubernetes/redirect.go @@ -37,7 +37,7 @@ func createRedirectInfo(defaultEnabled bool, defaultCode int) *redirectInfo { func (r *redirectInfo) initCurrent(m *definitions.Metadata) { r.enable = m.Annotations[redirectAnnotationKey] == "true" r.disable = m.Annotations[redirectAnnotationKey] == "false" - r.ignore = strings.Contains(m.Annotations[skipperpredicateAnnotationKey], `Header("X-Forwarded-Proto"`) || strings.Contains(m.Annotations[skipperRoutesAnnotationKey], `Header("X-Forwarded-Proto"`) + r.ignore = strings.Contains(m.Annotations[definitions.IngressPredicateAnnotation], `Header("X-Forwarded-Proto"`) || strings.Contains(m.Annotations[definitions.IngressRoutesAnnotation], `Header("X-Forwarded-Proto"`) r.code = r.defaultCode if annotationCode, ok := m.Annotations[redirectCodeAnnotationKey]; ok {