mirror of
https://github.com/rancher/steve.git
synced 2025-04-27 02:51:10 +00:00
Refactor
This commit is contained in:
parent
19c6732de0
commit
8b42d0aff8
@ -1,3 +1,4 @@
|
||||
./.certs
|
||||
./.dapper
|
||||
./.cache
|
||||
./dist
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
/.dapper
|
||||
/.cache
|
||||
/certs
|
||||
/bin
|
||||
/dist
|
||||
*.swp
|
||||
|
@ -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
|
||||
|
7
go.mod
7
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
|
||||
)
|
||||
|
41
go.sum
41
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=
|
||||
|
58
main.go
58
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)
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{}{}
|
||||
}
|
||||
|
67
pkg/auth/cli/webhookcli.go
Normal file
67
pkg/auth/cli/webhookcli.go
Normal file
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
146
pkg/auth/filter.go
Normal file
146
pkg/auth/filter.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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{
|
||||
|
72
pkg/debug/cli.go
Normal file
72
pkg/debug/cli.go
Normal file
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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]
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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{
|
||||
schema = &types.APISchema{
|
||||
Schema: &schemas.Schema{
|
||||
ID: GVKToSchemaID(gvk),
|
||||
Type: "schema",
|
||||
Dynamic: true,
|
||||
},
|
||||
}
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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{
|
||||
func modelToSchema(modelName string, k *proto.Kind) *types.APISchema {
|
||||
s := types.APISchema{
|
||||
Schema: &schemas.Schema{
|
||||
ID: modelName,
|
||||
Type: "schema",
|
||||
ResourceFields: map[string]types.Field{},
|
||||
ResourceFields: map[string]schemas.Field{},
|
||||
Attributes: map[string]interface{}{},
|
||||
Description: k.GetDescription(),
|
||||
Dynamic: true,
|
||||
},
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
80
pkg/schema/converter/openapiv3.go
Normal file
80
pkg/schema/converter/openapiv3.go
Normal file
@ -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
|
||||
}
|
@ -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) {
|
||||
|
@ -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...))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
88
pkg/schemaserver/builtin/schema.go
Normal file
88
pkg/schemaserver/builtin/schema.go
Normal file
@ -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 ""
|
||||
}
|
33
pkg/schemaserver/handlers/create.go
Normal file
33
pkg/schemaserver/handlers/create.go
Normal file
@ -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
|
||||
}
|
20
pkg/schemaserver/handlers/delete.go
Normal file
20
pkg/schemaserver/handlers/delete.go
Normal file
@ -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)
|
||||
}
|
65
pkg/schemaserver/handlers/error.go
Normal file
65
pkg/schemaserver/handlers/error.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
53
pkg/schemaserver/handlers/list.go
Normal file
53
pkg/schemaserver/handlers/list.go
Normal file
@ -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
|
||||
}
|
39
pkg/schemaserver/handlers/update.go
Normal file
39
pkg/schemaserver/handlers/update.go
Normal file
@ -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
|
||||
}
|
70
pkg/schemaserver/httperror/error.go
Normal file
70
pkg/schemaserver/httperror/error.go
Normal file
@ -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
|
||||
}
|
18
pkg/schemaserver/parse/browser.go
Normal file
18
pkg/schemaserver/parse/browser.go
Normal file
@ -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, "*/*")
|
||||
}
|
23
pkg/schemaserver/parse/mux.go
Normal file
23
pkg/schemaserver/parse/mux.go
Normal file
@ -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
|
||||
}
|
168
pkg/schemaserver/parse/parse.go
Normal file
168
pkg/schemaserver/parse/parse.go
Normal file
@ -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)
|
||||
}
|
56
pkg/schemaserver/parse/read_input.go
Normal file
56
pkg/schemaserver/parse/read_input.go
Normal file
@ -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
|
||||
}
|
47
pkg/schemaserver/parse/validate.go
Normal file
47
pkg/schemaserver/parse/validate.go
Normal file
@ -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))
|
||||
}
|
59
pkg/schemaserver/server/access.go
Normal file
59
pkg/schemaserver/server/access.go
Normal file
@ -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
|
||||
}
|
258
pkg/schemaserver/server/server.go
Normal file
258
pkg/schemaserver/server/server.go
Normal file
@ -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
|
||||
}
|
78
pkg/schemaserver/server/validate.go
Normal file
78
pkg/schemaserver/server/validate.go
Normal file
@ -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
|
||||
}
|
135
pkg/schemaserver/store/apiroot/apiroot.go
Normal file
135
pkg/schemaserver/store/apiroot/apiroot.go
Normal file
@ -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
|
||||
}
|
33
pkg/schemaserver/store/empty/empty_store.go
Normal file
33
pkg/schemaserver/store/empty/empty_store.go
Normal file
@ -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
|
||||
}
|
100
pkg/schemaserver/store/schema/schema_store.go
Normal file
100
pkg/schemaserver/store/schema/schema_store.go
Normal file
@ -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
|
||||
}
|
53
pkg/schemaserver/subscribe/convert.go
Normal file
53
pkg/schemaserver/subscribe/convert.go
Normal file
@ -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
|
||||
}
|
80
pkg/schemaserver/subscribe/handler.go
Normal file
80
pkg/schemaserver/subscribe/handler.go
Normal file
@ -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)
|
||||
}
|
16
pkg/schemaserver/subscribe/register.go
Normal file
16
pkg/schemaserver/subscribe/register.go
Normal file
@ -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"
|
||||
})
|
||||
}
|
140
pkg/schemaserver/subscribe/watcher.go
Normal file
140
pkg/schemaserver/subscribe/watcher.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
25
pkg/schemaserver/types/encoder.go
Normal file
25
pkg/schemaserver/types/encoder.go
Normal file
@ -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
|
||||
}
|
70
pkg/schemaserver/types/schemas.go
Normal file
70
pkg/schemaserver/types/schemas.go
Normal file
@ -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)]
|
||||
}
|
278
pkg/schemaserver/types/server_types.go
Normal file
278
pkg/schemaserver/types/server_types.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
86
pkg/schemaserver/types/types.go
Normal file
86
pkg/schemaserver/types/types.go
Normal file
@ -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)
|
||||
}
|
84
pkg/schemaserver/urlbuilder/base.go
Normal file
84
pkg/schemaserver/urlbuilder/base.go
Normal file
@ -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
|
||||
}
|
124
pkg/schemaserver/urlbuilder/url.go
Normal file
124
pkg/schemaserver/urlbuilder/url.go
Normal file
@ -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)
|
||||
}
|
124
pkg/schemaserver/writer/encoding.go
Normal file
124
pkg/schemaserver/writer/encoding.go
Normal file
@ -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
|
||||
}
|
24
pkg/schemaserver/writer/headers.go
Normal file
24
pkg/schemaserver/writer/headers.go
Normal file
@ -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")
|
||||
}
|
85
pkg/schemaserver/writer/html.go
Normal file
85
pkg/schemaserver/writer/html.go
Normal file
@ -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 = `
|
||||
<!DOCTYPE html>
|
||||
<!-- If you are reading this, there is a good chance you would prefer sending an
|
||||
"Accept: application/json" header and receiving actual JSON responses. -->
|
||||
<link rel="stylesheet" type="text/css" href="%CSSURL%" />
|
||||
<script src="%JSURL%"></script>
|
||||
<script>
|
||||
var user = "admin";
|
||||
var curlUser='${CATTLE_ACCESS_KEY}:${CATTLE_SECRET_KEY}';
|
||||
var schemas="%SCHEMAS%";
|
||||
var data =
|
||||
`
|
||||
end = []byte(`</script>
|
||||
`)
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
73
pkg/server/cli/clicontext.go
Normal file
73
pkg/server/cli/clicontext.go
Normal file
@ -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)...)
|
||||
}
|
89
pkg/server/config.go
Normal file
89
pkg/server/config.go
Normal file
@ -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
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
|
111
pkg/server/resources/common/dynamiccolumns.go
Normal file
111
pkg/server/resources/common/dynamiccolumns.go
Normal file
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 err
|
||||
return nil, nil, 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)
|
||||
server.PostStartHooks = append(server.PostStartHooks, func() error {
|
||||
return sync()
|
||||
})
|
||||
|
||||
return handler, sf, nil
|
||||
}
|
||||
|
||||
type controllers interface {
|
||||
Controllers() map[schema2.GroupVersionKind]*generic.Controller
|
||||
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
|
||||
}
|
||||
|
||||
return ListenAndServe(ctx, c.Core.Secret(), c.Namespace, handler, httpsPort, httpPort, opts)
|
||||
}
|
||||
|
66
pkg/server/store/proxy/client.go
Normal file
66
pkg/server/store/proxy/client.go
Normal file
@ -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)
|
||||
}
|
56
pkg/server/store/proxy/error_wrapper.go
Normal file
56
pkg/server/store/proxy/error_wrapper.go
Normal file
@ -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
|
||||
}
|
303
pkg/server/store/proxy/proxy_store.go
Normal file
303
pkg/server/store/proxy/proxy_store.go
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user