mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-01-04 15:05:20 +00:00
Refactor the client (again) to better support auth
* Allows consumers to provide their own transports for common cases. * Supports KUBE_API_VERSION on test cases for controlling which api version they test against * Provides a common flag registration method for CLIs that need to connect to an API server (to avoid duplicating flags) * Ensures errors are properly returned by the server * Add a Context field to client.Config
This commit is contained in:
@@ -27,8 +27,6 @@ import (
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
@@ -38,73 +36,112 @@ import (
|
||||
// TODO: Move this to a common place, it's needed in multiple tests.
|
||||
const apiPath = "/api/v1beta1"
|
||||
|
||||
func TestChecksCodec(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
Err bool
|
||||
Prefix string
|
||||
Codec runtime.Codec
|
||||
}{
|
||||
"v1beta1": {false, "/api/v1beta1/", v1beta1.Codec},
|
||||
"": {false, "/api/v1beta1/", v1beta1.Codec},
|
||||
"v1beta2": {false, "/api/v1beta2/", v1beta2.Codec},
|
||||
"v1beta3": {true, "", nil},
|
||||
type testRequest struct {
|
||||
Method string
|
||||
Path string
|
||||
Header string
|
||||
Query url.Values
|
||||
Body runtime.Object
|
||||
RawBody *string
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Body runtime.Object
|
||||
RawBody *string
|
||||
}
|
||||
|
||||
type testClient struct {
|
||||
*Client
|
||||
Request testRequest
|
||||
Response Response
|
||||
Error bool
|
||||
server *httptest.Server
|
||||
handler *util.FakeHandler
|
||||
// For query args, an optional function to validate the contents
|
||||
// useful when the contents can change but still be correct.
|
||||
// Maps from query arg key to validator.
|
||||
// If no validator is present, string equality is used.
|
||||
QueryValidator map[string]func(string, string) bool
|
||||
}
|
||||
|
||||
func (c *testClient) Setup() *testClient {
|
||||
c.handler = &util.FakeHandler{
|
||||
StatusCode: c.Response.StatusCode,
|
||||
}
|
||||
ctx := api.NewContext()
|
||||
for version, expected := range testCases {
|
||||
client, err := New(ctx, "127.0.0.1", version, nil)
|
||||
switch {
|
||||
case err == nil && expected.Err:
|
||||
t.Errorf("expected error but was nil")
|
||||
continue
|
||||
case err != nil && !expected.Err:
|
||||
t.Errorf("unexpected error %v", err)
|
||||
continue
|
||||
case err != nil:
|
||||
continue
|
||||
}
|
||||
if e, a := expected.Prefix, client.prefix; e != a {
|
||||
t.Errorf("expected %#v, got %#v", e, a)
|
||||
}
|
||||
if e, a := expected.Codec, client.Codec; e != a {
|
||||
t.Errorf("expected %#v, got %#v", e, a)
|
||||
}
|
||||
if responseBody := body(c.Response.Body, c.Response.RawBody); responseBody != nil {
|
||||
c.handler.ResponseBody = *responseBody
|
||||
}
|
||||
c.server = httptest.NewServer(c.handler)
|
||||
if c.Client == nil {
|
||||
c.Client = NewOrDie(&Config{
|
||||
Host: c.server.URL,
|
||||
Version: "v1beta1",
|
||||
})
|
||||
}
|
||||
c.QueryValidator = map[string]func(string, string) bool{}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *testClient) Validate(t *testing.T, received runtime.Object, err error) {
|
||||
c.ValidateCommon(t, err)
|
||||
|
||||
if c.Response.Body != nil && !reflect.DeepEqual(c.Response.Body, received) {
|
||||
t.Errorf("bad response for request %#v: expected %s, got %s", c.Request, c.Response.Body, received)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatesHostParameter(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
Host string
|
||||
Prefix string
|
||||
Err bool
|
||||
}{
|
||||
"127.0.0.1": {"http://127.0.0.1", "/api/v1beta1/", false},
|
||||
"127.0.0.1:8080": {"http://127.0.0.1:8080", "/api/v1beta1/", false},
|
||||
"foo.bar.com": {"http://foo.bar.com", "/api/v1beta1/", false},
|
||||
"http://host/server": {"http://host", "/server/api/v1beta1/", false},
|
||||
"host/server": {"", "", true},
|
||||
func (c *testClient) ValidateRaw(t *testing.T, received []byte, err error) {
|
||||
c.ValidateCommon(t, err)
|
||||
|
||||
if c.Response.Body != nil && !reflect.DeepEqual(c.Response.Body, received) {
|
||||
t.Errorf("bad response for request %#v: expected %s, got %s", c.Request, c.Response.Body, received)
|
||||
}
|
||||
ctx := api.NewContext()
|
||||
for k, expected := range testCases {
|
||||
c, err := NewRESTClient(ctx, k, nil, "/api/v1beta1/", v1beta1.Codec)
|
||||
switch {
|
||||
case err == nil && expected.Err:
|
||||
t.Errorf("expected error but was nil")
|
||||
continue
|
||||
case err != nil && !expected.Err:
|
||||
t.Errorf("unexpected error %v", err)
|
||||
continue
|
||||
case err != nil:
|
||||
continue
|
||||
}
|
||||
|
||||
func (c *testClient) ValidateCommon(t *testing.T, err error) {
|
||||
defer c.server.Close()
|
||||
|
||||
if c.Error {
|
||||
if err == nil {
|
||||
t.Errorf("error expected for %#v, got none", c.Request)
|
||||
}
|
||||
if e, a := expected.Host, c.host; e != a {
|
||||
t.Errorf("%s: expected host %s, got %s", k, e, a)
|
||||
continue
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("no error expected for %#v, got: %v", c.Request, err)
|
||||
}
|
||||
|
||||
if c.handler.RequestReceived == nil {
|
||||
t.Errorf("handler had an empty request, %#v", c)
|
||||
return
|
||||
}
|
||||
|
||||
requestBody := body(c.Request.Body, c.Request.RawBody)
|
||||
actualQuery := c.handler.RequestReceived.URL.Query()
|
||||
// We check the query manually, so blank it out so that FakeHandler.ValidateRequest
|
||||
// won't check it.
|
||||
c.handler.RequestReceived.URL.RawQuery = ""
|
||||
c.handler.ValidateRequest(t, path.Join(apiPath, c.Request.Path), c.Request.Method, requestBody)
|
||||
for key, values := range c.Request.Query {
|
||||
validator, ok := c.QueryValidator[key]
|
||||
if !ok {
|
||||
validator = func(a, b string) bool { return a == b }
|
||||
}
|
||||
if e, a := expected.Prefix, c.prefix; e != a {
|
||||
t.Errorf("%s: expected prefix %s, got %s", k, e, a)
|
||||
continue
|
||||
observed := actualQuery.Get(key)
|
||||
if !validator(values[0], observed) {
|
||||
t.Errorf("Unexpected query arg for key: %s. Expected %s, Received %s", key, values[0], observed)
|
||||
}
|
||||
}
|
||||
if c.Request.Header != "" {
|
||||
if c.handler.RequestReceived.Header.Get(c.Request.Header) == "" {
|
||||
t.Errorf("header %q not found in request %#v", c.Request.Header, c.handler.RequestReceived)
|
||||
}
|
||||
}
|
||||
|
||||
if expected, received := requestBody, c.handler.RequestBody; expected != nil && *expected != received {
|
||||
t.Errorf("bad body for request %#v: expected %s, got %s", c.Request, *expected, received)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListEmptyPods(t *testing.T) {
|
||||
@@ -353,109 +390,6 @@ func body(obj runtime.Object, raw *string) *string {
|
||||
return raw
|
||||
}
|
||||
|
||||
type testRequest struct {
|
||||
Method string
|
||||
Path string
|
||||
Header string
|
||||
Query url.Values
|
||||
Body runtime.Object
|
||||
RawBody *string
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Body runtime.Object
|
||||
RawBody *string
|
||||
}
|
||||
|
||||
type testClient struct {
|
||||
*Client
|
||||
Request testRequest
|
||||
Response Response
|
||||
Error bool
|
||||
server *httptest.Server
|
||||
handler *util.FakeHandler
|
||||
// For query args, an optional function to validate the contents
|
||||
// useful when the contents can change but still be correct.
|
||||
// Maps from query arg key to validator.
|
||||
// If no validator is present, string equality is used.
|
||||
QueryValidator map[string]func(string, string) bool
|
||||
}
|
||||
|
||||
func (c *testClient) Setup() *testClient {
|
||||
ctx := api.NewContext()
|
||||
c.handler = &util.FakeHandler{
|
||||
StatusCode: c.Response.StatusCode,
|
||||
}
|
||||
if responseBody := body(c.Response.Body, c.Response.RawBody); responseBody != nil {
|
||||
c.handler.ResponseBody = *responseBody
|
||||
}
|
||||
c.server = httptest.NewServer(c.handler)
|
||||
if c.Client == nil {
|
||||
c.Client = NewOrDie(ctx, "localhost", "v1beta1", nil)
|
||||
}
|
||||
c.Client.host = c.server.URL
|
||||
c.Client.prefix = "/api/v1beta1/"
|
||||
c.QueryValidator = map[string]func(string, string) bool{}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *testClient) Validate(t *testing.T, received runtime.Object, err error) {
|
||||
c.ValidateCommon(t, err)
|
||||
|
||||
if c.Response.Body != nil && !reflect.DeepEqual(c.Response.Body, received) {
|
||||
t.Errorf("bad response for request %#v: expected %s, got %s", c.Request, c.Response.Body, received)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *testClient) ValidateRaw(t *testing.T, received []byte, err error) {
|
||||
c.ValidateCommon(t, err)
|
||||
|
||||
if c.Response.Body != nil && !reflect.DeepEqual(c.Response.Body, received) {
|
||||
t.Errorf("bad response for request %#v: expected %s, got %s", c.Request, c.Response.Body, received)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *testClient) ValidateCommon(t *testing.T, err error) {
|
||||
defer c.server.Close()
|
||||
|
||||
if c.Error {
|
||||
if err == nil {
|
||||
t.Errorf("error expected for %#v, got none", c.Request)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("no error expected for %#v, got: %v", c.Request, err)
|
||||
}
|
||||
|
||||
requestBody := body(c.Request.Body, c.Request.RawBody)
|
||||
actualQuery := c.handler.RequestReceived.URL.Query()
|
||||
// We check the query manually, so blank it out so that FakeHandler.ValidateRequest
|
||||
// won't check it.
|
||||
c.handler.RequestReceived.URL.RawQuery = ""
|
||||
c.handler.ValidateRequest(t, path.Join(apiPath, c.Request.Path), c.Request.Method, requestBody)
|
||||
for key, values := range c.Request.Query {
|
||||
validator, ok := c.QueryValidator[key]
|
||||
if !ok {
|
||||
validator = func(a, b string) bool { return a == b }
|
||||
}
|
||||
observed := actualQuery.Get(key)
|
||||
if !validator(values[0], observed) {
|
||||
t.Errorf("Unexpected query arg for key: %s. Expected %s, Received %s", key, values[0], observed)
|
||||
}
|
||||
}
|
||||
if c.Request.Header != "" {
|
||||
if c.handler.RequestReceived.Header.Get(c.Request.Header) == "" {
|
||||
t.Errorf("header %q not found in request %#v", c.Request.Header, c.handler.RequestReceived)
|
||||
}
|
||||
}
|
||||
|
||||
if expected, received := requestBody, c.handler.RequestBody; expected != nil && *expected != received {
|
||||
t.Errorf("bad body for request %#v: expected %s, got %s", c.Request, *expected, received)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListServices(t *testing.T) {
|
||||
c := &testClient{
|
||||
Request: testRequest{Method: "GET", Path: "/services"},
|
||||
@@ -517,10 +451,10 @@ func TestGetService(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateService(t *testing.T) {
|
||||
c := (&testClient{
|
||||
c := &testClient{
|
||||
Request: testRequest{Method: "POST", Path: "/services", Body: &api.Service{JSONBase: api.JSONBase{ID: "service-1"}}},
|
||||
Response: Response{StatusCode: 200, Body: &api.Service{JSONBase: api.JSONBase{ID: "service-1"}}},
|
||||
}).Setup()
|
||||
}
|
||||
response, err := c.Setup().CreateService(&api.Service{JSONBase: api.JSONBase{ID: "service-1"}})
|
||||
c.Validate(t, response, err)
|
||||
}
|
||||
@@ -571,102 +505,6 @@ func TestGetEndpoints(t *testing.T) {
|
||||
c.Validate(t, response, err)
|
||||
}
|
||||
|
||||
func TestDoRequest(t *testing.T) {
|
||||
invalid := "aaaaa"
|
||||
testClients := []testClient{
|
||||
{Request: testRequest{Method: "GET", Path: "good"}, Response: Response{StatusCode: 200}},
|
||||
{Request: testRequest{Method: "GET", Path: "bad%ZZ"}, Error: true},
|
||||
{Client: NewOrDie(api.NewContext(), "localhost", "v1beta1", &AuthInfo{"foo", "bar", "", "", ""}), Request: testRequest{Method: "GET", Path: "auth", Header: "Authorization"}, Response: Response{StatusCode: 200}},
|
||||
{Client: &Client{&RESTClient{httpClient: http.DefaultClient}}, Request: testRequest{Method: "GET", Path: "nocertificate"}, 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}},
|
||||
}
|
||||
for _, c := range testClients {
|
||||
client := c.Setup()
|
||||
prefix, _ := url.Parse(client.host)
|
||||
prefix.Path = client.prefix + c.Request.Path
|
||||
request := &http.Request{
|
||||
Method: c.Request.Method,
|
||||
Header: make(http.Header),
|
||||
URL: prefix,
|
||||
}
|
||||
response, err := client.doRequest(request)
|
||||
c.ValidateRaw(t, response, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoRequestAccepted(t *testing.T) {
|
||||
status := &api.Status{Status: api.StatusWorking}
|
||||
expectedBody, _ := latest.Codec.Encode(status)
|
||||
fakeHandler := util.FakeHandler{
|
||||
StatusCode: 202,
|
||||
ResponseBody: string(expectedBody),
|
||||
T: t,
|
||||
}
|
||||
testServer := httptest.NewServer(&fakeHandler)
|
||||
request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil)
|
||||
auth := AuthInfo{User: "user", Password: "pass"}
|
||||
ctx := api.NewContext()
|
||||
c, err := New(ctx, testServer.URL, "", &auth)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
body, err := c.doRequest(request)
|
||||
if request.Header["Authorization"] == nil {
|
||||
t.Errorf("Request is missing authorization header: %#v", *request)
|
||||
}
|
||||
if err == nil {
|
||||
t.Error("Unexpected non-error")
|
||||
return
|
||||
}
|
||||
se, ok := err.(*StatusErr)
|
||||
if !ok {
|
||||
t.Errorf("Unexpected kind of error: %#v", err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(&se.Status, status) {
|
||||
t.Errorf("Unexpected status: %#v", se.Status)
|
||||
}
|
||||
if body != nil {
|
||||
t.Errorf("Expected nil body, but saw: '%s'", body)
|
||||
}
|
||||
fakeHandler.ValidateRequest(t, "/foo/bar", "GET", nil)
|
||||
}
|
||||
|
||||
func TestDoRequestAcceptedSuccess(t *testing.T) {
|
||||
status := &api.Status{Status: api.StatusSuccess}
|
||||
expectedBody, _ := latest.Codec.Encode(status)
|
||||
fakeHandler := util.FakeHandler{
|
||||
StatusCode: 202,
|
||||
ResponseBody: string(expectedBody),
|
||||
T: t,
|
||||
}
|
||||
testServer := httptest.NewServer(&fakeHandler)
|
||||
request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil)
|
||||
auth := AuthInfo{User: "user", Password: "pass"}
|
||||
ctx := api.NewContext()
|
||||
c, err := New(ctx, testServer.URL, "", &auth)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
body, err := c.doRequest(request)
|
||||
if request.Header["Authorization"] == nil {
|
||||
t.Errorf("Request is missing authorization header: %#v", *request)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %#v", err)
|
||||
}
|
||||
statusOut, err := latest.Codec.Decode(body)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %#v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(status, statusOut) {
|
||||
t.Errorf("Unexpected mis-match. Expected %#v. Saw %#v", status, statusOut)
|
||||
}
|
||||
fakeHandler.ValidateRequest(t, "/foo/bar", "GET", nil)
|
||||
}
|
||||
|
||||
func TestGetServerVersion(t *testing.T) {
|
||||
expect := version.Info{
|
||||
Major: "foo",
|
||||
@@ -683,7 +521,7 @@ func TestGetServerVersion(t *testing.T) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(output)
|
||||
}))
|
||||
client := NewOrDie(api.NewContext(), server.URL, "", nil)
|
||||
client := NewOrDie(&Config{Host: server.URL})
|
||||
|
||||
got, err := client.ServerVersion()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user