mirror of
https://github.com/kubernetes/client-go.git
synced 2025-09-17 23:57:52 +00:00
published by bot
(https://github.com/kubernetes/contrib/tree/master/mungegithub) Directory 1.4 is copied from https://github.com/kubernetes/kubernetes.git, branch release-1.4, last commit is 13fe69db3769db0ae6e6d0092d18c91a5c04ca1c Directory 1.5 is copied from https://github.com/kubernetes/kubernetes.git, branch master, last commit is 0cd24992c2ed1e46fbac9058ded5fae362b6ceef
This commit is contained in:
224
1.5/rest/client.go
Normal file
224
1.5/rest/client.go
Normal file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
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 rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/client-go/1.5/pkg/api"
|
||||
"k8s.io/client-go/1.5/pkg/api/unversioned"
|
||||
"k8s.io/client-go/1.5/pkg/runtime"
|
||||
"k8s.io/client-go/1.5/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"
|
||||
)
|
||||
|
||||
// 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 undelying 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 = &unversioned.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.
|
||||
func createSerializers(config ContentConfig) (*Serializers, error) {
|
||||
negotiated := config.NegotiatedSerializer
|
||||
contentType := config.ContentType
|
||||
info, ok := negotiated.SerializerForMediaType(contentType, nil)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("serializer for %s not registered", contentType)
|
||||
}
|
||||
streamInfo, ok := negotiated.StreamingSerializerForMediaType(contentType, nil)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("streaming serializer for %s not registered", contentType)
|
||||
}
|
||||
internalGV := unversioned.GroupVersion{
|
||||
Group: config.GroupVersion.Group,
|
||||
Version: runtime.APIVersionInternal,
|
||||
}
|
||||
return &Serializers{
|
||||
Encoder: negotiated.EncoderForVersion(info.Serializer, *config.GroupVersion),
|
||||
Decoder: negotiated.DecoderToVersion(info.Serializer, internalGV),
|
||||
StreamingSerializer: streamInfo.Serializer,
|
||||
Framer: streamInfo.Framer,
|
||||
RenegotiatedDecoder: func(contentType string, params map[string]string) (runtime.Decoder, error) {
|
||||
renegotiated, ok := negotiated.SerializerForMediaType(contentType, params)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("serializer for %s not registered", contentType)
|
||||
}
|
||||
return negotiated.DecoderToVersion(renegotiated.Serializer, internalGV), nil
|
||||
},
|
||||
}, 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 api.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() unversioned.GroupVersion {
|
||||
return *c.contentConfig.GroupVersion
|
||||
}
|
282
1.5/rest/client_test.go
Normal file
282
1.5/rest/client_test.go
Normal file
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
|
||||
"k8s.io/client-go/1.5/pkg/api"
|
||||
"k8s.io/client-go/1.5/pkg/api/errors"
|
||||
"k8s.io/client-go/1.5/pkg/api/testapi"
|
||||
"k8s.io/client-go/1.5/pkg/api/unversioned"
|
||||
"k8s.io/client-go/1.5/pkg/runtime"
|
||||
"k8s.io/client-go/1.5/pkg/util/diff"
|
||||
utiltesting "k8s.io/client-go/1.5/pkg/util/testing"
|
||||
)
|
||||
|
||||
type TestParam struct {
|
||||
actualError error
|
||||
expectingError bool
|
||||
actualCreated bool
|
||||
expCreated bool
|
||||
expStatus *unversioned.Status
|
||||
testBody bool
|
||||
testBodyErrorIsNotNil bool
|
||||
}
|
||||
|
||||
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 := &unversioned.Status{
|
||||
Code: http.StatusNotFound,
|
||||
Status: unversioned.StatusFailure,
|
||||
Reason: unversioned.StatusReasonNotFound,
|
||||
Message: " \"\" not found",
|
||||
Details: &unversioned.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)
|
||||
}
|
||||
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()
|
||||
expected := *status
|
||||
// The decoder will apply the default Version and Kind to the Status.
|
||||
expected.APIVersion = "v1"
|
||||
expected.Kind = "Status"
|
||||
if !reflect.DeepEqual(&expected, &actual) {
|
||||
t.Errorf("Unexpected mis-match: %s", diff.ObjectDiff(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) {
|
||||
if testParam.expectingError {
|
||||
if testParam.actualError == nil {
|
||||
t.Errorf("Expected error")
|
||||
}
|
||||
}
|
||||
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, "/"+testapi.Default.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(api.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, *unversioned.Status) {
|
||||
status := &unversioned.Status{Status: fmt.Sprintf("%s", unversioned.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: testapi.Default.GroupVersion(),
|
||||
NegotiatedSerializer: testapi.Default.NegotiatedSerializer(),
|
||||
},
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
})
|
||||
return c, err
|
||||
}
|
335
1.5/rest/config.go
Normal file
335
1.5/rest/config.go
Normal file
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
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 rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
gruntime "runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/client-go/1.5/pkg/api"
|
||||
"k8s.io/client-go/1.5/pkg/api/unversioned"
|
||||
"k8s.io/client-go/1.5/pkg/runtime"
|
||||
"k8s.io/client-go/1.5/pkg/util/crypto"
|
||||
"k8s.io/client-go/1.5/pkg/util/flowcontrol"
|
||||
"k8s.io/client-go/1.5/pkg/version"
|
||||
clientcmdapi "k8s.io/client-go/1.5/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
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 username that this RESTClient will impersonate
|
||||
Impersonate string
|
||||
|
||||
// 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
|
||||
|
||||
// Version forces a specific version to be used (if registered)
|
||||
// Do we need this?
|
||||
// Version 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 *unversioned.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}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
versionConfig := config.ContentConfig
|
||||
if versionConfig.GroupVersion == nil {
|
||||
v := unversioned.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 := crypto.CertPoolFromFile(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 rest.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
|
||||
}
|
99
1.5/rest/config_test.go
Normal file
99
1.5/rest/config_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
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 rest
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/1.5/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: testapi.Default.GroupVersion()}}); err == nil {
|
||||
t.Errorf("unexpected non-error")
|
||||
}
|
||||
if _, err := RESTClientFor(&Config{Host: "127.0.0.1", ContentConfig: ContentConfig{GroupVersion: testapi.Default.GroupVersion(), NegotiatedSerializer: testapi.Default.NegotiatedSerializer()}}); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
73
1.5/rest/plugin.go
Normal file
73
1.5/rest/plugin.go
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
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 rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
clientcmdapi "k8s.io/client-go/1.5/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)
|
||||
}
|
311
1.5/rest/plugin_test.go
Normal file
311
1.5/rest/plugin_test.go
Normal file
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
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 rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
clientcmdapi "k8s.io/client-go/1.5/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
|
||||
}
|
1111
1.5/rest/request.go
Normal file
1111
1.5/rest/request.go
Normal file
File diff suppressed because it is too large
Load Diff
1552
1.5/rest/request_test.go
Executable file
1552
1.5/rest/request_test.go
Executable file
File diff suppressed because it is too large
Load Diff
94
1.5/rest/transport.go
Normal file
94
1.5/rest/transport.go
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
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 rest
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/client-go/1.5/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: c.Impersonate,
|
||||
}, nil
|
||||
}
|
93
1.5/rest/url_utils.go
Normal file
93
1.5/rest/url_utils.go
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
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 rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"k8s.io/client-go/1.5/pkg/api/unversioned"
|
||||
)
|
||||
|
||||
// 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 unversioned.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 {
|
||||
return nil, "", err
|
||||
}
|
||||
if 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, unversioned.GroupVersion{}, defaultTLS)
|
||||
}
|
61
1.5/rest/url_utils_test.go
Normal file
61
1.5/rest/url_utils_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rest
|
||||
|
||||
import (
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/1.5/pkg/api/testapi"
|
||||
)
|
||||
|
||||
func TestValidatesHostParameter(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Host string
|
||||
APIPath string
|
||||
|
||||
URL string
|
||||
Err bool
|
||||
}{
|
||||
{"127.0.0.1", "", "http://127.0.0.1/" + testapi.Default.GroupVersion().Version, false},
|
||||
{"127.0.0.1:8080", "", "http://127.0.0.1:8080/" + testapi.Default.GroupVersion().Version, false},
|
||||
{"foo.bar.com", "", "http://foo.bar.com/" + testapi.Default.GroupVersion().Version, false},
|
||||
{"http://host/prefix", "", "http://host/prefix/" + testapi.Default.GroupVersion().Version, false},
|
||||
{"http://host", "", "http://host/" + testapi.Default.GroupVersion().Version, false},
|
||||
{"http://host", "/", "http://host/" + testapi.Default.GroupVersion().Version, false},
|
||||
{"http://host", "/other", "http://host/other/" + testapi.Default.GroupVersion().Version, false},
|
||||
{"host/server", "", "", true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
u, versionedAPIPath, err := DefaultServerURL(testCase.Host, testCase.APIPath, *testapi.Default.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
|
||||
}
|
||||
}
|
||||
}
|
107
1.5/rest/urlbackoff.go
Normal file
107
1.5/rest/urlbackoff.go
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
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 rest
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/client-go/1.5/pkg/util/flowcontrol"
|
||||
"k8s.io/client-go/1.5/pkg/util/sets"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
79
1.5/rest/urlbackoff_test.go
Normal file
79
1.5/rest/urlbackoff_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
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 rest
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/client-go/1.5/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])
|
||||
}
|
||||
}
|
88
1.5/rest/versions.go
Normal file
88
1.5/rest/versions.go
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
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 rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"k8s.io/client-go/1.5/pkg/api/unversioned"
|
||||
)
|
||||
|
||||
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 unversioned.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 unversioned.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
|
||||
}
|
Reference in New Issue
Block a user