mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-19 00:31:00 +00:00
Make client.Request more testable, break coupling with RESTClient
Moves polling to a function provided by the RESTClient, not innate to Request. Moves doRequest from RESTClient to Request for clarity.
This commit is contained in:
parent
1da5c444e8
commit
eac933eb44
@ -603,6 +603,7 @@ type Status struct {
|
|||||||
type StatusDetails struct {
|
type StatusDetails struct {
|
||||||
// The ID attribute of the resource associated with the status StatusReason
|
// The ID attribute of the resource associated with the status StatusReason
|
||||||
// (when there is a single ID which can be described).
|
// (when there is a single ID which can be described).
|
||||||
|
// TODO: replace with Name
|
||||||
ID string `json:"id,omitempty" yaml:"id,omitempty"`
|
ID string `json:"id,omitempty" yaml:"id,omitempty"`
|
||||||
// The kind attribute of the resource associated with the status StatusReason.
|
// The kind attribute of the resource associated with the status StatusReason.
|
||||||
// On some operations may differ from the requested resource Kind.
|
// On some operations may differ from the requested resource Kind.
|
||||||
|
@ -28,32 +28,65 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||||
watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json"
|
watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json"
|
||||||
"github.com/golang/glog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// specialParams lists parameters that are handled specially and which users of Request
|
// specialParams lists parameters that are handled specially and which users of Request
|
||||||
// are therefore not allowed to set manually.
|
// are therefore not allowed to set manually.
|
||||||
var specialParams = util.NewStringSet("sync", "timeout")
|
var specialParams = util.NewStringSet("sync", "timeout")
|
||||||
|
|
||||||
|
// PollFunc is called when a server operation returns 202 accepted. The name of the
|
||||||
|
// operation is extracted from the response and passed to this function. Return a
|
||||||
|
// request to retrieve the result of the operation, or false for the second argument
|
||||||
|
// if polling should end.
|
||||||
|
type PollFunc func(name string) (*Request, bool)
|
||||||
|
|
||||||
|
// HTTPClient is an interface for testing a request object.
|
||||||
|
type HTTPClient interface {
|
||||||
|
Do(req *http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Request allows for building up a request to a server in a chained fashion.
|
// Request allows for building up a request to a server in a chained fashion.
|
||||||
// Any errors are stored until the end of your call, so you only have to
|
// Any errors are stored until the end of your call, so you only have to
|
||||||
// check once.
|
// check once.
|
||||||
type Request struct {
|
type Request struct {
|
||||||
c *RESTClient
|
// required
|
||||||
err error
|
client HTTPClient
|
||||||
verb string
|
verb string
|
||||||
path string
|
baseURL *url.URL
|
||||||
body io.Reader
|
codec runtime.Codec
|
||||||
params map[string]string
|
|
||||||
selector labels.Selector
|
// optional, will be invoked if the server returns a 202 to decide
|
||||||
timeout time.Duration
|
// whether to poll.
|
||||||
sync bool
|
poller PollFunc
|
||||||
pollPeriod time.Duration
|
|
||||||
|
// accessible via method setters
|
||||||
|
path string
|
||||||
|
params map[string]string
|
||||||
|
selector labels.Selector
|
||||||
|
sync bool
|
||||||
|
timeout time.Duration
|
||||||
|
|
||||||
|
// output
|
||||||
|
err error
|
||||||
|
body io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRequest creates a new request with the core attributes.
|
||||||
|
func NewRequest(client HTTPClient, verb string, baseURL *url.URL, codec runtime.Codec) *Request {
|
||||||
|
return &Request{
|
||||||
|
client: client,
|
||||||
|
verb: verb,
|
||||||
|
baseURL: baseURL,
|
||||||
|
codec: codec,
|
||||||
|
|
||||||
|
path: baseURL.Path,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path appends an item to the request path. You must call Path at least once.
|
// Path appends an item to the request path. You must call Path at least once.
|
||||||
@ -135,6 +168,9 @@ func (r *Request) setParam(paramName, value string) *Request {
|
|||||||
r.err = fmt.Errorf("must set %v through the corresponding function, not directly.", paramName)
|
r.err = fmt.Errorf("must set %v through the corresponding function, not directly.", paramName)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
if r.params == nil {
|
||||||
|
r.params = make(map[string]string)
|
||||||
|
}
|
||||||
r.params[paramName] = value
|
r.params[paramName] = value
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@ -172,7 +208,7 @@ func (r *Request) Body(obj interface{}) *Request {
|
|||||||
case io.Reader:
|
case io.Reader:
|
||||||
r.body = t
|
r.body = t
|
||||||
case runtime.Object:
|
case runtime.Object:
|
||||||
data, err := r.c.Codec.Encode(t)
|
data, err := r.codec.Encode(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.err = err
|
r.err = err
|
||||||
return r
|
return r
|
||||||
@ -184,21 +220,23 @@ func (r *Request) Body(obj interface{}) *Request {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// PollPeriod sets the poll period.
|
// NoPoll indicates a server "working" response should be returned as an error
|
||||||
// If the server sends back a "working" status message, then repeatedly poll the server
|
func (r *Request) NoPoll() *Request {
|
||||||
// to see if the operation has completed yet, waiting 'd' between each poll.
|
return r.Poller(nil)
|
||||||
// If you want to handle the "working" status yourself (it'll be delivered as StatusErr),
|
}
|
||||||
// set d to 0 to turn off this behavior.
|
|
||||||
func (r *Request) PollPeriod(d time.Duration) *Request {
|
// Poller indicates this request should use the specify poll function to determine whether
|
||||||
|
// a server "working" response should be retried.
|
||||||
|
func (r *Request) Poller(poller PollFunc) *Request {
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
r.pollPeriod = d
|
r.poller = poller
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Request) finalURL() string {
|
func (r *Request) finalURL() string {
|
||||||
finalURL := *r.c.baseURL
|
finalURL := *r.baseURL
|
||||||
finalURL.Path = r.path
|
finalURL.Path = r.path
|
||||||
query := url.Values{}
|
query := url.Values{}
|
||||||
for key, value := range r.params {
|
for key, value := range r.params {
|
||||||
@ -227,18 +265,18 @@ func (r *Request) Watch() (watch.Interface, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
client := r.c.Client
|
client := r.client
|
||||||
if client == nil {
|
if client == nil {
|
||||||
client = http.DefaultClient
|
client = http.DefaultClient
|
||||||
}
|
}
|
||||||
response, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if response.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("Got status: %v", response.StatusCode)
|
return nil, fmt.Errorf("Got status: %v", resp.StatusCode)
|
||||||
}
|
}
|
||||||
return watch.NewStreamWatcher(watchjson.NewDecoder(response.Body, r.c.Codec)), nil
|
return watch.NewStreamWatcher(watchjson.NewDecoder(resp.Body, r.codec)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream formats and executes the request, and offers streaming of the response.
|
// Stream formats and executes the request, and offers streaming of the response.
|
||||||
@ -251,51 +289,106 @@ func (r *Request) Stream() (io.ReadCloser, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
client := r.c.Client
|
client := r.client
|
||||||
if client == nil {
|
if client == nil {
|
||||||
client = http.DefaultClient
|
client = http.DefaultClient
|
||||||
}
|
}
|
||||||
response, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return response.Body, nil
|
return resp.Body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do formats and executes the request. Returns the API object received, or an error.
|
// Do formats and executes the request. Returns a Result object for easy response
|
||||||
|
// processing. Handles polling the server in the event a continuation was sent.
|
||||||
func (r *Request) Do() Result {
|
func (r *Request) Do() Result {
|
||||||
|
client := r.client
|
||||||
|
if client == nil {
|
||||||
|
client = http.DefaultClient
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
return Result{err: r.err}
|
return Result{err: r.err}
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(r.verb, r.finalURL(), r.body)
|
req, err := http.NewRequest(r.verb, r.finalURL(), r.body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Result{err: err}
|
return Result{err: err}
|
||||||
}
|
}
|
||||||
respBody, created, err := r.c.doRequest(req)
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if s, ok := err.(APIStatus); ok {
|
return Result{err: err}
|
||||||
status := s.Status()
|
|
||||||
if status.Status == api.StatusWorking && r.pollPeriod != 0 {
|
|
||||||
if status.Details != nil {
|
|
||||||
id := status.Details.ID
|
|
||||||
if len(id) > 0 {
|
|
||||||
glog.Infof("Waiting for completion of /operations/%s", id)
|
|
||||||
time.Sleep(r.pollPeriod)
|
|
||||||
// Make a poll request
|
|
||||||
pollOp := r.c.PollFor(id).PollPeriod(r.pollPeriod)
|
|
||||||
// Could also say "return r.Do()" but this way doesn't grow the callstack.
|
|
||||||
r = pollOp
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Result{respBody, created, err, r.c.Codec}
|
|
||||||
|
respBody, created, err := r.transformResponse(resp, req)
|
||||||
|
if poll, ok := r.shouldPoll(err); ok {
|
||||||
|
r = poll
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result{respBody, created, err, r.codec}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shouldPoll checks the server error for an incomplete operation
|
||||||
|
// and if found returns a request that would check the response.
|
||||||
|
// If no polling is necessary or possible, it will return false.
|
||||||
|
func (r *Request) shouldPoll(err error) (*Request, bool) {
|
||||||
|
if err == nil || r.poller == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
apistatus, ok := err.(APIStatus)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
status := apistatus.Status()
|
||||||
|
if status.Status != api.StatusWorking {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if status.Details == nil || len(status.Details.ID) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return r.poller(status.Details.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// transformResponse converts an API response into a structured API object.
|
||||||
|
func (r *Request) transformResponse(resp *http.Response, req *http.Request) ([]byte, bool, error) {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did the server give us a status response?
|
||||||
|
isStatusResponse := false
|
||||||
|
var status api.Status
|
||||||
|
if err := r.codec.DecodeInto(body, &status); err == nil && status.Status != "" {
|
||||||
|
isStatusResponse = true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent:
|
||||||
|
if !isStatusResponse {
|
||||||
|
return nil, false, fmt.Errorf("request [%#v] failed (%d) %s: %s", req, resp.StatusCode, resp.Status, string(body))
|
||||||
|
}
|
||||||
|
return nil, false, errors.FromObject(&status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the server gave us a status back, look at what it was.
|
||||||
|
if isStatusResponse && status.Status != api.StatusSuccess {
|
||||||
|
// "Working" requests need to be handled specially.
|
||||||
|
// "Failed" requests are clearly just an error and it makes sense to return them as such.
|
||||||
|
return nil, false, errors.FromObject(&status)
|
||||||
|
}
|
||||||
|
|
||||||
|
created := resp.StatusCode == http.StatusCreated
|
||||||
|
return body, created, err
|
||||||
|
}
|
||||||
|
|
||||||
// Result contains the result of calling Request.Do().
|
// Result contains the result of calling Request.Do().
|
||||||
type Result struct {
|
type Result struct {
|
||||||
body []byte
|
body []byte
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -29,6 +30,7 @@ import (
|
|||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||||
@ -38,6 +40,41 @@ import (
|
|||||||
watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json"
|
watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestTransformResponse(t *testing.T) {
|
||||||
|
invalid := []byte("aaaaa")
|
||||||
|
uri, _ := url.Parse("http://localhost")
|
||||||
|
testCases := []struct {
|
||||||
|
Response *http.Response
|
||||||
|
Data []byte
|
||||||
|
Created bool
|
||||||
|
Error bool
|
||||||
|
}{
|
||||||
|
{Response: &http.Response{StatusCode: 200}, Data: []byte{}},
|
||||||
|
{Response: &http.Response{StatusCode: 201}, Data: []byte{}, Created: true},
|
||||||
|
{Response: &http.Response{StatusCode: 199}, Error: true},
|
||||||
|
{Response: &http.Response{StatusCode: 500}, Error: true},
|
||||||
|
{Response: &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader(invalid))}, Data: invalid},
|
||||||
|
{Response: &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader(invalid))}, Data: invalid},
|
||||||
|
}
|
||||||
|
for i, test := range testCases {
|
||||||
|
r := NewRequest(nil, "", uri, testapi.Codec())
|
||||||
|
if test.Response.Body == nil {
|
||||||
|
test.Response.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
|
||||||
|
}
|
||||||
|
response, created, err := r.transformResponse(test.Response, &http.Request{})
|
||||||
|
hasErr := err != nil
|
||||||
|
if hasErr != test.Error {
|
||||||
|
t.Errorf("%d: unexpected error: %f %v", i, test.Error, err)
|
||||||
|
}
|
||||||
|
if !(test.Data == nil && response == nil) && !reflect.DeepEqual(test.Data, response) {
|
||||||
|
t.Errorf("%d: unexpected response: %#v %#v", i, test.Data, response)
|
||||||
|
}
|
||||||
|
if test.Created != created {
|
||||||
|
t.Errorf("%d: expected created %f, got %f", i, test.Created, created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDoRequestNewWay(t *testing.T) {
|
func TestDoRequestNewWay(t *testing.T) {
|
||||||
reqBody := "request body"
|
reqBody := "request body"
|
||||||
expectedObj := &api.Service{Port: 12345}
|
expectedObj := &api.Service{Port: 12345}
|
||||||
@ -48,6 +85,7 @@ func TestDoRequestNewWay(t *testing.T) {
|
|||||||
T: t,
|
T: t,
|
||||||
}
|
}
|
||||||
testServer := httptest.NewServer(&fakeHandler)
|
testServer := httptest.NewServer(&fakeHandler)
|
||||||
|
defer testServer.Close()
|
||||||
c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta2", Username: "user", Password: "pass"})
|
c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta2", Username: "user", Password: "pass"})
|
||||||
obj, err := c.Verb("POST").
|
obj, err := c.Verb("POST").
|
||||||
Path("foo/bar").
|
Path("foo/bar").
|
||||||
@ -351,15 +389,18 @@ func TestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetPollPeriod(t *testing.T) {
|
func TestSetPoller(t *testing.T) {
|
||||||
c := NewOrDie(&Config{})
|
c := NewOrDie(&Config{})
|
||||||
r := c.Get()
|
r := c.Get()
|
||||||
if r.pollPeriod == 0 {
|
if c.PollPeriod == 0 {
|
||||||
t.Errorf("polling should be on by default")
|
t.Errorf("polling should be on by default")
|
||||||
}
|
}
|
||||||
r.PollPeriod(time.Hour)
|
if r.poller == nil {
|
||||||
if r.pollPeriod != time.Hour {
|
t.Errorf("polling should be on by default")
|
||||||
t.Errorf("'PollPeriod' doesn't work")
|
}
|
||||||
|
r.NoPoll()
|
||||||
|
if r.poller != nil {
|
||||||
|
t.Errorf("'NoPoll' doesn't work")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,6 +415,16 @@ func TestPolling(t *testing.T) {
|
|||||||
|
|
||||||
callNumber := 0
|
callNumber := 0
|
||||||
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if callNumber == 0 {
|
||||||
|
if r.URL.Path != "/api/v1beta1/" {
|
||||||
|
t.Fatalf("unexpected request URL path %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if r.URL.Path != "/api/v1beta1/operations/1234" {
|
||||||
|
t.Fatalf("unexpected request URL path %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Logf("About to write %d", callNumber)
|
||||||
data, err := v1beta1.Codec.Encode(objects[callNumber])
|
data, err := v1beta1.Codec.Encode(objects[callNumber])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected encode error")
|
t.Errorf("Unexpected encode error")
|
||||||
@ -383,11 +434,11 @@ func TestPolling(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta1", Username: "user", Password: "pass"})
|
c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta1", Username: "user", Password: "pass"})
|
||||||
|
c.PollPeriod = 1 * time.Millisecond
|
||||||
trials := []func(){
|
trials := []func(){
|
||||||
func() {
|
func() {
|
||||||
// Check that we do indeed poll when asked to.
|
// Check that we do indeed poll when asked to.
|
||||||
obj, err := c.Get().PollPeriod(5 * time.Millisecond).Do().Get()
|
obj, err := c.Get().Do().Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error: %v %#v", err, err)
|
t.Errorf("Unexpected error: %v %#v", err, err)
|
||||||
return
|
return
|
||||||
@ -402,7 +453,7 @@ func TestPolling(t *testing.T) {
|
|||||||
},
|
},
|
||||||
func() {
|
func() {
|
||||||
// Check that we don't poll when asked not to.
|
// Check that we don't poll when asked not to.
|
||||||
obj, err := c.Get().PollPeriod(0).Do().Get()
|
obj, err := c.Get().NoPoll().Do().Get()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Unexpected non error: %v", obj)
|
t.Errorf("Unexpected non error: %v", obj)
|
||||||
return
|
return
|
||||||
|
@ -17,16 +17,13 @@ limitations under the License.
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RESTClient imposes common Kubernetes API conventions on a set of resource paths.
|
// RESTClient imposes common Kubernetes API conventions on a set of resource paths.
|
||||||
@ -45,7 +42,11 @@ type RESTClient struct {
|
|||||||
|
|
||||||
// Set specific behavior of the client. If not set http.DefaultClient will be
|
// Set specific behavior of the client. If not set http.DefaultClient will be
|
||||||
// used.
|
// used.
|
||||||
Client *http.Client
|
Client HTTPClient
|
||||||
|
|
||||||
|
// Set the poll behavior of this client. If not set the DefaultPoll method will
|
||||||
|
// be called.
|
||||||
|
Poller PollFunc
|
||||||
|
|
||||||
Sync bool
|
Sync bool
|
||||||
PollPeriod time.Duration
|
PollPeriod time.Duration
|
||||||
@ -68,56 +69,13 @@ func NewRESTClient(baseURL *url.URL, c runtime.Codec) *RESTClient {
|
|||||||
Codec: c,
|
Codec: c,
|
||||||
|
|
||||||
// Make asynchronous requests by default
|
// Make asynchronous requests by default
|
||||||
// TODO: flip me to the default
|
|
||||||
Sync: false,
|
Sync: false,
|
||||||
|
|
||||||
// Poll frequently when asynchronous requests are provided
|
// Poll frequently when asynchronous requests are provided
|
||||||
PollPeriod: time.Second * 2,
|
PollPeriod: time.Second * 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// doRequest executes a request against a server
|
|
||||||
func (c *RESTClient) doRequest(request *http.Request) ([]byte, bool, error) {
|
|
||||||
client := c.Client
|
|
||||||
if client == nil {
|
|
||||||
client = http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := client.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
body, err := ioutil.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return body, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Did the server give us a status response?
|
|
||||||
isStatusResponse := false
|
|
||||||
var status api.Status
|
|
||||||
if err := c.Codec.DecodeInto(body, &status); err == nil && status.Status != "" {
|
|
||||||
isStatusResponse = true
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case response.StatusCode < http.StatusOK || response.StatusCode > http.StatusPartialContent:
|
|
||||||
if !isStatusResponse {
|
|
||||||
return nil, false, fmt.Errorf("request [%#v] failed (%d) %s: %s", request, response.StatusCode, response.Status, string(body))
|
|
||||||
}
|
|
||||||
return nil, false, errors.FromObject(&status)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the server gave us a status back, look at what it was.
|
|
||||||
if isStatusResponse && status.Status != api.StatusSuccess {
|
|
||||||
// "Working" requests need to be handled specially.
|
|
||||||
// "Failed" requests are clearly just an error and it makes sense to return them as such.
|
|
||||||
return nil, false, errors.FromObject(&status)
|
|
||||||
}
|
|
||||||
|
|
||||||
created := response.StatusCode == http.StatusCreated
|
|
||||||
return body, created, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verb begins a request with a verb (GET, POST, PUT, DELETE).
|
// Verb begins a request with a verb (GET, POST, PUT, DELETE).
|
||||||
//
|
//
|
||||||
// Example usage of RESTClient's request building interface:
|
// Example usage of RESTClient's request building interface:
|
||||||
@ -136,15 +94,11 @@ func (c *RESTClient) Verb(verb string) *Request {
|
|||||||
// if c.Client != nil {
|
// if c.Client != nil {
|
||||||
// timeout = c.Client.Timeout
|
// timeout = c.Client.Timeout
|
||||||
// }
|
// }
|
||||||
return &Request{
|
poller := c.Poller
|
||||||
verb: verb,
|
if poller == nil {
|
||||||
c: c,
|
poller = c.DefaultPoll
|
||||||
path: c.baseURL.Path,
|
|
||||||
sync: c.Sync,
|
|
||||||
timeout: c.Timeout,
|
|
||||||
params: map[string]string{},
|
|
||||||
pollPeriod: c.PollPeriod,
|
|
||||||
}
|
}
|
||||||
|
return NewRequest(c.Client, verb, c.baseURL, c.Codec).Poller(poller).Sync(c.Sync).Timeout(c.Timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post begins a POST request. Short for c.Verb("POST").
|
// Post begins a POST request. Short for c.Verb("POST").
|
||||||
@ -168,6 +122,16 @@ func (c *RESTClient) Delete() *Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PollFor makes a request to do a single poll of the completion of the given operation.
|
// PollFor makes a request to do a single poll of the completion of the given operation.
|
||||||
func (c *RESTClient) PollFor(operationID string) *Request {
|
func (c *RESTClient) Operation(name string) *Request {
|
||||||
return c.Get().Path("operations").Path(operationID).Sync(false).PollPeriod(0)
|
return c.Get().Path("operations").Path(name).Sync(false).NoPoll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RESTClient) DefaultPoll(name string) (*Request, bool) {
|
||||||
|
if c.PollPeriod == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
glog.Infof("Waiting for completion of operation %s", name)
|
||||||
|
time.Sleep(c.PollPeriod)
|
||||||
|
// Make a poll request
|
||||||
|
return c.Operation(name).Poller(c.DefaultPoll), true
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ package client
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -100,36 +99,6 @@ func TestValidatesHostParameter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDoRequest(t *testing.T) {
|
|
||||||
invalid := "aaaaa"
|
|
||||||
uri, _ := url.Parse("http://localhost")
|
|
||||||
testClients := []testClient{
|
|
||||||
{Request: testRequest{Method: "GET", Path: "good"}, Response: Response{StatusCode: 200}},
|
|
||||||
{Request: testRequest{Method: "GET", Path: "good"}, Response: Response{StatusCode: 201}, Created: true},
|
|
||||||
{Request: testRequest{Method: "GET", Path: "bad%ZZ"}, Error: true},
|
|
||||||
{Request: testRequest{Method: "GET", Path: "error"}, Response: Response{StatusCode: 500}, Error: true},
|
|
||||||
{Request: testRequest{Method: "POST", Path: "faildecode"}, Response: Response{StatusCode: 200, RawBody: &invalid}},
|
|
||||||
{Request: testRequest{Method: "GET", Path: "failread"}, Response: Response{StatusCode: 200, RawBody: &invalid}},
|
|
||||||
{Client: &Client{&RESTClient{baseURL: uri, Codec: testapi.Codec()}}, Request: testRequest{Method: "GET", Path: "nocertificate"}, Error: true},
|
|
||||||
}
|
|
||||||
for _, c := range testClients {
|
|
||||||
client := c.Setup()
|
|
||||||
prefix := *client.baseURL
|
|
||||||
prefix.Path += c.Request.Path
|
|
||||||
request := &http.Request{
|
|
||||||
Method: c.Request.Method,
|
|
||||||
Header: make(http.Header),
|
|
||||||
URL: &prefix,
|
|
||||||
}
|
|
||||||
response, created, err := client.doRequest(request)
|
|
||||||
if c.Created != created {
|
|
||||||
t.Errorf("expected created %f, got %f", c.Created, created)
|
|
||||||
}
|
|
||||||
//t.Logf("dorequest: %#v\n%#v\n%v", request.URL, response, err)
|
|
||||||
c.ValidateRaw(t, response, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDoRequestBearer(t *testing.T) {
|
func TestDoRequestBearer(t *testing.T) {
|
||||||
status := &api.Status{Status: api.StatusWorking}
|
status := &api.Status{Status: api.StatusWorking}
|
||||||
expectedBody, _ := latest.Codec.Encode(status)
|
expectedBody, _ := latest.Codec.Encode(status)
|
||||||
@ -139,12 +108,15 @@ func TestDoRequestBearer(t *testing.T) {
|
|||||||
T: t,
|
T: t,
|
||||||
}
|
}
|
||||||
testServer := httptest.NewServer(&fakeHandler)
|
testServer := httptest.NewServer(&fakeHandler)
|
||||||
request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil)
|
request, _ := http.NewRequest("GET", testServer.URL, nil)
|
||||||
c, err := RESTClientFor(&Config{Host: testServer.URL, BearerToken: "test"})
|
c, err := RESTClientFor(&Config{Host: testServer.URL, BearerToken: "test"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
c.doRequest(request)
|
err = c.Get().Do().Error()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("unexpected non-error: %v", err)
|
||||||
|
}
|
||||||
if fakeHandler.RequestReceived.Header.Get("Authorization") != "Bearer test" {
|
if fakeHandler.RequestReceived.Header.Get("Authorization") != "Bearer test" {
|
||||||
t.Errorf("Request is missing authorization header: %#v", *request)
|
t.Errorf("Request is missing authorization header: %#v", *request)
|
||||||
}
|
}
|
||||||
@ -159,18 +131,17 @@ func TestDoRequestAccepted(t *testing.T) {
|
|||||||
T: t,
|
T: t,
|
||||||
}
|
}
|
||||||
testServer := httptest.NewServer(&fakeHandler)
|
testServer := httptest.NewServer(&fakeHandler)
|
||||||
request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil)
|
c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "test", Version: testapi.Version()})
|
||||||
c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "test"})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
body, _, err := c.doRequest(request)
|
body, err := c.Get().Path("test").Do().Raw()
|
||||||
if fakeHandler.RequestReceived.Header["Authorization"] == nil {
|
|
||||||
t.Errorf("Request is missing authorization header: %#v", *request)
|
|
||||||
}
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Unexpected non-error")
|
t.Fatalf("Unexpected non-error")
|
||||||
}
|
}
|
||||||
|
if fakeHandler.RequestReceived.Header["Authorization"] == nil {
|
||||||
|
t.Errorf("Request is missing authorization header: %#v", fakeHandler.RequestReceived)
|
||||||
|
}
|
||||||
se, ok := err.(APIStatus)
|
se, ok := err.(APIStatus)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("Unexpected kind of error: %#v", err)
|
t.Fatalf("Unexpected kind of error: %#v", err)
|
||||||
@ -179,9 +150,9 @@ func TestDoRequestAccepted(t *testing.T) {
|
|||||||
t.Errorf("Unexpected status: %#v %#v", se.Status(), status)
|
t.Errorf("Unexpected status: %#v %#v", se.Status(), status)
|
||||||
}
|
}
|
||||||
if body != nil {
|
if body != nil {
|
||||||
t.Errorf("Expected nil body, but saw: '%s'", body)
|
t.Errorf("Expected nil body, but saw: '%s'", string(body))
|
||||||
}
|
}
|
||||||
fakeHandler.ValidateRequest(t, "/foo/bar", "GET", nil)
|
fakeHandler.ValidateRequest(t, "/"+testapi.Version()+"/test", "GET", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDoRequestAcceptedSuccess(t *testing.T) {
|
func TestDoRequestAcceptedSuccess(t *testing.T) {
|
||||||
@ -193,17 +164,16 @@ func TestDoRequestAcceptedSuccess(t *testing.T) {
|
|||||||
T: t,
|
T: t,
|
||||||
}
|
}
|
||||||
testServer := httptest.NewServer(&fakeHandler)
|
testServer := httptest.NewServer(&fakeHandler)
|
||||||
request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil)
|
c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "user", Password: "pass", Version: testapi.Version()})
|
||||||
c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "user", Password: "pass"})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
body, _, err := c.doRequest(request)
|
body, err := c.Get().Path("test").Do().Raw()
|
||||||
if fakeHandler.RequestReceived.Header["Authorization"] == nil {
|
|
||||||
t.Errorf("Request is missing authorization header: %#v", *request)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error %#v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if fakeHandler.RequestReceived.Header["Authorization"] == nil {
|
||||||
|
t.Errorf("Request is missing authorization header: %#v", fakeHandler.RequestReceived)
|
||||||
}
|
}
|
||||||
statusOut, err := latest.Codec.Decode(body)
|
statusOut, err := latest.Codec.Decode(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -212,7 +182,7 @@ func TestDoRequestAcceptedSuccess(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(status, statusOut) {
|
if !reflect.DeepEqual(status, statusOut) {
|
||||||
t.Errorf("Unexpected mis-match. Expected %#v. Saw %#v", status, statusOut)
|
t.Errorf("Unexpected mis-match. Expected %#v. Saw %#v", status, statusOut)
|
||||||
}
|
}
|
||||||
fakeHandler.ValidateRequest(t, "/foo/bar", "GET", nil)
|
fakeHandler.ValidateRequest(t, "/"+testapi.Version()+"/test", "GET", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDoRequestFailed(t *testing.T) {
|
func TestDoRequestFailed(t *testing.T) {
|
||||||
@ -224,12 +194,11 @@ func TestDoRequestFailed(t *testing.T) {
|
|||||||
T: t,
|
T: t,
|
||||||
}
|
}
|
||||||
testServer := httptest.NewServer(&fakeHandler)
|
testServer := httptest.NewServer(&fakeHandler)
|
||||||
request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil)
|
|
||||||
c, err := RESTClientFor(&Config{Host: testServer.URL})
|
c, err := RESTClientFor(&Config{Host: testServer.URL})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
body, _, err := c.doRequest(request)
|
body, err := c.Get().Do().Raw()
|
||||||
if err == nil || body != nil {
|
if err == nil || body != nil {
|
||||||
t.Errorf("unexpected non-error: %#v", body)
|
t.Errorf("unexpected non-error: %#v", body)
|
||||||
}
|
}
|
||||||
@ -252,12 +221,12 @@ func TestDoRequestCreated(t *testing.T) {
|
|||||||
T: t,
|
T: t,
|
||||||
}
|
}
|
||||||
testServer := httptest.NewServer(&fakeHandler)
|
testServer := httptest.NewServer(&fakeHandler)
|
||||||
request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil)
|
c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "user", Password: "pass", Version: testapi.Version()})
|
||||||
c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "user", Password: "pass"})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
body, created, err := c.doRequest(request)
|
created := false
|
||||||
|
body, err := c.Get().Path("test").Do().WasCreated(&created).Raw()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error %#v", err)
|
t.Errorf("Unexpected error %#v", err)
|
||||||
}
|
}
|
||||||
@ -271,5 +240,5 @@ func TestDoRequestCreated(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(status, statusOut) {
|
if !reflect.DeepEqual(status, statusOut) {
|
||||||
t.Errorf("Unexpected mis-match. Expected %#v. Saw %#v", status, statusOut)
|
t.Errorf("Unexpected mis-match. Expected %#v. Saw %#v", status, statusOut)
|
||||||
}
|
}
|
||||||
fakeHandler.ValidateRequest(t, "/foo/bar", "GET", nil)
|
fakeHandler.ValidateRequest(t, "/"+testapi.Version()+"/test", "GET", nil)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user