mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 10:51:29 +00:00
remove old restclient
This commit is contained in:
parent
392a4d949f
commit
32a2c2d05c
@ -60,7 +60,7 @@ func (g *genFakeForGroup) Imports(c *generator.Context) (imports []string) {
|
||||
func (g *genFakeForGroup) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
|
||||
sw := generator.NewSnippetWriter(w, c, "$", "$")
|
||||
const pkgTestingCore = "k8s.io/kubernetes/pkg/client/testing/core"
|
||||
const pkgRESTClient = "k8s.io/kubernetes/pkg/client/restclient"
|
||||
const pkgRESTClient = "k8s.io/client-go/rest"
|
||||
m := map[string]interface{}{
|
||||
"group": g.group,
|
||||
"GroupVersion": namer.IC(g.group) + namer.IC(g.version),
|
||||
|
@ -73,7 +73,7 @@ func (g *genClientset) GenerateType(c *generator.Context, t *types.Type, w io.Wr
|
||||
// perhaps we can adapt the go2ild framework to this kind of usage.
|
||||
sw := generator.NewSnippetWriter(w, c, "$", "$")
|
||||
const pkgDiscovery = "k8s.io/kubernetes/pkg/client/typed/discovery"
|
||||
const pkgRESTClient = "k8s.io/kubernetes/pkg/client/restclient"
|
||||
const pkgRESTClient = "k8s.io/client-go/rest"
|
||||
|
||||
allGroups := clientgentypes.ToGroupVersionPackages(g.groups)
|
||||
|
||||
|
@ -57,7 +57,7 @@ func (g *genGroup) Imports(c *generator.Context) (imports []string) {
|
||||
|
||||
func (g *genGroup) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
|
||||
sw := generator.NewSnippetWriter(w, c, "$", "$")
|
||||
const pkgRESTClient = "k8s.io/kubernetes/pkg/client/restclient"
|
||||
const pkgRESTClient = "k8s.io/client-go/rest"
|
||||
const pkgAPI = "k8s.io/kubernetes/pkg/api"
|
||||
const pkgSerializer = "k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
const pkgUnversioned = "k8s.io/kubernetes/pkg/api/unversioned"
|
||||
|
@ -84,7 +84,7 @@ func (g *genClientForType) GenerateType(c *generator.Context, t *types.Type, w i
|
||||
"Group": namer.IC(g.group),
|
||||
"GroupVersion": namer.IC(g.group) + namer.IC(g.version),
|
||||
"watchInterface": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/watch", Name: "Interface"}),
|
||||
"RESTClientInterface": c.Universe.Type(types.Name{Package: "k8s.io/kubernetes/pkg/client/restclient", Name: "Interface"}),
|
||||
"RESTClientInterface": c.Universe.Type(types.Name{Package: "k8s.io/client-go/rest", Name: "Interface"}),
|
||||
"apiParameterCodec": c.Universe.Type(types.Name{Package: "k8s.io/kubernetes/pkg/api", Name: "ParameterCodec"}),
|
||||
"PatchType": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/types", Name: "PatchType"}),
|
||||
"namespaced": namespaced,
|
||||
|
@ -1,100 +0,0 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"client.go",
|
||||
"config.go",
|
||||
"plugin.go",
|
||||
"request.go",
|
||||
"transport.go",
|
||||
"url_utils.go",
|
||||
"urlbackoff.go",
|
||||
"versions.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/api/v1:go_default_library",
|
||||
"//pkg/api/validation/path:go_default_library",
|
||||
"//pkg/client/metrics:go_default_library",
|
||||
"//pkg/util/cert:go_default_library",
|
||||
"//pkg/version:go_default_library",
|
||||
"//vendor:github.com/golang/glog",
|
||||
"//vendor:k8s.io/apimachinery/pkg/api/errors",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"//vendor:k8s.io/apimachinery/pkg/fields",
|
||||
"//vendor:k8s.io/apimachinery/pkg/labels",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime/serializer/streaming",
|
||||
"//vendor:k8s.io/apimachinery/pkg/types",
|
||||
"//vendor:k8s.io/apimachinery/pkg/util/net",
|
||||
"//vendor:k8s.io/apimachinery/pkg/util/sets",
|
||||
"//vendor:k8s.io/apimachinery/pkg/watch",
|
||||
"//vendor:k8s.io/client-go/pkg/util/flowcontrol",
|
||||
"//vendor:k8s.io/client-go/rest/watch",
|
||||
"//vendor:k8s.io/client-go/tools/clientcmd/api",
|
||||
"//vendor:k8s.io/client-go/transport",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"client_test.go",
|
||||
"config_test.go",
|
||||
"plugin_test.go",
|
||||
"request_test.go",
|
||||
"url_utils_test.go",
|
||||
"urlbackoff_test.go",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/api/testapi:go_default_library",
|
||||
"//pkg/api/v1:go_default_library",
|
||||
"//pkg/util/httpstream:go_default_library",
|
||||
"//pkg/util/intstr:go_default_library",
|
||||
"//pkg/util/testing:go_default_library",
|
||||
"//vendor:github.com/google/gofuzz",
|
||||
"//vendor:k8s.io/apimachinery/pkg/api/errors",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"//vendor:k8s.io/apimachinery/pkg/labels",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime/serializer/streaming",
|
||||
"//vendor:k8s.io/apimachinery/pkg/types",
|
||||
"//vendor:k8s.io/apimachinery/pkg/util/diff",
|
||||
"//vendor:k8s.io/apimachinery/pkg/watch",
|
||||
"//vendor:k8s.io/client-go/pkg/util/clock",
|
||||
"//vendor:k8s.io/client-go/pkg/util/flowcontrol",
|
||||
"//vendor:k8s.io/client-go/rest/watch",
|
||||
"//vendor:k8s.io/client-go/tools/clientcmd/api",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//pkg/client/restclient/fake:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
@ -1,24 +0,0 @@
|
||||
reviewers:
|
||||
- thockin
|
||||
- smarterclayton
|
||||
- caesarxuchao
|
||||
- wojtek-t
|
||||
- deads2k
|
||||
- brendandburns
|
||||
- liggitt
|
||||
- nikhiljindal
|
||||
- gmarek
|
||||
- erictune
|
||||
- sttts
|
||||
- luxas
|
||||
- dims
|
||||
- errordeveloper
|
||||
- hongchaodeng
|
||||
- krousey
|
||||
- resouer
|
||||
- cjcullen
|
||||
- rmmh
|
||||
- lixiaobing10051267
|
||||
- asalkeld
|
||||
- juanvallejo
|
||||
- lojies
|
@ -1,258 +0,0 @@
|
||||
/*
|
||||
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 restclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/pkg/util/flowcontrol"
|
||||
)
|
||||
|
||||
const (
|
||||
// Environment variables: Note that the duration should be long enough that the backoff
|
||||
// persists for some reasonable time (i.e. 120 seconds). The typical base might be "1".
|
||||
envBackoffBase = "KUBE_CLIENT_BACKOFF_BASE"
|
||||
envBackoffDuration = "KUBE_CLIENT_BACKOFF_DURATION"
|
||||
)
|
||||
|
||||
// Interface captures the set of operations for generically interacting with Kubernetes REST apis.
|
||||
type Interface interface {
|
||||
GetRateLimiter() flowcontrol.RateLimiter
|
||||
Verb(verb string) *Request
|
||||
Post() *Request
|
||||
Put() *Request
|
||||
Patch(pt types.PatchType) *Request
|
||||
Get() *Request
|
||||
Delete() *Request
|
||||
APIVersion() schema.GroupVersion
|
||||
}
|
||||
|
||||
// RESTClient imposes common Kubernetes API conventions on a set of resource paths.
|
||||
// The baseURL is expected to point to an HTTP or HTTPS path that is the parent
|
||||
// of one or more resources. The server should return a decodable API resource
|
||||
// object, or an api.Status object which contains information about the reason for
|
||||
// any failure.
|
||||
//
|
||||
// Most consumers should use client.New() to get a Kubernetes API client.
|
||||
type RESTClient struct {
|
||||
// base is the root URL for all invocations of the client
|
||||
base *url.URL
|
||||
// versionedAPIPath is a path segment connecting the base URL to the resource root
|
||||
versionedAPIPath string
|
||||
|
||||
// contentConfig is the information used to communicate with the server.
|
||||
contentConfig ContentConfig
|
||||
|
||||
// serializers contain all serializers for underlying content type.
|
||||
serializers Serializers
|
||||
|
||||
// creates BackoffManager that is passed to requests.
|
||||
createBackoffMgr func() BackoffManager
|
||||
|
||||
// TODO extract this into a wrapper interface via the RESTClient interface in kubectl.
|
||||
Throttle flowcontrol.RateLimiter
|
||||
|
||||
// Set specific behavior of the client. If not set http.DefaultClient will be used.
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
type Serializers struct {
|
||||
Encoder runtime.Encoder
|
||||
Decoder runtime.Decoder
|
||||
StreamingSerializer runtime.Serializer
|
||||
Framer runtime.Framer
|
||||
RenegotiatedDecoder func(contentType string, params map[string]string) (runtime.Decoder, error)
|
||||
}
|
||||
|
||||
// NewRESTClient creates a new RESTClient. This client performs generic REST functions
|
||||
// such as Get, Put, Post, and Delete on specified paths. Codec controls encoding and
|
||||
// decoding of responses from the server.
|
||||
func NewRESTClient(baseURL *url.URL, versionedAPIPath string, config ContentConfig, maxQPS float32, maxBurst int, rateLimiter flowcontrol.RateLimiter, client *http.Client) (*RESTClient, error) {
|
||||
base := *baseURL
|
||||
if !strings.HasSuffix(base.Path, "/") {
|
||||
base.Path += "/"
|
||||
}
|
||||
base.RawQuery = ""
|
||||
base.Fragment = ""
|
||||
|
||||
if config.GroupVersion == nil {
|
||||
config.GroupVersion = &schema.GroupVersion{}
|
||||
}
|
||||
if len(config.ContentType) == 0 {
|
||||
config.ContentType = "application/json"
|
||||
}
|
||||
serializers, err := createSerializers(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var throttle flowcontrol.RateLimiter
|
||||
if maxQPS > 0 && rateLimiter == nil {
|
||||
throttle = flowcontrol.NewTokenBucketRateLimiter(maxQPS, maxBurst)
|
||||
} else if rateLimiter != nil {
|
||||
throttle = rateLimiter
|
||||
}
|
||||
return &RESTClient{
|
||||
base: &base,
|
||||
versionedAPIPath: versionedAPIPath,
|
||||
contentConfig: config,
|
||||
serializers: *serializers,
|
||||
createBackoffMgr: readExpBackoffConfig,
|
||||
Throttle: throttle,
|
||||
Client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetRateLimiter returns rate limier for a given client, or nil if it's called on a nil client
|
||||
func (c *RESTClient) GetRateLimiter() flowcontrol.RateLimiter {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return c.Throttle
|
||||
}
|
||||
|
||||
// readExpBackoffConfig handles the internal logic of determining what the
|
||||
// backoff policy is. By default if no information is available, NoBackoff.
|
||||
// TODO Generalize this see #17727 .
|
||||
func readExpBackoffConfig() BackoffManager {
|
||||
backoffBase := os.Getenv(envBackoffBase)
|
||||
backoffDuration := os.Getenv(envBackoffDuration)
|
||||
|
||||
backoffBaseInt, errBase := strconv.ParseInt(backoffBase, 10, 64)
|
||||
backoffDurationInt, errDuration := strconv.ParseInt(backoffDuration, 10, 64)
|
||||
if errBase != nil || errDuration != nil {
|
||||
return &NoBackoff{}
|
||||
}
|
||||
return &URLBackoff{
|
||||
Backoff: flowcontrol.NewBackOff(
|
||||
time.Duration(backoffBaseInt)*time.Second,
|
||||
time.Duration(backoffDurationInt)*time.Second)}
|
||||
}
|
||||
|
||||
// createSerializers creates all necessary serializers for given contentType.
|
||||
// TODO: the negotiated serializer passed to this method should probably return
|
||||
// serializers that control decoding and versioning without this package
|
||||
// being aware of the types. Depends on whether RESTClient must deal with
|
||||
// generic infrastructure.
|
||||
func createSerializers(config ContentConfig) (*Serializers, error) {
|
||||
mediaTypes := config.NegotiatedSerializer.SupportedMediaTypes()
|
||||
contentType := config.ContentType
|
||||
mediaType, _, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the content type specified in the client configuration is not recognized: %v", err)
|
||||
}
|
||||
info, ok := runtime.SerializerInfoForMediaType(mediaTypes, mediaType)
|
||||
if !ok {
|
||||
if len(contentType) != 0 || len(mediaTypes) == 0 {
|
||||
return nil, fmt.Errorf("no serializers registered for %s", contentType)
|
||||
}
|
||||
info = mediaTypes[0]
|
||||
}
|
||||
|
||||
internalGV := schema.GroupVersions{
|
||||
{
|
||||
Group: config.GroupVersion.Group,
|
||||
Version: runtime.APIVersionInternal,
|
||||
},
|
||||
// always include the legacy group as a decoding target to handle non-error `Status` return types
|
||||
{
|
||||
Group: "",
|
||||
Version: runtime.APIVersionInternal,
|
||||
},
|
||||
}
|
||||
|
||||
s := &Serializers{
|
||||
Encoder: config.NegotiatedSerializer.EncoderForVersion(info.Serializer, *config.GroupVersion),
|
||||
Decoder: config.NegotiatedSerializer.DecoderToVersion(info.Serializer, internalGV),
|
||||
|
||||
RenegotiatedDecoder: func(contentType string, params map[string]string) (runtime.Decoder, error) {
|
||||
info, ok := runtime.SerializerInfoForMediaType(mediaTypes, contentType)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("serializer for %s not registered", contentType)
|
||||
}
|
||||
return config.NegotiatedSerializer.DecoderToVersion(info.Serializer, internalGV), nil
|
||||
},
|
||||
}
|
||||
if info.StreamSerializer != nil {
|
||||
s.StreamingSerializer = info.StreamSerializer.Serializer
|
||||
s.Framer = info.StreamSerializer.Framer
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Verb begins a request with a verb (GET, POST, PUT, DELETE).
|
||||
//
|
||||
// Example usage of RESTClient's request building interface:
|
||||
// c, err := NewRESTClient(...)
|
||||
// if err != nil { ... }
|
||||
// resp, err := c.Verb("GET").
|
||||
// Path("pods").
|
||||
// SelectorParam("labels", "area=staging").
|
||||
// Timeout(10*time.Second).
|
||||
// Do()
|
||||
// if err != nil { ... }
|
||||
// list, ok := resp.(*api.PodList)
|
||||
//
|
||||
func (c *RESTClient) Verb(verb string) *Request {
|
||||
backoff := c.createBackoffMgr()
|
||||
|
||||
if c.Client == nil {
|
||||
return NewRequest(nil, verb, c.base, c.versionedAPIPath, c.contentConfig, c.serializers, backoff, c.Throttle)
|
||||
}
|
||||
return NewRequest(c.Client, verb, c.base, c.versionedAPIPath, c.contentConfig, c.serializers, backoff, c.Throttle)
|
||||
}
|
||||
|
||||
// Post begins a POST request. Short for c.Verb("POST").
|
||||
func (c *RESTClient) Post() *Request {
|
||||
return c.Verb("POST")
|
||||
}
|
||||
|
||||
// Put begins a PUT request. Short for c.Verb("PUT").
|
||||
func (c *RESTClient) Put() *Request {
|
||||
return c.Verb("PUT")
|
||||
}
|
||||
|
||||
// Patch begins a PATCH request. Short for c.Verb("Patch").
|
||||
func (c *RESTClient) Patch(pt types.PatchType) *Request {
|
||||
return c.Verb("PATCH").SetHeader("Content-Type", string(pt))
|
||||
}
|
||||
|
||||
// Get begins a GET request. Short for c.Verb("GET").
|
||||
func (c *RESTClient) Get() *Request {
|
||||
return c.Verb("GET")
|
||||
}
|
||||
|
||||
// Delete begins a DELETE request. Short for c.Verb("DELETE").
|
||||
func (c *RESTClient) Delete() *Request {
|
||||
return c.Verb("DELETE")
|
||||
}
|
||||
|
||||
// APIVersion returns the APIVersion this RESTClient is expected to use.
|
||||
func (c *RESTClient) APIVersion() schema.GroupVersion {
|
||||
return *c.contentConfig.GroupVersion
|
||||
}
|
@ -1,340 +0,0 @@
|
||||
/*
|
||||
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 restclient
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
|
||||
"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/types"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/testapi"
|
||||
utiltesting "k8s.io/kubernetes/pkg/util/testing"
|
||||
)
|
||||
|
||||
type TestParam struct {
|
||||
actualError error
|
||||
expectingError bool
|
||||
actualCreated bool
|
||||
expCreated bool
|
||||
expStatus *metav1.Status
|
||||
testBody bool
|
||||
testBodyErrorIsNotNil bool
|
||||
}
|
||||
|
||||
// TestSerializer makes sure that you're always able to decode an unversioned API object
|
||||
func TestSerializer(t *testing.T) {
|
||||
contentConfig := ContentConfig{
|
||||
ContentType: "application/json",
|
||||
GroupVersion: &schema.GroupVersion{Group: "other", Version: runtime.APIVersionInternal},
|
||||
NegotiatedSerializer: api.Codecs,
|
||||
}
|
||||
|
||||
serializer, err := createSerializers(contentConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// bytes based on actual return from API server when encoding an "unversioned" object
|
||||
obj, err := runtime.Decode(serializer.Decoder, []byte(`{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Success"}`))
|
||||
t.Log(obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoRequestSuccess(t *testing.T) {
|
||||
testServer, fakeHandler, status := testServerEnv(t, 200)
|
||||
defer testServer.Close()
|
||||
|
||||
c, err := restClient(testServer)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
body, err := c.Get().Prefix("test").Do().Raw()
|
||||
|
||||
testParam := TestParam{actualError: err, expectingError: false, expCreated: true,
|
||||
expStatus: status, testBody: true, testBodyErrorIsNotNil: false}
|
||||
validate(testParam, t, body, fakeHandler)
|
||||
}
|
||||
|
||||
func TestDoRequestFailed(t *testing.T) {
|
||||
status := &metav1.Status{
|
||||
Code: http.StatusNotFound,
|
||||
Status: metav1.StatusFailure,
|
||||
Reason: metav1.StatusReasonNotFound,
|
||||
Message: " \"\" not found",
|
||||
Details: &metav1.StatusDetails{},
|
||||
}
|
||||
expectedBody, _ := runtime.Encode(testapi.Default.Codec(), status)
|
||||
fakeHandler := utiltesting.FakeHandler{
|
||||
StatusCode: 404,
|
||||
ResponseBody: string(expectedBody),
|
||||
T: t,
|
||||
}
|
||||
testServer := httptest.NewServer(&fakeHandler)
|
||||
defer testServer.Close()
|
||||
|
||||
c, err := restClient(testServer)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
err = c.Get().Do().Error()
|
||||
if err == nil {
|
||||
t.Errorf("unexpected non-error")
|
||||
}
|
||||
ss, ok := err.(errors.APIStatus)
|
||||
if !ok {
|
||||
t.Errorf("unexpected error type %v", err)
|
||||
}
|
||||
actual := ss.Status()
|
||||
if !reflect.DeepEqual(status, &actual) {
|
||||
t.Errorf("Unexpected mis-match: %s", diff.ObjectReflectDiff(status, &actual))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoRawRequestFailed(t *testing.T) {
|
||||
status := &metav1.Status{
|
||||
Code: http.StatusNotFound,
|
||||
Status: metav1.StatusFailure,
|
||||
Reason: metav1.StatusReasonNotFound,
|
||||
Message: "the server could not find the requested resource",
|
||||
Details: &metav1.StatusDetails{
|
||||
Causes: []metav1.StatusCause{
|
||||
{Type: metav1.CauseTypeUnexpectedServerResponse, Message: "unknown"},
|
||||
},
|
||||
},
|
||||
}
|
||||
expectedBody, _ := runtime.Encode(testapi.Default.Codec(), status)
|
||||
fakeHandler := utiltesting.FakeHandler{
|
||||
StatusCode: 404,
|
||||
ResponseBody: string(expectedBody),
|
||||
T: t,
|
||||
}
|
||||
testServer := httptest.NewServer(&fakeHandler)
|
||||
defer testServer.Close()
|
||||
|
||||
c, err := restClient(testServer)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
body, err := c.Get().Do().Raw()
|
||||
|
||||
if err == nil || body == nil {
|
||||
t.Errorf("unexpected non-error: %#v", body)
|
||||
}
|
||||
ss, ok := err.(errors.APIStatus)
|
||||
if !ok {
|
||||
t.Errorf("unexpected error type %v", err)
|
||||
}
|
||||
actual := ss.Status()
|
||||
if !reflect.DeepEqual(status, &actual) {
|
||||
t.Errorf("Unexpected mis-match: %s", diff.ObjectReflectDiff(status, &actual))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoRequestCreated(t *testing.T) {
|
||||
testServer, fakeHandler, status := testServerEnv(t, 201)
|
||||
defer testServer.Close()
|
||||
|
||||
c, err := restClient(testServer)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
created := false
|
||||
body, err := c.Get().Prefix("test").Do().WasCreated(&created).Raw()
|
||||
|
||||
testParam := TestParam{actualError: err, expectingError: false, expCreated: true,
|
||||
expStatus: status, testBody: false}
|
||||
validate(testParam, t, body, fakeHandler)
|
||||
}
|
||||
|
||||
func TestDoRequestNotCreated(t *testing.T) {
|
||||
testServer, fakeHandler, expectedStatus := testServerEnv(t, 202)
|
||||
defer testServer.Close()
|
||||
c, err := restClient(testServer)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
created := false
|
||||
body, err := c.Get().Prefix("test").Do().WasCreated(&created).Raw()
|
||||
testParam := TestParam{actualError: err, expectingError: false, expCreated: false,
|
||||
expStatus: expectedStatus, testBody: false}
|
||||
validate(testParam, t, body, fakeHandler)
|
||||
}
|
||||
|
||||
func TestDoRequestAcceptedNoContentReturned(t *testing.T) {
|
||||
testServer, fakeHandler, _ := testServerEnv(t, 204)
|
||||
defer testServer.Close()
|
||||
|
||||
c, err := restClient(testServer)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
created := false
|
||||
body, err := c.Get().Prefix("test").Do().WasCreated(&created).Raw()
|
||||
testParam := TestParam{actualError: err, expectingError: false, expCreated: false,
|
||||
testBody: false}
|
||||
validate(testParam, t, body, fakeHandler)
|
||||
}
|
||||
|
||||
func TestBadRequest(t *testing.T) {
|
||||
testServer, fakeHandler, _ := testServerEnv(t, 400)
|
||||
defer testServer.Close()
|
||||
c, err := restClient(testServer)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
created := false
|
||||
body, err := c.Get().Prefix("test").Do().WasCreated(&created).Raw()
|
||||
testParam := TestParam{actualError: err, expectingError: true, expCreated: false,
|
||||
testBody: true}
|
||||
validate(testParam, t, body, fakeHandler)
|
||||
}
|
||||
|
||||
func validate(testParam TestParam, t *testing.T, body []byte, fakeHandler *utiltesting.FakeHandler) {
|
||||
switch {
|
||||
case testParam.expectingError && testParam.actualError == nil:
|
||||
t.Errorf("Expected error")
|
||||
case !testParam.expectingError && testParam.actualError != nil:
|
||||
t.Error(testParam.actualError)
|
||||
}
|
||||
if !testParam.expCreated {
|
||||
if testParam.actualCreated {
|
||||
t.Errorf("Expected object not to be created")
|
||||
}
|
||||
}
|
||||
statusOut, err := runtime.Decode(testapi.Default.Codec(), body)
|
||||
if testParam.testBody {
|
||||
if testParam.testBodyErrorIsNotNil {
|
||||
if err == nil {
|
||||
t.Errorf("Expected Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if testParam.expStatus != nil {
|
||||
if !reflect.DeepEqual(testParam.expStatus, statusOut) {
|
||||
t.Errorf("Unexpected mis-match. Expected %#v. Saw %#v", testParam.expStatus, statusOut)
|
||||
}
|
||||
}
|
||||
fakeHandler.ValidateRequest(t, "/"+api.Registry.GroupOrDie(api.GroupName).GroupVersion.String()+"/test", "GET", nil)
|
||||
|
||||
}
|
||||
|
||||
func TestHttpMethods(t *testing.T) {
|
||||
testServer, _, _ := testServerEnv(t, 200)
|
||||
defer testServer.Close()
|
||||
c, _ := restClient(testServer)
|
||||
|
||||
request := c.Post()
|
||||
if request == nil {
|
||||
t.Errorf("Post : Object returned should not be nil")
|
||||
}
|
||||
|
||||
request = c.Get()
|
||||
if request == nil {
|
||||
t.Errorf("Get: Object returned should not be nil")
|
||||
}
|
||||
|
||||
request = c.Put()
|
||||
if request == nil {
|
||||
t.Errorf("Put : Object returned should not be nil")
|
||||
}
|
||||
|
||||
request = c.Delete()
|
||||
if request == nil {
|
||||
t.Errorf("Delete : Object returned should not be nil")
|
||||
}
|
||||
|
||||
request = c.Patch(types.JSONPatchType)
|
||||
if request == nil {
|
||||
t.Errorf("Patch : Object returned should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateBackoffManager(t *testing.T) {
|
||||
|
||||
theUrl, _ := url.Parse("http://localhost")
|
||||
|
||||
// 1 second base backoff + duration of 2 seconds -> exponential backoff for requests.
|
||||
os.Setenv(envBackoffBase, "1")
|
||||
os.Setenv(envBackoffDuration, "2")
|
||||
backoff := readExpBackoffConfig()
|
||||
backoff.UpdateBackoff(theUrl, nil, 500)
|
||||
backoff.UpdateBackoff(theUrl, nil, 500)
|
||||
if backoff.CalculateBackoff(theUrl)/time.Second != 2 {
|
||||
t.Errorf("Backoff env not working.")
|
||||
}
|
||||
|
||||
// 0 duration -> no backoff.
|
||||
os.Setenv(envBackoffBase, "1")
|
||||
os.Setenv(envBackoffDuration, "0")
|
||||
backoff.UpdateBackoff(theUrl, nil, 500)
|
||||
backoff.UpdateBackoff(theUrl, nil, 500)
|
||||
backoff = readExpBackoffConfig()
|
||||
if backoff.CalculateBackoff(theUrl)/time.Second != 0 {
|
||||
t.Errorf("Zero backoff duration, but backoff still occuring.")
|
||||
}
|
||||
|
||||
// No env -> No backoff.
|
||||
os.Setenv(envBackoffBase, "")
|
||||
os.Setenv(envBackoffDuration, "")
|
||||
backoff = readExpBackoffConfig()
|
||||
backoff.UpdateBackoff(theUrl, nil, 500)
|
||||
backoff.UpdateBackoff(theUrl, nil, 500)
|
||||
if backoff.CalculateBackoff(theUrl)/time.Second != 0 {
|
||||
t.Errorf("Backoff should have been 0.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testServerEnv(t *testing.T, statusCode int) (*httptest.Server, *utiltesting.FakeHandler, *metav1.Status) {
|
||||
status := &metav1.Status{Status: fmt.Sprintf("%s", metav1.StatusSuccess)}
|
||||
expectedBody, _ := runtime.Encode(testapi.Default.Codec(), status)
|
||||
fakeHandler := utiltesting.FakeHandler{
|
||||
StatusCode: statusCode,
|
||||
ResponseBody: string(expectedBody),
|
||||
T: t,
|
||||
}
|
||||
testServer := httptest.NewServer(&fakeHandler)
|
||||
return testServer, &fakeHandler, status
|
||||
}
|
||||
|
||||
func restClient(testServer *httptest.Server) (*RESTClient, error) {
|
||||
c, err := RESTClientFor(&Config{
|
||||
Host: testServer.URL,
|
||||
ContentConfig: ContentConfig{
|
||||
GroupVersion: &api.Registry.GroupOrDie(api.GroupName).GroupVersion,
|
||||
NegotiatedSerializer: testapi.Default.NegotiatedSerializer(),
|
||||
},
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
})
|
||||
return c, err
|
||||
}
|
@ -1,380 +0,0 @@
|
||||
/*
|
||||
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 restclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
gruntime "runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/pkg/util/flowcontrol"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
certutil "k8s.io/kubernetes/pkg/util/cert"
|
||||
"k8s.io/kubernetes/pkg/version"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultQPS float32 = 5.0
|
||||
DefaultBurst int = 10
|
||||
)
|
||||
|
||||
// Config holds the common attributes that can be passed to a Kubernetes client on
|
||||
// initialization.
|
||||
type Config struct {
|
||||
// Host must be a host string, a host:port pair, or a URL to the base of the apiserver.
|
||||
// If a URL is given then the (optional) Path of that URL represents a prefix that must
|
||||
// be appended to all request URIs used to access the apiserver. This allows a frontend
|
||||
// proxy to easily relocate all of the apiserver endpoints.
|
||||
Host string
|
||||
// APIPath is a sub-path that points to an API root.
|
||||
APIPath string
|
||||
// Prefix is the sub path of the server. If not specified, the client will set
|
||||
// a default value. Use "/" to indicate the server root should be used
|
||||
Prefix string
|
||||
|
||||
// ContentConfig contains settings that affect how objects are transformed when
|
||||
// sent to the server.
|
||||
ContentConfig
|
||||
|
||||
// Server requires Basic authentication
|
||||
Username string
|
||||
Password string
|
||||
|
||||
// Server requires Bearer authentication. This client will not attempt to use
|
||||
// refresh tokens for an OAuth2 flow.
|
||||
// TODO: demonstrate an OAuth2 compatible client.
|
||||
BearerToken string
|
||||
|
||||
// Impersonate is the configuration that RESTClient will use for impersonation.
|
||||
Impersonate ImpersonationConfig
|
||||
|
||||
// Server requires plugin-specified authentication.
|
||||
AuthProvider *clientcmdapi.AuthProviderConfig
|
||||
|
||||
// Callback to persist config for AuthProvider.
|
||||
AuthConfigPersister AuthProviderConfigPersister
|
||||
|
||||
// TLSClientConfig contains settings to enable transport layer security
|
||||
TLSClientConfig
|
||||
|
||||
// Server should be accessed without verifying the TLS
|
||||
// certificate. For testing only.
|
||||
Insecure bool
|
||||
|
||||
// UserAgent is an optional field that specifies the caller of this request.
|
||||
UserAgent string
|
||||
|
||||
// Transport may be used for custom HTTP behavior. This attribute may not
|
||||
// be specified with the TLS client certificate options. Use WrapTransport
|
||||
// for most client level operations.
|
||||
Transport http.RoundTripper
|
||||
// WrapTransport will be invoked for custom HTTP behavior after the underlying
|
||||
// transport is initialized (either the transport created from TLSClientConfig,
|
||||
// Transport, or http.DefaultTransport). The config may layer other RoundTrippers
|
||||
// on top of the returned RoundTripper.
|
||||
WrapTransport func(rt http.RoundTripper) http.RoundTripper
|
||||
|
||||
// QPS indicates the maximum QPS to the master from this client.
|
||||
// If it's zero, the created RESTClient will use DefaultQPS: 5
|
||||
QPS float32
|
||||
|
||||
// Maximum burst for throttle.
|
||||
// If it's zero, the created RESTClient will use DefaultBurst: 10.
|
||||
Burst int
|
||||
|
||||
// Rate limiter for limiting connections to the master from this client. If present overwrites QPS/Burst
|
||||
RateLimiter flowcontrol.RateLimiter
|
||||
|
||||
// The maximum length of time to wait before giving up on a server request. A value of zero means no timeout.
|
||||
Timeout time.Duration
|
||||
|
||||
// Version forces a specific version to be used (if registered)
|
||||
// Do we need this?
|
||||
// Version string
|
||||
}
|
||||
|
||||
// ImpersonationConfig has all the available impersonation options
|
||||
type ImpersonationConfig struct {
|
||||
// UserName is the username to impersonate on each request.
|
||||
UserName string
|
||||
// Groups are the groups to impersonate on each request.
|
||||
Groups []string
|
||||
// Extra is a free-form field which can be used to link some authentication information
|
||||
// to authorization information. This field allows you to impersonate it.
|
||||
Extra map[string][]string
|
||||
}
|
||||
|
||||
// TLSClientConfig contains settings to enable transport layer security
|
||||
type TLSClientConfig struct {
|
||||
// Server requires TLS client certificate authentication
|
||||
CertFile string
|
||||
// Server requires TLS client certificate authentication
|
||||
KeyFile string
|
||||
// Trusted root certificates for server
|
||||
CAFile string
|
||||
|
||||
// CertData holds PEM-encoded bytes (typically read from a client certificate file).
|
||||
// CertData takes precedence over CertFile
|
||||
CertData []byte
|
||||
// KeyData holds PEM-encoded bytes (typically read from a client certificate key file).
|
||||
// KeyData takes precedence over KeyFile
|
||||
KeyData []byte
|
||||
// CAData holds PEM-encoded bytes (typically read from a root certificates bundle).
|
||||
// CAData takes precedence over CAFile
|
||||
CAData []byte
|
||||
}
|
||||
|
||||
type ContentConfig struct {
|
||||
// AcceptContentTypes specifies the types the client will accept and is optional.
|
||||
// If not set, ContentType will be used to define the Accept header
|
||||
AcceptContentTypes string
|
||||
// ContentType specifies the wire format used to communicate with the server.
|
||||
// This value will be set as the Accept header on requests made to the server, and
|
||||
// as the default content type on any object sent to the server. If not set,
|
||||
// "application/json" is used.
|
||||
ContentType string
|
||||
// GroupVersion is the API version to talk to. Must be provided when initializing
|
||||
// a RESTClient directly. When initializing a Client, will be set with the default
|
||||
// code version.
|
||||
GroupVersion *schema.GroupVersion
|
||||
// NegotiatedSerializer is used for obtaining encoders and decoders for multiple
|
||||
// supported media types.
|
||||
NegotiatedSerializer runtime.NegotiatedSerializer
|
||||
}
|
||||
|
||||
// RESTClientFor returns a RESTClient that satisfies the requested attributes on a client Config
|
||||
// object. Note that a RESTClient may require fields that are optional when initializing a Client.
|
||||
// A RESTClient created by this method is generic - it expects to operate on an API that follows
|
||||
// the Kubernetes conventions, but may not be the Kubernetes API.
|
||||
func RESTClientFor(config *Config) (*RESTClient, error) {
|
||||
if config.GroupVersion == nil {
|
||||
return nil, fmt.Errorf("GroupVersion is required when initializing a RESTClient")
|
||||
}
|
||||
if config.NegotiatedSerializer == nil {
|
||||
return nil, fmt.Errorf("NegotiatedSerializer is required when initializing a RESTClient")
|
||||
}
|
||||
qps := config.QPS
|
||||
if config.QPS == 0.0 {
|
||||
qps = DefaultQPS
|
||||
}
|
||||
burst := config.Burst
|
||||
if config.Burst == 0 {
|
||||
burst = DefaultBurst
|
||||
}
|
||||
|
||||
baseURL, versionedAPIPath, err := defaultServerUrlFor(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport, err := TransportFor(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var httpClient *http.Client
|
||||
if transport != http.DefaultTransport {
|
||||
httpClient = &http.Client{Transport: transport}
|
||||
if config.Timeout > 0 {
|
||||
httpClient.Timeout = config.Timeout
|
||||
}
|
||||
}
|
||||
|
||||
return NewRESTClient(baseURL, versionedAPIPath, config.ContentConfig, qps, burst, config.RateLimiter, httpClient)
|
||||
}
|
||||
|
||||
// UnversionedRESTClientFor is the same as RESTClientFor, except that it allows
|
||||
// the config.Version to be empty.
|
||||
func UnversionedRESTClientFor(config *Config) (*RESTClient, error) {
|
||||
if config.NegotiatedSerializer == nil {
|
||||
return nil, fmt.Errorf("NeogitatedSerializer is required when initializing a RESTClient")
|
||||
}
|
||||
|
||||
baseURL, versionedAPIPath, err := defaultServerUrlFor(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport, err := TransportFor(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var httpClient *http.Client
|
||||
if transport != http.DefaultTransport {
|
||||
httpClient = &http.Client{Transport: transport}
|
||||
if config.Timeout > 0 {
|
||||
httpClient.Timeout = config.Timeout
|
||||
}
|
||||
}
|
||||
|
||||
versionConfig := config.ContentConfig
|
||||
if versionConfig.GroupVersion == nil {
|
||||
v := metav1.SchemeGroupVersion
|
||||
versionConfig.GroupVersion = &v
|
||||
}
|
||||
|
||||
return NewRESTClient(baseURL, versionedAPIPath, versionConfig, config.QPS, config.Burst, config.RateLimiter, httpClient)
|
||||
}
|
||||
|
||||
// SetKubernetesDefaults sets default values on the provided client config for accessing the
|
||||
// Kubernetes API or returns an error if any of the defaults are impossible or invalid.
|
||||
func SetKubernetesDefaults(config *Config) error {
|
||||
if len(config.UserAgent) == 0 {
|
||||
config.UserAgent = DefaultKubernetesUserAgent()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultKubernetesUserAgent returns the default user agent that clients can use.
|
||||
func DefaultKubernetesUserAgent() string {
|
||||
commit := version.Get().GitCommit
|
||||
if len(commit) > 7 {
|
||||
commit = commit[:7]
|
||||
}
|
||||
if len(commit) == 0 {
|
||||
commit = "unknown"
|
||||
}
|
||||
version := version.Get().GitVersion
|
||||
seg := strings.SplitN(version, "-", 2)
|
||||
version = seg[0]
|
||||
return fmt.Sprintf("%s/%s (%s/%s) kubernetes/%s", path.Base(os.Args[0]), version, gruntime.GOOS, gruntime.GOARCH, commit)
|
||||
}
|
||||
|
||||
// InClusterConfig returns a config object which uses the service account
|
||||
// kubernetes gives to pods. It's intended for clients that expect to be
|
||||
// running inside a pod running on kubernetes. It will return an error if
|
||||
// called from a process not running in a kubernetes environment.
|
||||
func InClusterConfig() (*Config, error) {
|
||||
host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
|
||||
if len(host) == 0 || len(port) == 0 {
|
||||
return nil, fmt.Errorf("unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined")
|
||||
}
|
||||
|
||||
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/" + api.ServiceAccountTokenKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsClientConfig := TLSClientConfig{}
|
||||
rootCAFile := "/var/run/secrets/kubernetes.io/serviceaccount/" + api.ServiceAccountRootCAKey
|
||||
if _, err := certutil.NewPool(rootCAFile); err != nil {
|
||||
glog.Errorf("Expected to load root CA config from %s, but got err: %v", rootCAFile, err)
|
||||
} else {
|
||||
tlsClientConfig.CAFile = rootCAFile
|
||||
}
|
||||
|
||||
return &Config{
|
||||
// TODO: switch to using cluster DNS.
|
||||
Host: "https://" + net.JoinHostPort(host, port),
|
||||
BearerToken: string(token),
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsConfigTransportTLS returns true if and only if the provided
|
||||
// config will result in a protected connection to the server when it
|
||||
// is passed to restclient.RESTClientFor(). Use to determine when to
|
||||
// send credentials over the wire.
|
||||
//
|
||||
// Note: the Insecure flag is ignored when testing for this value, so MITM attacks are
|
||||
// still possible.
|
||||
func IsConfigTransportTLS(config Config) bool {
|
||||
baseURL, _, err := defaultServerUrlFor(&config)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return baseURL.Scheme == "https"
|
||||
}
|
||||
|
||||
// LoadTLSFiles copies the data from the CertFile, KeyFile, and CAFile fields into the CertData,
|
||||
// KeyData, and CAFile fields, or returns an error. If no error is returned, all three fields are
|
||||
// either populated or were empty to start.
|
||||
func LoadTLSFiles(c *Config) error {
|
||||
var err error
|
||||
c.CAData, err = dataFromSliceOrFile(c.CAData, c.CAFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.CertData, err = dataFromSliceOrFile(c.CertData, c.CertFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.KeyData, err = dataFromSliceOrFile(c.KeyData, c.KeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dataFromSliceOrFile returns data from the slice (if non-empty), or from the file,
|
||||
// or an error if an error occurred reading the file
|
||||
func dataFromSliceOrFile(data []byte, file string) ([]byte, error) {
|
||||
if len(data) > 0 {
|
||||
return data, nil
|
||||
}
|
||||
if len(file) > 0 {
|
||||
fileData, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return fileData, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func AddUserAgent(config *Config, userAgent string) *Config {
|
||||
fullUserAgent := DefaultKubernetesUserAgent() + "/" + userAgent
|
||||
config.UserAgent = fullUserAgent
|
||||
return config
|
||||
}
|
||||
|
||||
// AnonymousClientConfig returns a copy of the given config with all user credentials (cert/key, bearer token, and username/password) removed
|
||||
func AnonymousClientConfig(config *Config) *Config {
|
||||
// copy only known safe fields
|
||||
return &Config{
|
||||
Host: config.Host,
|
||||
APIPath: config.APIPath,
|
||||
Prefix: config.Prefix,
|
||||
ContentConfig: config.ContentConfig,
|
||||
TLSClientConfig: TLSClientConfig{
|
||||
CAFile: config.TLSClientConfig.CAFile,
|
||||
CAData: config.TLSClientConfig.CAData,
|
||||
},
|
||||
RateLimiter: config.RateLimiter,
|
||||
Insecure: config.Insecure,
|
||||
UserAgent: config.UserAgent,
|
||||
Transport: config.Transport,
|
||||
WrapTransport: config.WrapTransport,
|
||||
QPS: config.QPS,
|
||||
Burst: config.Burst,
|
||||
Timeout: config.Timeout,
|
||||
}
|
||||
}
|
@ -1,231 +0,0 @@
|
||||
/*
|
||||
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 restclient
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
fuzz "github.com/google/gofuzz"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/client-go/pkg/util/flowcontrol"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/testapi"
|
||||
)
|
||||
|
||||
func TestIsConfigTransportTLS(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Config *Config
|
||||
TransportTLS bool
|
||||
}{
|
||||
{
|
||||
Config: &Config{},
|
||||
TransportTLS: false,
|
||||
},
|
||||
{
|
||||
Config: &Config{
|
||||
Host: "https://localhost",
|
||||
},
|
||||
TransportTLS: true,
|
||||
},
|
||||
{
|
||||
Config: &Config{
|
||||
Host: "localhost",
|
||||
TLSClientConfig: TLSClientConfig{
|
||||
CertFile: "foo",
|
||||
},
|
||||
},
|
||||
TransportTLS: true,
|
||||
},
|
||||
{
|
||||
Config: &Config{
|
||||
Host: "///:://localhost",
|
||||
TLSClientConfig: TLSClientConfig{
|
||||
CertFile: "foo",
|
||||
},
|
||||
},
|
||||
TransportTLS: false,
|
||||
},
|
||||
{
|
||||
Config: &Config{
|
||||
Host: "1.2.3.4:567",
|
||||
Insecure: true,
|
||||
},
|
||||
TransportTLS: true,
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
if err := SetKubernetesDefaults(testCase.Config); err != nil {
|
||||
t.Errorf("setting defaults failed for %#v: %v", testCase.Config, err)
|
||||
continue
|
||||
}
|
||||
useTLS := IsConfigTransportTLS(*testCase.Config)
|
||||
if testCase.TransportTLS != useTLS {
|
||||
t.Errorf("expected %v for %#v", testCase.TransportTLS, testCase.Config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetKubernetesDefaultsUserAgent(t *testing.T) {
|
||||
config := &Config{}
|
||||
if err := SetKubernetesDefaults(config); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(config.UserAgent, "kubernetes/") {
|
||||
t.Errorf("no user agent set: %#v", config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRESTClientRequires(t *testing.T) {
|
||||
if _, err := RESTClientFor(&Config{Host: "127.0.0.1", ContentConfig: ContentConfig{NegotiatedSerializer: testapi.Default.NegotiatedSerializer()}}); err == nil {
|
||||
t.Errorf("unexpected non-error")
|
||||
}
|
||||
if _, err := RESTClientFor(&Config{Host: "127.0.0.1", ContentConfig: ContentConfig{GroupVersion: &api.Registry.GroupOrDie(api.GroupName).GroupVersion}}); err == nil {
|
||||
t.Errorf("unexpected non-error")
|
||||
}
|
||||
if _, err := RESTClientFor(&Config{Host: "127.0.0.1", ContentConfig: ContentConfig{GroupVersion: &api.Registry.GroupOrDie(api.GroupName).GroupVersion, NegotiatedSerializer: testapi.Default.NegotiatedSerializer()}}); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeLimiter struct {
|
||||
FakeSaturation float64
|
||||
FakeQPS float32
|
||||
}
|
||||
|
||||
func (t *fakeLimiter) TryAccept() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *fakeLimiter) Saturation() float64 {
|
||||
return t.FakeSaturation
|
||||
}
|
||||
|
||||
func (t *fakeLimiter) QPS() float32 {
|
||||
return t.FakeQPS
|
||||
}
|
||||
|
||||
func (t *fakeLimiter) Stop() {}
|
||||
|
||||
func (t *fakeLimiter) Accept() {}
|
||||
|
||||
type fakeCodec struct{}
|
||||
|
||||
func (c *fakeCodec) Decode([]byte, *schema.GroupVersionKind, runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (c *fakeCodec) Encode(obj runtime.Object, stream io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeRoundTripper struct{}
|
||||
|
||||
func (r *fakeRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var fakeWrapperFunc = func(http.RoundTripper) http.RoundTripper {
|
||||
return &fakeRoundTripper{}
|
||||
}
|
||||
|
||||
type fakeNegotiatedSerializer struct{}
|
||||
|
||||
func (n *fakeNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeNegotiatedSerializer) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
|
||||
return &fakeCodec{}
|
||||
}
|
||||
|
||||
func (n *fakeNegotiatedSerializer) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
|
||||
return &fakeCodec{}
|
||||
}
|
||||
|
||||
func TestAnonymousConfig(t *testing.T) {
|
||||
f := fuzz.New().NilChance(0.0).NumElements(1, 1)
|
||||
f.Funcs(
|
||||
func(r *runtime.Codec, f fuzz.Continue) {
|
||||
codec := &fakeCodec{}
|
||||
f.Fuzz(codec)
|
||||
*r = codec
|
||||
},
|
||||
func(r *http.RoundTripper, f fuzz.Continue) {
|
||||
roundTripper := &fakeRoundTripper{}
|
||||
f.Fuzz(roundTripper)
|
||||
*r = roundTripper
|
||||
},
|
||||
func(fn *func(http.RoundTripper) http.RoundTripper, f fuzz.Continue) {
|
||||
*fn = fakeWrapperFunc
|
||||
},
|
||||
func(r *runtime.NegotiatedSerializer, f fuzz.Continue) {
|
||||
serializer := &fakeNegotiatedSerializer{}
|
||||
f.Fuzz(serializer)
|
||||
*r = serializer
|
||||
},
|
||||
func(r *flowcontrol.RateLimiter, f fuzz.Continue) {
|
||||
limiter := &fakeLimiter{}
|
||||
f.Fuzz(limiter)
|
||||
*r = limiter
|
||||
},
|
||||
// Authentication does not require fuzzer
|
||||
func(r *AuthProviderConfigPersister, f fuzz.Continue) {},
|
||||
func(r *clientcmdapi.AuthProviderConfig, f fuzz.Continue) {
|
||||
r.Config = map[string]string{}
|
||||
},
|
||||
)
|
||||
for i := 0; i < 20; i++ {
|
||||
original := &Config{}
|
||||
f.Fuzz(original)
|
||||
actual := AnonymousClientConfig(original)
|
||||
expected := *original
|
||||
|
||||
// this is the list of known security related fields, add to this list if a new field
|
||||
// is added to Config, update AnonymousClientConfig to preserve the field otherwise.
|
||||
expected.Impersonate = ImpersonationConfig{}
|
||||
expected.BearerToken = ""
|
||||
expected.Username = ""
|
||||
expected.Password = ""
|
||||
expected.AuthProvider = nil
|
||||
expected.AuthConfigPersister = nil
|
||||
expected.TLSClientConfig.CertData = nil
|
||||
expected.TLSClientConfig.CertFile = ""
|
||||
expected.TLSClientConfig.KeyData = nil
|
||||
expected.TLSClientConfig.KeyFile = ""
|
||||
|
||||
// The DeepEqual cannot handle the func comparison, so we just verify if the
|
||||
// function return the expected object.
|
||||
if actual.WrapTransport == nil || !reflect.DeepEqual(expected.WrapTransport(nil), &fakeRoundTripper{}) {
|
||||
t.Fatalf("AnonymousClientConfig dropped the WrapTransport field")
|
||||
} else {
|
||||
actual.WrapTransport = nil
|
||||
expected.WrapTransport = nil
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(*actual, expected) {
|
||||
t.Fatalf("AnonymousClientConfig dropped unexpected fields, identify whether they are security related or not: %s", diff.ObjectGoPrintDiff(expected, actual))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
/*
|
||||
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 restclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type AuthProvider interface {
|
||||
// WrapTransport allows the plugin to create a modified RoundTripper that
|
||||
// attaches authorization headers (or other info) to requests.
|
||||
WrapTransport(http.RoundTripper) http.RoundTripper
|
||||
// Login allows the plugin to initialize its configuration. It must not
|
||||
// require direct user interaction.
|
||||
Login() error
|
||||
}
|
||||
|
||||
// Factory generates an AuthProvider plugin.
|
||||
// clusterAddress is the address of the current cluster.
|
||||
// config is the initial configuration for this plugin.
|
||||
// persister allows the plugin to save updated configuration.
|
||||
type Factory func(clusterAddress string, config map[string]string, persister AuthProviderConfigPersister) (AuthProvider, error)
|
||||
|
||||
// AuthProviderConfigPersister allows a plugin to persist configuration info
|
||||
// for just itself.
|
||||
type AuthProviderConfigPersister interface {
|
||||
Persist(map[string]string) error
|
||||
}
|
||||
|
||||
// All registered auth provider plugins.
|
||||
var pluginsLock sync.Mutex
|
||||
var plugins = make(map[string]Factory)
|
||||
|
||||
func RegisterAuthProviderPlugin(name string, plugin Factory) error {
|
||||
pluginsLock.Lock()
|
||||
defer pluginsLock.Unlock()
|
||||
if _, found := plugins[name]; found {
|
||||
return fmt.Errorf("Auth Provider Plugin %q was registered twice", name)
|
||||
}
|
||||
glog.V(4).Infof("Registered Auth Provider Plugin %q", name)
|
||||
plugins[name] = plugin
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAuthProvider(clusterAddress string, apc *clientcmdapi.AuthProviderConfig, persister AuthProviderConfigPersister) (AuthProvider, error) {
|
||||
pluginsLock.Lock()
|
||||
defer pluginsLock.Unlock()
|
||||
p, ok := plugins[apc.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("No Auth Provider found for name %q", apc.Name)
|
||||
}
|
||||
return p(clusterAddress, apc.Config, persister)
|
||||
}
|
@ -1,311 +0,0 @@
|
||||
/*
|
||||
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 restclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
func TestAuthPluginWrapTransport(t *testing.T) {
|
||||
if err := RegisterAuthProviderPlugin("pluginA", pluginAProvider); err != nil {
|
||||
t.Errorf("Unexpected error: failed to register pluginA: %v", err)
|
||||
}
|
||||
if err := RegisterAuthProviderPlugin("pluginB", pluginBProvider); err != nil {
|
||||
t.Errorf("Unexpected error: failed to register pluginB: %v", err)
|
||||
}
|
||||
if err := RegisterAuthProviderPlugin("pluginFail", pluginFailProvider); err != nil {
|
||||
t.Errorf("Unexpected error: failed to register pluginFail: %v", err)
|
||||
}
|
||||
testCases := []struct {
|
||||
useWrapTransport bool
|
||||
plugin string
|
||||
expectErr bool
|
||||
expectPluginA bool
|
||||
expectPluginB bool
|
||||
}{
|
||||
{false, "", false, false, false},
|
||||
{false, "pluginA", false, true, false},
|
||||
{false, "pluginB", false, false, true},
|
||||
{false, "pluginFail", true, false, false},
|
||||
{false, "pluginUnknown", true, false, false},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
c := Config{}
|
||||
if tc.useWrapTransport {
|
||||
// Specify an existing WrapTransport in the config to make sure that
|
||||
// plugins play nicely.
|
||||
c.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
|
||||
return &wrapTransport{rt}
|
||||
}
|
||||
}
|
||||
if len(tc.plugin) != 0 {
|
||||
c.AuthProvider = &clientcmdapi.AuthProviderConfig{Name: tc.plugin}
|
||||
}
|
||||
tConfig, err := c.TransportConfig()
|
||||
if err != nil {
|
||||
// Unknown/bad plugins are expected to fail here.
|
||||
if !tc.expectErr {
|
||||
t.Errorf("%d. Did not expect errors loading Auth Plugin: %q. Got: %v", i, tc.plugin, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
var fullyWrappedTransport http.RoundTripper
|
||||
fullyWrappedTransport = &emptyTransport{}
|
||||
if tConfig.WrapTransport != nil {
|
||||
fullyWrappedTransport = tConfig.WrapTransport(&emptyTransport{})
|
||||
}
|
||||
res, err := fullyWrappedTransport.RoundTrip(&http.Request{})
|
||||
if err != nil {
|
||||
t.Errorf("%d. Unexpected error in RoundTrip: %v", i, err)
|
||||
continue
|
||||
}
|
||||
hasWrapTransport := res.Header.Get("wrapTransport") == "Y"
|
||||
hasPluginA := res.Header.Get("pluginA") == "Y"
|
||||
hasPluginB := res.Header.Get("pluginB") == "Y"
|
||||
if hasWrapTransport != tc.useWrapTransport {
|
||||
t.Errorf("%d. Expected Existing config.WrapTransport: %t; Got: %t", i, tc.useWrapTransport, hasWrapTransport)
|
||||
}
|
||||
if hasPluginA != tc.expectPluginA {
|
||||
t.Errorf("%d. Expected Plugin A: %t; Got: %t", i, tc.expectPluginA, hasPluginA)
|
||||
}
|
||||
if hasPluginB != tc.expectPluginB {
|
||||
t.Errorf("%d. Expected Plugin B: %t; Got: %t", i, tc.expectPluginB, hasPluginB)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthPluginPersist(t *testing.T) {
|
||||
// register pluginA by a different name so we don't collide across tests.
|
||||
if err := RegisterAuthProviderPlugin("pluginA2", pluginAProvider); err != nil {
|
||||
t.Errorf("Unexpected error: failed to register pluginA: %v", err)
|
||||
}
|
||||
if err := RegisterAuthProviderPlugin("pluginPersist", pluginPersistProvider); err != nil {
|
||||
t.Errorf("Unexpected error: failed to register pluginPersist: %v", err)
|
||||
}
|
||||
fooBarConfig := map[string]string{"foo": "bar"}
|
||||
testCases := []struct {
|
||||
plugin string
|
||||
startingConfig map[string]string
|
||||
expectedConfigAfterLogin map[string]string
|
||||
expectedConfigAfterRoundTrip map[string]string
|
||||
}{
|
||||
// non-persisting plugins should work fine without modifying config.
|
||||
{"pluginA2", map[string]string{}, map[string]string{}, map[string]string{}},
|
||||
{"pluginA2", fooBarConfig, fooBarConfig, fooBarConfig},
|
||||
// plugins that persist config should be able to persist when they want.
|
||||
{
|
||||
"pluginPersist",
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
"login": "Y",
|
||||
},
|
||||
map[string]string{
|
||||
"login": "Y",
|
||||
"roundTrips": "1",
|
||||
},
|
||||
},
|
||||
{
|
||||
"pluginPersist",
|
||||
map[string]string{
|
||||
"login": "Y",
|
||||
"roundTrips": "123",
|
||||
},
|
||||
map[string]string{
|
||||
"login": "Y",
|
||||
"roundTrips": "123",
|
||||
},
|
||||
map[string]string{
|
||||
"login": "Y",
|
||||
"roundTrips": "124",
|
||||
},
|
||||
},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
cfg := &clientcmdapi.AuthProviderConfig{
|
||||
Name: tc.plugin,
|
||||
Config: tc.startingConfig,
|
||||
}
|
||||
persister := &inMemoryPersister{make(map[string]string)}
|
||||
persister.Persist(tc.startingConfig)
|
||||
plugin, err := GetAuthProvider("127.0.0.1", cfg, persister)
|
||||
if err != nil {
|
||||
t.Errorf("%d. Unexpected error: failed to get plugin %q: %v", i, tc.plugin, err)
|
||||
}
|
||||
if err := plugin.Login(); err != nil {
|
||||
t.Errorf("%d. Unexpected error calling Login() w/ plugin %q: %v", i, tc.plugin, err)
|
||||
}
|
||||
// Make sure the plugin persisted what we expect after Login().
|
||||
if !reflect.DeepEqual(persister.savedConfig, tc.expectedConfigAfterLogin) {
|
||||
t.Errorf("%d. Unexpected persisted config after calling %s.Login(): \nGot:\n%v\nExpected:\n%v",
|
||||
i, tc.plugin, persister.savedConfig, tc.expectedConfigAfterLogin)
|
||||
}
|
||||
if _, err := plugin.WrapTransport(&emptyTransport{}).RoundTrip(&http.Request{}); err != nil {
|
||||
t.Errorf("%d. Unexpected error round-tripping w/ plugin %q: %v", i, tc.plugin, err)
|
||||
}
|
||||
// Make sure the plugin persisted what we expect after RoundTrip().
|
||||
if !reflect.DeepEqual(persister.savedConfig, tc.expectedConfigAfterRoundTrip) {
|
||||
t.Errorf("%d. Unexpected persisted config after calling %s.WrapTransport.RoundTrip(): \nGot:\n%v\nExpected:\n%v",
|
||||
i, tc.plugin, persister.savedConfig, tc.expectedConfigAfterLogin)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// emptyTransport provides an empty http.Response with an initialized header
|
||||
// to allow wrapping RoundTrippers to set header values.
|
||||
type emptyTransport struct{}
|
||||
|
||||
func (*emptyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
res := &http.Response{
|
||||
Header: make(map[string][]string),
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// wrapTransport sets "wrapTransport" = "Y" on the response.
|
||||
type wrapTransport struct {
|
||||
rt http.RoundTripper
|
||||
}
|
||||
|
||||
func (w *wrapTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
res, err := w.rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Header.Add("wrapTransport", "Y")
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// wrapTransportA sets "pluginA" = "Y" on the response.
|
||||
type wrapTransportA struct {
|
||||
rt http.RoundTripper
|
||||
}
|
||||
|
||||
func (w *wrapTransportA) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
res, err := w.rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Header.Add("pluginA", "Y")
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type pluginA struct{}
|
||||
|
||||
func (*pluginA) WrapTransport(rt http.RoundTripper) http.RoundTripper {
|
||||
return &wrapTransportA{rt}
|
||||
}
|
||||
|
||||
func (*pluginA) Login() error { return nil }
|
||||
|
||||
func pluginAProvider(string, map[string]string, AuthProviderConfigPersister) (AuthProvider, error) {
|
||||
return &pluginA{}, nil
|
||||
}
|
||||
|
||||
// wrapTransportB sets "pluginB" = "Y" on the response.
|
||||
type wrapTransportB struct {
|
||||
rt http.RoundTripper
|
||||
}
|
||||
|
||||
func (w *wrapTransportB) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
res, err := w.rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Header.Add("pluginB", "Y")
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type pluginB struct{}
|
||||
|
||||
func (*pluginB) WrapTransport(rt http.RoundTripper) http.RoundTripper {
|
||||
return &wrapTransportB{rt}
|
||||
}
|
||||
|
||||
func (*pluginB) Login() error { return nil }
|
||||
|
||||
func pluginBProvider(string, map[string]string, AuthProviderConfigPersister) (AuthProvider, error) {
|
||||
return &pluginB{}, nil
|
||||
}
|
||||
|
||||
// pluginFailProvider simulates a registered AuthPlugin that fails to load.
|
||||
func pluginFailProvider(string, map[string]string, AuthProviderConfigPersister) (AuthProvider, error) {
|
||||
return nil, fmt.Errorf("Failed to load AuthProvider")
|
||||
}
|
||||
|
||||
type inMemoryPersister struct {
|
||||
savedConfig map[string]string
|
||||
}
|
||||
|
||||
func (i *inMemoryPersister) Persist(config map[string]string) error {
|
||||
i.savedConfig = make(map[string]string)
|
||||
for k, v := range config {
|
||||
i.savedConfig[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapTransportPersist increments the "roundTrips" entry from the config when
|
||||
// roundTrip is called.
|
||||
type wrapTransportPersist struct {
|
||||
rt http.RoundTripper
|
||||
config map[string]string
|
||||
persister AuthProviderConfigPersister
|
||||
}
|
||||
|
||||
func (w *wrapTransportPersist) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
roundTrips := 0
|
||||
if rtVal, ok := w.config["roundTrips"]; ok {
|
||||
var err error
|
||||
roundTrips, err = strconv.Atoi(rtVal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
roundTrips++
|
||||
w.config["roundTrips"] = fmt.Sprintf("%d", roundTrips)
|
||||
if err := w.persister.Persist(w.config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return w.rt.RoundTrip(req)
|
||||
}
|
||||
|
||||
type pluginPersist struct {
|
||||
config map[string]string
|
||||
persister AuthProviderConfigPersister
|
||||
}
|
||||
|
||||
func (p *pluginPersist) WrapTransport(rt http.RoundTripper) http.RoundTripper {
|
||||
return &wrapTransportPersist{rt, p.config, p.persister}
|
||||
}
|
||||
|
||||
// Login sets the config entry "login" to "Y".
|
||||
func (p *pluginPersist) Login() error {
|
||||
p.config["login"] = "Y"
|
||||
p.persister.Persist(p.config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func pluginPersistProvider(_ string, config map[string]string, persister AuthProviderConfigPersister) (AuthProvider, error) {
|
||||
return &pluginPersist{config, persister}, nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,98 +0,0 @@
|
||||
/*
|
||||
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 restclient
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/client-go/transport"
|
||||
)
|
||||
|
||||
// TLSConfigFor returns a tls.Config that will provide the transport level security defined
|
||||
// by the provided Config. Will return nil if no transport level security is requested.
|
||||
func TLSConfigFor(config *Config) (*tls.Config, error) {
|
||||
cfg, err := config.TransportConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transport.TLSConfigFor(cfg)
|
||||
}
|
||||
|
||||
// TransportFor returns an http.RoundTripper that will provide the authentication
|
||||
// or transport level security defined by the provided Config. Will return the
|
||||
// default http.DefaultTransport if no special case behavior is needed.
|
||||
func TransportFor(config *Config) (http.RoundTripper, error) {
|
||||
cfg, err := config.TransportConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transport.New(cfg)
|
||||
}
|
||||
|
||||
// HTTPWrappersForConfig wraps a round tripper with any relevant layered behavior from the
|
||||
// config. Exposed to allow more clients that need HTTP-like behavior but then must hijack
|
||||
// the underlying connection (like WebSocket or HTTP2 clients). Pure HTTP clients should use
|
||||
// the higher level TransportFor or RESTClientFor methods.
|
||||
func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTripper, error) {
|
||||
cfg, err := config.TransportConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transport.HTTPWrappersForConfig(cfg, rt)
|
||||
}
|
||||
|
||||
// TransportConfig converts a client config to an appropriate transport config.
|
||||
func (c *Config) TransportConfig() (*transport.Config, error) {
|
||||
wt := c.WrapTransport
|
||||
if c.AuthProvider != nil {
|
||||
provider, err := GetAuthProvider(c.Host, c.AuthProvider, c.AuthConfigPersister)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wt != nil {
|
||||
previousWT := wt
|
||||
wt = func(rt http.RoundTripper) http.RoundTripper {
|
||||
return provider.WrapTransport(previousWT(rt))
|
||||
}
|
||||
} else {
|
||||
wt = provider.WrapTransport
|
||||
}
|
||||
}
|
||||
return &transport.Config{
|
||||
UserAgent: c.UserAgent,
|
||||
Transport: c.Transport,
|
||||
WrapTransport: wt,
|
||||
TLS: transport.TLSConfig{
|
||||
CAFile: c.CAFile,
|
||||
CAData: c.CAData,
|
||||
CertFile: c.CertFile,
|
||||
CertData: c.CertData,
|
||||
KeyFile: c.KeyFile,
|
||||
KeyData: c.KeyData,
|
||||
Insecure: c.Insecure,
|
||||
},
|
||||
Username: c.Username,
|
||||
Password: c.Password,
|
||||
BearerToken: c.BearerToken,
|
||||
Impersonate: transport.ImpersonationConfig{
|
||||
UserName: c.Impersonate.UserName,
|
||||
Groups: c.Impersonate.Groups,
|
||||
Extra: c.Impersonate.Extra,
|
||||
},
|
||||
}, nil
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
/*
|
||||
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 restclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// DefaultServerURL converts a host, host:port, or URL string to the default base server API path
|
||||
// to use with a Client at a given API version following the standard conventions for a
|
||||
// Kubernetes API.
|
||||
func DefaultServerURL(host, apiPath string, groupVersion schema.GroupVersion, defaultTLS bool) (*url.URL, string, error) {
|
||||
if host == "" {
|
||||
return nil, "", fmt.Errorf("host must be a URL or a host:port pair")
|
||||
}
|
||||
base := host
|
||||
hostURL, err := url.Parse(base)
|
||||
if err != nil || hostURL.Scheme == "" || hostURL.Host == "" {
|
||||
scheme := "http://"
|
||||
if defaultTLS {
|
||||
scheme = "https://"
|
||||
}
|
||||
hostURL, err = url.Parse(scheme + base)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if hostURL.Path != "" && hostURL.Path != "/" {
|
||||
return nil, "", fmt.Errorf("host must be a URL or a host:port pair: %q", base)
|
||||
}
|
||||
}
|
||||
|
||||
// hostURL.Path is optional; a non-empty Path is treated as a prefix that is to be applied to
|
||||
// all URIs used to access the host. this is useful when there's a proxy in front of the
|
||||
// apiserver that has relocated the apiserver endpoints, forwarding all requests from, for
|
||||
// example, /a/b/c to the apiserver. in this case the Path should be /a/b/c.
|
||||
//
|
||||
// if running without a frontend proxy (that changes the location of the apiserver), then
|
||||
// hostURL.Path should be blank.
|
||||
//
|
||||
// versionedAPIPath, a path relative to baseURL.Path, points to a versioned API base
|
||||
versionedAPIPath := path.Join("/", apiPath)
|
||||
|
||||
// Add the version to the end of the path
|
||||
if len(groupVersion.Group) > 0 {
|
||||
versionedAPIPath = path.Join(versionedAPIPath, groupVersion.Group, groupVersion.Version)
|
||||
|
||||
} else {
|
||||
versionedAPIPath = path.Join(versionedAPIPath, groupVersion.Version)
|
||||
|
||||
}
|
||||
|
||||
return hostURL, versionedAPIPath, nil
|
||||
}
|
||||
|
||||
// defaultServerUrlFor is shared between IsConfigTransportTLS and RESTClientFor. It
|
||||
// requires Host and Version to be set prior to being called.
|
||||
func defaultServerUrlFor(config *Config) (*url.URL, string, error) {
|
||||
// TODO: move the default to secure when the apiserver supports TLS by default
|
||||
// config.Insecure is taken to mean "I want HTTPS but don't bother checking the certs against a CA."
|
||||
hasCA := len(config.CAFile) != 0 || len(config.CAData) != 0
|
||||
hasCert := len(config.CertFile) != 0 || len(config.CertData) != 0
|
||||
defaultTLS := hasCA || hasCert || config.Insecure
|
||||
host := config.Host
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
|
||||
if config.GroupVersion != nil {
|
||||
return DefaultServerURL(host, config.APIPath, *config.GroupVersion, defaultTLS)
|
||||
}
|
||||
return DefaultServerURL(host, config.APIPath, schema.GroupVersion{}, defaultTLS)
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
/*
|
||||
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 restclient
|
||||
|
||||
import (
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
)
|
||||
|
||||
func TestValidatesHostParameter(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Host string
|
||||
APIPath string
|
||||
|
||||
URL string
|
||||
Err bool
|
||||
}{
|
||||
{"127.0.0.1", "", "http://127.0.0.1/" + api.Registry.GroupOrDie(api.GroupName).GroupVersion.Version, false},
|
||||
{"127.0.0.1:8080", "", "http://127.0.0.1:8080/" + api.Registry.GroupOrDie(api.GroupName).GroupVersion.Version, false},
|
||||
{"foo.bar.com", "", "http://foo.bar.com/" + api.Registry.GroupOrDie(api.GroupName).GroupVersion.Version, false},
|
||||
{"http://host/prefix", "", "http://host/prefix/" + api.Registry.GroupOrDie(api.GroupName).GroupVersion.Version, false},
|
||||
{"http://host", "", "http://host/" + api.Registry.GroupOrDie(api.GroupName).GroupVersion.Version, false},
|
||||
{"http://host", "/", "http://host/" + api.Registry.GroupOrDie(api.GroupName).GroupVersion.Version, false},
|
||||
{"http://host", "/other", "http://host/other/" + api.Registry.GroupOrDie(api.GroupName).GroupVersion.Version, false},
|
||||
{"host/server", "", "", true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
u, versionedAPIPath, err := DefaultServerURL(testCase.Host, testCase.APIPath, api.Registry.GroupOrDie(api.GroupName).GroupVersion, false)
|
||||
switch {
|
||||
case err == nil && testCase.Err:
|
||||
t.Errorf("expected error but was nil")
|
||||
continue
|
||||
case err != nil && !testCase.Err:
|
||||
t.Errorf("unexpected error %v", err)
|
||||
continue
|
||||
case err != nil:
|
||||
continue
|
||||
}
|
||||
u.Path = path.Join(u.Path, versionedAPIPath)
|
||||
if e, a := testCase.URL, u.String(); e != a {
|
||||
t.Errorf("%d: expected host %s, got %s", i, e, a)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
/*
|
||||
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 restclient
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/client-go/pkg/util/flowcontrol"
|
||||
)
|
||||
|
||||
// Set of resp. Codes that we backoff for.
|
||||
// In general these should be errors that indicate a server is overloaded.
|
||||
// These shouldn't be configured by any user, we set them based on conventions
|
||||
// described in
|
||||
var serverIsOverloadedSet = sets.NewInt(429)
|
||||
var maxResponseCode = 499
|
||||
|
||||
type BackoffManager interface {
|
||||
UpdateBackoff(actualUrl *url.URL, err error, responseCode int)
|
||||
CalculateBackoff(actualUrl *url.URL) time.Duration
|
||||
Sleep(d time.Duration)
|
||||
}
|
||||
|
||||
// URLBackoff struct implements the semantics on top of Backoff which
|
||||
// we need for URL specific exponential backoff.
|
||||
type URLBackoff struct {
|
||||
// Uses backoff as underlying implementation.
|
||||
Backoff *flowcontrol.Backoff
|
||||
}
|
||||
|
||||
// NoBackoff is a stub implementation, can be used for mocking or else as a default.
|
||||
type NoBackoff struct {
|
||||
}
|
||||
|
||||
func (n *NoBackoff) UpdateBackoff(actualUrl *url.URL, err error, responseCode int) {
|
||||
// do nothing.
|
||||
}
|
||||
|
||||
func (n *NoBackoff) CalculateBackoff(actualUrl *url.URL) time.Duration {
|
||||
return 0 * time.Second
|
||||
}
|
||||
|
||||
func (n *NoBackoff) Sleep(d time.Duration) {
|
||||
time.Sleep(d)
|
||||
}
|
||||
|
||||
// Disable makes the backoff trivial, i.e., sets it to zero. This might be used
|
||||
// by tests which want to run 1000s of mock requests without slowing down.
|
||||
func (b *URLBackoff) Disable() {
|
||||
glog.V(4).Infof("Disabling backoff strategy")
|
||||
b.Backoff = flowcontrol.NewBackOff(0*time.Second, 0*time.Second)
|
||||
}
|
||||
|
||||
// baseUrlKey returns the key which urls will be mapped to.
|
||||
// For example, 127.0.0.1:8080/api/v2/abcde -> 127.0.0.1:8080.
|
||||
func (b *URLBackoff) baseUrlKey(rawurl *url.URL) string {
|
||||
// Simple implementation for now, just the host.
|
||||
// We may backoff specific paths (i.e. "pods") differentially
|
||||
// in the future.
|
||||
host, err := url.Parse(rawurl.String())
|
||||
if err != nil {
|
||||
glog.V(4).Infof("Error extracting url: %v", rawurl)
|
||||
panic("bad url!")
|
||||
}
|
||||
return host.Host
|
||||
}
|
||||
|
||||
// UpdateBackoff updates backoff metadata
|
||||
func (b *URLBackoff) UpdateBackoff(actualUrl *url.URL, err error, responseCode int) {
|
||||
// range for retry counts that we store is [0,13]
|
||||
if responseCode > maxResponseCode || serverIsOverloadedSet.Has(responseCode) {
|
||||
b.Backoff.Next(b.baseUrlKey(actualUrl), b.Backoff.Clock.Now())
|
||||
return
|
||||
} else if responseCode >= 300 || err != nil {
|
||||
glog.V(4).Infof("Client is returning errors: code %v, error %v", responseCode, err)
|
||||
}
|
||||
|
||||
//If we got this far, there is no backoff required for this URL anymore.
|
||||
b.Backoff.Reset(b.baseUrlKey(actualUrl))
|
||||
}
|
||||
|
||||
// CalculateBackoff takes a url and back's off exponentially,
|
||||
// based on its knowledge of existing failures.
|
||||
func (b *URLBackoff) CalculateBackoff(actualUrl *url.URL) time.Duration {
|
||||
return b.Backoff.Get(b.baseUrlKey(actualUrl))
|
||||
}
|
||||
|
||||
func (b *URLBackoff) Sleep(d time.Duration) {
|
||||
b.Backoff.Clock.Sleep(d)
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
/*
|
||||
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 restclient
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/client-go/pkg/util/flowcontrol"
|
||||
)
|
||||
|
||||
func parse(raw string) *url.URL {
|
||||
theUrl, _ := url.Parse(raw)
|
||||
return theUrl
|
||||
}
|
||||
|
||||
func TestURLBackoffFunctionalityCollisions(t *testing.T) {
|
||||
myBackoff := &URLBackoff{
|
||||
Backoff: flowcontrol.NewBackOff(1*time.Second, 60*time.Second),
|
||||
}
|
||||
|
||||
// Add some noise and make sure backoff for a clean URL is zero.
|
||||
myBackoff.UpdateBackoff(parse("http://100.200.300.400:8080"), nil, 500)
|
||||
|
||||
myBackoff.UpdateBackoff(parse("http://1.2.3.4:8080"), nil, 500)
|
||||
|
||||
if myBackoff.CalculateBackoff(parse("http://1.2.3.4:100")) > 0 {
|
||||
t.Errorf("URLs are colliding in the backoff map!")
|
||||
}
|
||||
}
|
||||
|
||||
// TestURLBackoffFunctionality generally tests the URLBackoff wrapper. We avoid duplicating tests from backoff and request.
|
||||
func TestURLBackoffFunctionality(t *testing.T) {
|
||||
myBackoff := &URLBackoff{
|
||||
Backoff: flowcontrol.NewBackOff(1*time.Second, 60*time.Second),
|
||||
}
|
||||
|
||||
// Now test that backoff increases, then recovers.
|
||||
// 200 and 300 should both result in clearing the backoff.
|
||||
// all others like 429 should result in increased backoff.
|
||||
seconds := []int{0,
|
||||
1, 2, 4, 8, 0,
|
||||
1, 2}
|
||||
returnCodes := []int{
|
||||
429, 500, 501, 502, 300,
|
||||
500, 501, 502,
|
||||
}
|
||||
|
||||
if len(seconds) != len(returnCodes) {
|
||||
t.Fatalf("responseCode to backoff arrays should be the same length... sanity check failed.")
|
||||
}
|
||||
|
||||
for i, sec := range seconds {
|
||||
backoffSec := myBackoff.CalculateBackoff(parse("http://1.2.3.4:100"))
|
||||
if backoffSec < time.Duration(sec)*time.Second || backoffSec > time.Duration(sec+5)*time.Second {
|
||||
t.Errorf("Backoff out of range %v: %v %v", i, sec, backoffSec)
|
||||
}
|
||||
myBackoff.UpdateBackoff(parse("http://1.2.3.4:100/responseCodeForFuncTest"), nil, returnCodes[i])
|
||||
}
|
||||
|
||||
if myBackoff.CalculateBackoff(parse("http://1.2.3.4:100")) == 0 {
|
||||
t.Errorf("The final return code %v should have resulted in a backoff ! ", returnCodes[7])
|
||||
}
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
/*
|
||||
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 restclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
legacyAPIPath = "/api"
|
||||
defaultAPIPath = "/apis"
|
||||
)
|
||||
|
||||
// TODO: Is this obsoleted by the discovery client?
|
||||
|
||||
// ServerAPIVersions returns the GroupVersions supported by the API server.
|
||||
// It creates a RESTClient based on the passed in config, but it doesn't rely
|
||||
// on the Version and Codec of the config, because it uses AbsPath and
|
||||
// takes the raw response.
|
||||
func ServerAPIVersions(c *Config) (groupVersions []string, err error) {
|
||||
transport, err := TransportFor(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := http.Client{Transport: transport}
|
||||
|
||||
configCopy := *c
|
||||
configCopy.GroupVersion = nil
|
||||
configCopy.APIPath = ""
|
||||
baseURL, _, err := defaultServerUrlFor(&configCopy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Get the groupVersions exposed at /api
|
||||
originalPath := baseURL.Path
|
||||
baseURL.Path = path.Join(originalPath, legacyAPIPath)
|
||||
resp, err := client.Get(baseURL.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var v metav1.APIVersions
|
||||
defer resp.Body.Close()
|
||||
err = json.NewDecoder(resp.Body).Decode(&v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
groupVersions = append(groupVersions, v.Versions...)
|
||||
// Get the groupVersions exposed at /apis
|
||||
baseURL.Path = path.Join(originalPath, defaultAPIPath)
|
||||
resp2, err := client.Get(baseURL.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var apiGroupList metav1.APIGroupList
|
||||
defer resp2.Body.Close()
|
||||
err = json.NewDecoder(resp2.Body).Decode(&apiGroupList)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
for _, g := range apiGroupList.Groups {
|
||||
for _, gv := range g.Versions {
|
||||
groupVersions = append(groupVersions, gv.GroupVersion)
|
||||
}
|
||||
}
|
||||
|
||||
return groupVersions, nil
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"cert.go",
|
||||
"csr.go",
|
||||
"io.go",
|
||||
"pem.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["csr_test.go"],
|
||||
data = [
|
||||
"testdata/dontUseThisKey.pem",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//pkg/util/cert/triple:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
@ -1,207 +0,0 @@
|
||||
/*
|
||||
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 cert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
rsaKeySize = 2048
|
||||
duration365d = time.Hour * 24 * 365
|
||||
)
|
||||
|
||||
// Config containes the basic fields required for creating a certificate
|
||||
type Config struct {
|
||||
CommonName string
|
||||
Organization []string
|
||||
AltNames AltNames
|
||||
}
|
||||
|
||||
// AltNames contains the domain names and IP addresses that will be added
|
||||
// to the API Server's x509 certificate SubAltNames field. The values will
|
||||
// be passed directly to the x509.Certificate object.
|
||||
type AltNames struct {
|
||||
DNSNames []string
|
||||
IPs []net.IP
|
||||
}
|
||||
|
||||
// NewPrivateKey creates an RSA private key
|
||||
func NewPrivateKey() (*rsa.PrivateKey, error) {
|
||||
return rsa.GenerateKey(cryptorand.Reader, rsaKeySize)
|
||||
}
|
||||
|
||||
// NewSelfSignedCACert creates a CA certificate
|
||||
func NewSelfSignedCACert(cfg Config, key *rsa.PrivateKey) (*x509.Certificate, error) {
|
||||
now := time.Now()
|
||||
tmpl := x509.Certificate{
|
||||
SerialNumber: new(big.Int).SetInt64(0),
|
||||
Subject: pkix.Name{
|
||||
CommonName: cfg.CommonName,
|
||||
Organization: cfg.Organization,
|
||||
},
|
||||
NotBefore: now.UTC(),
|
||||
NotAfter: now.Add(duration365d * 10).UTC(),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &tmpl, &tmpl, key.Public(), key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x509.ParseCertificate(certDERBytes)
|
||||
}
|
||||
|
||||
// NewSignedCert creates a signed certificate using the given CA certificate and key
|
||||
func NewSignedCert(cfg Config, key *rsa.PrivateKey, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, error) {
|
||||
serial, err := cryptorand.Int(cryptorand.Reader, new(big.Int).SetInt64(math.MaxInt64))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certTmpl := x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
CommonName: cfg.CommonName,
|
||||
Organization: cfg.Organization,
|
||||
},
|
||||
DNSNames: cfg.AltNames.DNSNames,
|
||||
IPAddresses: cfg.AltNames.IPs,
|
||||
SerialNumber: serial,
|
||||
NotBefore: caCert.NotBefore,
|
||||
NotAfter: time.Now().Add(duration365d).UTC(),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
}
|
||||
certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &certTmpl, caCert, key.Public(), caKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x509.ParseCertificate(certDERBytes)
|
||||
}
|
||||
|
||||
// MakeEllipticPrivateKeyPEM creates an ECDSA private key
|
||||
func MakeEllipticPrivateKeyPEM() ([]byte, error) {
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
derBytes, err := x509.MarshalECPrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKeyPemBlock := &pem.Block{
|
||||
Type: "EC PRIVATE KEY",
|
||||
Bytes: derBytes,
|
||||
}
|
||||
return pem.EncodeToMemory(privateKeyPemBlock), nil
|
||||
}
|
||||
|
||||
// GenerateSelfSignedCertKey creates a self-signed certificate and key for the given host.
|
||||
// Host may be an IP or a DNS name
|
||||
// You may also specify additional subject alt names (either ip or dns names) for the certificate
|
||||
func GenerateSelfSignedCertKey(host string, alternateIPs []net.IP, alternateDNS []string) ([]byte, []byte, error) {
|
||||
priv, err := rsa.GenerateKey(cryptorand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()),
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * 365),
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else {
|
||||
template.DNSNames = append(template.DNSNames, host)
|
||||
}
|
||||
|
||||
template.IPAddresses = append(template.IPAddresses, alternateIPs...)
|
||||
template.DNSNames = append(template.DNSNames, alternateDNS...)
|
||||
|
||||
derBytes, err := x509.CreateCertificate(cryptorand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Generate cert
|
||||
certBuffer := bytes.Buffer{}
|
||||
if err := pem.Encode(&certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Generate key
|
||||
keyBuffer := bytes.Buffer{}
|
||||
if err := pem.Encode(&keyBuffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return certBuffer.Bytes(), keyBuffer.Bytes(), nil
|
||||
}
|
||||
|
||||
// FormatBytesCert receives byte array certificate and formats in human-readable format
|
||||
func FormatBytesCert(cert []byte) (string, error) {
|
||||
block, _ := pem.Decode(cert)
|
||||
c, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse certificate [%v]", err)
|
||||
}
|
||||
return FormatCert(c), nil
|
||||
}
|
||||
|
||||
// FormatCert receives certificate and formats in human-readable format
|
||||
func FormatCert(c *x509.Certificate) string {
|
||||
var ips []string
|
||||
for _, ip := range c.IPAddresses {
|
||||
ips = append(ips, ip.String())
|
||||
}
|
||||
altNames := append(ips, c.DNSNames...)
|
||||
res := fmt.Sprintf(
|
||||
"Issuer: CN=%s | Subject: CN=%s | CA: %t\n",
|
||||
c.Issuer.CommonName, c.Subject.CommonName, c.IsCA,
|
||||
)
|
||||
res += fmt.Sprintf("Not before: %s Not After: %s", c.NotBefore, c.NotAfter)
|
||||
if len(altNames) > 0 {
|
||||
res += fmt.Sprintf("\nAlternate Names: %v", altNames)
|
||||
}
|
||||
return res
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
/*
|
||||
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 cert
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"net"
|
||||
)
|
||||
|
||||
// MakeCSR generates a PEM-encoded CSR using the supplied private key, subject, and SANs.
|
||||
// All key types that are implemented via crypto.Signer are supported (This includes *rsa.PrivateKey and *ecdsa.PrivateKey.)
|
||||
func MakeCSR(privateKey interface{}, subject *pkix.Name, dnsSANs []string, ipSANs []net.IP) (csr []byte, err error) {
|
||||
// Customize the signature for RSA keys, depending on the key size
|
||||
var sigType x509.SignatureAlgorithm
|
||||
if privateKey, ok := privateKey.(*rsa.PrivateKey); ok {
|
||||
keySize := privateKey.N.BitLen()
|
||||
switch {
|
||||
case keySize >= 4096:
|
||||
sigType = x509.SHA512WithRSA
|
||||
case keySize >= 3072:
|
||||
sigType = x509.SHA384WithRSA
|
||||
default:
|
||||
sigType = x509.SHA256WithRSA
|
||||
}
|
||||
}
|
||||
|
||||
template := &x509.CertificateRequest{
|
||||
Subject: *subject,
|
||||
SignatureAlgorithm: sigType,
|
||||
DNSNames: dnsSANs,
|
||||
IPAddresses: ipSANs,
|
||||
}
|
||||
|
||||
csr, err = x509.CreateCertificateRequest(cryptorand.Reader, template, privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
csrPemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csr,
|
||||
}
|
||||
|
||||
return pem.EncodeToMemory(csrPemBlock), nil
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
/*
|
||||
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 cert
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// CanReadCertAndKey returns true if the certificate and key files already exists,
|
||||
// otherwise returns false. If lost one of cert and key, returns error.
|
||||
func CanReadCertAndKey(certPath, keyPath string) (bool, error) {
|
||||
certReadable := canReadFile(certPath)
|
||||
keyReadable := canReadFile(keyPath)
|
||||
|
||||
if certReadable == false && keyReadable == false {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if certReadable == false {
|
||||
return false, fmt.Errorf("error reading %s, certificate and key must be supplied as a pair", certPath)
|
||||
}
|
||||
|
||||
if keyReadable == false {
|
||||
return false, fmt.Errorf("error reading %s, certificate and key must be supplied as a pair", keyPath)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// If the file represented by path exists and
|
||||
// readable, returns true otherwise returns false.
|
||||
func canReadFile(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// WriteCert writes the pem-encoded certificate data to certPath.
|
||||
// The certificate file will be created with file mode 0644.
|
||||
// If the certificate file already exists, it will be overwritten.
|
||||
// The parent directory of the certPath will be created as needed with file mode 0755.
|
||||
func WriteCert(certPath string, data []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(certPath), os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(certPath, data, os.FileMode(0644)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteKey writes the pem-encoded key data to keyPath.
|
||||
// The key file will be created with file mode 0600.
|
||||
// If the key file already exists, it will be overwritten.
|
||||
// The parent directory of the keyPath will be created as needed with file mode 0755.
|
||||
func WriteKey(keyPath string, data []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(keyPath), os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(keyPath, data, os.FileMode(0600)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPool returns an x509.CertPool containing the certificates in the given PEM-encoded file.
|
||||
// Returns an error if the file could not be read, a certificate could not be parsed, or if the file does not contain any certificates
|
||||
func NewPool(filename string) (*x509.CertPool, error) {
|
||||
certs, err := CertsFromFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
for _, cert := range certs {
|
||||
pool.AddCert(cert)
|
||||
}
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// CertsFromFile returns the x509.Certificates contained in the given PEM-encoded file.
|
||||
// Returns an error if the file could not be read, a certificate could not be parsed, or if the file does not contain any certificates
|
||||
func CertsFromFile(file string) ([]*x509.Certificate, error) {
|
||||
pemBlock, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certs, err := ParseCertsPEM(pemBlock)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %s", file, err)
|
||||
}
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
// PrivateKeyFromFile returns the private key in rsa.PrivateKey or ecdsa.PrivateKey format from a given PEM-encoded file.
|
||||
// Returns an error if the file could not be read or if the private key could not be parsed.
|
||||
func PrivateKeyFromFile(file string) (interface{}, error) {
|
||||
pemBlock, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := ParsePrivateKeyPEM(pemBlock)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %v", file, err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
/*
|
||||
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 cert
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// EncodePublicKeyPEM returns PEM-endcode public data
|
||||
func EncodePublicKeyPEM(key *rsa.PublicKey) ([]byte, error) {
|
||||
der, err := x509.MarshalPKIXPublicKey(key)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
block := pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: der,
|
||||
}
|
||||
return pem.EncodeToMemory(&block), nil
|
||||
}
|
||||
|
||||
// EncodePrivateKeyPEM returns PEM-encoded private key data
|
||||
func EncodePrivateKeyPEM(key *rsa.PrivateKey) []byte {
|
||||
block := pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
}
|
||||
return pem.EncodeToMemory(&block)
|
||||
}
|
||||
|
||||
// EncodeCertPEM returns PEM-endcoded certificate data
|
||||
func EncodeCertPEM(cert *x509.Certificate) []byte {
|
||||
block := pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
return pem.EncodeToMemory(&block)
|
||||
}
|
||||
|
||||
// ParsePrivateKeyPEM returns a private key parsed from a PEM block in the supplied data.
|
||||
// Recognizes PEM blocks for "EC PRIVATE KEY" and "RSA PRIVATE KEY"
|
||||
func ParsePrivateKeyPEM(keyData []byte) (interface{}, error) {
|
||||
for {
|
||||
var privateKeyPemBlock *pem.Block
|
||||
privateKeyPemBlock, keyData = pem.Decode(keyData)
|
||||
if privateKeyPemBlock == nil {
|
||||
// we read all the PEM blocks and didn't recognize one
|
||||
return nil, fmt.Errorf("no private key PEM block found")
|
||||
}
|
||||
|
||||
switch privateKeyPemBlock.Type {
|
||||
case "EC PRIVATE KEY":
|
||||
return x509.ParseECPrivateKey(privateKeyPemBlock.Bytes)
|
||||
case "RSA PRIVATE KEY":
|
||||
return x509.ParsePKCS1PrivateKey(privateKeyPemBlock.Bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ParseCertsPEM returns the x509.Certificates contained in the given PEM-encoded byte array
|
||||
// Returns an error if a certificate could not be parsed, or if the data does not contain any certificates
|
||||
func ParseCertsPEM(pemCerts []byte) ([]*x509.Certificate, error) {
|
||||
ok := false
|
||||
certs := []*x509.Certificate{}
|
||||
for len(pemCerts) > 0 {
|
||||
var block *pem.Block
|
||||
block, pemCerts = pem.Decode(pemCerts)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
// Only use PEM "CERTIFICATE" blocks without extra headers
|
||||
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return certs, err
|
||||
}
|
||||
|
||||
certs = append(certs, cert)
|
||||
ok = true
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return certs, errors.New("could not read any certificates")
|
||||
}
|
||||
return certs, nil
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"fake_handler.go",
|
||||
"tmpdir.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["fake_handler_test.go"],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
@ -1,139 +0,0 @@
|
||||
/*
|
||||
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 testing
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// TestInterface is a simple interface providing Errorf, to make injection for
|
||||
// testing easier (insert 'yo dawg' meme here).
|
||||
type TestInterface interface {
|
||||
Errorf(format string, args ...interface{})
|
||||
Logf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
// LogInterface is a simple interface to allow injection of Logf to report serving errors.
|
||||
type LogInterface interface {
|
||||
Logf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
// FakeHandler is to assist in testing HTTP requests. Notice that FakeHandler is
|
||||
// not thread safe and you must not direct traffic to except for the request
|
||||
// you want to test. You can do this by hiding it in an http.ServeMux.
|
||||
type FakeHandler struct {
|
||||
RequestReceived *http.Request
|
||||
RequestBody string
|
||||
StatusCode int
|
||||
ResponseBody string
|
||||
// For logging - you can use a *testing.T
|
||||
// This will keep log messages associated with the test.
|
||||
T LogInterface
|
||||
|
||||
// Enforce "only one use" constraint.
|
||||
lock sync.Mutex
|
||||
requestCount int
|
||||
hasBeenChecked bool
|
||||
|
||||
SkipRequestFn func(verb string, url url.URL) bool
|
||||
}
|
||||
|
||||
func (f *FakeHandler) SetResponseBody(responseBody string) {
|
||||
f.lock.Lock()
|
||||
defer f.lock.Unlock()
|
||||
f.ResponseBody = responseBody
|
||||
}
|
||||
|
||||
func (f *FakeHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
|
||||
f.lock.Lock()
|
||||
defer f.lock.Unlock()
|
||||
|
||||
if f.SkipRequestFn != nil && f.SkipRequestFn(request.Method, *request.URL) {
|
||||
response.Header().Set("Content-Type", "application/json")
|
||||
response.WriteHeader(f.StatusCode)
|
||||
response.Write([]byte(f.ResponseBody))
|
||||
return
|
||||
}
|
||||
|
||||
f.requestCount++
|
||||
if f.hasBeenChecked {
|
||||
panic("got request after having been validated")
|
||||
}
|
||||
|
||||
f.RequestReceived = request
|
||||
response.Header().Set("Content-Type", "application/json")
|
||||
response.WriteHeader(f.StatusCode)
|
||||
response.Write([]byte(f.ResponseBody))
|
||||
|
||||
bodyReceived, err := ioutil.ReadAll(request.Body)
|
||||
if err != nil && f.T != nil {
|
||||
f.T.Logf("Received read error: %v", err)
|
||||
}
|
||||
f.RequestBody = string(bodyReceived)
|
||||
if f.T != nil {
|
||||
f.T.Logf("request body: %s", f.RequestBody)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FakeHandler) ValidateRequestCount(t TestInterface, count int) bool {
|
||||
ok := true
|
||||
f.lock.Lock()
|
||||
defer f.lock.Unlock()
|
||||
if f.requestCount != count {
|
||||
ok = false
|
||||
t.Errorf("Expected %d call, but got %d. Only the last call is recorded and checked.", count, f.requestCount)
|
||||
}
|
||||
f.hasBeenChecked = true
|
||||
return ok
|
||||
}
|
||||
|
||||
// ValidateRequest verifies that FakeHandler received a request with expected path, method, and body.
|
||||
func (f *FakeHandler) ValidateRequest(t TestInterface, expectedPath, expectedMethod string, body *string) {
|
||||
f.lock.Lock()
|
||||
defer f.lock.Unlock()
|
||||
if f.requestCount != 1 {
|
||||
t.Logf("Expected 1 call, but got %v. Only the last call is recorded and checked.", f.requestCount)
|
||||
}
|
||||
f.hasBeenChecked = true
|
||||
|
||||
expectURL, err := url.Parse(expectedPath)
|
||||
if err != nil {
|
||||
t.Errorf("Couldn't parse %v as a URL.", expectedPath)
|
||||
}
|
||||
if f.RequestReceived == nil {
|
||||
t.Errorf("Unexpected nil request received for %s", expectedPath)
|
||||
return
|
||||
}
|
||||
if f.RequestReceived.URL.Path != expectURL.Path {
|
||||
t.Errorf("Unexpected request path for request %#v, received: %q, expected: %q", f.RequestReceived, f.RequestReceived.URL.Path, expectURL.Path)
|
||||
}
|
||||
if e, a := expectURL.Query(), f.RequestReceived.URL.Query(); !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("Unexpected query for request %#v, received: %q, expected: %q", f.RequestReceived, a, e)
|
||||
}
|
||||
if f.RequestReceived.Method != expectedMethod {
|
||||
t.Errorf("Unexpected method: %q, expected: %q", f.RequestReceived.Method, expectedMethod)
|
||||
}
|
||||
if body != nil {
|
||||
if *body != f.RequestBody {
|
||||
t.Errorf("Received body:\n%s\n Doesn't match expected body:\n%s", f.RequestBody, *body)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
/*
|
||||
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 testing
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// MkTmpdir creates a temporary directory based upon the prefix passed in.
|
||||
// If successful, it returns the temporary directory path. The directory can be
|
||||
// deleted with a call to "os.RemoveAll(...)".
|
||||
// In case of error, it'll return an empty string and the error.
|
||||
func MkTmpdir(prefix string) (string, error) {
|
||||
tmpDir, err := ioutil.TempDir(os.TempDir(), prefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tmpDir, nil
|
||||
}
|
||||
|
||||
// MkTmpdir does the same work as "MkTmpdir", except in case of
|
||||
// errors, it'll trigger a panic.
|
||||
func MkTmpdirOrDie(prefix string) string {
|
||||
tmpDir, err := MkTmpdir(prefix)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tmpDir
|
||||
}
|
@ -70,11 +70,16 @@ function save() {
|
||||
|
||||
# save everything for which the staging directory is the source of truth
|
||||
save "transport"
|
||||
save "tools/metrics"
|
||||
save "tools/clientcmd/api"
|
||||
save "rest/watch"
|
||||
save "rest"
|
||||
# remove the rest/fake until we're authoritative for it (need to update for registry)
|
||||
rm -rf ${CLIENT_REPO_TEMP}/rest/fake
|
||||
save "pkg/util/cert"
|
||||
save "pkg/util/clock"
|
||||
save "pkg/util/integer"
|
||||
save "pkg/util/flowcontrol"
|
||||
save "pkg/util/integer"
|
||||
save "pkg/util/testing"
|
||||
|
||||
|
||||
|
||||
@ -90,7 +95,7 @@ mkcp "/pkg/client/record" "/pkg/client"
|
||||
mkcp "/pkg/client/cache" "/pkg/client"
|
||||
# TODO: make this test file not depending on pkg/client/unversioned
|
||||
rm "${CLIENT_REPO_TEMP}"/pkg/client/cache/listwatch_test.go
|
||||
mkcp "/pkg/client/restclient" "/pkg/client"
|
||||
mkcp "/pkg/client/restclient/fake" "/pkg/client/restclient"
|
||||
mkcp "/pkg/client/testing" "/pkg/client"
|
||||
# remove this test because it imports the internal clientset
|
||||
rm "${CLIENT_REPO_TEMP}"/pkg/client/testing/core/fake_test.go
|
||||
@ -147,8 +152,6 @@ find "${CLIENT_REPO_TEMP}"/pkg/client/record -type f -name "*.go" -print0 | xarg
|
||||
# gofmt the changed files
|
||||
|
||||
echo "rewrite conflicting Prometheus registration"
|
||||
sed -i "s/request_latency_microseconds/request_latency_microseconds_copy/g" "${CLIENT_REPO_TEMP}"/pkg/client/metrics/metrics.go
|
||||
sed -i "s/request_status_codes/request_status_codes_copy/g" "${CLIENT_REPO_TEMP}"/pkg/client/metrics/metrics.go
|
||||
sed -i "s/kubernetes_build_info/kubernetes_build_info_copy/g" "${CLIENT_REPO_TEMP}"/pkg/version/version.go
|
||||
|
||||
echo "rewrite proto names in proto.RegisterType"
|
||||
@ -191,12 +194,11 @@ mvfolder "pkg/client/clientset_generated/${CLIENTSET}" kubernetes
|
||||
mvfolder pkg/client/typed/discovery discovery
|
||||
mvfolder pkg/client/typed/dynamic dynamic
|
||||
mvfolder pkg/client/record tools/record
|
||||
mvfolder pkg/client/restclient rest
|
||||
mvfolder pkg/client/restclient/fake rest/fake
|
||||
mvfolder pkg/client/cache tools/cache
|
||||
mvfolder pkg/client/unversioned/auth tools/auth
|
||||
mvfolder pkg/client/unversioned/clientcmd tools/clientcmd
|
||||
mvfolder pkg/client/unversioned/portforward tools/portforward
|
||||
mvfolder pkg/client/metrics tools/metrics
|
||||
mvfolder pkg/client/testing/core testing
|
||||
mvfolder pkg/client/testing/cache tools/cache/testing
|
||||
mvfolder cmd/kubeadm/app/apis/kubeadm pkg/apis/kubeadm
|
||||
|
184
staging/src/k8s.io/client-go/pkg/util/clock/clock_test.go
Normal file
184
staging/src/k8s.io/client-go/pkg/util/clock/clock_test.go
Normal file
@ -0,0 +1,184 @@
|
||||
/*
|
||||
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 clock
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFakeClock(t *testing.T) {
|
||||
startTime := time.Now()
|
||||
tc := NewFakeClock(startTime)
|
||||
tc.Step(time.Second)
|
||||
now := tc.Now()
|
||||
if now.Sub(startTime) != time.Second {
|
||||
t.Errorf("input: %s now=%s gap=%s expected=%s", startTime, now, now.Sub(startTime), time.Second)
|
||||
}
|
||||
|
||||
tt := tc.Now()
|
||||
tc.SetTime(tt.Add(time.Hour))
|
||||
if tc.Now().Sub(tt) != time.Hour {
|
||||
t.Errorf("input: %s now=%s gap=%s expected=%s", tt, tc.Now(), tc.Now().Sub(tt), time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFakeClockSleep(t *testing.T) {
|
||||
startTime := time.Now()
|
||||
tc := NewFakeClock(startTime)
|
||||
tc.Sleep(time.Duration(1) * time.Hour)
|
||||
now := tc.Now()
|
||||
if now.Sub(startTime) != time.Hour {
|
||||
t.Errorf("Fake sleep failed, expected time to advance by one hour, instead, its %v", now.Sub(startTime))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFakeAfter(t *testing.T) {
|
||||
tc := NewFakeClock(time.Now())
|
||||
if tc.HasWaiters() {
|
||||
t.Errorf("unexpected waiter?")
|
||||
}
|
||||
oneSec := tc.After(time.Second)
|
||||
if !tc.HasWaiters() {
|
||||
t.Errorf("unexpected lack of waiter?")
|
||||
}
|
||||
|
||||
oneOhOneSec := tc.After(time.Second + time.Millisecond)
|
||||
twoSec := tc.After(2 * time.Second)
|
||||
select {
|
||||
case <-oneSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
case <-oneOhOneSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
case <-twoSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
default:
|
||||
}
|
||||
|
||||
tc.Step(999 * time.Millisecond)
|
||||
select {
|
||||
case <-oneSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
case <-oneOhOneSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
case <-twoSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
default:
|
||||
}
|
||||
|
||||
tc.Step(time.Millisecond)
|
||||
select {
|
||||
case <-oneSec:
|
||||
// Expected!
|
||||
case <-oneOhOneSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
case <-twoSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
default:
|
||||
t.Errorf("unexpected non-channel read")
|
||||
}
|
||||
tc.Step(time.Millisecond)
|
||||
select {
|
||||
case <-oneSec:
|
||||
// should not double-trigger!
|
||||
t.Errorf("unexpected channel read")
|
||||
case <-oneOhOneSec:
|
||||
// Expected!
|
||||
case <-twoSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
default:
|
||||
t.Errorf("unexpected non-channel read")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFakeTick(t *testing.T) {
|
||||
tc := NewFakeClock(time.Now())
|
||||
if tc.HasWaiters() {
|
||||
t.Errorf("unexpected waiter?")
|
||||
}
|
||||
oneSec := tc.Tick(time.Second)
|
||||
if !tc.HasWaiters() {
|
||||
t.Errorf("unexpected lack of waiter?")
|
||||
}
|
||||
|
||||
oneOhOneSec := tc.Tick(time.Second + time.Millisecond)
|
||||
twoSec := tc.Tick(2 * time.Second)
|
||||
select {
|
||||
case <-oneSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
case <-oneOhOneSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
case <-twoSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
default:
|
||||
}
|
||||
|
||||
tc.Step(999 * time.Millisecond) // t=.999
|
||||
select {
|
||||
case <-oneSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
case <-oneOhOneSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
case <-twoSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
default:
|
||||
}
|
||||
|
||||
tc.Step(time.Millisecond) // t=1.000
|
||||
select {
|
||||
case <-oneSec:
|
||||
// Expected!
|
||||
case <-oneOhOneSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
case <-twoSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
default:
|
||||
t.Errorf("unexpected non-channel read")
|
||||
}
|
||||
tc.Step(time.Millisecond) // t=1.001
|
||||
select {
|
||||
case <-oneSec:
|
||||
// should not double-trigger!
|
||||
t.Errorf("unexpected channel read")
|
||||
case <-oneOhOneSec:
|
||||
// Expected!
|
||||
case <-twoSec:
|
||||
t.Errorf("unexpected channel read")
|
||||
default:
|
||||
t.Errorf("unexpected non-channel read")
|
||||
}
|
||||
|
||||
tc.Step(time.Second) // t=2.001
|
||||
tc.Step(time.Second) // t=3.001
|
||||
tc.Step(time.Second) // t=4.001
|
||||
tc.Step(time.Second) // t=5.001
|
||||
|
||||
// The one second ticker should not accumulate ticks
|
||||
accumulatedTicks := 0
|
||||
drained := false
|
||||
for !drained {
|
||||
select {
|
||||
case <-oneSec:
|
||||
accumulatedTicks++
|
||||
default:
|
||||
drained = true
|
||||
}
|
||||
}
|
||||
if accumulatedTicks != 1 {
|
||||
t.Errorf("unexpected number of accumulated ticks: %d", accumulatedTicks)
|
||||
}
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
/*
|
||||
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 flowcontrol
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/kubernetes/pkg/util/clock"
|
||||
)
|
||||
|
||||
func TestSlowBackoff(t *testing.T) {
|
||||
id := "_idSlow"
|
||||
tc := clock.NewFakeClock(time.Now())
|
||||
step := time.Second
|
||||
maxDuration := 50 * step
|
||||
|
||||
b := NewFakeBackOff(step, maxDuration, tc)
|
||||
cases := []time.Duration{0, 1, 2, 4, 8, 16, 32, 50, 50, 50}
|
||||
for ix, c := range cases {
|
||||
tc.Step(step)
|
||||
w := b.Get(id)
|
||||
if w != c*step {
|
||||
t.Errorf("input: '%d': expected %s, got %s", ix, c*step, w)
|
||||
}
|
||||
b.Next(id, tc.Now())
|
||||
}
|
||||
|
||||
//Now confirm that the Reset cancels backoff.
|
||||
b.Next(id, tc.Now())
|
||||
b.Reset(id)
|
||||
if b.Get(id) != 0 {
|
||||
t.Errorf("Reset didn't clear the backoff.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestBackoffReset(t *testing.T) {
|
||||
id := "_idReset"
|
||||
tc := clock.NewFakeClock(time.Now())
|
||||
step := time.Second
|
||||
maxDuration := step * 5
|
||||
b := NewFakeBackOff(step, maxDuration, tc)
|
||||
startTime := tc.Now()
|
||||
|
||||
// get to backoff = maxDuration
|
||||
for i := 0; i <= int(maxDuration/step); i++ {
|
||||
tc.Step(step)
|
||||
b.Next(id, tc.Now())
|
||||
}
|
||||
|
||||
// backoff should be capped at maxDuration
|
||||
if !b.IsInBackOffSince(id, tc.Now()) {
|
||||
t.Errorf("expected to be in Backoff got %s", b.Get(id))
|
||||
}
|
||||
|
||||
lastUpdate := tc.Now()
|
||||
tc.Step(2*maxDuration + step) // time += 11s, 11 > 2*maxDuration
|
||||
if b.IsInBackOffSince(id, lastUpdate) {
|
||||
t.Errorf("expected to not be in Backoff after reset (start=%s, now=%s, lastUpdate=%s), got %s", startTime, tc.Now(), lastUpdate, b.Get(id))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackoffHightWaterMark(t *testing.T) {
|
||||
id := "_idHiWaterMark"
|
||||
tc := clock.NewFakeClock(time.Now())
|
||||
step := time.Second
|
||||
maxDuration := 5 * step
|
||||
b := NewFakeBackOff(step, maxDuration, tc)
|
||||
|
||||
// get to backoff = maxDuration
|
||||
for i := 0; i <= int(maxDuration/step); i++ {
|
||||
tc.Step(step)
|
||||
b.Next(id, tc.Now())
|
||||
}
|
||||
|
||||
// backoff high watermark expires after 2*maxDuration
|
||||
tc.Step(maxDuration + step)
|
||||
b.Next(id, tc.Now())
|
||||
|
||||
if b.Get(id) != maxDuration {
|
||||
t.Errorf("expected Backoff to stay at high watermark %s got %s", maxDuration, b.Get(id))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackoffGC(t *testing.T) {
|
||||
id := "_idGC"
|
||||
tc := clock.NewFakeClock(time.Now())
|
||||
step := time.Second
|
||||
maxDuration := 5 * step
|
||||
|
||||
b := NewFakeBackOff(step, maxDuration, tc)
|
||||
|
||||
for i := 0; i <= int(maxDuration/step); i++ {
|
||||
tc.Step(step)
|
||||
b.Next(id, tc.Now())
|
||||
}
|
||||
lastUpdate := tc.Now()
|
||||
tc.Step(maxDuration + step)
|
||||
b.GC()
|
||||
_, found := b.perItemBackoff[id]
|
||||
if !found {
|
||||
t.Errorf("expected GC to skip entry, elapsed time=%s maxDuration=%s", tc.Now().Sub(lastUpdate), maxDuration)
|
||||
}
|
||||
|
||||
tc.Step(maxDuration + step)
|
||||
b.GC()
|
||||
r, found := b.perItemBackoff[id]
|
||||
if found {
|
||||
t.Errorf("expected GC of entry after %s got entry %v", tc.Now().Sub(lastUpdate), r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsInBackOffSinceUpdate(t *testing.T) {
|
||||
id := "_idIsInBackOffSinceUpdate"
|
||||
tc := clock.NewFakeClock(time.Now())
|
||||
step := time.Second
|
||||
maxDuration := 10 * step
|
||||
b := NewFakeBackOff(step, maxDuration, tc)
|
||||
startTime := tc.Now()
|
||||
|
||||
cases := []struct {
|
||||
tick time.Duration
|
||||
inBackOff bool
|
||||
value int
|
||||
}{
|
||||
{tick: 0, inBackOff: false, value: 0},
|
||||
{tick: 1, inBackOff: false, value: 1},
|
||||
{tick: 2, inBackOff: true, value: 2},
|
||||
{tick: 3, inBackOff: false, value: 2},
|
||||
{tick: 4, inBackOff: true, value: 4},
|
||||
{tick: 5, inBackOff: true, value: 4},
|
||||
{tick: 6, inBackOff: true, value: 4},
|
||||
{tick: 7, inBackOff: false, value: 4},
|
||||
{tick: 8, inBackOff: true, value: 8},
|
||||
{tick: 9, inBackOff: true, value: 8},
|
||||
{tick: 10, inBackOff: true, value: 8},
|
||||
{tick: 11, inBackOff: true, value: 8},
|
||||
{tick: 12, inBackOff: true, value: 8},
|
||||
{tick: 13, inBackOff: true, value: 8},
|
||||
{tick: 14, inBackOff: true, value: 8},
|
||||
{tick: 15, inBackOff: false, value: 8},
|
||||
{tick: 16, inBackOff: true, value: 10},
|
||||
{tick: 17, inBackOff: true, value: 10},
|
||||
{tick: 18, inBackOff: true, value: 10},
|
||||
{tick: 19, inBackOff: true, value: 10},
|
||||
{tick: 20, inBackOff: true, value: 10},
|
||||
{tick: 21, inBackOff: true, value: 10},
|
||||
{tick: 22, inBackOff: true, value: 10},
|
||||
{tick: 23, inBackOff: true, value: 10},
|
||||
{tick: 24, inBackOff: true, value: 10},
|
||||
{tick: 25, inBackOff: false, value: 10},
|
||||
{tick: 26, inBackOff: true, value: 10},
|
||||
{tick: 27, inBackOff: true, value: 10},
|
||||
{tick: 28, inBackOff: true, value: 10},
|
||||
{tick: 29, inBackOff: true, value: 10},
|
||||
{tick: 30, inBackOff: true, value: 10},
|
||||
{tick: 31, inBackOff: true, value: 10},
|
||||
{tick: 32, inBackOff: true, value: 10},
|
||||
{tick: 33, inBackOff: true, value: 10},
|
||||
{tick: 34, inBackOff: true, value: 10},
|
||||
{tick: 35, inBackOff: false, value: 10},
|
||||
{tick: 56, inBackOff: false, value: 0},
|
||||
{tick: 57, inBackOff: false, value: 1},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
tc.SetTime(startTime.Add(c.tick * step))
|
||||
if c.inBackOff != b.IsInBackOffSinceUpdate(id, tc.Now()) {
|
||||
t.Errorf("expected IsInBackOffSinceUpdate %v got %v at tick %s", c.inBackOff, b.IsInBackOffSinceUpdate(id, tc.Now()), c.tick*step)
|
||||
}
|
||||
|
||||
if c.inBackOff && (time.Duration(c.value)*step != b.Get(id)) {
|
||||
t.Errorf("expected backoff value=%s got %s at tick %s", time.Duration(c.value)*step, b.Get(id), c.tick*step)
|
||||
}
|
||||
|
||||
if !c.inBackOff {
|
||||
b.Next(id, tc.Now())
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
/*
|
||||
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 flowcontrol
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMultithreadedThrottling(t *testing.T) {
|
||||
// Bucket with 100QPS and no burst
|
||||
r := NewTokenBucketRateLimiter(100, 1)
|
||||
|
||||
// channel to collect 100 tokens
|
||||
taken := make(chan bool, 100)
|
||||
|
||||
// Set up goroutines to hammer the throttler
|
||||
startCh := make(chan bool)
|
||||
endCh := make(chan bool)
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
// wait for the starting signal
|
||||
<-startCh
|
||||
for {
|
||||
// get a token
|
||||
r.Accept()
|
||||
select {
|
||||
// try to add it to the taken channel
|
||||
case taken <- true:
|
||||
continue
|
||||
// if taken is full, notify and return
|
||||
default:
|
||||
endCh <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// record wall time
|
||||
startTime := time.Now()
|
||||
// take the initial capacity so all tokens are the result of refill
|
||||
r.Accept()
|
||||
// start the thundering herd
|
||||
close(startCh)
|
||||
// wait for the first signal that we collected 100 tokens
|
||||
<-endCh
|
||||
// record wall time
|
||||
endTime := time.Now()
|
||||
|
||||
// tolerate a 1% clock change because these things happen
|
||||
if duration := endTime.Sub(startTime); duration < (time.Second * 99 / 100) {
|
||||
// We shouldn't be able to get 100 tokens out of the bucket in less than 1 second of wall clock time, no matter what
|
||||
t.Errorf("Expected it to take at least 1 second to get 100 tokens, took %v", duration)
|
||||
} else {
|
||||
t.Logf("Took %v to get 100 tokens", duration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicThrottle(t *testing.T) {
|
||||
r := NewTokenBucketRateLimiter(1, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
if !r.TryAccept() {
|
||||
t.Error("unexpected false accept")
|
||||
}
|
||||
}
|
||||
if r.TryAccept() {
|
||||
t.Error("unexpected true accept")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementThrottle(t *testing.T) {
|
||||
r := NewTokenBucketRateLimiter(1, 1)
|
||||
if !r.TryAccept() {
|
||||
t.Error("unexpected false accept")
|
||||
}
|
||||
if r.TryAccept() {
|
||||
t.Error("unexpected true accept")
|
||||
}
|
||||
|
||||
// Allow to refill
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
if !r.TryAccept() {
|
||||
t.Error("unexpected false accept")
|
||||
}
|
||||
}
|
||||
|
||||
func TestThrottle(t *testing.T) {
|
||||
r := NewTokenBucketRateLimiter(10, 5)
|
||||
|
||||
// Should consume 5 tokens immediately, then
|
||||
// the remaining 11 should take at least 1 second (0.1s each)
|
||||
expectedFinish := time.Now().Add(time.Second * 1)
|
||||
for i := 0; i < 16; i++ {
|
||||
r.Accept()
|
||||
}
|
||||
if time.Now().Before(expectedFinish) {
|
||||
t.Error("rate limit was not respected, finished too early")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiterSaturation(t *testing.T) {
|
||||
const e = 0.000001
|
||||
tests := []struct {
|
||||
capacity int
|
||||
take int
|
||||
|
||||
expectedSaturation float64
|
||||
}{
|
||||
{1, 1, 1},
|
||||
{10, 3, 0.3},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
rl := NewTokenBucketRateLimiter(1, tt.capacity)
|
||||
for i := 0; i < tt.take; i++ {
|
||||
rl.Accept()
|
||||
}
|
||||
if math.Abs(rl.Saturation()-tt.expectedSaturation) > e {
|
||||
t.Fatalf("#%d: Saturation rate difference isn't within tolerable range\n want=%f, get=%f",
|
||||
i, tt.expectedSaturation, rl.Saturation())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlwaysFake(t *testing.T) {
|
||||
rl := NewFakeAlwaysRateLimiter()
|
||||
if !rl.TryAccept() {
|
||||
t.Error("TryAccept in AlwaysFake should return true.")
|
||||
}
|
||||
// If this will block the test will timeout
|
||||
rl.Accept()
|
||||
}
|
||||
|
||||
func TestNeverFake(t *testing.T) {
|
||||
rl := NewFakeNeverRateLimiter()
|
||||
if rl.TryAccept() {
|
||||
t.Error("TryAccept in NeverFake should return false.")
|
||||
}
|
||||
|
||||
finished := false
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
rl.Accept()
|
||||
finished = true
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// Wait some time to make sure it never finished.
|
||||
time.Sleep(time.Second)
|
||||
if finished {
|
||||
t.Error("Accept should block forever in NeverFake.")
|
||||
}
|
||||
|
||||
rl.Stop()
|
||||
wg.Wait()
|
||||
if !finished {
|
||||
t.Error("Stop should make Accept unblock in NeverFake.")
|
||||
}
|
||||
}
|
244
staging/src/k8s.io/client-go/pkg/util/integer/integer_test.go
Normal file
244
staging/src/k8s.io/client-go/pkg/util/integer/integer_test.go
Normal file
@ -0,0 +1,244 @@
|
||||
/*
|
||||
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 integer
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIntMax(t *testing.T) {
|
||||
tests := []struct {
|
||||
nums []int
|
||||
expectedMax int
|
||||
}{
|
||||
{
|
||||
nums: []int{-1, 0},
|
||||
expectedMax: 0,
|
||||
},
|
||||
{
|
||||
nums: []int{-1, -2},
|
||||
expectedMax: -1,
|
||||
},
|
||||
{
|
||||
nums: []int{0, 1},
|
||||
expectedMax: 1,
|
||||
},
|
||||
{
|
||||
nums: []int{1, 2},
|
||||
expectedMax: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Logf("executing scenario %d", i)
|
||||
if max := IntMax(test.nums[0], test.nums[1]); max != test.expectedMax {
|
||||
t.Errorf("expected %v, got %v", test.expectedMax, max)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntMin(t *testing.T) {
|
||||
tests := []struct {
|
||||
nums []int
|
||||
expectedMin int
|
||||
}{
|
||||
{
|
||||
nums: []int{-1, 0},
|
||||
expectedMin: -1,
|
||||
},
|
||||
{
|
||||
nums: []int{-1, -2},
|
||||
expectedMin: -2,
|
||||
},
|
||||
{
|
||||
nums: []int{0, 1},
|
||||
expectedMin: 0,
|
||||
},
|
||||
{
|
||||
nums: []int{1, 2},
|
||||
expectedMin: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Logf("executing scenario %d", i)
|
||||
if min := IntMin(test.nums[0], test.nums[1]); min != test.expectedMin {
|
||||
t.Errorf("expected %v, got %v", test.expectedMin, min)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInt32Max(t *testing.T) {
|
||||
tests := []struct {
|
||||
nums []int32
|
||||
expectedMax int32
|
||||
}{
|
||||
{
|
||||
nums: []int32{-1, 0},
|
||||
expectedMax: 0,
|
||||
},
|
||||
{
|
||||
nums: []int32{-1, -2},
|
||||
expectedMax: -1,
|
||||
},
|
||||
{
|
||||
nums: []int32{0, 1},
|
||||
expectedMax: 1,
|
||||
},
|
||||
{
|
||||
nums: []int32{1, 2},
|
||||
expectedMax: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Logf("executing scenario %d", i)
|
||||
if max := Int32Max(test.nums[0], test.nums[1]); max != test.expectedMax {
|
||||
t.Errorf("expected %v, got %v", test.expectedMax, max)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInt32Min(t *testing.T) {
|
||||
tests := []struct {
|
||||
nums []int32
|
||||
expectedMin int32
|
||||
}{
|
||||
{
|
||||
nums: []int32{-1, 0},
|
||||
expectedMin: -1,
|
||||
},
|
||||
{
|
||||
nums: []int32{-1, -2},
|
||||
expectedMin: -2,
|
||||
},
|
||||
{
|
||||
nums: []int32{0, 1},
|
||||
expectedMin: 0,
|
||||
},
|
||||
{
|
||||
nums: []int32{1, 2},
|
||||
expectedMin: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Logf("executing scenario %d", i)
|
||||
if min := Int32Min(test.nums[0], test.nums[1]); min != test.expectedMin {
|
||||
t.Errorf("expected %v, got %v", test.expectedMin, min)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInt64Max(t *testing.T) {
|
||||
tests := []struct {
|
||||
nums []int64
|
||||
expectedMax int64
|
||||
}{
|
||||
{
|
||||
nums: []int64{-1, 0},
|
||||
expectedMax: 0,
|
||||
},
|
||||
{
|
||||
nums: []int64{-1, -2},
|
||||
expectedMax: -1,
|
||||
},
|
||||
{
|
||||
nums: []int64{0, 1},
|
||||
expectedMax: 1,
|
||||
},
|
||||
{
|
||||
nums: []int64{1, 2},
|
||||
expectedMax: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Logf("executing scenario %d", i)
|
||||
if max := Int64Max(test.nums[0], test.nums[1]); max != test.expectedMax {
|
||||
t.Errorf("expected %v, got %v", test.expectedMax, max)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInt64Min(t *testing.T) {
|
||||
tests := []struct {
|
||||
nums []int64
|
||||
expectedMin int64
|
||||
}{
|
||||
{
|
||||
nums: []int64{-1, 0},
|
||||
expectedMin: -1,
|
||||
},
|
||||
{
|
||||
nums: []int64{-1, -2},
|
||||
expectedMin: -2,
|
||||
},
|
||||
{
|
||||
nums: []int64{0, 1},
|
||||
expectedMin: 0,
|
||||
},
|
||||
{
|
||||
nums: []int64{1, 2},
|
||||
expectedMin: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Logf("executing scenario %d", i)
|
||||
if min := Int64Min(test.nums[0], test.nums[1]); min != test.expectedMin {
|
||||
t.Errorf("expected %v, got %v", test.expectedMin, min)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundToInt32(t *testing.T) {
|
||||
tests := []struct {
|
||||
num float64
|
||||
exp int32
|
||||
}{
|
||||
{
|
||||
num: 5.5,
|
||||
exp: 6,
|
||||
},
|
||||
{
|
||||
num: -3.7,
|
||||
exp: -4,
|
||||
},
|
||||
{
|
||||
num: 3.49,
|
||||
exp: 3,
|
||||
},
|
||||
{
|
||||
num: -7.9,
|
||||
exp: -8,
|
||||
},
|
||||
{
|
||||
num: -4.499999,
|
||||
exp: -4,
|
||||
},
|
||||
{
|
||||
num: 0,
|
||||
exp: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Logf("executing scenario %d", i)
|
||||
if got := RoundToInt32(test.num); got != test.exp {
|
||||
t.Errorf("expected %d, got %d", test.exp, got)
|
||||
}
|
||||
}
|
||||
}
|
@ -44,7 +44,6 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/pkg/api/v1"
|
||||
pathvalidation "k8s.io/client-go/pkg/api/validation/path"
|
||||
"k8s.io/client-go/pkg/util/flowcontrol"
|
||||
restclientwatch "k8s.io/client-go/rest/watch"
|
||||
"k8s.io/client-go/tools/metrics"
|
||||
@ -179,7 +178,7 @@ func (r *Request) Resource(resource string) *Request {
|
||||
r.err = fmt.Errorf("resource already set to %q, cannot change to %q", r.resource, resource)
|
||||
return r
|
||||
}
|
||||
if msgs := pathvalidation.IsValidPathSegmentName(resource); len(msgs) != 0 {
|
||||
if msgs := IsValidPathSegmentName(resource); len(msgs) != 0 {
|
||||
r.err = fmt.Errorf("invalid resource %q: %v", resource, msgs)
|
||||
return r
|
||||
}
|
||||
@ -199,7 +198,7 @@ func (r *Request) SubResource(subresources ...string) *Request {
|
||||
return r
|
||||
}
|
||||
for _, s := range subresources {
|
||||
if msgs := pathvalidation.IsValidPathSegmentName(s); len(msgs) != 0 {
|
||||
if msgs := IsValidPathSegmentName(s); len(msgs) != 0 {
|
||||
r.err = fmt.Errorf("invalid subresource %q: %v", s, msgs)
|
||||
return r
|
||||
}
|
||||
@ -221,7 +220,7 @@ func (r *Request) Name(resourceName string) *Request {
|
||||
r.err = fmt.Errorf("resource name already set to %q, cannot change to %q", r.resourceName, resourceName)
|
||||
return r
|
||||
}
|
||||
if msgs := pathvalidation.IsValidPathSegmentName(resourceName); len(msgs) != 0 {
|
||||
if msgs := IsValidPathSegmentName(resourceName); len(msgs) != 0 {
|
||||
r.err = fmt.Errorf("invalid resource name %q: %v", resourceName, msgs)
|
||||
return r
|
||||
}
|
||||
@ -238,7 +237,7 @@ func (r *Request) Namespace(namespace string) *Request {
|
||||
r.err = fmt.Errorf("namespace already set to %q, cannot change to %q", r.namespace, namespace)
|
||||
return r
|
||||
}
|
||||
if msgs := pathvalidation.IsValidPathSegmentName(namespace); len(msgs) != 0 {
|
||||
if msgs := IsValidPathSegmentName(namespace); len(msgs) != 0 {
|
||||
r.err = fmt.Errorf("invalid namespace %q: %v", namespace, msgs)
|
||||
return r
|
||||
}
|
||||
@ -760,10 +759,11 @@ func (r *Request) Stream() (io.ReadCloser, error) {
|
||||
defer resp.Body.Close()
|
||||
|
||||
result := r.transformResponse(resp, req)
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
err := result.Error()
|
||||
if err == nil {
|
||||
err = fmt.Errorf("%d while accessing %v: %s", result.statusCode, url, string(result.body))
|
||||
}
|
||||
return nil, fmt.Errorf("%d while accessing %v: %s", result.statusCode, url, string(result.body))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -1197,3 +1197,49 @@ func (r Result) Error() error {
|
||||
}
|
||||
return r.err
|
||||
}
|
||||
|
||||
// NameMayNotBe specifies strings that cannot be used as names specified as path segments (like the REST API or etcd store)
|
||||
var NameMayNotBe = []string{".", ".."}
|
||||
|
||||
// NameMayNotContain specifies substrings that cannot be used in names specified as path segments (like the REST API or etcd store)
|
||||
var NameMayNotContain = []string{"/", "%"}
|
||||
|
||||
// IsValidPathSegmentName validates the name can be safely encoded as a path segment
|
||||
func IsValidPathSegmentName(name string) []string {
|
||||
for _, illegalName := range NameMayNotBe {
|
||||
if name == illegalName {
|
||||
return []string{fmt.Sprintf(`may not be '%s'`, illegalName)}
|
||||
}
|
||||
}
|
||||
|
||||
var errors []string
|
||||
for _, illegalContent := range NameMayNotContain {
|
||||
if strings.Contains(name, illegalContent) {
|
||||
errors = append(errors, fmt.Sprintf(`may not contain '%s'`, illegalContent))
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// IsValidPathSegmentPrefix validates the name can be used as a prefix for a name which will be encoded as a path segment
|
||||
// It does not check for exact matches with disallowed names, since an arbitrary suffix might make the name valid
|
||||
func IsValidPathSegmentPrefix(name string) []string {
|
||||
var errors []string
|
||||
for _, illegalContent := range NameMayNotContain {
|
||||
if strings.Contains(name, illegalContent) {
|
||||
errors = append(errors, fmt.Sprintf(`may not contain '%s'`, illegalContent))
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// ValidatePathSegmentName validates the name can be safely encoded as a path segment
|
||||
func ValidatePathSegmentName(name string, prefix bool) []string {
|
||||
if prefix {
|
||||
return IsValidPathSegmentPrefix(name)
|
||||
} else {
|
||||
return IsValidPathSegmentName(name)
|
||||
}
|
||||
}
|
||||
|
@ -868,6 +868,7 @@ func TestRequestStream(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Request *Request
|
||||
Err bool
|
||||
ErrFn func(error) bool
|
||||
}{
|
||||
{
|
||||
Request: &Request{err: errors.New("bail")},
|
||||
@ -903,6 +904,26 @@ func TestRequestStream(t *testing.T) {
|
||||
},
|
||||
Err: true,
|
||||
},
|
||||
{
|
||||
Request: &Request{
|
||||
client: clientFunc(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"a container name must be specified for pod kube-dns-v20-mz5cv, choose one of: [kubedns dnsmasq healthz]","reason":"BadRequest","code":400}`))),
|
||||
}, nil
|
||||
}),
|
||||
content: defaultContentConfig(),
|
||||
serializers: defaultSerializers(),
|
||||
baseURL: &url.URL{},
|
||||
},
|
||||
Err: true,
|
||||
ErrFn: func(err error) bool {
|
||||
if err.Error() == "a container name must be specified for pod kube-dns-v20-mz5cv, choose one of: [kubedns dnsmasq healthz]" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
testCase.Request.backoffMgr = &NoBackoff{}
|
||||
@ -914,6 +935,12 @@ func TestRequestStream(t *testing.T) {
|
||||
if hasErr && body != nil {
|
||||
t.Errorf("%d: body should be nil when error is returned", i)
|
||||
}
|
||||
|
||||
if hasErr {
|
||||
if testCase.ErrFn != nil && !testCase.ErrFn(err) {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user