From 8b42d0aff84a35a061373702c8ff56af850a40e2 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Thu, 30 Jan 2020 22:37:59 -0700 Subject: [PATCH] Refactor --- .dockerignore | 1 + .gitignore | 1 + Dockerfile | 4 +- Riofile | 4 + go.mod | 7 +- go.sum | 41 +-- main.go | 58 +--- pkg/accesscontrol/access_control.go | 8 +- pkg/accesscontrol/access_set.go | 54 +--- pkg/attributes/attributes.go | 62 ++-- pkg/auth/cli/webhookcli.go | 67 ++++ pkg/auth/filter.go | 146 +++++++++ pkg/client/factory.go | 16 +- pkg/clustercache/controller.go | 9 +- pkg/controllers/schema/schemas.go | 12 +- pkg/debug/cli.go | 72 +++++ pkg/proxy/proxy.go | 39 +-- pkg/schema/collection.go | 26 +- pkg/schema/converter/crd.go | 17 +- pkg/schema/converter/discovery.go | 25 +- pkg/schema/converter/k8stonorman.go | 6 +- pkg/schema/converter/openapi.go | 28 +- pkg/schema/converter/openapiv3.go | 80 +++++ pkg/schema/defaultmapper.go | 10 +- pkg/schema/factory.go | 41 +-- pkg/schemaserver/builtin/schema.go | 88 +++++ pkg/schemaserver/handlers/create.go | 33 ++ pkg/schemaserver/handlers/delete.go | 20 ++ pkg/schemaserver/handlers/error.go | 65 ++++ pkg/schemaserver/handlers/list.go | 53 +++ pkg/schemaserver/handlers/update.go | 39 +++ pkg/schemaserver/httperror/error.go | 70 ++++ pkg/schemaserver/parse/browser.go | 18 ++ pkg/schemaserver/parse/mux.go | 23 ++ pkg/schemaserver/parse/parse.go | 168 ++++++++++ pkg/schemaserver/parse/read_input.go | 56 ++++ pkg/schemaserver/parse/validate.go | 47 +++ pkg/schemaserver/server/access.go | 59 ++++ pkg/schemaserver/server/server.go | 258 +++++++++++++++ pkg/schemaserver/server/validate.go | 78 +++++ pkg/schemaserver/store/apiroot/apiroot.go | 135 ++++++++ pkg/schemaserver/store/empty/empty_store.go | 33 ++ pkg/schemaserver/store/schema/schema_store.go | 100 ++++++ pkg/schemaserver/subscribe/convert.go | 53 +++ pkg/schemaserver/subscribe/handler.go | 80 +++++ pkg/schemaserver/subscribe/register.go | 16 + pkg/schemaserver/subscribe/watcher.go | 140 ++++++++ pkg/schemaserver/types/encoder.go | 25 ++ pkg/schemaserver/types/schemas.go | 70 ++++ pkg/schemaserver/types/server_types.go | 278 ++++++++++++++++ pkg/schemaserver/types/types.go | 86 +++++ pkg/schemaserver/urlbuilder/base.go | 84 +++++ pkg/schemaserver/urlbuilder/url.go | 124 +++++++ pkg/schemaserver/writer/encoding.go | 124 +++++++ pkg/schemaserver/writer/headers.go | 24 ++ pkg/schemaserver/writer/html.go | 85 +++++ pkg/server/cli/clicontext.go | 73 +++++ pkg/server/config.go | 89 +++++ pkg/server/handler/apiserver.go | 26 +- pkg/server/handler/handlers.go | 10 +- pkg/server/resources/apigroups/apigroup.go | 59 ++-- pkg/server/resources/common/defaultcolumns.go | 17 +- pkg/server/resources/common/dynamiccolumns.go | 111 +++++++ pkg/server/resources/common/formatter.go | 24 +- pkg/server/resources/counts/counts.go | 52 +-- pkg/server/resources/schema.go | 44 +-- pkg/server/router/router.go | 14 +- pkg/server/server.go | 221 +++++++------ pkg/server/store/proxy/client.go | 66 ++++ pkg/server/store/proxy/error_wrapper.go | 56 ++++ pkg/server/store/proxy/proxy_store.go | 303 ++++++++++++++++++ 71 files changed, 4024 insertions(+), 507 deletions(-) create mode 100644 Riofile create mode 100644 pkg/auth/cli/webhookcli.go create mode 100644 pkg/auth/filter.go create mode 100644 pkg/debug/cli.go create mode 100644 pkg/schema/converter/openapiv3.go create mode 100644 pkg/schemaserver/builtin/schema.go create mode 100644 pkg/schemaserver/handlers/create.go create mode 100644 pkg/schemaserver/handlers/delete.go create mode 100644 pkg/schemaserver/handlers/error.go create mode 100644 pkg/schemaserver/handlers/list.go create mode 100644 pkg/schemaserver/handlers/update.go create mode 100644 pkg/schemaserver/httperror/error.go create mode 100644 pkg/schemaserver/parse/browser.go create mode 100644 pkg/schemaserver/parse/mux.go create mode 100644 pkg/schemaserver/parse/parse.go create mode 100644 pkg/schemaserver/parse/read_input.go create mode 100644 pkg/schemaserver/parse/validate.go create mode 100644 pkg/schemaserver/server/access.go create mode 100644 pkg/schemaserver/server/server.go create mode 100644 pkg/schemaserver/server/validate.go create mode 100644 pkg/schemaserver/store/apiroot/apiroot.go create mode 100644 pkg/schemaserver/store/empty/empty_store.go create mode 100644 pkg/schemaserver/store/schema/schema_store.go create mode 100644 pkg/schemaserver/subscribe/convert.go create mode 100644 pkg/schemaserver/subscribe/handler.go create mode 100644 pkg/schemaserver/subscribe/register.go create mode 100644 pkg/schemaserver/subscribe/watcher.go create mode 100644 pkg/schemaserver/types/encoder.go create mode 100644 pkg/schemaserver/types/schemas.go create mode 100644 pkg/schemaserver/types/server_types.go create mode 100644 pkg/schemaserver/types/types.go create mode 100644 pkg/schemaserver/urlbuilder/base.go create mode 100644 pkg/schemaserver/urlbuilder/url.go create mode 100644 pkg/schemaserver/writer/encoding.go create mode 100644 pkg/schemaserver/writer/headers.go create mode 100644 pkg/schemaserver/writer/html.go create mode 100644 pkg/server/cli/clicontext.go create mode 100644 pkg/server/config.go create mode 100644 pkg/server/resources/common/dynamiccolumns.go create mode 100644 pkg/server/store/proxy/client.go create mode 100644 pkg/server/store/proxy/error_wrapper.go create mode 100644 pkg/server/store/proxy/proxy_store.go diff --git a/.dockerignore b/.dockerignore index bec10c10..ced95b87 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ +./.certs ./.dapper ./.cache ./dist diff --git a/.gitignore b/.gitignore index 4d328849..0ee2ac69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /.dapper /.cache +/certs /bin /dist *.swp diff --git a/Dockerfile b/Dockerfile index 13faeba0..264d9d27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,10 @@ +# syntax = docker/dockerfile:experimental FROM golang:1.12.7 as build COPY go.mod go.sum main.go /src/ COPY vendor /src/vendor/ COPY pkg /src/pkg/ -RUN cd /src && \ +RUN --mount=type=cache,target=/root/.cache/go-build \ + cd /src && \ CGO_ENABLED=0 go build -ldflags "-extldflags -static -s" -o /steve -mod=vendor FROM alpine diff --git a/Riofile b/Riofile new file mode 100644 index 00000000..ccf4cb9b --- /dev/null +++ b/Riofile @@ -0,0 +1,4 @@ +services: + steve: + ports: + - 80:8080 diff --git a/go.mod b/go.mod index 063b6743..fe14dd5d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.13 replace ( github.com/rancher/dynamiclistener => ../dynamiclistener - github.com/rancher/wrangler => ../wrangler k8s.io/client-go => k8s.io/client-go v0.17.2 ) @@ -14,17 +13,17 @@ require ( github.com/gorilla/websocket v1.4.0 github.com/pkg/errors v0.8.1 github.com/rancher/dynamiclistener v0.2.1-0.20191204183509-ab900b52683c - github.com/rancher/wrangler v0.4.0 + github.com/rancher/wrangler v0.4.1-0.20200131051624-f65ef17f3764 github.com/rancher/wrangler-api v0.4.1 github.com/sirupsen/logrus v1.4.2 github.com/urfave/cli v1.22.2 - github.com/urfave/cli/v2 v2.1.1 // indirect - golang.org/x/sync v0.0.0-20190423024810-112230192c58 + github.com/urfave/cli/v2 v2.1.1 k8s.io/api v0.17.2 k8s.io/apiextensions-apiserver v0.17.2 k8s.io/apimachinery v0.17.2 k8s.io/apiserver v0.17.2 k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible + k8s.io/klog v1.0.0 k8s.io/kube-aggregator v0.17.2 k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a ) diff --git a/go.sum b/go.sum index ad9d27d3..5c14a959 100644 --- a/go.sum +++ b/go.sum @@ -8,7 +8,6 @@ contrib.go.opencensus.io/exporter/prometheus v0.1.0/go.mod h1:cGFniUXGZlKRjzOyuZ contrib.go.opencensus.io/exporter/stackdriver v0.12.7/go.mod h1:ZOhmSfHIoyVaQ+bKN+lR4h7K2olTIJsrdOwWHsNGw4w= github.com/Azure/azure-sdk-for-go v32.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= @@ -84,10 +83,8 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deislabs/smi-sdk-go v0.0.0-20190819154013-e53a9b2d8c1a/go.mod h1:0k1wou4pOCBNFoyxOkTUoB9XDtB2RBvJ03S5aJREHCI= github.com/deislabs/smi-sdk-go v0.2.0/go.mod h1:0k1wou4pOCBNFoyxOkTUoB9XDtB2RBvJ03S5aJREHCI= github.com/denisenkom/go-mssqldb v0.0.0-20190412130859-3b1d194e553a/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= -github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/digitalocean/godo v1.6.0/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU= github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= @@ -105,7 +102,6 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -171,7 +167,6 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ= github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gocql/gocql v0.0.0-20190402132108-0e1d5de854df/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= -github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= @@ -199,7 +194,6 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/addlicense v0.0.0-20190510175307-22550fa7c1b0/go.mod h1:QtPG26W17m+OIQgE6gQ24gC1M6pUaMBAbFrTIDtwG/E= -github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -211,7 +205,6 @@ github.com/google/go-containerregistry v0.0.0-20190617215043-876b8855d23c/go.mod github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= -github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -228,20 +221,16 @@ github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhp github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk= github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= -github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= -github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -288,7 +277,6 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= @@ -326,8 +314,6 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/markbates/inflect v1.0.4/go.mod h1:1fR9+pO2KHEO9ZRtto13gDwwZaAKstQzferVeWqbgNs= -github.com/maruel/panicparse v0.0.0-20171209025017-c0182c169410/go.mod h1:nty42YY5QByNC5MM7q/nj938VbgPU7avs45z6NClpxI= -github.com/maruel/ut v1.0.0/go.mod h1:I68ffiAt5qre9obEVTy7S2/fj2dJku2NYLvzPuY0gqE= github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -371,7 +357,6 @@ github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c h1:eSfnfIuwhxZyULg1NNuZycJcYkjYVGYe7FczwQReM6U= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= @@ -408,10 +393,12 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/rancher/norman v0.0.0-20200124050618-768222c62d5b h1:0UOd9eV2GnOG7ojrpTw4RL5VsNUivovQjZ5aPTqRoZ4= github.com/rancher/wrangler v0.1.4 h1:bdzBw4H9JKQhXPBPNp4eHbmrkA24+VII865VLiVWcw8= github.com/rancher/wrangler v0.1.4/go.mod h1:EYP7cqpg42YqElaCm+U9ieSrGQKAXxUH5xsr+XGpWyE= +github.com/rancher/wrangler v0.4.0 h1:iLvuJcZkd38E3RGG74dFMMNEju0PeTzfT1PQiv5okVU= github.com/rancher/wrangler v0.4.0/go.mod h1:1cR91WLhZgkZ+U4fV9nVuXqKurWbgXcIReU4wnQvTN8= +github.com/rancher/wrangler v0.4.1-0.20200131051624-f65ef17f3764 h1:CH0RUk2iE3MsiwC7tSc1Z0ejQY5s0YZG5Jz1xWcNE60= +github.com/rancher/wrangler v0.4.1-0.20200131051624-f65ef17f3764/go.mod h1:1cR91WLhZgkZ+U4fV9nVuXqKurWbgXcIReU4wnQvTN8= github.com/rancher/wrangler-api v0.2.0/go.mod h1:zTPdNLZO07KvRaVOx6XQbKBSV55Fnn4s7nqmrMPJqd8= github.com/rancher/wrangler-api v0.4.1 h1:bwy6BbdCEq+zQrWmy9L8XXcGEQ/mbeuQ2q1kfk8fo3M= github.com/rancher/wrangler-api v0.4.1/go.mod h1:X+dwYUrZe9q7u9manPJf2ZF8OvP0L7AQ0CuFaqtZO0s= @@ -467,7 +454,6 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= @@ -493,7 +479,6 @@ go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslx go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= @@ -590,11 +575,8 @@ golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fq golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU= -golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= @@ -717,9 +699,6 @@ k8s.io/apimachinery v0.17.0 h1:xRBnuie9rXcPxUkDizUsGvPf1cnlZCFu210op7J7LJo= k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4= k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= -k8s.io/apiserver v0.0.0-20190313205120-8b27c41bdbb1/go.mod h1:6bqaTSOSJavUIXUtfaR9Os9JtTCm8ZqH2SUl2S60C4w= -k8s.io/apiserver v0.0.0-20190409021813-1ec86e4da56c h1:k7ALUVzrOEgz4hOF+pr4pePn7TqZ9lB/8Z8ndMSsWSU= -k8s.io/apiserver v0.0.0-20190409021813-1ec86e4da56c/go.mod h1:6bqaTSOSJavUIXUtfaR9Os9JtTCm8ZqH2SUl2S60C4w= k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg= k8s.io/apiserver v0.0.0-20191016112112-5190913f932d/go.mod h1:7OqfAolfWxUM/jJ/HBLyE+cdaWFBUoo5Q5pHgJVj2ws= k8s.io/apiserver v0.0.0-20191114103151-9ca1dc586682/go.mod h1:Idob8Va6/sMX5SmwPLsU0pdvFlkwxuJ5x+fXMG8NbKE= @@ -727,15 +706,8 @@ k8s.io/apiserver v0.17.0 h1:XhUix+FKFDcBygWkQNp7wKKvZL030QUlH1o8vFeSgZA= k8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg= k8s.io/apiserver v0.17.2 h1:NssVvPALll6SSeNgo1Wk1h2myU1UHNwmhxV0Oxbcl8Y= k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo= -k8s.io/client-go v0.0.0-20181213151034-8d9ed539ba31/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= -k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90/go.mod h1:J69/JveO6XESwVgG53q3Uz5OSfgsv4uxpScmmyYOOlk= -k8s.io/client-go v0.0.0-20191016111102-bec269661e48/go.mod h1:hrwktSwYGI4JK+TJA3dMaFyyvHVi/aLarVHpbs8bgCU= -k8s.io/client-go v0.0.0-20191114101535-6c5935290e33/go.mod h1:4L/zQOBkEf4pArQJ+CMk1/5xjA30B5oyWv+Bzb44DOw= -k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k= k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc= k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI= -k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible h1:U5Bt+dab9K8qaUmXINrkXO135kA11/i5Kg1RUydgaMQ= -k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= k8s.io/code-generator v0.0.0-20181114232248-ae218e241252/go.mod h1:IPqxl/YHk05nodzupwjke6ctMjyNRdV2zZ5/j3/F204= k8s.io/code-generator v0.0.0-20190311093542-50b561225d70/go.mod h1:MYiN+ZJZ9HkETbgVZdWw2AsuAi9PZ4V80cwfuf2axe8= k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE= @@ -752,34 +724,27 @@ k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8 k8s.io/gengo v0.0.0-20190327210449-e17681d19d3a/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20191120174120-e74f70b9b27e/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/helm v2.14.3+incompatible h1:uzotTcZXa/b2SWVoUzM1xiCXVjI38TuxMujS/1s+3Gw= -k8s.io/helm v2.14.3+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.0.0-20190306015804-8e90cee79f82/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0 h1:0VPpR+sizsiivjIfIAQH/rl8tan6jvWkS7lU+0di3lE= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.2 h1:qvP/U6CcZ6qyi/qSHlJKdlAboCzo3mT0DAm0XAarpz4= k8s.io/klog v0.3.2/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ= k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= -k8s.io/kube-aggregator v0.0.0-20190409022021-00b8e31abe9d h1:B9HhOvdcDeWdl/VABDjsz9upXW03r7BhhLhM9IMNq3Y= -k8s.io/kube-aggregator v0.0.0-20190409022021-00b8e31abe9d/go.mod h1:8sbzT4QQKDEmSCIbfqjV0sd97GpUT7A4W626sBiYJmU= k8s.io/kube-aggregator v0.0.0-20191114103820-f023614fb9ea/go.mod h1:LlqyQuTxPHvUzmEgT71Cl/BB86o5+UcbN1LiGgSz94U= k8s.io/kube-aggregator v0.17.0 h1:2/15hPpXp11GvQmtLeTlNP6WeZnmebs/uxckzZS3P9c= k8s.io/kube-aggregator v0.17.0/go.mod h1:Vw104PtCEuT12WTVuhRFWCHXGiVqXsTzFtrvoaHxpk4= k8s.io/kube-aggregator v0.17.2 h1:3E94T8cVy3Zsh75wffsyuk04CiQ8gLzsjlaFwb1wHRA= k8s.io/kube-aggregator v0.17.2/go.mod h1:8xQTzaH0GrcKPiSB4YYWwWbeQ0j/4zRsbQt8usEMbRg= -k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= k8s.io/kube-openapi v0.0.0-20190502190224-411b2483e503 h1:IrnrEIp9du1SngrzGC1fdYEdos7Il6I6EVxwFQHJwCg= k8s.io/kube-openapi v0.0.0-20190502190224-411b2483e503/go.mod h1:iU+ZGYsNlvU9XKUSso6SQfKTCCw7lFduMZy26Mgr2Fw= k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf h1:EYm5AW/UUDbnmnI+gK0TJDVK9qPLhM+sRHYanNKw0EQ= k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= -k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= k8s.io/utils v0.0.0-20190506122338-8fab8cb257d5 h1:VBM/0P5TWxwk+Nw6Z+lAw3DKgO76g90ETOiA6rfLV1Y= k8s.io/utils v0.0.0-20190506122338-8fab8cb257d5/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20190801114015-581e00157fb1 h1:+ySTxfHnfzZb9ys375PXNlLhkJPLKgHajBU0N62BDvE= diff --git a/main.go b/main.go index 88e2af9c..cae48b9e 100644 --- a/main.go +++ b/main.go @@ -2,19 +2,19 @@ package main import ( "context" - "flag" "os" - "github.com/rancher/steve/pkg/server" + "github.com/rancher/steve/pkg/debug" + stevecli "github.com/rancher/steve/pkg/server/cli" "github.com/rancher/steve/pkg/version" "github.com/rancher/wrangler/pkg/signals" "github.com/sirupsen/logrus" "github.com/urfave/cli" - "k8s.io/klog" ) var ( - config server.Config + config stevecli.Config + debugconfig debug.Config ) func main() { @@ -22,31 +22,9 @@ func main() { app.Name = "steve" app.Version = version.FriendlyVersion() app.Usage = "" - app.Flags = []cli.Flag{ - cli.BoolFlag{ - Name: "authentication", - Destination: &config.Authentication, - }, - cli.StringFlag{ - Name: "webhook-kubeconfig", - EnvVar: "WEBHOOK_KUBECONFIG", - Value: "webhook-kubeconfig.yaml", - Destination: &config.WebhookKubeconfig, - }, - cli.StringFlag{ - Name: "kubeconfig", - EnvVar: "KUBECONFIG", - Value: "", - Destination: &config.Kubeconfig, - }, - cli.StringFlag{ - Name: "listen-address", - EnvVar: "LISTEN_ADDRESS", - Value: ":8080", - Destination: &config.ListenAddress, - }, - cli.BoolFlag{Name: "debug"}, - } + app.Flags = append( + stevecli.Flags(&config), + debug.Flags(&debugconfig)...) app.Action = run if err := app.Run(os.Args); err != nil { @@ -54,23 +32,9 @@ func main() { } } -func run(c *cli.Context) error { - logging := flag.NewFlagSet("", flag.PanicOnError) - klog.InitFlags(logging) - if c.Bool("debug") { - logrus.SetLevel(logrus.DebugLevel) - if err := logging.Parse([]string{ - "-v=7", - }); err != nil { - return err - } - } else { - if err := logging.Parse([]string{ - "-v=0", - }); err != nil { - return err - } - } +func run(_ *cli.Context) error { ctx := signals.SetupSignalHandler(context.Background()) - return server.Run(ctx, config) + debugconfig.MustSetupDebug() + s := config.MustServerConfig().MustServer() + return s.ListenAndServe(ctx, nil) } diff --git a/pkg/accesscontrol/access_control.go b/pkg/accesscontrol/access_control.go index 886a5d8f..cba03fdb 100644 --- a/pkg/accesscontrol/access_control.go +++ b/pkg/accesscontrol/access_control.go @@ -3,19 +3,19 @@ package accesscontrol import ( "fmt" - "github.com/rancher/norman/pkg/authorization" - "github.com/rancher/norman/pkg/types" + "github.com/rancher/steve/pkg/schemaserver/server" + "github.com/rancher/steve/pkg/schemaserver/types" ) type AccessControl struct { - authorization.AllAccess + server.AllAccess } func NewAccessControl() *AccessControl { return &AccessControl{} } -func (a *AccessControl) CanWatch(apiOp *types.APIRequest, schema *types.Schema) error { +func (a *AccessControl) CanWatch(apiOp *types.APIRequest, schema *types.APISchema) error { access := GetAccessListMap(schema) if !access.Grants("watch", "*", "*") { return fmt.Errorf("watch not allowed") diff --git a/pkg/accesscontrol/access_set.go b/pkg/accesscontrol/access_set.go index 5210fd43..b3b04d9b 100644 --- a/pkg/accesscontrol/access_set.go +++ b/pkg/accesscontrol/access_set.go @@ -2,15 +2,15 @@ package accesscontrol import ( "github.com/rancher/steve/pkg/attributes" - "github.com/rancher/norman/pkg/types" + "github.com/rancher/steve/pkg/schemaserver/types" "k8s.io/apimachinery/pkg/runtime/schema" ) type AccessSet struct { - set map[key]ResourceAccess + set map[key]resourceAccessSet } -type ResourceAccess map[Access]bool +type resourceAccessSet map[Access]bool type key struct { verb string @@ -23,7 +23,7 @@ func (a *AccessSet) Merge(right *AccessSet) { if !ok { m = map[Access]bool{} if a.set == nil { - a.set = map[key]ResourceAccess{} + a.set = map[key]resourceAccessSet{} } a.set[k] = m } @@ -34,14 +34,7 @@ func (a *AccessSet) Merge(right *AccessSet) { } } -func (a AccessSet) ResourceAccessFor(verb string, gr schema.GroupResource) ResourceAccess { - return a.set[key{ - verb: verb, - gr: gr, - }] -} - -func (a AccessSet) AccessListFor(verb string, gr schema.GroupResource) (result []Access) { +func (a AccessSet) AccessListFor(verb string, gr schema.GroupResource) (result AccessList) { for _, v := range []string{all, verb} { for _, g := range []string{all, gr.Group} { for _, r := range []string{all, gr.Resource} { @@ -63,7 +56,7 @@ func (a AccessSet) AccessListFor(verb string, gr schema.GroupResource) (result [ func (a *AccessSet) Add(verb string, gr schema.GroupResource, access Access) { if a.set == nil { - a.set = map[key]ResourceAccess{} + a.set = map[key]resourceAccessSet{} } k := key{verb: verb, gr: gr} @@ -76,38 +69,13 @@ func (a *AccessSet) Add(verb string, gr schema.GroupResource, access Access) { } } -func (l ResourceAccess) None() bool { - return len(l) == 0 -} +type AccessListByVerb map[string]AccessList -func (l ResourceAccess) All() bool { - return l[Access{ - Namespace: all, - ResourceName: all, - }] -} - -func (l ResourceAccess) AllForNamespace(namespace string) bool { - return l[Access{ - Namespace: namespace, - ResourceName: all, - }] -} - -func (l ResourceAccess) HasAccess(namespace, name string) bool { - return l[Access{ - Namespace: namespace, - ResourceName: name, - }] -} - -type AccessListMap map[string]AccessList - -func (a AccessListMap) Grants(verb, namespace, name string) bool { +func (a AccessListByVerb) Grants(verb, namespace, name string) bool { return a[verb].Grants(namespace, name) } -func (a AccessListMap) AnyVerb(verb ...string) bool { +func (a AccessListByVerb) AnyVerb(verb ...string) bool { for _, v := range verb { if len(a[v]) > 0 { return true @@ -145,10 +113,10 @@ func (a Access) nameOK(name string) bool { return a.ResourceName == all || a.ResourceName == name } -func GetAccessListMap(s *types.Schema) AccessListMap { +func GetAccessListMap(s *types.APISchema) AccessListByVerb { if s == nil { return nil } - v, _ := attributes.Access(s).(AccessListMap) + v, _ := attributes.Access(s).(AccessListByVerb) return v } diff --git a/pkg/attributes/attributes.go b/pkg/attributes/attributes.go index 9c6a905d..822aa737 100644 --- a/pkg/attributes/attributes.go +++ b/pkg/attributes/attributes.go @@ -1,67 +1,67 @@ package attributes import ( - "github.com/rancher/norman/pkg/types" - "github.com/rancher/norman/pkg/types/convert" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/data/convert" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) -func Namespaced(s *types.Schema) bool { +func Namespaced(s *types.APISchema) bool { if s == nil { return false } return convert.ToBool(s.Attributes["namespaced"]) } -func SetNamespaced(s *types.Schema, value bool) { +func SetNamespaced(s *types.APISchema, value bool) { setVal(s, "namespaced", value) } -func str(s *types.Schema, key string) string { +func str(s *types.APISchema, key string) string { return convert.ToString(s.Attributes[key]) } -func setVal(s *types.Schema, key string, value interface{}) { +func setVal(s *types.APISchema, key string, value interface{}) { if s.Attributes == nil { s.Attributes = map[string]interface{}{} } s.Attributes[key] = value } -func Group(s *types.Schema) string { +func Group(s *types.APISchema) string { return str(s, "group") } -func SetGroup(s *types.Schema, value string) { +func SetGroup(s *types.APISchema, value string) { setVal(s, "group", value) } -func Version(s *types.Schema) string { +func Version(s *types.APISchema) string { return str(s, "version") } -func SetVersion(s *types.Schema, value string) { +func SetVersion(s *types.APISchema, value string) { setVal(s, "version", value) } -func Resource(s *types.Schema) string { +func Resource(s *types.APISchema) string { return str(s, "resource") } -func SetResource(s *types.Schema, value string) { +func SetResource(s *types.APISchema, value string) { setVal(s, "resource", value) } -func Kind(s *types.Schema) string { +func Kind(s *types.APISchema) string { return str(s, "kind") } -func SetKind(s *types.Schema, value string) { +func SetKind(s *types.APISchema, value string) { setVal(s, "kind", value) } -func GVK(s *types.Schema) schema.GroupVersionKind { +func GVK(s *types.APISchema) schema.GroupVersionKind { return schema.GroupVersionKind{ Group: Group(s), Version: Version(s), @@ -69,13 +69,13 @@ func GVK(s *types.Schema) schema.GroupVersionKind { } } -func SetGVK(s *types.Schema, gvk schema.GroupVersionKind) { +func SetGVK(s *types.APISchema, gvk schema.GroupVersionKind) { SetGroup(s, gvk.Group) SetVersion(s, gvk.Version) SetKind(s, gvk.Kind) } -func GVR(s *types.Schema) schema.GroupVersionResource { +func GVR(s *types.APISchema) schema.GroupVersionResource { return schema.GroupVersionResource{ Group: Group(s), Version: Version(s), @@ -83,73 +83,73 @@ func GVR(s *types.Schema) schema.GroupVersionResource { } } -func SetGVR(s *types.Schema, gvk schema.GroupVersionResource) { +func SetGVR(s *types.APISchema, gvk schema.GroupVersionResource) { SetGroup(s, gvk.Group) SetVersion(s, gvk.Version) SetResource(s, gvk.Resource) } -func Verbs(s *types.Schema) []string { +func Verbs(s *types.APISchema) []string { return convert.ToStringSlice(s.Attributes["verbs"]) } -func SetVerbs(s *types.Schema, verbs []string) { +func SetVerbs(s *types.APISchema, verbs []string) { setVal(s, "verbs", verbs) } -func GR(s *types.Schema) schema.GroupResource { +func GR(s *types.APISchema) schema.GroupResource { return schema.GroupResource{ Group: Group(s), Resource: Resource(s), } } -func SetGR(s *types.Schema, gr schema.GroupResource) { +func SetGR(s *types.APISchema, gr schema.GroupResource) { SetGroup(s, gr.Group) SetResource(s, gr.Resource) } -func SetAccess(s *types.Schema, access interface{}) { +func SetAccess(s *types.APISchema, access interface{}) { setVal(s, "access", access) } -func Access(s *types.Schema) interface{} { +func Access(s *types.APISchema) interface{} { return s.Attributes["access"] } -func SetAPIResource(s *types.Schema, resource v1.APIResource) { +func SetAPIResource(s *types.APISchema, resource v1.APIResource) { SetResource(s, resource.Name) SetVerbs(s, resource.Verbs) SetNamespaced(s, resource.Namespaced) } -func SetColumns(s *types.Schema, columns interface{}) { +func SetColumns(s *types.APISchema, columns interface{}) { if s.Attributes == nil { s.Attributes = map[string]interface{}{} } s.Attributes["columns"] = columns } -func Columns(s *types.Schema) interface{} { +func Columns(s *types.APISchema) interface{} { return s.Attributes["columns"] } -func PreferredVersion(s *types.Schema) string { +func PreferredVersion(s *types.APISchema) string { return convert.ToString(s.Attributes["preferredVersion"]) } -func SetPreferredVersion(s *types.Schema, ver string) { +func SetPreferredVersion(s *types.APISchema, ver string) { if s.Attributes == nil { s.Attributes = map[string]interface{}{} } s.Attributes["preferredVersion"] = ver } -func PreferredGroup(s *types.Schema) string { +func PreferredGroup(s *types.APISchema) string { return convert.ToString(s.Attributes["preferredGroup"]) } -func SetPreferredGroup(s *types.Schema, ver string) { +func SetPreferredGroup(s *types.APISchema, ver string) { if s.Attributes == nil { s.Attributes = map[string]interface{}{} } diff --git a/pkg/auth/cli/webhookcli.go b/pkg/auth/cli/webhookcli.go new file mode 100644 index 00000000..32e51902 --- /dev/null +++ b/pkg/auth/cli/webhookcli.go @@ -0,0 +1,67 @@ +package cli + +import ( + "os" + "time" + + "github.com/rancher/steve/pkg/auth" + "github.com/urfave/cli" +) + +type WebhookConfig struct { + WebhookAuthentication bool + WebhookKubeconfig string + WebhookURL string + CacheTTLSeconds int +} + +func (w *WebhookConfig) MustWebhookMiddleware() auth.Middleware { + m, err := w.WebhookMiddleware() + if err != nil { + panic("failed to create webhook middleware: " + err.Error()) + } + return m +} + +func (w *WebhookConfig) WebhookMiddleware() (auth.Middleware, error) { + if !w.WebhookAuthentication { + return nil, nil + } + + config := w.WebhookKubeconfig + if config == "" && w.WebhookURL != "" { + tempFile, err := auth.WebhookConfigForURL(w.WebhookURL) + if err != nil { + return nil, err + } + defer os.Remove(tempFile) + config = tempFile + } + + return auth.NewWebhookMiddleware(time.Duration(w.CacheTTLSeconds)*time.Second, config) +} + +func Flags(config *WebhookConfig) []cli.Flag { + return []cli.Flag{ + cli.BoolFlag{ + Name: "webhook-auth", + EnvVar: "WEBHOOK_AUTH", + Destination: &config.WebhookAuthentication, + }, + cli.StringFlag{ + Name: "webhook-kubeconfig", + EnvVar: "WEBHOOK_KUBECONFIG", + Destination: &config.WebhookKubeconfig, + }, + cli.StringFlag{ + Name: "webhook-url", + EnvVar: "WEBHOOK_URL", + Destination: &config.WebhookURL, + }, + cli.IntFlag{ + Name: "webhook-cache-ttl", + EnvVar: "WEBHOOK_CACHE_TTL", + Destination: &config.CacheTTLSeconds, + }, + } +} diff --git a/pkg/auth/filter.go b/pkg/auth/filter.go new file mode 100644 index 00000000..059fd6d6 --- /dev/null +++ b/pkg/auth/filter.go @@ -0,0 +1,146 @@ +package auth + +import ( + "io/ioutil" + "net/http" + "strings" + "time" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/token/cache" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +type Authenticator interface { + Authenticate(req *http.Request) (user.Info, bool, error) +} + +type AuthenticatorFunc func(req *http.Request) (user.Info, bool, error) + +func (a AuthenticatorFunc) Authenticate(req *http.Request) (user.Info, bool, error) { + return a(req) +} + +type Middleware func(http.ResponseWriter, *http.Request, http.Handler) + +func (m Middleware) Wrap(handler http.Handler) http.Handler { + if m == nil { + return handler + } + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + m(rw, req, handler) + }) +} + +func WebhookConfigForURL(url string) (string, error) { + config := clientcmdapi.Config{ + Clusters: map[string]*clientcmdapi.Cluster{ + "local": { + Server: url, + InsecureSkipTLSVerify: true, + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + "Default": { + Cluster: "local", + AuthInfo: "user", + Namespace: "default", + }, + }, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "user": {}, + }, + CurrentContext: "Default", + } + + tmpFile, err := ioutil.TempFile("", "webhook-config") + if err != nil { + return "", err + } + if err := tmpFile.Close(); err != nil { + return "", err + } + + return tmpFile.Name(), clientcmd.WriteToFile(config, tmpFile.Name()) +} + +func NewWebhookAuthenticator(cacheTTL time.Duration, kubeConfigFile string) (Authenticator, error) { + wh, err := webhook.New(kubeConfigFile, "v1", nil) + if err != nil { + return nil, err + } + + if cacheTTL > 0 { + return &webhookAuth{ + auth: cache.New(wh, false, cacheTTL, cacheTTL), + }, nil + } + + return &webhookAuth{ + auth: wh, + }, nil +} + +func NewWebhookMiddleware(cacheTTL time.Duration, kubeConfigFile string) (Middleware, error) { + auth, err := NewWebhookAuthenticator(cacheTTL, kubeConfigFile) + if err != nil { + return nil, err + } + return ToMiddleware(auth), nil +} + +type webhookAuth struct { + auth authenticator.Token +} + +func (w *webhookAuth) Authenticate(req *http.Request) (user.Info, bool, error) { + token := req.Header.Get("Authorization") + if strings.HasPrefix(token, "Bearer ") { + token = strings.TrimPrefix(token, "Bearer ") + } else { + token = "" + } + + if token == "" { + cookie, err := req.Cookie("R_SESS") + if err != nil && err != http.ErrNoCookie { + return nil, false, err + } else if err != http.ErrNoCookie && len(cookie.Value) > 0 { + token = "cookie://" + cookie.Value + } + } + + if token == "" { + return nil, false, nil + } + + resp, ok, err := w.auth.AuthenticateToken(req.Context(), token) + if resp == nil { + return nil, ok, err + } + return resp.User, ok, err +} + +func ToMiddleware(auth Authenticator) func(rw http.ResponseWriter, req *http.Request, next http.Handler) { + return func(rw http.ResponseWriter, req *http.Request, next http.Handler) { + info, ok, err := auth.Authenticate(req) + if err != nil { + rw.WriteHeader(http.StatusServiceUnavailable) + rw.Write([]byte(err.Error())) + return + } + + if !ok { + rw.WriteHeader(http.StatusUnauthorized) + return + } + + ctx := request.WithUser(req.Context(), info) + req = req.WithContext(ctx) + next.ServeHTTP(rw, req) + } +} diff --git a/pkg/client/factory.go b/pkg/client/factory.go index b6f0ae01..d2f6e09e 100644 --- a/pkg/client/factory.go +++ b/pkg/client/factory.go @@ -2,25 +2,27 @@ package client import ( "github.com/rancher/steve/pkg/attributes" - "github.com/rancher/norman/pkg/types" + "github.com/rancher/steve/pkg/schemaserver/types" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" ) type Factory struct { client dynamic.Interface + Config *rest.Config } func NewFactory(cfg *rest.Config) (*Factory, error) { - newCfg := *cfg + newCfg := rest.CopyConfig(cfg) newCfg.QPS = 10000 newCfg.Burst = 100 - c, err := dynamic.NewForConfig(&newCfg) + c, err := dynamic.NewForConfig(newCfg) if err != nil { return nil, err } return &Factory{ client: c, + Config: newCfg, }, nil } @@ -28,11 +30,7 @@ func (p *Factory) DynamicClient() dynamic.Interface { return p.client } -func (p *Factory) Client(ctx *types.APIRequest, s *types.Schema) (dynamic.ResourceInterface, error) { +func (p *Factory) Client(ctx *types.APIRequest, s *types.APISchema, namespace string) (dynamic.ResourceInterface, error) { gvr := attributes.GVR(s) - if len(ctx.Namespaces) > 0 { - return p.client.Resource(gvr).Namespace(ctx.Namespaces[0]), nil - } - - return p.client.Resource(gvr), nil + return p.client.Resource(gvr).Namespace(namespace), nil } diff --git a/pkg/clustercache/controller.go b/pkg/clustercache/controller.go index e3a10217..5ea6bc3c 100644 --- a/pkg/clustercache/controller.go +++ b/pkg/clustercache/controller.go @@ -6,14 +6,13 @@ import ( "sync" "time" - meta "k8s.io/apimachinery/pkg/api/meta" - "github.com/rancher/steve/pkg/attributes" - "github.com/rancher/steve/pkg/resources/schema" - "github.com/rancher/norman/pkg/types" + "github.com/rancher/steve/pkg/schema" + "github.com/rancher/steve/pkg/schemaserver/types" "github.com/rancher/wrangler/pkg/generic" "github.com/rancher/wrangler/pkg/merr" "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" schema2 "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" @@ -83,7 +82,7 @@ func (h *clusterCache) AddController(gvk schema2.GroupVersionKind, informer cach h.typed[gvk] = informer } -func validSchema(schema *types.Schema) bool { +func validSchema(schema *types.APISchema) bool { canList := false canWatch := false for _, verb := range attributes.Verbs(schema) { diff --git a/pkg/controllers/schema/schemas.go b/pkg/controllers/schema/schemas.go index 4c4c8167..a69e0a3b 100644 --- a/pkg/controllers/schema/schemas.go +++ b/pkg/controllers/schema/schemas.go @@ -7,9 +7,9 @@ import ( "time" "github.com/rancher/steve/pkg/attributes" - schema2 "github.com/rancher/steve/pkg/resources/schema" - "github.com/rancher/steve/pkg/resources/schema/converter" - "github.com/rancher/norman/pkg/types" + schema2 "github.com/rancher/steve/pkg/schema" + "github.com/rancher/steve/pkg/schema/converter" + "github.com/rancher/steve/pkg/schemaserver/types" apiextcontrollerv1beta1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io/v1beta1" v1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiregistration.k8s.io/v1" "github.com/sirupsen/logrus" @@ -82,7 +82,7 @@ func (h *handler) queueRefresh() { }() } -func isListWatchable(schema *types.Schema) bool { +func isListWatchable(schema *types.APISchema) bool { var ( canList bool canWatch bool @@ -114,7 +114,7 @@ func (h *handler) refreshAll() error { return err } - filteredSchemas := map[string]*types.Schema{} + filteredSchemas := map[string]*types.APISchema{} for id, schema := range schemas { if isListWatchable(schema) { if ok, err := h.allowed(schema); err != nil { @@ -134,7 +134,7 @@ func (h *handler) refreshAll() error { return nil } -func (h *handler) allowed(schema *types.Schema) (bool, error) { +func (h *handler) allowed(schema *types.APISchema) (bool, error) { gvr := attributes.GVR(schema) ssar, err := h.ssar.Create(&authorizationv1.SelfSubjectAccessReview{ Spec: authorizationv1.SelfSubjectAccessReviewSpec{ diff --git a/pkg/debug/cli.go b/pkg/debug/cli.go new file mode 100644 index 00000000..a3e9bbdb --- /dev/null +++ b/pkg/debug/cli.go @@ -0,0 +1,72 @@ +package debug + +import ( + "flag" + "fmt" + + "github.com/sirupsen/logrus" + "github.com/urfave/cli" + cliv2 "github.com/urfave/cli/v2" + "k8s.io/klog" +) + +type Config struct { + Debug bool + DebugLevel int +} + +func (c *Config) MustSetupDebug() { + err := c.SetupDebug() + if err != nil { + panic("failed to setup debug logging: " + err.Error()) + } +} + +func (c *Config) SetupDebug() error { + logging := flag.NewFlagSet("", flag.PanicOnError) + klog.InitFlags(logging) + if c.Debug { + logrus.SetLevel(logrus.DebugLevel) + if err := logging.Parse([]string{ + fmt.Sprintf("-v=%d", c.DebugLevel), + }); err != nil { + return err + } + } else { + if err := logging.Parse([]string{ + "-v=0", + }); err != nil { + return err + } + } + + return nil +} + +func Flags(config *Config) []cli.Flag { + return []cli.Flag{ + cli.BoolFlag{ + Name: "debug", + Destination: &config.Debug, + }, + cli.IntFlag{ + Name: "debug-level", + Value: 7, + Destination: &config.DebugLevel, + }, + } +} + +func FlagsV2(config *Config) []cliv2.Flag { + return []cliv2.Flag{ + &cliv2.BoolFlag{ + Name: "debug", + Destination: &config.Debug, + }, + &cliv2.IntFlag{ + Name: "debug-level", + Value: 7, + Destination: &config.DebugLevel, + }, + } +} diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index c38b8ff8..ca658a4c 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -1,14 +1,12 @@ package proxy import ( - "net" + "fmt" "net/http" "net/url" "strings" - "time" "github.com/rancher/wrangler/pkg/kubeconfig" - utilnet "k8s.io/apimachinery/pkg/util/net" "k8s.io/apimachinery/pkg/util/proxy" "k8s.io/client-go/rest" "k8s.io/client-go/transport" @@ -40,7 +38,7 @@ func Handler(prefix string, cfg *rest.Config) (http.Handler, error) { if err != nil { return nil, err } - upgradeTransport, err := makeUpgradeTransport(cfg, 0) + upgradeTransport, err := makeUpgradeTransport(cfg, transport) if err != nil { return nil, err } @@ -49,20 +47,27 @@ func Handler(prefix string, cfg *rest.Config) (http.Handler, error) { proxy.UpgradeTransport = upgradeTransport proxy.UseRequestLocation = true - handler := setHost(target.Host, proxy) + handler := http.Handler(proxy) if len(target.Path) > 1 { handler = prependPath(target.Path[:len(target.Path)-1], handler) } if len(prefix) > 2 { - return stripLeaveSlash(prefix, handler), nil + handler = stripLeaveSlash(prefix, handler) } - return handler, nil + return authHeaders(handler), nil } -func setHost(host string, h http.Handler) http.Handler { +func authHeaders(handler http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + req.Header.Del("Authorization") + handler.ServeHTTP(rw, req) + }) +} + +func SetHost(host string, h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { req.Host = host h.ServeHTTP(w, req) @@ -85,34 +90,20 @@ func prependPath(prefix string, h http.Handler) http.Handler { func stripLeaveSlash(prefix string, h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { p := strings.TrimPrefix(req.URL.Path, prefix) - if len(p) >= len(req.URL.Path) { - http.NotFound(w, req) - return - } if len(p) > 0 && p[:1] != "/" { p = "/" + p } req.URL.Path = p + fmt.Println(req.Method, " ", req.URL.String()) h.ServeHTTP(w, req) }) } -func makeUpgradeTransport(config *rest.Config, keepalive time.Duration) (proxy.UpgradeRequestRoundTripper, error) { +func makeUpgradeTransport(config *rest.Config, rt http.RoundTripper) (proxy.UpgradeRequestRoundTripper, error) { transportConfig, err := config.TransportConfig() if err != nil { return nil, err } - tlsConfig, err := transport.TLSConfigFor(transportConfig) - if err != nil { - return nil, err - } - rt := utilnet.SetOldTransportDefaults(&http.Transport{ - TLSClientConfig: tlsConfig, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: keepalive, - }).DialContext, - }) upgrader, err := transport.HTTPWrappersForConfig(transportConfig, proxy.MirrorRequest) if err != nil { diff --git a/pkg/schema/collection.go b/pkg/schema/collection.go index f9718984..ec3e9ef6 100644 --- a/pkg/schema/collection.go +++ b/pkg/schema/collection.go @@ -3,26 +3,27 @@ package schema import ( "strings" - "github.com/rancher/norman/v2/pkg/data" - "github.com/rancher/norman/v2/pkg/types" "github.com/rancher/steve/pkg/accesscontrol" "github.com/rancher/steve/pkg/attributes" - "github.com/rancher/steve/pkg/table" + "github.com/rancher/steve/pkg/schema/table" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/data" "github.com/rancher/wrangler/pkg/name" + "github.com/rancher/wrangler/pkg/schemas" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authentication/user" ) type Factory interface { - Schemas(user user.Info) (*types.Schemas, error) + Schemas(user user.Info) (*types.APISchemas, error) ByGVR(gvr schema.GroupVersionResource) string ByGVK(gvr schema.GroupVersionKind) string } type Collection struct { toSync int32 - baseSchema *types.Schemas - schemas map[string]*types.Schema + baseSchema *types.APISchemas + schemas map[string]*types.APISchema templates map[string]*Template byGVR map[schema.GroupVersionResource]string byGVK map[schema.GroupVersionKind]string @@ -34,20 +35,19 @@ type Template struct { Group string Kind string ID string - RegisterType interface{} - Customize func(*types.Schema) + Customize func(*types.APISchema) Formatter types.Formatter Store types.Store StoreFactory func(types.Store) types.Store - Mapper types.Mapper + Mapper schemas.Mapper Columns []table.Column ComputedColumns func(data.Object) } -func NewCollection(baseSchema *types.Schemas, access *accesscontrol.AccessStore) *Collection { +func NewCollection(baseSchema *types.APISchemas, access *accesscontrol.AccessStore) *Collection { return &Collection{ baseSchema: baseSchema, - schemas: map[string]*types.Schema{}, + schemas: map[string]*types.APISchema{}, templates: map[string]*Template{}, byGVR: map[schema.GroupVersionResource]string{}, byGVK: map[schema.GroupVersionKind]string{}, @@ -55,7 +55,7 @@ func NewCollection(baseSchema *types.Schemas, access *accesscontrol.AccessStore) } } -func (c *Collection) Reset(schemas map[string]*types.Schema) { +func (c *Collection) Reset(schemas map[string]*types.APISchema) { byGVK := map[schema.GroupVersionKind]string{} byGVR := map[schema.GroupVersionResource]string{} @@ -75,7 +75,7 @@ func (c *Collection) Reset(schemas map[string]*types.Schema) { c.byGVK = byGVK } -func (c *Collection) Schema(id string) *types.Schema { +func (c *Collection) Schema(id string) *types.APISchema { return c.schemas[id] } diff --git a/pkg/schema/converter/crd.go b/pkg/schema/converter/crd.go index 8d9719bb..f66229e5 100644 --- a/pkg/schema/converter/crd.go +++ b/pkg/schema/converter/crd.go @@ -1,17 +1,18 @@ package converter import ( - "github.com/rancher/norman/v2/pkg/types" "github.com/rancher/steve/pkg/attributes" - "github.com/rancher/steve/pkg/table" + "github.com/rancher/steve/pkg/schema/table" + "github.com/rancher/steve/pkg/schemaserver/types" "github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io/v1beta1" + "github.com/rancher/wrangler/pkg/schemas" beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) var ( - staticFields = map[string]types.Field{ + staticFields = map[string]schemas.Field{ "apiVersion": { Type: "string", }, @@ -24,7 +25,7 @@ var ( } ) -func AddCustomResources(crd v1beta1.CustomResourceDefinitionClient, schemas map[string]*types.Schema) error { +func AddCustomResources(crd v1beta1.CustomResourceDefinitionClient, schemas map[string]*types.APISchema) error { crds, err := crd.List(metav1.ListOptions{}) if err != nil { return nil @@ -57,7 +58,7 @@ func AddCustomResources(crd v1beta1.CustomResourceDefinitionClient, schemas map[ return nil } -func forVersion(crd *beta1.CustomResourceDefinition, group, version, kind string, schemas map[string]*types.Schema, columnDefs []beta1.CustomResourceColumnDefinition, columns []table.Column) { +func forVersion(crd *beta1.CustomResourceDefinition, group, version, kind string, schemasMap map[string]*types.APISchema, columnDefs []beta1.CustomResourceColumnDefinition, columns []table.Column) { var versionColumns []table.Column for _, col := range columnDefs { versionColumns = append(versionColumns, table.Column{ @@ -77,7 +78,7 @@ func forVersion(crd *beta1.CustomResourceDefinition, group, version, kind string Kind: kind, }) - schema := schemas[id] + schema := schemasMap[id] if schema == nil { return } @@ -86,13 +87,13 @@ func forVersion(crd *beta1.CustomResourceDefinition, group, version, kind string } if crd.Spec.Validation != nil && crd.Spec.Validation.OpenAPIV3Schema != nil { - if fieldsSchema := modelV3ToSchema(id, crd.Spec.Validation.OpenAPIV3Schema, schemas); fieldsSchema != nil { + if fieldsSchema := modelV3ToSchema(id, crd.Spec.Validation.OpenAPIV3Schema, schemasMap); fieldsSchema != nil { for k, v := range staticFields { fieldsSchema.ResourceFields[k] = v } for k, v := range fieldsSchema.ResourceFields { if schema.ResourceFields == nil { - schema.ResourceFields = map[string]types.Field{} + schema.ResourceFields = map[string]schemas.Field{} } if _, ok := schema.ResourceFields[k]; !ok { schema.ResourceFields[k] = v diff --git a/pkg/schema/converter/discovery.go b/pkg/schema/converter/discovery.go index 8c45647e..4c821ea6 100644 --- a/pkg/schema/converter/discovery.go +++ b/pkg/schema/converter/discovery.go @@ -3,9 +3,10 @@ package converter import ( "strings" - "github.com/rancher/norman/v2/pkg/types" "github.com/rancher/steve/pkg/attributes" + "github.com/rancher/steve/pkg/schemaserver/types" "github.com/rancher/wrangler/pkg/merr" + "github.com/rancher/wrangler/pkg/schemas" "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -18,11 +19,13 @@ var ( } ) -func AddDiscovery(client discovery.DiscoveryInterface, schemas map[string]*types.Schema) error { +func AddDiscovery(client discovery.DiscoveryInterface, schemasMap map[string]*types.APISchema) error { logrus.Info("Refreshing all schemas") groups, resourceLists, err := client.ServerGroupsAndResources() - if err != nil { + if gd, ok := err.(*discovery.ErrGroupDiscoveryFailed); ok { + logrus.Errorf("Failed to read API for groups %v", gd.Groups) + } else if err != nil { return err } @@ -35,7 +38,7 @@ func AddDiscovery(client discovery.DiscoveryInterface, schemas map[string]*types errs = append(errs, err) } - if err := refresh(gv, versions, resourceList, schemas); err != nil { + if err := refresh(gv, versions, resourceList, schemasMap); err != nil { errs = append(errs, err) } } @@ -51,7 +54,7 @@ func indexVersions(groups []*metav1.APIGroup) map[string]string { return result } -func refresh(gv schema.GroupVersion, groupToPreferredVersion map[string]string, resources *metav1.APIResourceList, schemas map[string]*types.Schema) error { +func refresh(gv schema.GroupVersion, groupToPreferredVersion map[string]string, resources *metav1.APIResourceList, schemasMap map[string]*types.APISchema) error { for _, resource := range resources.APIResources { if strings.Contains(resource.Name, "/") { continue @@ -66,12 +69,12 @@ func refresh(gv schema.GroupVersion, groupToPreferredVersion map[string]string, logrus.Infof("APIVersion %s/%s Kind %s", gvk.Group, gvk.Version, gvk.Kind) - schema := schemas[GVKToSchemaID(gvk)] + schema := schemasMap[GVKToSchemaID(gvk)] if schema == nil { - schema = &types.Schema{ - ID: GVKToSchemaID(gvk), - Type: "schema", - Dynamic: true, + schema = &types.APISchema{ + Schema: &schemas.Schema{ + ID: GVKToSchemaID(gvk), + }, } attributes.SetGVK(schema, gvk) } @@ -85,7 +88,7 @@ func refresh(gv schema.GroupVersion, groupToPreferredVersion map[string]string, attributes.SetPreferredGroup(schema, group) } - schemas[schema.ID] = schema + schemasMap[schema.ID] = schema } return nil diff --git a/pkg/schema/converter/k8stonorman.go b/pkg/schema/converter/k8stonorman.go index c4a24b58..f1eac873 100644 --- a/pkg/schema/converter/k8stonorman.go +++ b/pkg/schema/converter/k8stonorman.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/rancher/norman/v2/pkg/types" + "github.com/rancher/steve/pkg/schemaserver/types" "github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io/v1beta1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" @@ -24,8 +24,8 @@ func GVRToPluralName(gvr schema.GroupVersionResource) string { return fmt.Sprintf("%s.%s.%s", gvr.Group, gvr.Version, gvr.Resource) } -func ToSchemas(crd v1beta1.CustomResourceDefinitionClient, client discovery.DiscoveryInterface) (map[string]*types.Schema, error) { - result := map[string]*types.Schema{} +func ToSchemas(crd v1beta1.CustomResourceDefinitionClient, client discovery.DiscoveryInterface) (map[string]*types.APISchema, error) { + result := map[string]*types.APISchema{} if err := AddOpenAPI(client, result); err != nil { return nil, err diff --git a/pkg/schema/converter/openapi.go b/pkg/schema/converter/openapi.go index 54add9ff..1960f555 100644 --- a/pkg/schema/converter/openapi.go +++ b/pkg/schema/converter/openapi.go @@ -1,23 +1,24 @@ package converter import ( - "github.com/rancher/norman/v2/pkg/types" - "github.com/rancher/norman/v2/pkg/types/convert" "github.com/rancher/steve/pkg/attributes" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/data/convert" + "github.com/rancher/wrangler/pkg/schemas" "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" "k8s.io/kube-openapi/pkg/util/proto" ) -func modelToSchema(modelName string, k *proto.Kind) *types.Schema { - s := types.Schema{ - ID: modelName, - Type: "schema", - ResourceFields: map[string]types.Field{}, - Attributes: map[string]interface{}{}, - Description: k.GetDescription(), - Dynamic: true, +func modelToSchema(modelName string, k *proto.Kind) *types.APISchema { + s := types.APISchema{ + Schema: &schemas.Schema{ + ID: modelName, + ResourceFields: map[string]schemas.Field{}, + Attributes: map[string]interface{}{}, + Description: k.GetDescription(), + }, } for fieldName, schemaField := range k.Fields { @@ -49,7 +50,7 @@ func modelToSchema(modelName string, k *proto.Kind) *types.Schema { return &s } -func AddOpenAPI(client discovery.DiscoveryInterface, schemas map[string]*types.Schema) error { +func AddOpenAPI(client discovery.DiscoveryInterface, schemas map[string]*types.APISchema) error { openapi, err := client.OpenAPISchema() if err != nil { return err @@ -71,10 +72,9 @@ func AddOpenAPI(client discovery.DiscoveryInterface, schemas map[string]*types.S return nil } -func toField(schema proto.Schema) types.Field { - f := types.Field{ +func toField(schema proto.Schema) schemas.Field { + f := schemas.Field{ Description: schema.GetDescription(), - Nullable: true, Create: true, Update: true, } diff --git a/pkg/schema/converter/openapiv3.go b/pkg/schema/converter/openapiv3.go new file mode 100644 index 00000000..cd49f9d9 --- /dev/null +++ b/pkg/schema/converter/openapiv3.go @@ -0,0 +1,80 @@ +package converter + +import ( + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" +) + +func modelV3ToSchema(name string, k *v1beta1.JSONSchemaProps, schemasMap map[string]*types.APISchema) *types.APISchema { + s := types.APISchema{ + Schema: &schemas.Schema{ + ID: name, + ResourceFields: map[string]schemas.Field{}, + Attributes: map[string]interface{}{}, + Description: k.Description, + }, + } + + for fieldName, schemaField := range k.Properties { + s.ResourceFields[fieldName] = toResourceField(name+"."+fieldName, schemaField, schemasMap) + } + + for _, fieldName := range k.Required { + if f, ok := s.ResourceFields[fieldName]; ok { + f.Required = true + s.ResourceFields[fieldName] = f + } + } + + if _, ok := schemasMap[s.ID]; !ok { + schemasMap[s.ID] = &s + } + + return &s +} + +func toResourceField(name string, schema v1beta1.JSONSchemaProps, schemasMap map[string]*types.APISchema) schemas.Field { + f := schemas.Field{ + Description: schema.Description, + Nullable: true, + Create: true, + Update: true, + } + var itemSchema *v1beta1.JSONSchemaProps + if schema.Items != nil { + if schema.Items.Schema != nil { + itemSchema = schema.Items.Schema + } else if len(schema.Items.JSONSchemas) > 0 { + itemSchema = &schema.Items.JSONSchemas[0] + } + } + + switch schema.Type { + case "array": + if itemSchema == nil { + f.Type = "array[json]" + } else { + f.Type = "array[" + name + "]" + modelV3ToSchema(name, itemSchema, schemasMap) + } + case "object": + if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil { + f.Type = "map[" + name + "]" + modelV3ToSchema(name, schema.AdditionalProperties.Schema, schemasMap) + } else { + f.Type = name + modelV3ToSchema(name, &schema, schemasMap) + } + case "number": + f.Type = "int" + default: + f.Type = schema.Type + } + + if f.Type == "" { + f.Type = "json" + } + + return f +} diff --git a/pkg/schema/defaultmapper.go b/pkg/schema/defaultmapper.go index db75deb5..6765f16e 100644 --- a/pkg/schema/defaultmapper.go +++ b/pkg/schema/defaultmapper.go @@ -3,16 +3,18 @@ package schema import ( "fmt" - "github.com/rancher/norman/v2/pkg/data" - "github.com/rancher/norman/v2/pkg/types" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/data" + "github.com/rancher/wrangler/pkg/schemas" + "github.com/rancher/wrangler/pkg/schemas/mappers" ) -func newDefaultMapper() types.Mapper { +func newDefaultMapper() schemas.Mapper { return &defaultMapper{} } type defaultMapper struct { - types.EmptyMapper + mappers.EmptyMapper } func (d *defaultMapper) FromInternal(data data.Object) { diff --git a/pkg/schema/factory.go b/pkg/schema/factory.go index 6e2443db..b5f05a27 100644 --- a/pkg/schema/factory.go +++ b/pkg/schema/factory.go @@ -4,51 +4,42 @@ import ( "fmt" "net/http" - "github.com/rancher/norman/v2/pkg/api/builtin" - "github.com/rancher/norman/v2/pkg/types" "github.com/rancher/steve/pkg/accesscontrol" "github.com/rancher/steve/pkg/attributes" - "github.com/rancher/steve/pkg/table" + "github.com/rancher/steve/pkg/schema/table" + "github.com/rancher/steve/pkg/schemaserver/builtin" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas" "k8s.io/apiserver/pkg/authentication/user" ) -func newSchemas() (*types.Schemas, error) { - s, err := types.NewSchemas(builtin.Schemas) - if err != nil { +func newSchemas() (*types.APISchemas, error) { + apiSchemas := types.EmptyAPISchemas() + if err := apiSchemas.AddSchemas(builtin.Schemas); err != nil { return nil, err } - s.DefaultMapper = func() types.Mapper { + apiSchemas.InternalSchemas.DefaultMapper = func() schemas.Mapper { return newDefaultMapper() } - return s, nil + return apiSchemas, nil } -func (c *Collection) Schemas(user user.Info) (*types.Schemas, error) { +func (c *Collection) Schemas(user user.Info) (*types.APISchemas, error) { access := c.as.AccessFor(user) return c.schemasForSubject(access) } -func (c *Collection) schemasForSubject(access *accesscontrol.AccessSet) (*types.Schemas, error) { +func (c *Collection) schemasForSubject(access *accesscontrol.AccessSet) (*types.APISchemas, error) { result, err := newSchemas() if err != nil { return nil, err } - if _, err := result.AddSchemas(c.baseSchema); err != nil { + if err := result.AddSchemas(c.baseSchema); err != nil { return nil, err } - for _, template := range c.templates { - if template.RegisterType != nil { - s, err := result.Import(template.RegisterType) - if err != nil { - return nil, err - } - c.applyTemplates(result, s) - } - } - for _, s := range c.schemas { gr := attributes.GR(s) @@ -60,7 +51,7 @@ func (c *Collection) schemasForSubject(access *accesscontrol.AccessSet) (*types. } verbs := attributes.Verbs(s) - verbAccess := accesscontrol.AccessListMap{} + verbAccess := accesscontrol.AccessListByVerb{} for _, verb := range verbs { a := access.AccessListFor(verb, gr) @@ -100,7 +91,7 @@ func (c *Collection) schemasForSubject(access *accesscontrol.AccessSet) (*types. return result, nil } -func (c *Collection) applyTemplates(schemas *types.Schemas, schema *types.Schema) { +func (c *Collection) applyTemplates(schemas *types.APISchemas, schema *types.APISchema) { templates := []*Template{ c.templates[schema.ID], c.templates[fmt.Sprintf("%s/%s", attributes.Group(schema), attributes.Kind(schema))], @@ -112,7 +103,7 @@ func (c *Collection) applyTemplates(schemas *types.Schemas, schema *types.Schema continue } if t.Mapper != nil { - schemas.AddMapper(schema.ID, t.Mapper) + schemas.InternalSchemas.AddMapper(schema.ID, t.Mapper) } if schema.Formatter == nil { schema.Formatter = t.Formatter @@ -128,7 +119,7 @@ func (c *Collection) applyTemplates(schemas *types.Schemas, schema *types.Schema t.Customize(schema) } if len(t.Columns) > 0 { - schemas.AddMapper(schema.ID, table.NewColumns(t.ComputedColumns, t.Columns...)) + schemas.InternalSchemas.AddMapper(schema.ID, table.NewColumns(t.ComputedColumns, t.Columns...)) } } } diff --git a/pkg/schemaserver/builtin/schema.go b/pkg/schemaserver/builtin/schema.go new file mode 100644 index 00000000..3bb9f220 --- /dev/null +++ b/pkg/schemaserver/builtin/schema.go @@ -0,0 +1,88 @@ +package builtin + +import ( + "net/http" + + "github.com/rancher/steve/pkg/schemaserver/store/schema" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas" + "github.com/rancher/wrangler/pkg/slice" +) + +var ( + Schema = types.APISchema{ + Schema: &schemas.Schema{ + ID: "schema", + PluralName: "schemas", + CollectionMethods: []string{"GET"}, + ResourceMethods: []string{"GET"}, + ResourceFields: map[string]schemas.Field{ + "collectionActions": {Type: "map[json]"}, + "collectionFields": {Type: "map[json]"}, + "collectionFilters": {Type: "map[json]"}, + "collectionMethods": {Type: "array[string]"}, + "pluralName": {Type: "string"}, + "resourceActions": {Type: "map[json]"}, + "attributes": {Type: "map[json]"}, + "resourceFields": {Type: "map[json]"}, + "resourceMethods": {Type: "array[string]"}, + "version": {Type: "map[json]"}, + }, + }, + Formatter: SchemaFormatter, + Store: schema.NewSchemaStore(), + } + + Error = types.APISchema{ + Schema: &schemas.Schema{ + ID: "error", + ResourceMethods: []string{}, + CollectionMethods: []string{}, + ResourceFields: map[string]schemas.Field{ + "code": {Type: "string"}, + "detail": {Type: "string", Nullable: true}, + "message": {Type: "string", Nullable: true}, + "fieldName": {Type: "string", Nullable: true}, + "status": {Type: "int"}, + }, + }, + } + + Collection = types.APISchema{ + Schema: &schemas.Schema{ + ID: "collection", + ResourceMethods: []string{}, + CollectionMethods: []string{}, + ResourceFields: map[string]schemas.Field{ + "data": {Type: "array[json]"}, + "pagination": {Type: "map[json]"}, + "sort": {Type: "map[json]"}, + "filters": {Type: "map[json]"}, + }, + }, + } + + Schemas = types.EmptyAPISchemas(). + MustAddSchema(Schema). + MustAddSchema(Error). + MustAddSchema(Collection) +) + +func SchemaFormatter(apiOp *types.APIRequest, resource *types.RawResource) { + schema := apiOp.Schemas.LookupSchema(resource.ID) + if schema == nil { + return + } + + collectionLink := getSchemaCollectionLink(apiOp, schema) + if collectionLink != "" { + resource.Links["collection"] = collectionLink + } +} + +func getSchemaCollectionLink(apiOp *types.APIRequest, schema *types.APISchema) string { + if schema != nil && slice.ContainsString(schema.CollectionMethods, http.MethodGet) { + return apiOp.URLBuilder.Collection(schema) + } + return "" +} diff --git a/pkg/schemaserver/handlers/create.go b/pkg/schemaserver/handlers/create.go new file mode 100644 index 00000000..c992b552 --- /dev/null +++ b/pkg/schemaserver/handlers/create.go @@ -0,0 +1,33 @@ +package handlers + +import ( + "github.com/rancher/steve/pkg/schemaserver/httperror" + "github.com/rancher/steve/pkg/schemaserver/parse" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas/validation" +) + +func CreateHandler(apiOp *types.APIRequest) (types.APIObject, error) { + var err error + + if err := apiOp.AccessControl.CanCreate(apiOp, apiOp.Schema); err != nil { + return types.APIObject{}, err + } + + data, err := parse.Body(apiOp.Request) + if err != nil { + return types.APIObject{}, err + } + + store := apiOp.Schema.Store + if store == nil { + return types.APIObject{}, httperror.NewAPIError(validation.NotFound, "no store found") + } + + data, err = store.Create(apiOp, apiOp.Schema, data) + if err != nil { + return types.APIObject{}, err + } + + return data, nil +} diff --git a/pkg/schemaserver/handlers/delete.go b/pkg/schemaserver/handlers/delete.go new file mode 100644 index 00000000..2ec787ee --- /dev/null +++ b/pkg/schemaserver/handlers/delete.go @@ -0,0 +1,20 @@ +package handlers + +import ( + "github.com/rancher/steve/pkg/schemaserver/httperror" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas/validation" +) + +func DeleteHandler(request *types.APIRequest) (types.APIObject, error) { + if err := request.AccessControl.CanDelete(request, types.APIObject{}, request.Schema); err != nil { + return types.APIObject{}, err + } + + store := request.Schema.Store + if store == nil { + return types.APIObject{}, httperror.NewAPIError(validation.NotFound, "no store found") + } + + return store.Delete(request, request.Schema, request.Name) +} diff --git a/pkg/schemaserver/handlers/error.go b/pkg/schemaserver/handlers/error.go new file mode 100644 index 00000000..ed72f4a6 --- /dev/null +++ b/pkg/schemaserver/handlers/error.go @@ -0,0 +1,65 @@ +package handlers + +import ( + "net/http" + "net/url" + + "github.com/rancher/steve/pkg/schemaserver/httperror" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas/validation" + "github.com/sirupsen/logrus" +) + +func ErrorHandler(request *types.APIRequest, err error) { + if err == validation.ErrComplete { + return + } + + if ec, ok := err.(validation.ErrorCode); ok { + err = httperror.NewAPIError(ec, "") + } + + var error *httperror.APIError + if apiError, ok := err.(*httperror.APIError); ok { + if apiError.Cause != nil { + url, _ := url.PathUnescape(request.Request.URL.String()) + if url == "" { + url = request.Request.URL.String() + } + logrus.Errorf("API error response %v for %v %v. Cause: %v", apiError.Code.Status, request.Request.Method, + url, apiError.Cause) + } + error = apiError + } else { + logrus.Errorf("Unknown error: %v", err) + error = &httperror.APIError{ + Code: validation.ServerError, + Message: err.Error(), + } + } + + if error.Code.Status == http.StatusNoContent { + request.Response.WriteHeader(http.StatusNoContent) + return + } + + data := toError(error) + request.WriteResponse(error.Code.Status, data) +} + +func toError(apiError *httperror.APIError) types.APIObject { + e := map[string]interface{}{ + "type": "error", + "status": apiError.Code.Status, + "code": apiError.Code.Code, + "message": apiError.Message, + } + if apiError.FieldName != "" { + e["fieldName"] = apiError.FieldName + } + + return types.APIObject{ + Type: "error", + Object: e, + } +} diff --git a/pkg/schemaserver/handlers/list.go b/pkg/schemaserver/handlers/list.go new file mode 100644 index 00000000..f0097d37 --- /dev/null +++ b/pkg/schemaserver/handlers/list.go @@ -0,0 +1,53 @@ +package handlers + +import ( + "github.com/rancher/steve/pkg/schemaserver/httperror" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas/validation" +) + +func ByIDHandler(request *types.APIRequest) (types.APIObject, error) { + if err := request.AccessControl.CanGet(request, request.Schema); err != nil { + return types.APIObject{}, err + } + + store := request.Schema.Store + if store == nil { + return types.APIObject{}, httperror.NewAPIError(validation.NotFound, "no store found") + } + + return store.ByID(request, request.Schema, request.Name) +} + +func ListHandler(request *types.APIRequest) (types.APIObjectList, error) { + if request.Name == "" { + if err := request.AccessControl.CanList(request, request.Schema); err != nil { + return types.APIObjectList{}, err + } + } else { + if err := request.AccessControl.CanGet(request, request.Schema); err != nil { + return types.APIObjectList{}, err + } + } + + store := request.Schema.Store + if store == nil { + return types.APIObjectList{}, httperror.NewAPIError(validation.NotFound, "no store found") + } + + if request.Link == "" { + return store.List(request, request.Schema) + } + + _, err := store.ByID(request, request.Schema, request.Name) + if err != nil { + return types.APIObjectList{}, err + } + + if handler, ok := request.Schema.LinkHandlers[request.Link]; ok { + handler.ServeHTTP(request.Response, request.Request) + return types.APIObjectList{}, validation.ErrComplete + } + + return types.APIObjectList{}, validation.NotFound +} diff --git a/pkg/schemaserver/handlers/update.go b/pkg/schemaserver/handlers/update.go new file mode 100644 index 00000000..4831ab7f --- /dev/null +++ b/pkg/schemaserver/handlers/update.go @@ -0,0 +1,39 @@ +package handlers + +import ( + "net/http" + + "github.com/rancher/steve/pkg/schemaserver/httperror" + "github.com/rancher/steve/pkg/schemaserver/parse" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas/validation" +) + +func UpdateHandler(apiOp *types.APIRequest) (types.APIObject, error) { + if err := apiOp.AccessControl.CanUpdate(apiOp, types.APIObject{}, apiOp.Schema); err != nil { + return types.APIObject{}, err + } + + var ( + data types.APIObject + err error + ) + if apiOp.Method != http.MethodPatch { + data, err = parse.Body(apiOp.Request) + if err != nil { + return types.APIObject{}, err + } + } + + store := apiOp.Schema.Store + if store == nil { + return types.APIObject{}, httperror.NewAPIError(validation.NotFound, "no store found") + } + + data, err = store.Update(apiOp, apiOp.Schema, data, apiOp.Name) + if err != nil { + return types.APIObject{}, err + } + + return data, nil +} diff --git a/pkg/schemaserver/httperror/error.go b/pkg/schemaserver/httperror/error.go new file mode 100644 index 00000000..7fefdd1e --- /dev/null +++ b/pkg/schemaserver/httperror/error.go @@ -0,0 +1,70 @@ +package httperror + +import ( + "fmt" + + "github.com/rancher/wrangler/pkg/schemas/validation" +) + +type APIError struct { + Code validation.ErrorCode + Message string + Cause error + FieldName string +} + +func NewAPIError(code validation.ErrorCode, message string) error { + return &APIError{ + Code: code, + Message: message, + } +} + +func NewFieldAPIError(code validation.ErrorCode, fieldName, message string) error { + return &APIError{ + Code: code, + Message: message, + FieldName: fieldName, + } +} + +// WrapFieldAPIError will cause the API framework to log the underlying err before returning the APIError as a response. +// err WILL NOT be in the API response +func WrapFieldAPIError(err error, code validation.ErrorCode, fieldName, message string) error { + return &APIError{ + Cause: err, + Code: code, + Message: message, + FieldName: fieldName, + } +} + +// WrapAPIError will cause the API framework to log the underlying err before returning the APIError as a response. +// err WILL NOT be in the API response +func WrapAPIError(err error, code validation.ErrorCode, message string) error { + return &APIError{ + Code: code, + Message: message, + Cause: err, + } +} + +func (a *APIError) Error() string { + if a.FieldName != "" { + return fmt.Sprintf("%s=%s: %s", a.FieldName, a.Code, a.Message) + } + return fmt.Sprintf("%s: %s", a.Code, a.Message) +} + +func IsAPIError(err error) bool { + _, ok := err.(*APIError) + return ok +} + +func IsConflict(err error) bool { + if apiError, ok := err.(*APIError); ok { + return apiError.Code.Status == 409 + } + + return false +} diff --git a/pkg/schemaserver/parse/browser.go b/pkg/schemaserver/parse/browser.go new file mode 100644 index 00000000..6d195d0e --- /dev/null +++ b/pkg/schemaserver/parse/browser.go @@ -0,0 +1,18 @@ +package parse + +import ( + "net/http" + "strings" +) + +func IsBrowser(req *http.Request, checkAccepts bool) bool { + accepts := strings.ToLower(req.Header.Get("Accept")) + userAgent := strings.ToLower(req.Header.Get("User-Agent")) + + if accepts == "" || !checkAccepts { + accepts = "*/*" + } + + // User agent has Mozilla and browser accepts */* + return strings.Contains(userAgent, "mozilla") && strings.Contains(accepts, "*/*") +} diff --git a/pkg/schemaserver/parse/mux.go b/pkg/schemaserver/parse/mux.go new file mode 100644 index 00000000..d2e38c86 --- /dev/null +++ b/pkg/schemaserver/parse/mux.go @@ -0,0 +1,23 @@ +package parse + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/rancher/steve/pkg/schemaserver/types" +) + +func MuxURLParser(rw http.ResponseWriter, req *http.Request, schemas *types.APISchemas) (ParsedURL, error) { + vars := mux.Vars(req) + url := ParsedURL{ + Type: vars["type"], + Name: vars["name"], + Link: vars["link"], + Prefix: vars["prefix"], + Method: req.Method, + Action: vars["action"], + Query: req.URL.Query(), + } + + return url, nil +} diff --git a/pkg/schemaserver/parse/parse.go b/pkg/schemaserver/parse/parse.go new file mode 100644 index 00000000..6a0181b8 --- /dev/null +++ b/pkg/schemaserver/parse/parse.go @@ -0,0 +1,168 @@ +package parse + +import ( + "net/http" + "net/url" + "strings" + + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/steve/pkg/schemaserver/urlbuilder" +) + +const ( + maxFormSize = 2 * 1 << 20 +) + +var ( + allowedFormats = map[string]bool{ + "html": true, + "json": true, + "yaml": true, + } +) + +type ParsedURL struct { + Type string + Name string + Link string + Method string + Action string + Prefix string + SubContext map[string]string + Query url.Values +} + +type URLParser func(rw http.ResponseWriter, req *http.Request, schemas *types.APISchemas) (ParsedURL, error) + +type Parser func(apiOp *types.APIRequest, urlParser URLParser) error + +func Parse(apiOp *types.APIRequest, urlParser URLParser) error { + var err error + + if apiOp.Request == nil { + apiOp.Request, err = http.NewRequest("GET", "/", nil) + if err != nil { + return err + } + } + + apiOp = types.StoreAPIContext(apiOp) + + if apiOp.Method == "" { + apiOp.Method = parseMethod(apiOp.Request) + } + if apiOp.ResponseFormat == "" { + apiOp.ResponseFormat = parseResponseFormat(apiOp.Request) + } + + // The response format is guaranteed to be set even in the event of an error + parsedURL, err := urlParser(apiOp.Response, apiOp.Request, apiOp.Schemas) + // wait to check error, want to set as much as possible + + if apiOp.Type == "" { + apiOp.Type = parsedURL.Type + } + if apiOp.Name == "" { + apiOp.Name = parsedURL.Name + } + if apiOp.Link == "" { + apiOp.Link = parsedURL.Link + } + if apiOp.Action == "" { + apiOp.Action = parsedURL.Action + } + if apiOp.Query == nil { + apiOp.Query = parsedURL.Query + } + if apiOp.Method == "" && parsedURL.Method != "" { + apiOp.Method = parsedURL.Method + } + if apiOp.URLPrefix == "" { + apiOp.URLPrefix = parsedURL.Prefix + } + + if apiOp.URLBuilder == nil { + // make error local to not override the outer error we have yet to check + var err error + apiOp.URLBuilder, err = urlbuilder.New(apiOp.Request, &urlbuilder.DefaultPathResolver{ + Prefix: apiOp.URLPrefix, + }, apiOp.Schemas) + if err != nil { + return err + } + } + + if err != nil { + return err + } + + if apiOp.Schema == nil && apiOp.Schemas != nil { + apiOp.Schema = apiOp.Schemas.LookupSchema(apiOp.Type) + } + + if apiOp.Schema != nil && apiOp.Type == "" { + apiOp.Type = apiOp.Schema.ID + } + + if err := ValidateMethod(apiOp); err != nil { + return err + } + + return nil +} + +func parseResponseFormat(req *http.Request) string { + format := req.URL.Query().Get("_format") + + if format != "" { + format = strings.TrimSpace(strings.ToLower(format)) + } + + /* Format specified */ + if allowedFormats[format] { + return format + } + + // User agent has Mozilla and browser accepts */* + if IsBrowser(req, true) { + return "html" + } + + if isYaml(req) { + return "yaml" + } + return "json" +} + +func isYaml(req *http.Request) bool { + return strings.Contains(req.Header.Get("Accept"), "application/yaml") +} + +func parseMethod(req *http.Request) string { + method := req.URL.Query().Get("_method") + if method == "" { + method = req.Method + } + return method +} + +func Body(req *http.Request) (types.APIObject, error) { + req.ParseMultipartForm(maxFormSize) + if req.MultipartForm != nil { + return valuesToBody(req.MultipartForm.Value), nil + } + + if req.PostForm != nil && len(req.PostForm) > 0 { + return valuesToBody(map[string][]string(req.Form)), nil + } + + return ReadBody(req) +} + +func valuesToBody(input map[string][]string) types.APIObject { + result := map[string]interface{}{} + for k, v := range input { + result[k] = v + } + return toAPI(result) +} diff --git a/pkg/schemaserver/parse/read_input.go b/pkg/schemaserver/parse/read_input.go new file mode 100644 index 00000000..a3e0d456 --- /dev/null +++ b/pkg/schemaserver/parse/read_input.go @@ -0,0 +1,56 @@ +package parse + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/rancher/steve/pkg/schemaserver/httperror" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/data/convert" + "github.com/rancher/wrangler/pkg/schemas/validation" + "k8s.io/apimachinery/pkg/util/yaml" +) + +const reqMaxSize = (2 * 1 << 20) + 1 + +var bodyMethods = map[string]bool{ + http.MethodPut: true, + http.MethodPost: true, +} + +type Decode func(interface{}) error + +func ReadBody(req *http.Request) (types.APIObject, error) { + if !bodyMethods[req.Method] { + return types.APIObject{}, nil + } + + decode := getDecoder(req, io.LimitReader(req.Body, maxFormSize)) + + data := map[string]interface{}{} + if err := decode(&data); err != nil { + return types.APIObject{}, httperror.NewAPIError(validation.InvalidBodyContent, + fmt.Sprintf("Failed to parse body: %v", err)) + } + + return toAPI(data), nil +} + +func toAPI(data map[string]interface{}) types.APIObject { + return types.APIObject{ + Type: convert.ToString(data["type"]), + ID: convert.ToString(data["id"]), + Object: data, + } +} + +func getDecoder(req *http.Request, reader io.Reader) Decode { + if req.Header.Get("Content-type") == "application/yaml" { + return yaml.NewYAMLToJSONDecoder(reader).Decode + } + decoder := json.NewDecoder(reader) + decoder.UseNumber() + return decoder.Decode +} diff --git a/pkg/schemaserver/parse/validate.go b/pkg/schemaserver/parse/validate.go new file mode 100644 index 00000000..0de2fdb2 --- /dev/null +++ b/pkg/schemaserver/parse/validate.go @@ -0,0 +1,47 @@ +package parse + +import ( + "fmt" + "net/http" + + "github.com/rancher/steve/pkg/schemaserver/httperror" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas/validation" +) + +var ( + supportedMethods = map[string]bool{ + http.MethodPost: true, + http.MethodGet: true, + http.MethodPut: true, + http.MethodPatch: true, + http.MethodDelete: true, + } +) + +func ValidateMethod(request *types.APIRequest) error { + if request.Action != "" && request.Method == http.MethodPost { + return nil + } + + if !supportedMethods[request.Method] { + return httperror.NewAPIError(validation.MethodNotAllowed, fmt.Sprintf("Invalid method %s not supported", request.Method)) + } + + if request.Type == "" || request.Schema == nil || request.Link != "" { + return nil + } + + allowed := request.Schema.ResourceMethods + if request.Name == "" { + allowed = request.Schema.CollectionMethods + } + + for _, method := range allowed { + if method == request.Method { + return nil + } + } + + return httperror.NewAPIError(validation.MethodNotAllowed, fmt.Sprintf("Method %s not supported", request.Method)) +} diff --git a/pkg/schemaserver/server/access.go b/pkg/schemaserver/server/access.go new file mode 100644 index 00000000..e0461e9b --- /dev/null +++ b/pkg/schemaserver/server/access.go @@ -0,0 +1,59 @@ +package server + +import ( + "net/http" + + "github.com/rancher/steve/pkg/schemaserver/httperror" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas/validation" + "github.com/rancher/wrangler/pkg/slice" +) + +type AllAccess struct { +} + +func (*AllAccess) CanCreate(apiOp *types.APIRequest, schema *types.APISchema) error { + if slice.ContainsString(schema.CollectionMethods, http.MethodPost) { + return nil + } + return httperror.NewAPIError(validation.PermissionDenied, "can not create "+schema.ID) +} + +func (*AllAccess) CanGet(apiOp *types.APIRequest, schema *types.APISchema) error { + if slice.ContainsString(schema.ResourceMethods, http.MethodGet) { + return nil + } + return httperror.NewAPIError(validation.PermissionDenied, "can not get "+schema.ID) +} + +func (*AllAccess) CanList(apiOp *types.APIRequest, schema *types.APISchema) error { + if slice.ContainsString(schema.CollectionMethods, http.MethodGet) { + return nil + } + return httperror.NewAPIError(validation.PermissionDenied, "can not list "+schema.ID) +} + +func (*AllAccess) CanUpdate(apiOp *types.APIRequest, obj types.APIObject, schema *types.APISchema) error { + if slice.ContainsString(schema.ResourceMethods, http.MethodPut) { + return nil + } + return httperror.NewAPIError(validation.PermissionDenied, "can not update "+schema.ID) +} + +func (*AllAccess) CanDelete(apiOp *types.APIRequest, obj types.APIObject, schema *types.APISchema) error { + if slice.ContainsString(schema.ResourceMethods, http.MethodDelete) { + return nil + } + return httperror.NewAPIError(validation.PermissionDenied, "can not delete "+schema.ID) +} + +func (a *AllAccess) CanWatch(apiOp *types.APIRequest, schema *types.APISchema) error { + return a.CanList(apiOp, schema) +} + +func (*AllAccess) CanAction(apiOp *types.APIRequest, schema *types.APISchema, name string) error { + if _, ok := schema.ActionHandlers[name]; ok { + return httperror.NewAPIError(validation.PermissionDenied, "no such action "+name) + } + return nil +} diff --git a/pkg/schemaserver/server/server.go b/pkg/schemaserver/server/server.go new file mode 100644 index 00000000..8f1cf784 --- /dev/null +++ b/pkg/schemaserver/server/server.go @@ -0,0 +1,258 @@ +package server + +import ( + "net/http" + + "github.com/rancher/steve/pkg/schemaserver/handlers" + "github.com/rancher/steve/pkg/schemaserver/parse" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/steve/pkg/schemaserver/writer" + "github.com/rancher/wrangler/pkg/merr" + "github.com/rancher/wrangler/pkg/schemas/validation" +) + +type RequestHandler interface { + http.Handler + + GetSchemas() *types.APISchemas + Handle(apiOp *types.APIRequest) +} + +type Server struct { + ResponseWriters map[string]types.ResponseWriter + Schemas *types.APISchemas + Defaults Defaults + AccessControl types.AccessControl + Parser parse.Parser + URLParser parse.URLParser +} + +type Defaults struct { + ByIDHandler types.RequestHandler + ListHandler types.RequestListHandler + CreateHandler types.RequestHandler + DeleteHandler types.RequestHandler + UpdateHandler types.RequestHandler + Store types.Store + ErrorHandler types.ErrorHandler +} + +func DefaultAPIServer() *Server { + s := &Server{ + Schemas: types.EmptyAPISchemas(), + ResponseWriters: map[string]types.ResponseWriter{ + "json": &writer.EncodingResponseWriter{ + ContentType: "application/json", + Encoder: types.JSONEncoder, + }, + "html": &writer.HTMLResponseWriter{ + EncodingResponseWriter: writer.EncodingResponseWriter{ + Encoder: types.JSONEncoder, + ContentType: "application/json", + }, + }, + "yaml": &writer.EncodingResponseWriter{ + ContentType: "application/yaml", + Encoder: types.YAMLEncoder, + }, + }, + AccessControl: &AllAccess{}, + Defaults: Defaults{ + ByIDHandler: handlers.ByIDHandler, + CreateHandler: handlers.CreateHandler, + DeleteHandler: handlers.DeleteHandler, + UpdateHandler: handlers.UpdateHandler, + ListHandler: handlers.ListHandler, + ErrorHandler: handlers.ErrorHandler, + }, + Parser: parse.Parse, + URLParser: parse.MuxURLParser, + } + + return s +} + +func (s *Server) setDefaults(ctx *types.APIRequest) { + if ctx.ResponseWriter == nil { + ctx.ResponseWriter = s.ResponseWriters[ctx.ResponseFormat] + if ctx.ResponseWriter == nil { + ctx.ResponseWriter = s.ResponseWriters["json"] + } + } + + ctx.AccessControl = s.AccessControl + + if ctx.Schemas == nil { + ctx.Schemas = s.Schemas + } +} + +func (s *Server) AddSchemas(schemas *types.APISchemas) error { + var errs []error + + for _, schema := range schemas.Schemas { + if err := s.addSchema(*schema); err != nil { + errs = append(errs, err) + } + } + + return merr.NewErrors(errs...) +} + +func (s *Server) addSchema(schema types.APISchema) error { + s.setupDefaults(&schema) + return s.Schemas.AddSchema(schema) +} + +func (s *Server) setupDefaults(schema *types.APISchema) { + if schema.Store == nil { + schema.Store = s.Defaults.Store + } + + if schema.ListHandler == nil { + schema.ListHandler = s.Defaults.ListHandler + } + + if schema.CreateHandler == nil { + schema.CreateHandler = s.Defaults.CreateHandler + } + + if schema.ByIDHandler == nil { + schema.ByIDHandler = s.Defaults.ByIDHandler + } + + if schema.UpdateHandler == nil { + schema.UpdateHandler = s.Defaults.UpdateHandler + } + + if schema.DeleteHandler == nil { + schema.DeleteHandler = s.Defaults.DeleteHandler + } + + if schema.ErrorHandler == nil { + schema.ErrorHandler = s.Defaults.ErrorHandler + } +} + +func (s *Server) GetSchemas() *types.APISchemas { + return s.Schemas +} + +func (s *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + s.Handle(&types.APIRequest{ + Request: req, + Response: rw, + }) +} + +func (s *Server) Handle(apiOp *types.APIRequest) { + s.handle(apiOp, s.Parser) +} + +func (s *Server) handle(apiOp *types.APIRequest, parser parse.Parser) { + if err := parser(apiOp, parse.MuxURLParser); err != nil { + // ensure defaults set so writer is assigned + s.setDefaults(apiOp) + s.handleError(apiOp, err) + return + } + + s.setDefaults(apiOp) + + if code, data, err := s.handleOp(apiOp); err != nil { + s.handleError(apiOp, err) + } else if obj, ok := data.(types.APIObject); ok { + apiOp.WriteResponse(code, obj) + } else if list, ok := data.(types.APIObjectList); ok { + apiOp.WriteResponseList(code, list) + } +} + +func (s *Server) handleOp(apiOp *types.APIRequest) (int, interface{}, error) { + if err := CheckCSRF(apiOp); err != nil { + return 0, nil, err + } + + action, err := ValidateAction(apiOp) + if err != nil { + return 0, nil, err + } + + if apiOp.Schema == nil { + return http.StatusNotFound, nil, nil + } + + if action != nil { + return http.StatusOK, nil, handleAction(apiOp) + } + + switch apiOp.Method { + case http.MethodGet: + if apiOp.Name == "" { + data, err := handleList(apiOp, apiOp.Schema.ListHandler, s.Defaults.ListHandler) + return http.StatusOK, data, err + } + data, err := handle(apiOp, apiOp.Schema.ByIDHandler, s.Defaults.ByIDHandler) + return http.StatusOK, data, err + case http.MethodPatch: + fallthrough + case http.MethodPut: + data, err := handle(apiOp, apiOp.Schema.UpdateHandler, s.Defaults.UpdateHandler) + return http.StatusOK, data, err + case http.MethodPost: + data, err := handle(apiOp, apiOp.Schema.CreateHandler, s.Defaults.CreateHandler) + return http.StatusCreated, data, err + case http.MethodDelete: + data, err := handle(apiOp, apiOp.Schema.DeleteHandler, s.Defaults.DeleteHandler) + return http.StatusOK, data, err + } + + return http.StatusNotFound, nil, nil +} + +func handleList(apiOp *types.APIRequest, custom types.RequestListHandler, handler types.RequestListHandler) (types.APIObjectList, error) { + if custom != nil { + return custom(apiOp) + } + return handler(apiOp) +} + +func handle(apiOp *types.APIRequest, custom types.RequestHandler, handler types.RequestHandler) (types.APIObject, error) { + if custom != nil { + return custom(apiOp) + } + return handler(apiOp) +} + +func handleAction(context *types.APIRequest) error { + if err := context.AccessControl.CanAction(context, context.Schema, context.Action); err != nil { + return err + } + if handler, ok := context.Schema.ActionHandlers[context.Action]; ok { + handler.ServeHTTP(context.Response, context.Request) + return validation.ErrComplete + } + return nil +} + +func (s *Server) handleError(apiOp *types.APIRequest, err error) { + if apiOp.Schema != nil && apiOp.Schema.ErrorHandler != nil { + apiOp.Schema.ErrorHandler(apiOp, err) + } else if s.Defaults.ErrorHandler != nil { + s.Defaults.ErrorHandler(apiOp, err) + } +} + +func (s *Server) CustomAPIUIResponseWriter(cssURL, jsURL, version writer.StringGetter) { + wi, ok := s.ResponseWriters["html"] + if !ok { + return + } + w, ok := wi.(*writer.HTMLResponseWriter) + if !ok { + return + } + w.CSSURL = cssURL + w.JSURL = jsURL + w.APIUIVersion = version +} diff --git a/pkg/schemaserver/server/validate.go b/pkg/schemaserver/server/validate.go new file mode 100644 index 00000000..2063c803 --- /dev/null +++ b/pkg/schemaserver/server/validate.go @@ -0,0 +1,78 @@ +package server + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "net/http" + + "github.com/rancher/steve/pkg/schemaserver/httperror" + "github.com/rancher/steve/pkg/schemaserver/parse" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas" + "github.com/rancher/wrangler/pkg/schemas/validation" +) + +const ( + csrfCookie = "CSRF" + csrfHeader = "X-API-CSRF" +) + +func ValidateAction(request *types.APIRequest) (*schemas.Action, error) { + if request.Action == "" || request.Link != "" || request.Method != http.MethodPost { + return nil, nil + } + + if err := request.AccessControl.CanAction(request, request.Schema, request.Action); err != nil { + return nil, err + } + + actions := request.Schema.CollectionActions + if request.Name != "" { + actions = request.Schema.ResourceActions + } + + action, ok := actions[request.Action] + if !ok { + return nil, httperror.NewAPIError(validation.InvalidAction, fmt.Sprintf("Invalid action: %s", request.Action)) + } + + return &action, nil +} + +func CheckCSRF(apiOp *types.APIRequest) error { + if !parse.IsBrowser(apiOp.Request, false) { + return nil + } + + cookie, err := apiOp.Request.Cookie(csrfCookie) + if err == http.ErrNoCookie { + bytes := make([]byte, 5) + _, err := rand.Read(bytes) + if err != nil { + return httperror.WrapAPIError(err, validation.ServerError, "Failed in CSRF processing") + } + + cookie = &http.Cookie{ + Name: csrfCookie, + Value: hex.EncodeToString(bytes), + } + } else if err != nil { + return httperror.NewAPIError(validation.InvalidCSRFToken, "Failed to parse cookies") + } else if apiOp.Method != http.MethodGet { + /* + * Very important to use apiOp.Method and not apiOp.Request.Method. The client can override the HTTP method with _method + */ + if cookie.Value == apiOp.Request.Header.Get(csrfHeader) { + // Good + } else if cookie.Value == apiOp.Request.URL.Query().Get(csrfCookie) { + // Good + } else { + return httperror.NewAPIError(validation.InvalidCSRFToken, "Invalid CSRF token") + } + } + + cookie.Path = "/" + http.SetCookie(apiOp.Response, cookie) + return nil +} diff --git a/pkg/schemaserver/store/apiroot/apiroot.go b/pkg/schemaserver/store/apiroot/apiroot.go new file mode 100644 index 00000000..2bc8be7a --- /dev/null +++ b/pkg/schemaserver/store/apiroot/apiroot.go @@ -0,0 +1,135 @@ +package apiroot + +import ( + "net/http" + "strings" + + "github.com/rancher/steve/pkg/schemaserver/store/empty" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas" +) + +func Register(apiSchemas *types.APISchemas, versions, roots []string) { + apiSchemas.MustAddSchema(types.APISchema{ + Schema: &schemas.Schema{ + ID: "apiRoot", + CollectionMethods: []string{"GET"}, + ResourceMethods: []string{"GET"}, + ResourceFields: map[string]schemas.Field{ + "apiVersion": {Type: "map[json]"}, + "path": {Type: "string"}, + }, + }, + Formatter: Formatter, + Store: NewAPIRootStore(versions, roots), + }) +} + +func Formatter(apiOp *types.APIRequest, resource *types.RawResource) { + data := resource.APIObject.Data() + path, _ := data["path"].(string) + if path == "" { + return + } + delete(data, "path") + + resource.Links["root"] = apiOp.URLBuilder.RelativeToRoot(path) + + if data, isAPIRoot := data["apiVersion"].(map[string]interface{}); isAPIRoot { + apiVersion := apiVersionFromMap(apiOp.Schemas, data) + resource.Links["self"] = apiOp.URLBuilder.RelativeToRoot(apiVersion) + + resource.Links["schemas"] = apiOp.URLBuilder.RelativeToRoot(path) + for _, schema := range apiOp.Schemas.Schemas { + addCollectionLink(apiOp, schema, resource.Links) + } + } + + return +} + +func addCollectionLink(apiOp *types.APIRequest, schema *types.APISchema, links map[string]string) { + collectionLink := getSchemaCollectionLink(apiOp, schema) + if collectionLink != "" { + links[schema.PluralName] = collectionLink + } +} + +func getSchemaCollectionLink(apiOp *types.APIRequest, schema *types.APISchema) string { + if schema != nil && contains(schema.CollectionMethods, http.MethodGet) { + return apiOp.URLBuilder.Collection(schema) + } + return "" +} + +type Store struct { + empty.Store + roots []string + versions []string +} + +func NewAPIRootStore(versions []string, roots []string) types.Store { + return &Store{ + roots: roots, + versions: versions, + } +} + +func (a *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + return types.DefaultByID(a, apiOp, schema, id) +} + +func (a *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) { + var roots types.APIObjectList + + versions := a.versions + + for _, version := range versions { + roots.Objects = append(roots.Objects, types.APIObject{ + Type: "apiRoot", + ID: version, + Object: apiVersionToAPIRootMap(version), + }) + } + + for _, root := range a.roots { + parts := strings.SplitN(root, ":", 2) + if len(parts) == 2 { + roots.Objects = append(roots.Objects, types.APIObject{ + Type: "apiRoot", + ID: parts[0], + Object: map[string]interface{}{ + "id": parts[0], + "path": parts[1], + }, + }) + } + } + + return roots, nil +} + +func apiVersionToAPIRootMap(version string) map[string]interface{} { + return map[string]interface{}{ + "id": version, + "type": "apiRoot", + "apiVersion": map[string]interface{}{ + "version": version, + }, + "path": "/" + version, + } +} + +func apiVersionFromMap(schemas *types.APISchemas, apiVersion map[string]interface{}) string { + version, _ := apiVersion["version"].(string) + return version +} + +func contains(list []string, needle string) bool { + for _, v := range list { + if v == needle { + return true + } + } + return false +} diff --git a/pkg/schemaserver/store/empty/empty_store.go b/pkg/schemaserver/store/empty/empty_store.go new file mode 100644 index 00000000..47d49a2a --- /dev/null +++ b/pkg/schemaserver/store/empty/empty_store.go @@ -0,0 +1,33 @@ +package empty + +import ( + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas/validation" +) + +type Store struct { +} + +func (e *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + return types.APIObject{}, validation.NotFound +} + +func (e *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + return types.APIObject{}, validation.NotFound +} + +func (e *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) { + return types.APIObjectList{}, validation.NotFound +} + +func (e *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (types.APIObject, error) { + return types.APIObject{}, validation.NotFound +} + +func (e *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) { + return types.APIObject{}, validation.NotFound +} + +func (e *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) { + return nil, nil +} diff --git a/pkg/schemaserver/store/schema/schema_store.go b/pkg/schemaserver/store/schema/schema_store.go new file mode 100644 index 00000000..0cdf01a8 --- /dev/null +++ b/pkg/schemaserver/store/schema/schema_store.go @@ -0,0 +1,100 @@ +package schema + +import ( + "github.com/rancher/wrangler/pkg/schemas/validation" + + "github.com/rancher/steve/pkg/schemaserver/httperror" + "github.com/rancher/steve/pkg/schemaserver/store/empty" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas/definition" +) + +type Store struct { + empty.Store +} + +func NewSchemaStore() types.Store { + return &Store{} +} + +func toAPIObject(schema *types.APISchema) types.APIObject { + return types.APIObject{ + Type: "schema", + ID: schema.ID, + Object: schema, + } +} + +func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + schema = apiOp.Schemas.LookupSchema(id) + if schema == nil { + return types.APIObject{}, httperror.NewAPIError(validation.NotFound, "no such schema") + } + return toAPIObject(schema), nil +} + +func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) { + schemaMap := apiOp.Schemas.Schemas + schemas := types.APIObjectList{} + + included := map[string]bool{} + for _, schema := range schemaMap { + if included[schema.ID] { + continue + } + + if apiOp.AccessControl.CanList(apiOp, schema) == nil || apiOp.AccessControl.CanGet(apiOp, schema) == nil { + schemas = s.addSchema(apiOp, schema, schemaMap, schemas, included) + } + } + + return schemas, nil +} + +func (s *Store) addSchema(apiOp *types.APIRequest, schema *types.APISchema, schemaMap map[string]*types.APISchema, schemas types.APIObjectList, included map[string]bool) types.APIObjectList { + included[schema.ID] = true + schemas = s.traverseAndAdd(apiOp, schema, schemaMap, schemas, included) + schemas.Objects = append(schemas.Objects, toAPIObject(schema)) + return schemas +} + +func (s *Store) traverseAndAdd(apiOp *types.APIRequest, schema *types.APISchema, schemaMap map[string]*types.APISchema, schemas types.APIObjectList, included map[string]bool) types.APIObjectList { + for _, field := range schema.ResourceFields { + t := "" + subType := field.Type + for subType != t { + t = subType + subType = definition.SubType(t) + } + + if refSchema, ok := schemaMap[t]; ok && !included[t] { + schemas = s.addSchema(apiOp, refSchema, schemaMap, schemas, included) + } + } + + for _, action := range schema.ResourceActions { + for _, t := range []string{action.Output, action.Input} { + if t == "" { + continue + } + + if refSchema, ok := schemaMap[t]; ok && !included[t] { + schemas = s.addSchema(apiOp, refSchema, schemaMap, schemas, included) + } + } + } + + for _, action := range schema.CollectionActions { + for _, t := range []string{action.Output, action.Input} { + if t == "" { + continue + } + + if refSchema, ok := schemaMap[t]; ok && !included[t] { + schemas = s.addSchema(apiOp, refSchema, schemaMap, schemas, included) + } + } + } + + return schemas +} diff --git a/pkg/schemaserver/subscribe/convert.go b/pkg/schemaserver/subscribe/convert.go new file mode 100644 index 00000000..06c3fd57 --- /dev/null +++ b/pkg/schemaserver/subscribe/convert.go @@ -0,0 +1,53 @@ +package subscribe + +import ( + "io" + + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/steve/pkg/schemaserver/writer" +) + +type Converter struct { + writer.EncodingResponseWriter + apiOp *types.APIRequest + obj interface{} +} + +func MarshallObject(apiOp *types.APIRequest, event types.APIEvent) types.APIEvent { + if event.Error != nil { + return event + } + + data, err := newConverter(apiOp).ToAPIObject(event.Object) + if err != nil { + event.Error = err + return event + } + + event.Data = data + return event +} + +func newConverter(apiOp *types.APIRequest) *Converter { + c := &Converter{ + apiOp: apiOp, + } + c.EncodingResponseWriter = writer.EncodingResponseWriter{ + ContentType: "application/json", + Encoder: c.Encoder, + } + return c +} + +func (c *Converter) ToAPIObject(data types.APIObject) (interface{}, error) { + c.obj = nil + if err := c.Body(c.apiOp, nil, data); err != nil { + return types.APIObject{}, err + } + return c.obj, nil +} + +func (c *Converter) Encoder(_ io.Writer, obj interface{}) error { + c.obj = obj + return nil +} diff --git a/pkg/schemaserver/subscribe/handler.go b/pkg/schemaserver/subscribe/handler.go new file mode 100644 index 00000000..728ea9ae --- /dev/null +++ b/pkg/schemaserver/subscribe/handler.go @@ -0,0 +1,80 @@ +package subscribe + +import ( + "encoding/json" + "time" + + "github.com/rancher/wrangler/pkg/schemas/validation" + + "github.com/gorilla/websocket" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/sirupsen/logrus" +) + +var upgrader = websocket.Upgrader{ + HandshakeTimeout: 60 * time.Second, + EnableCompression: true, +} + +type Subscribe struct { + Stop bool `json:"stop,omitempty"` + ResourceType string `json:"resourceType,omitempty"` + ResourceVersion string `json:"resourceVersion,omitempty"` +} + +func Handler(apiOp *types.APIRequest) (types.APIObjectList, error) { + err := handler(apiOp) + if err != nil { + logrus.Errorf("Error during subscribe %v", err) + } + return types.APIObjectList{}, validation.ErrComplete +} + +func handler(apiOp *types.APIRequest) error { + c, err := upgrader.Upgrade(apiOp.Response, apiOp.Request, nil) + if err != nil { + return err + } + defer c.Close() + + watches := NewWatchSession(apiOp) + defer watches.Close() + + events := watches.Watch(c) + t := time.NewTicker(30 * time.Second) + defer t.Stop() + + for { + select { + case event, ok := <-events: + if !ok { + return nil + } + if err := writeData(apiOp, c, event); err != nil { + return err + } + case <-t.C: + if err := writeData(apiOp, c, types.APIEvent{Name: "ping"}); err != nil { + return err + } + } + } +} + +func writeData(apiOp *types.APIRequest, c *websocket.Conn, event types.APIEvent) error { + event = MarshallObject(apiOp, event) + if event.Error != nil { + event.Name = "resource.error" + event.Data = map[string]interface{}{ + "error": event.Error.Error(), + } + } + + messageWriter, err := c.NextWriter(websocket.TextMessage) + if err != nil { + return err + } + defer messageWriter.Close() + + return json.NewEncoder(messageWriter).Encode(event) +} diff --git a/pkg/schemaserver/subscribe/register.go b/pkg/schemaserver/subscribe/register.go new file mode 100644 index 00000000..458deb4b --- /dev/null +++ b/pkg/schemaserver/subscribe/register.go @@ -0,0 +1,16 @@ +package subscribe + +import ( + "net/http" + + "github.com/rancher/steve/pkg/schemaserver/types" +) + +func Register(schemas *types.APISchemas) { + schemas.MustImportAndCustomize(Subscribe{}, func(schema *types.APISchema) { + schema.CollectionMethods = []string{http.MethodGet} + schema.ResourceMethods = []string{} + schema.ListHandler = Handler + schema.PluralName = "subscribe" + }) +} diff --git a/pkg/schemaserver/subscribe/watcher.go b/pkg/schemaserver/subscribe/watcher.go new file mode 100644 index 00000000..1bedc923 --- /dev/null +++ b/pkg/schemaserver/subscribe/watcher.go @@ -0,0 +1,140 @@ +package subscribe + +import ( + "context" + "encoding/json" + "fmt" + "sync" + + "github.com/gorilla/websocket" + "github.com/rancher/steve/pkg/schemaserver/types" +) + +type WatchSession struct { + sync.Mutex + + apiOp *types.APIRequest + watchers map[string]func() + wg sync.WaitGroup + ctx context.Context + cancel func() +} + +func (s *WatchSession) stop(id string, resp chan<- types.APIEvent) { + s.Lock() + defer s.Unlock() + if cancel, ok := s.watchers[id]; ok { + cancel() + resp <- types.APIEvent{ + Name: "resource.stop", + ResourceType: id, + } + } + delete(s.watchers, id) +} + +func (s *WatchSession) add(resourceType, revision string, resp chan<- types.APIEvent) { + s.Lock() + defer s.Unlock() + + ctx, cancel := context.WithCancel(s.ctx) + s.watchers[resourceType] = cancel + + s.wg.Add(1) + go func() { + defer s.wg.Done() + defer s.stop(resourceType, resp) + + if err := s.stream(ctx, resourceType, revision, resp); err != nil { + sendErr(resp, err, resourceType) + } + }() +} + +func (s *WatchSession) stream(ctx context.Context, resourceType, revision string, result chan<- types.APIEvent) error { + schema := s.apiOp.Schemas.LookupSchema(resourceType) + if schema == nil { + return fmt.Errorf("failed to find schema %s", resourceType) + } else if schema.Store == nil { + return fmt.Errorf("schema %s does not support watching", resourceType) + } + + if err := s.apiOp.AccessControl.CanWatch(s.apiOp, schema); err != nil { + return err + } + + c, err := schema.Store.Watch(s.apiOp.WithContext(ctx), schema, types.WatchRequest{Revision: revision}) + if err != nil { + return err + } + + result <- types.APIEvent{ + Name: "resource.start", + ResourceType: resourceType, + } + + for event := range c { + result <- event + } + + return nil +} + +func NewWatchSession(apiOp *types.APIRequest) *WatchSession { + ws := &WatchSession{ + apiOp: apiOp, + watchers: map[string]func(){}, + } + + ws.ctx, ws.cancel = context.WithCancel(apiOp.Request.Context()) + return ws +} + +func (s *WatchSession) Watch(conn *websocket.Conn) <-chan types.APIEvent { + result := make(chan types.APIEvent, 100) + go func() { + defer close(result) + + if err := s.watch(conn, result); err != nil { + sendErr(result, err, "") + } + }() + return result +} + +func (s *WatchSession) Close() { + s.cancel() + s.wg.Wait() +} + +func (s *WatchSession) watch(conn *websocket.Conn, resp chan types.APIEvent) error { + defer s.wg.Wait() + defer s.cancel() + + for { + _, r, err := conn.NextReader() + if err != nil { + return err + } + + var sub Subscribe + + if err := json.NewDecoder(r).Decode(&sub); err != nil { + sendErr(resp, err, "") + continue + } + + if sub.Stop { + s.stop(sub.ResourceType, resp) + } else if _, ok := s.watchers[sub.ResourceType]; !ok { + s.add(sub.ResourceType, sub.ResourceVersion, resp) + } + } +} + +func sendErr(resp chan<- types.APIEvent, err error, resourceType string) { + resp <- types.APIEvent{ + ResourceType: resourceType, + Error: err, + } +} diff --git a/pkg/schemaserver/types/encoder.go b/pkg/schemaserver/types/encoder.go new file mode 100644 index 00000000..0dcf5626 --- /dev/null +++ b/pkg/schemaserver/types/encoder.go @@ -0,0 +1,25 @@ +package types + +import ( + "encoding/json" + "io" + + "github.com/ghodss/yaml" +) + +func JSONEncoder(writer io.Writer, v interface{}) error { + return json.NewEncoder(writer).Encode(v) +} + +func YAMLEncoder(writer io.Writer, v interface{}) error { + data, err := json.Marshal(v) + if err != nil { + return err + } + buf, err := yaml.JSONToYAML(data) + if err != nil { + return err + } + _, err = writer.Write(buf) + return err +} diff --git a/pkg/schemaserver/types/schemas.go b/pkg/schemaserver/types/schemas.go new file mode 100644 index 00000000..182c4644 --- /dev/null +++ b/pkg/schemaserver/types/schemas.go @@ -0,0 +1,70 @@ +package types + +import ( + "strings" + + "github.com/rancher/wrangler/pkg/schemas" + "github.com/sirupsen/logrus" +) + +type APISchemas struct { + InternalSchemas *schemas.Schemas + Schemas map[string]*APISchema + index map[string]*APISchema +} + +func EmptyAPISchemas() *APISchemas { + return &APISchemas{ + InternalSchemas: schemas.EmptySchemas(), + Schemas: map[string]*APISchema{}, + index:map[string]*APISchema{}, + } +} + +func (a *APISchemas) MustAddSchema(obj APISchema) *APISchemas { + err := a.AddSchema(obj) + if err != nil { + logrus.Fatalf("failed to add schema: %v", err) + } + return a +} + +func (a *APISchemas) MustImportAndCustomize(obj interface{}, f func(*APISchema)) { + schema, err := a.InternalSchemas.Import(obj) + if err != nil { + panic(err) + } + apiSchema := &APISchema{ + Schema: schema, + } + a.Schemas[schema.ID] = apiSchema + f(apiSchema) +} + +func (a *APISchemas) AddSchemas(schema *APISchemas) error { + for _, schema := range schema.Schemas { + if err := a.AddSchema(*schema); err != nil { + return err + } + } + return nil +} + +func (a *APISchemas) AddSchema(schema APISchema) error { + if err := a.InternalSchemas.AddSchema(*schema.Schema); err != nil { + return err + } + schema.Schema = a.InternalSchemas.Schema(schema.ID) + a.Schemas[schema.ID] = &schema + a.index[strings.ToLower(schema.ID)] = &schema + a.index[strings.ToLower(schema.PluralName)] = &schema + return nil +} + +func (a *APISchemas) LookupSchema(name string) *APISchema { + s, ok := a.Schemas[name] + if ok { + return s + } + return a.index[strings.ToLower(name)] +} diff --git a/pkg/schemaserver/types/server_types.go b/pkg/schemaserver/types/server_types.go new file mode 100644 index 00000000..a57154d5 --- /dev/null +++ b/pkg/schemaserver/types/server_types.go @@ -0,0 +1,278 @@ +package types + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + + "github.com/rancher/wrangler/pkg/data" + "github.com/rancher/wrangler/pkg/data/convert" + "github.com/rancher/wrangler/pkg/schemas/validation" + meta2 "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" +) + +type RawResource struct { + ID string `json:"id,omitempty" yaml:"id,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Schema *APISchema `json:"-" yaml:"-"` + Links map[string]string `json:"links" yaml:"links,omitempty"` + Actions map[string]string `json:"actions,omitempty" yaml:"actions,omitempty"` + ActionLinks bool `json:"-" yaml:"-"` + APIObject APIObject `json:"-" yaml:"-"` +} + +func (r *RawResource) MarshalJSON() ([]byte, error) { + type r_ RawResource + outer, err := json.Marshal((*r_)(r)) + if err != nil { + return nil, err + } + + last := len(outer) - 1 + if len(outer) < 2 || outer[last] != '}' { + return outer, nil + } + + data, err := json.Marshal(r.APIObject.Object) + if err != nil { + return nil, err + } + + if len(data) < 2 || data[0] != '{' || data[len(data)-1] != '}' { + return outer, nil + } + + if outer[last-1] == '{' { + outer[last] = ' ' + } else { + outer[last] = ',' + } + + return append(outer, data[1:]...), nil +} + +func (r *RawResource) AddAction(apiOp *APIRequest, name string) { + r.Actions[name] = apiOp.URLBuilder.Action(r.Schema, r.ID, name) +} + +type RequestHandler func(request *APIRequest) (APIObject, error) + +type RequestListHandler func(request *APIRequest) (APIObjectList, error) + +type Formatter func(request *APIRequest, resource *RawResource) + +type CollectionFormatter func(request *APIRequest, collection *GenericCollection) + +type ErrorHandler func(request *APIRequest, err error) + +type ResponseWriter interface { + Write(apiOp *APIRequest, code int, obj APIObject) + WriteList(apiOp *APIRequest, code int, obj APIObjectList) +} + +type AccessControl interface { + CanAction(apiOp *APIRequest, schema *APISchema, name string) error + CanCreate(apiOp *APIRequest, schema *APISchema) error + CanList(apiOp *APIRequest, schema *APISchema) error + CanGet(apiOp *APIRequest, schema *APISchema) error + CanUpdate(apiOp *APIRequest, obj APIObject, schema *APISchema) error + CanDelete(apiOp *APIRequest, obj APIObject, schema *APISchema) error + CanWatch(apiOp *APIRequest, schema *APISchema) error +} + +type APIRequest struct { + Action string + Name string + Type string + Link string + Method string + Namespace string + Schema *APISchema + Schemas *APISchemas + Query url.Values + ResponseFormat string + ResponseWriter ResponseWriter + URLPrefix string + URLBuilder URLBuilder + AccessControl AccessControl + + Request *http.Request + Response http.ResponseWriter +} + +type apiOpKey struct{} + +func GetAPIContext(ctx context.Context) *APIRequest { + apiOp, _ := ctx.Value(apiOpKey{}).(*APIRequest) + return apiOp +} + +func StoreAPIContext(apiOp *APIRequest) *APIRequest { + ctx := context.WithValue(apiOp.Request.Context(), apiOpKey{}, apiOp) + apiOp.Request = apiOp.Request.WithContext(ctx) + return apiOp +} + +func (r *APIRequest) WithContext(ctx context.Context) *APIRequest { + result := *r + result.Request = result.Request.WithContext(ctx) + return &result +} + +func (r *APIRequest) Context() context.Context { + return r.Request.Context() +} + +func (r *APIRequest) GetUser() string { + user, ok := request.UserFrom(r.Request.Context()) + if ok { + return user.GetName() + } + return "" +} + +func (r *APIRequest) GetUserInfo() (user.Info, bool) { + return request.UserFrom(r.Request.Context()) +} + +func (r *APIRequest) Option(key string) string { + return r.Query.Get("_" + key) +} + +func (r *APIRequest) WriteResponse(code int, obj APIObject) { + r.ResponseWriter.Write(r, code, obj) +} + +func (r *APIRequest) WriteResponseList(code int, list APIObjectList) { + r.ResponseWriter.WriteList(r, code, list) +} + +type URLBuilder interface { + Current() string + + Collection(schema *APISchema) string + CollectionAction(schema *APISchema, action string) string + ResourceLink(schema *APISchema, id string) string + Link(schema *APISchema, id string, linkName string) string + Action(schema *APISchema, id string, action string) string + + RelativeToRoot(path string) string +} + +type Store interface { + ByID(apiOp *APIRequest, schema *APISchema, id string) (APIObject, error) + List(apiOp *APIRequest, schema *APISchema) (APIObjectList, error) + Create(apiOp *APIRequest, schema *APISchema, data APIObject) (APIObject, error) + Update(apiOp *APIRequest, schema *APISchema, data APIObject, id string) (APIObject, error) + Delete(apiOp *APIRequest, schema *APISchema, id string) (APIObject, error) + Watch(apiOp *APIRequest, schema *APISchema, w WatchRequest) (chan APIEvent, error) +} + +func DefaultByID(store Store, apiOp *APIRequest, schema *APISchema, id string) (APIObject, error) { + list, err := store.List(apiOp, schema) + if err != nil { + return APIObject{}, err + } + + for _, item := range list.Objects { + if item.ID == id { + return item, nil + } + } + + return APIObject{}, validation.NotFound +} + +type WatchRequest struct { + Revision string +} + +var ( + ChangeAPIEvent = "resource.change" + RemoveAPIEvent = "resource.remove" + CreateAPIEvent = "resource.create" +) + +type APIEvent struct { + Name string `json:"name,omitempty"` + ResourceType string `json:"resourceType,omitempty"` + Revision string `json:"revision,omitempty"` + Object APIObject `json:"-"` + Error error `json:"-"` + // Data is the output format of the object + Data interface{} `json:"data,omitempty"` +} + +type APIObject struct { + Type string + ID string + Object interface{} +} + +type APIObjectList struct { + Revision string + Continue string + Objects []APIObject +} + +func (a *APIObject) Data() data.Object { + data, err := convert.EncodeToMap(a.Object) + if err != nil { + return convert.ToMapInterface(a.Object) + } + return data +} + +func (a *APIObject) Name() string { + if ro, ok := a.Object.(runtime.Object); ok { + meta, err := meta2.Accessor(ro) + if err == nil { + return meta.GetName() + } + } + return Name(a.Data()) +} + +func (a *APIObject) Namespace() string { + if ro, ok := a.Object.(runtime.Object); ok { + meta, err := meta2.Accessor(ro) + if err == nil { + return meta.GetNamespace() + } + } + return Namespace(a.Data()) +} + +func Name(d map[string]interface{}) string { + return convert.ToString(data.GetValueN(d, "metadata", "name")) +} + +func Namespace(d map[string]interface{}) string { + return convert.ToString(data.GetValueN(d, "metadata", "namespace")) +} + +func APIChan(c <-chan APIEvent, f func(APIObject) APIObject) chan APIEvent { + if c == nil { + return nil + } + result := make(chan APIEvent) + go func() { + for data := range c { + data.Object = f(data.Object) + result <- data + } + close(result) + }() + return result +} + +func FormatterChain(formatter Formatter, next Formatter) Formatter { + return func(request *APIRequest, resource *RawResource) { + formatter(request, resource) + next(request, resource) + } +} diff --git a/pkg/schemaserver/types/types.go b/pkg/schemaserver/types/types.go new file mode 100644 index 00000000..22191434 --- /dev/null +++ b/pkg/schemaserver/types/types.go @@ -0,0 +1,86 @@ +package types + +import ( + "net/http" + + "github.com/rancher/wrangler/pkg/schemas" +) + +const ( + ResourceFieldID = "id" +) + +type Collection struct { + Type string `json:"type,omitempty"` + Links map[string]string `json:"links"` + CreateTypes map[string]string `json:"createTypes,omitempty"` + Actions map[string]string `json:"actions"` + ResourceType string `json:"resourceType"` + Revision string `json:"revision,omitempty"` + Continue string `json:"continue,omitempty"` +} + +type GenericCollection struct { + Collection + Data []*RawResource `json:"data"` +} + +var ( + ModifierEQ ModifierType = "eq" + ModifierNE ModifierType = "ne" + ModifierNull ModifierType = "null" + ModifierNotNull ModifierType = "notnull" + ModifierIn ModifierType = "in" + ModifierNotIn ModifierType = "notin" +) + +type ModifierType string + +type Condition struct { + Modifier ModifierType `json:"modifier,omitempty"` + Value interface{} `json:"value,omitempty"` +} + +type Resource struct { + ID string `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Links map[string]string `json:"links"` + Actions map[string]string `json:"actions"` +} + +type NamedResource struct { + Resource + Name string `json:"name"` + Description string `json:"description"` +} + +type NamedResourceCollection struct { + Collection + Data []NamedResource `json:"data,omitempty"` +} + +type APISchema struct { + *schemas.Schema + + ActionHandlers map[string]http.Handler `json:"-"` + LinkHandlers map[string]http.Handler `json:"-"` + ListHandler RequestListHandler `json:"-"` + ByIDHandler RequestHandler `json:"-"` + CreateHandler RequestHandler `json:"-"` + DeleteHandler RequestHandler `json:"-"` + UpdateHandler RequestHandler `json:"-"` + Formatter Formatter `json:"-"` + CollectionFormatter CollectionFormatter `json:"-"` + ErrorHandler ErrorHandler `json:"-"` + Store Store `json:"-"` +} + +func (a *APISchema) DeepCopy() *APISchema { + r := *a + r.Schema = r.Schema.DeepCopy() + return &r +} + +func (c *Collection) AddAction(apiOp *APIRequest, name string) { + c.Actions[name] = apiOp.URLBuilder.CollectionAction(apiOp.Schema, name) +} diff --git a/pkg/schemaserver/urlbuilder/base.go b/pkg/schemaserver/urlbuilder/base.go new file mode 100644 index 00000000..23872d04 --- /dev/null +++ b/pkg/schemaserver/urlbuilder/base.go @@ -0,0 +1,84 @@ +package urlbuilder + +import ( + "bytes" + "fmt" + "net" + "net/http" + "net/url" + "strings" +) + +func ParseRequestURL(r *http.Request) string { + scheme := GetScheme(r) + host := GetHost(r, scheme) + return fmt.Sprintf("%s://%s%s%s", scheme, host, r.Header.Get(PrefixHeader), r.URL.Path) +} + +func GetHost(r *http.Request, scheme string) string { + host := r.Header.Get(ForwardedAPIHostHeader) + if host == "" { + host = strings.Split(r.Header.Get(ForwardedHostHeader), ",")[0] + } + if host == "" { + host = r.Host + } + + port := r.Header.Get(ForwardedPortHeader) + if port == "" { + return host + } + + if port == "80" && scheme == "http" { + return host + } + + if port == "443" && scheme == "http" { + return host + } + + hostname, _, err := net.SplitHostPort(host) + if err != nil { + return host + } + + return strings.Join([]string{hostname, port}, ":") +} + +func GetScheme(r *http.Request) string { + scheme := r.Header.Get(ForwardedProtoHeader) + if scheme != "" { + switch scheme { + case "ws": + return "http" + case "wss": + return "https" + default: + return scheme + } + } else if r.TLS != nil { + return "https" + } + return "http" +} + +func ParseResponseURLBase(currentURL string, r *http.Request) (string, error) { + path := r.URL.Path + + index := strings.LastIndex(currentURL, path) + if index == -1 { + // Fallback, if we can't find path in currentURL, then we just assume the base is the root of the web request + u, err := url.Parse(currentURL) + if err != nil { + return "", err + } + + buffer := bytes.Buffer{} + buffer.WriteString(u.Scheme) + buffer.WriteString("://") + buffer.WriteString(u.Host) + return buffer.String(), nil + } + + return currentURL[0:index], nil +} diff --git a/pkg/schemaserver/urlbuilder/url.go b/pkg/schemaserver/urlbuilder/url.go new file mode 100644 index 00000000..86a810b6 --- /dev/null +++ b/pkg/schemaserver/urlbuilder/url.go @@ -0,0 +1,124 @@ +package urlbuilder + +import ( + "net/http" + "net/url" + "path" + "strings" + + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/name" +) + +const ( + PrefixHeader = "X-API-URL-Prefix" + ForwardedAPIHostHeader = "X-API-Host" + ForwardedHostHeader = "X-Forwarded-Host" + ForwardedProtoHeader = "X-Forwarded-Proto" + ForwardedPortHeader = "X-Forwarded-Port" +) + +func NewPrefixed(r *http.Request, schemas *types.APISchemas, prefix string) (types.URLBuilder, error) { + return New(r, &DefaultPathResolver{ + Prefix: prefix, + }, schemas) +} + +func New(r *http.Request, resolver PathResolver, schemas *types.APISchemas) (types.URLBuilder, error) { + requestURL := ParseRequestURL(r) + responseURLBase, err := ParseResponseURLBase(requestURL, r) + if err != nil { + return nil, err + } + + builder := &DefaultURLBuilder{ + schemas: schemas, + currentURL: requestURL, + responseURLBase: responseURLBase, + pathResolver: resolver, + query: r.URL.Query(), + } + + return builder, nil +} + +type PathResolver interface { + Schema(base string, schema *types.APISchema) string +} + +type DefaultPathResolver struct { + Prefix string +} + +func (d *DefaultPathResolver) Schema(base string, schema *types.APISchema) string { + return ConstructBasicURL(base, d.Prefix, schema.PluralName) +} + +type DefaultURLBuilder struct { + pathResolver PathResolver + schemas *types.APISchemas + currentURL string + responseURLBase string + query url.Values +} + +func (u *DefaultURLBuilder) Link(schema *types.APISchema, id string, linkName string) string { + return u.schemaURL(schema, id, linkName) +} + +func (u *DefaultURLBuilder) ResourceLink(schema *types.APISchema, id string) string { + return u.schemaURL(schema, id) +} + +func (u *DefaultURLBuilder) Current() string { + return u.currentURL +} + +func (u *DefaultURLBuilder) RelativeToRoot(path string) string { + if len(path) > 0 && path[0] != '/' { + return u.responseURLBase + "/" + path + } + return u.responseURLBase + path +} + +func (u *DefaultURLBuilder) Collection(schema *types.APISchema) string { + return u.schemaURL(schema) +} + +func (u *DefaultURLBuilder) schemaURL(schema *types.APISchema, parts ...string) string { + base := []string{ + u.pathResolver.Schema(u.responseURLBase, schema), + } + return ConstructBasicURL(append(base, parts...)...) +} + +func ConstructBasicURL(parts ...string) string { + switch len(parts) { + case 0: + return "" + case 1: + return parts[0] + default: + base := parts[0] + rest := path.Join(parts[1:]...) + if !strings.HasSuffix(base, "/") && !strings.HasPrefix(rest, "/") { + return base + "/" + rest + } + return base + rest + } +} + +func (u *DefaultURLBuilder) getPluralName(schema *types.APISchema) string { + if schema.PluralName == "" { + return strings.ToLower(name.GuessPluralName(schema.ID)) + } + return strings.ToLower(schema.PluralName) +} + +func (u *DefaultURLBuilder) Action(schema *types.APISchema, id, action string) string { + return u.schemaURL(schema, id) + "?action=" + url.QueryEscape(action) +} + +func (u *DefaultURLBuilder) CollectionAction(schema *types.APISchema, action string) string { + return u.schemaURL(schema) + "?action=" + url.QueryEscape(action) +} diff --git a/pkg/schemaserver/writer/encoding.go b/pkg/schemaserver/writer/encoding.go new file mode 100644 index 00000000..812978c1 --- /dev/null +++ b/pkg/schemaserver/writer/encoding.go @@ -0,0 +1,124 @@ +package writer + +import ( + "io" + "net/http" + + "github.com/rancher/steve/pkg/schemaserver/types" +) + +type EncodingResponseWriter struct { + ContentType string + Encoder func(io.Writer, interface{}) error +} + +func (j *EncodingResponseWriter) start(apiOp *types.APIRequest, code int) { + AddCommonResponseHeader(apiOp) + apiOp.Response.Header().Set("content-type", j.ContentType) + apiOp.Response.WriteHeader(code) +} + +func (j *EncodingResponseWriter) Write(apiOp *types.APIRequest, code int, obj types.APIObject) { + j.start(apiOp, code) + j.Body(apiOp, apiOp.Response, obj) +} + +func (j *EncodingResponseWriter) WriteList(apiOp *types.APIRequest, code int, list types.APIObjectList) { + j.start(apiOp, code) + j.BodyList(apiOp, apiOp.Response, list) +} + +func (j *EncodingResponseWriter) Body(apiOp *types.APIRequest, writer io.Writer, obj types.APIObject) error { + return j.Encoder(writer, j.convert(apiOp, obj)) +} + +func (j *EncodingResponseWriter) BodyList(apiOp *types.APIRequest, writer io.Writer, list types.APIObjectList) error { + return j.Encoder(writer, j.convertList(apiOp, list)) +} + +func (j *EncodingResponseWriter) convertList(apiOp *types.APIRequest, input types.APIObjectList) *types.GenericCollection { + collection := newCollection(apiOp, input) + for _, value := range input.Objects { + converted := j.convert(apiOp, value) + collection.Data = append(collection.Data, converted) + } + + if apiOp.Schema.CollectionFormatter != nil { + apiOp.Schema.CollectionFormatter(apiOp, collection) + } + + return collection +} + +func (j *EncodingResponseWriter) convert(context *types.APIRequest, input types.APIObject) *types.RawResource { + schema := context.Schemas.LookupSchema(input.Type) + if schema == nil { + schema = context.Schema + } + if schema == nil { + return nil + } + + rawResource := &types.RawResource{ + ID: input.ID, + Type: schema.ID, + Schema: schema, + Links: map[string]string{}, + Actions: map[string]string{}, + ActionLinks: context.Request.Header.Get("X-API-Action-Links") != "", + APIObject: input, + } + + j.addLinks(schema, context, input, rawResource) + + if schema.Formatter != nil { + schema.Formatter(context, rawResource) + } + + return rawResource +} + +func (j *EncodingResponseWriter) addLinks(schema *types.APISchema, context *types.APIRequest, input types.APIObject, rawResource *types.RawResource) { + if rawResource.ID == "" { + return + } + + self := context.URLBuilder.ResourceLink(rawResource.Schema, rawResource.ID) + if _, ok := rawResource.Links["self"]; !ok { + rawResource.Links["self"] = self + } + if _, ok := rawResource.Links["update"]; !ok { + if context.AccessControl.CanUpdate(context, input, schema) == nil { + rawResource.Links["update"] = self + } + } + if _, ok := rawResource.Links["remove"]; !ok { + if context.AccessControl.CanDelete(context, input, schema) == nil { + rawResource.Links["remove"] = self + } + } +} + +func newCollection(apiOp *types.APIRequest, list types.APIObjectList) *types.GenericCollection { + result := &types.GenericCollection{ + Collection: types.Collection{ + Type: "collection", + ResourceType: apiOp.Type, + CreateTypes: map[string]string{}, + Links: map[string]string{ + "self": apiOp.URLBuilder.Current(), + }, + Actions: map[string]string{}, + Continue: list.Continue, + Revision: list.Revision, + }, + } + + if apiOp.Method == http.MethodGet { + if apiOp.AccessControl.CanCreate(apiOp, apiOp.Schema) == nil { + result.CreateTypes[apiOp.Schema.ID] = apiOp.URLBuilder.Collection(apiOp.Schema) + } + } + + return result +} diff --git a/pkg/schemaserver/writer/headers.go b/pkg/schemaserver/writer/headers.go new file mode 100644 index 00000000..314dd5ea --- /dev/null +++ b/pkg/schemaserver/writer/headers.go @@ -0,0 +1,24 @@ +package writer + +import ( + "github.com/rancher/steve/pkg/schemaserver/types" +) + +func AddCommonResponseHeader(apiOp *types.APIRequest) error { + addExpires(apiOp) + return addSchemasHeader(apiOp) +} + +func addSchemasHeader(apiOp *types.APIRequest) error { + schema := apiOp.Schemas.Schemas["schema"] + if schema == nil { + return nil + } + + apiOp.Response.Header().Set("X-Api-Schemas", apiOp.URLBuilder.Collection(schema)) + return nil +} + +func addExpires(apiOp *types.APIRequest) { + apiOp.Response.Header().Set("Expires", "Wed 24 Feb 1982 18:42:00 GMT") +} diff --git a/pkg/schemaserver/writer/html.go b/pkg/schemaserver/writer/html.go new file mode 100644 index 00000000..833f59c5 --- /dev/null +++ b/pkg/schemaserver/writer/html.go @@ -0,0 +1,85 @@ +package writer + +import ( + "strings" + + "github.com/rancher/steve/pkg/schemaserver/types" +) + +const ( + JSURL = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.js" + CSSURL = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.css" + DefaultVersion = "1.1.8" +) + +var ( + start = ` + + + + + +`) +) + +type StringGetter func() string + +type HTMLResponseWriter struct { + EncodingResponseWriter + CSSURL StringGetter + JSURL StringGetter + APIUIVersion StringGetter +} + +func (h *HTMLResponseWriter) start(apiOp *types.APIRequest, code int) { + AddCommonResponseHeader(apiOp) + apiOp.Response.Header().Set("content-type", "text/html") + apiOp.Response.WriteHeader(code) +} + +func (h *HTMLResponseWriter) Write(apiOp *types.APIRequest, code int, obj types.APIObject) { + h.write(apiOp, code, obj) +} + +func (h *HTMLResponseWriter) WriteList(apiOp *types.APIRequest, code int, list types.APIObjectList) { + h.write(apiOp, code, list) +} + +func (h *HTMLResponseWriter) write(apiOp *types.APIRequest, code int, obj interface{}) { + h.start(apiOp, code) + schemaSchema := apiOp.Schemas.Schemas["schema"] + headerString := start + if schemaSchema != nil { + headerString = strings.Replace(headerString, "%SCHEMAS%", apiOp.URLBuilder.Collection(schemaSchema), 1) + } + var jsurl, cssurl string + if h.CSSURL != nil && h.JSURL != nil && h.CSSURL() != "" && h.JSURL() != "" { + jsurl = h.JSURL() + cssurl = h.CSSURL() + } else if h.APIUIVersion != nil && h.APIUIVersion() != "" { + jsurl = strings.Replace(JSURL, "%API_UI_VERSION%", h.APIUIVersion(), 1) + cssurl = strings.Replace(CSSURL, "%API_UI_VERSION%", h.APIUIVersion(), 1) + } else { + jsurl = strings.Replace(JSURL, "%API_UI_VERSION%", DefaultVersion, 1) + cssurl = strings.Replace(CSSURL, "%API_UI_VERSION%", DefaultVersion, 1) + } + headerString = strings.Replace(headerString, "%JSURL%", jsurl, 1) + headerString = strings.Replace(headerString, "%CSSURL%", cssurl, 1) + + apiOp.Response.Write([]byte(headerString)) + if apiObj, ok := obj.(types.APIObject); ok { + h.Body(apiOp, apiOp.Response, apiObj) + } else if list, ok := obj.(types.APIObjectList); ok { + h.BodyList(apiOp, apiOp.Response, list) + } + if schemaSchema != nil { + apiOp.Response.Write(end) + } +} diff --git a/pkg/server/cli/clicontext.go b/pkg/server/cli/clicontext.go new file mode 100644 index 00000000..bc68ca9f --- /dev/null +++ b/pkg/server/cli/clicontext.go @@ -0,0 +1,73 @@ +package cli + +import ( + authcli "github.com/rancher/steve/pkg/auth/cli" + "github.com/rancher/steve/pkg/server" + "github.com/rancher/wrangler/pkg/kubeconfig" + "github.com/urfave/cli" +) + +type Config struct { + KubeConfig string + HTTPSListenPort int + HTTPListenPort int + Namespace string + + WebhookConfig authcli.WebhookConfig +} + +func (c *Config) MustServerConfig() *server.Server { + cc, err := c.ToServerConfig() + if err != nil { + panic(err) + } + return cc +} + +func (c *Config) ToServerConfig() (*server.Server, error) { + restConfig, err := kubeconfig.GetNonInteractiveClientConfig(c.KubeConfig).ClientConfig() + if err != nil { + return nil, err + } + + auth, err := c.WebhookConfig.WebhookMiddleware() + if err != nil { + return nil, err + } + + return &server.Server{ + Namespace: c.Namespace, + RestConfig: restConfig, + AuthMiddleware: auth, + HTTPPort: c.HTTPListenPort, + HTTPSPort: c.HTTPSListenPort, + }, nil +} + +func Flags(config *Config) []cli.Flag { + flags := []cli.Flag{ + cli.StringFlag{ + Name: "kubeconfig", + EnvVar: "KUBECONFIG", + Destination: &config.KubeConfig, + }, + cli.IntFlag{ + Name: "https-listen-port", + Value: 8443, + Destination: &config.HTTPSListenPort, + }, + cli.IntFlag{ + Name: "http-listen-port", + Value: 8080, + Destination: &config.HTTPListenPort, + }, + cli.StringFlag{ + Name: "namespace", + EnvVar: "NAMESPACE", + Value: "steve", + Destination: &config.Namespace, + }, + } + + return append(flags, authcli.Flags(&config.WebhookConfig)...) +} diff --git a/pkg/server/config.go b/pkg/server/config.go new file mode 100644 index 00000000..3fb78151 --- /dev/null +++ b/pkg/server/config.go @@ -0,0 +1,89 @@ +package server + +import ( + "context" + "net/http" + + "github.com/rancher/steve/pkg/auth" + "github.com/rancher/steve/pkg/schema" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/steve/pkg/server/router" + "github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io" + apiextensionsv1beta1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io/v1beta1" + "github.com/rancher/wrangler-api/pkg/generated/controllers/apiregistration.k8s.io" + apiregistrationv1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiregistration.k8s.io/v1" + "github.com/rancher/wrangler-api/pkg/generated/controllers/core" + corev1 "github.com/rancher/wrangler-api/pkg/generated/controllers/core/v1" + "github.com/rancher/wrangler-api/pkg/generated/controllers/rbac" + rbacv1 "github.com/rancher/wrangler-api/pkg/generated/controllers/rbac/v1" + "github.com/rancher/wrangler/pkg/start" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +type Server struct { + *Controllers + + RestConfig *rest.Config + + Namespace string + HTTPSPort int + HTTPPort int + BaseSchemas *types.APISchemas + SchemaTemplates []schema.Template + AuthMiddleware auth.Middleware + Next http.Handler + Router router.RouterFunc + PostStartHooks []func() error + StartHooks []StartHook +} + +type Controllers struct { + K8s kubernetes.Interface + Core corev1.Interface + RBAC rbacv1.Interface + API apiregistrationv1.Interface + CRD apiextensionsv1beta1.Interface + starters []start.Starter +} + +func NewController(cfg *rest.Config) (*Controllers, error) { + c := &Controllers{} + + core, err := core.NewFactoryFromConfig(cfg) + if err != nil { + return nil, err + } + c.starters = append(c.starters, core) + + rbac, err := rbac.NewFactoryFromConfig(cfg) + if err != nil { + return nil, err + } + c.starters = append(c.starters, rbac) + + api, err := apiregistration.NewFactoryFromConfig(cfg) + if err != nil { + return nil, err + } + c.starters = append(c.starters, api) + + crd, err := apiextensions.NewFactoryFromConfig(cfg) + if err != nil { + return nil, err + } + c.starters = append(c.starters, crd) + + c.K8s, err = kubernetes.NewForConfig(cfg) + if err != nil { + return nil, err + } + c.Core = core.Core().V1() + c.RBAC = rbac.Rbac().V1() + c.API = api.Apiregistration().V1() + c.CRD = crd.Apiextensions().V1beta1() + + return c, nil +} + +type StartHook func(context.Context, *Server) error diff --git a/pkg/server/handler/apiserver.go b/pkg/server/handler/apiserver.go index f8f0d823..6281f4df 100644 --- a/pkg/server/handler/apiserver.go +++ b/pkg/server/handler/apiserver.go @@ -1,29 +1,29 @@ -package publicapi +package handler import ( "net/http" - "github.com/rancher/norman/v2/pkg/api" - "github.com/rancher/norman/v2/pkg/auth" - "github.com/rancher/norman/v2/pkg/types" - "github.com/rancher/norman/v2/pkg/urlbuilder" "github.com/rancher/steve/pkg/accesscontrol" + "github.com/rancher/steve/pkg/auth" k8sproxy "github.com/rancher/steve/pkg/proxy" - "github.com/rancher/steve/pkg/resources/schema" + "github.com/rancher/steve/pkg/schema" + "github.com/rancher/steve/pkg/schemaserver/server" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/steve/pkg/schemaserver/urlbuilder" "github.com/rancher/steve/pkg/server/router" "github.com/sirupsen/logrus" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/client-go/rest" ) -func NewHandler(cfg *rest.Config, sf schema.Factory, authMiddleware auth.Middleware, next http.Handler) (http.Handler, error) { +func New(cfg *rest.Config, sf schema.Factory, authMiddleware auth.Middleware, next http.Handler, routerFunc router.RouterFunc) (http.Handler, error) { var ( err error ) a := &apiServer{ sf: sf, - server: api.DefaultAPIServer(), + server: server.DefaultAPIServer(), } a.server.AccessControl = accesscontrol.NewAccessControl() @@ -33,18 +33,22 @@ func NewHandler(cfg *rest.Config, sf schema.Factory, authMiddleware auth.Middlew } w := authMiddleware.Wrap - return router.Routes(router.Handlers{ + handlers := router.Handlers{ Next: next, K8sResource: w(a.apiHandler(k8sAPI)), GenericResource: w(a.apiHandler(nil)), K8sProxy: w(proxy), APIRoot: w(a.apiHandler(apiRoot)), - }), nil + } + if routerFunc == nil { + return router.Routes(handlers), nil + } + return routerFunc(handlers), nil } type apiServer struct { sf schema.Factory - server *api.Server + server *server.Server } func (a *apiServer) common(rw http.ResponseWriter, req *http.Request) (*types.APIRequest, bool) { diff --git a/pkg/server/handler/handlers.go b/pkg/server/handler/handlers.go index 7e62b59d..35d27ae9 100644 --- a/pkg/server/handler/handlers.go +++ b/pkg/server/handler/handlers.go @@ -1,10 +1,10 @@ -package publicapi +package handler import ( "github.com/gorilla/mux" - "github.com/rancher/norman/v2/pkg/types" "github.com/rancher/steve/pkg/attributes" - "github.com/rancher/steve/pkg/resources/schema" + "github.com/rancher/steve/pkg/schema" + "github.com/rancher/steve/pkg/schemaserver/types" runtimeschema "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -24,7 +24,7 @@ func k8sAPI(sf schema.Factory, apiOp *types.APIRequest) { nOrN := vars["nameorns"] if nOrN != "" { - schema := apiOp.Schemas.Schema(apiOp.Type) + schema := apiOp.Schemas.LookupSchema(apiOp.Type) if attributes.Namespaced(schema) { vars["namespace"] = nOrN } else { @@ -33,7 +33,7 @@ func k8sAPI(sf schema.Factory, apiOp *types.APIRequest) { } if namespace := vars["namespace"]; namespace != "" { - apiOp.Namespaces = []string{namespace} + apiOp.Namespace = namespace } } diff --git a/pkg/server/resources/apigroups/apigroup.go b/pkg/server/resources/apigroups/apigroup.go index 399f64b3..89cd6db7 100644 --- a/pkg/server/resources/apigroups/apigroup.go +++ b/pkg/server/resources/apigroups/apigroup.go @@ -3,20 +3,19 @@ package apigroups import ( "net/http" - "github.com/rancher/norman/v2/pkg/data" - "github.com/rancher/norman/v2/pkg/store/empty" - "github.com/rancher/norman/v2/pkg/types" + "github.com/rancher/steve/pkg/schemaserver/store/empty" + "github.com/rancher/steve/pkg/schemaserver/types" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/discovery" ) -func Register(schemas *types.Schemas, discovery discovery.DiscoveryInterface) { - schemas.MustImportAndCustomize(v1.APIGroup{}, func(schema *types.Schema) { +func Register(schemas *types.APISchemas, discovery discovery.DiscoveryInterface) { + schemas.MustImportAndCustomize(v1.APIGroup{}, func(schema *types.APISchema) { schema.CollectionMethods = []string{http.MethodGet} schema.ResourceMethods = []string{http.MethodGet} schema.Store = NewStore(discovery) schema.Formatter = func(request *types.APIRequest, resource *types.RawResource) { - resource.ID = data.Object(resource.Values).String("name") + resource.ID = resource.APIObject.Data().String("name") } }) } @@ -34,38 +33,32 @@ func NewStore(discovery discovery.DiscoveryInterface) types.Store { } } -func (e *Store) ByID(apiOp *types.APIRequest, schema *types.Schema, id string) (types.APIObject, error) { - groupList, err := e.discovery.ServerGroups() - if err != nil { - return types.APIObject{}, err - } - - if id == "core" { - id = "" - } - - for _, group := range groupList.Groups { - if group.Name == id { - return types.ToAPI(group), nil - } - } - - return types.APIObject{}, nil +func (e *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + return types.DefaultByID(e, apiOp, schema, id) } -func (e *Store) List(apiOp *types.APIRequest, schema *types.Schema, opt *types.QueryOptions) (types.APIObject, error) { - groupList, err := e.discovery.ServerGroups() - if err != nil { - return types.APIObject{}, err +func toAPIObject(schema *types.APISchema, group v1.APIGroup) types.APIObject { + if group.Name == "" { + group.Name = "core" + } + return types.APIObject{ + Type: schema.ID, + ID: group.Name, + Object: group, } - var result []interface{} +} + +func (e *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) { + groupList, err := e.discovery.ServerGroups() + if err != nil { + return types.APIObjectList{}, err + } + + var result types.APIObjectList for _, item := range groupList.Groups { - if item.Name == "" { - item.Name = "core" - } - result = append(result, item) + result.Objects = append(result.Objects, toAPIObject(schema, item)) } - return types.ToAPI(result), nil + return result, nil } diff --git a/pkg/server/resources/common/defaultcolumns.go b/pkg/server/resources/common/defaultcolumns.go index 50084e40..f5ab0219 100644 --- a/pkg/server/resources/common/defaultcolumns.go +++ b/pkg/server/resources/common/defaultcolumns.go @@ -1,9 +1,11 @@ package common import ( - "github.com/rancher/norman/v2/pkg/types" "github.com/rancher/steve/pkg/attributes" - "github.com/rancher/steve/pkg/table" + "github.com/rancher/steve/pkg/schema/table" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas" + "github.com/rancher/wrangler/pkg/schemas/mappers" ) var ( @@ -22,12 +24,15 @@ var ( ) type DefaultColumns struct { - types.EmptyMapper + mappers.EmptyMapper } -func (d *DefaultColumns) ModifySchema(schema *types.Schema, schemas *types.Schemas) error { - if attributes.Columns(schema) == nil { - attributes.SetColumns(schema, []table.Column{ +func (d *DefaultColumns) ModifySchema(schema *schemas.Schema, schemas *schemas.Schemas) error { + as := &types.APISchema{ + Schema: schema, + } + if attributes.Columns(as) == nil { + attributes.SetColumns(as, []table.Column{ NameColumn, CreatedColumn, }) diff --git a/pkg/server/resources/common/dynamiccolumns.go b/pkg/server/resources/common/dynamiccolumns.go new file mode 100644 index 00000000..81a09268 --- /dev/null +++ b/pkg/server/resources/common/dynamiccolumns.go @@ -0,0 +1,111 @@ +package common + +import ( + "net/http" + + "github.com/rancher/steve/pkg/attributes" + "github.com/rancher/steve/pkg/schema/table" + "github.com/rancher/steve/pkg/schemaserver/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/rest" +) + +type DynamicColumns struct { + client *rest.RESTClient +} + +func NewDynamicColumns(config *rest.Config) (*DynamicColumns, error) { + c, err := newClient(config) + if err != nil { + return nil, err + } + return &DynamicColumns{ + client: c, + }, nil +} + +func hasGet(methods []string) bool { + for _, method := range methods { + if method == http.MethodGet { + return true + } + } + return false +} + +func (d *DynamicColumns) SetColumns(schema *types.APISchema) error { + if attributes.Columns(schema) != nil { + return nil + } + + gvr := attributes.GVR(schema) + if gvr.Resource == "" { + return nil + } + nsed := attributes.Namespaced(schema) + + if !hasGet(schema.CollectionMethods) { + return nil + } + + r := d.client.Get() + if gvr.Group == "" { + r.Prefix("api") + } else { + r.Prefix("apis", gvr.Group) + } + r.Prefix(gvr.Version) + if nsed { + r.Prefix("namespaces", "default") + } + r.Prefix(gvr.Resource) + + obj, err := r.Do().Get() + if err != nil { + return err + } + t, ok := obj.(*metav1.Table) + if !ok { + return nil + } + + var cols []table.Column + for _, cd := range t.ColumnDefinitions { + cols = append(cols, table.Column{ + Name: cd.Name, + Field: "metadata.computed.fields." + cd.Name, + Type: cd.Type, + Format: cd.Format, + }) + } + + if len(cols) > 0 { + attributes.SetColumns(schema, cols) + schema.Attributes["server-side-column"] = "true" + } + + return nil +} + +func newClient(config *rest.Config) (*rest.RESTClient, error) { + scheme := runtime.NewScheme() + if err := metav1.AddMetaToScheme(scheme); err != nil { + return nil, err + } + if err := metav1beta1.AddMetaToScheme(scheme); err != nil { + return nil, err + } + + config = rest.CopyConfig(config) + config.UserAgent = rest.DefaultKubernetesUserAgent() + config.AcceptContentTypes = "application/json;as=Table;v=v1beta1;g=meta.k8s.io" + config.ContentType = "application/json;as=Table;v=v1beta1;g=meta.k8s.io" + config.GroupVersion = &schema.GroupVersion{} + config.NegotiatedSerializer = serializer.NewCodecFactory(scheme) + config.APIPath = "/" + return rest.RESTClientFor(config) +} diff --git a/pkg/server/resources/common/formatter.go b/pkg/server/resources/common/formatter.go index ffcc3178..4f7a40b4 100644 --- a/pkg/server/resources/common/formatter.go +++ b/pkg/server/resources/common/formatter.go @@ -1,25 +1,27 @@ package common import ( - "github.com/rancher/norman/v2/pkg/store/proxy" - "github.com/rancher/norman/v2/pkg/types" - "github.com/rancher/norman/v2/pkg/types/convert" - "github.com/rancher/norman/v2/pkg/types/values" - "github.com/rancher/steve/pkg/resources/schema" + "github.com/rancher/steve/pkg/schema" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/steve/pkg/server/store/proxy" + "k8s.io/apimachinery/pkg/api/meta" ) -func Register(collection *schema.Collection, clientGetter proxy.ClientGetter) error { - collection.AddTemplate(&schema.Template{ +func DefaultTemplate(clientGetter proxy.ClientGetter) schema.Template { + return schema.Template{ Store: proxy.NewProxyStore(clientGetter), Formatter: Formatter, Mapper: &DefaultColumns{}, - }) - - return nil + } } func Formatter(request *types.APIRequest, resource *types.RawResource) { - selfLink := convert.ToString(values.GetValueN(resource.Values, "metadata", "selfLink")) + meta, err := meta.Accessor(resource.APIObject.Object) + if err != nil { + return + } + + selfLink := meta.GetSelfLink() if selfLink == "" { return } diff --git a/pkg/server/resources/counts/counts.go b/pkg/server/resources/counts/counts.go index 9754c351..7f379fcd 100644 --- a/pkg/server/resources/counts/counts.go +++ b/pkg/server/resources/counts/counts.go @@ -5,15 +5,14 @@ import ( "strconv" "sync" - schema2 "k8s.io/apimachinery/pkg/runtime/schema" - - "github.com/rancher/norman/v2/pkg/store/empty" - "github.com/rancher/norman/v2/pkg/types" "github.com/rancher/steve/pkg/accesscontrol" "github.com/rancher/steve/pkg/attributes" "github.com/rancher/steve/pkg/clustercache" + "github.com/rancher/steve/pkg/schemaserver/store/empty" + "github.com/rancher/steve/pkg/schemaserver/types" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" + schema2 "k8s.io/apimachinery/pkg/runtime/schema" ) var ( @@ -24,11 +23,11 @@ var ( } ) -func Register(schemas *types.Schemas, ccache clustercache.ClusterCache) { - schemas.MustImportAndCustomize(Count{}, func(schema *types.Schema) { +func Register(schemas *types.APISchemas, ccache clustercache.ClusterCache) { + schemas.MustImportAndCustomize(Count{}, func(schema *types.APISchema) { schema.CollectionMethods = []string{http.MethodGet} schema.ResourceMethods = []string{http.MethodGet} - schema.Attributes["access"] = accesscontrol.AccessListMap{ + schema.Attributes["access"] = accesscontrol.AccessListByVerb{ "watch": accesscontrol.AccessList{ { Namespace: "*", @@ -58,27 +57,39 @@ type Store struct { ccache clustercache.ClusterCache } -func (s *Store) ByID(apiOp *types.APIRequest, schema *types.Schema, id string) (types.APIObject, error) { - c := s.getCount(apiOp) - return types.ToAPI(c), nil +func toAPIObject(c Count) types.APIObject { + return types.APIObject{ + Type: "count", + ID: c.ID, + Object: c, + } } -func (s *Store) List(apiOp *types.APIRequest, schema *types.Schema, opt *types.QueryOptions) (types.APIObject, error) { +func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { c := s.getCount(apiOp) - return types.ToAPI([]interface{}{c}), nil + return toAPIObject(c), nil } -func (s *Store) Watch(apiOp *types.APIRequest, schema *types.Schema, w types.WatchRequest) (chan types.APIEvent, error) { +func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) { + c := s.getCount(apiOp) + return types.APIObjectList{ + Objects: []types.APIObject{ + toAPIObject(c), + }, + }, nil +} + +func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan types.APIEvent, error) { var ( result = make(chan types.APIEvent, 100) counts map[string]ItemCount - gvrToSchema = map[schema2.GroupVersionResource]*types.Schema{} + gvrToSchema = map[schema2.GroupVersionResource]*types.APISchema{} countLock sync.Mutex ) counts = s.getCount(apiOp).Counts for id := range counts { - schema := apiOp.Schemas.Schema(id) + schema := apiOp.Schemas.LookupSchema(id) if schema == nil { continue } @@ -107,11 +118,6 @@ func (s *Store) Watch(apiOp *types.APIRequest, schema *types.Schema, w types.Wat return nil } - apiObj := apiOp.Filter(nil, schema, types.ToAPI(obj)) - if apiObj.IsNil() { - return nil - } - _, namespace, revision, ok := getInfo(obj) if !ok { return nil @@ -151,7 +157,7 @@ func (s *Store) Watch(apiOp *types.APIRequest, schema *types.Schema, w types.Wat result <- types.APIEvent{ Name: "resource.change", ResourceType: "counts", - Object: types.ToAPI(Count{ + Object: toAPIObject(Count{ ID: "count", Counts: countsCopy, }), @@ -170,8 +176,8 @@ func (s *Store) Watch(apiOp *types.APIRequest, schema *types.Schema, w types.Wat return result, nil } -func (s *Store) schemasToWatch(apiOp *types.APIRequest) (result []*types.Schema) { - for _, schema := range apiOp.Schemas.Schemas() { +func (s *Store) schemasToWatch(apiOp *types.APIRequest) (result []*types.APISchema) { + for _, schema := range apiOp.Schemas.Schemas { if ignore[schema.ID] { continue } diff --git a/pkg/server/resources/schema.go b/pkg/server/resources/schema.go index 001d3864..3d8cb354 100644 --- a/pkg/server/resources/schema.go +++ b/pkg/server/resources/schema.go @@ -1,38 +1,28 @@ package resources import ( - "github.com/rancher/norman/v2/pkg/store/apiroot" - "github.com/rancher/norman/v2/pkg/subscribe" - "github.com/rancher/norman/v2/pkg/types" - "github.com/rancher/steve/pkg/accesscontrol" "github.com/rancher/steve/pkg/client" "github.com/rancher/steve/pkg/clustercache" - "github.com/rancher/steve/pkg/resources/apigroups" - "github.com/rancher/steve/pkg/resources/common" - "github.com/rancher/steve/pkg/resources/core" - "github.com/rancher/steve/pkg/resources/counts" - "github.com/rancher/steve/pkg/resources/schema" - "k8s.io/client-go/kubernetes" + "github.com/rancher/steve/pkg/schema" + "github.com/rancher/steve/pkg/schemaserver/store/apiroot" + "github.com/rancher/steve/pkg/schemaserver/subscribe" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/steve/pkg/server/resources/apigroups" + "github.com/rancher/steve/pkg/server/resources/common" + "github.com/rancher/steve/pkg/server/resources/counts" + "k8s.io/client-go/discovery" ) -func SchemaFactory( - cf *client.Factory, - as *accesscontrol.AccessStore, - k8s kubernetes.Interface, - ccache clustercache.ClusterCache, -) (*schema.Collection, error) { - baseSchema := types.EmptySchemas() - collection := schema.NewCollection(baseSchema, as) - - core.Register(collection) +func DefaultSchemas(baseSchema *types.APISchemas, discovery discovery.DiscoveryInterface, ccache clustercache.ClusterCache) *types.APISchemas { counts.Register(baseSchema, ccache) subscribe.Register(baseSchema) - apigroups.Register(baseSchema, k8s.Discovery()) + apigroups.Register(baseSchema, discovery) apiroot.Register(baseSchema, []string{"v1"}, []string{"proxy:/apis"}) - - if err := common.Register(collection, cf); err != nil { - return nil, err - } - - return collection, nil + return baseSchema +} + +func DefaultSchemaTemplates(cf *client.Factory) []schema.Template { + return []schema.Template{ + common.DefaultTemplate(cf), + } } diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index caa7dd6c..622798be 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -6,28 +6,36 @@ import ( "github.com/gorilla/mux" ) +type RouterFunc func(h Handlers) http.Handler + type Handlers struct { K8sResource http.Handler GenericResource http.Handler APIRoot http.Handler K8sProxy http.Handler + Next http.Handler } func Routes(h Handlers) http.Handler { m := mux.NewRouter() m.UseEncodedPath() m.StrictSlash(true) - m.NotFoundHandler = h.K8sProxy - m.Path("/").Handler(h.APIRoot) + m.Path("/").Handler(h.APIRoot).HeadersRegexp("Accepts", ".*json.*") m.Path("/{name:v1}").Handler(h.APIRoot) - m.Path("/v1/{type:schemas}/{name:.*}").Handler(h.GenericResource) m.Path("/v1/{group}.{version}.{resource}").Handler(h.K8sResource) m.Path("/v1/{group}.{version}.{resource}/{nameorns}").Handler(h.K8sResource) m.Path("/v1/{group}.{version}.{resource}/{namespace}/{name}").Handler(h.K8sResource) + m.Path("/v1/{group}.{version}.{resource}/{nameorns}").Queries("action", "{action}").Handler(h.K8sResource) + m.Path("/v1/{group}.{version}.{resource}/{namespace}/{name}").Queries("action", "{action}").Handler(h.K8sResource) + m.Path("/v1/{type:schemas}/{name:.*}").Handler(h.GenericResource) m.Path("/v1/{type}").Handler(h.GenericResource) m.Path("/v1/{type}/{name}").Handler(h.GenericResource) + m.PathPrefix("/api").Handler(h.K8sProxy) + m.PathPrefix("/openapi").Handler(h.K8sProxy) + m.PathPrefix("/version").Handler(h.K8sProxy) + m.NotFoundHandler = h.Next return m } diff --git a/pkg/server/server.go b/pkg/server/server.go index 22d1f334..d2ea3dd4 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -2,127 +2,152 @@ package server import ( "context" - "github.com/rancher/norman/pkg/auth" + "errors" "net/http" + "github.com/rancher/dynamiclistener/server" + "github.com/rancher/dynamiclistener/storage/kubernetes" + "github.com/rancher/dynamiclistener/storage/memory" "github.com/rancher/steve/pkg/accesscontrol" "github.com/rancher/steve/pkg/client" "github.com/rancher/steve/pkg/clustercache" - "github.com/rancher/steve/pkg/controllers/schema" - "github.com/rancher/steve/pkg/resources" - "github.com/rancher/steve/pkg/server/publicapi" - "github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io" - "github.com/rancher/wrangler-api/pkg/generated/controllers/apiregistration.k8s.io" - "github.com/rancher/wrangler-api/pkg/generated/controllers/core" - rbaccontroller "github.com/rancher/wrangler-api/pkg/generated/controllers/rbac" - "github.com/rancher/wrangler/pkg/generic" - "github.com/rancher/wrangler/pkg/kubeconfig" + schemacontroller "github.com/rancher/steve/pkg/controllers/schema" + "github.com/rancher/steve/pkg/schema" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/steve/pkg/server/handler" + "github.com/rancher/steve/pkg/server/resources" + v1 "github.com/rancher/wrangler-api/pkg/generated/controllers/core/v1" "github.com/rancher/wrangler/pkg/start" - "github.com/sirupsen/logrus" - schema2 "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/kubernetes" ) -type Config struct { - Kubeconfig string - ListenAddress string - WebhookKubeconfig string - Authentication bool +var ErrConfigRequired = errors.New("rest config is required") + +func setDefaults(server *Server) error { + if server.RestConfig == nil { + return ErrConfigRequired + } + + if server.Namespace == "" { + server.Namespace = "steve" + } + + if server.Controllers == nil { + var err error + server.Controllers, err = NewController(server.RestConfig) + if err != nil { + return err + } + } + + if server.Next == nil { + server.Next = http.NotFoundHandler() + } + + if server.BaseSchemas == nil { + server.BaseSchemas = types.EmptyAPISchemas() + } + + return nil } -func Run(ctx context.Context, cfg Config) error { - restConfig, err := kubeconfig.GetNonInteractiveClientConfig(cfg.Kubeconfig).ClientConfig() - if err != nil { - return err +func setup(ctx context.Context, server *Server) (http.Handler, *schema.Collection, error) { + if err := setDefaults(server); err != nil { + return nil, nil, err } - restConfig.QPS = 100 - restConfig.Burst = 100 - - rbac, err := rbaccontroller.NewFactoryFromConfig(restConfig) + cf, err := client.NewFactory(server.RestConfig) if err != nil { - return err - } - - core, err := core.NewFactoryFromConfig(restConfig) - if err != nil { - return err - } - - k8s, err := kubernetes.NewForConfig(restConfig) - if err != nil { - return err - } - - api, err := apiregistration.NewFactoryFromConfig(restConfig) - if err != nil { - return err - } - - crd, err := apiextensions.NewFactoryFromConfig(restConfig) - if err != nil { - return err - } - - cf, err := client.NewFactory(restConfig) - if err != nil { - return err + return nil, nil, err } ccache := clustercache.NewClusterCache(ctx, cf.DynamicClient()) - sf := resources.SchemaFactory(cf, - accesscontrol.NewAccessStore(rbac.Rbac().V1()), - k8s, - ccache, - core.Core().V1().ConfigMap(), - core.Core().V1().Secret()) + server.BaseSchemas = resources.DefaultSchemas(server.BaseSchemas, server.K8s.Discovery(), ccache) + server.SchemaTemplates = append(server.SchemaTemplates, resources.DefaultSchemaTemplates(cf)...) - sync := schema.Register(ctx, - k8s.Discovery(), - crd.Apiextensions().V1beta1().CustomResourceDefinition(), - api.Apiregistration().V1().APIService(), - k8s.AuthorizationV1().SelfSubjectAccessReviews(), + sf := schema.NewCollection(server.BaseSchemas, accesscontrol.NewAccessStore(server.RBAC)) + sync := schemacontroller.Register(ctx, + server.K8s.Discovery(), + server.CRD.CustomResourceDefinition(), + server.API.APIService(), + server.K8s.AuthorizationV1().SelfSubjectAccessReviews(), ccache, sf) - handler, err := publicapi.NewHandler(restConfig, sf) + handler, err := handler.New(server.RestConfig, sf, server.AuthMiddleware, server.Next, server.Router) + if err != nil { + return nil, nil, err + } + + server.PostStartHooks = append(server.PostStartHooks, func() error { + return sync() + }) + + return handler, sf, nil +} + +func (c *Server) Handler(ctx context.Context) (http.Handler, error) { + handler, sf, err := setup(ctx, c) + if err != nil { + return nil, err + } + + for _, hook := range c.StartHooks { + if err := hook(ctx, c); err != nil { + return nil, err + } + } + + for i := range c.SchemaTemplates { + sf.AddTemplate(&c.SchemaTemplates[i]) + } + + if err := start.All(ctx, 5, c.starters...); err != nil { + return nil, err + } + + for _, hook := range c.PostStartHooks { + if err := hook(); err != nil { + return nil, err + } + } + + return handler, nil +} + +func ListenAndServe(ctx context.Context, secrets v1.SecretController, namespace string, handler http.Handler, httpsPort, httpPort int, opts *server.ListenOpts) error { + var ( + err error + ) + + if opts == nil { + opts = &server.ListenOpts{} + } + + if opts.CA == nil || opts.CAKey == nil { + opts.CA, opts.CAKey, err = kubernetes.LoadOrGenCA(secrets, namespace, "serving-ca") + if err != nil { + return err + } + } + + if opts.Storage == nil { + storage := kubernetes.Load(ctx, secrets, namespace, "service-cert", memory.New()) + opts.Storage = storage + } + + if err := server.ListenAndServe(ctx, httpsPort, httpPort, handler, opts); err != nil { + return err + } + + return nil +} + +func (c *Server) ListenAndServe(ctx context.Context, httpsPort, httpPort int, opts *server.ListenOpts) error { + handler, err := c.Handler(ctx) if err != nil { return err } - if cfg.Authentication { - authMiddleware, err := auth.NewWebhookMiddleware(cfg.WebhookKubeconfig) - if err != nil { - return err - } - handler = wrapHandler(handler, authMiddleware) - } - - for _, controllers := range []controllers{api, crd, rbac} { - for gvk, controller := range controllers.Controllers() { - ccache.AddController(gvk, controller.Informer()) - } - } - - if err := start.All(ctx, 5, api, crd, rbac); err != nil { - return err - } - - if err := sync(); err != nil { - return err - } - - logrus.Infof("listening on %s", cfg.ListenAddress) - return http.ListenAndServe(cfg.ListenAddress, handler) -} - -func wrapHandler(handler http.Handler, middleware func(http.ResponseWriter, *http.Request, http.Handler)) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - middleware(resp, req, handler) - }) -} - -type controllers interface { - Controllers() map[schema2.GroupVersionKind]*generic.Controller + return ListenAndServe(ctx, c.Core.Secret(), c.Namespace, handler, httpsPort, httpPort, opts) } diff --git a/pkg/server/store/proxy/client.go b/pkg/server/store/proxy/client.go new file mode 100644 index 00000000..d08f65fc --- /dev/null +++ b/pkg/server/store/proxy/client.go @@ -0,0 +1,66 @@ +package proxy + +import ( + "fmt" + + "github.com/rancher/steve/pkg/attributes" + "github.com/rancher/steve/pkg/schemaserver/httperror" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas/validation" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" +) + +type ClientFactory struct { + cfg rest.Config + client dynamic.Interface + impersonate bool + idToGVR map[string]schema.GroupVersionResource +} + +func NewClientFactory(cfg *rest.Config, impersonate bool) *ClientFactory { + return &ClientFactory{ + impersonate: impersonate, + cfg: *cfg, + idToGVR: map[string]schema.GroupVersionResource{}, + } +} + +func (p *ClientFactory) Client(ctx *types.APIRequest, schema *types.APISchema) (dynamic.ResourceInterface, error) { + gvr := attributes.GVR(schema) + if gvr.Resource == "" { + return nil, httperror.NewAPIError(validation.NotFound, "Failed to find gvr for "+schema.ID) + } + + user, ok := request.UserFrom(ctx.Request.Context()) + if !ok { + return nil, fmt.Errorf("failed to find user context for client") + } + + client, err := p.getClient(user) + if err != nil { + return nil, err + } + + return client.Resource(gvr), nil +} + +func (p *ClientFactory) getClient(user user.Info) (dynamic.Interface, error) { + if p.impersonate { + return p.client, nil + } + + if user.GetName() == "" { + return nil, fmt.Errorf("failed to determine current user") + } + + newCfg := p.cfg + newCfg.Impersonate.UserName = user.GetName() + newCfg.Impersonate.Groups = user.GetGroups() + newCfg.Impersonate.Extra = user.GetExtra() + + return dynamic.NewForConfig(&newCfg) +} diff --git a/pkg/server/store/proxy/error_wrapper.go b/pkg/server/store/proxy/error_wrapper.go new file mode 100644 index 00000000..43c2ef18 --- /dev/null +++ b/pkg/server/store/proxy/error_wrapper.go @@ -0,0 +1,56 @@ +package proxy + +import ( + "github.com/rancher/steve/pkg/schemaserver/httperror" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/schemas/validation" + "k8s.io/apimachinery/pkg/api/errors" +) + +type errorStore struct { + types.Store +} + +func (e *errorStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + data, err := e.Store.ByID(apiOp, schema, id) + return data, translateError(err) +} + +func (e *errorStore) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) { + data, err := e.Store.List(apiOp, schema) + return data, translateError(err) +} + +func (e *errorStore) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (types.APIObject, error) { + data, err := e.Store.Create(apiOp, schema, data) + return data, translateError(err) + +} + +func (e *errorStore) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) { + data, err := e.Store.Update(apiOp, schema, data, id) + return data, translateError(err) + +} + +func (e *errorStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + data, err := e.Store.Delete(apiOp, schema, id) + return data, translateError(err) + +} + +func (e *errorStore) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) { + data, err := e.Store.Watch(apiOp, schema, wr) + return data, translateError(err) +} + +func translateError(err error) error { + if apiError, ok := err.(errors.APIStatus); ok { + status := apiError.Status() + return httperror.NewAPIError(validation.ErrorCode{ + Status: int(status.Code), + Code: string(status.Reason), + }, status.Message) + } + return err +} diff --git a/pkg/server/store/proxy/proxy_store.go b/pkg/server/store/proxy/proxy_store.go new file mode 100644 index 00000000..017d6eaa --- /dev/null +++ b/pkg/server/store/proxy/proxy_store.go @@ -0,0 +1,303 @@ +package proxy + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "regexp" + + "github.com/pkg/errors" + "github.com/rancher/steve/pkg/attributes" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/wrangler/pkg/data" + "github.com/rancher/wrangler/pkg/schemas/validation" + "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + apitypes "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" +) + +var ( + lowerChars = regexp.MustCompile("[a-z]+") +) + +type ClientGetter interface { + Client(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error) +} + +type Store struct { + clientGetter ClientGetter +} + +func NewProxyStore(clientGetter ClientGetter) types.Store { + return &errorStore{ + Store: &Store{ + clientGetter: clientGetter, + }, + } +} + +func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + result, err := s.byID(apiOp, schema, id) + return toAPI(schema, result), err +} + +func decodeParams(apiOp *types.APIRequest, target runtime.Object) error { + return metav1.ParameterCodec.DecodeParameters(apiOp.Request.URL.Query(), metav1.SchemeGroupVersion, target) +} + +func toAPI(schema *types.APISchema, obj *unstructured.Unstructured) types.APIObject { + if obj == nil { + return types.APIObject{} + } + + gvr := attributes.GVR(schema) + + id := obj.GetName() + ns := obj.GetNamespace() + if ns != "" { + id = fmt.Sprintf("%s/%s", ns, id) + } + t := fmt.Sprintf("%s/%s/%s", gvr.Group, gvr.Version, gvr.Resource) + return types.APIObject{ + Type: t, + ID: id, + Object: obj, + } +} + +func (s *Store) byID(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, error) { + k8sClient, err := s.clientGetter.Client(apiOp, schema, apiOp.Namespace) + if err != nil { + return nil, err + } + + opts := metav1.GetOptions{} + if err := decodeParams(apiOp, &opts); err != nil { + return nil, err + } + + return k8sClient.Get(id, opts) +} + +func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) { + k8sClient, err := s.clientGetter.Client(apiOp, schema, apiOp.Namespace) + if err != nil { + return types.APIObjectList{}, err + } + + opts := metav1.ListOptions{} + if err := decodeParams(apiOp, &opts); err != nil { + return types.APIObjectList{}, nil + } + + resultList, err := k8sClient.List(opts) + if err != nil { + return types.APIObjectList{}, err + } + + result := types.APIObjectList{ + Revision: resultList.GetResourceVersion(), + Continue: resultList.GetContinue(), + } + + for i := range resultList.Items { + result.Objects = append(result.Objects, toAPI(schema, &resultList.Items[i])) + } + + return result, nil +} + +func returnErr(err error, c chan types.APIEvent) { + c <- types.APIEvent{ + Name: "resource.error", + Error: err, + } +} + +func (s *Store) listAndWatch(apiOp *types.APIRequest, k8sClient dynamic.ResourceInterface, schema *types.APISchema, w types.WatchRequest, result chan types.APIEvent) { + rev := w.Revision + if rev == "" { + list, err := k8sClient.List(metav1.ListOptions{ + Limit: 1, + }) + if err != nil { + returnErr(errors.Wrapf(err, "failed to list %s", schema.ID), result) + return + } + rev = list.GetResourceVersion() + } else if rev == "-1" { + rev = "" + } + + timeout := int64(60 * 30) + watcher, err := k8sClient.Watch(metav1.ListOptions{ + Watch: true, + TimeoutSeconds: &timeout, + ResourceVersion: rev, + }) + if err != nil { + returnErr(errors.Wrapf(err, "stopping watch for %s: %v", schema.ID, err), result) + return + } + defer watcher.Stop() + logrus.Debugf("opening watcher for %s", schema.ID) + + go func() { + <-apiOp.Request.Context().Done() + watcher.Stop() + }() + + for event := range watcher.ResultChan() { + data := event.Object.(*unstructured.Unstructured) + result <- s.toAPIEvent(apiOp, schema, event.Type, data) + } +} + +func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan types.APIEvent, error) { + k8sClient, err := s.clientGetter.Client(apiOp, schema, apiOp.Namespace) + if err != nil { + return nil, err + } + + result := make(chan types.APIEvent) + go func() { + s.listAndWatch(apiOp, k8sClient, schema, w, result) + logrus.Debugf("closing watcher for %s", schema.ID) + close(result) + }() + return result, nil +} + +func (s *Store) toAPIEvent(apiOp *types.APIRequest, schema *types.APISchema, et watch.EventType, obj *unstructured.Unstructured) types.APIEvent { + name := types.ChangeAPIEvent + switch et { + case watch.Deleted: + name = types.RemoveAPIEvent + case watch.Added: + name = types.CreateAPIEvent + } + + return types.APIEvent{ + Name: name, + Revision: obj.GetResourceVersion(), + Object: toAPI(schema, obj), + } +} + +func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, params types.APIObject) (types.APIObject, error) { + var ( + resp *unstructured.Unstructured + ) + + input := params.Data() + + if input == nil { + input = data.Object{} + } + + name := types.Name(input) + ns := types.Namespace(input) + if name == "" && input.String("metadata", "generateName") == "" { + input.SetNested(schema.ID[0:1]+"-", "metadata", "generatedName") + } + + gvk := attributes.GVK(schema) + input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind() + + k8sClient, err := s.clientGetter.Client(apiOp, schema, ns) + if err != nil { + return types.APIObject{}, err + } + + opts := metav1.CreateOptions{} + if err := decodeParams(apiOp, &opts); err != nil { + return types.APIObject{}, err + } + + resp, err = k8sClient.Create(&unstructured.Unstructured{Object: input}, opts) + return toAPI(schema, resp), err +} + +func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, params types.APIObject, id string) (types.APIObject, error) { + var ( + err error + input = params.Data() + ) + + ns := types.Namespace(input) + k8sClient, err := s.clientGetter.Client(apiOp, schema, ns) + if err != nil { + return types.APIObject{}, err + } + + if apiOp.Method == http.MethodPatch { + bytes, err := ioutil.ReadAll(io.LimitReader(apiOp.Request.Body, 2<<20)) + if err != nil { + return types.APIObject{}, err + } + + pType := apitypes.StrategicMergePatchType + if apiOp.Request.Header.Get("content-type") == string(apitypes.JSONPatchType) { + pType = apitypes.JSONPatchType + } + + opts := metav1.PatchOptions{} + if err := decodeParams(apiOp, &opts); err != nil { + return types.APIObject{}, err + } + + resp, err := k8sClient.Patch(id, pType, bytes, opts) + if err != nil { + return types.APIObject{}, err + } + + return toAPI(schema, resp), nil + } + + resourceVersion := input.String("metadata", "resourceVersion") + if resourceVersion == "" { + return types.APIObject{}, fmt.Errorf("metadata.resourceVersion is required for update") + } + + opts := metav1.UpdateOptions{} + if err := decodeParams(apiOp, &opts); err != nil { + return types.APIObject{}, err + } + + resp, err := k8sClient.Update(&unstructured.Unstructured{Object: input}, metav1.UpdateOptions{}) + if err != nil { + return types.APIObject{}, err + } + + return toAPI(schema, resp), nil +} + +func (s *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + opts := metav1.DeleteOptions{} + if err := decodeParams(apiOp, &opts); err != nil { + return types.APIObject{}, nil + } + + k8sClient, err := s.clientGetter.Client(apiOp, schema, apiOp.Namespace) + if err != nil { + return types.APIObject{}, err + } + + if err := k8sClient.Delete(id, &opts); err != nil { + return types.APIObject{}, err + } + + obj, err := s.byID(apiOp, schema, id) + if err != nil { + // ignore lookup error + return types.APIObject{}, validation.ErrorCode{ + Status: http.StatusNoContent, + } + } + return toAPI(schema, obj), nil +}