mirror of
https://github.com/kubernetes-sigs/prometheus-adapter.git
synced 2026-04-07 10:17:51 +00:00
Add vendor folder to git
This commit is contained in:
parent
66cf5eaafb
commit
183585f56f
6916 changed files with 2629581 additions and 1 deletions
81
vendor/k8s.io/apiserver/pkg/endpoints/BUILD
generated
vendored
Normal file
81
vendor/k8s.io/apiserver/pkg/endpoints/BUILD
generated
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"apiserver_test.go",
|
||||
"installer_test.go",
|
||||
"proxy_test.go",
|
||||
"watch_test.go",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/emicklei/go-restful:go_default_library",
|
||||
"//vendor/golang.org/x/net/websocket:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/testing:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer/streaming:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/apis/example:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/apis/example/fuzzer:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/apis/example/v1:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/filters:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/handlers:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/testing:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"apiserver.go",
|
||||
"doc.go",
|
||||
"groupversion.go",
|
||||
"installer.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/emicklei/go-restful:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/discovery:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/handlers:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/metrics:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||
],
|
||||
)
|
||||
41
vendor/k8s.io/apiserver/pkg/endpoints/OWNERS
generated
vendored
Normal file
41
vendor/k8s.io/apiserver/pkg/endpoints/OWNERS
generated
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
approvers:
|
||||
- lavalamp
|
||||
- nikhiljindal
|
||||
- smarterclayton
|
||||
- deads2k
|
||||
- sttts
|
||||
reviewers:
|
||||
- thockin
|
||||
- lavalamp
|
||||
- smarterclayton
|
||||
- wojtek-t
|
||||
- deads2k
|
||||
- derekwaynecarr
|
||||
- caesarxuchao
|
||||
- mikedanese
|
||||
- liggitt
|
||||
- nikhiljindal
|
||||
- bprashanth
|
||||
- gmarek
|
||||
- erictune
|
||||
- davidopp
|
||||
- sttts
|
||||
- zmerlynn
|
||||
- luxas
|
||||
- roberthbailey
|
||||
- ncdc
|
||||
- timstclair
|
||||
- yifan-gu
|
||||
- timothysc
|
||||
- feiskyer
|
||||
- soltysh
|
||||
- piosz
|
||||
- jbeda
|
||||
- dims
|
||||
- errordeveloper
|
||||
- madhusudancs
|
||||
- hongchaodeng
|
||||
- krousey
|
||||
- vmarmol
|
||||
- fgrzadkowski
|
||||
- xiang90
|
||||
25
vendor/k8s.io/apiserver/pkg/endpoints/apiserver.go
generated
vendored
Normal file
25
vendor/k8s.io/apiserver/pkg/endpoints/apiserver.go
generated
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"k8s.io/apiserver/pkg/endpoints/metrics"
|
||||
)
|
||||
|
||||
func init() {
|
||||
metrics.Register()
|
||||
}
|
||||
3598
vendor/k8s.io/apiserver/pkg/endpoints/apiserver_test.go
generated
vendored
Normal file
3598
vendor/k8s.io/apiserver/pkg/endpoints/apiserver_test.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
51
vendor/k8s.io/apiserver/pkg/endpoints/discovery/BUILD
generated
vendored
Normal file
51
vendor/k8s.io/apiserver/pkg/endpoints/discovery/BUILD
generated
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"addresses_test.go",
|
||||
"root_test.go",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/stretchr/testify/assert:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"addresses.go",
|
||||
"group.go",
|
||||
"legacy.go",
|
||||
"root.go",
|
||||
"util.go",
|
||||
"version.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/emicklei/go-restful:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library",
|
||||
],
|
||||
)
|
||||
72
vendor/k8s.io/apiserver/pkg/endpoints/discovery/addresses.go
generated
vendored
Normal file
72
vendor/k8s.io/apiserver/pkg/endpoints/discovery/addresses.go
generated
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type Addresses interface {
|
||||
ServerAddressByClientCIDRs(net.IP) []metav1.ServerAddressByClientCIDR
|
||||
}
|
||||
|
||||
// DefaultAddresses is a default implementation of Addresses that will work in most cases
|
||||
type DefaultAddresses struct {
|
||||
// CIDRRules is a list of CIDRs and Addresses to use if a client is in the range
|
||||
CIDRRules []CIDRRule
|
||||
|
||||
// DefaultAddress is the address (hostname or IP and port) that should be used in
|
||||
// if no CIDR matches more specifically.
|
||||
DefaultAddress string
|
||||
}
|
||||
|
||||
// CIDRRule is a rule for adding an alternate path to the master based on matching CIDR
|
||||
type CIDRRule struct {
|
||||
IPRange net.IPNet
|
||||
|
||||
// Address is the address (hostname or IP and port) that should be used in
|
||||
// if this CIDR matches
|
||||
Address string
|
||||
}
|
||||
|
||||
func (d DefaultAddresses) ServerAddressByClientCIDRs(clientIP net.IP) []metav1.ServerAddressByClientCIDR {
|
||||
addressCIDRMap := []metav1.ServerAddressByClientCIDR{
|
||||
{
|
||||
ClientCIDR: "0.0.0.0/0",
|
||||
ServerAddress: d.DefaultAddress,
|
||||
},
|
||||
}
|
||||
|
||||
for _, rule := range d.CIDRRules {
|
||||
addressCIDRMap = append(addressCIDRMap, rule.ServerAddressByClientCIDRs(clientIP)...)
|
||||
}
|
||||
return addressCIDRMap
|
||||
}
|
||||
|
||||
func (d CIDRRule) ServerAddressByClientCIDRs(clientIP net.IP) []metav1.ServerAddressByClientCIDR {
|
||||
addressCIDRMap := []metav1.ServerAddressByClientCIDR{}
|
||||
|
||||
if d.IPRange.Contains(clientIP) {
|
||||
addressCIDRMap = append(addressCIDRMap, metav1.ServerAddressByClientCIDR{
|
||||
ClientCIDR: d.IPRange.String(),
|
||||
ServerAddress: d.Address,
|
||||
})
|
||||
}
|
||||
return addressCIDRMap
|
||||
}
|
||||
116
vendor/k8s.io/apiserver/pkg/endpoints/discovery/addresses_test.go
generated
vendored
Normal file
116
vendor/k8s.io/apiserver/pkg/endpoints/discovery/addresses_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
)
|
||||
|
||||
func TestGetServerAddressByClientCIDRs(t *testing.T) {
|
||||
publicAddressCIDRMap := []metav1.ServerAddressByClientCIDR{
|
||||
{
|
||||
ClientCIDR: "0.0.0.0/0",
|
||||
ServerAddress: "ExternalAddress",
|
||||
},
|
||||
}
|
||||
internalAddressCIDRMap := []metav1.ServerAddressByClientCIDR{
|
||||
publicAddressCIDRMap[0],
|
||||
{
|
||||
ClientCIDR: "10.0.0.0/24",
|
||||
ServerAddress: "serviceIP",
|
||||
},
|
||||
}
|
||||
internalIP := "10.0.0.1"
|
||||
publicIP := "1.1.1.1"
|
||||
testCases := []struct {
|
||||
Request http.Request
|
||||
ExpectedMap []metav1.ServerAddressByClientCIDR
|
||||
}{
|
||||
{
|
||||
Request: http.Request{},
|
||||
ExpectedMap: publicAddressCIDRMap,
|
||||
},
|
||||
{
|
||||
Request: http.Request{
|
||||
Header: map[string][]string{
|
||||
"X-Real-Ip": {internalIP},
|
||||
},
|
||||
},
|
||||
ExpectedMap: internalAddressCIDRMap,
|
||||
},
|
||||
{
|
||||
Request: http.Request{
|
||||
Header: map[string][]string{
|
||||
"X-Real-Ip": {publicIP},
|
||||
},
|
||||
},
|
||||
ExpectedMap: publicAddressCIDRMap,
|
||||
},
|
||||
{
|
||||
Request: http.Request{
|
||||
Header: map[string][]string{
|
||||
"X-Forwarded-For": {internalIP},
|
||||
},
|
||||
},
|
||||
ExpectedMap: internalAddressCIDRMap,
|
||||
},
|
||||
{
|
||||
Request: http.Request{
|
||||
Header: map[string][]string{
|
||||
"X-Forwarded-For": {publicIP},
|
||||
},
|
||||
},
|
||||
ExpectedMap: publicAddressCIDRMap,
|
||||
},
|
||||
|
||||
{
|
||||
Request: http.Request{
|
||||
RemoteAddr: internalIP,
|
||||
},
|
||||
ExpectedMap: internalAddressCIDRMap,
|
||||
},
|
||||
{
|
||||
Request: http.Request{
|
||||
RemoteAddr: publicIP,
|
||||
},
|
||||
ExpectedMap: publicAddressCIDRMap,
|
||||
},
|
||||
{
|
||||
Request: http.Request{
|
||||
RemoteAddr: "invalidIP",
|
||||
},
|
||||
ExpectedMap: publicAddressCIDRMap,
|
||||
},
|
||||
}
|
||||
|
||||
_, ipRange, _ := net.ParseCIDR("10.0.0.0/24")
|
||||
discoveryAddresses := DefaultAddresses{DefaultAddress: "ExternalAddress"}
|
||||
discoveryAddresses.CIDRRules = append(discoveryAddresses.CIDRRules,
|
||||
CIDRRule{IPRange: *ipRange, Address: "serviceIP"})
|
||||
|
||||
for i, test := range testCases {
|
||||
if a, e := discoveryAddresses.ServerAddressByClientCIDRs(utilnet.GetClientIP(&test.Request)), test.ExpectedMap; reflect.DeepEqual(e, a) != true {
|
||||
t.Fatalf("test case %d failed. expected: %v, actual: %v", i+1, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
74
vendor/k8s.io/apiserver/pkg/endpoints/discovery/group.go
generated
vendored
Normal file
74
vendor/k8s.io/apiserver/pkg/endpoints/discovery/group.go
generated
vendored
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
)
|
||||
|
||||
// APIGroupHandler creates a webservice serving the supported versions, preferred version, and name
|
||||
// of a group. E.g., such a web service will be registered at /apis/extensions.
|
||||
type APIGroupHandler struct {
|
||||
serializer runtime.NegotiatedSerializer
|
||||
|
||||
group metav1.APIGroup
|
||||
}
|
||||
|
||||
func NewAPIGroupHandler(serializer runtime.NegotiatedSerializer, group metav1.APIGroup) *APIGroupHandler {
|
||||
if keepUnversioned(group.Name) {
|
||||
// Because in release 1.1, /apis/extensions returns response with empty
|
||||
// APIVersion, we use stripVersionNegotiatedSerializer to keep the
|
||||
// response backwards compatible.
|
||||
serializer = stripVersionNegotiatedSerializer{serializer}
|
||||
}
|
||||
|
||||
return &APIGroupHandler{
|
||||
serializer: serializer,
|
||||
group: group,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *APIGroupHandler) WebService() *restful.WebService {
|
||||
mediaTypes, _ := negotiation.MediaTypesForSerializer(s.serializer)
|
||||
ws := new(restful.WebService)
|
||||
ws.Path(APIGroupPrefix + "/" + s.group.Name)
|
||||
ws.Doc("get information of a group")
|
||||
ws.Route(ws.GET("/").To(s.handle).
|
||||
Doc("get information of a group").
|
||||
Operation("getAPIGroup").
|
||||
Produces(mediaTypes...).
|
||||
Consumes(mediaTypes...).
|
||||
Writes(metav1.APIGroup{}))
|
||||
return ws
|
||||
}
|
||||
|
||||
// handle returns a handler which will return the api.GroupAndVersion of the group.
|
||||
func (s *APIGroupHandler) handle(req *restful.Request, resp *restful.Response) {
|
||||
s.ServeHTTP(resp.ResponseWriter, req.Request)
|
||||
}
|
||||
|
||||
func (s *APIGroupHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
responsewriters.WriteObjectNegotiated(s.serializer, schema.GroupVersion{}, w, req, http.StatusOK, &s.group)
|
||||
}
|
||||
78
vendor/k8s.io/apiserver/pkg/endpoints/discovery/legacy.go
generated
vendored
Normal file
78
vendor/k8s.io/apiserver/pkg/endpoints/discovery/legacy.go
generated
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
)
|
||||
|
||||
// legacyRootAPIHandler creates a webservice serving api group discovery.
|
||||
type legacyRootAPIHandler struct {
|
||||
// addresses is used to build cluster IPs for discovery.
|
||||
addresses Addresses
|
||||
apiPrefix string
|
||||
serializer runtime.NegotiatedSerializer
|
||||
apiVersions []string
|
||||
}
|
||||
|
||||
func NewLegacyRootAPIHandler(addresses Addresses, serializer runtime.NegotiatedSerializer, apiPrefix string, apiVersions []string) *legacyRootAPIHandler {
|
||||
// Because in release 1.1, /apis returns response with empty APIVersion, we
|
||||
// use stripVersionNegotiatedSerializer to keep the response backwards
|
||||
// compatible.
|
||||
serializer = stripVersionNegotiatedSerializer{serializer}
|
||||
|
||||
return &legacyRootAPIHandler{
|
||||
addresses: addresses,
|
||||
apiPrefix: apiPrefix,
|
||||
serializer: serializer,
|
||||
apiVersions: apiVersions,
|
||||
}
|
||||
}
|
||||
|
||||
// AddApiWebService adds a service to return the supported api versions at the legacy /api.
|
||||
func (s *legacyRootAPIHandler) WebService() *restful.WebService {
|
||||
mediaTypes, _ := negotiation.MediaTypesForSerializer(s.serializer)
|
||||
ws := new(restful.WebService)
|
||||
ws.Path(s.apiPrefix)
|
||||
ws.Doc("get available API versions")
|
||||
ws.Route(ws.GET("/").To(s.handle).
|
||||
Doc("get available API versions").
|
||||
Operation("getAPIVersions").
|
||||
Produces(mediaTypes...).
|
||||
Consumes(mediaTypes...).
|
||||
Writes(metav1.APIVersions{}))
|
||||
return ws
|
||||
}
|
||||
|
||||
func (s *legacyRootAPIHandler) handle(req *restful.Request, resp *restful.Response) {
|
||||
clientIP := utilnet.GetClientIP(req.Request)
|
||||
apiVersions := &metav1.APIVersions{
|
||||
ServerAddressByClientCIDRs: s.addresses.ServerAddressByClientCIDRs(clientIP),
|
||||
Versions: s.apiVersions,
|
||||
}
|
||||
|
||||
responsewriters.WriteObjectNegotiated(s.serializer, schema.GroupVersion{}, resp.ResponseWriter, req.Request, http.StatusOK, apiVersions)
|
||||
}
|
||||
135
vendor/k8s.io/apiserver/pkg/endpoints/discovery/root.go
generated
vendored
Normal file
135
vendor/k8s.io/apiserver/pkg/endpoints/discovery/root.go
generated
vendored
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
)
|
||||
|
||||
// GroupManager is an interface that allows dynamic mutation of the existing webservice to handle
|
||||
// API groups being added or removed.
|
||||
type GroupManager interface {
|
||||
AddGroup(apiGroup metav1.APIGroup)
|
||||
RemoveGroup(groupName string)
|
||||
|
||||
WebService() *restful.WebService
|
||||
}
|
||||
|
||||
// rootAPIsHandler creates a webservice serving api group discovery.
|
||||
// The list of APIGroups may change while the server is running because additional resources
|
||||
// are registered or removed. It is not safe to cache the values.
|
||||
type rootAPIsHandler struct {
|
||||
// addresses is used to build cluster IPs for discovery.
|
||||
addresses Addresses
|
||||
|
||||
serializer runtime.NegotiatedSerializer
|
||||
|
||||
// Map storing information about all groups to be exposed in discovery response.
|
||||
// The map is from name to the group.
|
||||
lock sync.RWMutex
|
||||
apiGroups map[string]metav1.APIGroup
|
||||
// apiGroupNames preserves insertion order
|
||||
apiGroupNames []string
|
||||
}
|
||||
|
||||
func NewRootAPIsHandler(addresses Addresses, serializer runtime.NegotiatedSerializer) *rootAPIsHandler {
|
||||
// Because in release 1.1, /apis returns response with empty APIVersion, we
|
||||
// use stripVersionNegotiatedSerializer to keep the response backwards
|
||||
// compatible.
|
||||
serializer = stripVersionNegotiatedSerializer{serializer}
|
||||
|
||||
return &rootAPIsHandler{
|
||||
addresses: addresses,
|
||||
serializer: serializer,
|
||||
apiGroups: map[string]metav1.APIGroup{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *rootAPIsHandler) AddGroup(apiGroup metav1.APIGroup) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
_, alreadyExists := s.apiGroups[apiGroup.Name]
|
||||
|
||||
s.apiGroups[apiGroup.Name] = apiGroup
|
||||
if !alreadyExists {
|
||||
s.apiGroupNames = append(s.apiGroupNames, apiGroup.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *rootAPIsHandler) RemoveGroup(groupName string) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
delete(s.apiGroups, groupName)
|
||||
for i := range s.apiGroupNames {
|
||||
if s.apiGroupNames[i] == groupName {
|
||||
s.apiGroupNames = append(s.apiGroupNames[:i], s.apiGroupNames[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *rootAPIsHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
orderedGroups := []metav1.APIGroup{}
|
||||
for _, groupName := range s.apiGroupNames {
|
||||
orderedGroups = append(orderedGroups, s.apiGroups[groupName])
|
||||
}
|
||||
|
||||
clientIP := utilnet.GetClientIP(req)
|
||||
serverCIDR := s.addresses.ServerAddressByClientCIDRs(clientIP)
|
||||
groups := make([]metav1.APIGroup, len(orderedGroups))
|
||||
for i := range orderedGroups {
|
||||
groups[i] = orderedGroups[i]
|
||||
groups[i].ServerAddressByClientCIDRs = serverCIDR
|
||||
}
|
||||
|
||||
responsewriters.WriteObjectNegotiated(s.serializer, schema.GroupVersion{}, resp, req, http.StatusOK, &metav1.APIGroupList{Groups: groups})
|
||||
}
|
||||
|
||||
func (s *rootAPIsHandler) restfulHandle(req *restful.Request, resp *restful.Response) {
|
||||
s.ServeHTTP(resp.ResponseWriter, req.Request)
|
||||
}
|
||||
|
||||
// WebService returns a webservice serving api group discovery.
|
||||
// Note: during the server runtime apiGroups might change.
|
||||
func (s *rootAPIsHandler) WebService() *restful.WebService {
|
||||
mediaTypes, _ := negotiation.MediaTypesForSerializer(s.serializer)
|
||||
ws := new(restful.WebService)
|
||||
ws.Path(APIGroupPrefix)
|
||||
ws.Doc("get available API versions")
|
||||
ws.Route(ws.GET("/").To(s.restfulHandle).
|
||||
Doc("get available API versions").
|
||||
Operation("getAPIVersions").
|
||||
Produces(mediaTypes...).
|
||||
Consumes(mediaTypes...).
|
||||
Writes(metav1.APIGroupList{}))
|
||||
return ws
|
||||
}
|
||||
190
vendor/k8s.io/apiserver/pkg/endpoints/discovery/root_test.go
generated
vendored
Normal file
190
vendor/k8s.io/apiserver/pkg/endpoints/discovery/root_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apimachinery/announced"
|
||||
"k8s.io/apimachinery/pkg/apimachinery/registered"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
)
|
||||
|
||||
var (
|
||||
groupFactoryRegistry = make(announced.APIGroupFactoryRegistry)
|
||||
registry = registered.NewOrDie("")
|
||||
scheme = runtime.NewScheme()
|
||||
codecs = serializer.NewCodecFactory(scheme)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register Unversioned types under their own special group
|
||||
scheme.AddUnversionedTypes(schema.GroupVersion{Group: "", Version: "v1"},
|
||||
&metav1.Status{},
|
||||
&metav1.APIVersions{},
|
||||
&metav1.APIGroupList{},
|
||||
&metav1.APIGroup{},
|
||||
&metav1.APIResourceList{},
|
||||
)
|
||||
}
|
||||
|
||||
func decodeResponse(t *testing.T, resp *http.Response, obj interface{}) error {
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
t.Log(string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(data, obj); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getGroupList(t *testing.T, server *httptest.Server) (*metav1.APIGroupList, error) {
|
||||
resp, err := http.Get(server.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected server response, expected %d, actual: %d", http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
groupList := metav1.APIGroupList{}
|
||||
err = decodeResponse(t, resp, &groupList)
|
||||
return &groupList, err
|
||||
}
|
||||
|
||||
func TestDiscoveryAtAPIS(t *testing.T) {
|
||||
handler := NewRootAPIsHandler(DefaultAddresses{DefaultAddress: "192.168.1.1"}, codecs)
|
||||
|
||||
server := httptest.NewServer(handler)
|
||||
groupList, err := getGroupList(t, server)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
assert.Equal(t, 0, len(groupList.Groups))
|
||||
|
||||
// Add a Group.
|
||||
extensionsGroupName := "extensions"
|
||||
extensionsVersions := []metav1.GroupVersionForDiscovery{
|
||||
{
|
||||
GroupVersion: extensionsGroupName + "/v1",
|
||||
Version: "v1",
|
||||
},
|
||||
}
|
||||
extensionsPreferredVersion := metav1.GroupVersionForDiscovery{
|
||||
GroupVersion: extensionsGroupName + "/preferred",
|
||||
Version: "preferred",
|
||||
}
|
||||
handler.AddGroup(metav1.APIGroup{
|
||||
Name: extensionsGroupName,
|
||||
Versions: extensionsVersions,
|
||||
PreferredVersion: extensionsPreferredVersion,
|
||||
})
|
||||
|
||||
groupList, err = getGroupList(t, server)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(groupList.Groups))
|
||||
groupListGroup := groupList.Groups[0]
|
||||
assert.Equal(t, extensionsGroupName, groupListGroup.Name)
|
||||
assert.Equal(t, extensionsVersions, groupListGroup.Versions)
|
||||
assert.Equal(t, extensionsPreferredVersion, groupListGroup.PreferredVersion)
|
||||
assert.Equal(t, handler.addresses.ServerAddressByClientCIDRs(utilnet.GetClientIP(&http.Request{})), groupListGroup.ServerAddressByClientCIDRs)
|
||||
|
||||
// Remove the group.
|
||||
handler.RemoveGroup(extensionsGroupName)
|
||||
groupList, err = getGroupList(t, server)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, len(groupList.Groups))
|
||||
}
|
||||
|
||||
func TestDiscoveryOrdering(t *testing.T) {
|
||||
handler := NewRootAPIsHandler(DefaultAddresses{DefaultAddress: "192.168.1.1"}, codecs)
|
||||
|
||||
server := httptest.NewServer(handler)
|
||||
groupList, err := getGroupList(t, server)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
assert.Equal(t, 0, len(groupList.Groups))
|
||||
|
||||
// Register three groups
|
||||
handler.AddGroup(metav1.APIGroup{Name: "x"})
|
||||
handler.AddGroup(metav1.APIGroup{Name: "y"})
|
||||
handler.AddGroup(metav1.APIGroup{Name: "z"})
|
||||
// Register three additional groups that come earlier alphabetically
|
||||
handler.AddGroup(metav1.APIGroup{Name: "a"})
|
||||
handler.AddGroup(metav1.APIGroup{Name: "b"})
|
||||
handler.AddGroup(metav1.APIGroup{Name: "c"})
|
||||
// Make sure re-adding doesn't double-register or make a group lose its place
|
||||
handler.AddGroup(metav1.APIGroup{Name: "x"})
|
||||
|
||||
groupList, err = getGroupList(t, server)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 6, len(groupList.Groups))
|
||||
assert.Equal(t, "x", groupList.Groups[0].Name)
|
||||
assert.Equal(t, "y", groupList.Groups[1].Name)
|
||||
assert.Equal(t, "z", groupList.Groups[2].Name)
|
||||
assert.Equal(t, "a", groupList.Groups[3].Name)
|
||||
assert.Equal(t, "b", groupList.Groups[4].Name)
|
||||
assert.Equal(t, "c", groupList.Groups[5].Name)
|
||||
|
||||
// Remove a group.
|
||||
handler.RemoveGroup("a")
|
||||
groupList, err = getGroupList(t, server)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
assert.Equal(t, 5, len(groupList.Groups))
|
||||
|
||||
// Re-adding should move to the end.
|
||||
handler.AddGroup(metav1.APIGroup{Name: "a"})
|
||||
groupList, err = getGroupList(t, server)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
assert.Equal(t, 6, len(groupList.Groups))
|
||||
assert.Equal(t, "x", groupList.Groups[0].Name)
|
||||
assert.Equal(t, "y", groupList.Groups[1].Name)
|
||||
assert.Equal(t, "z", groupList.Groups[2].Name)
|
||||
assert.Equal(t, "b", groupList.Groups[3].Name)
|
||||
assert.Equal(t, "c", groupList.Groups[4].Name)
|
||||
assert.Equal(t, "a", groupList.Groups[5].Name)
|
||||
}
|
||||
73
vendor/k8s.io/apiserver/pkg/endpoints/discovery/util.go
generated
vendored
Normal file
73
vendor/k8s.io/apiserver/pkg/endpoints/discovery/util.go
generated
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
const APIGroupPrefix = "/apis"
|
||||
|
||||
func keepUnversioned(group string) bool {
|
||||
return group == "" || group == "extensions"
|
||||
}
|
||||
|
||||
// stripVersionEncoder strips APIVersion field from the encoding output. It's
|
||||
// used to keep the responses at the discovery endpoints backward compatible
|
||||
// with release-1.1, when the responses have empty APIVersion.
|
||||
type stripVersionEncoder struct {
|
||||
encoder runtime.Encoder
|
||||
serializer runtime.Serializer
|
||||
}
|
||||
|
||||
func (c stripVersionEncoder) Encode(obj runtime.Object, w io.Writer) error {
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err := c.encoder.Encode(obj, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
roundTrippedObj, gvk, err := c.serializer.Decode(buf.Bytes(), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gvk.Group = ""
|
||||
gvk.Version = ""
|
||||
roundTrippedObj.GetObjectKind().SetGroupVersionKind(*gvk)
|
||||
return c.serializer.Encode(roundTrippedObj, w)
|
||||
}
|
||||
|
||||
// stripVersionNegotiatedSerializer will return stripVersionEncoder when
|
||||
// EncoderForVersion is called. See comments for stripVersionEncoder.
|
||||
type stripVersionNegotiatedSerializer struct {
|
||||
runtime.NegotiatedSerializer
|
||||
}
|
||||
|
||||
func (n stripVersionNegotiatedSerializer) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
|
||||
serializer, ok := encoder.(runtime.Serializer)
|
||||
if !ok {
|
||||
// The stripVersionEncoder needs both an encoder and decoder, but is called from a context that doesn't have access to the
|
||||
// decoder. We do a best effort cast here (since this code path is only for backwards compatibility) to get access to the caller's
|
||||
// decoder.
|
||||
panic(fmt.Sprintf("Unable to extract serializer from %#v", encoder))
|
||||
}
|
||||
versioned := n.NegotiatedSerializer.EncoderForVersion(encoder, gv)
|
||||
return stripVersionEncoder{versioned, serializer}
|
||||
}
|
||||
83
vendor/k8s.io/apiserver/pkg/endpoints/discovery/version.go
generated
vendored
Normal file
83
vendor/k8s.io/apiserver/pkg/endpoints/discovery/version.go
generated
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
)
|
||||
|
||||
type APIResourceLister interface {
|
||||
ListAPIResources() []metav1.APIResource
|
||||
}
|
||||
|
||||
type APIResourceListerFunc func() []metav1.APIResource
|
||||
|
||||
func (f APIResourceListerFunc) ListAPIResources() []metav1.APIResource {
|
||||
return f()
|
||||
}
|
||||
|
||||
// APIVersionHandler creates a webservice serving the supported resources for the version
|
||||
// E.g., such a web service will be registered at /apis/extensions/v1beta1.
|
||||
type APIVersionHandler struct {
|
||||
serializer runtime.NegotiatedSerializer
|
||||
|
||||
groupVersion schema.GroupVersion
|
||||
apiResourceLister APIResourceLister
|
||||
}
|
||||
|
||||
func NewAPIVersionHandler(serializer runtime.NegotiatedSerializer, groupVersion schema.GroupVersion, apiResourceLister APIResourceLister) *APIVersionHandler {
|
||||
if keepUnversioned(groupVersion.Group) {
|
||||
// Because in release 1.1, /apis/extensions returns response with empty
|
||||
// APIVersion, we use stripVersionNegotiatedSerializer to keep the
|
||||
// response backwards compatible.
|
||||
serializer = stripVersionNegotiatedSerializer{serializer}
|
||||
}
|
||||
|
||||
return &APIVersionHandler{
|
||||
serializer: serializer,
|
||||
groupVersion: groupVersion,
|
||||
apiResourceLister: apiResourceLister,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *APIVersionHandler) AddToWebService(ws *restful.WebService) {
|
||||
mediaTypes, _ := negotiation.MediaTypesForSerializer(s.serializer)
|
||||
ws.Route(ws.GET("/").To(s.handle).
|
||||
Doc("get available resources").
|
||||
Operation("getAPIResources").
|
||||
Produces(mediaTypes...).
|
||||
Consumes(mediaTypes...).
|
||||
Writes(metav1.APIResourceList{}))
|
||||
}
|
||||
|
||||
// handle returns a handler which will return the api.VersionAndVersion of the group.
|
||||
func (s *APIVersionHandler) handle(req *restful.Request, resp *restful.Response) {
|
||||
s.ServeHTTP(resp.ResponseWriter, req.Request)
|
||||
}
|
||||
|
||||
func (s *APIVersionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
responsewriters.WriteObjectNegotiated(s.serializer, schema.GroupVersion{}, w, req, http.StatusOK,
|
||||
&metav1.APIResourceList{GroupVersion: s.groupVersion.String(), APIResources: s.apiResourceLister.ListAPIResources()})
|
||||
}
|
||||
18
vendor/k8s.io/apiserver/pkg/endpoints/doc.go
generated
vendored
Normal file
18
vendor/k8s.io/apiserver/pkg/endpoints/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package endpoints contains the generic code that provides a RESTful Kubernetes-style API service.
|
||||
package endpoints // import "k8s.io/apiserver/pkg/endpoints"
|
||||
60
vendor/k8s.io/apiserver/pkg/endpoints/filters/BUILD
generated
vendored
Normal file
60
vendor/k8s.io/apiserver/pkg/endpoints/filters/BUILD
generated
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"audit_test.go",
|
||||
"authentication_test.go",
|
||||
"authorization_test.go",
|
||||
"impersonation_test.go",
|
||||
"requestinfo_test.go",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
"//vendor/k8s.io/client-go/pkg/apis/authentication/v1:go_default_library",
|
||||
"//vendor/k8s.io/client-go/pkg/apis/batch/v1:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"audit.go",
|
||||
"authentication.go",
|
||||
"authorization.go",
|
||||
"doc.go",
|
||||
"impersonation.go",
|
||||
"requestinfo.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/github.com/pborman/uuid:go_default_library",
|
||||
"//vendor/github.com/prometheus/client_golang/prometheus:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/serviceaccount:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server/httplog:go_default_library",
|
||||
"//vendor/k8s.io/client-go/pkg/api/v1:go_default_library",
|
||||
"//vendor/k8s.io/client-go/pkg/apis/authentication/v1:go_default_library",
|
||||
],
|
||||
)
|
||||
4
vendor/k8s.io/apiserver/pkg/endpoints/filters/OWNERS
generated
vendored
Normal file
4
vendor/k8s.io/apiserver/pkg/endpoints/filters/OWNERS
generated
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
reviewers:
|
||||
- deads2k
|
||||
- sttts
|
||||
- soltysh
|
||||
162
vendor/k8s.io/apiserver/pkg/endpoints/filters/audit.go
generated
vendored
Normal file
162
vendor/k8s.io/apiserver/pkg/endpoints/filters/audit.go
generated
vendored
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package filters
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/pborman/uuid"
|
||||
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
authenticationapi "k8s.io/client-go/pkg/apis/authentication/v1"
|
||||
)
|
||||
|
||||
var _ http.ResponseWriter = &auditResponseWriter{}
|
||||
|
||||
type auditResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
out io.Writer
|
||||
id string
|
||||
}
|
||||
|
||||
func (a *auditResponseWriter) WriteHeader(code int) {
|
||||
line := fmt.Sprintf("%s AUDIT: id=%q response=\"%d\"\n", time.Now().Format(time.RFC3339Nano), a.id, code)
|
||||
if _, err := fmt.Fprint(a.out, line); err != nil {
|
||||
glog.Errorf("Unable to write audit log: %s, the error is: %v", line, err)
|
||||
}
|
||||
|
||||
a.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// fancyResponseWriterDelegator implements http.CloseNotifier, http.Flusher and
|
||||
// http.Hijacker which are needed to make certain http operation (e.g. watch, rsh, etc)
|
||||
// working.
|
||||
type fancyResponseWriterDelegator struct {
|
||||
*auditResponseWriter
|
||||
}
|
||||
|
||||
func (f *fancyResponseWriterDelegator) CloseNotify() <-chan bool {
|
||||
return f.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
||||
}
|
||||
|
||||
func (f *fancyResponseWriterDelegator) Flush() {
|
||||
f.ResponseWriter.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
func (f *fancyResponseWriterDelegator) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
return f.ResponseWriter.(http.Hijacker).Hijack()
|
||||
}
|
||||
|
||||
var _ http.CloseNotifier = &fancyResponseWriterDelegator{}
|
||||
var _ http.Flusher = &fancyResponseWriterDelegator{}
|
||||
var _ http.Hijacker = &fancyResponseWriterDelegator{}
|
||||
|
||||
// WithAudit decorates a http.Handler with audit logging information for all the
|
||||
// requests coming to the server. If out is nil, no decoration takes place.
|
||||
// Each audit log contains two entries:
|
||||
// 1. the request line containing:
|
||||
// - unique id allowing to match the response line (see 2)
|
||||
// - source ip of the request
|
||||
// - HTTP method being invoked
|
||||
// - original user invoking the operation
|
||||
// - original user's groups info
|
||||
// - impersonated user for the operation
|
||||
// - impersonated groups info
|
||||
// - namespace of the request or <none>
|
||||
// - uri is the full URI as requested
|
||||
// 2. the response line containing:
|
||||
// - the unique id from 1
|
||||
// - response code
|
||||
func WithAudit(handler http.Handler, requestContextMapper request.RequestContextMapper, out io.Writer) http.Handler {
|
||||
if out == nil {
|
||||
return handler
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx, ok := requestContextMapper.Get(req)
|
||||
if !ok {
|
||||
responsewriters.InternalError(w, req, errors.New("no context found for request"))
|
||||
return
|
||||
}
|
||||
attribs, err := GetAuthorizerAttributes(ctx)
|
||||
if err != nil {
|
||||
responsewriters.InternalError(w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
username := "<none>"
|
||||
groups := "<none>"
|
||||
if attribs.GetUser() != nil {
|
||||
username = attribs.GetUser().GetName()
|
||||
if userGroups := attribs.GetUser().GetGroups(); len(userGroups) > 0 {
|
||||
groups = auditStringSlice(userGroups)
|
||||
}
|
||||
}
|
||||
asuser := req.Header.Get(authenticationapi.ImpersonateUserHeader)
|
||||
if len(asuser) == 0 {
|
||||
asuser = "<self>"
|
||||
}
|
||||
asgroups := "<lookup>"
|
||||
requestedGroups := req.Header[authenticationapi.ImpersonateGroupHeader]
|
||||
if len(requestedGroups) > 0 {
|
||||
asgroups = auditStringSlice(requestedGroups)
|
||||
}
|
||||
namespace := attribs.GetNamespace()
|
||||
if len(namespace) == 0 {
|
||||
namespace = "<none>"
|
||||
}
|
||||
id := uuid.NewRandom().String()
|
||||
|
||||
line := fmt.Sprintf("%s AUDIT: id=%q ip=%q method=%q user=%q groups=%q as=%q asgroups=%q namespace=%q uri=%q\n",
|
||||
time.Now().Format(time.RFC3339Nano), id, utilnet.GetClientIP(req), req.Method, username, groups, asuser, asgroups, namespace, req.URL)
|
||||
if _, err := fmt.Fprint(out, line); err != nil {
|
||||
glog.Errorf("Unable to write audit log: %s, the error is: %v", line, err)
|
||||
}
|
||||
respWriter := decorateResponseWriter(w, out, id)
|
||||
handler.ServeHTTP(respWriter, req)
|
||||
})
|
||||
}
|
||||
|
||||
func auditStringSlice(inList []string) string {
|
||||
quotedElements := make([]string, len(inList))
|
||||
for i, in := range inList {
|
||||
quotedElements[i] = fmt.Sprintf("%q", in)
|
||||
}
|
||||
return strings.Join(quotedElements, ",")
|
||||
}
|
||||
|
||||
func decorateResponseWriter(responseWriter http.ResponseWriter, out io.Writer, id string) http.ResponseWriter {
|
||||
delegate := &auditResponseWriter{ResponseWriter: responseWriter, out: out, id: id}
|
||||
// check if the ResponseWriter we're wrapping is the fancy one we need
|
||||
// or if the basic is sufficient
|
||||
_, cn := responseWriter.(http.CloseNotifier)
|
||||
_, fl := responseWriter.(http.Flusher)
|
||||
_, hj := responseWriter.(http.Hijacker)
|
||||
if cn && fl && hj {
|
||||
return &fancyResponseWriterDelegator{delegate}
|
||||
}
|
||||
return delegate
|
||||
}
|
||||
152
vendor/k8s.io/apiserver/pkg/endpoints/filters/audit_test.go
generated
vendored
Normal file
152
vendor/k8s.io/apiserver/pkg/endpoints/filters/audit_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package filters
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
type simpleResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (*simpleResponseWriter) WriteHeader(code int) {}
|
||||
|
||||
type fancyResponseWriter struct {
|
||||
simpleResponseWriter
|
||||
}
|
||||
|
||||
func (*fancyResponseWriter) CloseNotify() <-chan bool { return nil }
|
||||
|
||||
func (*fancyResponseWriter) Flush() {}
|
||||
|
||||
func (*fancyResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return nil, nil, nil }
|
||||
|
||||
func TestConstructResponseWriter(t *testing.T) {
|
||||
actual := decorateResponseWriter(&simpleResponseWriter{}, ioutil.Discard, "")
|
||||
switch v := actual.(type) {
|
||||
case *auditResponseWriter:
|
||||
default:
|
||||
t.Errorf("Expected auditResponseWriter, got %v", reflect.TypeOf(v))
|
||||
}
|
||||
|
||||
actual = decorateResponseWriter(&fancyResponseWriter{}, ioutil.Discard, "")
|
||||
switch v := actual.(type) {
|
||||
case *fancyResponseWriterDelegator:
|
||||
default:
|
||||
t.Errorf("Expected fancyResponseWriterDelegator, got %v", reflect.TypeOf(v))
|
||||
}
|
||||
}
|
||||
|
||||
type fakeHTTPHandler struct{}
|
||||
|
||||
func (*fakeHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}
|
||||
|
||||
func TestAudit(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
handler := WithAudit(&fakeHTTPHandler{}, &fakeRequestContextMapper{
|
||||
user: &user.DefaultInfo{Name: "admin"},
|
||||
}, &buf)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/api/v1/namespaces/default/pods", nil)
|
||||
req.RemoteAddr = "127.0.0.1"
|
||||
handler.ServeHTTP(httptest.NewRecorder(), req)
|
||||
line := strings.Split(strings.TrimSpace(buf.String()), "\n")
|
||||
if len(line) != 2 {
|
||||
t.Fatalf("Unexpected amount of lines in audit log: %d", len(line))
|
||||
}
|
||||
match, err := regexp.MatchString(`[\d\:\-\.\+TZ]+ AUDIT: id="[\w-]+" ip="127.0.0.1" method="GET" user="admin" groups="<none>" as="<self>" asgroups="<lookup>" namespace="default" uri="/api/v1/namespaces/default/pods"`, line[0])
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error matching first line: %v", err)
|
||||
}
|
||||
if !match {
|
||||
t.Errorf("Unexpected first line of audit: %s", line[0])
|
||||
}
|
||||
match, err = regexp.MatchString(`[\d\:\-\.\+TZ]+ AUDIT: id="[\w-]+" response="200"`, line[1])
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error matching second line: %v", err)
|
||||
}
|
||||
if !match {
|
||||
t.Errorf("Unexpected second line of audit: %s", line[1])
|
||||
}
|
||||
}
|
||||
|
||||
type fakeRequestContextMapper struct {
|
||||
user *user.DefaultInfo
|
||||
}
|
||||
|
||||
func (m *fakeRequestContextMapper) Get(req *http.Request) (request.Context, bool) {
|
||||
ctx := request.NewContext()
|
||||
if m.user != nil {
|
||||
ctx = request.WithUser(ctx, m.user)
|
||||
}
|
||||
|
||||
resolver := newTestRequestInfoResolver()
|
||||
info, err := resolver.NewRequestInfo(req)
|
||||
if err == nil {
|
||||
ctx = request.WithRequestInfo(ctx, info)
|
||||
}
|
||||
|
||||
return ctx, true
|
||||
}
|
||||
|
||||
func (*fakeRequestContextMapper) Update(req *http.Request, context request.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAuditNoPanicOnNilUser(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
handler := WithAudit(&fakeHTTPHandler{}, &fakeRequestContextMapper{}, &buf)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/api/v1/namespaces/default/pods", nil)
|
||||
req.RemoteAddr = "127.0.0.1"
|
||||
handler.ServeHTTP(httptest.NewRecorder(), req)
|
||||
line := strings.Split(strings.TrimSpace(buf.String()), "\n")
|
||||
if len(line) != 2 {
|
||||
t.Fatalf("Unexpected amount of lines in audit log: %d", len(line))
|
||||
}
|
||||
match, err := regexp.MatchString(`[\d\:\-\.\+TZ]+ AUDIT: id="[\w-]+" ip="127.0.0.1" method="GET" user="<none>" groups="<none>" as="<self>" asgroups="<lookup>" namespace="default" uri="/api/v1/namespaces/default/pods"`, line[0])
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error matching first line: %v", err)
|
||||
}
|
||||
if !match {
|
||||
t.Errorf("Unexpected first line of audit: %s", line[0])
|
||||
}
|
||||
match, err = regexp.MatchString(`[\d\:\-\.\+TZ]+ AUDIT: id="[\w-]+" response="200"`, line[1])
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error matching second line: %v", err)
|
||||
}
|
||||
if !match {
|
||||
t.Errorf("Unexpected second line of audit: %s", line[1])
|
||||
}
|
||||
}
|
||||
117
vendor/k8s.io/apiserver/pkg/endpoints/filters/authentication.go
generated
vendored
Normal file
117
vendor/k8s.io/apiserver/pkg/endpoints/filters/authentication.go
generated
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package filters
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
var (
|
||||
authenticatedUserCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "authenticated_user_requests",
|
||||
Help: "Counter of authenticated requests broken out by username.",
|
||||
},
|
||||
[]string{"username"},
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(authenticatedUserCounter)
|
||||
}
|
||||
|
||||
// WithAuthentication creates an http handler that tries to authenticate the given request as a user, and then
|
||||
// stores any such user found onto the provided context for the request. If authentication fails or returns an error
|
||||
// the failed handler is used. On success, "Authorization" header is removed from the request and handler
|
||||
// is invoked to serve the request.
|
||||
func WithAuthentication(handler http.Handler, mapper genericapirequest.RequestContextMapper, auth authenticator.Request, failed http.Handler) http.Handler {
|
||||
if auth == nil {
|
||||
glog.Warningf("Authentication is disabled")
|
||||
return handler
|
||||
}
|
||||
return genericapirequest.WithRequestContext(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
user, ok, err := auth.AuthenticateRequest(req)
|
||||
if err != nil || !ok {
|
||||
if err != nil {
|
||||
glog.Errorf("Unable to authenticate the request due to an error: %v", err)
|
||||
}
|
||||
failed.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// authorization header is not required anymore in case of a successful authentication.
|
||||
req.Header.Del("Authorization")
|
||||
|
||||
if ctx, ok := mapper.Get(req); ok {
|
||||
mapper.Update(req, genericapirequest.WithUser(ctx, user))
|
||||
}
|
||||
|
||||
authenticatedUserCounter.WithLabelValues(compressUsername(user.GetName())).Inc()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
}),
|
||||
mapper,
|
||||
)
|
||||
}
|
||||
|
||||
func Unauthorized(supportsBasicAuth bool) http.HandlerFunc {
|
||||
if supportsBasicAuth {
|
||||
return unauthorizedBasicAuth
|
||||
}
|
||||
return unauthorized
|
||||
}
|
||||
|
||||
// unauthorizedBasicAuth serves an unauthorized message to clients.
|
||||
func unauthorizedBasicAuth(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="kubernetes-master"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
// unauthorized serves an unauthorized message to clients.
|
||||
func unauthorized(w http.ResponseWriter, req *http.Request) {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
// compressUsername maps all possible usernames onto a small set of categories
|
||||
// of usernames. This is done both to limit the cardinality of the
|
||||
// authorized_user_requests metric, and to avoid pushing actual usernames in the
|
||||
// metric.
|
||||
func compressUsername(username string) string {
|
||||
switch {
|
||||
// Known internal identities.
|
||||
case username == "admin" ||
|
||||
username == "client" ||
|
||||
username == "kube_proxy" ||
|
||||
username == "kubelet" ||
|
||||
username == "system:serviceaccount:kube-system:default":
|
||||
return username
|
||||
// Probably an email address.
|
||||
case strings.Contains(username, "@"):
|
||||
return "email_id"
|
||||
// Anything else (custom service accounts, custom external identities, etc.)
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
126
vendor/k8s.io/apiserver/pkg/endpoints/filters/authentication_test.go
generated
vendored
Normal file
126
vendor/k8s.io/apiserver/pkg/endpoints/filters/authentication_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package filters
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
func TestAuthenticateRequest(t *testing.T) {
|
||||
success := make(chan struct{})
|
||||
contextMapper := genericapirequest.NewRequestContextMapper()
|
||||
auth := WithAuthentication(
|
||||
http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
|
||||
ctx, ok := contextMapper.Get(req)
|
||||
if ctx == nil || !ok {
|
||||
t.Errorf("no context stored on contextMapper: %#v", contextMapper)
|
||||
}
|
||||
user, ok := genericapirequest.UserFrom(ctx)
|
||||
if user == nil || !ok {
|
||||
t.Errorf("no user stored in context: %#v", ctx)
|
||||
}
|
||||
if req.Header.Get("Authorization") != "" {
|
||||
t.Errorf("Authorization header should be removed from request on success: %#v", req)
|
||||
}
|
||||
close(success)
|
||||
}),
|
||||
contextMapper,
|
||||
authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) {
|
||||
if req.Header.Get("Authorization") == "Something" {
|
||||
return &user.DefaultInfo{Name: "user"}, true, nil
|
||||
}
|
||||
return nil, false, errors.New("Authorization header is missing.")
|
||||
}),
|
||||
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
|
||||
t.Errorf("unexpected call to failed")
|
||||
}),
|
||||
)
|
||||
|
||||
auth.ServeHTTP(httptest.NewRecorder(), &http.Request{Header: map[string][]string{"Authorization": {"Something"}}})
|
||||
|
||||
<-success
|
||||
empty, err := genericapirequest.IsEmpty(contextMapper)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !empty {
|
||||
t.Fatalf("contextMapper should have no stored requests: %v", contextMapper)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticateRequestFailed(t *testing.T) {
|
||||
failed := make(chan struct{})
|
||||
contextMapper := genericapirequest.NewRequestContextMapper()
|
||||
auth := WithAuthentication(
|
||||
http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
|
||||
t.Errorf("unexpected call to handler")
|
||||
}),
|
||||
contextMapper,
|
||||
authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) {
|
||||
return nil, false, nil
|
||||
}),
|
||||
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
|
||||
close(failed)
|
||||
}),
|
||||
)
|
||||
|
||||
auth.ServeHTTP(httptest.NewRecorder(), &http.Request{})
|
||||
|
||||
<-failed
|
||||
empty, err := genericapirequest.IsEmpty(contextMapper)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !empty {
|
||||
t.Fatalf("contextMapper should have no stored requests: %v", contextMapper)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticateRequestError(t *testing.T) {
|
||||
failed := make(chan struct{})
|
||||
contextMapper := genericapirequest.NewRequestContextMapper()
|
||||
auth := WithAuthentication(
|
||||
http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
|
||||
t.Errorf("unexpected call to handler")
|
||||
}),
|
||||
contextMapper,
|
||||
authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) {
|
||||
return nil, false, errors.New("failure")
|
||||
}),
|
||||
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
|
||||
close(failed)
|
||||
}),
|
||||
)
|
||||
|
||||
auth.ServeHTTP(httptest.NewRecorder(), &http.Request{})
|
||||
|
||||
<-failed
|
||||
empty, err := genericapirequest.IsEmpty(contextMapper)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !empty {
|
||||
t.Fatalf("contextMapper should have no stored requests: %v", contextMapper)
|
||||
}
|
||||
}
|
||||
89
vendor/k8s.io/apiserver/pkg/endpoints/filters/authorization.go
generated
vendored
Normal file
89
vendor/k8s.io/apiserver/pkg/endpoints/filters/authorization.go
generated
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package filters
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
// WithAuthorizationCheck passes all authorized requests on to handler, and returns a forbidden error otherwise.
|
||||
func WithAuthorization(handler http.Handler, requestContextMapper request.RequestContextMapper, a authorizer.Authorizer) http.Handler {
|
||||
if a == nil {
|
||||
glog.Warningf("Authorization is disabled")
|
||||
return handler
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx, ok := requestContextMapper.Get(req)
|
||||
if !ok {
|
||||
responsewriters.InternalError(w, req, errors.New("no context found for request"))
|
||||
return
|
||||
}
|
||||
|
||||
attributes, err := GetAuthorizerAttributes(ctx)
|
||||
if err != nil {
|
||||
responsewriters.InternalError(w, req, err)
|
||||
return
|
||||
}
|
||||
authorized, reason, err := a.Authorize(attributes)
|
||||
if authorized {
|
||||
handler.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
responsewriters.InternalError(w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
glog.V(4).Infof("Forbidden: %#v, Reason: %q", req.RequestURI, reason)
|
||||
responsewriters.Forbidden(attributes, w, req, reason)
|
||||
})
|
||||
}
|
||||
|
||||
func GetAuthorizerAttributes(ctx request.Context) (authorizer.Attributes, error) {
|
||||
attribs := authorizer.AttributesRecord{}
|
||||
|
||||
user, ok := request.UserFrom(ctx)
|
||||
if ok {
|
||||
attribs.User = user
|
||||
}
|
||||
|
||||
requestInfo, found := request.RequestInfoFrom(ctx)
|
||||
if !found {
|
||||
return nil, errors.New("no RequestInfo found in the context")
|
||||
}
|
||||
|
||||
// Start with common attributes that apply to resource and non-resource requests
|
||||
attribs.ResourceRequest = requestInfo.IsResourceRequest
|
||||
attribs.Path = requestInfo.Path
|
||||
attribs.Verb = requestInfo.Verb
|
||||
|
||||
attribs.APIGroup = requestInfo.APIGroup
|
||||
attribs.APIVersion = requestInfo.APIVersion
|
||||
attribs.Resource = requestInfo.Resource
|
||||
attribs.Subresource = requestInfo.Subresource
|
||||
attribs.Namespace = requestInfo.Namespace
|
||||
attribs.Name = requestInfo.Name
|
||||
|
||||
return &attribs, nil
|
||||
}
|
||||
129
vendor/k8s.io/apiserver/pkg/endpoints/filters/authorization_test.go
generated
vendored
Normal file
129
vendor/k8s.io/apiserver/pkg/endpoints/filters/authorization_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package filters
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
batch "k8s.io/client-go/pkg/apis/batch/v1"
|
||||
)
|
||||
|
||||
func TestGetAuthorizerAttributes(t *testing.T) {
|
||||
mapper := request.NewRequestContextMapper()
|
||||
|
||||
testcases := map[string]struct {
|
||||
Verb string
|
||||
Path string
|
||||
ExpectedAttributes *authorizer.AttributesRecord
|
||||
}{
|
||||
"non-resource root": {
|
||||
Verb: "POST",
|
||||
Path: "/",
|
||||
ExpectedAttributes: &authorizer.AttributesRecord{
|
||||
Verb: "post",
|
||||
Path: "/",
|
||||
},
|
||||
},
|
||||
"non-resource api prefix": {
|
||||
Verb: "GET",
|
||||
Path: "/api/",
|
||||
ExpectedAttributes: &authorizer.AttributesRecord{
|
||||
Verb: "get",
|
||||
Path: "/api/",
|
||||
},
|
||||
},
|
||||
"non-resource group api prefix": {
|
||||
Verb: "GET",
|
||||
Path: "/apis/extensions/",
|
||||
ExpectedAttributes: &authorizer.AttributesRecord{
|
||||
Verb: "get",
|
||||
Path: "/apis/extensions/",
|
||||
},
|
||||
},
|
||||
|
||||
"resource": {
|
||||
Verb: "POST",
|
||||
Path: "/api/v1/nodes/mynode",
|
||||
ExpectedAttributes: &authorizer.AttributesRecord{
|
||||
Verb: "create",
|
||||
Path: "/api/v1/nodes/mynode",
|
||||
ResourceRequest: true,
|
||||
Resource: "nodes",
|
||||
APIVersion: "v1",
|
||||
Name: "mynode",
|
||||
},
|
||||
},
|
||||
"namespaced resource": {
|
||||
Verb: "PUT",
|
||||
Path: "/api/v1/namespaces/myns/pods/mypod",
|
||||
ExpectedAttributes: &authorizer.AttributesRecord{
|
||||
Verb: "update",
|
||||
Path: "/api/v1/namespaces/myns/pods/mypod",
|
||||
ResourceRequest: true,
|
||||
Namespace: "myns",
|
||||
Resource: "pods",
|
||||
APIVersion: "v1",
|
||||
Name: "mypod",
|
||||
},
|
||||
},
|
||||
"API group resource": {
|
||||
Verb: "GET",
|
||||
Path: "/apis/batch/v1/namespaces/myns/jobs",
|
||||
ExpectedAttributes: &authorizer.AttributesRecord{
|
||||
Verb: "list",
|
||||
Path: "/apis/batch/v1/namespaces/myns/jobs",
|
||||
ResourceRequest: true,
|
||||
APIGroup: batch.GroupName,
|
||||
APIVersion: "v1",
|
||||
Namespace: "myns",
|
||||
Resource: "jobs",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for k, tc := range testcases {
|
||||
req, _ := http.NewRequest(tc.Verb, tc.Path, nil)
|
||||
req.RemoteAddr = "127.0.0.1"
|
||||
|
||||
var attribs authorizer.Attributes
|
||||
var err error
|
||||
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx, ok := mapper.Get(req)
|
||||
if !ok {
|
||||
responsewriters.InternalError(w, req, errors.New("no context found for request"))
|
||||
return
|
||||
}
|
||||
attribs, err = GetAuthorizerAttributes(ctx)
|
||||
})
|
||||
handler = WithRequestInfo(handler, newTestRequestInfoResolver(), mapper)
|
||||
handler = request.WithRequestContext(handler, mapper)
|
||||
handler.ServeHTTP(httptest.NewRecorder(), req)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", k, err)
|
||||
} else if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) {
|
||||
t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs)
|
||||
}
|
||||
}
|
||||
}
|
||||
21
vendor/k8s.io/apiserver/pkg/endpoints/filters/doc.go
generated
vendored
Normal file
21
vendor/k8s.io/apiserver/pkg/endpoints/filters/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package filters contains all the http handler chain filters which
|
||||
// _are_ api related, i.e. which are prerequisite for the API services
|
||||
// to work (in contrast to the filters in the server package which are
|
||||
// not part of the API contract).
|
||||
package filters // import "k8s.io/apiserver/pkg/endpoints/filters"
|
||||
210
vendor/k8s.io/apiserver/pkg/endpoints/filters/impersonation.go
generated
vendored
Normal file
210
vendor/k8s.io/apiserver/pkg/endpoints/filters/impersonation.go
generated
vendored
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package filters
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/server/httplog"
|
||||
"k8s.io/client-go/pkg/api/v1"
|
||||
authenticationv1 "k8s.io/client-go/pkg/apis/authentication/v1"
|
||||
)
|
||||
|
||||
// WithImpersonation is a filter that will inspect and check requests that attempt to change the user.Info for their requests
|
||||
func WithImpersonation(handler http.Handler, requestContextMapper request.RequestContextMapper, a authorizer.Authorizer) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
impersonationRequests, err := buildImpersonationRequests(req.Header)
|
||||
if err != nil {
|
||||
glog.V(4).Infof("%v", err)
|
||||
responsewriters.InternalError(w, req, err)
|
||||
return
|
||||
}
|
||||
if len(impersonationRequests) == 0 {
|
||||
handler.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, exists := requestContextMapper.Get(req)
|
||||
if !exists {
|
||||
responsewriters.InternalError(w, req, errors.New("no context found for request"))
|
||||
return
|
||||
}
|
||||
requestor, exists := request.UserFrom(ctx)
|
||||
if !exists {
|
||||
responsewriters.InternalError(w, req, errors.New("no user found for request"))
|
||||
return
|
||||
}
|
||||
|
||||
// if groups are not specified, then we need to look them up differently depending on the type of user
|
||||
// if they are specified, then they are the authority (including the inclusion of system:authenticated/system:unauthenticated groups)
|
||||
groupsSpecified := len(req.Header[authenticationv1.ImpersonateGroupHeader]) > 0
|
||||
|
||||
// make sure we're allowed to impersonate each thing we're requesting. While we're iterating through, start building username
|
||||
// and group information
|
||||
username := ""
|
||||
groups := []string{}
|
||||
userExtra := map[string][]string{}
|
||||
for _, impersonationRequest := range impersonationRequests {
|
||||
actingAsAttributes := &authorizer.AttributesRecord{
|
||||
User: requestor,
|
||||
Verb: "impersonate",
|
||||
APIGroup: impersonationRequest.GetObjectKind().GroupVersionKind().Group,
|
||||
Namespace: impersonationRequest.Namespace,
|
||||
Name: impersonationRequest.Name,
|
||||
ResourceRequest: true,
|
||||
}
|
||||
|
||||
switch impersonationRequest.GetObjectKind().GroupVersionKind().GroupKind() {
|
||||
case v1.SchemeGroupVersion.WithKind("ServiceAccount").GroupKind():
|
||||
actingAsAttributes.Resource = "serviceaccounts"
|
||||
username = serviceaccount.MakeUsername(impersonationRequest.Namespace, impersonationRequest.Name)
|
||||
if !groupsSpecified {
|
||||
// if groups aren't specified for a service account, we know the groups because its a fixed mapping. Add them
|
||||
groups = serviceaccount.MakeGroupNames(impersonationRequest.Namespace, impersonationRequest.Name)
|
||||
}
|
||||
|
||||
case v1.SchemeGroupVersion.WithKind("User").GroupKind():
|
||||
actingAsAttributes.Resource = "users"
|
||||
username = impersonationRequest.Name
|
||||
|
||||
case v1.SchemeGroupVersion.WithKind("Group").GroupKind():
|
||||
actingAsAttributes.Resource = "groups"
|
||||
groups = append(groups, impersonationRequest.Name)
|
||||
|
||||
case authenticationv1.SchemeGroupVersion.WithKind("UserExtra").GroupKind():
|
||||
extraKey := impersonationRequest.FieldPath
|
||||
extraValue := impersonationRequest.Name
|
||||
actingAsAttributes.Resource = "userextras"
|
||||
actingAsAttributes.Subresource = extraKey
|
||||
userExtra[extraKey] = append(userExtra[extraKey], extraValue)
|
||||
|
||||
default:
|
||||
glog.V(4).Infof("unknown impersonation request type: %v", impersonationRequest)
|
||||
responsewriters.Forbidden(actingAsAttributes, w, req, fmt.Sprintf("unknown impersonation request type: %v", impersonationRequest))
|
||||
return
|
||||
}
|
||||
|
||||
allowed, reason, err := a.Authorize(actingAsAttributes)
|
||||
if err != nil || !allowed {
|
||||
glog.V(4).Infof("Forbidden: %#v, Reason: %s, Error: %v", req.RequestURI, reason, err)
|
||||
responsewriters.Forbidden(actingAsAttributes, w, req, reason)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !groupsSpecified && username != user.Anonymous {
|
||||
// When impersonating a non-anonymous user, if no groups were specified
|
||||
// if neither the system:authenticated nor system:unauthenticated groups are explicitly included,
|
||||
// include the system:authenticated group in the impersonated user info
|
||||
found := false
|
||||
for _, group := range groups {
|
||||
if group == user.AllAuthenticated || group == user.AllUnauthenticated {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
groups = append(groups, user.AllAuthenticated)
|
||||
}
|
||||
}
|
||||
|
||||
newUser := &user.DefaultInfo{
|
||||
Name: username,
|
||||
Groups: groups,
|
||||
Extra: userExtra,
|
||||
}
|
||||
requestContextMapper.Update(req, request.WithUser(ctx, newUser))
|
||||
|
||||
oldUser, _ := request.UserFrom(ctx)
|
||||
httplog.LogOf(req, w).Addf("%v is acting as %v", oldUser, newUser)
|
||||
|
||||
// clear all the impersonation headers from the request
|
||||
req.Header.Del(authenticationv1.ImpersonateUserHeader)
|
||||
req.Header.Del(authenticationv1.ImpersonateGroupHeader)
|
||||
for headerName := range req.Header {
|
||||
if strings.HasPrefix(headerName, authenticationv1.ImpersonateUserExtraHeaderPrefix) {
|
||||
req.Header.Del(headerName)
|
||||
}
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
// buildImpersonationRequests returns a list of objectreferences that represent the different things we're requesting to impersonate.
|
||||
// Also includes a map[string][]string representing user.Info.Extra
|
||||
// Each request must be authorized against the current user before switching contexts.
|
||||
func buildImpersonationRequests(headers http.Header) ([]v1.ObjectReference, error) {
|
||||
impersonationRequests := []v1.ObjectReference{}
|
||||
|
||||
requestedUser := headers.Get(authenticationv1.ImpersonateUserHeader)
|
||||
hasUser := len(requestedUser) > 0
|
||||
if hasUser {
|
||||
if namespace, name, err := serviceaccount.SplitUsername(requestedUser); err == nil {
|
||||
impersonationRequests = append(impersonationRequests, v1.ObjectReference{Kind: "ServiceAccount", Namespace: namespace, Name: name})
|
||||
} else {
|
||||
impersonationRequests = append(impersonationRequests, v1.ObjectReference{Kind: "User", Name: requestedUser})
|
||||
}
|
||||
}
|
||||
|
||||
hasGroups := false
|
||||
for _, group := range headers[authenticationv1.ImpersonateGroupHeader] {
|
||||
hasGroups = true
|
||||
impersonationRequests = append(impersonationRequests, v1.ObjectReference{Kind: "Group", Name: group})
|
||||
}
|
||||
|
||||
hasUserExtra := false
|
||||
for headerName, values := range headers {
|
||||
if !strings.HasPrefix(headerName, authenticationv1.ImpersonateUserExtraHeaderPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
hasUserExtra = true
|
||||
extraKey := strings.ToLower(headerName[len(authenticationv1.ImpersonateUserExtraHeaderPrefix):])
|
||||
|
||||
// make a separate request for each extra value they're trying to set
|
||||
for _, value := range values {
|
||||
impersonationRequests = append(impersonationRequests,
|
||||
v1.ObjectReference{
|
||||
Kind: "UserExtra",
|
||||
// we only parse out a group above, but the parsing will fail if there isn't SOME version
|
||||
// using the internal version will help us fail if anyone starts using it
|
||||
APIVersion: authenticationv1.SchemeGroupVersion.String(),
|
||||
Name: value,
|
||||
// ObjectReference doesn't have a subresource field. FieldPath is close and available, so we'll use that
|
||||
// TODO fight the good fight for ObjectReference to refer to resources and subresources
|
||||
FieldPath: extraKey,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (hasGroups || hasUserExtra) && !hasUser {
|
||||
return nil, fmt.Errorf("requested %v without impersonating a user", impersonationRequests)
|
||||
}
|
||||
|
||||
return impersonationRequests, nil
|
||||
}
|
||||
388
vendor/k8s.io/apiserver/pkg/endpoints/filters/impersonation_test.go
generated
vendored
Normal file
388
vendor/k8s.io/apiserver/pkg/endpoints/filters/impersonation_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package filters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
authenticationapi "k8s.io/client-go/pkg/apis/authentication/v1"
|
||||
)
|
||||
|
||||
type impersonateAuthorizer struct{}
|
||||
|
||||
func (impersonateAuthorizer) Authorize(a authorizer.Attributes) (authorized bool, reason string, err error) {
|
||||
user := a.GetUser()
|
||||
|
||||
switch {
|
||||
case user.GetName() == "system:admin":
|
||||
return true, "", nil
|
||||
|
||||
case user.GetName() == "tester":
|
||||
return false, "", fmt.Errorf("works on my machine")
|
||||
|
||||
case user.GetName() == "deny-me":
|
||||
return false, "denied", nil
|
||||
}
|
||||
|
||||
if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "wheel" && a.GetVerb() == "impersonate" && a.GetResource() == "users" {
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "sa-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "serviceaccounts" {
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "regular-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "users" {
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "group-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "groups" {
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-scopes" && a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "scopes" {
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-particular-scopes" &&
|
||||
a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "scopes" && a.GetName() == "scope-a" {
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-project" && a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "project" {
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
return false, "deny by default", nil
|
||||
}
|
||||
|
||||
func TestImpersonationFilter(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
user user.Info
|
||||
impersonationUser string
|
||||
impersonationGroups []string
|
||||
impersonationUserExtras map[string][]string
|
||||
expectedUser user.Info
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
name: "not-impersonating",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "tester",
|
||||
},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "tester",
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "impersonating-error",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "tester",
|
||||
},
|
||||
impersonationUser: "anyone",
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "tester",
|
||||
},
|
||||
expectedCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "impersonating-group-without-user",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "tester",
|
||||
},
|
||||
impersonationGroups: []string{"some-group"},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "tester",
|
||||
},
|
||||
expectedCode: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "impersonating-extra-without-user",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "tester",
|
||||
},
|
||||
impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "tester",
|
||||
},
|
||||
expectedCode: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "disallowed-group",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"wheel"},
|
||||
},
|
||||
impersonationUser: "system:admin",
|
||||
impersonationGroups: []string{"some-group"},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"wheel"},
|
||||
},
|
||||
expectedCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "allowed-group",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"wheel", "group-impersonater"},
|
||||
},
|
||||
impersonationUser: "system:admin",
|
||||
impersonationGroups: []string{"some-group"},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "system:admin",
|
||||
Groups: []string{"some-group"},
|
||||
Extra: map[string][]string{},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "disallowed-userextra-1",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"wheel"},
|
||||
},
|
||||
impersonationUser: "system:admin",
|
||||
impersonationGroups: []string{"some-group"},
|
||||
impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"wheel"},
|
||||
},
|
||||
expectedCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "disallowed-userextra-2",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"wheel", "extra-setter-project"},
|
||||
},
|
||||
impersonationUser: "system:admin",
|
||||
impersonationGroups: []string{"some-group"},
|
||||
impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"wheel", "extra-setter-project"},
|
||||
},
|
||||
expectedCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "disallowed-userextra-3",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"wheel", "extra-setter-particular-scopes"},
|
||||
},
|
||||
impersonationUser: "system:admin",
|
||||
impersonationGroups: []string{"some-group"},
|
||||
impersonationUserExtras: map[string][]string{"scopes": {"scope-a", "scope-b"}},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"wheel", "extra-setter-particular-scopes"},
|
||||
},
|
||||
expectedCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "allowed-userextras",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"wheel", "extra-setter-scopes"},
|
||||
},
|
||||
impersonationUser: "system:admin",
|
||||
impersonationUserExtras: map[string][]string{"scopes": {"scope-a", "scope-b"}},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "system:admin",
|
||||
Groups: []string{"system:authenticated"},
|
||||
Extra: map[string][]string{"scopes": {"scope-a", "scope-b"}},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "allowed-users-impersonation",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"regular-impersonater"},
|
||||
},
|
||||
impersonationUser: "tester",
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "tester",
|
||||
Groups: []string{"system:authenticated"},
|
||||
Extra: map[string][]string{},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "disallowed-impersonating",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"sa-impersonater"},
|
||||
},
|
||||
impersonationUser: "tester",
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"sa-impersonater"},
|
||||
},
|
||||
expectedCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "allowed-sa-impersonating",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "dev",
|
||||
Groups: []string{"sa-impersonater"},
|
||||
Extra: map[string][]string{},
|
||||
},
|
||||
impersonationUser: "system:serviceaccount:foo:default",
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "system:serviceaccount:foo:default",
|
||||
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:foo", "system:authenticated"},
|
||||
Extra: map[string][]string{},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "anonymous-username-prevents-adding-authenticated-group",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "system:admin",
|
||||
},
|
||||
impersonationUser: "system:anonymous",
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "system:anonymous",
|
||||
Groups: []string{},
|
||||
Extra: map[string][]string{},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "unauthenticated-group-prevents-adding-authenticated-group",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "system:admin",
|
||||
},
|
||||
impersonationUser: "unknown",
|
||||
impersonationGroups: []string{"system:unauthenticated"},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "unknown",
|
||||
Groups: []string{"system:unauthenticated"},
|
||||
Extra: map[string][]string{},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "unauthenticated-group-prevents-double-adding-authenticated-group",
|
||||
user: &user.DefaultInfo{
|
||||
Name: "system:admin",
|
||||
},
|
||||
impersonationUser: "unknown",
|
||||
impersonationGroups: []string{"system:authenticated"},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "unknown",
|
||||
Groups: []string{"system:authenticated"},
|
||||
Extra: map[string][]string{},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
requestContextMapper := request.NewRequestContextMapper()
|
||||
var ctx request.Context
|
||||
var actualUser user.Info
|
||||
var lock sync.Mutex
|
||||
|
||||
doNothingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
currentCtx, _ := requestContextMapper.Get(req)
|
||||
user, exists := request.UserFrom(currentCtx)
|
||||
if !exists {
|
||||
actualUser = nil
|
||||
return
|
||||
}
|
||||
|
||||
actualUser = user
|
||||
})
|
||||
handler := func(delegate http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Recovered %v", r)
|
||||
}
|
||||
}()
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
requestContextMapper.Update(req, ctx)
|
||||
currentCtx, _ := requestContextMapper.Get(req)
|
||||
|
||||
user, exists := request.UserFrom(currentCtx)
|
||||
if !exists {
|
||||
actualUser = nil
|
||||
return
|
||||
} else {
|
||||
actualUser = user
|
||||
}
|
||||
|
||||
delegate.ServeHTTP(w, req)
|
||||
})
|
||||
}(WithImpersonation(doNothingHandler, requestContextMapper, impersonateAuthorizer{}))
|
||||
handler = request.WithRequestContext(handler, requestContextMapper)
|
||||
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
|
||||
for _, tc := range testCases {
|
||||
func() {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
ctx = request.WithUser(request.NewContext(), tc.user)
|
||||
}()
|
||||
|
||||
req, err := http.NewRequest("GET", server.URL, nil)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
req.Header.Add(authenticationapi.ImpersonateUserHeader, tc.impersonationUser)
|
||||
for _, group := range tc.impersonationGroups {
|
||||
req.Header.Add(authenticationapi.ImpersonateGroupHeader, group)
|
||||
}
|
||||
for extraKey, values := range tc.impersonationUserExtras {
|
||||
for _, value := range values {
|
||||
req.Header.Add(authenticationapi.ImpersonateUserExtraHeaderPrefix+extraKey, value)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode != tc.expectedCode {
|
||||
t.Errorf("%s: expected %v, actual %v", tc.name, tc.expectedCode, resp.StatusCode)
|
||||
continue
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actualUser, tc.expectedUser) {
|
||||
t.Errorf("%s: expected %#v, actual %#v", tc.name, tc.expectedUser, actualUser)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
47
vendor/k8s.io/apiserver/pkg/endpoints/filters/requestinfo.go
generated
vendored
Normal file
47
vendor/k8s.io/apiserver/pkg/endpoints/filters/requestinfo.go
generated
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package filters
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
// WithRequestInfo attaches a RequestInfo to the context.
|
||||
func WithRequestInfo(handler http.Handler, resolver *request.RequestInfoFactory, requestContextMapper request.RequestContextMapper) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx, ok := requestContextMapper.Get(req)
|
||||
if !ok {
|
||||
responsewriters.InternalError(w, req, errors.New("no context found for request"))
|
||||
return
|
||||
}
|
||||
|
||||
info, err := resolver.NewRequestInfo(req)
|
||||
if err != nil {
|
||||
responsewriters.InternalError(w, req, fmt.Errorf("failed to create RequestInfo: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
requestContextMapper.Update(req, request.WithRequestInfo(ctx, info))
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
29
vendor/k8s.io/apiserver/pkg/endpoints/filters/requestinfo_test.go
generated
vendored
Normal file
29
vendor/k8s.io/apiserver/pkg/endpoints/filters/requestinfo_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package filters
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
func newTestRequestInfoResolver() *request.RequestInfoFactory {
|
||||
return &request.RequestInfoFactory{
|
||||
APIPrefixes: sets.NewString("api", "apis"),
|
||||
GrouplessAPIPrefixes: sets.NewString("api"),
|
||||
}
|
||||
}
|
||||
157
vendor/k8s.io/apiserver/pkg/endpoints/groupversion.go
generated
vendored
Normal file
157
vendor/k8s.io/apiserver/pkg/endpoints/groupversion.go
generated
vendored
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
)
|
||||
|
||||
// APIGroupVersion is a helper for exposing rest.Storage objects as http.Handlers via go-restful
|
||||
// It handles URLs of the form:
|
||||
// /${storage_key}[/${object_name}]
|
||||
// Where 'storage_key' points to a rest.Storage object stored in storage.
|
||||
// This object should contain all parameterization necessary for running a particular API version
|
||||
type APIGroupVersion struct {
|
||||
Storage map[string]rest.Storage
|
||||
|
||||
Root string
|
||||
|
||||
// GroupVersion is the external group version
|
||||
GroupVersion schema.GroupVersion
|
||||
|
||||
// OptionsExternalVersion controls the Kubernetes APIVersion used for common objects in the apiserver
|
||||
// schema like api.Status, api.DeleteOptions, and metav1.ListOptions. Other implementors may
|
||||
// define a version "v1beta1" but want to use the Kubernetes "v1" internal objects. If
|
||||
// empty, defaults to GroupVersion.
|
||||
OptionsExternalVersion *schema.GroupVersion
|
||||
// MetaGroupVersion defaults to "meta.k8s.io/v1" and is the scheme group version used to decode
|
||||
// common API implementations like ListOptions. Future changes will allow this to vary by group
|
||||
// version (for when the inevitable meta/v2 group emerges).
|
||||
MetaGroupVersion *schema.GroupVersion
|
||||
|
||||
Mapper meta.RESTMapper
|
||||
|
||||
// Serializer is used to determine how to convert responses from API methods into bytes to send over
|
||||
// the wire.
|
||||
Serializer runtime.NegotiatedSerializer
|
||||
ParameterCodec runtime.ParameterCodec
|
||||
|
||||
Typer runtime.ObjectTyper
|
||||
Creater runtime.ObjectCreater
|
||||
Convertor runtime.ObjectConvertor
|
||||
Copier runtime.ObjectCopier
|
||||
Defaulter runtime.ObjectDefaulter
|
||||
Linker runtime.SelfLinker
|
||||
UnsafeConvertor runtime.ObjectConvertor
|
||||
|
||||
Admit admission.Interface
|
||||
Context request.RequestContextMapper
|
||||
|
||||
MinRequestTimeout time.Duration
|
||||
|
||||
// SubresourceGroupVersionKind contains the GroupVersionKind overrides for each subresource that is
|
||||
// accessible from this API group version. The GroupVersionKind is that of the external version of
|
||||
// the subresource. The key of this map should be the path of the subresource. The keys here should
|
||||
// match the keys in the Storage map above for subresources.
|
||||
SubresourceGroupVersionKind map[string]schema.GroupVersionKind
|
||||
|
||||
// ResourceLister is an interface that knows how to list resources
|
||||
// for this API Group.
|
||||
ResourceLister discovery.APIResourceLister
|
||||
}
|
||||
|
||||
// InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container.
|
||||
// It is expected that the provided path root prefix will serve all operations. Root MUST NOT end
|
||||
// in a slash.
|
||||
func (g *APIGroupVersion) InstallREST(container *restful.Container) error {
|
||||
installer := g.newInstaller()
|
||||
ws := installer.NewWebService()
|
||||
apiResources, registrationErrors := installer.Install(ws)
|
||||
lister := g.ResourceLister
|
||||
if lister == nil {
|
||||
lister = staticLister{apiResources}
|
||||
}
|
||||
versionDiscoveryHandler := discovery.NewAPIVersionHandler(g.Serializer, g.GroupVersion, lister)
|
||||
versionDiscoveryHandler.AddToWebService(ws)
|
||||
container.Add(ws)
|
||||
return utilerrors.NewAggregate(registrationErrors)
|
||||
}
|
||||
|
||||
// UpdateREST registers the REST handlers for this APIGroupVersion to an existing web service
|
||||
// in the restful Container. It will use the prefix (root/version) to find the existing
|
||||
// web service. If a web service does not exist within the container to support the prefix
|
||||
// this method will return an error.
|
||||
func (g *APIGroupVersion) UpdateREST(container *restful.Container) error {
|
||||
installer := g.newInstaller()
|
||||
var ws *restful.WebService = nil
|
||||
|
||||
for i, s := range container.RegisteredWebServices() {
|
||||
if s.RootPath() == installer.prefix {
|
||||
ws = container.RegisteredWebServices()[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ws == nil {
|
||||
return apierrors.NewInternalError(fmt.Errorf("unable to find an existing webservice for prefix %s", installer.prefix))
|
||||
}
|
||||
apiResources, registrationErrors := installer.Install(ws)
|
||||
lister := g.ResourceLister
|
||||
if lister == nil {
|
||||
lister = staticLister{apiResources}
|
||||
}
|
||||
versionDiscoveryHandler := discovery.NewAPIVersionHandler(g.Serializer, g.GroupVersion, lister)
|
||||
versionDiscoveryHandler.AddToWebService(ws)
|
||||
return utilerrors.NewAggregate(registrationErrors)
|
||||
}
|
||||
|
||||
// newInstaller is a helper to create the installer. Used by InstallREST and UpdateREST.
|
||||
func (g *APIGroupVersion) newInstaller() *APIInstaller {
|
||||
prefix := path.Join(g.Root, g.GroupVersion.Group, g.GroupVersion.Version)
|
||||
installer := &APIInstaller{
|
||||
group: g,
|
||||
prefix: prefix,
|
||||
minRequestTimeout: g.MinRequestTimeout,
|
||||
}
|
||||
return installer
|
||||
}
|
||||
|
||||
// staticLister implements the APIResourceLister interface
|
||||
type staticLister struct {
|
||||
list []metav1.APIResource
|
||||
}
|
||||
|
||||
func (s staticLister) ListAPIResources() []metav1.APIResource {
|
||||
return s.list
|
||||
}
|
||||
|
||||
var _ discovery.APIResourceLister = &staticLister{}
|
||||
77
vendor/k8s.io/apiserver/pkg/endpoints/handlers/BUILD
generated
vendored
Normal file
77
vendor/k8s.io/apiserver/pkg/endpoints/handlers/BUILD
generated
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["rest_test.go"],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/evanphx/json-patch:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/apis/example:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/apis/example/v1:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"namer.go",
|
||||
"patch.go",
|
||||
"proxy.go",
|
||||
"rest.go",
|
||||
"watch.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/evanphx/json-patch:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/golang.org/x/net/websocket:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/conversion/unstructured:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer/streaming:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/httpstream:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/mergepatch:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/metrics:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server/httplog:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/proxy:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/trace:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/wsstream:go_default_library",
|
||||
],
|
||||
)
|
||||
18
vendor/k8s.io/apiserver/pkg/endpoints/handlers/doc.go
generated
vendored
Normal file
18
vendor/k8s.io/apiserver/pkg/endpoints/handlers/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package handlers contains HTTP handlers to implement the apiserver APIs.
|
||||
package handlers // import "k8s.io/apiserver/pkg/endpoints/handlers"
|
||||
144
vendor/k8s.io/apiserver/pkg/endpoints/handlers/namer.go
generated
vendored
Normal file
144
vendor/k8s.io/apiserver/pkg/endpoints/handlers/namer.go
generated
vendored
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
// ContextFunc returns a Context given a request - a context must be returned
|
||||
type ContextFunc func(req *http.Request) request.Context
|
||||
|
||||
// ScopeNamer handles accessing names from requests and objects
|
||||
type ScopeNamer interface {
|
||||
// Namespace returns the appropriate namespace value from the request (may be empty) or an
|
||||
// error.
|
||||
Namespace(req *http.Request) (namespace string, err error)
|
||||
// Name returns the name from the request, and an optional namespace value if this is a namespace
|
||||
// scoped call. An error is returned if the name is not available.
|
||||
Name(req *http.Request) (namespace, name string, err error)
|
||||
// ObjectName returns the namespace and name from an object if they exist, or an error if the object
|
||||
// does not support names.
|
||||
ObjectName(obj runtime.Object) (namespace, name string, err error)
|
||||
// SetSelfLink sets the provided URL onto the object. The method should return nil if the object
|
||||
// does not support selfLinks.
|
||||
SetSelfLink(obj runtime.Object, url string) error
|
||||
// GenerateLink creates an encoded URI for a given runtime object that represents the canonical path
|
||||
// and query.
|
||||
GenerateLink(req *http.Request, obj runtime.Object) (uri string, err error)
|
||||
// GenerateListLink creates an encoded URI for a list that represents the canonical path and query.
|
||||
GenerateListLink(req *http.Request) (uri string, err error)
|
||||
}
|
||||
|
||||
type ContextBasedNaming struct {
|
||||
GetContext ContextFunc
|
||||
SelfLinker runtime.SelfLinker
|
||||
ClusterScoped bool
|
||||
|
||||
SelfLinkPathPrefix string
|
||||
SelfLinkPathSuffix string
|
||||
}
|
||||
|
||||
// ContextBasedNaming implements ScopeNamer
|
||||
var _ ScopeNamer = ContextBasedNaming{}
|
||||
|
||||
func (n ContextBasedNaming) SetSelfLink(obj runtime.Object, url string) error {
|
||||
return n.SelfLinker.SetSelfLink(obj, url)
|
||||
}
|
||||
|
||||
func (n ContextBasedNaming) Namespace(req *http.Request) (namespace string, err error) {
|
||||
requestInfo, ok := request.RequestInfoFrom(n.GetContext(req))
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing requestInfo")
|
||||
}
|
||||
return requestInfo.Namespace, nil
|
||||
}
|
||||
|
||||
func (n ContextBasedNaming) Name(req *http.Request) (namespace, name string, err error) {
|
||||
requestInfo, ok := request.RequestInfoFrom(n.GetContext(req))
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("missing requestInfo")
|
||||
}
|
||||
ns, err := n.Namespace(req)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if len(requestInfo.Name) == 0 {
|
||||
return "", "", errEmptyName
|
||||
}
|
||||
return ns, requestInfo.Name, nil
|
||||
}
|
||||
|
||||
func (n ContextBasedNaming) GenerateLink(req *http.Request, obj runtime.Object) (uri string, err error) {
|
||||
requestInfo, ok := request.RequestInfoFrom(n.GetContext(req))
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing requestInfo")
|
||||
}
|
||||
|
||||
namespace, name, err := n.ObjectName(obj)
|
||||
if err == errEmptyName && len(requestInfo.Name) > 0 {
|
||||
name = requestInfo.Name
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(namespace) == 0 && len(requestInfo.Namespace) > 0 {
|
||||
namespace = requestInfo.Namespace
|
||||
}
|
||||
|
||||
if n.ClusterScoped {
|
||||
return n.SelfLinkPathPrefix + url.QueryEscape(name) + n.SelfLinkPathSuffix, nil
|
||||
}
|
||||
|
||||
return n.SelfLinkPathPrefix +
|
||||
url.QueryEscape(namespace) +
|
||||
"/" + url.QueryEscape(requestInfo.Resource) + "/" +
|
||||
url.QueryEscape(name) +
|
||||
n.SelfLinkPathSuffix,
|
||||
nil
|
||||
}
|
||||
|
||||
func (n ContextBasedNaming) GenerateListLink(req *http.Request) (uri string, err error) {
|
||||
if len(req.URL.RawPath) > 0 {
|
||||
return req.URL.RawPath, nil
|
||||
}
|
||||
return req.URL.EscapedPath(), nil
|
||||
}
|
||||
|
||||
func (n ContextBasedNaming) ObjectName(obj runtime.Object) (namespace, name string, err error) {
|
||||
name, err = n.SelfLinker.Name(obj)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if len(name) == 0 {
|
||||
return "", "", errEmptyName
|
||||
}
|
||||
namespace, err = n.SelfLinker.Namespace(obj)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return namespace, name, err
|
||||
}
|
||||
|
||||
// errEmptyName is returned when API requests do not fill the name section of the path.
|
||||
var errEmptyName = errors.NewBadRequest("name must be provided")
|
||||
36
vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/BUILD
generated
vendored
Normal file
36
vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/BUILD
generated
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["negotiate_test.go"],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"errors.go",
|
||||
"negotiate.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/bitbucket.org/ww/goautoneg:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
],
|
||||
)
|
||||
18
vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/doc.go
generated
vendored
Normal file
18
vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package negotation contains media type negotiation logic.
|
||||
package negotiation // import "k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||
61
vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go
generated
vendored
Normal file
61
vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go
generated
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package negotiation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// errNotAcceptable indicates Accept negotiation has failed
|
||||
type errNotAcceptable struct {
|
||||
accepted []string
|
||||
}
|
||||
|
||||
func (e errNotAcceptable) Error() string {
|
||||
return fmt.Sprintf("only the following media types are accepted: %v", strings.Join(e.accepted, ", "))
|
||||
}
|
||||
|
||||
func (e errNotAcceptable) Status() metav1.Status {
|
||||
return metav1.Status{
|
||||
Status: metav1.StatusFailure,
|
||||
Code: http.StatusNotAcceptable,
|
||||
Reason: metav1.StatusReason("NotAcceptable"),
|
||||
Message: e.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// errUnsupportedMediaType indicates Content-Type is not recognized
|
||||
type errUnsupportedMediaType struct {
|
||||
accepted []string
|
||||
}
|
||||
|
||||
func (e errUnsupportedMediaType) Error() string {
|
||||
return fmt.Sprintf("the body of the request was in an unknown format - accepted media types include: %v", strings.Join(e.accepted, ", "))
|
||||
}
|
||||
|
||||
func (e errUnsupportedMediaType) Status() metav1.Status {
|
||||
return metav1.Status{
|
||||
Status: metav1.StatusFailure,
|
||||
Code: http.StatusUnsupportedMediaType,
|
||||
Reason: metav1.StatusReason("UnsupportedMediaType"),
|
||||
Message: e.Error(),
|
||||
}
|
||||
}
|
||||
305
vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go
generated
vendored
Normal file
305
vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go
generated
vendored
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package negotiation
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"bitbucket.org/ww/goautoneg"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// MediaTypesForSerializer returns a list of media and stream media types for the server.
|
||||
func MediaTypesForSerializer(ns runtime.NegotiatedSerializer) (mediaTypes, streamMediaTypes []string) {
|
||||
for _, info := range ns.SupportedMediaTypes() {
|
||||
mediaTypes = append(mediaTypes, info.MediaType)
|
||||
if info.StreamSerializer != nil {
|
||||
// stream=watch is the existing mime-type parameter for watch
|
||||
streamMediaTypes = append(streamMediaTypes, info.MediaType+";stream=watch")
|
||||
}
|
||||
}
|
||||
return mediaTypes, streamMediaTypes
|
||||
}
|
||||
|
||||
func NegotiateOutputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) {
|
||||
mediaType, ok := negotiateMediaTypeOptions(req.Header.Get("Accept"), acceptedMediaTypesForEndpoint(ns), defaultEndpointRestrictions)
|
||||
if !ok {
|
||||
supported, _ := MediaTypesForSerializer(ns)
|
||||
return runtime.SerializerInfo{}, errNotAcceptable{supported}
|
||||
}
|
||||
// TODO: move into resthandler
|
||||
info := mediaType.accepted.Serializer
|
||||
if (mediaType.pretty || isPrettyPrint(req)) && info.PrettySerializer != nil {
|
||||
info.Serializer = info.PrettySerializer
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func NegotiateOutputStreamSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) {
|
||||
mediaType, ok := negotiateMediaTypeOptions(req.Header.Get("Accept"), acceptedMediaTypesForEndpoint(ns), defaultEndpointRestrictions)
|
||||
if !ok || mediaType.accepted.Serializer.StreamSerializer == nil {
|
||||
_, supported := MediaTypesForSerializer(ns)
|
||||
return runtime.SerializerInfo{}, errNotAcceptable{supported}
|
||||
}
|
||||
return mediaType.accepted.Serializer, nil
|
||||
}
|
||||
|
||||
func NegotiateInputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) {
|
||||
mediaTypes := ns.SupportedMediaTypes()
|
||||
mediaType := req.Header.Get("Content-Type")
|
||||
if len(mediaType) == 0 {
|
||||
mediaType = mediaTypes[0].MediaType
|
||||
}
|
||||
mediaType, _, err := mime.ParseMediaType(mediaType)
|
||||
if err != nil {
|
||||
_, supported := MediaTypesForSerializer(ns)
|
||||
return runtime.SerializerInfo{}, errUnsupportedMediaType{supported}
|
||||
}
|
||||
|
||||
for _, info := range mediaTypes {
|
||||
if info.MediaType != mediaType {
|
||||
continue
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
_, supported := MediaTypesForSerializer(ns)
|
||||
return runtime.SerializerInfo{}, errUnsupportedMediaType{supported}
|
||||
}
|
||||
|
||||
// isPrettyPrint returns true if the "pretty" query parameter is true or if the User-Agent
|
||||
// matches known "human" clients.
|
||||
func isPrettyPrint(req *http.Request) bool {
|
||||
// DEPRECATED: should be part of the content type
|
||||
if req.URL != nil {
|
||||
pp := req.URL.Query().Get("pretty")
|
||||
if len(pp) > 0 {
|
||||
pretty, _ := strconv.ParseBool(pp)
|
||||
return pretty
|
||||
}
|
||||
}
|
||||
userAgent := req.UserAgent()
|
||||
// This covers basic all browers and cli http tools
|
||||
if strings.HasPrefix(userAgent, "curl") || strings.HasPrefix(userAgent, "Wget") || strings.HasPrefix(userAgent, "Mozilla/5.0") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// negotiate the most appropriate content type given the accept header and a list of
|
||||
// alternatives.
|
||||
func negotiate(header string, alternatives []string) (goautoneg.Accept, bool) {
|
||||
alternates := make([][]string, 0, len(alternatives))
|
||||
for _, alternate := range alternatives {
|
||||
alternates = append(alternates, strings.SplitN(alternate, "/", 2))
|
||||
}
|
||||
for _, clause := range goautoneg.ParseAccept(header) {
|
||||
for _, alternate := range alternates {
|
||||
if clause.Type == alternate[0] && clause.SubType == alternate[1] {
|
||||
return clause, true
|
||||
}
|
||||
if clause.Type == alternate[0] && clause.SubType == "*" {
|
||||
clause.SubType = alternate[1]
|
||||
return clause, true
|
||||
}
|
||||
if clause.Type == "*" && clause.SubType == "*" {
|
||||
clause.Type = alternate[0]
|
||||
clause.SubType = alternate[1]
|
||||
return clause, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return goautoneg.Accept{}, false
|
||||
}
|
||||
|
||||
// endpointRestrictions is an interface that allows content-type negotiation
|
||||
// to verify server support for specific options
|
||||
type endpointRestrictions interface {
|
||||
// AllowsConversion should return true if the specified group version kind
|
||||
// is an allowed target object.
|
||||
AllowsConversion(schema.GroupVersionKind) bool
|
||||
// AllowsServerVersion should return true if the specified version is valid
|
||||
// for the server group.
|
||||
AllowsServerVersion(version string) bool
|
||||
// AllowsStreamSchema should return true if the specified stream schema is
|
||||
// valid for the server group.
|
||||
AllowsStreamSchema(schema string) bool
|
||||
}
|
||||
|
||||
var defaultEndpointRestrictions = emptyEndpointRestrictions{}
|
||||
|
||||
type emptyEndpointRestrictions struct{}
|
||||
|
||||
func (emptyEndpointRestrictions) AllowsConversion(schema.GroupVersionKind) bool { return false }
|
||||
func (emptyEndpointRestrictions) AllowsServerVersion(string) bool { return false }
|
||||
func (emptyEndpointRestrictions) AllowsStreamSchema(s string) bool { return s == "watch" }
|
||||
|
||||
// acceptedMediaType contains information about a valid media type that the
|
||||
// server can serialize.
|
||||
type acceptedMediaType struct {
|
||||
// Type is the first part of the media type ("application")
|
||||
Type string
|
||||
// SubType is the second part of the media type ("json")
|
||||
SubType string
|
||||
// Serializer is the serialization info this object accepts
|
||||
Serializer runtime.SerializerInfo
|
||||
}
|
||||
|
||||
// mediaTypeOptions describes information for a given media type that may alter
|
||||
// the server response
|
||||
type mediaTypeOptions struct {
|
||||
// pretty is true if the requested representation should be formatted for human
|
||||
// viewing
|
||||
pretty bool
|
||||
|
||||
// stream, if set, indicates that a streaming protocol variant of this encoding
|
||||
// is desired. The only currently supported value is watch which returns versioned
|
||||
// events. In the future, this may refer to other stream protocols.
|
||||
stream string
|
||||
|
||||
// convert is a request to alter the type of object returned by the server from the
|
||||
// normal response
|
||||
convert *schema.GroupVersionKind
|
||||
// useServerVersion is an optional version for the server group
|
||||
useServerVersion string
|
||||
|
||||
// export is true if the representation requested should exclude fields the server
|
||||
// has set
|
||||
export bool
|
||||
|
||||
// unrecognized is a list of all unrecognized keys
|
||||
unrecognized []string
|
||||
|
||||
// the accepted media type from the client
|
||||
accepted *acceptedMediaType
|
||||
}
|
||||
|
||||
// acceptMediaTypeOptions returns an options object that matches the provided media type params. If
|
||||
// it returns false, the provided options are not allowed and the media type must be skipped. These
|
||||
// parameters are unversioned and may not be changed.
|
||||
func acceptMediaTypeOptions(params map[string]string, accepts *acceptedMediaType, endpoint endpointRestrictions) (mediaTypeOptions, bool) {
|
||||
var options mediaTypeOptions
|
||||
|
||||
// extract all known parameters
|
||||
for k, v := range params {
|
||||
switch k {
|
||||
|
||||
// controls transformation of the object when returned
|
||||
case "as":
|
||||
if options.convert == nil {
|
||||
options.convert = &schema.GroupVersionKind{}
|
||||
}
|
||||
options.convert.Kind = v
|
||||
case "g":
|
||||
if options.convert == nil {
|
||||
options.convert = &schema.GroupVersionKind{}
|
||||
}
|
||||
options.convert.Group = v
|
||||
case "v":
|
||||
if options.convert == nil {
|
||||
options.convert = &schema.GroupVersionKind{}
|
||||
}
|
||||
options.convert.Version = v
|
||||
|
||||
// controls the streaming schema
|
||||
case "stream":
|
||||
if len(v) > 0 && (accepts.Serializer.StreamSerializer == nil || !endpoint.AllowsStreamSchema(v)) {
|
||||
return mediaTypeOptions{}, false
|
||||
}
|
||||
options.stream = v
|
||||
|
||||
// controls the version of the server API group used
|
||||
// for generic output
|
||||
case "sv":
|
||||
if len(v) > 0 && !endpoint.AllowsServerVersion(v) {
|
||||
return mediaTypeOptions{}, false
|
||||
}
|
||||
options.useServerVersion = v
|
||||
|
||||
// if specified, the server should transform the returned
|
||||
// output and remove fields that are always server specified,
|
||||
// or which fit the default behavior.
|
||||
case "export":
|
||||
options.export = v == "1"
|
||||
|
||||
// if specified, the pretty serializer will be used
|
||||
case "pretty":
|
||||
options.pretty = v == "1"
|
||||
|
||||
default:
|
||||
options.unrecognized = append(options.unrecognized, k)
|
||||
}
|
||||
}
|
||||
|
||||
if options.convert != nil && !endpoint.AllowsConversion(*options.convert) {
|
||||
return mediaTypeOptions{}, false
|
||||
}
|
||||
|
||||
options.accepted = accepts
|
||||
|
||||
return options, true
|
||||
}
|
||||
|
||||
// negotiateMediaTypeOptions returns the most appropriate content type given the accept header and
|
||||
// a list of alternatives along with the accepted media type parameters.
|
||||
func negotiateMediaTypeOptions(header string, accepted []acceptedMediaType, endpoint endpointRestrictions) (mediaTypeOptions, bool) {
|
||||
if len(header) == 0 && len(accepted) > 0 {
|
||||
return mediaTypeOptions{
|
||||
accepted: &accepted[0],
|
||||
}, true
|
||||
}
|
||||
|
||||
clauses := goautoneg.ParseAccept(header)
|
||||
for _, clause := range clauses {
|
||||
for i := range accepted {
|
||||
accepts := &accepted[i]
|
||||
switch {
|
||||
case clause.Type == accepts.Type && clause.SubType == accepts.SubType,
|
||||
clause.Type == accepts.Type && clause.SubType == "*",
|
||||
clause.Type == "*" && clause.SubType == "*":
|
||||
// TODO: should we prefer the first type with no unrecognized options? Do we need to ignore unrecognized
|
||||
// parameters.
|
||||
return acceptMediaTypeOptions(clause.Params, accepts, endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
return mediaTypeOptions{}, false
|
||||
}
|
||||
|
||||
// acceptedMediaTypesForEndpoint returns an array of structs that are used to efficiently check which
|
||||
// allowed media types the server exposes.
|
||||
func acceptedMediaTypesForEndpoint(ns runtime.NegotiatedSerializer) []acceptedMediaType {
|
||||
var acceptedMediaTypes []acceptedMediaType
|
||||
for _, info := range ns.SupportedMediaTypes() {
|
||||
segments := strings.SplitN(info.MediaType, "/", 2)
|
||||
if len(segments) == 1 {
|
||||
segments = append(segments, "*")
|
||||
}
|
||||
t := acceptedMediaType{
|
||||
Type: segments[0],
|
||||
SubType: segments[1],
|
||||
Serializer: info,
|
||||
}
|
||||
acceptedMediaTypes = append(acceptedMediaTypes, t)
|
||||
}
|
||||
return acceptedMediaTypes
|
||||
}
|
||||
245
vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate_test.go
generated
vendored
Normal file
245
vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package negotiation
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// statusError is an object that can be converted into an metav1.Status
|
||||
type statusError interface {
|
||||
Status() metav1.Status
|
||||
}
|
||||
|
||||
type fakeNegotiater struct {
|
||||
serializer, streamSerializer runtime.Serializer
|
||||
framer runtime.Framer
|
||||
types, streamTypes []string
|
||||
}
|
||||
|
||||
func (n *fakeNegotiater) SupportedMediaTypes() []runtime.SerializerInfo {
|
||||
var out []runtime.SerializerInfo
|
||||
for _, s := range n.types {
|
||||
info := runtime.SerializerInfo{Serializer: n.serializer, MediaType: s, EncodesAsText: true}
|
||||
for _, t := range n.streamTypes {
|
||||
if t == s {
|
||||
info.StreamSerializer = &runtime.StreamSerializerInfo{
|
||||
EncodesAsText: true,
|
||||
Framer: n.framer,
|
||||
Serializer: n.streamSerializer,
|
||||
}
|
||||
}
|
||||
}
|
||||
out = append(out, info)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (n *fakeNegotiater) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
|
||||
return n.serializer
|
||||
}
|
||||
|
||||
func (n *fakeNegotiater) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
|
||||
return n.serializer
|
||||
}
|
||||
|
||||
var fakeCodec = runtime.NewCodec(runtime.NoopEncoder{}, runtime.NoopDecoder{})
|
||||
|
||||
func TestNegotiate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
accept string
|
||||
req *http.Request
|
||||
ns *fakeNegotiater
|
||||
serializer runtime.Serializer
|
||||
contentType string
|
||||
params map[string]string
|
||||
errFn func(error) bool
|
||||
}{
|
||||
// pick a default
|
||||
{
|
||||
req: &http.Request{},
|
||||
contentType: "application/json",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
|
||||
serializer: fakeCodec,
|
||||
},
|
||||
{
|
||||
accept: "",
|
||||
contentType: "application/json",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
|
||||
serializer: fakeCodec,
|
||||
},
|
||||
{
|
||||
accept: "*/*",
|
||||
contentType: "application/json",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
|
||||
serializer: fakeCodec,
|
||||
},
|
||||
{
|
||||
accept: "application/*",
|
||||
contentType: "application/json",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
|
||||
serializer: fakeCodec,
|
||||
},
|
||||
{
|
||||
accept: "application/json",
|
||||
contentType: "application/json",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
|
||||
serializer: fakeCodec,
|
||||
},
|
||||
{
|
||||
accept: "application/json",
|
||||
contentType: "application/json",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json", "application/protobuf"}},
|
||||
serializer: fakeCodec,
|
||||
},
|
||||
{
|
||||
accept: "application/protobuf",
|
||||
contentType: "application/protobuf",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json", "application/protobuf"}},
|
||||
serializer: fakeCodec,
|
||||
},
|
||||
{
|
||||
accept: "application/json; pretty=1",
|
||||
contentType: "application/json",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
|
||||
serializer: fakeCodec,
|
||||
params: map[string]string{"pretty": "1"},
|
||||
},
|
||||
{
|
||||
accept: "unrecognized/stuff,application/json; pretty=1",
|
||||
contentType: "application/json",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
|
||||
serializer: fakeCodec,
|
||||
params: map[string]string{"pretty": "1"},
|
||||
},
|
||||
|
||||
// query param triggers pretty
|
||||
{
|
||||
req: &http.Request{
|
||||
Header: http.Header{"Accept": []string{"application/json"}},
|
||||
URL: &url.URL{RawQuery: "pretty=1"},
|
||||
},
|
||||
contentType: "application/json",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
|
||||
serializer: fakeCodec,
|
||||
params: map[string]string{"pretty": "1"},
|
||||
},
|
||||
|
||||
// certain user agents trigger pretty
|
||||
{
|
||||
req: &http.Request{
|
||||
Header: http.Header{
|
||||
"Accept": []string{"application/json"},
|
||||
"User-Agent": []string{"curl"},
|
||||
},
|
||||
},
|
||||
contentType: "application/json",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
|
||||
serializer: fakeCodec,
|
||||
params: map[string]string{"pretty": "1"},
|
||||
},
|
||||
{
|
||||
req: &http.Request{
|
||||
Header: http.Header{
|
||||
"Accept": []string{"application/json"},
|
||||
"User-Agent": []string{"Wget"},
|
||||
},
|
||||
},
|
||||
contentType: "application/json",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
|
||||
serializer: fakeCodec,
|
||||
params: map[string]string{"pretty": "1"},
|
||||
},
|
||||
{
|
||||
req: &http.Request{
|
||||
Header: http.Header{
|
||||
"Accept": []string{"application/json"},
|
||||
"User-Agent": []string{"Mozilla/5.0"},
|
||||
},
|
||||
},
|
||||
contentType: "application/json",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
|
||||
serializer: fakeCodec,
|
||||
params: map[string]string{"pretty": "1"},
|
||||
},
|
||||
|
||||
// "application" is not a valid media type, so the server will reject the response during
|
||||
// negotiation (the server, in error, has specified an invalid media type)
|
||||
{
|
||||
accept: "application",
|
||||
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application"}},
|
||||
errFn: func(err error) bool {
|
||||
return err.Error() == "only the following media types are accepted: application"
|
||||
},
|
||||
},
|
||||
{
|
||||
ns: &fakeNegotiater{},
|
||||
errFn: func(err error) bool {
|
||||
return err.Error() == "only the following media types are accepted: "
|
||||
},
|
||||
},
|
||||
{
|
||||
accept: "*/*",
|
||||
ns: &fakeNegotiater{},
|
||||
errFn: func(err error) bool {
|
||||
return err.Error() == "only the following media types are accepted: "
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
req := test.req
|
||||
if req == nil {
|
||||
req = &http.Request{Header: http.Header{}}
|
||||
req.Header.Set("Accept", test.accept)
|
||||
}
|
||||
s, err := NegotiateOutputSerializer(req, test.ns)
|
||||
switch {
|
||||
case err == nil && test.errFn != nil:
|
||||
t.Errorf("%d: failed: expected error", i)
|
||||
continue
|
||||
case err != nil && test.errFn == nil:
|
||||
t.Errorf("%d: failed: %v", i, err)
|
||||
continue
|
||||
case err != nil:
|
||||
if !test.errFn(err) {
|
||||
t.Errorf("%d: failed: %v", i, err)
|
||||
}
|
||||
status, ok := err.(statusError)
|
||||
if !ok {
|
||||
t.Errorf("%d: failed, error should be statusError: %v", i, err)
|
||||
continue
|
||||
}
|
||||
if status.Status().Status != metav1.StatusFailure || status.Status().Code != http.StatusNotAcceptable {
|
||||
t.Errorf("%d: failed: %v", i, err)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
if test.contentType != s.MediaType {
|
||||
t.Errorf("%d: unexpected %s %s", i, test.contentType, s.MediaType)
|
||||
}
|
||||
if s.Serializer != test.serializer {
|
||||
t.Errorf("%d: unexpected %s %s", i, test.serializer, s.Serializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
130
vendor/k8s.io/apiserver/pkg/endpoints/handlers/patch.go
generated
vendored
Normal file
130
vendor/k8s.io/apiserver/pkg/endpoints/handlers/patch.go
generated
vendored
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/conversion/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
|
||||
"github.com/evanphx/json-patch"
|
||||
)
|
||||
|
||||
// patchObjectJSON patches the <originalObject> with <patchJS> and stores
|
||||
// the result in <objToUpdate>.
|
||||
// Currently it also returns the original and patched objects serialized to
|
||||
// JSONs (this may not be needed once we can apply patches at the
|
||||
// map[string]interface{} level).
|
||||
func patchObjectJSON(
|
||||
patchType types.PatchType,
|
||||
codec runtime.Codec,
|
||||
originalObject runtime.Object,
|
||||
patchJS []byte,
|
||||
objToUpdate runtime.Object,
|
||||
versionedObj runtime.Object,
|
||||
) (originalObjJS []byte, patchedObjJS []byte, retErr error) {
|
||||
js, err := runtime.Encode(codec, originalObject)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
originalObjJS = js
|
||||
|
||||
switch patchType {
|
||||
case types.JSONPatchType:
|
||||
patchObj, err := jsonpatch.DecodePatch(patchJS)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if patchedObjJS, err = patchObj.Apply(originalObjJS); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
case types.MergePatchType:
|
||||
if patchedObjJS, err = jsonpatch.MergePatch(originalObjJS, patchJS); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
case types.StrategicMergePatchType:
|
||||
if patchedObjJS, err = strategicpatch.StrategicMergePatch(originalObjJS, patchJS, versionedObj); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
default:
|
||||
// only here as a safety net - go-restful filters content-type
|
||||
return nil, nil, fmt.Errorf("unknown Content-Type header for patch: %v", patchType)
|
||||
}
|
||||
if err := runtime.DecodeInto(codec, patchedObjJS, objToUpdate); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// strategicPatchObject applies a strategic merge patch of <patchJS> to
|
||||
// <originalObject> and stores the result in <objToUpdate>.
|
||||
// It additionally returns the map[string]interface{} representation of the
|
||||
// <originalObject> and <patchJS>.
|
||||
// NOTE: Both <originalObject> and <objToUpdate> are supposed to be versioned.
|
||||
func strategicPatchObject(
|
||||
codec runtime.Codec,
|
||||
defaulter runtime.ObjectDefaulter,
|
||||
originalObject runtime.Object,
|
||||
patchJS []byte,
|
||||
objToUpdate runtime.Object,
|
||||
versionedObj runtime.Object,
|
||||
) error {
|
||||
originalObjMap := make(map[string]interface{})
|
||||
if err := unstructured.DefaultConverter.ToUnstructured(originalObject, &originalObjMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patchMap := make(map[string]interface{})
|
||||
if err := json.Unmarshal(patchJS, &patchMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := applyPatchToObject(codec, defaulter, originalObjMap, patchMap, objToUpdate, versionedObj); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyPatchToObject applies a strategic merge patch of <patchMap> to
|
||||
// <originalMap> and stores the result in <objToUpdate>.
|
||||
// NOTE: <objToUpdate> must be a versioned object.
|
||||
func applyPatchToObject(
|
||||
codec runtime.Codec,
|
||||
defaulter runtime.ObjectDefaulter,
|
||||
originalMap map[string]interface{},
|
||||
patchMap map[string]interface{},
|
||||
objToUpdate runtime.Object,
|
||||
versionedObj runtime.Object,
|
||||
) error {
|
||||
patchedObjMap, err := strategicpatch.StrategicMergeMapPatch(originalMap, patchMap, versionedObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Rather than serialize the patched map to JSON, then decode it to an object, we go directly from a map to an object
|
||||
if err := unstructured.DefaultConverter.FromUnstructured(patchedObjMap, objToUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
// Decoding from JSON to a versioned object would apply defaults, so we do the same here
|
||||
defaulter.Default(objToUpdate)
|
||||
|
||||
return nil
|
||||
}
|
||||
282
vendor/k8s.io/apiserver/pkg/endpoints/handlers/proxy.go
generated
vendored
Normal file
282
vendor/k8s.io/apiserver/pkg/endpoints/handlers/proxy.go
generated
vendored
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||
"k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/apiserver/pkg/endpoints/metrics"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/apiserver/pkg/server/httplog"
|
||||
proxyutil "k8s.io/apiserver/pkg/util/proxy"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// ProxyHandler provides a http.Handler which will proxy traffic to locations
|
||||
// specified by items implementing Redirector.
|
||||
type ProxyHandler struct {
|
||||
Prefix string
|
||||
Storage map[string]rest.Storage
|
||||
Serializer runtime.NegotiatedSerializer
|
||||
Mapper request.RequestContextMapper
|
||||
}
|
||||
|
||||
func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
proxyHandlerTraceID := rand.Int63()
|
||||
|
||||
var verb string
|
||||
var apiResource string
|
||||
var httpCode int
|
||||
reqStart := time.Now()
|
||||
defer func() {
|
||||
metrics.Monitor(&verb, &apiResource,
|
||||
net.GetHTTPClient(req),
|
||||
w.Header().Get("Content-Type"),
|
||||
httpCode, reqStart)
|
||||
}()
|
||||
|
||||
ctx, ok := r.Mapper.Get(req)
|
||||
if !ok {
|
||||
responsewriters.InternalError(w, req, errors.New("Error getting request context"))
|
||||
httpCode = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
|
||||
requestInfo, ok := request.RequestInfoFrom(ctx)
|
||||
if !ok {
|
||||
responsewriters.InternalError(w, req, errors.New("Error getting RequestInfo from context"))
|
||||
httpCode = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
if !requestInfo.IsResourceRequest {
|
||||
responsewriters.NotFound(w, req)
|
||||
httpCode = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
verb = requestInfo.Verb
|
||||
namespace, resource, parts := requestInfo.Namespace, requestInfo.Resource, requestInfo.Parts
|
||||
|
||||
ctx = request.WithNamespace(ctx, namespace)
|
||||
if len(parts) < 2 {
|
||||
responsewriters.NotFound(w, req)
|
||||
httpCode = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
id := parts[1]
|
||||
remainder := ""
|
||||
if len(parts) > 2 {
|
||||
proxyParts := parts[2:]
|
||||
remainder = strings.Join(proxyParts, "/")
|
||||
if strings.HasSuffix(req.URL.Path, "/") {
|
||||
// The original path had a trailing slash, which has been stripped
|
||||
// by KindAndNamespace(). We should add it back because some
|
||||
// servers (like etcd) require it.
|
||||
remainder = remainder + "/"
|
||||
}
|
||||
}
|
||||
storage, ok := r.Storage[resource]
|
||||
if !ok {
|
||||
httplog.LogOf(req, w).Addf("'%v' has no storage object", resource)
|
||||
responsewriters.NotFound(w, req)
|
||||
httpCode = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
apiResource = resource
|
||||
|
||||
gv := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
|
||||
|
||||
redirector, ok := storage.(rest.Redirector)
|
||||
if !ok {
|
||||
httplog.LogOf(req, w).Addf("'%v' is not a redirector", resource)
|
||||
httpCode = responsewriters.ErrorNegotiated(apierrors.NewMethodNotSupported(schema.GroupResource{Resource: resource}, "proxy"), r.Serializer, gv, w, req)
|
||||
return
|
||||
}
|
||||
|
||||
location, roundTripper, err := redirector.ResourceLocation(ctx, id)
|
||||
if err != nil {
|
||||
httplog.LogOf(req, w).Addf("Error getting ResourceLocation: %v", err)
|
||||
httpCode = responsewriters.ErrorNegotiated(err, r.Serializer, gv, w, req)
|
||||
return
|
||||
}
|
||||
if location == nil {
|
||||
httplog.LogOf(req, w).Addf("ResourceLocation for %v returned nil", id)
|
||||
responsewriters.NotFound(w, req)
|
||||
httpCode = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
|
||||
if roundTripper != nil {
|
||||
glog.V(5).Infof("[%x: %v] using transport %T...", proxyHandlerTraceID, req.URL, roundTripper)
|
||||
}
|
||||
|
||||
// Default to http
|
||||
if location.Scheme == "" {
|
||||
location.Scheme = "http"
|
||||
}
|
||||
// Add the subpath
|
||||
if len(remainder) > 0 {
|
||||
location.Path = singleJoiningSlash(location.Path, remainder)
|
||||
}
|
||||
// Start with anything returned from the storage, and add the original request's parameters
|
||||
values := location.Query()
|
||||
for k, vs := range req.URL.Query() {
|
||||
for _, v := range vs {
|
||||
values.Add(k, v)
|
||||
}
|
||||
}
|
||||
location.RawQuery = values.Encode()
|
||||
|
||||
newReq, err := http.NewRequest(req.Method, location.String(), req.Body)
|
||||
if err != nil {
|
||||
httpCode = responsewriters.ErrorNegotiated(err, r.Serializer, gv, w, req)
|
||||
return
|
||||
}
|
||||
httpCode = http.StatusOK
|
||||
newReq.Header = req.Header
|
||||
newReq.ContentLength = req.ContentLength
|
||||
// Copy the TransferEncoding is for future-proofing. Currently Go only supports "chunked" and
|
||||
// it can determine the TransferEncoding based on ContentLength and the Body.
|
||||
newReq.TransferEncoding = req.TransferEncoding
|
||||
|
||||
// TODO convert this entire proxy to an UpgradeAwareProxy similar to
|
||||
// https://github.com/openshift/origin/blob/master/pkg/util/httpproxy/upgradeawareproxy.go.
|
||||
// That proxy needs to be modified to support multiple backends, not just 1.
|
||||
if r.tryUpgrade(w, req, newReq, location, roundTripper, gv) {
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect requests of the form "/{resource}/{name}" to "/{resource}/{name}/"
|
||||
// This is essentially a hack for http://issue.k8s.io/4958.
|
||||
// Note: Keep this code after tryUpgrade to not break that flow.
|
||||
if len(parts) == 2 && !strings.HasSuffix(req.URL.Path, "/") {
|
||||
var queryPart string
|
||||
if len(req.URL.RawQuery) > 0 {
|
||||
queryPart = "?" + req.URL.RawQuery
|
||||
}
|
||||
w.Header().Set("Location", req.URL.Path+"/"+queryPart)
|
||||
w.WriteHeader(http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
glog.V(4).Infof("[%x] Beginning proxy %s...", proxyHandlerTraceID, req.URL)
|
||||
defer func() {
|
||||
glog.V(4).Infof("[%x] Proxy %v finished %v.", proxyHandlerTraceID, req.URL, time.Now().Sub(start))
|
||||
}()
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: location.Scheme, Host: location.Host})
|
||||
alreadyRewriting := false
|
||||
if roundTripper != nil {
|
||||
_, alreadyRewriting = roundTripper.(*proxyutil.Transport)
|
||||
glog.V(5).Infof("[%x] Not making a rewriting transport for proxy %s...", proxyHandlerTraceID, req.URL)
|
||||
}
|
||||
if !alreadyRewriting {
|
||||
glog.V(5).Infof("[%x] making a transport for proxy %s...", proxyHandlerTraceID, req.URL)
|
||||
prepend := path.Join(r.Prefix, resource, id)
|
||||
if len(namespace) > 0 {
|
||||
prepend = path.Join(r.Prefix, "namespaces", namespace, resource, id)
|
||||
}
|
||||
pTransport := &proxyutil.Transport{
|
||||
Scheme: req.URL.Scheme,
|
||||
Host: req.URL.Host,
|
||||
PathPrepend: prepend,
|
||||
RoundTripper: roundTripper,
|
||||
}
|
||||
roundTripper = pTransport
|
||||
}
|
||||
proxy.Transport = roundTripper
|
||||
proxy.FlushInterval = 200 * time.Millisecond
|
||||
proxy.ServeHTTP(w, newReq)
|
||||
}
|
||||
|
||||
// tryUpgrade returns true if the request was handled.
|
||||
func (r *ProxyHandler) tryUpgrade(w http.ResponseWriter, req, newReq *http.Request, location *url.URL, transport http.RoundTripper, gv schema.GroupVersion) bool {
|
||||
if !httpstream.IsUpgradeRequest(req) {
|
||||
return false
|
||||
}
|
||||
backendConn, err := proxyutil.DialURL(location, transport)
|
||||
if err != nil {
|
||||
responsewriters.ErrorNegotiated(err, r.Serializer, gv, w, req)
|
||||
return true
|
||||
}
|
||||
defer backendConn.Close()
|
||||
|
||||
// TODO should we use _ (a bufio.ReadWriter) instead of requestHijackedConn
|
||||
// when copying between the client and the backend? Docker doesn't when they
|
||||
// hijack, just for reference...
|
||||
requestHijackedConn, _, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
responsewriters.ErrorNegotiated(err, r.Serializer, gv, w, req)
|
||||
return true
|
||||
}
|
||||
defer requestHijackedConn.Close()
|
||||
|
||||
if err = newReq.Write(backendConn); err != nil {
|
||||
responsewriters.ErrorNegotiated(err, r.Serializer, gv, w, req)
|
||||
return true
|
||||
}
|
||||
|
||||
done := make(chan struct{}, 2)
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(backendConn, requestHijackedConn)
|
||||
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
glog.Errorf("Error proxying data from client to backend: %v", err)
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(requestHijackedConn, backendConn)
|
||||
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
glog.Errorf("Error proxying data from backend to client: %v", err)
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
<-done
|
||||
return true
|
||||
}
|
||||
|
||||
// borrowed from net/http/httputil/reverseproxy.go
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
case !aslash && !bslash:
|
||||
return a + "/" + b
|
||||
}
|
||||
return a + b
|
||||
}
|
||||
49
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/BUILD
generated
vendored
Normal file
49
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/BUILD
generated
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"errors_test.go",
|
||||
"status_test.go",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"errors.go",
|
||||
"status.go",
|
||||
"writers.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/storage:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/flushwriter:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/wsstream:go_default_library",
|
||||
],
|
||||
)
|
||||
18
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/doc.go
generated
vendored
Normal file
18
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package responsewriters containers helpers to write responses in HTTP handlers.
|
||||
package responsewriters // import "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
91
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/errors.go
generated
vendored
Normal file
91
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/errors.go
generated
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package responsewriters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
// Avoid emitting errors that look like valid HTML. Quotes are okay.
|
||||
var sanitizer = strings.NewReplacer(`&`, "&", `<`, "<", `>`, ">")
|
||||
|
||||
// BadGatewayError renders a simple bad gateway error.
|
||||
func BadGatewayError(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
fmt.Fprintf(w, "Bad Gateway: %q", sanitizer.Replace(req.RequestURI))
|
||||
}
|
||||
|
||||
// Forbidden renders a simple forbidden error
|
||||
func Forbidden(attributes authorizer.Attributes, w http.ResponseWriter, req *http.Request, reason string) {
|
||||
msg := sanitizer.Replace(forbiddenMessage(attributes))
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
|
||||
if len(reason) == 0 {
|
||||
fmt.Fprintf(w, "%s", msg)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s: %q", msg, reason)
|
||||
}
|
||||
}
|
||||
|
||||
func forbiddenMessage(attributes authorizer.Attributes) string {
|
||||
username := ""
|
||||
if user := attributes.GetUser(); user != nil {
|
||||
username = user.GetName()
|
||||
}
|
||||
|
||||
if !attributes.IsResourceRequest() {
|
||||
return fmt.Sprintf("User %q cannot %s path %q.", username, attributes.GetVerb(), attributes.GetPath())
|
||||
}
|
||||
|
||||
resource := attributes.GetResource()
|
||||
if group := attributes.GetAPIGroup(); len(group) > 0 {
|
||||
resource = resource + "." + group
|
||||
}
|
||||
if subresource := attributes.GetSubresource(); len(subresource) > 0 {
|
||||
resource = resource + "/" + subresource
|
||||
}
|
||||
|
||||
if ns := attributes.GetNamespace(); len(ns) > 0 {
|
||||
return fmt.Sprintf("User %q cannot %s %s in the namespace %q.", username, attributes.GetVerb(), resource, ns)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("User %q cannot %s %s at the cluster scope.", username, attributes.GetVerb(), resource)
|
||||
}
|
||||
|
||||
// InternalError renders a simple internal error
|
||||
func InternalError(w http.ResponseWriter, req *http.Request, err error) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Internal Server Error: %q: %v", sanitizer.Replace(req.RequestURI), err)
|
||||
runtime.HandleError(err)
|
||||
}
|
||||
|
||||
// NotFound renders a simple not found error.
|
||||
func NotFound(w http.ResponseWriter, req *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "Not Found: %q", sanitizer.Replace(req.RequestURI))
|
||||
}
|
||||
84
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/errors_test.go
generated
vendored
Normal file
84
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/errors_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package responsewriters
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
internalError := errors.New("ARGH")
|
||||
fns := map[string]func(http.ResponseWriter, *http.Request){
|
||||
"BadGatewayError": BadGatewayError,
|
||||
"NotFound": NotFound,
|
||||
"InternalError": func(w http.ResponseWriter, req *http.Request) {
|
||||
InternalError(w, req, internalError)
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
fn string
|
||||
uri string
|
||||
expected string
|
||||
}{
|
||||
{"BadGatewayError", "/get", `Bad Gateway: "/get"`},
|
||||
{"BadGatewayError", "/<script>", `Bad Gateway: "/<script>"`},
|
||||
{"NotFound", "/get", `Not Found: "/get"`},
|
||||
{"NotFound", "/<script&>", `Not Found: "/<script&>"`},
|
||||
{"InternalError", "/get", `Internal Server Error: "/get": ARGH`},
|
||||
{"InternalError", "/<script>", `Internal Server Error: "/<script>": ARGH`},
|
||||
}
|
||||
for _, test := range cases {
|
||||
observer := httptest.NewRecorder()
|
||||
fns[test.fn](observer, &http.Request{RequestURI: test.uri})
|
||||
result := string(observer.Body.Bytes())
|
||||
if result != test.expected {
|
||||
t.Errorf("%s(..., %q) != %q, got %q", test.fn, test.uri, test.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestForbidden(t *testing.T) {
|
||||
u := &user.DefaultInfo{Name: "NAME"}
|
||||
cases := []struct {
|
||||
expected string
|
||||
attributes authorizer.Attributes
|
||||
reason string
|
||||
}{
|
||||
{`User "NAME" cannot GET path "/whatever".`,
|
||||
authorizer.AttributesRecord{User: u, Verb: "GET", Path: "/whatever"}, ""},
|
||||
{`User "NAME" cannot GET path "/<script>".`,
|
||||
authorizer.AttributesRecord{User: u, Verb: "GET", Path: "/<script>"}, ""},
|
||||
{`User "NAME" cannot GET pod at the cluster scope.`,
|
||||
authorizer.AttributesRecord{User: u, Verb: "GET", Resource: "pod", ResourceRequest: true}, ""},
|
||||
{`User "NAME" cannot GET pod.v2/quota in the namespace "test".`,
|
||||
authorizer.AttributesRecord{User: u, Verb: "GET", Namespace: "test", APIGroup: "v2", Resource: "pod", Subresource: "quota", ResourceRequest: true}, ""},
|
||||
}
|
||||
for _, test := range cases {
|
||||
observer := httptest.NewRecorder()
|
||||
Forbidden(test.attributes, observer, &http.Request{}, test.reason)
|
||||
result := string(observer.Body.Bytes())
|
||||
if result != test.expected {
|
||||
t.Errorf("Forbidden(%#v...) != %#v, got %#v", test.attributes, test.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
70
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status.go
generated
vendored
Normal file
70
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status.go
generated
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package responsewriters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/storage"
|
||||
)
|
||||
|
||||
// statusError is an object that can be converted into an metav1.Status
|
||||
type statusError interface {
|
||||
Status() metav1.Status
|
||||
}
|
||||
|
||||
// apiStatus converts an error to an metav1.Status object.
|
||||
func apiStatus(err error) *metav1.Status {
|
||||
switch t := err.(type) {
|
||||
case statusError:
|
||||
status := t.Status()
|
||||
if len(status.Status) == 0 {
|
||||
status.Status = metav1.StatusFailure
|
||||
}
|
||||
if status.Code == 0 {
|
||||
switch status.Status {
|
||||
case metav1.StatusSuccess:
|
||||
status.Code = http.StatusOK
|
||||
case metav1.StatusFailure:
|
||||
status.Code = http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
//TODO: check for invalid responses
|
||||
return &status
|
||||
default:
|
||||
status := http.StatusInternalServerError
|
||||
switch {
|
||||
//TODO: replace me with NewConflictErr
|
||||
case storage.IsConflict(err):
|
||||
status = http.StatusConflict
|
||||
}
|
||||
// Log errors that were not converted to an error status
|
||||
// by REST storage - these typically indicate programmer
|
||||
// error by not using pkg/api/errors, or unexpected failure
|
||||
// cases.
|
||||
runtime.HandleError(fmt.Errorf("apiserver received an error that is not an metav1.Status: %v", err))
|
||||
return &metav1.Status{
|
||||
Status: metav1.StatusFailure,
|
||||
Code: int32(status),
|
||||
Reason: metav1.StatusReasonUnknown,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
72
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status_test.go
generated
vendored
Normal file
72
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package responsewriters
|
||||
|
||||
import (
|
||||
stderrs "errors"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func TestAPIStatus(t *testing.T) {
|
||||
cases := map[error]metav1.Status{
|
||||
errors.NewNotFound(schema.GroupResource{Group: "legacy.kubernetes.io", Resource: "foos"}, "bar"): {
|
||||
Status: metav1.StatusFailure,
|
||||
Code: http.StatusNotFound,
|
||||
Reason: metav1.StatusReasonNotFound,
|
||||
Message: "foos.legacy.kubernetes.io \"bar\" not found",
|
||||
Details: &metav1.StatusDetails{
|
||||
Group: "legacy.kubernetes.io",
|
||||
Kind: "foos",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
errors.NewAlreadyExists(schema.GroupResource{Resource: "foos"}, "bar"): {
|
||||
Status: metav1.StatusFailure,
|
||||
Code: http.StatusConflict,
|
||||
Reason: "AlreadyExists",
|
||||
Message: "foos \"bar\" already exists",
|
||||
Details: &metav1.StatusDetails{
|
||||
Group: "",
|
||||
Kind: "foos",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
errors.NewConflict(schema.GroupResource{Resource: "foos"}, "bar", stderrs.New("failure")): {
|
||||
Status: metav1.StatusFailure,
|
||||
Code: http.StatusConflict,
|
||||
Reason: "Conflict",
|
||||
Message: "Operation cannot be fulfilled on foos \"bar\": failure",
|
||||
Details: &metav1.StatusDetails{
|
||||
Group: "",
|
||||
Kind: "foos",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
}
|
||||
for k, v := range cases {
|
||||
actual := apiStatus(k)
|
||||
if !reflect.DeepEqual(actual, &v) {
|
||||
t.Errorf("%s: Expected %#v, Got %#v", k, v, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
144
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go
generated
vendored
Normal file
144
vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go
generated
vendored
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package responsewriters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/apiserver/pkg/util/flushwriter"
|
||||
"k8s.io/apiserver/pkg/util/wsstream"
|
||||
)
|
||||
|
||||
// WriteObject renders a returned runtime.Object to the response as a stream or an encoded object. If the object
|
||||
// returned by the response implements rest.ResourceStreamer that interface will be used to render the
|
||||
// response. The Accept header and current API version will be passed in, and the output will be copied
|
||||
// directly to the response body. If content type is returned it is used, otherwise the content type will
|
||||
// be "application/octet-stream". All other objects are sent to standard JSON serialization.
|
||||
func WriteObject(statusCode int, gv schema.GroupVersion, s runtime.NegotiatedSerializer, object runtime.Object, w http.ResponseWriter, req *http.Request) {
|
||||
stream, ok := object.(rest.ResourceStreamer)
|
||||
if !ok {
|
||||
WriteObjectNegotiated(s, gv, w, req, statusCode, object)
|
||||
return
|
||||
}
|
||||
|
||||
out, flush, contentType, err := stream.InputStream(gv.String(), req.Header.Get("Accept"))
|
||||
if err != nil {
|
||||
ErrorNegotiated(err, s, gv, w, req)
|
||||
return
|
||||
}
|
||||
if out == nil {
|
||||
// No output provided - return StatusNoContent
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if wsstream.IsWebSocketRequest(req) {
|
||||
r := wsstream.NewReader(out, true, wsstream.NewDefaultReaderProtocols())
|
||||
if err := r.Copy(w, req); err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("error encountered while streaming results via websocket: %v", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(contentType) == 0 {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.WriteHeader(statusCode)
|
||||
writer := w.(io.Writer)
|
||||
if flush {
|
||||
writer = flushwriter.Wrap(w)
|
||||
}
|
||||
io.Copy(writer, out)
|
||||
}
|
||||
|
||||
// WriteObjectNegotiated renders an object in the content type negotiated by the client
|
||||
func WriteObjectNegotiated(s runtime.NegotiatedSerializer, gv schema.GroupVersion, w http.ResponseWriter, req *http.Request, statusCode int, object runtime.Object) {
|
||||
serializer, err := negotiation.NegotiateOutputSerializer(req, s)
|
||||
if err != nil {
|
||||
status := apiStatus(err)
|
||||
WriteRawJSON(int(status.Code), status, w)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", serializer.MediaType)
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
encoder := s.EncoderForVersion(serializer.Serializer, gv)
|
||||
if err := encoder.Encode(object, w); err != nil {
|
||||
errorJSONFatal(err, encoder, w)
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorNegotiated renders an error to the response. Returns the HTTP status code of the error.
|
||||
func ErrorNegotiated(err error, s runtime.NegotiatedSerializer, gv schema.GroupVersion, w http.ResponseWriter, req *http.Request) int {
|
||||
status := apiStatus(err)
|
||||
code := int(status.Code)
|
||||
// when writing an error, check to see if the status indicates a retry after period
|
||||
if status.Details != nil && status.Details.RetryAfterSeconds > 0 {
|
||||
delay := strconv.Itoa(int(status.Details.RetryAfterSeconds))
|
||||
w.Header().Set("Retry-After", delay)
|
||||
}
|
||||
|
||||
if code == http.StatusNoContent {
|
||||
w.WriteHeader(code)
|
||||
return code
|
||||
}
|
||||
|
||||
WriteObjectNegotiated(s, gv, w, req, code, status)
|
||||
return code
|
||||
}
|
||||
|
||||
// errorJSONFatal renders an error to the response, and if codec fails will render plaintext.
|
||||
// Returns the HTTP status code of the error.
|
||||
func errorJSONFatal(err error, codec runtime.Encoder, w http.ResponseWriter) int {
|
||||
utilruntime.HandleError(fmt.Errorf("apiserver was unable to write a JSON response: %v", err))
|
||||
status := apiStatus(err)
|
||||
code := int(status.Code)
|
||||
output, err := runtime.Encode(codec, status)
|
||||
if err != nil {
|
||||
w.WriteHeader(code)
|
||||
fmt.Fprintf(w, "%s: %s", status.Reason, status.Message)
|
||||
return code
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
w.Write(output)
|
||||
return code
|
||||
}
|
||||
|
||||
// WriteRawJSON writes a non-API object in JSON.
|
||||
func WriteRawJSON(statusCode int, object interface{}, w http.ResponseWriter) {
|
||||
output, err := json.MarshalIndent(object, "", " ")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
w.Write(output)
|
||||
}
|
||||
1215
vendor/k8s.io/apiserver/pkg/endpoints/handlers/rest.go
generated
vendored
Normal file
1215
vendor/k8s.io/apiserver/pkg/endpoints/handlers/rest.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
658
vendor/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go
generated
vendored
Normal file
658
vendor/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,658 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/evanphx/json-patch"
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/apiserver/pkg/apis/example"
|
||||
examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
codecs = serializer.NewCodecFactory(scheme)
|
||||
)
|
||||
|
||||
func init() {
|
||||
metav1.AddToGroupVersion(scheme, metav1.SchemeGroupVersion)
|
||||
example.AddToScheme(scheme)
|
||||
examplev1.AddToScheme(scheme)
|
||||
}
|
||||
|
||||
type testPatchType struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
TestPatchSubType `json:",inline"`
|
||||
}
|
||||
|
||||
// We explicitly make it public as private types doesn't
|
||||
// work correctly with json inlined types.
|
||||
type TestPatchSubType struct {
|
||||
StringField string `json:"theField"`
|
||||
}
|
||||
|
||||
func (obj *testPatchType) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta }
|
||||
|
||||
func TestPatchAnonymousField(t *testing.T) {
|
||||
testGV := schema.GroupVersion{Group: "", Version: "v"}
|
||||
scheme.AddKnownTypes(testGV, &testPatchType{})
|
||||
codec := codecs.LegacyCodec(testGV)
|
||||
defaulter := runtime.ObjectDefaulter(scheme)
|
||||
|
||||
original := &testPatchType{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "testPatchType", APIVersion: "v"},
|
||||
TestPatchSubType: TestPatchSubType{StringField: "my-value"},
|
||||
}
|
||||
patch := `{"theField": "changed!"}`
|
||||
expected := &testPatchType{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "testPatchType", APIVersion: "v"},
|
||||
TestPatchSubType: TestPatchSubType{StringField: "changed!"},
|
||||
}
|
||||
|
||||
actual := &testPatchType{}
|
||||
err := strategicPatchObject(codec, defaulter, original, []byte(patch), actual, &testPatchType{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !apiequality.Semantic.DeepEqual(actual, expected) {
|
||||
t.Errorf("expected %#v, got %#v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
type testPatcher struct {
|
||||
t *testing.T
|
||||
|
||||
// startingPod is used for the first Update
|
||||
startingPod *example.Pod
|
||||
|
||||
// updatePod is the pod that is used for conflict comparison and used for subsequent Update calls
|
||||
updatePod *example.Pod
|
||||
|
||||
numUpdates int
|
||||
}
|
||||
|
||||
func (p *testPatcher) New() runtime.Object {
|
||||
return &example.Pod{}
|
||||
}
|
||||
|
||||
func (p *testPatcher) Update(ctx request.Context, name string, objInfo rest.UpdatedObjectInfo) (runtime.Object, bool, error) {
|
||||
currentPod := p.startingPod
|
||||
if p.numUpdates > 0 {
|
||||
currentPod = p.updatePod
|
||||
}
|
||||
p.numUpdates++
|
||||
|
||||
obj, err := objInfo.UpdatedObject(ctx, currentPod)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
inPod := obj.(*example.Pod)
|
||||
if inPod.ResourceVersion != p.updatePod.ResourceVersion {
|
||||
return nil, false, apierrors.NewConflict(example.Resource("pods"), inPod.Name, fmt.Errorf("existing %v, new %v", p.updatePod.ResourceVersion, inPod.ResourceVersion))
|
||||
}
|
||||
|
||||
return inPod, false, nil
|
||||
}
|
||||
|
||||
func (p *testPatcher) Get(ctx request.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
p.t.Fatal("Unexpected call to testPatcher.Get")
|
||||
return nil, errors.New("Unexpected call to testPatcher.Get")
|
||||
}
|
||||
|
||||
type testNamer struct {
|
||||
namespace string
|
||||
name string
|
||||
}
|
||||
|
||||
func (p *testNamer) Namespace(req *http.Request) (namespace string, err error) {
|
||||
return p.namespace, nil
|
||||
}
|
||||
|
||||
// Name returns the name from the request, and an optional namespace value if this is a namespace
|
||||
// scoped call. An error is returned if the name is not available.
|
||||
func (p *testNamer) Name(req *http.Request) (namespace, name string, err error) {
|
||||
return p.namespace, p.name, nil
|
||||
}
|
||||
|
||||
// ObjectName returns the namespace and name from an object if they exist, or an error if the object
|
||||
// does not support names.
|
||||
func (p *testNamer) ObjectName(obj runtime.Object) (namespace, name string, err error) {
|
||||
return p.namespace, p.name, nil
|
||||
}
|
||||
|
||||
// SetSelfLink sets the provided URL onto the object. The method should return nil if the object
|
||||
// does not support selfLinks.
|
||||
func (p *testNamer) SetSelfLink(obj runtime.Object, url string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
// GenerateLink creates a path and query for a given runtime object that represents the canonical path.
|
||||
func (p *testNamer) GenerateLink(req *http.Request, obj runtime.Object) (uri string, err error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
// GenerateLink creates a path and query for a list that represents the canonical path.
|
||||
func (p *testNamer) GenerateListLink(req *http.Request) (uri string, err error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
type patchTestCase struct {
|
||||
name string
|
||||
|
||||
// admission chain to use, nil is fine
|
||||
admit updateAdmissionFunc
|
||||
|
||||
// startingPod is used as the starting point for the first Update
|
||||
startingPod *example.Pod
|
||||
// changedPod is the "destination" pod for the patch. The test will create a patch from the startingPod to the changedPod
|
||||
// to use when calling the patch operation
|
||||
changedPod *example.Pod
|
||||
// updatePod is the pod that is used for conflict comparison and as the starting point for the second Update
|
||||
updatePod *example.Pod
|
||||
|
||||
// expectedPod is the pod that you expect to get back after the patch is complete
|
||||
expectedPod *example.Pod
|
||||
expectedError string
|
||||
}
|
||||
|
||||
func (tc *patchTestCase) Run(t *testing.T) {
|
||||
t.Logf("Starting test %s", tc.name)
|
||||
|
||||
namespace := tc.startingPod.Namespace
|
||||
name := tc.startingPod.Name
|
||||
|
||||
codec := codecs.LegacyCodec(examplev1.SchemeGroupVersion)
|
||||
admit := tc.admit
|
||||
if admit == nil {
|
||||
admit = func(updatedObject runtime.Object, currentObject runtime.Object) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
ctx := request.NewDefaultContext()
|
||||
ctx = request.WithNamespace(ctx, namespace)
|
||||
|
||||
namer := &testNamer{namespace, name}
|
||||
copier := runtime.ObjectCopier(scheme)
|
||||
creater := runtime.ObjectCreater(scheme)
|
||||
defaulter := runtime.ObjectDefaulter(scheme)
|
||||
convertor := runtime.UnsafeObjectConvertor(scheme)
|
||||
kind := examplev1.SchemeGroupVersion.WithKind("Pod")
|
||||
resource := examplev1.SchemeGroupVersion.WithResource("pods")
|
||||
versionedObj := &examplev1.Pod{}
|
||||
|
||||
for _, patchType := range []types.PatchType{types.JSONPatchType, types.MergePatchType, types.StrategicMergePatchType} {
|
||||
// This needs to be reset on each iteration.
|
||||
testPatcher := &testPatcher{
|
||||
t: t,
|
||||
startingPod: tc.startingPod,
|
||||
updatePod: tc.updatePod,
|
||||
}
|
||||
|
||||
// TODO SUPPORT THIS!
|
||||
if patchType == types.JSONPatchType {
|
||||
continue
|
||||
}
|
||||
t.Logf("Working with patchType %v", patchType)
|
||||
|
||||
originalObjJS, err := runtime.Encode(codec, tc.startingPod)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
changedJS, err := runtime.Encode(codec, tc.changedPod)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
patch := []byte{}
|
||||
switch patchType {
|
||||
case types.JSONPatchType:
|
||||
continue
|
||||
|
||||
case types.StrategicMergePatchType:
|
||||
patch, err = strategicpatch.CreateTwoWayMergePatch(originalObjJS, changedJS, versionedObj)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
case types.MergePatchType:
|
||||
patch, err = jsonpatch.CreateMergePatch(originalObjJS, changedJS)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
resultObj, err := patchResource(ctx, admit, 1*time.Second, versionedObj, testPatcher, name, patchType, patch, namer, copier, creater, defaulter, convertor, kind, resource, codec)
|
||||
if len(tc.expectedError) != 0 {
|
||||
if err == nil || err.Error() != tc.expectedError {
|
||||
t.Errorf("%s: expected error %v, but got %v", tc.name, tc.expectedError, err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if tc.expectedPod == nil {
|
||||
if resultObj != nil {
|
||||
t.Errorf("%s: unexpected result: %v", tc.name, resultObj)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
resultPod := resultObj.(*example.Pod)
|
||||
|
||||
// roundtrip to get defaulting
|
||||
expectedJS, err := runtime.Encode(codec, tc.expectedPod)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
expectedObj, err := runtime.Decode(codec, expectedJS)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
reallyExpectedPod := expectedObj.(*example.Pod)
|
||||
|
||||
if !reflect.DeepEqual(*reallyExpectedPod, *resultPod) {
|
||||
t.Errorf("%s mismatch: %v\n", tc.name, diff.ObjectGoPrintDiff(reallyExpectedPod, resultPod))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNumberConversion(t *testing.T) {
|
||||
codec := codecs.LegacyCodec(examplev1.SchemeGroupVersion)
|
||||
defaulter := runtime.ObjectDefaulter(scheme)
|
||||
|
||||
terminationGracePeriodSeconds := int64(42)
|
||||
activeDeadlineSeconds := int64(42)
|
||||
currentVersionedObject := &examplev1.Pod{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "Example", APIVersion: examplev1.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-example"},
|
||||
Spec: examplev1.PodSpec{
|
||||
TerminationGracePeriodSeconds: &terminationGracePeriodSeconds,
|
||||
ActiveDeadlineSeconds: &activeDeadlineSeconds,
|
||||
},
|
||||
}
|
||||
versionedObjToUpdate := &examplev1.Pod{}
|
||||
versionedObj := &examplev1.Pod{}
|
||||
|
||||
patchJS := []byte(`{"spec":{"terminationGracePeriodSeconds":42,"activeDeadlineSeconds":120}}`)
|
||||
|
||||
err := strategicPatchObject(codec, defaulter, currentVersionedObject, patchJS, versionedObjToUpdate, versionedObj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if versionedObjToUpdate.Spec.TerminationGracePeriodSeconds == nil || *versionedObjToUpdate.Spec.TerminationGracePeriodSeconds != 42 ||
|
||||
versionedObjToUpdate.Spec.ActiveDeadlineSeconds == nil || *versionedObjToUpdate.Spec.ActiveDeadlineSeconds != 120 {
|
||||
t.Fatal(errors.New("Ports failed to merge because of number conversion issue"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchResourceNumberConversion(t *testing.T) {
|
||||
namespace := "bar"
|
||||
name := "foo"
|
||||
uid := types.UID("uid")
|
||||
fifteen := int64(15)
|
||||
thirty := int64(30)
|
||||
|
||||
tc := &patchTestCase{
|
||||
name: "TestPatchResourceNumberConversion",
|
||||
|
||||
startingPod: &example.Pod{},
|
||||
changedPod: &example.Pod{},
|
||||
updatePod: &example.Pod{},
|
||||
|
||||
expectedPod: &example.Pod{},
|
||||
}
|
||||
|
||||
tc.startingPod.Name = name
|
||||
tc.startingPod.Namespace = namespace
|
||||
tc.startingPod.UID = uid
|
||||
tc.startingPod.ResourceVersion = "1"
|
||||
tc.startingPod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.startingPod.Spec.ActiveDeadlineSeconds = &fifteen
|
||||
|
||||
// Patch tries to change to 30.
|
||||
tc.changedPod.Name = name
|
||||
tc.changedPod.Namespace = namespace
|
||||
tc.changedPod.UID = uid
|
||||
tc.changedPod.ResourceVersion = "1"
|
||||
tc.changedPod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.changedPod.Spec.ActiveDeadlineSeconds = &thirty
|
||||
|
||||
// Someone else already changed it to 30.
|
||||
// This should be fine since it's not a "meaningful conflict".
|
||||
// Previously this was detected as a meaningful conflict because int64(30) != float64(30).
|
||||
tc.updatePod.Name = name
|
||||
tc.updatePod.Namespace = namespace
|
||||
tc.updatePod.UID = uid
|
||||
tc.updatePod.ResourceVersion = "2"
|
||||
tc.updatePod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.updatePod.Spec.ActiveDeadlineSeconds = &thirty
|
||||
tc.updatePod.Spec.NodeName = "anywhere"
|
||||
|
||||
tc.expectedPod.Name = name
|
||||
tc.expectedPod.Namespace = namespace
|
||||
tc.expectedPod.UID = uid
|
||||
tc.expectedPod.ResourceVersion = "2"
|
||||
tc.expectedPod.Spec.ActiveDeadlineSeconds = &thirty
|
||||
tc.expectedPod.Spec.NodeName = "anywhere"
|
||||
|
||||
tc.Run(t)
|
||||
}
|
||||
|
||||
func TestPatchResourceWithVersionConflict(t *testing.T) {
|
||||
namespace := "bar"
|
||||
name := "foo"
|
||||
uid := types.UID("uid")
|
||||
fifteen := int64(15)
|
||||
thirty := int64(30)
|
||||
|
||||
tc := &patchTestCase{
|
||||
name: "TestPatchResourceWithVersionConflict",
|
||||
|
||||
startingPod: &example.Pod{},
|
||||
changedPod: &example.Pod{},
|
||||
updatePod: &example.Pod{},
|
||||
|
||||
expectedPod: &example.Pod{},
|
||||
}
|
||||
|
||||
tc.startingPod.Name = name
|
||||
tc.startingPod.Namespace = namespace
|
||||
tc.startingPod.UID = uid
|
||||
tc.startingPod.ResourceVersion = "1"
|
||||
tc.startingPod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.startingPod.Spec.ActiveDeadlineSeconds = &fifteen
|
||||
|
||||
tc.changedPod.Name = name
|
||||
tc.changedPod.Namespace = namespace
|
||||
tc.changedPod.UID = uid
|
||||
tc.changedPod.ResourceVersion = "1"
|
||||
tc.changedPod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.changedPod.Spec.ActiveDeadlineSeconds = &thirty
|
||||
|
||||
tc.updatePod.Name = name
|
||||
tc.updatePod.Namespace = namespace
|
||||
tc.updatePod.UID = uid
|
||||
tc.updatePod.ResourceVersion = "2"
|
||||
tc.updatePod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.updatePod.Spec.ActiveDeadlineSeconds = &fifteen
|
||||
tc.updatePod.Spec.NodeName = "anywhere"
|
||||
|
||||
tc.expectedPod.Name = name
|
||||
tc.expectedPod.Namespace = namespace
|
||||
tc.expectedPod.UID = uid
|
||||
tc.expectedPod.ResourceVersion = "2"
|
||||
tc.expectedPod.Spec.ActiveDeadlineSeconds = &thirty
|
||||
tc.expectedPod.Spec.NodeName = "anywhere"
|
||||
|
||||
tc.Run(t)
|
||||
}
|
||||
|
||||
func TestPatchResourceWithConflict(t *testing.T) {
|
||||
namespace := "bar"
|
||||
name := "foo"
|
||||
uid := types.UID("uid")
|
||||
|
||||
tc := &patchTestCase{
|
||||
name: "TestPatchResourceWithConflict",
|
||||
|
||||
startingPod: &example.Pod{},
|
||||
changedPod: &example.Pod{},
|
||||
updatePod: &example.Pod{},
|
||||
|
||||
expectedError: `Operation cannot be fulfilled on pods.example.apiserver.k8s.io "foo": existing 2, new 1`,
|
||||
}
|
||||
|
||||
tc.startingPod.Name = name
|
||||
tc.startingPod.Namespace = namespace
|
||||
tc.startingPod.UID = uid
|
||||
tc.startingPod.ResourceVersion = "1"
|
||||
tc.startingPod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.startingPod.Spec.NodeName = "here"
|
||||
|
||||
tc.changedPod.Name = name
|
||||
tc.changedPod.Namespace = namespace
|
||||
tc.changedPod.UID = uid
|
||||
tc.changedPod.ResourceVersion = "1"
|
||||
tc.changedPod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.changedPod.Spec.NodeName = "there"
|
||||
|
||||
tc.updatePod.Name = name
|
||||
tc.updatePod.Namespace = namespace
|
||||
tc.updatePod.UID = uid
|
||||
tc.updatePod.ResourceVersion = "2"
|
||||
tc.updatePod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.updatePod.Spec.NodeName = "anywhere"
|
||||
|
||||
tc.Run(t)
|
||||
}
|
||||
|
||||
func TestPatchWithAdmissionRejection(t *testing.T) {
|
||||
namespace := "bar"
|
||||
name := "foo"
|
||||
uid := types.UID("uid")
|
||||
fifteen := int64(15)
|
||||
thirty := int64(30)
|
||||
|
||||
tc := &patchTestCase{
|
||||
name: "TestPatchWithAdmissionRejection",
|
||||
|
||||
admit: func(updatedObject runtime.Object, currentObject runtime.Object) error {
|
||||
return errors.New("admission failure")
|
||||
},
|
||||
|
||||
startingPod: &example.Pod{},
|
||||
changedPod: &example.Pod{},
|
||||
updatePod: &example.Pod{},
|
||||
|
||||
expectedError: "admission failure",
|
||||
}
|
||||
|
||||
tc.startingPod.Name = name
|
||||
tc.startingPod.Namespace = namespace
|
||||
tc.startingPod.UID = uid
|
||||
tc.startingPod.ResourceVersion = "1"
|
||||
tc.startingPod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.startingPod.Spec.ActiveDeadlineSeconds = &fifteen
|
||||
|
||||
tc.changedPod.Name = name
|
||||
tc.changedPod.Namespace = namespace
|
||||
tc.changedPod.UID = uid
|
||||
tc.changedPod.ResourceVersion = "1"
|
||||
tc.changedPod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.changedPod.Spec.ActiveDeadlineSeconds = &thirty
|
||||
|
||||
tc.Run(t)
|
||||
}
|
||||
|
||||
func TestPatchWithVersionConflictThenAdmissionFailure(t *testing.T) {
|
||||
namespace := "bar"
|
||||
name := "foo"
|
||||
uid := types.UID("uid")
|
||||
fifteen := int64(15)
|
||||
thirty := int64(30)
|
||||
seen := false
|
||||
|
||||
tc := &patchTestCase{
|
||||
name: "TestPatchWithVersionConflictThenAdmissionFailure",
|
||||
|
||||
admit: func(updatedObject runtime.Object, currentObject runtime.Object) error {
|
||||
if seen {
|
||||
return errors.New("admission failure")
|
||||
}
|
||||
|
||||
seen = true
|
||||
return nil
|
||||
},
|
||||
|
||||
startingPod: &example.Pod{},
|
||||
changedPod: &example.Pod{},
|
||||
updatePod: &example.Pod{},
|
||||
|
||||
expectedError: "admission failure",
|
||||
}
|
||||
|
||||
tc.startingPod.Name = name
|
||||
tc.startingPod.Namespace = namespace
|
||||
tc.startingPod.UID = uid
|
||||
tc.startingPod.ResourceVersion = "1"
|
||||
tc.startingPod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.startingPod.Spec.ActiveDeadlineSeconds = &fifteen
|
||||
|
||||
tc.changedPod.Name = name
|
||||
tc.changedPod.Namespace = namespace
|
||||
tc.changedPod.UID = uid
|
||||
tc.changedPod.ResourceVersion = "1"
|
||||
tc.changedPod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.changedPod.Spec.ActiveDeadlineSeconds = &thirty
|
||||
|
||||
tc.updatePod.Name = name
|
||||
tc.updatePod.Namespace = namespace
|
||||
tc.updatePod.UID = uid
|
||||
tc.updatePod.ResourceVersion = "2"
|
||||
tc.updatePod.APIVersion = examplev1.SchemeGroupVersion.String()
|
||||
tc.updatePod.Spec.ActiveDeadlineSeconds = &fifteen
|
||||
tc.updatePod.Spec.NodeName = "anywhere"
|
||||
|
||||
tc.Run(t)
|
||||
}
|
||||
|
||||
func TestHasUID(t *testing.T) {
|
||||
testcases := []struct {
|
||||
obj runtime.Object
|
||||
hasUID bool
|
||||
}{
|
||||
{obj: nil, hasUID: false},
|
||||
{obj: &example.Pod{}, hasUID: false},
|
||||
{obj: nil, hasUID: false},
|
||||
{obj: runtime.Object(nil), hasUID: false},
|
||||
{obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("A")}}, hasUID: true},
|
||||
}
|
||||
for i, tc := range testcases {
|
||||
actual, err := hasUID(tc.obj)
|
||||
if err != nil {
|
||||
t.Errorf("%d: unexpected error %v", i, err)
|
||||
continue
|
||||
}
|
||||
if tc.hasUID != actual {
|
||||
t.Errorf("%d: expected %v, got %v", i, tc.hasUID, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTimeout(t *testing.T) {
|
||||
if d := parseTimeout(""); d != 30*time.Second {
|
||||
t.Errorf("blank timeout produces %v", d)
|
||||
}
|
||||
if d := parseTimeout("not a timeout"); d != 30*time.Second {
|
||||
t.Errorf("bad timeout produces %v", d)
|
||||
}
|
||||
if d := parseTimeout("10s"); d != 10*time.Second {
|
||||
t.Errorf("10s timeout produced: %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinishRequest(t *testing.T) {
|
||||
exampleObj := &example.Pod{}
|
||||
exampleErr := fmt.Errorf("error")
|
||||
successStatusObj := &metav1.Status{Status: metav1.StatusSuccess, Message: "success message"}
|
||||
errorStatusObj := &metav1.Status{Status: metav1.StatusFailure, Message: "error message"}
|
||||
testcases := []struct {
|
||||
timeout time.Duration
|
||||
fn resultFunc
|
||||
expectedObj runtime.Object
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
// Expected obj is returned.
|
||||
timeout: time.Second,
|
||||
fn: func() (runtime.Object, error) {
|
||||
return exampleObj, nil
|
||||
},
|
||||
expectedObj: exampleObj,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
// Expected error is returned.
|
||||
timeout: time.Second,
|
||||
fn: func() (runtime.Object, error) {
|
||||
return nil, exampleErr
|
||||
},
|
||||
expectedObj: nil,
|
||||
expectedErr: exampleErr,
|
||||
},
|
||||
{
|
||||
// Successful status object is returned as expected.
|
||||
timeout: time.Second,
|
||||
fn: func() (runtime.Object, error) {
|
||||
return successStatusObj, nil
|
||||
},
|
||||
expectedObj: successStatusObj,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
// Error status object is converted to StatusError.
|
||||
timeout: time.Second,
|
||||
fn: func() (runtime.Object, error) {
|
||||
return errorStatusObj, nil
|
||||
},
|
||||
expectedObj: nil,
|
||||
expectedErr: apierrors.FromObject(errorStatusObj),
|
||||
},
|
||||
}
|
||||
for i, tc := range testcases {
|
||||
obj, err := finishRequest(tc.timeout, tc.fn)
|
||||
if (err == nil && tc.expectedErr != nil) || (err != nil && tc.expectedErr == nil) || (err != nil && tc.expectedErr != nil && err.Error() != tc.expectedErr.Error()) {
|
||||
t.Errorf("%d: unexpected err. expected: %v, got: %v", i, tc.expectedErr, err)
|
||||
}
|
||||
if !apiequality.Semantic.DeepEqual(obj, tc.expectedObj) {
|
||||
t.Errorf("%d: unexpected obj. expected %#v, got %#v", i, tc.expectedObj, obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
311
vendor/k8s.io/apiserver/pkg/endpoints/handlers/watch.go
generated
vendored
Normal file
311
vendor/k8s.io/apiserver/pkg/endpoints/handlers/watch.go
generated
vendored
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/streaming"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||
"k8s.io/apiserver/pkg/server/httplog"
|
||||
"k8s.io/apiserver/pkg/util/wsstream"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
// nothing will ever be sent down this channel
|
||||
var neverExitWatch <-chan time.Time = make(chan time.Time)
|
||||
|
||||
// timeoutFactory abstracts watch timeout logic for testing
|
||||
type TimeoutFactory interface {
|
||||
TimeoutCh() (<-chan time.Time, func() bool)
|
||||
}
|
||||
|
||||
// realTimeoutFactory implements timeoutFactory
|
||||
type realTimeoutFactory struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// TimeoutChan returns a channel which will receive something when the watch times out,
|
||||
// and a cleanup function to call when this happens.
|
||||
func (w *realTimeoutFactory) TimeoutCh() (<-chan time.Time, func() bool) {
|
||||
if w.timeout == 0 {
|
||||
return neverExitWatch, func() bool { return false }
|
||||
}
|
||||
t := time.NewTimer(w.timeout)
|
||||
return t.C, t.Stop
|
||||
}
|
||||
|
||||
// serveWatch handles serving requests to the server
|
||||
// TODO: the functionality in this method and in WatchServer.Serve is not cleanly decoupled.
|
||||
func serveWatch(watcher watch.Interface, scope RequestScope, req *http.Request, w http.ResponseWriter, timeout time.Duration) {
|
||||
// negotiate for the stream serializer
|
||||
serializer, err := negotiation.NegotiateOutputStreamSerializer(req, scope.Serializer)
|
||||
if err != nil {
|
||||
scope.err(err, w, req)
|
||||
return
|
||||
}
|
||||
framer := serializer.StreamSerializer.Framer
|
||||
streamSerializer := serializer.StreamSerializer.Serializer
|
||||
embedded := serializer.Serializer
|
||||
if framer == nil {
|
||||
scope.err(fmt.Errorf("no framer defined for %q available for embedded encoding", serializer.MediaType), w, req)
|
||||
return
|
||||
}
|
||||
encoder := scope.Serializer.EncoderForVersion(streamSerializer, scope.Kind.GroupVersion())
|
||||
|
||||
useTextFraming := serializer.EncodesAsText
|
||||
|
||||
// find the embedded serializer matching the media type
|
||||
embeddedEncoder := scope.Serializer.EncoderForVersion(embedded, scope.Kind.GroupVersion())
|
||||
|
||||
// TODO: next step, get back mediaTypeOptions from negotiate and return the exact value here
|
||||
mediaType := serializer.MediaType
|
||||
if mediaType != runtime.ContentTypeJSON {
|
||||
mediaType += ";stream=watch"
|
||||
}
|
||||
|
||||
server := &WatchServer{
|
||||
Watching: watcher,
|
||||
Scope: scope,
|
||||
|
||||
UseTextFraming: useTextFraming,
|
||||
MediaType: mediaType,
|
||||
Framer: framer,
|
||||
Encoder: encoder,
|
||||
EmbeddedEncoder: embeddedEncoder,
|
||||
Fixup: func(obj runtime.Object) {
|
||||
if err := setSelfLink(obj, req, scope.Namer); err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("failed to set link for object %v: %v", reflect.TypeOf(obj), err))
|
||||
}
|
||||
},
|
||||
|
||||
TimeoutFactory: &realTimeoutFactory{timeout},
|
||||
}
|
||||
|
||||
server.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
// WatchServer serves a watch.Interface over a websocket or vanilla HTTP.
|
||||
type WatchServer struct {
|
||||
Watching watch.Interface
|
||||
Scope RequestScope
|
||||
|
||||
// true if websocket messages should use text framing (as opposed to binary framing)
|
||||
UseTextFraming bool
|
||||
// the media type this watch is being served with
|
||||
MediaType string
|
||||
// used to frame the watch stream
|
||||
Framer runtime.Framer
|
||||
// used to encode the watch stream event itself
|
||||
Encoder runtime.Encoder
|
||||
// used to encode the nested object in the watch stream
|
||||
EmbeddedEncoder runtime.Encoder
|
||||
Fixup func(runtime.Object)
|
||||
|
||||
TimeoutFactory TimeoutFactory
|
||||
}
|
||||
|
||||
// ServeHTTP serves a series of encoded events via HTTP with Transfer-Encoding: chunked
|
||||
// or over a websocket connection.
|
||||
func (s *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
w = httplog.Unlogged(w)
|
||||
|
||||
if wsstream.IsWebSocketRequest(req) {
|
||||
w.Header().Set("Content-Type", s.MediaType)
|
||||
websocket.Handler(s.HandleWS).ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
cn, ok := w.(http.CloseNotifier)
|
||||
if !ok {
|
||||
err := fmt.Errorf("unable to start watch - can't get http.CloseNotifier: %#v", w)
|
||||
utilruntime.HandleError(err)
|
||||
s.Scope.err(errors.NewInternalError(err), w, req)
|
||||
return
|
||||
}
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
err := fmt.Errorf("unable to start watch - can't get http.Flusher: %#v", w)
|
||||
utilruntime.HandleError(err)
|
||||
s.Scope.err(errors.NewInternalError(err), w, req)
|
||||
return
|
||||
}
|
||||
|
||||
framer := s.Framer.NewFrameWriter(w)
|
||||
if framer == nil {
|
||||
// programmer error
|
||||
err := fmt.Errorf("no stream framing support is available for media type %q", s.MediaType)
|
||||
utilruntime.HandleError(err)
|
||||
s.Scope.err(errors.NewBadRequest(err.Error()), w, req)
|
||||
return
|
||||
}
|
||||
e := streaming.NewEncoder(framer, s.Encoder)
|
||||
|
||||
// ensure the connection times out
|
||||
timeoutCh, cleanup := s.TimeoutFactory.TimeoutCh()
|
||||
defer cleanup()
|
||||
defer s.Watching.Stop()
|
||||
|
||||
// begin the stream
|
||||
w.Header().Set("Content-Type", s.MediaType)
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
flusher.Flush()
|
||||
|
||||
var unknown runtime.Unknown
|
||||
internalEvent := &metav1.InternalEvent{}
|
||||
buf := &bytes.Buffer{}
|
||||
ch := s.Watching.ResultChan()
|
||||
for {
|
||||
select {
|
||||
case <-cn.CloseNotify():
|
||||
return
|
||||
case <-timeoutCh:
|
||||
return
|
||||
case event, ok := <-ch:
|
||||
if !ok {
|
||||
// End of results.
|
||||
return
|
||||
}
|
||||
|
||||
obj := event.Object
|
||||
s.Fixup(obj)
|
||||
if err := s.EmbeddedEncoder.Encode(obj, buf); err != nil {
|
||||
// unexpected error
|
||||
utilruntime.HandleError(fmt.Errorf("unable to encode watch object: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// ContentType is not required here because we are defaulting to the serializer
|
||||
// type
|
||||
unknown.Raw = buf.Bytes()
|
||||
event.Object = &unknown
|
||||
|
||||
// create the external type directly and encode it. Clients will only recognize the serialization we provide.
|
||||
// The internal event is being reused, not reallocated so its just a few extra assignments to do it this way
|
||||
// and we get the benefit of using conversion functions which already have to stay in sync
|
||||
outEvent := &metav1.WatchEvent{}
|
||||
*internalEvent = metav1.InternalEvent(event)
|
||||
err := metav1.Convert_versioned_InternalEvent_to_versioned_Event(internalEvent, outEvent, nil)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("unable to convert watch object: %v", err))
|
||||
// client disconnect.
|
||||
return
|
||||
}
|
||||
if err := e.Encode(outEvent); err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("unable to encode watch object: %v (%#v)", err, e))
|
||||
// client disconnect.
|
||||
return
|
||||
}
|
||||
if len(ch) == 0 {
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleWS implements a websocket handler.
|
||||
func (s *WatchServer) HandleWS(ws *websocket.Conn) {
|
||||
defer ws.Close()
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer utilruntime.HandleCrash()
|
||||
// This blocks until the connection is closed.
|
||||
// Client should not send anything.
|
||||
wsstream.IgnoreReceives(ws, 0)
|
||||
// Once the client closes, we should also close
|
||||
close(done)
|
||||
}()
|
||||
|
||||
var unknown runtime.Unknown
|
||||
internalEvent := &metav1.InternalEvent{}
|
||||
buf := &bytes.Buffer{}
|
||||
streamBuf := &bytes.Buffer{}
|
||||
ch := s.Watching.ResultChan()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
s.Watching.Stop()
|
||||
return
|
||||
case event, ok := <-ch:
|
||||
if !ok {
|
||||
// End of results.
|
||||
return
|
||||
}
|
||||
obj := event.Object
|
||||
s.Fixup(obj)
|
||||
if err := s.EmbeddedEncoder.Encode(obj, buf); err != nil {
|
||||
// unexpected error
|
||||
utilruntime.HandleError(fmt.Errorf("unable to encode watch object: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// ContentType is not required here because we are defaulting to the serializer
|
||||
// type
|
||||
unknown.Raw = buf.Bytes()
|
||||
event.Object = &unknown
|
||||
|
||||
// the internal event will be versioned by the encoder
|
||||
// create the external type directly and encode it. Clients will only recognize the serialization we provide.
|
||||
// The internal event is being reused, not reallocated so its just a few extra assignments to do it this way
|
||||
// and we get the benefit of using conversion functions which already have to stay in sync
|
||||
outEvent := &metav1.WatchEvent{}
|
||||
*internalEvent = metav1.InternalEvent(event)
|
||||
err := metav1.Convert_versioned_InternalEvent_to_versioned_Event(internalEvent, outEvent, nil)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("unable to convert watch object: %v", err))
|
||||
// client disconnect.
|
||||
s.Watching.Stop()
|
||||
return
|
||||
}
|
||||
if err := s.Encoder.Encode(outEvent, streamBuf); err != nil {
|
||||
// encoding error
|
||||
utilruntime.HandleError(fmt.Errorf("unable to encode event: %v", err))
|
||||
s.Watching.Stop()
|
||||
return
|
||||
}
|
||||
if s.UseTextFraming {
|
||||
if err := websocket.Message.Send(ws, streamBuf.String()); err != nil {
|
||||
// Client disconnect.
|
||||
s.Watching.Stop()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := websocket.Message.Send(ws, streamBuf.Bytes()); err != nil {
|
||||
// Client disconnect.
|
||||
s.Watching.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
buf.Reset()
|
||||
streamBuf.Reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
1054
vendor/k8s.io/apiserver/pkg/endpoints/installer.go
generated
vendored
Normal file
1054
vendor/k8s.io/apiserver/pkg/endpoints/installer.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
89
vendor/k8s.io/apiserver/pkg/endpoints/installer_test.go
generated
vendored
Normal file
89
vendor/k8s.io/apiserver/pkg/endpoints/installer_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsVowel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg rune
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "yes",
|
||||
arg: 'E',
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no",
|
||||
arg: 'n',
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := isVowel(tt.arg); got != tt.want {
|
||||
t.Errorf("%q. IsVowel() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetArticleForNoun(t *testing.T) {
|
||||
tests := []struct {
|
||||
noun string
|
||||
padding string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
noun: "Frog",
|
||||
padding: " ",
|
||||
want: " a ",
|
||||
},
|
||||
{
|
||||
noun: "frogs",
|
||||
padding: " ",
|
||||
want: " ",
|
||||
},
|
||||
{
|
||||
noun: "apple",
|
||||
padding: "",
|
||||
want: "an",
|
||||
},
|
||||
{
|
||||
noun: "Apples",
|
||||
padding: " ",
|
||||
want: " ",
|
||||
},
|
||||
{
|
||||
noun: "Ingress",
|
||||
padding: " ",
|
||||
want: " an ",
|
||||
},
|
||||
{
|
||||
noun: "Class",
|
||||
padding: " ",
|
||||
want: " a ",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := getArticleForNoun(tt.noun, tt.padding); got != tt.want {
|
||||
t.Errorf("%q. GetArticleForNoun() = %v, want %v", tt.noun, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
vendor/k8s.io/apiserver/pkg/endpoints/metrics/BUILD
generated
vendored
Normal file
27
vendor/k8s.io/apiserver/pkg/endpoints/metrics/BUILD
generated
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["metrics_test.go"],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["metrics.go"],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/emicklei/go-restful:go_default_library",
|
||||
"//vendor/github.com/prometheus/client_golang/prometheus:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
|
||||
],
|
||||
)
|
||||
3
vendor/k8s.io/apiserver/pkg/endpoints/metrics/OWNERS
generated
vendored
Normal file
3
vendor/k8s.io/apiserver/pkg/endpoints/metrics/OWNERS
generated
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
reviewers:
|
||||
- wojtek-t
|
||||
- jimmidyson
|
||||
263
vendor/k8s.io/apiserver/pkg/endpoints/metrics/metrics.go
generated
vendored
Normal file
263
vendor/k8s.io/apiserver/pkg/endpoints/metrics/metrics.go
generated
vendored
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
// TODO(a-robinson): Add unit tests for the handling of these metrics once
|
||||
// the upstream library supports it.
|
||||
requestCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "apiserver_request_count",
|
||||
Help: "Counter of apiserver requests broken out for each verb, API resource, client, and HTTP response contentType and code.",
|
||||
},
|
||||
[]string{"verb", "resource", "client", "contentType", "code"},
|
||||
)
|
||||
requestLatencies = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "apiserver_request_latencies",
|
||||
Help: "Response latency distribution in microseconds for each verb, resource and client.",
|
||||
// Use buckets ranging from 125 ms to 8 seconds.
|
||||
Buckets: prometheus.ExponentialBuckets(125000, 2.0, 7),
|
||||
},
|
||||
[]string{"verb", "resource"},
|
||||
)
|
||||
requestLatenciesSummary = prometheus.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: "apiserver_request_latencies_summary",
|
||||
Help: "Response latency summary in microseconds for each verb and resource.",
|
||||
// Make the sliding window of 1h.
|
||||
MaxAge: time.Hour,
|
||||
},
|
||||
[]string{"verb", "resource"},
|
||||
)
|
||||
kubectlExeRegexp = regexp.MustCompile(`^.*((?i:kubectl\.exe))`)
|
||||
)
|
||||
|
||||
// Register all metrics.
|
||||
func Register() {
|
||||
prometheus.MustRegister(requestCounter)
|
||||
prometheus.MustRegister(requestLatencies)
|
||||
prometheus.MustRegister(requestLatenciesSummary)
|
||||
}
|
||||
|
||||
func Monitor(verb, resource *string, client, contentType string, httpCode int, reqStart time.Time) {
|
||||
elapsed := float64((time.Since(reqStart)) / time.Microsecond)
|
||||
requestCounter.WithLabelValues(*verb, *resource, client, contentType, codeToString(httpCode)).Inc()
|
||||
requestLatencies.WithLabelValues(*verb, *resource).Observe(elapsed)
|
||||
requestLatenciesSummary.WithLabelValues(*verb, *resource).Observe(elapsed)
|
||||
}
|
||||
|
||||
func Reset() {
|
||||
requestCounter.Reset()
|
||||
requestLatencies.Reset()
|
||||
requestLatenciesSummary.Reset()
|
||||
}
|
||||
|
||||
// InstrumentRouteFunc works like Prometheus' InstrumentHandlerFunc but wraps
|
||||
// the go-restful RouteFunction instead of a HandlerFunc
|
||||
func InstrumentRouteFunc(verb, resource string, routeFunc restful.RouteFunction) restful.RouteFunction {
|
||||
return restful.RouteFunction(func(request *restful.Request, response *restful.Response) {
|
||||
now := time.Now()
|
||||
|
||||
delegate := &responseWriterDelegator{ResponseWriter: response.ResponseWriter}
|
||||
|
||||
_, cn := response.ResponseWriter.(http.CloseNotifier)
|
||||
_, fl := response.ResponseWriter.(http.Flusher)
|
||||
_, hj := response.ResponseWriter.(http.Hijacker)
|
||||
var rw http.ResponseWriter
|
||||
if cn && fl && hj {
|
||||
rw = &fancyResponseWriterDelegator{delegate}
|
||||
} else {
|
||||
rw = delegate
|
||||
}
|
||||
response.ResponseWriter = rw
|
||||
|
||||
routeFunc(request, response)
|
||||
|
||||
reportedVerb := verb
|
||||
if verb == "LIST" && strings.ToLower(request.QueryParameter("watch")) == "true" {
|
||||
reportedVerb = "WATCH"
|
||||
}
|
||||
Monitor(&reportedVerb, &resource, cleanUserAgent(utilnet.GetHTTPClient(request.Request)), rw.Header().Get("Content-Type"), delegate.status, now)
|
||||
})
|
||||
}
|
||||
|
||||
func cleanUserAgent(ua string) string {
|
||||
// We collapse all "web browser"-type user agents into one "browser" to reduce metric cardinality.
|
||||
if strings.HasPrefix(ua, "Mozilla/") {
|
||||
return "Browser"
|
||||
}
|
||||
// If an old "kubectl.exe" has passed us its full path, we discard the path portion.
|
||||
ua = kubectlExeRegexp.ReplaceAllString(ua, "$1")
|
||||
return ua
|
||||
}
|
||||
|
||||
type responseWriterDelegator struct {
|
||||
http.ResponseWriter
|
||||
|
||||
status int
|
||||
written int64
|
||||
wroteHeader bool
|
||||
}
|
||||
|
||||
func (r *responseWriterDelegator) WriteHeader(code int) {
|
||||
r.status = code
|
||||
r.wroteHeader = true
|
||||
r.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (r *responseWriterDelegator) Write(b []byte) (int, error) {
|
||||
if !r.wroteHeader {
|
||||
r.WriteHeader(http.StatusOK)
|
||||
}
|
||||
n, err := r.ResponseWriter.Write(b)
|
||||
r.written += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
type fancyResponseWriterDelegator struct {
|
||||
*responseWriterDelegator
|
||||
}
|
||||
|
||||
func (f *fancyResponseWriterDelegator) CloseNotify() <-chan bool {
|
||||
return f.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
||||
}
|
||||
|
||||
func (f *fancyResponseWriterDelegator) Flush() {
|
||||
f.ResponseWriter.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
func (f *fancyResponseWriterDelegator) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
return f.ResponseWriter.(http.Hijacker).Hijack()
|
||||
}
|
||||
|
||||
// Small optimization over Itoa
|
||||
func codeToString(s int) string {
|
||||
switch s {
|
||||
case 100:
|
||||
return "100"
|
||||
case 101:
|
||||
return "101"
|
||||
|
||||
case 200:
|
||||
return "200"
|
||||
case 201:
|
||||
return "201"
|
||||
case 202:
|
||||
return "202"
|
||||
case 203:
|
||||
return "203"
|
||||
case 204:
|
||||
return "204"
|
||||
case 205:
|
||||
return "205"
|
||||
case 206:
|
||||
return "206"
|
||||
|
||||
case 300:
|
||||
return "300"
|
||||
case 301:
|
||||
return "301"
|
||||
case 302:
|
||||
return "302"
|
||||
case 304:
|
||||
return "304"
|
||||
case 305:
|
||||
return "305"
|
||||
case 307:
|
||||
return "307"
|
||||
|
||||
case 400:
|
||||
return "400"
|
||||
case 401:
|
||||
return "401"
|
||||
case 402:
|
||||
return "402"
|
||||
case 403:
|
||||
return "403"
|
||||
case 404:
|
||||
return "404"
|
||||
case 405:
|
||||
return "405"
|
||||
case 406:
|
||||
return "406"
|
||||
case 407:
|
||||
return "407"
|
||||
case 408:
|
||||
return "408"
|
||||
case 409:
|
||||
return "409"
|
||||
case 410:
|
||||
return "410"
|
||||
case 411:
|
||||
return "411"
|
||||
case 412:
|
||||
return "412"
|
||||
case 413:
|
||||
return "413"
|
||||
case 414:
|
||||
return "414"
|
||||
case 415:
|
||||
return "415"
|
||||
case 416:
|
||||
return "416"
|
||||
case 417:
|
||||
return "417"
|
||||
case 418:
|
||||
return "418"
|
||||
|
||||
case 500:
|
||||
return "500"
|
||||
case 501:
|
||||
return "501"
|
||||
case 502:
|
||||
return "502"
|
||||
case 503:
|
||||
return "503"
|
||||
case 504:
|
||||
return "504"
|
||||
case 505:
|
||||
return "505"
|
||||
|
||||
case 428:
|
||||
return "428"
|
||||
case 429:
|
||||
return "429"
|
||||
case 431:
|
||||
return "431"
|
||||
case 511:
|
||||
return "511"
|
||||
|
||||
default:
|
||||
return strconv.Itoa(s)
|
||||
}
|
||||
}
|
||||
54
vendor/k8s.io/apiserver/pkg/endpoints/metrics/metrics_test.go
generated
vendored
Normal file
54
vendor/k8s.io/apiserver/pkg/endpoints/metrics/metrics_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package metrics
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCleanUserAgent(t *testing.T) {
|
||||
panicBuf := []byte{198, 73, 129, 133, 90, 216, 104, 29, 13, 134, 209, 233, 30, 0, 22}
|
||||
|
||||
for _, tc := range []struct {
|
||||
In string
|
||||
Out string
|
||||
}{
|
||||
{
|
||||
In: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
|
||||
Out: "Browser",
|
||||
},
|
||||
{
|
||||
In: "kubectl/v1.2.4",
|
||||
Out: "kubectl/v1.2.4",
|
||||
},
|
||||
{
|
||||
In: `C:\Users\Kubernetes\kubectl.exe/v1.5.4`,
|
||||
Out: "kubectl.exe/v1.5.4",
|
||||
},
|
||||
{
|
||||
In: `C:\Program Files\kubectl.exe/v1.5.4`,
|
||||
Out: "kubectl.exe/v1.5.4",
|
||||
},
|
||||
{
|
||||
// This malicious input courtesy of enisoc.
|
||||
In: string(panicBuf) + "kubectl.exe",
|
||||
Out: "kubectl.exe",
|
||||
},
|
||||
} {
|
||||
if cleanUserAgent(tc.In) != tc.Out {
|
||||
t.Errorf("Failed to clean User-Agent: %s", tc.In)
|
||||
}
|
||||
}
|
||||
}
|
||||
36
vendor/k8s.io/apiserver/pkg/endpoints/openapi/BUILD
generated
vendored
Normal file
36
vendor/k8s.io/apiserver/pkg/endpoints/openapi/BUILD
generated
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["openapi_test.go"],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/go-openapi/spec:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["openapi.go"],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/emicklei/go-restful:go_default_library",
|
||||
"//vendor/github.com/go-openapi/spec:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/trie:go_default_library",
|
||||
],
|
||||
)
|
||||
2
vendor/k8s.io/apiserver/pkg/endpoints/openapi/OWNERS
generated
vendored
Normal file
2
vendor/k8s.io/apiserver/pkg/endpoints/openapi/OWNERS
generated
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
reviewers:
|
||||
- mbohlool
|
||||
171
vendor/k8s.io/apiserver/pkg/endpoints/openapi/openapi.go
generated
vendored
Normal file
171
vendor/k8s.io/apiserver/pkg/endpoints/openapi/openapi.go
generated
vendored
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
"github.com/go-openapi/spec"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/util/trie"
|
||||
"sort"
|
||||
)
|
||||
|
||||
var verbs = trie.New([]string{"get", "log", "read", "replace", "patch", "delete", "deletecollection", "watch", "connect", "proxy", "list", "create", "patch"})
|
||||
|
||||
const (
|
||||
extensionGVK = "x-kubernetes-group-version-kind"
|
||||
)
|
||||
|
||||
// ToValidOperationID makes an string a valid op ID (e.g. removing punctuations and whitespaces and make it camel case)
|
||||
func ToValidOperationID(s string, capitalizeFirstLetter bool) string {
|
||||
var buffer bytes.Buffer
|
||||
capitalize := capitalizeFirstLetter
|
||||
for i, r := range s {
|
||||
if unicode.IsLetter(r) || r == '_' || (i != 0 && unicode.IsDigit(r)) {
|
||||
if capitalize {
|
||||
buffer.WriteRune(unicode.ToUpper(r))
|
||||
capitalize = false
|
||||
} else {
|
||||
buffer.WriteRune(r)
|
||||
}
|
||||
} else {
|
||||
capitalize = true
|
||||
}
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// GetOperationIDAndTags returns a customize operation ID and a list of tags for kubernetes API server's OpenAPI spec to prevent duplicate IDs.
|
||||
func GetOperationIDAndTags(servePath string, r *restful.Route) (string, []string, error) {
|
||||
op := r.Operation
|
||||
path := r.Path
|
||||
var tags []string
|
||||
// TODO: This is hacky, figure out where this name conflict is created and fix it at the root.
|
||||
if strings.HasPrefix(path, "/apis/extensions/v1beta1/namespaces/{namespace}/") && strings.HasSuffix(op, "ScaleScale") {
|
||||
op = op[:len(op)-10] + strings.Title(strings.Split(path[48:], "/")[0]) + "Scale"
|
||||
}
|
||||
switch servePath {
|
||||
case "/swagger.json":
|
||||
prefix, exists := verbs.GetPrefix(op)
|
||||
if !exists {
|
||||
return op, tags, fmt.Errorf("operation names should start with a verb. Cannot determine operation verb from %v", op)
|
||||
}
|
||||
op = op[len(prefix):]
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
// Assume /api is /apis/core, remove this when we actually server /api/... on /apis/core/...
|
||||
if len(parts) >= 1 && parts[0] == "api" {
|
||||
parts = append([]string{"apis", "core"}, parts[1:]...)
|
||||
}
|
||||
if len(parts) >= 2 && parts[0] == "apis" {
|
||||
prefix = prefix + ToValidOperationID(strings.TrimSuffix(parts[1], ".k8s.io"), prefix != "")
|
||||
tag := ToValidOperationID(strings.TrimSuffix(parts[1], ".k8s.io"), false)
|
||||
if len(parts) > 2 {
|
||||
prefix = prefix + ToValidOperationID(parts[2], prefix != "")
|
||||
tag = tag + "_" + ToValidOperationID(parts[2], false)
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
} else if len(parts) >= 1 {
|
||||
tags = append(tags, ToValidOperationID(parts[0], false))
|
||||
}
|
||||
return prefix + ToValidOperationID(op, prefix != ""), tags, nil
|
||||
default:
|
||||
return op, tags, nil
|
||||
}
|
||||
}
|
||||
|
||||
type groupVersionKinds []v1.GroupVersionKind
|
||||
|
||||
func (s groupVersionKinds) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s groupVersionKinds) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
func (s groupVersionKinds) Less(i, j int) bool {
|
||||
if s[i].Group == s[j].Group {
|
||||
if s[i].Version == s[j].Version {
|
||||
return s[i].Kind < s[j].Kind
|
||||
}
|
||||
return s[i].Version < s[j].Version
|
||||
}
|
||||
return s[i].Group < s[j].Group
|
||||
}
|
||||
|
||||
// DefinitionNamer is the type to customize OpenAPI definition name.
|
||||
type DefinitionNamer struct {
|
||||
typeGroupVersionKinds map[string]groupVersionKinds
|
||||
}
|
||||
|
||||
func gvkConvert(gvk schema.GroupVersionKind) v1.GroupVersionKind {
|
||||
return v1.GroupVersionKind{
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: gvk.Kind,
|
||||
}
|
||||
}
|
||||
|
||||
func friendlyName(name string) string {
|
||||
nameParts := strings.Split(name, "/")
|
||||
// Reverse first part. e.g., io.k8s... instead of k8s.io...
|
||||
if len(nameParts) > 0 && strings.Contains(nameParts[0], ".") {
|
||||
parts := strings.Split(nameParts[0], ".")
|
||||
for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 {
|
||||
parts[i], parts[j] = parts[j], parts[i]
|
||||
}
|
||||
nameParts[0] = strings.Join(parts, ".")
|
||||
}
|
||||
return strings.Join(nameParts, ".")
|
||||
}
|
||||
|
||||
func typeName(t reflect.Type) string {
|
||||
return fmt.Sprintf("%s.%s", t.PkgPath(), t.Name())
|
||||
}
|
||||
|
||||
// NewDefinitionNamer constructs a new DefinitionNamer to be used to customize OpenAPI spec.
|
||||
func NewDefinitionNamer(s *runtime.Scheme) DefinitionNamer {
|
||||
ret := DefinitionNamer{
|
||||
typeGroupVersionKinds: map[string]groupVersionKinds{},
|
||||
}
|
||||
for gvk, rtype := range s.AllKnownTypes() {
|
||||
ret.typeGroupVersionKinds[typeName(rtype)] = append(ret.typeGroupVersionKinds[typeName(rtype)], gvkConvert(gvk))
|
||||
}
|
||||
for _, gvk := range ret.typeGroupVersionKinds {
|
||||
sort.Sort(gvk)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// GetDefinitionName returns the name and tags for a given definition
|
||||
func (d *DefinitionNamer) GetDefinitionName(servePath string, name string) (string, spec.Extensions) {
|
||||
if groupVersionKinds, ok := d.typeGroupVersionKinds[name]; ok {
|
||||
return friendlyName(name), spec.Extensions{
|
||||
extensionGVK: []v1.GroupVersionKind(groupVersionKinds),
|
||||
}
|
||||
}
|
||||
return friendlyName(name), nil
|
||||
}
|
||||
86
vendor/k8s.io/apiserver/pkg/endpoints/openapi/openapi_test.go
generated
vendored
Normal file
86
vendor/k8s.io/apiserver/pkg/endpoints/openapi/openapi_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type TestType struct {
|
||||
}
|
||||
|
||||
func (t TestType) GetObjectKind() schema.ObjectKind {
|
||||
return t
|
||||
}
|
||||
|
||||
func (t TestType) SetGroupVersionKind(kind schema.GroupVersionKind) {
|
||||
}
|
||||
|
||||
func (t TestType) GroupVersionKind() schema.GroupVersionKind {
|
||||
return schema.GroupVersionKind{
|
||||
Group: "test",
|
||||
Version: "v1",
|
||||
Kind: "TestType",
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, expected, actual interface{}) {
|
||||
var equal bool
|
||||
if expected == nil || actual == nil {
|
||||
equal = expected == actual
|
||||
} else {
|
||||
equal = reflect.DeepEqual(expected, actual)
|
||||
}
|
||||
if !equal {
|
||||
t.Errorf("%v != %v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefinitionName(t *testing.T) {
|
||||
testType := TestType{}
|
||||
typePkgName := "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/endpoints/openapi.TestType"
|
||||
typeFriendlyName := "io.k8s.kubernetes.vendor.k8s.io.apiserver.pkg.endpoints.openapi.TestType"
|
||||
if strings.HasSuffix(reflect.TypeOf(testType).PkgPath(), "go_default_test") {
|
||||
// the test is running inside bazel where the package name is changed and
|
||||
// "go_default_test" will add to package path.
|
||||
typePkgName = "k8s.io/apiserver/pkg/endpoints/openapi/go_default_test.TestType"
|
||||
typeFriendlyName = "io.k8s.apiserver.pkg.endpoints.openapi.go_default_test.TestType"
|
||||
}
|
||||
s := runtime.NewScheme()
|
||||
s.AddKnownTypeWithName(testType.GroupVersionKind(), &testType)
|
||||
namer := NewDefinitionNamer(s)
|
||||
n, e := namer.GetDefinitionName("", typePkgName)
|
||||
assertEqual(t, typeFriendlyName, n)
|
||||
assertEqual(t, e["x-kubernetes-group-version-kind"], []v1.GroupVersionKind{
|
||||
{
|
||||
Group: "test",
|
||||
Version: "v1",
|
||||
Kind: "TestType",
|
||||
},
|
||||
})
|
||||
n, e2 := namer.GetDefinitionName("", "test.com/another.Type")
|
||||
assertEqual(t, "com.test.another.Type", n)
|
||||
assertEqual(t, e2, spec.Extensions(nil))
|
||||
}
|
||||
570
vendor/k8s.io/apiserver/pkg/endpoints/proxy_test.go
generated
vendored
Normal file
570
vendor/k8s.io/apiserver/pkg/endpoints/proxy_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,570 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
)
|
||||
|
||||
func TestProxyRequestContentLengthAndTransferEncoding(t *testing.T) {
|
||||
chunk := func(data []byte) []byte {
|
||||
out := &bytes.Buffer{}
|
||||
chunker := httputil.NewChunkedWriter(out)
|
||||
for _, b := range data {
|
||||
if _, err := chunker.Write([]byte{b}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
chunker.Close()
|
||||
out.Write([]byte("\r\n"))
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
zip := func(data []byte) []byte {
|
||||
out := &bytes.Buffer{}
|
||||
zipper := gzip.NewWriter(out)
|
||||
if _, err := zipper.Write(data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
zipper.Close()
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
sampleData := []byte("abcde")
|
||||
|
||||
table := map[string]struct {
|
||||
reqHeaders http.Header
|
||||
reqBody []byte
|
||||
|
||||
expectedHeaders http.Header
|
||||
expectedBody []byte
|
||||
}{
|
||||
"content-length": {
|
||||
reqHeaders: http.Header{
|
||||
"Content-Length": []string{"5"},
|
||||
},
|
||||
reqBody: sampleData,
|
||||
|
||||
expectedHeaders: http.Header{
|
||||
"Content-Length": []string{"5"},
|
||||
"Content-Encoding": nil, // none set
|
||||
"Transfer-Encoding": nil, // none set
|
||||
},
|
||||
expectedBody: sampleData,
|
||||
},
|
||||
|
||||
"content-length + identity transfer-encoding": {
|
||||
reqHeaders: http.Header{
|
||||
"Content-Length": []string{"5"},
|
||||
"Transfer-Encoding": []string{"identity"},
|
||||
},
|
||||
reqBody: sampleData,
|
||||
|
||||
expectedHeaders: http.Header{
|
||||
"Content-Length": []string{"5"},
|
||||
"Content-Encoding": nil, // none set
|
||||
"Transfer-Encoding": nil, // gets removed
|
||||
},
|
||||
expectedBody: sampleData,
|
||||
},
|
||||
|
||||
"content-length + gzip content-encoding": {
|
||||
reqHeaders: http.Header{
|
||||
"Content-Length": []string{strconv.Itoa(len(zip(sampleData)))},
|
||||
"Content-Encoding": []string{"gzip"},
|
||||
},
|
||||
reqBody: zip(sampleData),
|
||||
|
||||
expectedHeaders: http.Header{
|
||||
"Content-Length": []string{strconv.Itoa(len(zip(sampleData)))},
|
||||
"Content-Encoding": []string{"gzip"},
|
||||
"Transfer-Encoding": nil, // none set
|
||||
},
|
||||
expectedBody: zip(sampleData),
|
||||
},
|
||||
|
||||
"chunked transfer-encoding": {
|
||||
reqHeaders: http.Header{
|
||||
"Transfer-Encoding": []string{"chunked"},
|
||||
},
|
||||
reqBody: chunk(sampleData),
|
||||
|
||||
expectedHeaders: http.Header{
|
||||
"Content-Length": nil, // none set
|
||||
"Content-Encoding": nil, // none set
|
||||
"Transfer-Encoding": nil, // Transfer-Encoding gets removed
|
||||
},
|
||||
expectedBody: sampleData, // sample data is unchunked
|
||||
},
|
||||
|
||||
"chunked transfer-encoding + gzip content-encoding": {
|
||||
reqHeaders: http.Header{
|
||||
"Content-Encoding": []string{"gzip"},
|
||||
"Transfer-Encoding": []string{"chunked"},
|
||||
},
|
||||
reqBody: chunk(zip(sampleData)),
|
||||
|
||||
expectedHeaders: http.Header{
|
||||
"Content-Length": nil, // none set
|
||||
"Content-Encoding": []string{"gzip"},
|
||||
"Transfer-Encoding": nil, // gets removed
|
||||
},
|
||||
expectedBody: zip(sampleData), // sample data is unchunked, but content-encoding is preserved
|
||||
},
|
||||
|
||||
// "Transfer-Encoding: gzip" is not supported by go
|
||||
// See http/transfer.go#fixTransferEncoding (https://golang.org/src/net/http/transfer.go#L427)
|
||||
// Once it is supported, this test case should succeed
|
||||
//
|
||||
// "gzip+chunked transfer-encoding": {
|
||||
// reqHeaders: http.Header{
|
||||
// "Transfer-Encoding": []string{"chunked,gzip"},
|
||||
// },
|
||||
// reqBody: chunk(zip(sampleData)),
|
||||
//
|
||||
// expectedHeaders: http.Header{
|
||||
// "Content-Length": nil, // no content-length headers
|
||||
// "Transfer-Encoding": nil, // Transfer-Encoding gets removed
|
||||
// },
|
||||
// expectedBody: sampleData,
|
||||
// },
|
||||
}
|
||||
|
||||
successfulResponse := "backend passed tests"
|
||||
for k, item := range table {
|
||||
// Start the downstream server
|
||||
downstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
// Verify headers
|
||||
for header, v := range item.expectedHeaders {
|
||||
if !reflect.DeepEqual(v, req.Header[header]) {
|
||||
t.Errorf("%s: Expected headers for %s to be %v, got %v", k, header, v, req.Header[header])
|
||||
}
|
||||
}
|
||||
|
||||
// Read body
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error %v", k, err)
|
||||
}
|
||||
req.Body.Close()
|
||||
|
||||
// Verify length
|
||||
if req.ContentLength > 0 && req.ContentLength != int64(len(body)) {
|
||||
t.Errorf("%s: ContentLength was %d, len(data) was %d", k, req.ContentLength, len(body))
|
||||
}
|
||||
|
||||
// Verify content
|
||||
if !bytes.Equal(item.expectedBody, body) {
|
||||
t.Errorf("%s: Expected %q, got %q", k, string(item.expectedBody), string(body))
|
||||
}
|
||||
|
||||
// Write successful response
|
||||
w.Write([]byte(successfulResponse))
|
||||
}))
|
||||
defer downstreamServer.Close()
|
||||
|
||||
// Start the proxy server
|
||||
serverURL, _ := url.Parse(downstreamServer.URL)
|
||||
simpleStorage := &SimpleRESTStorage{
|
||||
errors: map[string]error{},
|
||||
resourceLocation: serverURL,
|
||||
expectedResourceNamespace: "default",
|
||||
}
|
||||
namespaceHandler := handleNamespaced(map[string]rest.Storage{"foo": simpleStorage})
|
||||
server := newTestServer(namespaceHandler)
|
||||
defer server.Close()
|
||||
|
||||
// Dial the proxy server
|
||||
conn, err := net.Dial(server.Listener.Addr().Network(), server.Listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error %v", k, err)
|
||||
continue
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Add standard http 1.1 headers
|
||||
if item.reqHeaders == nil {
|
||||
item.reqHeaders = http.Header{}
|
||||
}
|
||||
item.reqHeaders.Add("Connection", "close")
|
||||
item.reqHeaders.Add("Host", server.Listener.Addr().String())
|
||||
|
||||
// We directly write to the connection to bypass the Go library's manipulation of the Request.Header.
|
||||
// Write the request headers
|
||||
post := fmt.Sprintf("POST /%s/%s/%s/proxy/namespaces/default/foo/id/some/dir HTTP/1.1\r\n", prefix, newGroupVersion.Group, newGroupVersion.Version)
|
||||
if _, err := fmt.Fprint(conn, post); err != nil {
|
||||
t.Fatalf("%s: unexpected error %v", k, err)
|
||||
}
|
||||
for header, values := range item.reqHeaders {
|
||||
for _, value := range values {
|
||||
if _, err := fmt.Fprintf(conn, "%s: %s\r\n", header, value); err != nil {
|
||||
t.Fatalf("%s: unexpected error %v", k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Header separator
|
||||
if _, err := fmt.Fprint(conn, "\r\n"); err != nil {
|
||||
t.Fatalf("%s: unexpected error %v", k, err)
|
||||
}
|
||||
// Body
|
||||
if _, err := conn.Write(item.reqBody); err != nil {
|
||||
t.Fatalf("%s: unexpected error %v", k, err)
|
||||
}
|
||||
|
||||
// Read response
|
||||
response, err := ioutil.ReadAll(conn)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error %v", k, err)
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(string(response), successfulResponse) {
|
||||
t.Errorf("%s: Did not get successful response: %s", k, string(response))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxy(t *testing.T) {
|
||||
table := []struct {
|
||||
method string
|
||||
path string
|
||||
reqBody string
|
||||
respBody string
|
||||
respContentType string
|
||||
reqNamespace string
|
||||
}{
|
||||
{"GET", "/some/dir", "", "answer", "text/css", "default"},
|
||||
{"GET", "/some/dir", "", "<html><head></head><body>answer</body></html>", "text/html", "default"},
|
||||
{"POST", "/some/other/dir", "question", "answer", "text/css", "default"},
|
||||
{"PUT", "/some/dir/id", "different question", "answer", "text/css", "default"},
|
||||
{"DELETE", "/some/dir/id", "", "ok", "text/css", "default"},
|
||||
{"GET", "/some/dir/id", "", "answer", "text/css", "other"},
|
||||
{"GET", "/trailing/slash/", "", "answer", "text/css", "default"},
|
||||
{"GET", "/", "", "answer", "text/css", "default"},
|
||||
}
|
||||
|
||||
for _, item := range table {
|
||||
downstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
gotBody, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Errorf("%v - unexpected error %v", item.method, err)
|
||||
}
|
||||
if e, a := item.reqBody, string(gotBody); e != a {
|
||||
t.Errorf("%v - expected %v, got %v", item.method, e, a)
|
||||
}
|
||||
if e, a := item.path, req.URL.Path; e != a {
|
||||
t.Errorf("%v - expected %v, got %v", item.method, e, a)
|
||||
}
|
||||
w.Header().Set("Content-Type", item.respContentType)
|
||||
var out io.Writer = w
|
||||
if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
||||
// The proxier can ask for gzip'd data; we need to provide it with that
|
||||
// in order to test our processing of that data.
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
gzw := gzip.NewWriter(w)
|
||||
out = gzw
|
||||
defer gzw.Close()
|
||||
}
|
||||
fmt.Fprint(out, item.respBody)
|
||||
}))
|
||||
defer downstreamServer.Close()
|
||||
|
||||
serverURL, _ := url.Parse(downstreamServer.URL)
|
||||
simpleStorage := &SimpleRESTStorage{
|
||||
errors: map[string]error{},
|
||||
resourceLocation: serverURL,
|
||||
expectedResourceNamespace: item.reqNamespace,
|
||||
}
|
||||
|
||||
namespaceHandler := handleNamespaced(map[string]rest.Storage{"foo": simpleStorage})
|
||||
namespaceServer := newTestServer(namespaceHandler)
|
||||
defer namespaceServer.Close()
|
||||
|
||||
// test each supported URL pattern for finding the redirection resource in the proxy in a particular namespace
|
||||
serverPatterns := []struct {
|
||||
server *httptest.Server
|
||||
proxyTestPattern string
|
||||
}{
|
||||
{namespaceServer, "/" + prefix + "/" + newGroupVersion.Group + "/" + newGroupVersion.Version + "/proxy/namespaces/" + item.reqNamespace + "/foo/id" + item.path},
|
||||
}
|
||||
|
||||
for _, serverPattern := range serverPatterns {
|
||||
server := serverPattern.server
|
||||
proxyTestPattern := serverPattern.proxyTestPattern
|
||||
req, err := http.NewRequest(
|
||||
item.method,
|
||||
server.URL+proxyTestPattern,
|
||||
strings.NewReader(item.reqBody),
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("%v - unexpected error %v", item.method, err)
|
||||
continue
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("%v - unexpected error %v", item.method, err)
|
||||
continue
|
||||
}
|
||||
gotResp, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Errorf("%v - unexpected error %v", item.method, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if e, a := item.respBody, string(gotResp); e != a {
|
||||
t.Errorf("%v - expected %v, got %v. url: %#v", item.method, e, a, req.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyUpgrade(t *testing.T) {
|
||||
|
||||
localhostPool := x509.NewCertPool()
|
||||
if !localhostPool.AppendCertsFromPEM(localhostCert) {
|
||||
t.Errorf("error setting up localhostCert pool")
|
||||
}
|
||||
|
||||
testcases := map[string]struct {
|
||||
ServerFunc func(http.Handler) *httptest.Server
|
||||
ProxyTransport http.RoundTripper
|
||||
}{
|
||||
"http": {
|
||||
ServerFunc: httptest.NewServer,
|
||||
ProxyTransport: nil,
|
||||
},
|
||||
"https (invalid hostname + InsecureSkipVerify)": {
|
||||
ServerFunc: func(h http.Handler) *httptest.Server {
|
||||
cert, err := tls.X509KeyPair(exampleCert, exampleKey)
|
||||
if err != nil {
|
||||
t.Errorf("https (invalid hostname): proxy_test: %v", err)
|
||||
}
|
||||
ts := httptest.NewUnstartedServer(h)
|
||||
ts.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
ts.StartTLS()
|
||||
return ts
|
||||
},
|
||||
ProxyTransport: utilnet.SetTransportDefaults(&http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}),
|
||||
},
|
||||
"https (valid hostname + RootCAs)": {
|
||||
ServerFunc: func(h http.Handler) *httptest.Server {
|
||||
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||
if err != nil {
|
||||
t.Errorf("https (valid hostname): proxy_test: %v", err)
|
||||
}
|
||||
ts := httptest.NewUnstartedServer(h)
|
||||
ts.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
ts.StartTLS()
|
||||
return ts
|
||||
},
|
||||
ProxyTransport: utilnet.SetTransportDefaults(&http.Transport{TLSClientConfig: &tls.Config{RootCAs: localhostPool}}),
|
||||
},
|
||||
"https (valid hostname + RootCAs + custom dialer)": {
|
||||
ServerFunc: func(h http.Handler) *httptest.Server {
|
||||
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||
if err != nil {
|
||||
t.Errorf("https (valid hostname): proxy_test: %v", err)
|
||||
}
|
||||
ts := httptest.NewUnstartedServer(h)
|
||||
ts.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
ts.StartTLS()
|
||||
return ts
|
||||
},
|
||||
ProxyTransport: utilnet.SetTransportDefaults(&http.Transport{Dial: net.Dial, TLSClientConfig: &tls.Config{RootCAs: localhostPool}}),
|
||||
},
|
||||
}
|
||||
|
||||
for k, tc := range testcases {
|
||||
|
||||
backendServer := tc.ServerFunc(websocket.Handler(func(ws *websocket.Conn) {
|
||||
defer ws.Close()
|
||||
body := make([]byte, 5)
|
||||
ws.Read(body)
|
||||
ws.Write([]byte("hello " + string(body)))
|
||||
}))
|
||||
defer backendServer.Close()
|
||||
|
||||
serverURL, _ := url.Parse(backendServer.URL)
|
||||
simpleStorage := &SimpleRESTStorage{
|
||||
errors: map[string]error{},
|
||||
resourceLocation: serverURL,
|
||||
resourceLocationTransport: tc.ProxyTransport,
|
||||
expectedResourceNamespace: "myns",
|
||||
}
|
||||
|
||||
namespaceHandler := handleNamespaced(map[string]rest.Storage{"foo": simpleStorage})
|
||||
|
||||
server := newTestServer(namespaceHandler)
|
||||
defer server.Close()
|
||||
|
||||
ws, err := websocket.Dial("ws://"+server.Listener.Addr().String()+"/"+prefix+"/"+newGroupVersion.Group+"/"+newGroupVersion.Version+"/proxy/namespaces/myns/foo/123", "", "http://127.0.0.1/")
|
||||
if err != nil {
|
||||
t.Errorf("%s: websocket dial err: %s", k, err)
|
||||
continue
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
if _, err := ws.Write([]byte("world")); err != nil {
|
||||
t.Errorf("%s: write err: %s", k, err)
|
||||
continue
|
||||
}
|
||||
|
||||
response := make([]byte, 20)
|
||||
n, err := ws.Read(response)
|
||||
if err != nil {
|
||||
t.Errorf("%s: read err: %s", k, err)
|
||||
continue
|
||||
}
|
||||
if e, a := "hello world", string(response[0:n]); e != a {
|
||||
t.Errorf("%s: expected '%#v', got '%#v'", k, e, a)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectOnMissingTrailingSlash(t *testing.T) {
|
||||
table := []struct {
|
||||
// The requested path
|
||||
path string
|
||||
// The path requested on the proxy server.
|
||||
proxyServerPath string
|
||||
// query string
|
||||
query string
|
||||
}{
|
||||
{"/trailing/slash/", "/trailing/slash/", ""},
|
||||
{"/", "/", "test1=value1&test2=value2"},
|
||||
// "/" should be added at the end.
|
||||
{"", "/", "test1=value1&test2=value2"},
|
||||
// "/" should not be added at a non-root path.
|
||||
{"/some/path", "/some/path", ""},
|
||||
}
|
||||
|
||||
for _, item := range table {
|
||||
downstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.Path != item.proxyServerPath {
|
||||
t.Errorf("Unexpected request on path: %s, expected path: %s, item: %v", req.URL.Path, item.proxyServerPath, item)
|
||||
}
|
||||
if req.URL.RawQuery != item.query {
|
||||
t.Errorf("Unexpected query on url: %s, expected: %s", req.URL.RawQuery, item.query)
|
||||
}
|
||||
}))
|
||||
defer downstreamServer.Close()
|
||||
|
||||
serverURL, _ := url.Parse(downstreamServer.URL)
|
||||
simpleStorage := &SimpleRESTStorage{
|
||||
errors: map[string]error{},
|
||||
resourceLocation: serverURL,
|
||||
expectedResourceNamespace: "ns",
|
||||
}
|
||||
|
||||
handler := handleNamespaced(map[string]rest.Storage{"foo": simpleStorage})
|
||||
server := newTestServer(handler)
|
||||
defer server.Close()
|
||||
|
||||
proxyTestPattern := "/" + prefix + "/" + newGroupVersion.Group + "/" + newGroupVersion.Version + "/proxy/namespaces/ns/foo/id" + item.path
|
||||
req, err := http.NewRequest(
|
||||
"GET",
|
||||
server.URL+proxyTestPattern+"?"+item.query,
|
||||
strings.NewReader(""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
continue
|
||||
}
|
||||
// Note: We are using a default client here, that follows redirects.
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Unexpected errorCode: %v, expected: 200. Response: %v, item: %v", resp.StatusCode, resp, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// exampleCert was generated from crypto/tls/generate_cert.go with the following command:
|
||||
// go run generate_cert.go --rsa-bits 512 --host example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
|
||||
var exampleCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIBdzCCASGgAwIBAgIRAOVTAdPnfbS5V85mfS90TfIwDQYJKoZIhvcNAQELBQAw
|
||||
EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
|
||||
MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgC
|
||||
QQCoVSqeu8TBvF+70T7Jm4340YQNhds6IxjRoifenYodAO1dnKGrcbF266DJGunh
|
||||
nIjQH7B12tduhl0fLK4Ezf7/AgMBAAGjUDBOMA4GA1UdDwEB/wQEAwICpDATBgNV
|
||||
HSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MBYGA1UdEQQPMA2CC2V4
|
||||
YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EAk1kVa5uZ/AzwYDVcS9bpM/czwjjV
|
||||
xq3VeSCfmNa2uNjbFvodmCRwZOHUvipAMGCUCV6j5vMrJ8eMj8tCQ36W9A==
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
var exampleKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIBOgIBAAJBAKhVKp67xMG8X7vRPsmbjfjRhA2F2zojGNGiJ96dih0A7V2coatx
|
||||
sXbroMka6eGciNAfsHXa126GXR8srgTN/v8CAwEAAQJASdzdD7vKsUwMIejGCUb1
|
||||
fAnLTPfAY3lFCa+CmR89nE22dAoRDv+5RbnBsZ58BazPNJHrsVPRlfXB3OQmSQr0
|
||||
SQIhANoJhs+xOJE/i8nJv0uAbzKyiD1YkvRkta0GpUOULyAVAiEAxaQus3E/SuqD
|
||||
P7y5NeJnE7X6XkyC35zrsJRkz7orE8MCIHdDjsI8pjyNDeGqwUCDWE/a6DrmIDwe
|
||||
emHSqMN2YvChAiEAnxLCM9NWaenOsaIoP+J1rDuvw+4499nJKVqGuVrSCRkCIEqK
|
||||
4KSchPMc3x8M/uhw9oWTtKFmjA/PPh0FsWCdKrEy
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
// localhostCert was generated from crypto/tls/generate_cert.go with the following command:
|
||||
// go run generate_cert.go --rsa-bits 512 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
|
||||
var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIBjzCCATmgAwIBAgIRAKpi2WmTcFrVjxrl5n5YDUEwDQYJKoZIhvcNAQELBQAw
|
||||
EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
|
||||
MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgC
|
||||
QQC9fEbRszP3t14Gr4oahV7zFObBI4TfA5i7YnlMXeLinb7MnvT4bkfOJzE6zktn
|
||||
59zP7UiHs3l4YOuqrjiwM413AgMBAAGjaDBmMA4GA1UdDwEB/wQEAwICpDATBgNV
|
||||
HSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MC4GA1UdEQQnMCWCC2V4
|
||||
YW1wbGUuY29thwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUA
|
||||
A0EAUsVE6KMnza/ZbodLlyeMzdo7EM/5nb5ywyOxgIOCf0OOLHsPS9ueGLQX9HEG
|
||||
//yjTXuhNcUugExIjM/AIwAZPQ==
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
// localhostKey is the private key for localhostCert.
|
||||
var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIBOwIBAAJBAL18RtGzM/e3XgavihqFXvMU5sEjhN8DmLtieUxd4uKdvsye9Phu
|
||||
R84nMTrOS2fn3M/tSIezeXhg66quOLAzjXcCAwEAAQJBAKcRxH9wuglYLBdI/0OT
|
||||
BLzfWPZCEw1vZmMR2FF1Fm8nkNOVDPleeVGTWoOEcYYlQbpTmkGSxJ6ya+hqRi6x
|
||||
goECIQDx3+X49fwpL6B5qpJIJMyZBSCuMhH4B7JevhGGFENi3wIhAMiNJN5Q3UkL
|
||||
IuSvv03kaPR5XVQ99/UeEetUgGvBcABpAiBJSBzVITIVCGkGc7d+RCf49KTCIklv
|
||||
bGWObufAR8Ni4QIgWpILjW8dkGg8GOUZ0zaNA6Nvt6TIv2UWGJ4v5PoV98kCIQDx
|
||||
rIiZs5QbKdycsv9gQJzwQAogC8o04X3Zz3dsoX+h4A==
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
47
vendor/k8s.io/apiserver/pkg/endpoints/request/BUILD
generated
vendored
Normal file
47
vendor/k8s.io/apiserver/pkg/endpoints/request/BUILD
generated
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["requestinfo_test.go"],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = ["//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library"],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"context.go",
|
||||
"doc.go",
|
||||
"requestcontext.go",
|
||||
"requestinfo.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/golang.org/x/net/context:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_xtest",
|
||||
srcs = ["context_test.go"],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
],
|
||||
)
|
||||
2
vendor/k8s.io/apiserver/pkg/endpoints/request/OWNERS
generated
vendored
Normal file
2
vendor/k8s.io/apiserver/pkg/endpoints/request/OWNERS
generated
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
reviewers:
|
||||
- sttts
|
||||
145
vendor/k8s.io/apiserver/pkg/endpoints/request/context.go
generated
vendored
Normal file
145
vendor/k8s.io/apiserver/pkg/endpoints/request/context.go
generated
vendored
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
stderrs "errors"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
|
||||
// Context carries values across API boundaries.
|
||||
// This context matches the context.Context interface
|
||||
// (https://blog.golang.org/context), for the purposes
|
||||
// of passing the api.Context through to the storage tier.
|
||||
// TODO: Determine the extent that this abstraction+interface
|
||||
// is used by the api, and whether we can remove.
|
||||
type Context interface {
|
||||
// Value returns the value associated with key or nil if none.
|
||||
Value(key interface{}) interface{}
|
||||
|
||||
// Deadline returns the time when this Context will be canceled, if any.
|
||||
Deadline() (deadline time.Time, ok bool)
|
||||
|
||||
// Done returns a channel that is closed when this Context is canceled
|
||||
// or times out.
|
||||
Done() <-chan struct{}
|
||||
|
||||
// Err indicates why this context was canceled, after the Done channel
|
||||
// is closed.
|
||||
Err() error
|
||||
}
|
||||
|
||||
// The key type is unexported to prevent collisions
|
||||
type key int
|
||||
|
||||
const (
|
||||
// namespaceKey is the context key for the request namespace.
|
||||
namespaceKey key = iota
|
||||
|
||||
// userKey is the context key for the request user.
|
||||
userKey
|
||||
|
||||
// uidKey is the context key for the uid to assign to an object on create.
|
||||
uidKey
|
||||
|
||||
// userAgentKey is the context key for the request user agent.
|
||||
userAgentKey
|
||||
|
||||
namespaceDefault = "default" // TODO(sttts): solve import cycle when using metav1.NamespaceDefault
|
||||
)
|
||||
|
||||
// NewContext instantiates a base context object for request flows.
|
||||
func NewContext() Context {
|
||||
return context.TODO()
|
||||
}
|
||||
|
||||
// NewDefaultContext instantiates a base context object for request flows in the default namespace
|
||||
func NewDefaultContext() Context {
|
||||
return WithNamespace(NewContext(), namespaceDefault)
|
||||
}
|
||||
|
||||
// WithValue returns a copy of parent in which the value associated with key is val.
|
||||
func WithValue(parent Context, key interface{}, val interface{}) Context {
|
||||
internalCtx, ok := parent.(context.Context)
|
||||
if !ok {
|
||||
panic(stderrs.New("Invalid context type"))
|
||||
}
|
||||
return context.WithValue(internalCtx, key, val)
|
||||
}
|
||||
|
||||
// WithNamespace returns a copy of parent in which the namespace value is set
|
||||
func WithNamespace(parent Context, namespace string) Context {
|
||||
return WithValue(parent, namespaceKey, namespace)
|
||||
}
|
||||
|
||||
// NamespaceFrom returns the value of the namespace key on the ctx
|
||||
func NamespaceFrom(ctx Context) (string, bool) {
|
||||
namespace, ok := ctx.Value(namespaceKey).(string)
|
||||
return namespace, ok
|
||||
}
|
||||
|
||||
// NamespaceValue returns the value of the namespace key on the ctx, or the empty string if none
|
||||
func NamespaceValue(ctx Context) string {
|
||||
namespace, _ := NamespaceFrom(ctx)
|
||||
return namespace
|
||||
}
|
||||
|
||||
// WithNamespaceDefaultIfNone returns a context whose namespace is the default if and only if the parent context has no namespace value
|
||||
func WithNamespaceDefaultIfNone(parent Context) Context {
|
||||
namespace, ok := NamespaceFrom(parent)
|
||||
if !ok || len(namespace) == 0 {
|
||||
return WithNamespace(parent, namespaceDefault)
|
||||
}
|
||||
return parent
|
||||
}
|
||||
|
||||
// WithUser returns a copy of parent in which the user value is set
|
||||
func WithUser(parent Context, user user.Info) Context {
|
||||
return WithValue(parent, userKey, user)
|
||||
}
|
||||
|
||||
// UserFrom returns the value of the user key on the ctx
|
||||
func UserFrom(ctx Context) (user.Info, bool) {
|
||||
user, ok := ctx.Value(userKey).(user.Info)
|
||||
return user, ok
|
||||
}
|
||||
|
||||
// WithUID returns a copy of parent in which the uid value is set
|
||||
func WithUID(parent Context, uid types.UID) Context {
|
||||
return WithValue(parent, uidKey, uid)
|
||||
}
|
||||
|
||||
// UIDFrom returns the value of the uid key on the ctx
|
||||
func UIDFrom(ctx Context) (types.UID, bool) {
|
||||
uid, ok := ctx.Value(uidKey).(types.UID)
|
||||
return uid, ok
|
||||
}
|
||||
|
||||
// WithUserAgent returns a copy of parent in which the user value is set
|
||||
func WithUserAgent(parent Context, userAgent string) Context {
|
||||
return WithValue(parent, userAgentKey, userAgent)
|
||||
}
|
||||
|
||||
// UserAgentFrom returns the value of the userAgent key on the ctx
|
||||
func UserAgentFrom(ctx Context) (string, bool) {
|
||||
userAgent, ok := ctx.Value(userAgentKey).(string)
|
||||
return userAgent, ok
|
||||
}
|
||||
134
vendor/k8s.io/apiserver/pkg/endpoints/request/context_test.go
generated
vendored
Normal file
134
vendor/k8s.io/apiserver/pkg/endpoints/request/context_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package request_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
// TestNamespaceContext validates that a namespace can be get/set on a context object
|
||||
func TestNamespaceContext(t *testing.T) {
|
||||
ctx := genericapirequest.NewDefaultContext()
|
||||
result, ok := genericapirequest.NamespaceFrom(ctx)
|
||||
if !ok {
|
||||
t.Fatalf("Error getting namespace")
|
||||
}
|
||||
if metav1.NamespaceDefault != result {
|
||||
t.Fatalf("Expected: %s, Actual: %s", metav1.NamespaceDefault, result)
|
||||
}
|
||||
|
||||
ctx = genericapirequest.NewContext()
|
||||
result, ok = genericapirequest.NamespaceFrom(ctx)
|
||||
if ok {
|
||||
t.Fatalf("Should not be ok because there is no namespace on the context")
|
||||
}
|
||||
}
|
||||
|
||||
//TestUserContext validates that a userinfo can be get/set on a context object
|
||||
func TestUserContext(t *testing.T) {
|
||||
ctx := genericapirequest.NewContext()
|
||||
_, ok := genericapirequest.UserFrom(ctx)
|
||||
if ok {
|
||||
t.Fatalf("Should not be ok because there is no user.Info on the context")
|
||||
}
|
||||
ctx = genericapirequest.WithUser(
|
||||
ctx,
|
||||
&user.DefaultInfo{
|
||||
Name: "bob",
|
||||
UID: "123",
|
||||
Groups: []string{"group1"},
|
||||
Extra: map[string][]string{"foo": {"bar"}},
|
||||
},
|
||||
)
|
||||
|
||||
result, ok := genericapirequest.UserFrom(ctx)
|
||||
if !ok {
|
||||
t.Fatalf("Error getting user info")
|
||||
}
|
||||
|
||||
expectedName := "bob"
|
||||
if result.GetName() != expectedName {
|
||||
t.Fatalf("Get user name error, Expected: %s, Actual: %s", expectedName, result.GetName())
|
||||
}
|
||||
|
||||
expectedUID := "123"
|
||||
if result.GetUID() != expectedUID {
|
||||
t.Fatalf("Get UID error, Expected: %s, Actual: %s", expectedUID, result.GetName())
|
||||
}
|
||||
|
||||
expectedGroup := "group1"
|
||||
actualGroup := result.GetGroups()
|
||||
if len(actualGroup) != 1 {
|
||||
t.Fatalf("Get user group number error, Expected: 1, Actual: %d", len(actualGroup))
|
||||
} else if actualGroup[0] != expectedGroup {
|
||||
t.Fatalf("Get user group error, Expected: %s, Actual: %s", expectedGroup, actualGroup[0])
|
||||
}
|
||||
|
||||
expectedExtraKey := "foo"
|
||||
expectedExtraValue := "bar"
|
||||
actualExtra := result.GetExtra()
|
||||
if len(actualExtra[expectedExtraKey]) != 1 {
|
||||
t.Fatalf("Get user extra map number error, Expected: 1, Actual: %d", len(actualExtra[expectedExtraKey]))
|
||||
} else if actualExtra[expectedExtraKey][0] != expectedExtraValue {
|
||||
t.Fatalf("Get user extra map value error, Expected: %s, Actual: %s", expectedExtraValue, actualExtra[expectedExtraKey])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//TestUIDContext validates that a UID can be get/set on a context object
|
||||
func TestUIDContext(t *testing.T) {
|
||||
ctx := genericapirequest.NewContext()
|
||||
_, ok := genericapirequest.UIDFrom(ctx)
|
||||
if ok {
|
||||
t.Fatalf("Should not be ok because there is no UID on the context")
|
||||
}
|
||||
ctx = genericapirequest.WithUID(
|
||||
ctx,
|
||||
types.UID("testUID"),
|
||||
)
|
||||
_, ok = genericapirequest.UIDFrom(ctx)
|
||||
if !ok {
|
||||
t.Fatalf("Error getting UID")
|
||||
}
|
||||
}
|
||||
|
||||
//TestUserAgentContext validates that a useragent can be get/set on a context object
|
||||
func TestUserAgentContext(t *testing.T) {
|
||||
ctx := genericapirequest.NewContext()
|
||||
_, ok := genericapirequest.UserAgentFrom(ctx)
|
||||
if ok {
|
||||
t.Fatalf("Should not be ok because there is no UserAgent on the context")
|
||||
}
|
||||
|
||||
ctx = genericapirequest.WithUserAgent(
|
||||
ctx,
|
||||
"TestUserAgent",
|
||||
)
|
||||
result, ok := genericapirequest.UserAgentFrom(ctx)
|
||||
if !ok {
|
||||
t.Fatalf("Error getting UserAgent")
|
||||
}
|
||||
expectedResult := "TestUserAgent"
|
||||
if result != expectedResult {
|
||||
t.Fatalf("Get user agent error, Expected: %s, Actual: %s", expectedResult, result)
|
||||
}
|
||||
}
|
||||
20
vendor/k8s.io/apiserver/pkg/endpoints/request/doc.go
generated
vendored
Normal file
20
vendor/k8s.io/apiserver/pkg/endpoints/request/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package request contains everything around extracting info from
|
||||
// a http request object.
|
||||
// TODO: this package is temporary. Handlers must move into pkg/apiserver/handlers to avoid dependency cycle
|
||||
package request // import "k8s.io/apiserver/pkg/endpoints/request"
|
||||
117
vendor/k8s.io/apiserver/pkg/endpoints/request/requestcontext.go
generated
vendored
Normal file
117
vendor/k8s.io/apiserver/pkg/endpoints/request/requestcontext.go
generated
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// RequestContextMapper keeps track of the context associated with a particular request
|
||||
type RequestContextMapper interface {
|
||||
// Get returns the context associated with the given request (if any), and true if the request has an associated context, and false if it does not.
|
||||
Get(req *http.Request) (Context, bool)
|
||||
// Update maps the request to the given context. If no context was previously associated with the request, an error is returned.
|
||||
// Update should only be called with a descendant context of the previously associated context.
|
||||
// Updating to an unrelated context may return an error in the future.
|
||||
// The context associated with a request should only be updated by a limited set of callers.
|
||||
// Valid examples include the authentication layer, or an audit/tracing layer.
|
||||
Update(req *http.Request, context Context) error
|
||||
}
|
||||
|
||||
type requestContextMap struct {
|
||||
contexts map[*http.Request]Context
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// NewRequestContextMapper returns a new RequestContextMapper.
|
||||
// The returned mapper must be added as a request filter using NewRequestContextFilter.
|
||||
func NewRequestContextMapper() RequestContextMapper {
|
||||
return &requestContextMap{
|
||||
contexts: make(map[*http.Request]Context),
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns the context associated with the given request (if any), and true if the request has an associated context, and false if it does not.
|
||||
// Get will only return a valid context when called from inside the filter chain set up by NewRequestContextFilter()
|
||||
func (c *requestContextMap) Get(req *http.Request) (Context, bool) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
context, ok := c.contexts[req]
|
||||
return context, ok
|
||||
}
|
||||
|
||||
// Update maps the request to the given context.
|
||||
// If no context was previously associated with the request, an error is returned and the context is ignored.
|
||||
func (c *requestContextMap) Update(req *http.Request, context Context) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if _, ok := c.contexts[req]; !ok {
|
||||
return errors.New("No context associated")
|
||||
}
|
||||
// TODO: ensure the new context is a descendant of the existing one
|
||||
c.contexts[req] = context
|
||||
return nil
|
||||
}
|
||||
|
||||
// init maps the request to the given context and returns true if there was no context associated with the request already.
|
||||
// if a context was already associated with the request, it ignores the given context and returns false.
|
||||
// init is intentionally unexported to ensure that all init calls are paired with a remove after a request is handled
|
||||
func (c *requestContextMap) init(req *http.Request, context Context) bool {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if _, exists := c.contexts[req]; exists {
|
||||
return false
|
||||
}
|
||||
c.contexts[req] = context
|
||||
return true
|
||||
}
|
||||
|
||||
// remove is intentionally unexported to ensure that the context is not removed until a request is handled
|
||||
func (c *requestContextMap) remove(req *http.Request) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
delete(c.contexts, req)
|
||||
}
|
||||
|
||||
// WithRequestContext ensures there is a Context object associated with the request before calling the passed handler.
|
||||
// After the passed handler runs, the context is cleaned up.
|
||||
func WithRequestContext(handler http.Handler, mapper RequestContextMapper) http.Handler {
|
||||
rcMap, ok := mapper.(*requestContextMap)
|
||||
if !ok {
|
||||
glog.Fatal("Unknown RequestContextMapper implementation.")
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if rcMap.init(req, NewContext()) {
|
||||
// If we were the ones to successfully initialize, pair with a remove
|
||||
defer rcMap.remove(req)
|
||||
}
|
||||
handler.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
// IsEmpty returns true if there are no contexts registered, or an error if it could not be determined. Intended for use by tests.
|
||||
func IsEmpty(requestsToContexts RequestContextMapper) (bool, error) {
|
||||
if requestsToContexts, ok := requestsToContexts.(*requestContextMap); ok {
|
||||
return len(requestsToContexts.contexts) == 0, nil
|
||||
}
|
||||
return true, errors.New("Unknown RequestContextMapper implementation")
|
||||
}
|
||||
241
vendor/k8s.io/apiserver/pkg/endpoints/request/requestinfo.go
generated
vendored
Normal file
241
vendor/k8s.io/apiserver/pkg/endpoints/request/requestinfo.go
generated
vendored
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
// RequestInfo holds information parsed from the http.Request
|
||||
type RequestInfo struct {
|
||||
// IsResourceRequest indicates whether or not the request is for an API resource or subresource
|
||||
IsResourceRequest bool
|
||||
// Path is the URL path of the request
|
||||
Path string
|
||||
// Verb is the kube verb associated with the request for API requests, not the http verb. This includes things like list and watch.
|
||||
// for non-resource requests, this is the lowercase http verb
|
||||
Verb string
|
||||
|
||||
APIPrefix string
|
||||
APIGroup string
|
||||
APIVersion string
|
||||
Namespace string
|
||||
// Resource is the name of the resource being requested. This is not the kind. For example: pods
|
||||
Resource string
|
||||
// Subresource is the name of the subresource being requested. This is a different resource, scoped to the parent resource, but it may have a different kind.
|
||||
// For instance, /pods has the resource "pods" and the kind "Pod", while /pods/foo/status has the resource "pods", the sub resource "status", and the kind "Pod"
|
||||
// (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource "pods", subresource "binding", and kind "Binding".
|
||||
Subresource string
|
||||
// Name is empty for some verbs, but if the request directly indicates a name (not in body content) then this field is filled in.
|
||||
Name string
|
||||
// Parts are the path parts for the request, always starting with /{resource}/{name}
|
||||
Parts []string
|
||||
}
|
||||
|
||||
// specialVerbs contains just strings which are used in REST paths for special actions that don't fall under the normal
|
||||
// CRUDdy GET/POST/PUT/DELETE actions on REST objects.
|
||||
// TODO: find a way to keep this up to date automatically. Maybe dynamically populate list as handlers added to
|
||||
// master's Mux.
|
||||
var specialVerbs = sets.NewString("proxy", "redirect", "watch")
|
||||
|
||||
// specialVerbsNoSubresources contains root verbs which do not allow subresources
|
||||
var specialVerbsNoSubresources = sets.NewString("proxy", "redirect")
|
||||
|
||||
// namespaceSubresources contains subresources of namespace
|
||||
// this list allows the parser to distinguish between a namespace subresource, and a namespaced resource
|
||||
var namespaceSubresources = sets.NewString("status", "finalize")
|
||||
|
||||
// NamespaceSubResourcesForTest exports namespaceSubresources for testing in pkg/master/master_test.go, so we never drift
|
||||
var NamespaceSubResourcesForTest = sets.NewString(namespaceSubresources.List()...)
|
||||
|
||||
type RequestInfoFactory struct {
|
||||
APIPrefixes sets.String // without leading and trailing slashes
|
||||
GrouplessAPIPrefixes sets.String // without leading and trailing slashes
|
||||
}
|
||||
|
||||
// TODO write an integration test against the swagger doc to test the RequestInfo and match up behavior to responses
|
||||
// NewRequestInfo returns the information from the http request. If error is not nil, RequestInfo holds the information as best it is known before the failure
|
||||
// It handles both resource and non-resource requests and fills in all the pertinent information for each.
|
||||
// Valid Inputs:
|
||||
// Resource paths
|
||||
// /apis/{api-group}/{version}/namespaces
|
||||
// /api/{version}/namespaces
|
||||
// /api/{version}/namespaces/{namespace}
|
||||
// /api/{version}/namespaces/{namespace}/{resource}
|
||||
// /api/{version}/namespaces/{namespace}/{resource}/{resourceName}
|
||||
// /api/{version}/{resource}
|
||||
// /api/{version}/{resource}/{resourceName}
|
||||
//
|
||||
// Special verbs without subresources:
|
||||
// /api/{version}/proxy/{resource}/{resourceName}
|
||||
// /api/{version}/proxy/namespaces/{namespace}/{resource}/{resourceName}
|
||||
// /api/{version}/redirect/namespaces/{namespace}/{resource}/{resourceName}
|
||||
// /api/{version}/redirect/{resource}/{resourceName}
|
||||
//
|
||||
// Special verbs with subresources:
|
||||
// /api/{version}/watch/{resource}
|
||||
// /api/{version}/watch/namespaces/{namespace}/{resource}
|
||||
//
|
||||
// NonResource paths
|
||||
// /apis/{api-group}/{version}
|
||||
// /apis/{api-group}
|
||||
// /apis
|
||||
// /api/{version}
|
||||
// /api
|
||||
// /healthz
|
||||
// /
|
||||
func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, error) {
|
||||
// start with a non-resource request until proven otherwise
|
||||
requestInfo := RequestInfo{
|
||||
IsResourceRequest: false,
|
||||
Path: req.URL.Path,
|
||||
Verb: strings.ToLower(req.Method),
|
||||
}
|
||||
|
||||
currentParts := splitPath(req.URL.Path)
|
||||
if len(currentParts) < 3 {
|
||||
// return a non-resource request
|
||||
return &requestInfo, nil
|
||||
}
|
||||
|
||||
if !r.APIPrefixes.Has(currentParts[0]) {
|
||||
// return a non-resource request
|
||||
return &requestInfo, nil
|
||||
}
|
||||
requestInfo.APIPrefix = currentParts[0]
|
||||
currentParts = currentParts[1:]
|
||||
|
||||
if !r.GrouplessAPIPrefixes.Has(requestInfo.APIPrefix) {
|
||||
// one part (APIPrefix) has already been consumed, so this is actually "do we have four parts?"
|
||||
if len(currentParts) < 3 {
|
||||
// return a non-resource request
|
||||
return &requestInfo, nil
|
||||
}
|
||||
|
||||
requestInfo.APIGroup = currentParts[0]
|
||||
currentParts = currentParts[1:]
|
||||
}
|
||||
|
||||
requestInfo.IsResourceRequest = true
|
||||
requestInfo.APIVersion = currentParts[0]
|
||||
currentParts = currentParts[1:]
|
||||
|
||||
// handle input of form /{specialVerb}/*
|
||||
if specialVerbs.Has(currentParts[0]) {
|
||||
if len(currentParts) < 2 {
|
||||
return &requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL)
|
||||
}
|
||||
|
||||
requestInfo.Verb = currentParts[0]
|
||||
currentParts = currentParts[1:]
|
||||
|
||||
} else {
|
||||
switch req.Method {
|
||||
case "POST":
|
||||
requestInfo.Verb = "create"
|
||||
case "GET", "HEAD":
|
||||
requestInfo.Verb = "get"
|
||||
case "PUT":
|
||||
requestInfo.Verb = "update"
|
||||
case "PATCH":
|
||||
requestInfo.Verb = "patch"
|
||||
case "DELETE":
|
||||
requestInfo.Verb = "delete"
|
||||
default:
|
||||
requestInfo.Verb = ""
|
||||
}
|
||||
}
|
||||
|
||||
// URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative to kind
|
||||
if currentParts[0] == "namespaces" {
|
||||
if len(currentParts) > 1 {
|
||||
requestInfo.Namespace = currentParts[1]
|
||||
|
||||
// if there is another step after the namespace name and it is not a known namespace subresource
|
||||
// move currentParts to include it as a resource in its own right
|
||||
if len(currentParts) > 2 && !namespaceSubresources.Has(currentParts[2]) {
|
||||
currentParts = currentParts[2:]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
requestInfo.Namespace = "" // TODO(sttts): solve import cycle when using metav1.NamespaceNone
|
||||
}
|
||||
|
||||
// parsing successful, so we now know the proper value for .Parts
|
||||
requestInfo.Parts = currentParts
|
||||
|
||||
// parts look like: resource/resourceName/subresource/other/stuff/we/don't/interpret
|
||||
switch {
|
||||
case len(requestInfo.Parts) >= 3 && !specialVerbsNoSubresources.Has(requestInfo.Verb):
|
||||
requestInfo.Subresource = requestInfo.Parts[2]
|
||||
fallthrough
|
||||
case len(requestInfo.Parts) >= 2:
|
||||
requestInfo.Name = requestInfo.Parts[1]
|
||||
fallthrough
|
||||
case len(requestInfo.Parts) >= 1:
|
||||
requestInfo.Resource = requestInfo.Parts[0]
|
||||
}
|
||||
|
||||
// if there's no name on the request and we thought it was a get before, then the actual verb is a list or a watch
|
||||
if len(requestInfo.Name) == 0 && requestInfo.Verb == "get" {
|
||||
// Assumes v1.ListOptions
|
||||
// Duplicates logic of Convert_Slice_string_To_bool
|
||||
switch strings.ToLower(req.URL.Query().Get("watch")) {
|
||||
case "false", "0", "":
|
||||
requestInfo.Verb = "list"
|
||||
default:
|
||||
requestInfo.Verb = "watch"
|
||||
}
|
||||
}
|
||||
// if there's no name on the request and we thought it was a delete before, then the actual verb is deletecollection
|
||||
if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" {
|
||||
requestInfo.Verb = "deletecollection"
|
||||
}
|
||||
|
||||
return &requestInfo, nil
|
||||
}
|
||||
|
||||
type requestInfoKeyType int
|
||||
|
||||
// requestInfoKey is the RequestInfo key for the context. It's of private type here. Because
|
||||
// keys are interfaces and interfaces are equal when the type and the value is equal, this
|
||||
// does not conflict with the keys defined in pkg/api.
|
||||
const requestInfoKey requestInfoKeyType = iota
|
||||
|
||||
// WithRequestInfo returns a copy of parent in which the request info value is set
|
||||
func WithRequestInfo(parent Context, info *RequestInfo) Context {
|
||||
return WithValue(parent, requestInfoKey, info)
|
||||
}
|
||||
|
||||
// RequestInfoFrom returns the value of the RequestInfo key on the ctx
|
||||
func RequestInfoFrom(ctx Context) (*RequestInfo, bool) {
|
||||
info, ok := ctx.Value(requestInfoKey).(*RequestInfo)
|
||||
return info, ok
|
||||
}
|
||||
|
||||
// splitPath returns the segments for a URL path.
|
||||
func splitPath(path string) []string {
|
||||
path = strings.Trim(path, "/")
|
||||
if path == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(path, "/")
|
||||
}
|
||||
196
vendor/k8s.io/apiserver/pkg/endpoints/request/requestinfo_test.go
generated
vendored
Normal file
196
vendor/k8s.io/apiserver/pkg/endpoints/request/requestinfo_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
type fakeRL bool
|
||||
|
||||
func (fakeRL) Stop() {}
|
||||
func (f fakeRL) TryAccept() bool { return bool(f) }
|
||||
func (f fakeRL) Accept() {}
|
||||
|
||||
func TestGetAPIRequestInfo(t *testing.T) {
|
||||
namespaceAll := "" // TODO(sttts): solve import cycle when using metav1.NamespaceAll
|
||||
successCases := []struct {
|
||||
method string
|
||||
url string
|
||||
expectedVerb string
|
||||
expectedAPIPrefix string
|
||||
expectedAPIGroup string
|
||||
expectedAPIVersion string
|
||||
expectedNamespace string
|
||||
expectedResource string
|
||||
expectedSubresource string
|
||||
expectedName string
|
||||
expectedParts []string
|
||||
}{
|
||||
|
||||
// resource paths
|
||||
{"GET", "/api/v1/namespaces", "list", "api", "", "v1", "", "namespaces", "", "", []string{"namespaces"}},
|
||||
{"GET", "/api/v1/namespaces/other", "get", "api", "", "v1", "other", "namespaces", "", "other", []string{"namespaces", "other"}},
|
||||
|
||||
{"GET", "/api/v1/namespaces/other/pods", "list", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
|
||||
{"GET", "/api/v1/namespaces/other/pods/foo", "get", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
|
||||
{"HEAD", "/api/v1/namespaces/other/pods/foo", "get", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
|
||||
{"GET", "/api/v1/pods", "list", "api", "", "v1", namespaceAll, "pods", "", "", []string{"pods"}},
|
||||
{"HEAD", "/api/v1/pods", "list", "api", "", "v1", namespaceAll, "pods", "", "", []string{"pods"}},
|
||||
{"GET", "/api/v1/namespaces/other/pods/foo", "get", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
|
||||
{"GET", "/api/v1/namespaces/other/pods", "list", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
|
||||
|
||||
// special verbs
|
||||
{"GET", "/api/v1/proxy/namespaces/other/pods/foo", "proxy", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
|
||||
{"GET", "/api/v1/proxy/namespaces/other/pods/foo/subpath/not/a/subresource", "proxy", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo", "subpath", "not", "a", "subresource"}},
|
||||
{"GET", "/api/v1/redirect/namespaces/other/pods/foo", "redirect", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
|
||||
{"GET", "/api/v1/redirect/namespaces/other/pods/foo/subpath/not/a/subresource", "redirect", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo", "subpath", "not", "a", "subresource"}},
|
||||
{"GET", "/api/v1/watch/pods", "watch", "api", "", "v1", namespaceAll, "pods", "", "", []string{"pods"}},
|
||||
{"GET", "/api/v1/pods?watch=true", "watch", "api", "", "v1", namespaceAll, "pods", "", "", []string{"pods"}},
|
||||
{"GET", "/api/v1/pods?watch=false", "list", "api", "", "v1", namespaceAll, "pods", "", "", []string{"pods"}},
|
||||
{"GET", "/api/v1/watch/namespaces/other/pods", "watch", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
|
||||
{"GET", "/api/v1/namespaces/other/pods?watch=1", "watch", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
|
||||
{"GET", "/api/v1/namespaces/other/pods?watch=0", "list", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
|
||||
|
||||
// subresource identification
|
||||
{"GET", "/api/v1/namespaces/other/pods/foo/status", "get", "api", "", "v1", "other", "pods", "status", "foo", []string{"pods", "foo", "status"}},
|
||||
{"GET", "/api/v1/namespaces/other/pods/foo/proxy/subpath", "get", "api", "", "v1", "other", "pods", "proxy", "foo", []string{"pods", "foo", "proxy", "subpath"}},
|
||||
{"PUT", "/api/v1/namespaces/other/finalize", "update", "api", "", "v1", "other", "namespaces", "finalize", "other", []string{"namespaces", "other", "finalize"}},
|
||||
{"PUT", "/api/v1/namespaces/other/status", "update", "api", "", "v1", "other", "namespaces", "status", "other", []string{"namespaces", "other", "status"}},
|
||||
|
||||
// verb identification
|
||||
{"PATCH", "/api/v1/namespaces/other/pods/foo", "patch", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
|
||||
{"DELETE", "/api/v1/namespaces/other/pods/foo", "delete", "api", "", "v1", "other", "pods", "", "foo", []string{"pods", "foo"}},
|
||||
{"POST", "/api/v1/namespaces/other/pods", "create", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
|
||||
|
||||
// deletecollection verb identification
|
||||
{"DELETE", "/api/v1/nodes", "deletecollection", "api", "", "v1", "", "nodes", "", "", []string{"nodes"}},
|
||||
{"DELETE", "/api/v1/namespaces", "deletecollection", "api", "", "v1", "", "namespaces", "", "", []string{"namespaces"}},
|
||||
{"DELETE", "/api/v1/namespaces/other/pods", "deletecollection", "api", "", "v1", "other", "pods", "", "", []string{"pods"}},
|
||||
{"DELETE", "/apis/extensions/v1/namespaces/other/pods", "deletecollection", "api", "extensions", "v1", "other", "pods", "", "", []string{"pods"}},
|
||||
|
||||
// api group identification
|
||||
{"POST", "/apis/extensions/v1/namespaces/other/pods", "create", "api", "extensions", "v1", "other", "pods", "", "", []string{"pods"}},
|
||||
|
||||
// api version identification
|
||||
{"POST", "/apis/extensions/v1beta3/namespaces/other/pods", "create", "api", "extensions", "v1beta3", "other", "pods", "", "", []string{"pods"}},
|
||||
}
|
||||
|
||||
resolver := newTestRequestInfoResolver()
|
||||
|
||||
for _, successCase := range successCases {
|
||||
req, _ := http.NewRequest(successCase.method, successCase.url, nil)
|
||||
|
||||
apiRequestInfo, err := resolver.NewRequestInfo(req)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for url: %s %v", successCase.url, err)
|
||||
}
|
||||
if !apiRequestInfo.IsResourceRequest {
|
||||
t.Errorf("Expected resource request")
|
||||
}
|
||||
if successCase.expectedVerb != apiRequestInfo.Verb {
|
||||
t.Errorf("Unexpected verb for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedVerb, apiRequestInfo.Verb)
|
||||
}
|
||||
if successCase.expectedAPIVersion != apiRequestInfo.APIVersion {
|
||||
t.Errorf("Unexpected apiVersion for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedAPIVersion, apiRequestInfo.APIVersion)
|
||||
}
|
||||
if successCase.expectedNamespace != apiRequestInfo.Namespace {
|
||||
t.Errorf("Unexpected namespace for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedNamespace, apiRequestInfo.Namespace)
|
||||
}
|
||||
if successCase.expectedResource != apiRequestInfo.Resource {
|
||||
t.Errorf("Unexpected resource for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedResource, apiRequestInfo.Resource)
|
||||
}
|
||||
if successCase.expectedSubresource != apiRequestInfo.Subresource {
|
||||
t.Errorf("Unexpected resource for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedSubresource, apiRequestInfo.Subresource)
|
||||
}
|
||||
if successCase.expectedName != apiRequestInfo.Name {
|
||||
t.Errorf("Unexpected name for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedName, apiRequestInfo.Name)
|
||||
}
|
||||
if !reflect.DeepEqual(successCase.expectedParts, apiRequestInfo.Parts) {
|
||||
t.Errorf("Unexpected parts for url: %s, expected: %v, actual: %v", successCase.url, successCase.expectedParts, apiRequestInfo.Parts)
|
||||
}
|
||||
}
|
||||
|
||||
errorCases := map[string]string{
|
||||
"no resource path": "/",
|
||||
"just apiversion": "/api/version/",
|
||||
"just prefix, group, version": "/apis/group/version/",
|
||||
"apiversion with no resource": "/api/version/",
|
||||
"bad prefix": "/badprefix/version/resource",
|
||||
"missing api group": "/apis/version/resource",
|
||||
}
|
||||
for k, v := range errorCases {
|
||||
req, err := http.NewRequest("GET", v, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
apiRequestInfo, err := resolver.NewRequestInfo(req)
|
||||
if err != nil {
|
||||
t.Errorf("%s: Unexpected error %v", k, err)
|
||||
}
|
||||
if apiRequestInfo.IsResourceRequest {
|
||||
t.Errorf("%s: expected non-resource request", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNonAPIRequestInfo(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
url string
|
||||
expected bool
|
||||
}{
|
||||
"simple groupless": {"/api/version/resource", true},
|
||||
"simple group": {"/apis/group/version/resource/name/subresource", true},
|
||||
"more steps": {"/api/version/resource/name/subresource", true},
|
||||
"group list": {"/apis/batch/v1/job", true},
|
||||
"group get": {"/apis/batch/v1/job/foo", true},
|
||||
"group subresource": {"/apis/batch/v1/job/foo/scale", true},
|
||||
|
||||
"bad root": {"/not-api/version/resource", false},
|
||||
"group without enough steps": {"/apis/extensions/v1beta1", false},
|
||||
"group without enough steps 2": {"/apis/extensions/v1beta1/", false},
|
||||
"not enough steps": {"/api/version", false},
|
||||
"one step": {"/api", false},
|
||||
"zero step": {"/", false},
|
||||
"empty": {"", false},
|
||||
}
|
||||
|
||||
resolver := newTestRequestInfoResolver()
|
||||
|
||||
for testName, tc := range tests {
|
||||
req, _ := http.NewRequest("GET", tc.url, nil)
|
||||
|
||||
apiRequestInfo, err := resolver.NewRequestInfo(req)
|
||||
if err != nil {
|
||||
t.Errorf("%s: Unexpected error %v", testName, err)
|
||||
}
|
||||
if e, a := tc.expected, apiRequestInfo.IsResourceRequest; e != a {
|
||||
t.Errorf("%s: expected %v, actual %v", testName, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTestRequestInfoResolver() *RequestInfoFactory {
|
||||
return &RequestInfoFactory{
|
||||
APIPrefixes: sets.NewString("api", "apis"),
|
||||
GrouplessAPIPrefixes: sets.NewString("api"),
|
||||
}
|
||||
}
|
||||
23
vendor/k8s.io/apiserver/pkg/endpoints/testing/BUILD
generated
vendored
Normal file
23
vendor/k8s.io/apiserver/pkg/endpoints/testing/BUILD
generated
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"types.generated.go",
|
||||
"types.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/ugorji/go/codec:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
],
|
||||
)
|
||||
11
vendor/k8s.io/apiserver/pkg/endpoints/testing/OWNERS
generated
vendored
Normal file
11
vendor/k8s.io/apiserver/pkg/endpoints/testing/OWNERS
generated
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
reviewers:
|
||||
- smarterclayton
|
||||
- wojtek-t
|
||||
- caesarxuchao
|
||||
- liggitt
|
||||
- erictune
|
||||
- timothysc
|
||||
- soltysh
|
||||
- mml
|
||||
- mbohlool
|
||||
- jianhuiz
|
||||
1813
vendor/k8s.io/apiserver/pkg/endpoints/testing/types.generated.go
generated
vendored
Normal file
1813
vendor/k8s.io/apiserver/pkg/endpoints/testing/types.generated.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
69
vendor/k8s.io/apiserver/pkg/endpoints/testing/types.go
generated
vendored
Normal file
69
vendor/k8s.io/apiserver/pkg/endpoints/testing/types.go
generated
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package testing
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type Simple struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata"`
|
||||
// +optional
|
||||
Other string `json:"other,omitempty"`
|
||||
// +optional
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
func (obj *Simple) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta }
|
||||
|
||||
type SimpleRoot struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata"`
|
||||
// +optional
|
||||
Other string `json:"other,omitempty"`
|
||||
// +optional
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
func (obj *SimpleRoot) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta }
|
||||
|
||||
type SimpleGetOptions struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
Param1 string `json:"param1"`
|
||||
Param2 string `json:"param2"`
|
||||
Path string `json:"atAPath"`
|
||||
}
|
||||
|
||||
func (SimpleGetOptions) SwaggerDoc() map[string]string {
|
||||
return map[string]string{
|
||||
"param1": "description for param1",
|
||||
"param2": "description for param2",
|
||||
}
|
||||
}
|
||||
|
||||
func (obj *SimpleGetOptions) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta }
|
||||
|
||||
type SimpleList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,inline"`
|
||||
// +optional
|
||||
Items []Simple `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
func (obj *SimpleList) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta }
|
||||
764
vendor/k8s.io/apiserver/pkg/endpoints/watch_test.go
generated
vendored
Normal file
764
vendor/k8s.io/apiserver/pkg/endpoints/watch_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,764 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/streaming"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
example "k8s.io/apiserver/pkg/apis/example"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers"
|
||||
apitesting "k8s.io/apiserver/pkg/endpoints/testing"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
)
|
||||
|
||||
// watchJSON defines the expected JSON wire equivalent of watch.Event
|
||||
type watchJSON struct {
|
||||
Type watch.EventType `json:"type,omitempty"`
|
||||
Object json.RawMessage `json:"object,omitempty"`
|
||||
}
|
||||
|
||||
// roundTripOrDie round trips an object to get defaults set.
|
||||
func roundTripOrDie(codec runtime.Codec, object runtime.Object) runtime.Object {
|
||||
data, err := runtime.Encode(codec, object)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
obj, err := runtime.Decode(codec, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
var watchTestTable = []struct {
|
||||
t watch.EventType
|
||||
obj runtime.Object
|
||||
}{
|
||||
{watch.Added, &apitesting.Simple{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}},
|
||||
{watch.Modified, &apitesting.Simple{ObjectMeta: metav1.ObjectMeta{Name: "bar"}}},
|
||||
{watch.Deleted, &apitesting.Simple{ObjectMeta: metav1.ObjectMeta{Name: "bar"}}},
|
||||
}
|
||||
|
||||
func podWatchTestTable() []struct {
|
||||
t watch.EventType
|
||||
obj runtime.Object
|
||||
} {
|
||||
// creaze lazily here in a func because podWatchTestTable can only be used after all types are registered.
|
||||
return []struct {
|
||||
t watch.EventType
|
||||
obj runtime.Object
|
||||
}{
|
||||
{watch.Added, roundTripOrDie(codec, &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}})},
|
||||
{watch.Modified, roundTripOrDie(codec, &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "bar"}})},
|
||||
{watch.Deleted, roundTripOrDie(codec, &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "bar"}})},
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchWebsocket(t *testing.T) {
|
||||
simpleStorage := &SimpleRESTStorage{}
|
||||
_ = rest.Watcher(simpleStorage) // Give compile error if this doesn't work.
|
||||
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
|
||||
dest, _ := url.Parse(server.URL)
|
||||
dest.Scheme = "ws" // Required by websocket, though the server never sees it.
|
||||
dest.Path = "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/watch/simples"
|
||||
dest.RawQuery = ""
|
||||
|
||||
ws, err := websocket.Dial(dest.String(), "", "http://localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
try := func(action watch.EventType, object runtime.Object) {
|
||||
// Send
|
||||
simpleStorage.fakeWatch.Action(action, object)
|
||||
// Test receive
|
||||
var got watchJSON
|
||||
err := websocket.JSON.Receive(ws, &got)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if got.Type != action {
|
||||
t.Errorf("Unexpected type: %v", got.Type)
|
||||
}
|
||||
gotObj, err := runtime.Decode(codec, got.Object)
|
||||
if err != nil {
|
||||
t.Fatalf("Decode error: %v\n%v", err, got)
|
||||
}
|
||||
if e, a := object, gotObj; !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("Expected %#v, got %#v", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range watchTestTable {
|
||||
try(item.t, item.obj)
|
||||
}
|
||||
simpleStorage.fakeWatch.Stop()
|
||||
|
||||
var got watchJSON
|
||||
err = websocket.JSON.Receive(ws, &got)
|
||||
if err == nil {
|
||||
t.Errorf("Unexpected non-error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchWebsocketClientClose(t *testing.T) {
|
||||
simpleStorage := &SimpleRESTStorage{}
|
||||
_ = rest.Watcher(simpleStorage) // Give compile error if this doesn't work.
|
||||
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
|
||||
dest, _ := url.Parse(server.URL)
|
||||
dest.Scheme = "ws" // Required by websocket, though the server never sees it.
|
||||
dest.Path = "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/watch/simples"
|
||||
dest.RawQuery = ""
|
||||
|
||||
ws, err := websocket.Dial(dest.String(), "", "http://localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
try := func(action watch.EventType, object runtime.Object) {
|
||||
// Send
|
||||
simpleStorage.fakeWatch.Action(action, object)
|
||||
// Test receive
|
||||
var got watchJSON
|
||||
err := websocket.JSON.Receive(ws, &got)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if got.Type != action {
|
||||
t.Errorf("Unexpected type: %v", got.Type)
|
||||
}
|
||||
gotObj, err := runtime.Decode(codec, got.Object)
|
||||
if err != nil {
|
||||
t.Fatalf("Decode error: %v\n%v", err, got)
|
||||
}
|
||||
if e, a := object, gotObj; !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("Expected %#v, got %#v", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
// Send/receive should work
|
||||
for _, item := range watchTestTable {
|
||||
try(item.t, item.obj)
|
||||
}
|
||||
|
||||
// Sending normal data should be ignored
|
||||
websocket.JSON.Send(ws, map[string]interface{}{"test": "data"})
|
||||
|
||||
// Send/receive should still work
|
||||
for _, item := range watchTestTable {
|
||||
try(item.t, item.obj)
|
||||
}
|
||||
|
||||
// Client requests a close
|
||||
ws.Close()
|
||||
|
||||
select {
|
||||
case data, ok := <-simpleStorage.fakeWatch.ResultChan():
|
||||
if ok {
|
||||
t.Errorf("expected a closed result channel, but got watch result %#v", data)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("watcher did not close when client closed")
|
||||
}
|
||||
|
||||
var got watchJSON
|
||||
err = websocket.JSON.Receive(ws, &got)
|
||||
if err == nil {
|
||||
t.Errorf("Unexpected non-error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchRead(t *testing.T) {
|
||||
simpleStorage := &SimpleRESTStorage{}
|
||||
_ = rest.Watcher(simpleStorage) // Give compile error if this doesn't work.
|
||||
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
|
||||
dest, _ := url.Parse(server.URL)
|
||||
dest.Path = "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/simples"
|
||||
dest.RawQuery = "watch=1"
|
||||
|
||||
connectHTTP := func(accept string) (io.ReadCloser, string) {
|
||||
client := http.Client{}
|
||||
request, err := http.NewRequest("GET", dest.String(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
request.Header.Add("Accept", accept)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
b, _ := ioutil.ReadAll(response.Body)
|
||||
t.Fatalf("Unexpected response for accept: %q: %#v\n%s", accept, response, string(b))
|
||||
}
|
||||
return response.Body, response.Header.Get("Content-Type")
|
||||
}
|
||||
|
||||
connectWebSocket := func(accept string) (io.ReadCloser, string) {
|
||||
dest := *dest
|
||||
dest.Scheme = "ws" // Required by websocket, though the server never sees it.
|
||||
config, err := websocket.NewConfig(dest.String(), "http://localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
config.Header.Add("Accept", accept)
|
||||
ws, err := websocket.DialConfig(config)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
return ws, "__default__"
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
Accept string
|
||||
ExpectedContentType string
|
||||
MediaType string
|
||||
}{
|
||||
{
|
||||
Accept: "application/json",
|
||||
ExpectedContentType: "application/json",
|
||||
MediaType: "application/json",
|
||||
},
|
||||
{
|
||||
Accept: "application/json;stream=watch",
|
||||
ExpectedContentType: "application/json", // legacy behavior
|
||||
MediaType: "application/json",
|
||||
},
|
||||
// TODO: yaml stream serialization requires that RawExtension.MarshalJSON
|
||||
// be able to understand nested encoding (since yaml calls json.Marshal
|
||||
// rather than yaml.Marshal, which results in the raw bytes being in yaml).
|
||||
// Same problem as thirdparty object.
|
||||
/*{
|
||||
Accept: "application/yaml",
|
||||
ExpectedContentType: "application/yaml;stream=watch",
|
||||
MediaType: "application/yaml",
|
||||
},*/
|
||||
{
|
||||
Accept: "application/vnd.kubernetes.protobuf",
|
||||
ExpectedContentType: "application/vnd.kubernetes.protobuf;stream=watch",
|
||||
MediaType: "application/vnd.kubernetes.protobuf",
|
||||
},
|
||||
{
|
||||
Accept: "application/vnd.kubernetes.protobuf;stream=watch",
|
||||
ExpectedContentType: "application/vnd.kubernetes.protobuf;stream=watch",
|
||||
MediaType: "application/vnd.kubernetes.protobuf",
|
||||
},
|
||||
}
|
||||
protocols := []struct {
|
||||
name string
|
||||
selfFraming bool
|
||||
fn func(string) (io.ReadCloser, string)
|
||||
}{
|
||||
{name: "http", fn: connectHTTP},
|
||||
{name: "websocket", selfFraming: true, fn: connectWebSocket},
|
||||
}
|
||||
|
||||
for _, protocol := range protocols {
|
||||
for _, test := range testCases {
|
||||
func() {
|
||||
info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), test.MediaType)
|
||||
if !ok || info.StreamSerializer == nil {
|
||||
t.Fatal(info)
|
||||
}
|
||||
streamSerializer := info.StreamSerializer
|
||||
|
||||
r, contentType := protocol.fn(test.Accept)
|
||||
defer r.Close()
|
||||
|
||||
if contentType != "__default__" && contentType != test.ExpectedContentType {
|
||||
t.Errorf("Unexpected content type: %#v", contentType)
|
||||
}
|
||||
objectCodec := codecs.DecoderToVersion(info.Serializer, testInternalGroupVersion)
|
||||
|
||||
var fr io.ReadCloser = r
|
||||
if !protocol.selfFraming {
|
||||
fr = streamSerializer.Framer.NewFrameReader(r)
|
||||
}
|
||||
d := streaming.NewDecoder(fr, streamSerializer.Serializer)
|
||||
|
||||
var w *watch.FakeWatcher
|
||||
for w == nil {
|
||||
w = simpleStorage.Watcher()
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
|
||||
for i, item := range podWatchTestTable() {
|
||||
action, object := item.t, item.obj
|
||||
name := fmt.Sprintf("%s-%s-%d", protocol.name, test.MediaType, i)
|
||||
|
||||
// Send
|
||||
w.Action(action, object)
|
||||
// Test receive
|
||||
var got metav1.WatchEvent
|
||||
_, _, err := d.Decode(nil, &got)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Unexpected error: %v", name, err)
|
||||
}
|
||||
if got.Type != string(action) {
|
||||
t.Errorf("%s: Unexpected type: %v", name, got.Type)
|
||||
}
|
||||
|
||||
gotObj, err := runtime.Decode(objectCodec, got.Object.Raw)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Decode error: %v", name, err)
|
||||
}
|
||||
if e, a := object, gotObj; !apiequality.Semantic.DeepEqual(e, a) {
|
||||
t.Errorf("%s: different: %s", name, diff.ObjectDiff(e, a))
|
||||
}
|
||||
}
|
||||
w.Stop()
|
||||
|
||||
var got metav1.WatchEvent
|
||||
_, _, err := d.Decode(nil, &got)
|
||||
if err == nil {
|
||||
t.Errorf("Unexpected non-error")
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchHTTPAccept(t *testing.T) {
|
||||
simpleStorage := &SimpleRESTStorage{}
|
||||
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
client := http.Client{}
|
||||
|
||||
dest, _ := url.Parse(server.URL)
|
||||
dest.Path = "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/watch/simples"
|
||||
dest.RawQuery = ""
|
||||
|
||||
request, err := http.NewRequest("GET", dest.String(), nil)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Accept", "application/XYZ")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// TODO: once this is fixed, this test will change
|
||||
if response.StatusCode != http.StatusNotAcceptable {
|
||||
t.Errorf("Unexpected response %#v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchParamParsing(t *testing.T) {
|
||||
simpleStorage := &SimpleRESTStorage{}
|
||||
handler := handle(map[string]rest.Storage{
|
||||
"simples": simpleStorage,
|
||||
"simpleroots": simpleStorage,
|
||||
})
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
|
||||
dest, _ := url.Parse(server.URL)
|
||||
|
||||
rootPath := "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/watch/simples"
|
||||
namespacedPath := "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/watch/namespaces/other/simpleroots"
|
||||
|
||||
table := []struct {
|
||||
path string
|
||||
rawQuery string
|
||||
resourceVersion string
|
||||
labelSelector string
|
||||
fieldSelector string
|
||||
namespace string
|
||||
}{
|
||||
{
|
||||
path: rootPath,
|
||||
rawQuery: "resourceVersion=1234",
|
||||
resourceVersion: "1234",
|
||||
labelSelector: "",
|
||||
fieldSelector: "",
|
||||
namespace: metav1.NamespaceAll,
|
||||
}, {
|
||||
path: rootPath,
|
||||
rawQuery: "resourceVersion=314159&fieldSelector=Host%3D&labelSelector=name%3Dfoo",
|
||||
resourceVersion: "314159",
|
||||
labelSelector: "name=foo",
|
||||
fieldSelector: "Host=",
|
||||
namespace: metav1.NamespaceAll,
|
||||
}, {
|
||||
path: rootPath,
|
||||
rawQuery: "fieldSelector=id%3dfoo&resourceVersion=1492",
|
||||
resourceVersion: "1492",
|
||||
labelSelector: "",
|
||||
fieldSelector: "id=foo",
|
||||
namespace: metav1.NamespaceAll,
|
||||
}, {
|
||||
path: rootPath,
|
||||
rawQuery: "",
|
||||
resourceVersion: "",
|
||||
labelSelector: "",
|
||||
fieldSelector: "",
|
||||
namespace: metav1.NamespaceAll,
|
||||
},
|
||||
{
|
||||
path: namespacedPath,
|
||||
rawQuery: "resourceVersion=1234",
|
||||
resourceVersion: "1234",
|
||||
labelSelector: "",
|
||||
fieldSelector: "",
|
||||
namespace: "other",
|
||||
}, {
|
||||
path: namespacedPath,
|
||||
rawQuery: "resourceVersion=314159&fieldSelector=Host%3D&labelSelector=name%3Dfoo",
|
||||
resourceVersion: "314159",
|
||||
labelSelector: "name=foo",
|
||||
fieldSelector: "Host=",
|
||||
namespace: "other",
|
||||
}, {
|
||||
path: namespacedPath,
|
||||
rawQuery: "fieldSelector=id%3dfoo&resourceVersion=1492",
|
||||
resourceVersion: "1492",
|
||||
labelSelector: "",
|
||||
fieldSelector: "id=foo",
|
||||
namespace: "other",
|
||||
}, {
|
||||
path: namespacedPath,
|
||||
rawQuery: "",
|
||||
resourceVersion: "",
|
||||
labelSelector: "",
|
||||
fieldSelector: "",
|
||||
namespace: "other",
|
||||
},
|
||||
}
|
||||
|
||||
for _, item := range table {
|
||||
simpleStorage.requestedLabelSelector = labels.Everything()
|
||||
simpleStorage.requestedFieldSelector = fields.Everything()
|
||||
simpleStorage.requestedResourceVersion = "5" // Prove this is set in all cases
|
||||
simpleStorage.requestedResourceNamespace = ""
|
||||
dest.Path = item.path
|
||||
dest.RawQuery = item.rawQuery
|
||||
resp, err := http.Get(dest.String())
|
||||
if err != nil {
|
||||
t.Errorf("%v: unexpected error: %v", item.rawQuery, err)
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
if e, a := item.namespace, simpleStorage.requestedResourceNamespace; e != a {
|
||||
t.Errorf("%v: expected %v, got %v", item.rawQuery, e, a)
|
||||
}
|
||||
if e, a := item.resourceVersion, simpleStorage.requestedResourceVersion; e != a {
|
||||
t.Errorf("%v: expected %v, got %v", item.rawQuery, e, a)
|
||||
}
|
||||
if e, a := item.labelSelector, simpleStorage.requestedLabelSelector.String(); e != a {
|
||||
t.Errorf("%v: expected %v, got %v", item.rawQuery, e, a)
|
||||
}
|
||||
if e, a := item.fieldSelector, simpleStorage.requestedFieldSelector.String(); e != a {
|
||||
t.Errorf("%v: expected %v, got %v", item.rawQuery, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchProtocolSelection(t *testing.T) {
|
||||
simpleStorage := &SimpleRESTStorage{}
|
||||
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
defer server.CloseClientConnections()
|
||||
client := http.Client{}
|
||||
|
||||
dest, _ := url.Parse(server.URL)
|
||||
dest.Path = "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/watch/simples"
|
||||
dest.RawQuery = ""
|
||||
|
||||
table := []struct {
|
||||
isWebsocket bool
|
||||
connHeader string
|
||||
}{
|
||||
{true, "Upgrade"},
|
||||
{true, "keep-alive, Upgrade"},
|
||||
{true, "upgrade"},
|
||||
{false, "keep-alive"},
|
||||
}
|
||||
|
||||
for _, item := range table {
|
||||
request, err := http.NewRequest("GET", dest.String(), nil)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
request.Header.Set("Connection", item.connHeader)
|
||||
request.Header.Set("Upgrade", "websocket")
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// The requests recognized as websocket requests based on connection
|
||||
// and upgrade headers will not also have the necessary Sec-Websocket-*
|
||||
// headers so it is expected to throw a 400
|
||||
if item.isWebsocket && response.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Unexpected response %#v", response)
|
||||
}
|
||||
|
||||
if !item.isWebsocket && response.StatusCode != http.StatusOK {
|
||||
t.Errorf("Unexpected response %#v", response)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type fakeTimeoutFactory struct {
|
||||
timeoutCh chan time.Time
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (t *fakeTimeoutFactory) TimeoutCh() (<-chan time.Time, func() bool) {
|
||||
return t.timeoutCh, func() bool {
|
||||
defer close(t.done)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchHTTPTimeout(t *testing.T) {
|
||||
watcher := watch.NewFake()
|
||||
timeoutCh := make(chan time.Time)
|
||||
done := make(chan struct{})
|
||||
|
||||
info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), runtime.ContentTypeJSON)
|
||||
if !ok || info.StreamSerializer == nil {
|
||||
t.Fatal(info)
|
||||
}
|
||||
serializer := info.StreamSerializer
|
||||
|
||||
// Setup a new watchserver
|
||||
watchServer := &handlers.WatchServer{
|
||||
Watching: watcher,
|
||||
|
||||
MediaType: "testcase/json",
|
||||
Framer: serializer.Framer,
|
||||
Encoder: newCodec,
|
||||
EmbeddedEncoder: newCodec,
|
||||
|
||||
Fixup: func(obj runtime.Object) {},
|
||||
TimeoutFactory: &fakeTimeoutFactory{timeoutCh, done},
|
||||
}
|
||||
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
watchServer.ServeHTTP(w, req)
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
// Setup a client
|
||||
dest, _ := url.Parse(s.URL)
|
||||
dest.Path = "/" + prefix + "/" + newGroupVersion.Group + "/" + newGroupVersion.Version + "/simple"
|
||||
dest.RawQuery = "watch=true"
|
||||
|
||||
req, _ := http.NewRequest("GET", dest.String(), nil)
|
||||
client := http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
watcher.Add(&apitesting.Simple{TypeMeta: metav1.TypeMeta{APIVersion: newGroupVersion.String()}})
|
||||
|
||||
// Make sure we can actually watch an endpoint
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
var got watchJSON
|
||||
err = decoder.Decode(&got)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Timeout and check for leaks
|
||||
close(timeoutCh)
|
||||
select {
|
||||
case <-done:
|
||||
if !watcher.Stopped {
|
||||
t.Errorf("Leaked watch on timeout")
|
||||
}
|
||||
case <-time.After(wait.ForeverTestTimeout):
|
||||
t.Errorf("Failed to stop watcher after %s of timeout signal", wait.ForeverTestTimeout.String())
|
||||
}
|
||||
|
||||
// Make sure we can't receive any more events through the timeout watch
|
||||
err = decoder.Decode(&got)
|
||||
if err != io.EOF {
|
||||
t.Errorf("Unexpected non-error")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkWatchHTTP measures the cost of serving a watch.
|
||||
func BenchmarkWatchHTTP(b *testing.B) {
|
||||
items := benchmarkItems(b)
|
||||
|
||||
simpleStorage := &SimpleRESTStorage{}
|
||||
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
client := http.Client{}
|
||||
|
||||
dest, _ := url.Parse(server.URL)
|
||||
dest.Path = "/" + prefix + "/" + newGroupVersion.Group + "/" + newGroupVersion.Version + "/watch/simples"
|
||||
dest.RawQuery = ""
|
||||
|
||||
request, err := http.NewRequest("GET", dest.String(), nil)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
b.Fatalf("Unexpected response %#v", response)
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer response.Body.Close()
|
||||
if _, err := io.Copy(ioutil.Discard, response.Body); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
actions := []watch.EventType{watch.Added, watch.Modified, watch.Deleted}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
simpleStorage.fakeWatch.Action(actions[i%len(actions)], &items[i%len(items)])
|
||||
}
|
||||
simpleStorage.fakeWatch.Stop()
|
||||
wg.Wait()
|
||||
b.StopTimer()
|
||||
}
|
||||
|
||||
// BenchmarkWatchWebsocket measures the cost of serving a watch.
|
||||
func BenchmarkWatchWebsocket(b *testing.B) {
|
||||
items := benchmarkItems(b)
|
||||
|
||||
simpleStorage := &SimpleRESTStorage{}
|
||||
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
|
||||
dest, _ := url.Parse(server.URL)
|
||||
dest.Scheme = "ws" // Required by websocket, though the server never sees it.
|
||||
dest.Path = "/" + prefix + "/" + newGroupVersion.Group + "/" + newGroupVersion.Version + "/watch/simples"
|
||||
dest.RawQuery = ""
|
||||
|
||||
ws, err := websocket.Dial(dest.String(), "", "http://localhost")
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer ws.Close()
|
||||
if _, err := io.Copy(ioutil.Discard, ws); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
actions := []watch.EventType{watch.Added, watch.Modified, watch.Deleted}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
simpleStorage.fakeWatch.Action(actions[i%len(actions)], &items[i%len(items)])
|
||||
}
|
||||
simpleStorage.fakeWatch.Stop()
|
||||
wg.Wait()
|
||||
b.StopTimer()
|
||||
}
|
||||
|
||||
// BenchmarkWatchProtobuf measures the cost of serving a watch.
|
||||
func BenchmarkWatchProtobuf(b *testing.B) {
|
||||
items := benchmarkItems(b)
|
||||
|
||||
simpleStorage := &SimpleRESTStorage{}
|
||||
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
client := http.Client{}
|
||||
|
||||
dest, _ := url.Parse(server.URL)
|
||||
dest.Path = "/" + prefix + "/" + newGroupVersion.Group + "/" + newGroupVersion.Version + "/watch/simples"
|
||||
dest.RawQuery = ""
|
||||
|
||||
request, err := http.NewRequest("GET", dest.String(), nil)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
request.Header.Set("Accept", "application/vnd.kubernetes.protobuf")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
b.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
b.Fatalf("Unexpected response %#v\n%s", response, body)
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer response.Body.Close()
|
||||
if _, err := io.Copy(ioutil.Discard, response.Body); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
actions := []watch.EventType{watch.Added, watch.Modified, watch.Deleted}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
simpleStorage.fakeWatch.Action(actions[i%len(actions)], &items[i%len(items)])
|
||||
}
|
||||
simpleStorage.fakeWatch.Stop()
|
||||
wg.Wait()
|
||||
b.StopTimer()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue