Add vendor folder to git

This commit is contained in:
Lucas Käldström 2017-06-26 19:23:05 +03:00
parent 66cf5eaafb
commit 183585f56f
No known key found for this signature in database
GPG key ID: 600FEFBBD0D40D21
6916 changed files with 2629581 additions and 1 deletions

81
vendor/k8s.io/apiserver/pkg/endpoints/BUILD generated vendored Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

51
vendor/k8s.io/apiserver/pkg/endpoints/discovery/BUILD generated vendored Normal file
View 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",
],
)

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

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

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

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

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

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

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

@ -0,0 +1,4 @@
reviewers:
- deads2k
- sttts
- soltysh

162
vendor/k8s.io/apiserver/pkg/endpoints/filters/audit.go generated vendored Normal file
View 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
}

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

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

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

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

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

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

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

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

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

View 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",
],
)

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

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

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

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

View 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",
],
)

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

View 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(`&`, "&amp;", `<`, "&lt;", `>`, "&gt;")
// 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))
}

View 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: "/&lt;script&gt;"`},
{"NotFound", "/get", `Not Found: "/get"`},
{"NotFound", "/<script&>", `Not Found: "/&lt;script&amp;&gt;"`},
{"InternalError", "/get", `Internal Server Error: "/get": ARGH`},
{"InternalError", "/<script>", `Internal Server Error: "/&lt;script&gt;": 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 "/&lt;script&gt;".`,
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)
}
}
}

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

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

View 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

File diff suppressed because it is too large Load diff

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

File diff suppressed because it is too large Load diff

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

@ -0,0 +1,3 @@
reviewers:
- wojtek-t
- jimmidyson

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

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

@ -0,0 +1,2 @@
reviewers:
- mbohlool

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

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

@ -0,0 +1,2 @@
reviewers:
- sttts

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

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

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

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

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

@ -0,0 +1,11 @@
reviewers:
- smarterclayton
- wojtek-t
- caesarxuchao
- liggitt
- erictune
- timothysc
- soltysh
- mml
- mbohlool
- jianhuiz

File diff suppressed because it is too large Load diff

69
vendor/k8s.io/apiserver/pkg/endpoints/testing/types.go generated vendored Normal file
View 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
View 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()
}