From 7146fe841f3c2aa97dbbd2344fdb34a76ae81335 Mon Sep 17 00:00:00 2001 From: sh2 Date: Wed, 12 Feb 2025 20:58:41 +0800 Subject: [PATCH] feat: add defaulter for gateway-api resources loading from file (#5232) * add defaulter that can set default values for our crd Signed-off-by: shawnh2 * resolve conflicts Signed-off-by: shawnh2 * add gateway schema defaulter while loading resources and fix all existing tests Signed-off-by: shawnh2 * add load test for all supported kind resources Signed-off-by: shawnh2 * fix lint Signed-off-by: shawnh2 --------- Signed-off-by: shawnh2 Co-authored-by: zirain --- go.mod | 10 +- go.sum | 15 + .../translate/out/backend-endpoint.all.yaml | 12 +- .../translate/out/default-resources.all.yaml | 50 ++- .../out/echo-gateway-api.cluster.yaml | 11 +- .../translate/out/echo-gateway-api.route.json | 11 +- .../translate/out/invalid-envoyproxy.all.yaml | 54 ++- .../translate/out/quickstart.all.yaml | 11 +- .../out/rejected-http-route.route.yaml | 11 +- .../translate/out/valid-envoyproxy.all.yaml | 56 ++- internal/gatewayapi/resource/defaulter.go | 270 +++++++++++++ .../gatewayapi/resource/defaulter_test.go | 157 ++++++++ internal/gatewayapi/resource/load.go | 13 +- internal/gatewayapi/resource/load_test.go | 43 +++ .../resource/testdata/all-resources.in.yaml | 231 +++++++++++ .../resource/testdata/all-resources.out.yaml | 360 ++++++++++++++++++ .../resource/testdata/schema/crd.yaml | 79 ++++ .../provider/file/testdata/resources.all.yaml | 10 +- release-notes/current.yaml | 1 + tools/make/golang.mk | 1 + 20 files changed, 1363 insertions(+), 43 deletions(-) create mode 100644 internal/gatewayapi/resource/defaulter.go create mode 100644 internal/gatewayapi/resource/defaulter_test.go create mode 100644 internal/gatewayapi/resource/testdata/all-resources.in.yaml create mode 100644 internal/gatewayapi/resource/testdata/all-resources.out.yaml create mode 100644 internal/gatewayapi/resource/testdata/schema/crd.yaml diff --git a/go.mod b/go.mod index 685f17f014b..f2ec7ba1d44 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,9 @@ require ( github.com/go-logfmt/logfmt v0.6.0 github.com/go-logr/logr v1.4.2 github.com/go-logr/zapr v1.3.0 + github.com/go-openapi/spec v0.21.0 + github.com/go-openapi/strfmt v0.23.0 + github.com/go-openapi/validate v0.24.0 github.com/golang/protobuf v1.5.4 github.com/google/cel-go v0.22.1 github.com/google/go-cmp v0.6.0 @@ -69,6 +72,7 @@ require ( k8s.io/cli-runtime v0.32.1 k8s.io/client-go v0.32.1 k8s.io/klog/v2 v2.130.1 + k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 k8s.io/kubectl v0.32.1 k8s.io/utils v0.0.0-20241210054802-24370beab758 sigs.k8s.io/controller-runtime v0.20.1 @@ -139,8 +143,11 @@ require ( github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/analysis v0.23.0 // indirect + github.com/go-openapi/errors v0.22.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/loads v0.22.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-redis/redis/v7 v7.4.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect @@ -211,6 +218,7 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runtime-spec v1.2.0 // indirect @@ -257,6 +265,7 @@ require ( go.etcd.io/etcd/api/v3 v3.5.16 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect go.etcd.io/etcd/client/v3 v3.5.16 // indirect + go.mongodb.org/mongo-driver v1.14.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect @@ -282,7 +291,6 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/apiserver v0.32.1 // indirect k8s.io/component-base v0.32.1 // indirect - k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect k8s.io/metrics v0.32.1 // indirect oras.land/oras-go v1.2.6 // indirect periph.io/x/host/v3 v3.8.2 // indirect diff --git a/go.sum b/go.sum index 08d5c65526f..673998ea42a 100644 --- a/go.sum +++ b/go.sum @@ -293,9 +293,13 @@ github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpR github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= @@ -315,6 +319,8 @@ github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= +github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= +github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= @@ -323,10 +329,14 @@ github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsd github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= @@ -337,6 +347,8 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= +github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= @@ -634,6 +646,7 @@ github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/ohler55/ojg v1.26.1 h1:J5TaLmVEuvnpVH7JMdT1QdbpJU545Yp6cKiCO4aQILc= github.com/ohler55/ojg v1.26.1/go.mod h1:gQhDVpQLqrmnd2eqGAvJtn+NfKoYJbe/A4Sj3/Vro4o= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -890,6 +903,8 @@ go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCq go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= diff --git a/internal/cmd/egctl/testdata/translate/out/backend-endpoint.all.yaml b/internal/cmd/egctl/testdata/translate/out/backend-endpoint.all.yaml index d3f3ed2c771..0841f848773 100644 --- a/internal/cmd/egctl/testdata/translate/out/backend-endpoint.all.yaml +++ b/internal/cmd/egctl/testdata/translate/out/backend-endpoint.all.yaml @@ -40,7 +40,10 @@ gateways: spec: gatewayClassName: eg listeners: - - name: http + - allowedRoutes: + namespaces: + from: Same + name: http port: 80 protocol: HTTP status: @@ -78,12 +81,15 @@ httpRoutes: hostnames: - www.example.com parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg rules: - backendRefs: - group: gateway.envoyproxy.io kind: Backend name: backend + weight: 1 matches: - path: type: PathPrefix @@ -103,4 +109,6 @@ httpRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg diff --git a/internal/cmd/egctl/testdata/translate/out/default-resources.all.yaml b/internal/cmd/egctl/testdata/translate/out/default-resources.all.yaml index 0963e89f94a..d05242b7006 100644 --- a/internal/cmd/egctl/testdata/translate/out/default-resources.all.yaml +++ b/internal/cmd/egctl/testdata/translate/out/default-resources.all.yaml @@ -202,13 +202,22 @@ gateways: spec: gatewayClassName: eg listeners: - - name: tcp + - allowedRoutes: + namespaces: + from: Same + name: tcp port: 1234 protocol: TCP - - name: udp + - allowedRoutes: + namespaces: + from: Same + name: udp port: 1234 protocol: UDP - - hostname: foo.com + - allowedRoutes: + namespaces: + from: Same + hostname: foo.com name: tls-passthrough port: 8443 protocol: TLS @@ -218,6 +227,8 @@ gateways: kinds: - group: gateway.networking.k8s.io kind: HTTPRoute + namespaces: + from: Same name: http port: 80 protocol: HTTP @@ -225,6 +236,8 @@ gateways: kinds: - group: gateway.networking.k8s.io kind: GRPCRoute + namespaces: + from: Same name: grpc port: 8080 protocol: HTTP @@ -345,7 +358,9 @@ grpcRoutes: hostnames: - www.grpc-example.com parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg sectionName: grpc rules: - backendRefs: @@ -358,6 +373,7 @@ grpcRoutes: - method: method: DoThing service: com.example.Things + type: Exact status: parents: - conditions: @@ -373,6 +389,8 @@ grpcRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg sectionName: grpc httpRoutes: @@ -385,7 +403,9 @@ httpRoutes: hostnames: - www.example.com parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg rules: - backendRefs: - group: "" @@ -412,6 +432,8 @@ httpRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg tcpRoutes: - kind: TCPRoute @@ -421,7 +443,9 @@ tcpRoutes: namespace: default spec: parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg sectionName: tcp rules: - backendRefs: @@ -445,6 +469,8 @@ tcpRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg sectionName: tcp tlsRoutes: @@ -455,7 +481,9 @@ tlsRoutes: namespace: default spec: parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg sectionName: tls-passthrough rules: - backendRefs: @@ -479,6 +507,8 @@ tlsRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg sectionName: tls-passthrough udpRoutes: @@ -489,7 +519,9 @@ udpRoutes: namespace: default spec: parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg sectionName: udp rules: - backendRefs: @@ -513,6 +545,8 @@ udpRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg sectionName: udp xds: diff --git a/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.cluster.yaml b/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.cluster.yaml index de5d16949be..8d819d61200 100644 --- a/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.cluster.yaml +++ b/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.cluster.yaml @@ -22,7 +22,10 @@ gateways: spec: gatewayClassName: eg listeners: - - name: http + - allowedRoutes: + namespaces: + from: Same + name: http port: 80 protocol: HTTP status: @@ -60,7 +63,9 @@ httpRoutes: hostnames: - www.example.com parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg rules: - backendRefs: - group: "" @@ -87,6 +92,8 @@ httpRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg xds: envoy-gateway-system/eg: diff --git a/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.route.json b/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.route.json index f069c670afb..cb22c731eac 100644 --- a/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.route.json +++ b/internal/cmd/egctl/testdata/translate/out/echo-gateway-api.route.json @@ -35,7 +35,12 @@ { "name": "http", "port": 80, - "protocol": "HTTP" + "protocol": "HTTP", + "allowedRoutes": { + "namespaces": { + "from": "Same" + } + } } ] }, @@ -93,6 +98,8 @@ "spec": { "parentRefs": [ { + "group": "gateway.networking.k8s.io", + "kind": "Gateway", "name": "eg" } ], @@ -125,6 +132,8 @@ "parents": [ { "parentRef": { + "group": "gateway.networking.k8s.io", + "kind": "Gateway", "name": "eg" }, "controllerName": "gateway.envoyproxy.io/gatewayclass-controller", diff --git a/internal/cmd/egctl/testdata/translate/out/invalid-envoyproxy.all.yaml b/internal/cmd/egctl/testdata/translate/out/invalid-envoyproxy.all.yaml index bd4ac1d198d..b51220df310 100644 --- a/internal/cmd/egctl/testdata/translate/out/invalid-envoyproxy.all.yaml +++ b/internal/cmd/egctl/testdata/translate/out/invalid-envoyproxy.all.yaml @@ -18,7 +18,9 @@ envoyProxyForGatewayClass: socket_address: address: 127.0.0.1 port_value: 19000 - logging: {} + logging: + level: + default: warn status: {} gatewayClass: kind: GatewayClass @@ -49,13 +51,22 @@ gateways: spec: gatewayClassName: eg listeners: - - name: tcp + - allowedRoutes: + namespaces: + from: Same + name: tcp port: 1234 protocol: TCP - - name: udp + - allowedRoutes: + namespaces: + from: Same + name: udp port: 1234 protocol: UDP - - hostname: foo.com + - allowedRoutes: + namespaces: + from: Same + hostname: foo.com name: tls-passthrough port: 8443 protocol: TLS @@ -65,6 +76,8 @@ gateways: kinds: - group: gateway.networking.k8s.io kind: HTTPRoute + namespaces: + from: Same name: http port: 80 protocol: HTTP @@ -72,6 +85,8 @@ gateways: kinds: - group: gateway.networking.k8s.io kind: GRPCRoute + namespaces: + from: Same name: grpc port: 8080 protocol: HTTP @@ -192,7 +207,9 @@ grpcRoutes: hostnames: - www.grpc-example.com parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg sectionName: grpc rules: - backendRefs: @@ -205,6 +222,7 @@ grpcRoutes: - method: method: DoThing service: com.example.Things + type: Exact status: parents: - conditions: @@ -220,6 +238,8 @@ grpcRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg sectionName: grpc httpRoutes: @@ -232,7 +252,9 @@ httpRoutes: hostnames: - www.example.com parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg rules: - backendRefs: - group: "" @@ -259,6 +281,8 @@ httpRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg tcpRoutes: - kind: TCPRoute @@ -268,7 +292,9 @@ tcpRoutes: namespace: default spec: parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg sectionName: tcp rules: - backendRefs: @@ -292,6 +318,8 @@ tcpRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg sectionName: tcp tlsRoutes: @@ -302,7 +330,9 @@ tlsRoutes: namespace: default spec: parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg sectionName: tls-passthrough rules: - backendRefs: @@ -326,6 +356,8 @@ tlsRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg sectionName: tls-passthrough udpRoutes: @@ -336,7 +368,9 @@ udpRoutes: namespace: default spec: parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg sectionName: udp rules: - backendRefs: @@ -360,5 +394,7 @@ udpRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg sectionName: udp diff --git a/internal/cmd/egctl/testdata/translate/out/quickstart.all.yaml b/internal/cmd/egctl/testdata/translate/out/quickstart.all.yaml index 84a0ebc0f6d..696d00a2559 100644 --- a/internal/cmd/egctl/testdata/translate/out/quickstart.all.yaml +++ b/internal/cmd/egctl/testdata/translate/out/quickstart.all.yaml @@ -7,7 +7,10 @@ gateways: spec: gatewayClassName: eg listeners: - - name: http + - allowedRoutes: + namespaces: + from: Same + name: http port: 80 protocol: HTTP status: @@ -45,7 +48,9 @@ httpRoutes: hostnames: - www.example.com parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg rules: - backendRefs: - group: "" @@ -72,6 +77,8 @@ httpRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg infraIR: envoy-gateway-system/eg: diff --git a/internal/cmd/egctl/testdata/translate/out/rejected-http-route.route.yaml b/internal/cmd/egctl/testdata/translate/out/rejected-http-route.route.yaml index 18e5910acc2..cff3a5f3354 100644 --- a/internal/cmd/egctl/testdata/translate/out/rejected-http-route.route.yaml +++ b/internal/cmd/egctl/testdata/translate/out/rejected-http-route.route.yaml @@ -22,7 +22,10 @@ gateways: spec: gatewayClassName: eg listeners: - - name: tls + - allowedRoutes: + namespaces: + from: Same + name: tls port: 8443 protocol: TLS status: @@ -53,7 +56,9 @@ httpRoutes: namespace: envoy-gateway-system spec: parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg rules: - backendRefs: - group: "" @@ -80,4 +85,6 @@ httpRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg diff --git a/internal/cmd/egctl/testdata/translate/out/valid-envoyproxy.all.yaml b/internal/cmd/egctl/testdata/translate/out/valid-envoyproxy.all.yaml index fe1b452f291..7ba42aa75e0 100644 --- a/internal/cmd/egctl/testdata/translate/out/valid-envoyproxy.all.yaml +++ b/internal/cmd/egctl/testdata/translate/out/valid-envoyproxy.all.yaml @@ -5,12 +5,16 @@ envoyProxyForGatewayClass: name: example namespace: default spec: - logging: {} + logging: + level: + default: warn provider: kubernetes: envoyService: annotations: custom1: svc-annotation1 + externalTrafficPolicy: Local + type: LoadBalancer type: Kubernetes status: {} gatewayClass: @@ -42,13 +46,22 @@ gateways: spec: gatewayClassName: eg listeners: - - name: tcp + - allowedRoutes: + namespaces: + from: Same + name: tcp port: 1234 protocol: TCP - - name: udp + - allowedRoutes: + namespaces: + from: Same + name: udp port: 1234 protocol: UDP - - hostname: foo.com + - allowedRoutes: + namespaces: + from: Same + hostname: foo.com name: tls-passthrough port: 8443 protocol: TLS @@ -58,6 +71,8 @@ gateways: kinds: - group: gateway.networking.k8s.io kind: HTTPRoute + namespaces: + from: Same name: http port: 80 protocol: HTTP @@ -65,6 +80,8 @@ gateways: kinds: - group: gateway.networking.k8s.io kind: GRPCRoute + namespaces: + from: Same name: grpc port: 8080 protocol: HTTP @@ -185,7 +202,9 @@ grpcRoutes: hostnames: - www.grpc-example.com parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg sectionName: grpc rules: - backendRefs: @@ -198,6 +217,7 @@ grpcRoutes: - method: method: DoThing service: com.example.Things + type: Exact status: parents: - conditions: @@ -213,6 +233,8 @@ grpcRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg sectionName: grpc httpRoutes: @@ -225,7 +247,9 @@ httpRoutes: hostnames: - www.example.com parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg rules: - backendRefs: - group: "" @@ -252,6 +276,8 @@ httpRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg tcpRoutes: - kind: TCPRoute @@ -261,7 +287,9 @@ tcpRoutes: namespace: default spec: parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg sectionName: tcp rules: - backendRefs: @@ -285,6 +313,8 @@ tcpRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg sectionName: tcp tlsRoutes: @@ -295,7 +325,9 @@ tlsRoutes: namespace: default spec: parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg sectionName: tls-passthrough rules: - backendRefs: @@ -319,6 +351,8 @@ tlsRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg sectionName: tls-passthrough udpRoutes: @@ -329,7 +363,9 @@ udpRoutes: namespace: default spec: parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg sectionName: udp rules: - backendRefs: @@ -353,5 +389,7 @@ udpRoutes: type: ResolvedRefs controllerName: gateway.envoyproxy.io/gatewayclass-controller parentRef: + group: gateway.networking.k8s.io + kind: Gateway name: eg sectionName: udp diff --git a/internal/gatewayapi/resource/defaulter.go b/internal/gatewayapi/resource/defaulter.go new file mode 100644 index 00000000000..9a380c48e1e --- /dev/null +++ b/internal/gatewayapi/resource/defaulter.go @@ -0,0 +1,270 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package resource + +import ( + "encoding/json" + "errors" + "fmt" + "path" + "sort" + "strings" + + gospec "github.com/go-openapi/spec" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" + "github.com/go-openapi/validate/post" + "golang.org/x/exp/maps" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/openapi" + "k8s.io/kube-openapi/pkg/spec3" + kubespec "k8s.io/kube-openapi/pkg/validation/spec" + "sigs.k8s.io/kubectl-validate/pkg/openapiclient" + "sigs.k8s.io/kubectl-validate/pkg/utils" + "sigs.k8s.io/kubectl-validate/pkg/validator" +) + +// This file contains code derived from kubectl-validate, +// https://github.com/kubernetes-sigs/kubectl-validate +// from the source file +// https://github.com/kubernetes-sigs/kubectl-validate/blob/main/pkg/validator/validator.go +// and is provided here subject to the following: +// Copyright Project kubectl-validate Authors +// SPDX-License-Identifier: Apache-2.0 +// +// The Defaulter in this file is derived from Validator in kubectl-validate, +// since the Validator field `validatorCache` is not exposed and we would like +// to use the parsed schema for our CRD from it, we build this Defaulter that +// meets our needs. +// TODO: remove this file once can directly get schema from the Validator in kubectl-validate. + +var gatewaySchemaDefaulter, _ = newDefaulter(openapiclient.NewLocalCRDFiles(gatewayCRDsFS)) + +// Defaulter can set default values for crd object according to their schema. +type Defaulter struct { + gvs map[string]openapi.GroupVersion + schemaCache map[schema.GroupVersionKind]*kubespec.Schema +} + +func newDefaulter(client openapi.Client) (*Defaulter, error) { + gvs, err := client.Paths() + if err != nil { + return nil, err + } + + return &Defaulter{ + gvs: gvs, + schemaCache: map[schema.GroupVersionKind]*kubespec.Schema{}, + }, nil +} + +// ApplyDefault applies default values for input object, and return the object with default values. +func (d *Defaulter) ApplyDefault(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + if obj == nil || obj.Object == nil { + return nil, fmt.Errorf("passed object cannot be nil") + } + + // shallow copy input object, this method can modify apiVersion, kind, or metadata. + obj = &unstructured.Unstructured{Object: maps.Clone(obj.UnstructuredContent())} + // deep copy metadata object. + obj.Object["metadata"] = runtime.DeepCopyJSONValue(obj.Object["metadata"]) + gvk := obj.GroupVersionKind() + schema, err := d.parseSchema(gvk) + if err != nil { + return nil, fmt.Errorf("failed to retrieve validator: %w", err) + } + + // convert kube-openapi spec to go-openapi spec via JSON format. + schemaBytes, err := schema.MarshalJSON() + if err != nil { + return nil, fmt.Errorf("failed to marshal schema: %w", err) + } + var goSchema gospec.Schema + err = goSchema.UnmarshalJSON(schemaBytes) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal schema: %w", err) + } + + v := validate.NewSchemaValidator(&goSchema, nil, "", strfmt.Default) + rs := v.Validate(obj.Object) + post.ApplyDefaults(rs) + // convert output object into unstructured one. + output, ok := rs.Data().(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("failed to convert output object") + } + + return &unstructured.Unstructured{Object: output}, nil +} + +func (d *Defaulter) parseSchema(gvk schema.GroupVersionKind) (*kubespec.Schema, error) { + if existing, ok := d.schemaCache[gvk]; ok { + return existing, nil + } + + // Otherwise, fetch the open API schema for this GV and do the above + // Lookup gvk in client + // Guess the rest mapping since we don't have a rest mapper for the target + // cluster + gvPath := "apis/" + gvk.Group + "/" + gvk.Version + if len(gvk.Group) == 0 { + gvPath = "api/" + gvk.Version + } + gvFetcher, exists := d.gvs[gvPath] + if !exists { + return nil, fmt.Errorf("failed to locate OpenAPI spec for GV: %v", gvk.GroupVersion()) + } + + documentBytes, err := gvFetcher.Schema("application/json") + if err != nil { + return nil, fmt.Errorf("error fetching openapi at path %s: %w", gvPath, err) + } + + openapiSpec := spec3.OpenAPI{} + if err := json.Unmarshal(documentBytes, &openapiSpec); err != nil { + return nil, fmt.Errorf("error parsing openapi spec: %w", err) + } + + // Apply our transformations to workaround known k8s schema deficiencies + for name, def := range openapiSpec.Components.Schemas { + //!TODO: would be useful to know which version of k8s each schema is believed + // to come from. + openapiSpec.Components.Schemas[name] = validator.ApplySchemaPatches(0, gvk.GroupVersion(), name, def) + } + + // Remove all references/indirection. + // This is kinda hacky because we still do allow recursive schemas via + // pointer trickery. + // No need for stack/queue approach since we mutate same dictionary/slice instances + // destructively. + // Replaces subschemas that contain refs with copy of the thing they refer to + // !TODO validate that no cyces are created by this process. If so, do not + // allow structural schema creation via JSON + // !TODO: track unresolved references? + // !TODO: Once Declarative Validation for native types lands we will be + // able to validate against the spec.Schema directly rather than + // StructuralSchema, so this will be able to be removed + var referenceErrors []error + for name, def := range openapiSpec.Components.Schemas { + // This hack only works because top level schemas never have references + // so we can reliably copy them knowing they won't change and pointer-share + // their subfields. The only schemas being modified here should be sub-fields. + openapiSpec.Components.Schemas[name] = utils.VisitSchema(name, def, utils.PreorderVisitor(func(ctx utils.VisitingContext, sch *kubespec.Schema) (*kubespec.Schema, bool) { + defName := sch.Ref.String() + + if len(sch.AllOf) == 1 && len(sch.AllOf[0].Ref.String()) > 0 { + // SPECIAL CASE + // OpenAPIV3 does not support having Refs in schemas with fields like + // Description, Default filled in. So k8s stuffs the Ref into a standalone + // AllOf in these cases. + // But structural schema doesn't like schemas that specify fields inside AllOf + // SO in the case of + // Properties + // -> AllOf + // -> Ref + defName = sch.AllOf[0].Ref.String() + } + + if len(defName) == 0 { + // Nothing to do for no references + return sch, true + } + + defName = path.Base(defName) + resolved, ok := openapiSpec.Components.Schemas[defName] + if !ok { + // Can't resolve schema. This is an error. + var path []string + for cursor := &ctx; cursor != nil; cursor = cursor.Parent { + if len(cursor.Key) == 0 { + path = append(path, fmt.Sprint(cursor.Index)) + } else { + path = append(path, cursor.Key) + } + } + sort.Stable(sort.Reverse(sort.StringSlice(path))) + referenceErrors = append(referenceErrors, fmt.Errorf("cannot resolve reference %v in %v.%v", defName, name, strings.Join(path, "."))) + return sch, true + } + + resolvedCopy := *resolved + + if sch.Default != nil { + resolvedCopy.Default = sch.Default + } + + // NOTE: No way to tell if field overrides nullable + // or if it is unset. Right now if the referred schema is + // nullable we will resolve to a nullable schema. + // There are no upstream schemas where nullable is used as a field + // level override, so we will assume `false` means `unset`. + // But this should be fixed in kube-openapi. + resolvedCopy.Nullable = resolvedCopy.Nullable || sch.Nullable + + if len(sch.Type) > 0 { + resolvedCopy.Type = sch.Type + } + + if len(sch.Description) > 0 { + resolvedCopy.Description = sch.Description + } + + newExtensions := kubespec.Extensions{} + for k, v := range resolvedCopy.Extensions { + newExtensions.Add(k, v) + } + for k, v := range sch.Extensions { + newExtensions.Add(k, v) + } + if len(newExtensions) > 0 { + resolvedCopy.Extensions = newExtensions + } + + // Don't explore children. This was a reference node and shares + // pointers with its schema which will be traversed in this loop. + return &resolvedCopy, false + })) + } + + if len(referenceErrors) > 0 { + return nil, errors.Join(referenceErrors...) + } + + namespaced := sets.New[schema.GroupVersionKind]() + if openapiSpec.Paths != nil { + for path, pathInfo := range openapiSpec.Paths.Paths { + for _, gvk := range utils.ExtractPathGVKs(pathInfo) { + if !namespaced.Has(gvk) { + if strings.Contains(path, "namespaces/{namespace}") { + namespaced.Insert(gvk) + } + } + } + } + } + + for _, def := range openapiSpec.Components.Schemas { + gvks := utils.ExtractExtensionGVKs(def.Extensions) + if len(gvks) == 0 { + continue + } + + for _, specGVK := range gvks { + d.schemaCache[specGVK] = def + } + } + + // Check again to see if the desired GVK was added to the spec cache. + // If so, create validator for it + if existing, ok := d.schemaCache[gvk]; ok { + return existing, nil + } + + return nil, fmt.Errorf("kind %v not found in %v groupversion", gvk.Kind, gvk.GroupVersion()) +} diff --git a/internal/gatewayapi/resource/defaulter_test.go b/internal/gatewayapi/resource/defaulter_test.go new file mode 100644 index 00000000000..79014e95201 --- /dev/null +++ b/internal/gatewayapi/resource/defaulter_test.go @@ -0,0 +1,157 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package resource + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/kubectl-validate/pkg/openapiclient" +) + +func TestApplyDefault(t *testing.T) { + defaulter, err := newDefaulter(openapiclient.NewLocalCRDFiles(os.DirFS("testdata/schema"))) + require.NoError(t, err) + + testCases := []struct { + name string + error bool + input map[string]interface{} + expect map[string]interface{} + }{ + { + name: "empty object with nested field", + input: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "TestCR", + "metadata": map[string]interface{}{ + "name": "test-cr", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "objectField": map[string]interface{}{}, + }, + }, + expect: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "TestCR", + "metadata": map[string]interface{}{ + "name": "test-cr", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "stringField": "defaultString", + "integerField": 42., + "floatField": 3.14, + "booleanField": true, + "enumField": "option1", + "objectField": map[string]interface{}{ + "nestedString": "nestedDefault", + "nestedInteger": 10., + }, + "mapField": map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + error: false, + }, + { + name: "empty object without nested field", + input: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "TestCR", + "metadata": map[string]interface{}{ + "name": "test-cr", + "namespace": "default", + }, + "spec": map[string]interface{}{}, + }, + expect: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "TestCR", + "metadata": map[string]interface{}{ + "name": "test-cr", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "stringField": "defaultString", + "integerField": 42., + "floatField": 3.14, + "booleanField": true, + "enumField": "option1", + "mapField": map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + error: false, + }, + { + name: "object with few field unset", + input: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "TestCR", + "metadata": map[string]interface{}{ + "name": "test-cr", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "stringField": "exampleString", + "booleanField": false, + "objectField": map[string]interface{}{ + "nestedString": "nestedExample", + }, + }, + }, + expect: map[string]interface{}{ + "apiVersion": "example.com/v1", + "kind": "TestCR", + "metadata": map[string]interface{}{ + "name": "test-cr", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "stringField": "exampleString", + "integerField": 42., + "floatField": 3.14, + "booleanField": false, + "enumField": "option1", + "objectField": map[string]interface{}{ + "nestedString": "nestedExample", + "nestedInteger": 10., + }, + "mapField": map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + error: false, + }, + { + name: "nil input", + input: nil, + error: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := defaulter.ApplyDefault(&unstructured.Unstructured{Object: tc.input}) + if tc.error { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expect, got.Object) + } + }) + } +} diff --git a/internal/gatewayapi/resource/load.go b/internal/gatewayapi/resource/load.go index 7c87ffb7918..3229aabe458 100644 --- a/internal/gatewayapi/resource/load.go +++ b/internal/gatewayapi/resource/load.go @@ -45,11 +45,9 @@ func LoadResourcesFromYAMLBytes(yamlBytes []byte, addMissingResources bool) (*Re // loadKubernetesYAMLToResources converts a Kubernetes YAML string into GatewayAPI Resources. // TODO: add support for kind: // - EnvoyExtensionPolicy (gateway.envoyproxy.io/v1alpha1) -// - HTTPRouteFilter (gateway.envoyproxy.io/v1alpha1) // - BackendLPPolicy (gateway.networking.k8s.io/v1alpha2) // - BackendTLSPolicy (gateway.networking.k8s.io/v1alpha3) // - ReferenceGrant (gateway.networking.k8s.io/v1alpha2) -// - TLSRoute (gateway.networking.k8s.io/v1alpha2) func loadKubernetesYAMLToResources(input []byte, addMissingResources bool) (*Resources, error) { resources := NewResources() var useDefaultNamespace bool @@ -64,7 +62,7 @@ func loadKubernetesYAMLToResources(input []byte, addMissingResources bool) (*Res return err } - un := unstructured.Unstructured{Object: obj} + un := &unstructured.Unstructured{Object: obj} gvk := un.GroupVersionKind() name, namespace := un.GetName(), un.GetNamespace() if len(namespace) == 0 { @@ -72,11 +70,16 @@ func loadKubernetesYAMLToResources(input []byte, addMissingResources bool) (*Res namespace = config.DefaultNamespace } - // Perform local validation for gateway-api related resources only. + // Perform local validation and apply default values for gateway-api related resources only. if gvk.Group == egv1a1.GroupName || gvk.Group == gwapiv1.GroupName { if err = defaultValidator.Validate(yamlByte); err != nil { return fmt.Errorf("local validation error: %w", err) } + + un, err = gatewaySchemaDefaulter.ApplyDefault(un) + if err != nil { + return fmt.Errorf("failed to apply default values for %s/%s: %w", un.GetKind(), un.GetName(), err) + } } requiredNamespaceMap.Insert(namespace) @@ -84,7 +87,7 @@ func loadKubernetesYAMLToResources(input []byte, addMissingResources bool) (*Res if err != nil { return err } - err = combinedScheme.Convert(&un, kobj, nil) + err = combinedScheme.Convert(un, kobj, nil) if err != nil { return err } diff --git a/internal/gatewayapi/resource/load_test.go b/internal/gatewayapi/resource/load_test.go index df3629251e9..534730608e6 100644 --- a/internal/gatewayapi/resource/load_test.go +++ b/internal/gatewayapi/resource/load_test.go @@ -6,12 +6,22 @@ package resource import ( + "flag" + "fmt" + "os" + "path/filepath" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" "sigs.k8s.io/yaml" + + "github.com/envoyproxy/gateway/internal/utils/file" ) +var overrideTestData = flag.Bool("override-testdata", true, "if override the test output data.") + func TestIterYAMLBytes(t *testing.T) { inputs := `test: foo1 --- @@ -37,3 +47,36 @@ test: foo3 require.NoError(t, err) require.ElementsMatch(t, names, []string{"foo1", "foo2", "foo3"}) } + +func TestLoadAllSupportedResourcesFromYAMLBytes(t *testing.T) { + inFile := requireTestDataFile(t, "all-resources", "in") + got, err := loadKubernetesYAMLToResources(inFile, true) + require.NoError(t, err) + + if *overrideTestData { + out, err := yaml.Marshal(got) + require.NoError(t, err) + require.NoError(t, file.Write(string(out), filepath.Join("testdata", "all-resources.out.yaml"))) + } + + want := &Resources{} + outFile := requireTestDataFile(t, "all-resources", "out") + mustUnmarshal(t, outFile, want) + + opts := []cmp.Option{ + cmpopts.IgnoreFields(Resources{}, "serviceMap"), + cmpopts.EquateEmpty(), + } + require.Empty(t, cmp.Diff(want, got, opts...)) +} + +func requireTestDataFile(t *testing.T, name, ioType string) []byte { + t.Helper() + content, err := os.ReadFile(filepath.Join("testdata", fmt.Sprintf("%s.%s.yaml", name, ioType))) + require.NoError(t, err) + return content +} + +func mustUnmarshal(t *testing.T, val []byte, out interface{}) { + require.NoError(t, yaml.UnmarshalStrict(val, out, yaml.DisallowUnknownFields)) +} diff --git a/internal/gatewayapi/resource/testdata/all-resources.in.yaml b/internal/gatewayapi/resource/testdata/all-resources.in.yaml new file mode 100644 index 00000000000..733660178e4 --- /dev/null +++ b/internal/gatewayapi/resource/testdata/all-resources.in.yaml @@ -0,0 +1,231 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: example + namespace: default +spec: + provider: + type: Kubernetes + kubernetes: + envoyService: + annotations: + custom1: svc-annotation1 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: eg +spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: eg +spec: + gatewayClassName: eg + listeners: + - name: http + protocol: HTTP + port: 80 +--- +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TCPRoute +metadata: + name: backend + namespace: default +spec: + parentRefs: + - name: eg + sectionName: tcp + rules: + - backendRefs: + - name: backend + port: 3000 +--- +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: UDPRoute +metadata: + name: backend + namespace: default +spec: + parentRefs: + - name: eg + sectionName: udp + rules: + - backendRefs: + - name: backend + port: 3000 +--- +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: backend + namespace: default +spec: + parentRefs: + - name: eg + sectionName: tls-passthrough + rules: + - backendRefs: + - name: backend + port: 3000 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: backend + namespace: default +spec: + parentRefs: + - name: eg + hostnames: + - "www.example.com" + rules: + - backendRefs: + - name: providedBackend + port: 8000 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GRPCRoute +metadata: + name: backend + namespace: default +spec: + parentRefs: + - name: eg + sectionName: grpc + hostnames: + - "www.grpc-example.com" + rules: + - matches: + - method: + service: com.example.Things + method: DoThing + headers: + - name: com.example.Header + value: foobar + backendRefs: + - name: providedBackend + port: 9000 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyPatchPolicy +metadata: + name: ratelimit-patch-policy + namespace: default +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: eg + type: JSONPatch + jsonPatches: + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + # The listener name is of the form // + name: default/eg/http + operation: + op: add + path: "/default_filter_chain/filters/0/typed_config/http_filters/0" + value: + name: "envoy.filters.http.ratelimit" + typed_config: + "@type": "type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit" + domain: "eag-ratelimit" + failure_mode_deny: true + timeout: 1s + rate_limit_service: + grpc_service: + envoy_grpc: + cluster_name: rate-limit-cluster + transport_api_version: V3 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: jwt-example +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: backend + apiKeyAuth: + credentialRefs: + - name: foobar + extractFrom: + - headers: + - foobar + jwt: + providers: + - name: example + remoteJWKS: + uri: https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/jwks.json +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: cookie-lb-policy + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: cookie-lb-route + loadBalancer: + type: ConsistentHash + consistentHash: + type: Cookie + cookie: + name: "Lb-Test-Cookie" + ttl: 60s + attributes: + SameSite: Strict + retry: + retryOn: + httpStatusCodes: + - 200 + - 404 + healthCheck: + active: + type: HTTP + http: + path: "/" + method: GET + circuitBreaker: + maxRequestsPerConnection: 123 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: ClientTrafficPolicy +metadata: + name: client-timeout + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: same-namespace + timeout: + http: + requestReceivedTimeout: 50ms +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: HTTPRouteFilter +metadata: + name: direct-response-inline + namespace: default +spec: + directResponse: + contentType: text/plain + body: + type: Inline + inline: "OK" +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: Backend +metadata: + name: backend +spec: + endpoints: + - ip: + address: 0.0.0.0 + port: 4321 diff --git a/internal/gatewayapi/resource/testdata/all-resources.out.yaml b/internal/gatewayapi/resource/testdata/all-resources.out.yaml new file mode 100644 index 00000000000..162b782ab23 --- /dev/null +++ b/internal/gatewayapi/resource/testdata/all-resources.out.yaml @@ -0,0 +1,360 @@ +backendTrafficPolicies: +- kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: cookie-lb-policy + namespace: gateway-conformance-infra + spec: + circuitBreaker: + maxConnections: 1024 + maxParallelRequests: 1024 + maxParallelRetries: 1024 + maxPendingRequests: 1024 + maxRequestsPerConnection: 123 + healthCheck: + active: + healthyThreshold: 1 + http: + method: GET + path: / + interval: 3s + timeout: 1s + type: HTTP + unhealthyThreshold: 3 + loadBalancer: + consistentHash: + cookie: + attributes: + SameSite: Strict + name: Lb-Test-Cookie + ttl: 1m0s + tableSize: 65537 + type: Cookie + type: ConsistentHash + retry: + numRetries: 2 + retryOn: + httpStatusCodes: + - 200 + - 404 + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: cookie-lb-route + status: + ancestors: null +backends: +- kind: Backend + metadata: + creationTimestamp: null + name: backend + namespace: envoy-gateway-system + spec: + endpoints: + - ip: + address: 0.0.0.0 + port: 4321 + status: {} +clientTrafficPolicies: +- kind: ClientTrafficPolicy + metadata: + creationTimestamp: null + name: client-timeout + namespace: gateway-conformance-infra + spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: same-namespace + timeout: + http: + requestReceivedTimeout: 50ms + status: + ancestors: null +envoyPatchPolicies: +- kind: EnvoyPatchPolicy + metadata: + creationTimestamp: null + name: ratelimit-patch-policy + namespace: default + spec: + jsonPatches: + - name: default/eg/http + operation: + op: add + path: /default_filter_chain/filters/0/typed_config/http_filters/0 + value: + name: envoy.filters.http.ratelimit + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit + domain: eag-ratelimit + failure_mode_deny: true + rate_limit_service: + grpc_service: + envoy_grpc: + cluster_name: rate-limit-cluster + transport_api_version: V3 + timeout: 1s + type: type.googleapis.com/envoy.config.listener.v3.Listener + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: eg + type: JSONPatch + status: + ancestors: null +envoyProxyForGatewayClass: + kind: EnvoyProxy + metadata: + creationTimestamp: null + name: example + namespace: default + spec: + logging: + level: + default: warn + provider: + kubernetes: + envoyService: + annotations: + custom1: svc-annotation1 + externalTrafficPolicy: Local + type: LoadBalancer + type: Kubernetes + status: {} +gatewayClass: + kind: GatewayClass + metadata: + creationTimestamp: null + name: eg + namespace: envoy-gateway-system + spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller + status: {} +gateways: +- kind: Gateway + metadata: + creationTimestamp: null + name: eg + namespace: envoy-gateway-system + spec: + gatewayClassName: eg + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http + port: 80 + protocol: HTTP + status: {} +grpcRoutes: +- kind: GRPCRoute + metadata: + creationTimestamp: null + name: backend + namespace: default + spec: + hostnames: + - www.grpc-example.com + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: eg + sectionName: grpc + rules: + - backendRefs: + - group: "" + kind: Service + name: providedBackend + port: 9000 + weight: 1 + matches: + - headers: + - name: com.example.Header + type: Exact + value: foobar + method: + method: DoThing + service: com.example.Things + type: Exact + status: + parents: null +httpFilters: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: HTTPRouteFilter + metadata: + creationTimestamp: null + name: direct-response-inline + namespace: default + spec: + directResponse: + body: + inline: OK + type: Inline + contentType: text/plain +httpRoutes: +- kind: HTTPRoute + metadata: + creationTimestamp: null + name: backend + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: eg + rules: + - backendRefs: + - group: "" + kind: Service + name: providedBackend + port: 8000 + weight: 1 + matches: + - path: + type: PathPrefix + value: / + status: + parents: null +namespaces: +- metadata: + creationTimestamp: null + name: envoy-gateway-system + spec: {} + status: {} +- metadata: + creationTimestamp: null + name: default + spec: {} + status: {} +- metadata: + creationTimestamp: null + name: gateway-conformance-infra + spec: {} + status: {} +securityPolicies: +- kind: SecurityPolicy + metadata: + creationTimestamp: null + name: jwt-example + namespace: envoy-gateway-system + spec: + apiKeyAuth: + credentialRefs: + - group: "" + kind: Secret + name: foobar + extractFrom: + - headers: + - foobar + jwt: + providers: + - name: example + remoteJWKS: + uri: https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/jwks.json + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: backend + status: + ancestors: null +services: +- metadata: + creationTimestamp: null + name: backend + namespace: default + spec: + clusterIP: 1.2.3.4 + ports: + - name: TCP-3000 + port: 3000 + protocol: TCP + targetPort: 0 + - name: UDP-3000 + port: 3000 + protocol: UDP + targetPort: 0 + status: + loadBalancer: {} +- metadata: + creationTimestamp: null + name: providedBackend + namespace: default + spec: + clusterIP: 1.2.3.4 + ports: + - name: TCP-8000 + port: 8000 + protocol: TCP + targetPort: 0 + - name: TCP-9000 + port: 9000 + protocol: TCP + targetPort: 0 + status: + loadBalancer: {} +tcpRoutes: +- kind: TCPRoute + metadata: + creationTimestamp: null + name: backend + namespace: default + spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: eg + sectionName: tcp + rules: + - backendRefs: + - group: "" + kind: Service + name: backend + port: 3000 + weight: 1 + status: + parents: null +tlsRoutes: +- kind: TLSRoute + metadata: + creationTimestamp: null + name: backend + namespace: default + spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: eg + sectionName: tls-passthrough + rules: + - backendRefs: + - group: "" + kind: Service + name: backend + port: 3000 + weight: 1 + status: + parents: null +udpRoutes: +- kind: UDPRoute + metadata: + creationTimestamp: null + name: backend + namespace: default + spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: eg + sectionName: udp + rules: + - backendRefs: + - group: "" + kind: Service + name: backend + port: 3000 + weight: 1 + status: + parents: null diff --git a/internal/gatewayapi/resource/testdata/schema/crd.yaml b/internal/gatewayapi/resource/testdata/schema/crd.yaml new file mode 100644 index 00000000000..69c42f323ea --- /dev/null +++ b/internal/gatewayapi/resource/testdata/schema/crd.yaml @@ -0,0 +1,79 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: testcrs.example.com +spec: + group: example.com + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + stringField: + type: string + default: "defaultString" + minLength: 3 + maxLength: 10 + integerField: + type: integer + default: 42 + minimum: 0 + maximum: 100 + floatField: + type: number + default: 3.14 + minimum: 0.0 + maximum: 10.0 + booleanField: + type: boolean + default: true + enumField: + type: string + enum: + - option1 + - option2 + - option3 + default: "option1" + arrayField: + type: array + items: + type: string + minItems: 1 + maxItems: 5 + objectField: + type: object + properties: + nestedString: + type: string + default: "nestedDefault" + nestedInteger: + type: integer + default: 10 + minimum: 1 + maximum: 20 + required: + - nestedString + mapField: + type: object + additionalProperties: + type: string + default: + key1: "value1" + key2: "value2" + required: + - stringField + - integerField + - booleanField + scope: Namespaced + names: + plural: testcrs + singular: testcr + kind: TestCR + shortNames: + - tc diff --git a/internal/provider/file/testdata/resources.all.yaml b/internal/provider/file/testdata/resources.all.yaml index 079647dc6c0..19d93d0c2b9 100644 --- a/internal/provider/file/testdata/resources.all.yaml +++ b/internal/provider/file/testdata/resources.all.yaml @@ -28,7 +28,10 @@ gateways: spec: gatewayClassName: eg listeners: - - name: http + - allowedRoutes: + namespaces: + from: Same + name: http port: 8888 protocol: HTTP status: {} @@ -42,12 +45,15 @@ httpRoutes: hostnames: - www.example.com parentRefs: - - name: eg + - group: gateway.networking.k8s.io + kind: Gateway + name: eg rules: - backendRefs: - group: gateway.envoyproxy.io kind: Backend name: backend + weight: 1 matches: - path: type: PathPrefix diff --git a/release-notes/current.yaml b/release-notes/current.yaml index f036c6a8752..892ad239ddb 100644 --- a/release-notes/current.yaml +++ b/release-notes/current.yaml @@ -11,6 +11,7 @@ security updates: | new features: | Added support for configuring maxUnavailable in KubernetesPodDisruptionBudgetSpec Added support for percentage-based request mirroring + Add defaulter for gateway-api resources loading from file to be able to set default values. bug fixes: | diff --git a/tools/make/golang.mk b/tools/make/golang.mk index 4f4dce00faa..36531fed65d 100644 --- a/tools/make/golang.mk +++ b/tools/make/golang.mk @@ -53,6 +53,7 @@ go.testdata.complete: ## Override test ouputdata go test -timeout 30s github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/proxy --override-testdata=true go test -timeout 30s github.com/envoyproxy/gateway/internal/xds/bootstrap --override-testdata=true go test -timeout 60s github.com/envoyproxy/gateway/internal/gatewayapi --override-testdata=true + go test -timeout 60s github.com/envoyproxy/gateway/internal/gatewayapi/resource --override-testdata=true .PHONY: go.test.coverage go.test.coverage: go.test.cel ## Run go unit and integration tests in GitHub Actions