1
0
mirror of https://github.com/rancher/steve.git synced 2025-04-27 02:51:10 +00:00
This commit is contained in:
Darren Shepherd 2020-01-30 22:37:59 -07:00
parent 19c6732de0
commit 8b42d0aff8
71 changed files with 4024 additions and 507 deletions

View File

@ -1,3 +1,4 @@
./.certs
./.dapper
./.cache
./dist

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
/.dapper
/.cache
/certs
/bin
/dist
*.swp

View File

@ -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

4
Riofile Normal file
View File

@ -0,0 +1,4 @@
services:
steve:
ports:
- 80:8080

7
go.mod
View File

@ -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
View File

@ -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
View File

@ -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)
}

View File

@ -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")

View File

@ -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
}

View File

@ -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{}{}
}

View 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
View 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)
}
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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
View 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,
},
}
}

View File

@ -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 {

View File

@ -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]
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
}

View 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
}

View File

@ -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) {

View File

@ -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...))
}
}
}

View 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 ""
}

View 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
}

View 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)
}

View 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,
}
}

View 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
}

View 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
}

View 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
}

View 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, "*/*")
}

View 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
}

View 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)
}

View 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
}

View 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))
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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"
})
}

View 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,
}
}

View 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
}

View 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)]
}

View 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)
}
}

View 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)
}

View 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
}

View 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)
}

View 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
}

View 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")
}

View 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)
}
}

View 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
View 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

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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) {
func (e *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
return types.DefaultByID(e, apiOp, schema, id)
}
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,
}
}
func (e *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
groupList, err := e.discovery.ServerGroups()
if err != nil {
return types.APIObject{}, err
return types.APIObjectList{}, 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) 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
}
var result []interface{}
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
}

View File

@ -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,
})

View 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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 baseSchema
}
return collection, nil
func DefaultSchemaTemplates(cf *client.Factory) []schema.Template {
return []schema.Template{
common.DefaultTemplate(cf),
}
}

View File

@ -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
}

View File

@ -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
}
func Run(ctx context.Context, cfg Config) error {
restConfig, err := kubeconfig.GetNonInteractiveClientConfig(cfg.Kubeconfig).ClientConfig()
if server.Namespace == "" {
server.Namespace = "steve"
}
if server.Controllers == nil {
var err error
server.Controllers, err = NewController(server.RestConfig)
if err != nil {
return err
}
restConfig.QPS = 100
restConfig.Burst = 100
rbac, err := rbaccontroller.NewFactoryFromConfig(restConfig)
if err != nil {
return err
}
core, err := core.NewFactoryFromConfig(restConfig)
if err != nil {
return err
if server.Next == nil {
server.Next = http.NotFoundHandler()
}
k8s, err := kubernetes.NewForConfig(restConfig)
if err != nil {
return err
if server.BaseSchemas == nil {
server.BaseSchemas = types.EmptyAPISchemas()
}
api, err := apiregistration.NewFactoryFromConfig(restConfig)
if err != nil {
return err
return nil
}
crd, err := apiextensions.NewFactoryFromConfig(restConfig)
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
}
cf, err := client.NewFactory(restConfig)
cf, err := client.NewFactory(server.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)
}

View 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)
}

View 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
}

View 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
}