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