From ff2eca97d9f202fb05f35582f2dad57fd53a1450 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Mon, 29 Sep 2014 20:15:00 -0400 Subject: [PATCH] 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 --- cmd/apiserver/apiserver.go | 9 +- cmd/controller-manager/controller-manager.go | 20 +- cmd/integration/integration.go | 16 +- cmd/kubecfg/kubecfg.go | 56 ++- cmd/proxy/proxy.go | 17 +- pkg/api/testapi/testapi.go | 42 ++ pkg/client/client.go | 213 +---------- pkg/client/client_test.go | 362 +++++------------- pkg/client/doc.go | 32 +- pkg/client/flags.go | 29 ++ pkg/client/helper.go | 231 +++++++++++ pkg/client/helper_test.go | 86 +++++ pkg/client/request.go | 73 +--- pkg/client/request_test.go | 59 ++- pkg/client/restclient.go | 172 +++++++++ pkg/client/restclient_test.go | 236 ++++++++++++ pkg/client/transport.go | 101 +++++ pkg/client/transport_test.go | 71 ++++ pkg/controller/replication_controller_test.go | 42 +- pkg/kubecfg/kubecfg.go | 9 +- pkg/kubecfg/kubecfg_test.go | 6 +- pkg/service/endpoints_controller_test.go | 36 +- pkg/util/fake_handler.go | 4 + plugin/cmd/scheduler/scheduler.go | 18 +- plugin/pkg/scheduler/factory/factory_test.go | 43 ++- test/integration/client_test.go | 2 +- 26 files changed, 1281 insertions(+), 704 deletions(-) create mode 100644 pkg/api/testapi/testapi.go create mode 100644 pkg/client/flags.go create mode 100644 pkg/client/helper.go create mode 100644 pkg/client/helper_test.go create mode 100644 pkg/client/restclient.go create mode 100644 pkg/client/restclient_test.go create mode 100644 pkg/client/transport.go create mode 100644 pkg/client/transport_test.go diff --git a/cmd/apiserver/apiserver.go b/cmd/apiserver/apiserver.go index 27d2f53333..3fbd579a31 100644 --- a/cmd/apiserver/apiserver.go +++ b/cmd/apiserver/apiserver.go @@ -27,7 +27,6 @@ import ( "strings" "time" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" @@ -127,8 +126,12 @@ func main() { Port: *minionPort, } - ctx := api.NewContext() - client, err := client.New(ctx, net.JoinHostPort(*address, strconv.Itoa(int(*port))), *storageVersion, nil) + // TODO: expose same flags as client.BindClientConfigFlags but for a server + clientConfig := &client.Config{ + Host: net.JoinHostPort(*address, strconv.Itoa(int(*port))), + Version: *storageVersion, + } + client, err := client.New(clientConfig) if err != nil { glog.Fatalf("Invalid server address: %v", err) } diff --git a/cmd/controller-manager/controller-manager.go b/cmd/controller-manager/controller-manager.go index 6572a75fd6..c1bc1ed2fc 100644 --- a/cmd/controller-manager/controller-manager.go +++ b/cmd/controller-manager/controller-manager.go @@ -27,8 +27,6 @@ import ( "strconv" "time" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/controller" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" @@ -39,11 +37,15 @@ import ( ) var ( - master = flag.String("master", "", "The address of the Kubernetes API server") - port = flag.Int("port", masterPkg.ControllerManagerPort, "The port that the controller-manager's http service runs on") - address = flag.String("address", "127.0.0.1", "The address to serve from") + port = flag.Int("port", masterPkg.ControllerManagerPort, "The port that the controller-manager's http service runs on") + address = flag.String("address", "127.0.0.1", "The address to serve from") + clientConfig = &client.Config{} ) +func init() { + client.BindClientConfigFlags(flag.CommandLine, clientConfig) +} + func main() { flag.Parse() util.InitLogs() @@ -51,13 +53,13 @@ func main() { verflag.PrintAndExitIfRequested() - if len(*master) == 0 { + if len(clientConfig.Host) == 0 { glog.Fatal("usage: controller-manager -master ") } - ctx := api.NewContext() - kubeClient, err := client.New(ctx, *master, latest.OldestVersion, nil) + + kubeClient, err := client.New(clientConfig) if err != nil { - glog.Fatalf("Invalid -master: %v", err) + glog.Fatalf("Invalid API configuration: %v", err) } go http.ListenAndServe(net.JoinHostPort(*address, strconv.Itoa(*port)), nil) diff --git a/cmd/integration/integration.go b/cmd/integration/integration.go index fffd209d04..5214327e9a 100644 --- a/cmd/integration/integration.go +++ b/cmd/integration/integration.go @@ -29,7 +29,9 @@ import ( "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/controller" @@ -106,7 +108,7 @@ func startComponents(manifestURL string) (apiServerURL string) { } } - cl := client.NewOrDie(api.NewContext(), apiServer.URL, "", nil) + cl := client.NewOrDie(&client.Config{Host: apiServer.URL, Version: testapi.Version()}) cl.PollPeriod = time.Second * 1 cl.Sync = true @@ -262,12 +264,10 @@ func runAtomicPutTest(c *client.Client) { glog.Infof("Posting update (%s, %s)", l, v) err = c.Put().Path("services").Path(svc.ID).Body(&tmpSvc).Do().Error() if err != nil { - if se, ok := err.(*client.StatusErr); ok { - if se.Status.Code == http.StatusConflict { - glog.Infof("Conflict: (%s, %s)", l, v) - // This is what we expect. - continue - } + if errors.IsConflict(err) { + glog.Infof("Conflict: (%s, %s)", l, v) + // This is what we expect. + continue } glog.Errorf("Unexpected error putting atomicService: %v", err) continue @@ -311,7 +311,7 @@ func main() { // Wait for the synchronization threads to come up. time.Sleep(time.Second * 10) - kubeClient := client.NewOrDie(api.NewContext(), apiServerURL, "", nil) + kubeClient := client.NewOrDie(&client.Config{Host: apiServerURL, Version: testapi.Version()}) // Run tests in parallel testFuncs := []testFunc{ diff --git a/cmd/kubecfg/kubecfg.go b/cmd/kubecfg/kubecfg.go index be09546857..66a9278da5 100644 --- a/cmd/kubecfg/kubecfg.go +++ b/cmd/kubecfg/kubecfg.go @@ -43,7 +43,6 @@ import ( var ( serverVersion = verflag.Version("server_version", verflag.VersionFalse, "Print the server's version information and quit") preventSkew = flag.Bool("expect_version_match", false, "Fail if server's version doesn't match own version.") - httpServer = flag.String("h", "", "The host to connect to.") config = flag.String("c", "", "Path or URL to the config file, or '-' to read from STDIN") selector = flag.String("l", "", "Selector (label query) to use for listing") updatePeriod = flag.Duration("u", 60*time.Second, "Update interval period") @@ -58,12 +57,17 @@ var ( templateFile = flag.String("template_file", "", "If present, load this file as a golang template and use it for output printing") templateStr = flag.String("template", "", "If present, parse this string as a golang template and use it for output printing") imageName = flag.String("image", "", "Image used when updating a replicationController. Will apply to the first container in the pod template.") - apiVersion = flag.String("api_version", latest.Version, "The version of the API to use against this server.") - caFile = flag.String("certificate_authority", "", "Path to a cert. file for the certificate authority") - certFile = flag.String("client_certificate", "", "Path to a client certificate for TLS.") - keyFile = flag.String("client_key", "", "Path to a client key file for TLS.") + clientConfig = &client.Config{} ) +func init() { + flag.StringVar(&clientConfig.Host, "h", "", "The host to connect to.") + flag.StringVar(&clientConfig.Version, "api_version", latest.Version, "The version of the API to use against this server.") + flag.StringVar(&clientConfig.CAFile, "certificate_authority", "", "Path to a cert. file for the certificate authority") + flag.StringVar(&clientConfig.CertFile, "client_certificate", "", "Path to a client certificate for TLS.") + flag.StringVar(&clientConfig.KeyFile, "client_key", "", "Path to a client key file for TLS.") +} + var parser = kubecfg.NewParser(map[string]runtime.Object{ "pods": &api.Pod{}, "services": &api.Service{}, @@ -165,43 +169,29 @@ func main() { verflag.PrintAndExitIfRequested() - var masterServer string - if len(*httpServer) > 0 { - masterServer = *httpServer - } else if len(os.Getenv("KUBERNETES_MASTER")) > 0 { - masterServer = os.Getenv("KUBERNETES_MASTER") - } else { - masterServer = "http://localhost:8080" + // Initialize the client + if clientConfig.Host == "" { + clientConfig.Host = os.Getenv("KUBERNETES_MASTER") } // TODO: get the namespace context when kubecfg ns is completed - ctx := api.NewContext() + clientConfig.Context = api.NewContext() - kubeClient, err := client.New(ctx, masterServer, *apiVersion, nil) - if err != nil { - glog.Fatalf("Can't configure client: %v", err) + if clientConfig.Host == "" { + // TODO: eventually apiserver should start on 443 and be secure by default + clientConfig.Host = "http://localhost:8080" } - - // TODO: this won't work if TLS is enabled with client cert auth, but no - // passwords are required. Refactor when we address client auth abstraction. - if kubeClient.Secure() { + if client.IsConfigTransportSecure(clientConfig) { auth, err := kubecfg.LoadAuthInfo(*authConfig, os.Stdin) if err != nil { glog.Fatalf("Error loading auth: %v", err) } - if *caFile != "" { - auth.CAFile = *caFile - } - if *certFile != "" { - auth.CertFile = *certFile - } - if *keyFile != "" { - auth.KeyFile = *keyFile - } - kubeClient, err = client.New(ctx, masterServer, *apiVersion, auth) - if err != nil { - glog.Fatalf("Can't configure client: %v", err) - } + clientConfig.Username = auth.User + clientConfig.Password = auth.Password + } + kubeClient, err := client.New(clientConfig) + if err != nil { + glog.Fatalf("Can't configure client: %v", err) } if *serverVersion != verflag.VersionFalse { diff --git a/cmd/proxy/proxy.go b/cmd/proxy/proxy.go index 6dbcf3e6dd..dfb526ab78 100644 --- a/cmd/proxy/proxy.go +++ b/cmd/proxy/proxy.go @@ -20,8 +20,6 @@ import ( "flag" "time" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/proxy" "github.com/GoogleCloudPlatform/kubernetes/pkg/proxy/config" @@ -33,12 +31,13 @@ import ( var ( configFile = flag.String("configfile", "/tmp/proxy_config", "Configuration file for the proxy") - master = flag.String("master", "", "The address of the Kubernetes API server (optional)") etcdServerList util.StringList bindAddress = flag.String("bindaddress", "0.0.0.0", "The address for the proxy server to serve on (set to 0.0.0.0 or \"\" for all interfaces)") + clientConfig = &client.Config{} ) func init() { + client.BindClientConfigFlags(flag.CommandLine, clientConfig) flag.Var(&etcdServerList, "etcd_servers", "List of etcd servers to watch (http://ip:port), comma separated (optional)") } @@ -53,13 +52,11 @@ func main() { endpointsConfig := config.NewEndpointsConfig() // define api config source - if *master != "" { - glog.Infof("Using api calls to get config %v", *master) - ctx := api.NewContext() - //TODO: add auth info - client, err := client.New(ctx, *master, latest.OldestVersion, nil) + if clientConfig.Host != "" { + glog.Infof("Using api calls to get config %v", clientConfig.Host) + client, err := client.New(clientConfig) if err != nil { - glog.Fatalf("Invalid -master: %v", err) + glog.Fatalf("Invalid API configuration: %v", err) } config.NewSourceAPI( client, @@ -70,7 +67,7 @@ func main() { } // Create a configuration source that handles configuration from etcd. - if len(etcdServerList) > 0 && *master == "" { + if len(etcdServerList) > 0 && clientConfig.Host == "" { glog.Infof("Using etcd servers %v", etcdServerList) // Set up logger for etcd client diff --git a/pkg/api/testapi/testapi.go b/pkg/api/testapi/testapi.go new file mode 100644 index 0000000000..79e4fedcf9 --- /dev/null +++ b/pkg/api/testapi/testapi.go @@ -0,0 +1,42 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 testapi provides a helper for retrieving the KUBE_API_VERSION environment variable. +package testapi + +import ( + "os" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// Version returns the API version to test against as set by the KUBE_API_VERSION env var. +func Version() string { + version := os.Getenv("KUBE_API_VERSION") + if version == "" { + version = latest.Version + } + return version +} + +func CodecForVersionOrDie() runtime.Codec { + interfaces, err := latest.InterfacesFor(Version()) + if err != nil { + panic(err) + } + return interfaces.Codec +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 7b81492c5e..eed1a14f81 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -17,20 +17,11 @@ limitations under the License. package client import ( - "crypto/tls" - "crypto/x509" "encoding/json" "fmt" - "io/ioutil" - "net/http" - "net/url" - "strings" - "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" - "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/version" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) @@ -91,207 +82,17 @@ type MinionInterface interface { ListMinions() (*api.MinionList, error) } -// Client is the actual implementation of a Kubernetes client. +// APIStatus is exposed by errors that can be converted to an api.Status object +// for finer grained details. +type APIStatus interface { + Status() api.Status +} + +// Client is the implementation of a Kubernetes client. type Client struct { *RESTClient } -// New creates a Kubernetes client. This client works with pods, replication controllers -// and services. It allows operations such as list, get, update and delete on these objects. -// host must be a host string, a host:port combo, or an http or https URL. Passing a prefix -// to a URL will prepend the server path. The API version to use may be specified or left -// empty to use the client preferred version. Returns an error if host cannot be converted to -// a valid URL. -func New(ctx api.Context, host, version string, auth *AuthInfo) (*Client, error) { - if version == "" { - // Clients default to the preferred code API version - // TODO: implement version negotation (highest version supported by server) - version = latest.Version - } - versionInterfaces, err := latest.InterfacesFor(version) - if err != nil { - return nil, fmt.Errorf("API version '%s' is not recognized (valid values: %s)", version, strings.Join(latest.Versions, ", ")) - } - prefix := fmt.Sprintf("/api/%s/", version) - restClient, err := NewRESTClient(ctx, host, auth, prefix, versionInterfaces.Codec) - if err != nil { - return nil, fmt.Errorf("API URL '%s' is not valid: %v", host, err) - } - return &Client{restClient}, nil -} - -// NewOrDie creates a Kubernetes client and panics if the provided host is invalid. -func NewOrDie(ctx api.Context, host, version string, auth *AuthInfo) *Client { - client, err := New(ctx, host, version, auth) - if err != nil { - panic(err) - } - return client -} - -// StatusErr might get returned from an api call if your request is still being processed -// and hence the expected return data is not available yet. -type StatusErr struct { - Status api.Status -} - -func (s *StatusErr) Error() string { - return fmt.Sprintf("Status: %v (%#v)", s.Status.Status, s.Status) -} - -// AuthInfo is used to store authorization information. -type AuthInfo struct { - User string - Password string - CAFile string - CertFile string - KeyFile string -} - -// RESTClient holds common code used to work with API resources that follow the -// Kubernetes API pattern. -// Host is the http://... base for the URL -type RESTClient struct { - ctx api.Context - host string - prefix string - secure bool - auth *AuthInfo - httpClient *http.Client - Sync bool - PollPeriod time.Duration - Timeout time.Duration - Codec runtime.Codec -} - -// NewRESTClient creates a new RESTClient. This client performs generic REST functions -// such as Get, Put, Post, and Delete on specified paths. -func NewRESTClient(ctx api.Context, host string, auth *AuthInfo, path string, c runtime.Codec) (*RESTClient, error) { - prefix, err := normalizePrefix(host, path) - if err != nil { - return nil, err - } - base := *prefix - base.Path = "" - base.RawQuery = "" - base.Fragment = "" - - var config *tls.Config - if auth != nil && len(auth.CertFile) != 0 { - cert, err := tls.LoadX509KeyPair(auth.CertFile, auth.KeyFile) - if err != nil { - return nil, err - } - data, err := ioutil.ReadFile(auth.CAFile) - if err != nil { - return nil, err - } - certPool := x509.NewCertPool() - certPool.AppendCertsFromPEM(data) - config = &tls.Config{ - Certificates: []tls.Certificate{ - cert, - }, - RootCAs: certPool, - ClientCAs: certPool, - ClientAuth: tls.RequireAndVerifyClientCert, - } - } else { - config = &tls.Config{ - InsecureSkipVerify: true, - } - } - - return &RESTClient{ - ctx: ctx, - host: base.String(), - prefix: prefix.Path, - secure: prefix.Scheme == "https", - auth: auth, - httpClient: &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: config, - }, - }, - Sync: false, - PollPeriod: time.Second * 2, - Timeout: time.Second * 20, - Codec: c, - }, nil -} - -// normalizePrefix ensures the passed initial value is valid. -func normalizePrefix(host, prefix string) (*url.URL, 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, err = url.Parse("http://" + 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: %s", base) - } - } - hostURL.Path += prefix - - return hostURL, nil -} - -// Secure returns true if the client is configured for secure connections. -func (c *RESTClient) Secure() bool { - return c.secure -} - -// doRequest executes a request, adds authentication (if auth != nil), and HTTPS -// cert ignoring. -func (c *RESTClient) doRequest(request *http.Request) ([]byte, error) { - if c.auth != nil { - request.SetBasicAuth(c.auth.User, c.auth.Password) - } - response, err := c.httpClient.Do(request) - if err != nil { - return nil, err - } - defer response.Body.Close() - body, err := ioutil.ReadAll(response.Body) - if err != nil { - return body, err - } - - // Did the server give us a status response? - isStatusResponse := false - var status api.Status - if err := latest.Codec.DecodeInto(body, &status); err == nil && status.Status != "" { - isStatusResponse = true - } - - switch { - case response.StatusCode == http.StatusConflict: - // Return error given by server, if there was one. - if isStatusResponse { - return nil, &StatusErr{status} - } - fallthrough - case response.StatusCode < http.StatusOK || response.StatusCode > http.StatusPartialContent: - return nil, fmt.Errorf("request [%#v] failed (%d) %s: %s", request, response.StatusCode, response.Status, string(body)) - } - - // 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, &StatusErr{status} - } - return body, err -} - // ListPods takes a selector, and returns the list of pods that match that selector. func (c *Client) ListPods(selector labels.Selector) (result *api.PodList, err error) { result = &api.PodList{} diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 4ed5323964..7a48fc7839 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -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 { diff --git a/pkg/client/doc.go b/pkg/client/doc.go index 7eee62baed..93d3f957c3 100644 --- a/pkg/client/doc.go +++ b/pkg/client/doc.go @@ -14,6 +14,34 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package client contains the implementation of the client side communication with the -// Kubernetes master. +/* +Package client contains the implementation of the client side communication with the +Kubernetes master. The Client class provides methods for reading, creating, updating, +and deleting pods, replication controllers, services, and minions. + +Most consumers should use the Config object to create a Client: + + config := &client.Config{ + Host: "http://localhost:8080", + Username: "test", + Password: "password", + } + client, err := client.New(&config) + if err != nil { + // handle error + } + client.ListPods() + +More advanced consumers may wish to provide their own transport via a http.RoundTripper: + + config := &client.Config{ + Host: "https://localhost:8080", + Transport: oauthclient.Transport(), + } + client, err := client.New(&config) + +The RESTClient type implements the Kubernetes API conventions (see `docs/api-conventions.md`) +for a given API path and is intended for use by consumers implementing their own Kubernetes +compatible APIs. +*/ package client diff --git a/pkg/client/flags.go b/pkg/client/flags.go new file mode 100644 index 0000000000..c838a60ba0 --- /dev/null +++ b/pkg/client/flags.go @@ -0,0 +1,29 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 client + +// FlagSet abstracts the flag interface for compatibility with both Golang "flag" +// and cobra pflags (Posix style). +type FlagSet interface { + StringVar(p *string, name, value, usage string) +} + +// BindClientConfigFlags registers a standard set of CLI flags for connecting to a Kubernetes API server. +func BindClientConfigFlags(flags FlagSet, config *Config) { + flags.StringVar(&config.Host, "master", config.Host, "The address of the Kubernetes API server") + flags.StringVar(&config.Version, "api_version", config.Version, "The API version to use when talking to the server") +} diff --git a/pkg/client/helper.go b/pkg/client/helper.go new file mode 100644 index 0000000000..6d84281654 --- /dev/null +++ b/pkg/client/helper.go @@ -0,0 +1,231 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 client + +import ( + "fmt" + "net/http" + "net/url" + "path" + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" +) + +// 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 API. + Host string + Version string + + // 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 + + // Server requires TLS client certificate authentication + CertFile string + KeyFile string + CAFile string + + // Server should be accessed without verifying the TLS + // certificate. For testing only. + Insecure bool + + // Transport may be used for custom HTTP behavior. This attribute may not + // be specified with the TLS client certificate options. + Transport http.RoundTripper + + // Context is the context that should be passed down to the server. If nil, the + // context will be set to the appropriate default. + Context api.Context +} + +// New creates a Kubernetes client for the given config. This client works with pods, +// replication controllers and services. It allows operations such as list, get, update +// and delete on these objects. An error is returned if the provided configuration +// is not valid. +func New(c *Config) (*Client, error) { + client, err := RESTClientFor(c) + if err != nil { + return nil, err + } + return &Client{client}, nil +} + +// NewOrDie creates a Kubernetes client and panics if the provided API version is not recognized. +func NewOrDie(c *Config) *Client { + client, err := New(c) + if err != nil { + panic(err) + } + return client +} + +// RESTClientFor returns a RESTClient that satisfies the requested attributes on a client Config +// object. +func RESTClientFor(config *Config) (*RESTClient, error) { + version := defaultVersionFor(config) + + // Set version + versionInterfaces, err := latest.InterfacesFor(version) + if err != nil { + return nil, fmt.Errorf("API version '%s' is not recognized (valid values: %s)", version, strings.Join(latest.Versions, ", ")) + } + + baseURL, err := defaultServerUrlFor(config) + if err != nil { + return nil, err + } + + client := NewRESTClient(baseURL, versionInterfaces.Codec) + + transport, err := TransportFor(config) + if err != nil { + return nil, err + } + + if transport != http.DefaultTransport { + client.Client = &http.Client{Transport: transport} + } + return client, nil +} + +// 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) { + // Set transport level security + if config.Transport != nil && (config.CertFile != "" || config.Insecure) { + return nil, fmt.Errorf("using a custom transport with TLS certificate options or the insecure flag is not allowed") + } + var transport http.RoundTripper + switch { + case config.Transport != nil: + transport = config.Transport + case config.CertFile != "": + t, err := NewClientCertTLSTransport(config.CertFile, config.KeyFile, config.CAFile) + if err != nil { + return nil, err + } + transport = t + case config.Insecure: + transport = NewUnsafeTLSTransport() + default: + transport = http.DefaultTransport + } + + // Set authentication wrappers + hasBasicAuth := config.Username != "" || config.Password != "" + if hasBasicAuth && config.BearerToken != "" { + return nil, fmt.Errorf("username/password or bearer token may be set, but not both") + } + switch { + case config.BearerToken != "": + transport = NewBearerAuthRoundTripper(config.BearerToken, transport) + case hasBasicAuth: + transport = NewBasicAuthRoundTripper(config.Username, config.Password, transport) + } + + // TODO: use the config context to wrap a transport + + return transport, nil +} + +// 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, version string, defaultSecure bool) (*url.URL, error) { + if host == "" { + return nil, fmt.Errorf("host must be a URL or a host:port pair") + } + if version == "" { + return nil, fmt.Errorf("version must be set") + } + base := host + hostURL, err := url.Parse(base) + if err != nil { + return nil, err + } + if hostURL.Scheme == "" { + scheme := "http://" + if defaultSecure { + 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: %s", base) + } + } + + // If the user specified a URL without a path component (http://server.com), automatically + // append the default API prefix + if hostURL.Path == "" { + hostURL.Path = "/api" + } + + // Add the version to the end of the path + hostURL.Path = path.Join(hostURL.Path, version) + + return hostURL, nil +} + +// IsConfigTransportSecure returns true iff the provided config will result in a protected +// connection to the server when it is passed to client.New() or client.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 IsConfigTransportSecure(config *Config) bool { + baseURL, err := defaultServerUrlFor(config) + if err != nil { + return false + } + return baseURL.Scheme == "https" +} + +// defaultServerUrlFor is shared between IsConfigSecure and RESTClientFor +func defaultServerUrlFor(config *Config) (*url.URL, error) { + version := defaultVersionFor(config) + // TODO: move the default to secure when the apiserver supports TLS by default + defaultSecure := config.CertFile != "" + host := config.Host + if host == "" { + host = "localhost" + } + return DefaultServerURL(host, version, defaultSecure) +} + +// defaultVersionFor is shared between defaultServerUrlFor and RESTClientFor +func defaultVersionFor(config *Config) string { + version := config.Version + if version == "" { + // Clients default to the preferred code API version + // TODO: implement version negotiation (highest version supported by server) + version = latest.Version + } + return version +} diff --git a/pkg/client/helper_test.go b/pkg/client/helper_test.go new file mode 100644 index 0000000000..b2937248e9 --- /dev/null +++ b/pkg/client/helper_test.go @@ -0,0 +1,86 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 client + +import ( + "net/http" + "testing" +) + +func TestTransportFor(t *testing.T) { + testCases := map[string]struct { + Config *Config + Err bool + Default bool + }{ + "default transport": { + Config: &Config{}, + }, + } + for k, testCase := range testCases { + transport, err := TransportFor(testCase.Config) + switch { + case testCase.Err && err == nil: + t.Errorf("%s: unexpected non-error", k) + continue + case !testCase.Err && err != nil: + t.Errorf("%s: unexpected error: %v", k, err) + continue + } + if testCase.Default && transport != http.DefaultTransport { + t.Errorf("%s: expected the default transport, got %#v", k, transport) + } + } +} + +func TestIsConfigTransportSecure(t *testing.T) { + testCases := []struct { + Config *Config + Secure bool + }{ + { + Config: &Config{}, + Secure: false, + }, + { + Config: &Config{ + Host: "https://localhost", + }, + Secure: true, + }, + { + Config: &Config{ + Host: "localhost", + CertFile: "foo", + }, + Secure: true, + }, + { + Config: &Config{ + Host: "///:://localhost", + CertFile: "foo", + }, + Secure: false, + }, + } + for _, testCase := range testCases { + secure := IsConfigTransportSecure(testCase.Config) + if testCase.Secure != secure { + t.Errorf("expected %d for %#v", testCase.Secure, testCase.Config) + } + } +} diff --git a/pkg/client/request.go b/pkg/client/request.go index 65cdd4d701..0884b2d00f 100644 --- a/pkg/client/request.go +++ b/pkg/client/request.go @@ -40,56 +40,6 @@ import ( // are therefore not allowed to set manually. var specialParams = util.NewStringSet("sync", "timeout") -// Verb begins a request with a verb (GET, POST, PUT, DELETE). -// -// Example usage of Client's request building interface: -// auth, err := LoadAuth(filename) -// c := New(url, auth) -// 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 { - return &Request{ - verb: verb, - c: c, - path: c.prefix, - sync: c.Sync, - timeout: c.Timeout, - params: map[string]string{}, - pollPeriod: c.PollPeriod, - } -} - -// 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") -} - -// 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") -} - -// PollFor makes a request to do a single poll of the completion of the given operation. -func (c *RESTClient) PollFor(operationID string) *Request { - return c.Get().Path("operations").Path(operationID).Sync(false).PollPeriod(0) -} - // 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 // check once. @@ -232,7 +182,8 @@ func (r *Request) PollPeriod(d time.Duration) *Request { } func (r *Request) finalURL() string { - finalURL := r.c.host + r.path + finalURL := *r.c.baseURL + finalURL.Path = r.path query := url.Values{} for key, value := range r.params { query.Add(key, value) @@ -245,8 +196,8 @@ func (r *Request) finalURL() string { query.Add("timeout", r.timeout.String()) } } - finalURL += "?" + query.Encode() - return finalURL + finalURL.RawQuery = query.Encode() + return finalURL.String() } // Watch attempts to begin watching the requested location. @@ -259,10 +210,11 @@ func (r *Request) Watch() (watch.Interface, error) { if err != nil { return nil, err } - if r.c.auth != nil { - req.SetBasicAuth(r.c.auth.User, r.c.auth.Password) + client := r.c.Client + if client == nil { + client = http.DefaultClient } - response, err := r.c.httpClient.Do(req) + response, err := client.Do(req) if err != nil { return nil, err } @@ -284,10 +236,11 @@ func (r *Request) Do() Result { } respBody, err := r.c.doRequest(req) if err != nil { - if statusErr, ok := err.(*StatusErr); ok { - if statusErr.Status.Status == api.StatusWorking && r.pollPeriod != 0 { - if statusErr.Status.Details != nil { - id := statusErr.Status.Details.ID + if s, ok := err.(APIStatus); ok { + 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) diff --git a/pkg/client/request_test.go b/pkg/client/request_test.go index 8882edc22b..2e06f3f2f5 100644 --- a/pkg/client/request_test.go +++ b/pkg/client/request_test.go @@ -48,9 +48,7 @@ func TestDoRequestNewWay(t *testing.T) { T: t, } testServer := httptest.NewServer(&fakeHandler) - auth := AuthInfo{User: "user", Password: "pass"} - ctx := api.NewContext() - c := NewOrDie(ctx, testServer.URL, "v1beta2", &auth) + c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta2", Username: "user", Password: "pass"}) obj, err := c.Verb("POST"). Path("foo/bar"). Path("baz"). @@ -84,9 +82,7 @@ func TestDoRequestNewWayReader(t *testing.T) { T: t, } testServer := httptest.NewServer(&fakeHandler) - auth := AuthInfo{User: "user", Password: "pass"} - ctx := api.NewContext() - c := NewOrDie(ctx, testServer.URL, "v1beta1", &auth) + c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta1", Username: "user", Password: "pass"}) obj, err := c.Verb("POST"). Path("foo/bar"). Path("baz"). @@ -122,9 +118,7 @@ func TestDoRequestNewWayObj(t *testing.T) { T: t, } testServer := httptest.NewServer(&fakeHandler) - auth := AuthInfo{User: "user", Password: "pass"} - ctx := api.NewContext() - c := NewOrDie(ctx, testServer.URL, "v1beta2", &auth) + c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta2", Username: "user", Password: "pass"}) obj, err := c.Verb("POST"). Path("foo/bar"). Path("baz"). @@ -173,9 +167,7 @@ func TestDoRequestNewWayFile(t *testing.T) { T: t, } testServer := httptest.NewServer(&fakeHandler) - auth := AuthInfo{User: "user", Password: "pass"} - ctx := api.NewContext() - c := NewOrDie(ctx, testServer.URL, "v1beta1", &auth) + c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta1", Username: "user", Password: "pass"}) obj, err := c.Verb("POST"). Path("foo/bar"). Path("baz"). @@ -200,8 +192,7 @@ func TestDoRequestNewWayFile(t *testing.T) { } func TestVerbs(t *testing.T) { - ctx := api.NewContext() - c := NewOrDie(ctx, "localhost", "", nil) + c := NewOrDie(&Config{}) if r := c.Post(); r.verb != "POST" { t.Errorf("Post verb is wrong") } @@ -217,9 +208,8 @@ func TestVerbs(t *testing.T) { } func TestAbsPath(t *testing.T) { - ctx := api.NewContext() expectedPath := "/bar/foo" - c := NewOrDie(ctx, "localhost", "", nil) + c := NewOrDie(&Config{}) r := c.Post().Path("/foo").AbsPath(expectedPath) if r.path != expectedPath { t.Errorf("unexpected path: %s, expected %s", r.path, expectedPath) @@ -227,8 +217,7 @@ func TestAbsPath(t *testing.T) { } func TestSync(t *testing.T) { - ctx := api.NewContext() - c := NewOrDie(ctx, "localhost", "", nil) + c := NewOrDie(&Config{}) r := c.Get() if r.sync { t.Errorf("sync has wrong default") @@ -254,9 +243,8 @@ func TestUintParam(t *testing.T) { {"baz", 0, "http://localhost?baz=0"}, } - ctx := api.NewContext() for _, item := range table { - c := NewOrDie(ctx, "localhost", "", nil) + c := NewOrDie(&Config{}) r := c.Get().AbsPath("").UintParam(item.name, item.testVal) if e, a := item.expectStr, r.finalURL(); e != a { t.Errorf("expected %v, got %v", e, a) @@ -273,9 +261,9 @@ func TestUnacceptableParamNames(t *testing.T) { {"sync", "foo", false}, {"timeout", "42", false}, } - ctx := api.NewContext() + for _, item := range table { - c := NewOrDie(ctx, "localhost", "", nil) + c := NewOrDie(&Config{}) r := c.Get().setParam(item.name, item.testVal) if e, a := item.expectSuccess, r.err == nil; e != a { t.Errorf("expected %v, got %v (%v)", e, a, r.err) @@ -284,8 +272,7 @@ func TestUnacceptableParamNames(t *testing.T) { } func TestSetPollPeriod(t *testing.T) { - ctx := api.NewContext() - c := NewOrDie(ctx, "localhost", "", nil) + c := NewOrDie(&Config{}) r := c.Get() if r.pollPeriod == 0 { t.Errorf("polling should be on by default") @@ -315,9 +302,7 @@ func TestPolling(t *testing.T) { w.Write(data) })) - auth := AuthInfo{User: "user", Password: "pass"} - ctx := api.NewContext() - c := NewOrDie(ctx, testServer.URL, "v1beta1", &auth) + c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta1", Username: "user", Password: "pass"}) trials := []func(){ func() { @@ -342,7 +327,7 @@ func TestPolling(t *testing.T) { t.Errorf("Unexpected non error: %v", obj) return } - if se, ok := err.(*StatusErr); !ok || se.Status.Status != api.StatusWorking { + if se, ok := err.(APIStatus); !ok || se.Status().Status != api.StatusWorking { t.Errorf("Unexpected kind of error: %#v", err) return } @@ -357,7 +342,7 @@ func TestPolling(t *testing.T) { } } -func authFromReq(r *http.Request) (*AuthInfo, bool) { +func authFromReq(r *http.Request) (*Config, bool) { auth, ok := r.Header["Authorization"] if !ok { return nil, false @@ -376,16 +361,16 @@ func authFromReq(r *http.Request) (*AuthInfo, bool) { if len(parts) != 2 { return nil, false } - return &AuthInfo{User: parts[0], Password: parts[1]}, true + return &Config{Username: parts[0], Password: parts[1]}, true } // checkAuth sets errors if the auth found in r doesn't match the expectation. // TODO: Move to util, test in more places. -func checkAuth(t *testing.T, expect AuthInfo, r *http.Request) { +func checkAuth(t *testing.T, expect *Config, r *http.Request) { foundAuth, found := authFromReq(r) if !found { t.Errorf("no auth found") - } else if e, a := expect, *foundAuth; !reflect.DeepEqual(e, a) { + } else if e, a := expect, foundAuth; !reflect.DeepEqual(e, a) { t.Fatalf("Wrong basic auth: wanted %#v, got %#v", e, a) } } @@ -400,7 +385,7 @@ func TestWatch(t *testing.T) { {watch.Deleted, &api.Pod{JSONBase: api.JSONBase{ID: "last"}}}, } - auth := AuthInfo{User: "user", Password: "pass"} + auth := &Config{Username: "user", Password: "pass"} testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { checkAuth(t, auth, r) flusher, ok := w.(http.Flusher) @@ -421,8 +406,12 @@ func TestWatch(t *testing.T) { } })) - ctx := api.NewContext() - s, err := New(ctx, testServer.URL, "v1beta1", &auth) + s, err := New(&Config{ + Host: testServer.URL, + Version: "v1beta1", + Username: "user", + Password: "pass", + }) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/pkg/client/restclient.go b/pkg/client/restclient.go new file mode 100644 index 0000000000..d714cc59e1 --- /dev/null +++ b/pkg/client/restclient.go @@ -0,0 +1,172 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 client + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// 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 { + baseURL *url.URL + + // Codec is the encoding and decoding scheme that applies to a particular set of + // REST resources. + Codec runtime.Codec + + // Set specific behavior of the client. If not set http.DefaultClient will be + // used. + Client *http.Client + + Sync bool + PollPeriod time.Duration + Timeout time.Duration +} + +// 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, c runtime.Codec) *RESTClient { + base := *baseURL + if !strings.HasSuffix(base.Path, "/") { + base.Path += "/" + } + base.RawQuery = "" + base.Fragment = "" + + return &RESTClient{ + baseURL: &base, + Codec: c, + + // Make asynchronous requests by default + // TODO: flip me to the default + Sync: false, + // Poll frequently when asynchronous requests are provided + PollPeriod: time.Second * 2, + } +} + +// doRequest executes a request against a server +func (c *RESTClient) doRequest(request *http.Request) ([]byte, error) { + client := c.Client + if client == nil { + client = http.DefaultClient + } + + response, err := client.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return body, 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, fmt.Errorf("request [%#v] failed (%d) %s: %s", request, response.StatusCode, response.Status, string(body)) + } + return nil, 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, errors.FromObject(&status) + } + + return body, err +} + +// Verb begins a request with a verb (GET, POST, PUT, DELETE). +// +// Example usage of RESTClient's request building interface: +// c := NewRESTClient(url, codec) +// 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 { + // TODO: uncomment when Go 1.2 support is dropped + //var timeout time.Duration = 0 + // if c.Client != nil { + // timeout = c.Client.Timeout + // } + return &Request{ + verb: verb, + c: c, + path: c.baseURL.Path, + sync: c.Sync, + timeout: c.Timeout, + params: map[string]string{}, + pollPeriod: c.PollPeriod, + } +} + +// 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") +} + +// 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") +} + +// PollFor makes a request to do a single poll of the completion of the given operation. +func (c *RESTClient) PollFor(operationID string) *Request { + return c.Get().Path("operations").Path(operationID).Sync(false).PollPeriod(0) +} diff --git a/pkg/client/restclient_test.go b/pkg/client/restclient_test.go new file mode 100644 index 0000000000..b85a888aae --- /dev/null +++ b/pkg/client/restclient_test.go @@ -0,0 +1,236 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 client + +import ( + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" + + "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/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +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}, + } + for version, expected := range testCases { + client, err := RESTClientFor(&Config{Host: "127.0.0.1", Version: version}) + 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.baseURL.Path; 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) + } + } +} + +func TestValidatesHostParameter(t *testing.T) { + testCases := map[string]struct { + URL 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/prefix": {"http://host/prefix/v1beta1/", false}, + "http://host": {"http://host/api/v1beta1/", false}, + "host/server": {"", true}, + } + for k, expected := range testCases { + c, err := RESTClientFor(&Config{Host: k, Version: "v1beta1"}) + 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.URL, c.baseURL.String(); e != a { + t.Errorf("%s: expected host %s, got %s", k, e, a) + continue + } + } +} + +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: "bad%ZZ"}, Error: true}, + {Client: &Client{&RESTClient{baseURL: uri}}, 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 := *client.baseURL + prefix.Path += c.Request.Path + request := &http.Request{ + Method: c.Request.Method, + Header: make(http.Header), + URL: &prefix, + } + response, err := client.doRequest(request) + //t.Logf("dorequest: %#v\n%#v\n%v", request.URL, response, err) + c.ValidateRaw(t, response, err) + } +} + +func TestDoRequestBearer(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) + c, err := RESTClientFor(&Config{Host: testServer.URL, BearerToken: "test"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + c.doRequest(request) + if fakeHandler.RequestReceived.Header.Get("Authorization") != "Bearer test" { + t.Errorf("Request is missing authorization header: %#v", *request) + } +} + +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) + c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "test"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + body, err := c.doRequest(request) + if fakeHandler.RequestReceived.Header["Authorization"] == nil { + t.Errorf("Request is missing authorization header: %#v", *request) + } + if err == nil { + t.Error("Unexpected non-error") + return + } + se, ok := err.(APIStatus) + if !ok { + t.Errorf("Unexpected kind of error: %#v", err) + return + } + if !reflect.DeepEqual(se.Status(), *status) { + t.Errorf("Unexpected status: %#v %#v", se.Status(), 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) + c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "user", Password: "pass"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + body, err := c.doRequest(request) + if fakeHandler.RequestReceived.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 TestDoRequestFailed(t *testing.T) { + status := &api.Status{Status: api.StatusFailure, Reason: api.StatusReasonInvalid, Details: &api.StatusDetails{ID: "test", Kind: "test"}} + expectedBody, _ := latest.Codec.Encode(status) + fakeHandler := util.FakeHandler{ + StatusCode: 404, + ResponseBody: string(expectedBody), + T: t, + } + testServer := httptest.NewServer(&fakeHandler) + request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil) + c, err := RESTClientFor(&Config{Host: testServer.URL}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + body, err := c.doRequest(request) + if err == nil || body != nil { + t.Errorf("unexpected non-error: %#v", body) + } + ss, ok := err.(APIStatus) + if !ok { + t.Errorf("unexpected error type %v", err) + } + actual := ss.Status() + if !reflect.DeepEqual(status, &actual) { + t.Errorf("Unexpected mis-match. Expected %#v. Saw %#v", status, actual) + } +} diff --git a/pkg/client/transport.go b/pkg/client/transport.go new file mode 100644 index 0000000000..ca9706e5c4 --- /dev/null +++ b/pkg/client/transport.go @@ -0,0 +1,101 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 client + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net/http" +) + +type basicAuthRoundTripper struct { + username string + password string + rt http.RoundTripper +} + +func NewBasicAuthRoundTripper(username, password string, rt http.RoundTripper) http.RoundTripper { + return &basicAuthRoundTripper{username, password, rt} +} + +func (rt *basicAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + req = cloneRequest(req) + req.SetBasicAuth(rt.username, rt.password) + return rt.rt.RoundTrip(req) +} + +type bearerAuthRoundTripper struct { + bearer string + rt http.RoundTripper +} + +func NewBearerAuthRoundTripper(bearer string, rt http.RoundTripper) http.RoundTripper { + return &bearerAuthRoundTripper{bearer, rt} +} + +func (rt *bearerAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + req = cloneRequest(req) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", rt.bearer)) + return rt.rt.RoundTrip(req) +} + +func NewClientCertTLSTransport(certFile, keyFile, caFile string) (*http.Transport, error) { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, err + } + data, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, err + } + certPool := x509.NewCertPool() + certPool.AppendCertsFromPEM(data) + return &http.Transport{ + TLSClientConfig: &tls.Config{ + Certificates: []tls.Certificate{ + cert, + }, + RootCAs: certPool, + ClientCAs: certPool, + ClientAuth: tls.RequireAndVerifyClientCert, + }, + }, nil +} + +func NewUnsafeTLSTransport() *http.Transport { + return &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } +} + +// cloneRequest returns a clone of the provided *http.Request. +// The clone is a shallow copy of the struct and its Header map. +func cloneRequest(r *http.Request) *http.Request { + // shallow copy of the struct + r2 := new(http.Request) + *r2 = *r + // deep copy of the Header + r2.Header = make(http.Header) + for k, s := range r.Header { + r2.Header[k] = s + } + return r2 +} diff --git a/pkg/client/transport_test.go b/pkg/client/transport_test.go new file mode 100644 index 0000000000..4f88e054e0 --- /dev/null +++ b/pkg/client/transport_test.go @@ -0,0 +1,71 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 client + +import ( + "encoding/base64" + "net/http" + "testing" +) + +func TestUnsecuredTLSTransport(t *testing.T) { + transport := NewUnsafeTLSTransport() + if !transport.TLSClientConfig.InsecureSkipVerify { + t.Errorf("expected transport to be insecure") + } +} + +type testRoundTripper struct { + Request *http.Request + Response *http.Response + Err error +} + +func (rt *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + rt.Request = req + return rt.Response, rt.Err +} + +func TestBearerAuthRoundTripper(t *testing.T) { + rt := &testRoundTripper{} + req := &http.Request{} + NewBearerAuthRoundTripper("test", rt).RoundTrip(req) + if rt.Request == nil { + t.Fatalf("unexpected nil request", rt) + } + if rt.Request == req { + t.Fatalf("round tripper should have copied request object: %#v", rt.Request) + } + if rt.Request.Header.Get("Authorization") != "Bearer test" { + t.Errorf("unexpected authorization header: %#v", rt.Request) + } +} + +func TestBasicAuthRoundTripper(t *testing.T) { + rt := &testRoundTripper{} + req := &http.Request{} + NewBasicAuthRoundTripper("user", "pass", rt).RoundTrip(req) + if rt.Request == nil { + t.Fatalf("unexpected nil request", rt) + } + if rt.Request == req { + t.Fatalf("round tripper should have copied request object: %#v", rt.Request) + } + if rt.Request.Header.Get("Authorization") != "Basic "+base64.StdEncoding.EncodeToString([]byte("user:pass")) { + t.Errorf("unexpected authorization header: %#v", rt.Request) + } +} diff --git a/pkg/controller/replication_controller_test.go b/pkg/controller/replication_controller_test.go index a4ed5a9cb2..fb96b9d915 100644 --- a/pkg/controller/replication_controller_test.go +++ b/pkg/controller/replication_controller_test.go @@ -21,6 +21,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "path" "reflect" "sync" "testing" @@ -28,7 +29,7 @@ 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/testapi" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" @@ -38,11 +39,8 @@ import ( "github.com/coreos/go-etcd/etcd" ) -// TODO: Move this to a common place, it's needed in multiple tests. -var apiPath = "/api/v1beta1" - func makeURL(suffix string) string { - return apiPath + suffix + return path.Join("/api", testapi.Version(), suffix) } type FakePodControl struct { @@ -116,8 +114,8 @@ func TestSyncReplicationControllerDoesNothing(t *testing.T) { StatusCode: 200, ResponseBody: string(body), } - testServer := httptest.NewTLSServer(&fakeHandler) - client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) + testServer := httptest.NewServer(&fakeHandler) + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) fakePodControl := FakePodControl{} @@ -136,8 +134,8 @@ func TestSyncReplicationControllerDeletes(t *testing.T) { StatusCode: 200, ResponseBody: string(body), } - testServer := httptest.NewTLSServer(&fakeHandler) - client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) + testServer := httptest.NewServer(&fakeHandler) + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) fakePodControl := FakePodControl{} @@ -151,13 +149,13 @@ func TestSyncReplicationControllerDeletes(t *testing.T) { } func TestSyncReplicationControllerCreates(t *testing.T) { - body, _ := latest.Codec.Encode(newPodList(0)) + body := runtime.EncodeOrDie(testapi.CodecForVersionOrDie(), newPodList(0)) fakeHandler := util.FakeHandler{ StatusCode: 200, ResponseBody: string(body), } - testServer := httptest.NewTLSServer(&fakeHandler) - client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) + testServer := httptest.NewServer(&fakeHandler) + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) fakePodControl := FakePodControl{} @@ -171,13 +169,13 @@ func TestSyncReplicationControllerCreates(t *testing.T) { } func TestCreateReplica(t *testing.T) { - body, _ := v1beta1.Codec.Encode(&api.Pod{}) + body := runtime.EncodeOrDie(testapi.CodecForVersionOrDie(), &api.Pod{}) fakeHandler := util.FakeHandler{ StatusCode: 200, ResponseBody: string(body), } - testServer := httptest.NewTLSServer(&fakeHandler) - client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) + testServer := httptest.NewServer(&fakeHandler) + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) podControl := RealPodControl{ kubeClient: client, @@ -211,7 +209,7 @@ func TestCreateReplica(t *testing.T) { expectedPod := api.Pod{ JSONBase: api.JSONBase{ Kind: "Pod", - APIVersion: latest.Version, + APIVersion: testapi.Version(), }, Labels: controllerSpec.DesiredState.PodTemplate.Labels, DesiredState: controllerSpec.DesiredState.PodTemplate.DesiredState, @@ -229,7 +227,7 @@ func TestCreateReplica(t *testing.T) { func TestSynchonize(t *testing.T) { controllerSpec1 := api.ReplicationController{ - JSONBase: api.JSONBase{APIVersion: "v1beta1"}, + JSONBase: api.JSONBase{APIVersion: testapi.Version()}, DesiredState: api.ReplicationControllerState{ Replicas: 4, PodTemplate: api.PodTemplate{ @@ -250,7 +248,7 @@ func TestSynchonize(t *testing.T) { }, } controllerSpec2 := api.ReplicationController{ - JSONBase: api.JSONBase{APIVersion: "v1beta1"}, + JSONBase: api.JSONBase{APIVersion: testapi.Version()}, DesiredState: api.ReplicationControllerState{ Replicas: 3, PodTemplate: api.PodTemplate{ @@ -289,7 +287,7 @@ func TestSynchonize(t *testing.T) { fakePodHandler := util.FakeHandler{ StatusCode: 200, - ResponseBody: "{\"apiVersion\": \"" + latest.Version + "\", \"kind\": \"PodList\"}", + ResponseBody: "{\"apiVersion\": \"" + testapi.Version() + "\", \"kind\": \"PodList\"}", T: t, } fakeControllerHandler := util.FakeHandler{ @@ -303,14 +301,14 @@ func TestSynchonize(t *testing.T) { T: t, } mux := http.NewServeMux() - mux.Handle("/api/v1beta1/pods/", &fakePodHandler) - mux.Handle("/api/v1beta1/replicationControllers/", &fakeControllerHandler) + mux.Handle("/api/"+testapi.Version()+"/pods/", &fakePodHandler) + mux.Handle("/api/"+testapi.Version()+"/replicationControllers/", &fakeControllerHandler) mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusNotFound) t.Errorf("Unexpected request for %v", req.RequestURI) }) testServer := httptest.NewServer(mux) - client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) manager := NewReplicationManager(client) fakePodControl := FakePodControl{} manager.podControl = &fakePodControl diff --git a/pkg/kubecfg/kubecfg.go b/pkg/kubecfg/kubecfg.go index c85cfc23d5..0aae3588ab 100644 --- a/pkg/kubecfg/kubecfg.go +++ b/pkg/kubecfg/kubecfg.go @@ -51,9 +51,14 @@ func promptForString(field string, r io.Reader) string { return result } +type AuthInfo struct { + User string + Password string +} + // LoadAuthInfo parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist. -func LoadAuthInfo(path string, r io.Reader) (*client.AuthInfo, error) { - var auth client.AuthInfo +func LoadAuthInfo(path string, r io.Reader) (*AuthInfo, error) { + var auth AuthInfo if _, err := os.Stat(path); os.IsNotExist(err) { auth.User = promptForString("Username", r) auth.Password = promptForString("Password", r) diff --git a/pkg/kubecfg/kubecfg_test.go b/pkg/kubecfg/kubecfg_test.go index 120f4e3d03..912cbfa723 100644 --- a/pkg/kubecfg/kubecfg_test.go +++ b/pkg/kubecfg/kubecfg_test.go @@ -222,12 +222,12 @@ func TestCloudCfgDeleteControllerWithReplicas(t *testing.T) { func TestLoadAuthInfo(t *testing.T) { loadAuthInfoTests := []struct { authData string - authInfo *client.AuthInfo + authInfo *AuthInfo r io.Reader }{ { `{"user": "user", "password": "pass"}`, - &client.AuthInfo{User: "user", Password: "pass"}, + &AuthInfo{User: "user", Password: "pass"}, nil, }, { @@ -235,7 +235,7 @@ func TestLoadAuthInfo(t *testing.T) { }, { "missing", - &client.AuthInfo{User: "user", Password: "pass"}, + &AuthInfo{User: "user", Password: "pass"}, bytes.NewBufferString("user\npass"), }, } diff --git a/pkg/service/endpoints_controller_test.go b/pkg/service/endpoints_controller_test.go index 42fc648a54..fd9c6b412a 100644 --- a/pkg/service/endpoints_controller_test.go +++ b/pkg/service/endpoints_controller_test.go @@ -24,7 +24,7 @@ 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/testapi" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" @@ -37,7 +37,7 @@ func newPodList(count int) api.PodList { pods = append(pods, api.Pod{ JSONBase: api.JSONBase{ ID: fmt.Sprintf("pod%d", i), - APIVersion: "v1beta1", + APIVersion: testapi.Version(), }, DesiredState: api.PodState{ Manifest: api.ContainerManifest{ @@ -58,7 +58,7 @@ func newPodList(count int) api.PodList { }) } return api.PodList{ - JSONBase: api.JSONBase{APIVersion: "v1beta1", Kind: "PodList"}, + JSONBase: api.JSONBase{APIVersion: testapi.Version(), Kind: "PodList"}, Items: pods, } } @@ -143,10 +143,10 @@ func makeTestServer(t *testing.T, podResponse serverResponse, serviceResponse se ResponseBody: util.EncodeJSON(endpointsResponse.obj), } mux := http.NewServeMux() - mux.Handle("/api/v1beta1/pods", &fakePodHandler) - mux.Handle("/api/v1beta1/services", &fakeServiceHandler) - mux.Handle("/api/v1beta1/endpoints", &fakeEndpointsHandler) - mux.Handle("/api/v1beta1/endpoints/", &fakeEndpointsHandler) + mux.Handle("/api/"+testapi.Version()+"/pods", &fakePodHandler) + mux.Handle("/api/"+testapi.Version()+"/services", &fakeServiceHandler) + mux.Handle("/api/"+testapi.Version()+"/endpoints", &fakeEndpointsHandler) + mux.Handle("/api/"+testapi.Version()+"/endpoints/", &fakeEndpointsHandler) mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { t.Errorf("unexpected request: %v", req.RequestURI) res.WriteHeader(http.StatusNotFound) @@ -159,7 +159,7 @@ func TestSyncEndpointsEmpty(t *testing.T) { serverResponse{http.StatusOK, newPodList(0)}, serverResponse{http.StatusOK, api.ServiceList{}}, serverResponse{http.StatusOK, api.Endpoints{}}) - client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) serviceRegistry := registrytest.ServiceRegistry{} endpoints := NewEndpointController(&serviceRegistry, client) if err := endpoints.SyncServiceEndpoints(); err != nil { @@ -172,7 +172,7 @@ func TestSyncEndpointsError(t *testing.T) { serverResponse{http.StatusOK, newPodList(0)}, serverResponse{http.StatusInternalServerError, api.ServiceList{}}, serverResponse{http.StatusOK, api.Endpoints{}}) - client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) serviceRegistry := registrytest.ServiceRegistry{ Err: fmt.Errorf("test error"), } @@ -203,20 +203,20 @@ func TestSyncEndpointsItemsPreexisting(t *testing.T) { }, Endpoints: []string{"6.7.8.9:1000"}, }}) - client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) serviceRegistry := registrytest.ServiceRegistry{} endpoints := NewEndpointController(&serviceRegistry, client) if err := endpoints.SyncServiceEndpoints(); err != nil { t.Errorf("unexpected error: %v", err) } - data := runtime.EncodeOrDie(v1beta1.Codec, &api.Endpoints{ + data := runtime.EncodeOrDie(testapi.CodecForVersionOrDie(), &api.Endpoints{ JSONBase: api.JSONBase{ ID: "foo", ResourceVersion: 1, }, Endpoints: []string{"1.2.3.4:8080"}, }) - endpointsHandler.ValidateRequest(t, "/api/v1beta1/endpoints/foo", "PUT", &data) + endpointsHandler.ValidateRequest(t, "/api/"+testapi.Version()+"/endpoints/foo", "PUT", &data) } func TestSyncEndpointsItemsPreexistingIdentical(t *testing.T) { @@ -239,13 +239,13 @@ func TestSyncEndpointsItemsPreexistingIdentical(t *testing.T) { }, Endpoints: []string{"1.2.3.4:8080"}, }}) - client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) serviceRegistry := registrytest.ServiceRegistry{} endpoints := NewEndpointController(&serviceRegistry, client) if err := endpoints.SyncServiceEndpoints(); err != nil { t.Errorf("unexpected error: %v", err) } - endpointsHandler.ValidateRequest(t, "/api/v1beta1/endpoints/foo", "GET", nil) + endpointsHandler.ValidateRequest(t, "/api/"+testapi.Version()+"/endpoints/foo", "GET", nil) } func TestSyncEndpointsItems(t *testing.T) { @@ -263,19 +263,19 @@ func TestSyncEndpointsItems(t *testing.T) { serverResponse{http.StatusOK, newPodList(1)}, serverResponse{http.StatusOK, serviceList}, serverResponse{http.StatusOK, api.Endpoints{}}) - client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) serviceRegistry := registrytest.ServiceRegistry{} endpoints := NewEndpointController(&serviceRegistry, client) if err := endpoints.SyncServiceEndpoints(); err != nil { t.Errorf("unexpected error: %v", err) } - data := runtime.EncodeOrDie(v1beta1.Codec, &api.Endpoints{ + data := runtime.EncodeOrDie(testapi.CodecForVersionOrDie(), &api.Endpoints{ JSONBase: api.JSONBase{ ResourceVersion: 0, }, Endpoints: []string{"1.2.3.4:8080"}, }) - endpointsHandler.ValidateRequest(t, "/api/v1beta1/endpoints", "POST", &data) + endpointsHandler.ValidateRequest(t, "/api/"+testapi.Version()+"/endpoints", "POST", &data) } func TestSyncEndpointsPodError(t *testing.T) { @@ -292,7 +292,7 @@ func TestSyncEndpointsPodError(t *testing.T) { serverResponse{http.StatusInternalServerError, api.PodList{}}, serverResponse{http.StatusOK, serviceList}, serverResponse{http.StatusOK, api.Endpoints{}}) - client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) serviceRegistry := registrytest.ServiceRegistry{ List: api.ServiceList{ Items: []api.Service{ diff --git a/pkg/util/fake_handler.go b/pkg/util/fake_handler.go index d76d060843..2a980e7032 100644 --- a/pkg/util/fake_handler.go +++ b/pkg/util/fake_handler.go @@ -65,6 +65,10 @@ func (f FakeHandler) ValidateRequest(t TestInterface, expectedPath, expectedMeth if err != nil { t.Errorf("Couldn't parse %v as a URL.", expectedPath) } + if f.RequestReceived == nil { + t.Errorf("Unexpected nil request received for %s", expectedPath) + return + } if f.RequestReceived.URL.Path != expectURL.Path { t.Errorf("Unexpected request path for request %#v, received: %q, expected: %q", f.RequestReceived, f.RequestReceived.URL.Path, expectURL.Path) } diff --git a/plugin/cmd/scheduler/scheduler.go b/plugin/cmd/scheduler/scheduler.go index eae83eddec..0dbdb85114 100644 --- a/plugin/cmd/scheduler/scheduler.go +++ b/plugin/cmd/scheduler/scheduler.go @@ -22,8 +22,6 @@ import ( "net/http" "strconv" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" masterPkg "github.com/GoogleCloudPlatform/kubernetes/pkg/master" @@ -35,11 +33,15 @@ import ( ) var ( - master = flag.String("master", "", "The address of the Kubernetes API server") - port = flag.Int("port", masterPkg.SchedulerPort, "The port that the scheduler's http service runs on") - address = flag.String("address", "127.0.0.1", "The address to serve from") + port = flag.Int("port", masterPkg.SchedulerPort, "The port that the scheduler's http service runs on") + address = flag.String("address", "127.0.0.1", "The address to serve from") + clientConfig = &client.Config{} ) +func init() { + client.BindClientConfigFlags(flag.CommandLine, clientConfig) +} + func main() { flag.Parse() util.InitLogs() @@ -47,11 +49,9 @@ func main() { verflag.PrintAndExitIfRequested() - // TODO: security story for plugins! - ctx := api.NewContext() - kubeClient, err := client.New(ctx, *master, latest.OldestVersion, nil) + kubeClient, err := client.New(clientConfig) if err != nil { - glog.Fatalf("Invalid -master: %v", err) + glog.Fatalf("Invalid API configuration: %v", err) } go http.ListenAndServe(net.JoinHostPort(*address, strconv.Itoa(*port)), nil) diff --git a/plugin/pkg/scheduler/factory/factory_test.go b/plugin/pkg/scheduler/factory/factory_test.go index 9befb570ae..2f834a3aab 100644 --- a/plugin/pkg/scheduler/factory/factory_test.go +++ b/plugin/pkg/scheduler/factory/factory_test.go @@ -25,6 +25,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" @@ -39,7 +40,7 @@ func TestCreate(t *testing.T) { T: t, } server := httptest.NewServer(&handler) - client := client.NewOrDie(api.NewContext(), server.URL, "", nil) + client := client.NewOrDie(&client.Config{Host: server.URL, Version: testapi.Version()}) factory := ConfigFactory{client} factory.Create() } @@ -52,17 +53,17 @@ func TestCreateLists(t *testing.T) { }{ // Minion { - location: "/api/v1beta1/minions?fields=", + location: "/api/" + testapi.Version() + "/minions?fields=", factory: factory.createMinionLW, }, // Assigned pod { - location: "/api/v1beta1/pods?fields=DesiredState.Host!%3D", + location: "/api/" + testapi.Version() + "/pods?fields=DesiredState.Host!%3D", factory: factory.createAssignedPodLW, }, // Unassigned pod { - location: "/api/v1beta1/pods?fields=DesiredState.Host%3D", + location: "/api/" + testapi.Version() + "/pods?fields=DesiredState.Host%3D", factory: factory.createUnassignedPodLW, }, } @@ -74,7 +75,7 @@ func TestCreateLists(t *testing.T) { T: t, } server := httptest.NewServer(&handler) - factory.Client = client.NewOrDie(api.NewContext(), server.URL, latest.OldestVersion, nil) + factory.Client = client.NewOrDie(&client.Config{Host: server.URL, Version: testapi.Version()}) // This test merely tests that the correct request is made. item.factory().List() handler.ValidateRequest(t, item.location, "GET", nil) @@ -91,31 +92,31 @@ func TestCreateWatches(t *testing.T) { // Minion watch { rv: 0, - location: "/api/v1beta1/watch/minions?fields=&resourceVersion=0", + location: "/api/" + testapi.Version() + "/watch/minions?fields=&resourceVersion=0", factory: factory.createMinionLW, }, { rv: 42, - location: "/api/v1beta1/watch/minions?fields=&resourceVersion=42", + location: "/api/" + testapi.Version() + "/watch/minions?fields=&resourceVersion=42", factory: factory.createMinionLW, }, // Assigned pod watches { rv: 0, - location: "/api/v1beta1/watch/pods?fields=DesiredState.Host!%3D&resourceVersion=0", + location: "/api/" + testapi.Version() + "/watch/pods?fields=DesiredState.Host!%3D&resourceVersion=0", factory: factory.createAssignedPodLW, }, { rv: 42, - location: "/api/v1beta1/watch/pods?fields=DesiredState.Host!%3D&resourceVersion=42", + location: "/api/" + testapi.Version() + "/watch/pods?fields=DesiredState.Host!%3D&resourceVersion=42", factory: factory.createAssignedPodLW, }, // Unassigned pod watches { rv: 0, - location: "/api/v1beta1/watch/pods?fields=DesiredState.Host%3D&resourceVersion=0", + location: "/api/" + testapi.Version() + "/watch/pods?fields=DesiredState.Host%3D&resourceVersion=0", factory: factory.createUnassignedPodLW, }, { rv: 42, - location: "/api/v1beta1/watch/pods?fields=DesiredState.Host%3D&resourceVersion=42", + location: "/api/" + testapi.Version() + "/watch/pods?fields=DesiredState.Host%3D&resourceVersion=42", factory: factory.createUnassignedPodLW, }, } @@ -127,7 +128,7 @@ func TestCreateWatches(t *testing.T) { T: t, } server := httptest.NewServer(&handler) - factory.Client = client.NewOrDie(api.NewContext(), server.URL, "v1beta1", nil) + factory.Client = client.NewOrDie(&client.Config{Host: server.URL, Version: testapi.Version()}) // This test merely tests that the correct request is made. item.factory().Watch(item.rv) handler.ValidateRequest(t, item.location, "GET", nil) @@ -155,9 +156,9 @@ func TestPollMinions(t *testing.T) { } mux := http.NewServeMux() // FakeHandler musn't be sent requests other than the one you want to test. - mux.Handle("/api/v1beta1/minions", &handler) + mux.Handle("/api/"+testapi.Version()+"/minions", &handler) server := httptest.NewServer(mux) - client := client.NewOrDie(api.NewContext(), server.URL, "v1beta1", nil) + client := client.NewOrDie(&client.Config{Host: server.URL, Version: testapi.Version()}) cf := ConfigFactory{client} ce, err := cf.pollMinions() @@ -165,7 +166,7 @@ func TestPollMinions(t *testing.T) { t.Errorf("Unexpected error: %v", err) continue } - handler.ValidateRequest(t, "/api/v1beta1/minions", "GET", nil) + handler.ValidateRequest(t, "/api/"+testapi.Version()+"/minions", "GET", nil) if e, a := len(item.minions), ce.Len(); e != a { t.Errorf("Expected %v, got %v", e, a) @@ -182,9 +183,9 @@ func TestDefaultErrorFunc(t *testing.T) { } mux := http.NewServeMux() // FakeHandler musn't be sent requests other than the one you want to test. - mux.Handle("/api/v1beta1/pods/foo", &handler) + mux.Handle("/api/"+testapi.Version()+"/pods/foo", &handler) server := httptest.NewServer(mux) - factory := ConfigFactory{client.NewOrDie(api.NewContext(), server.URL, "", nil)} + factory := ConfigFactory{client.NewOrDie(&client.Config{Host: server.URL, Version: testapi.Version()})} queue := cache.NewFIFO() errFunc := factory.makeDefaultErrorFunc(queue) @@ -198,7 +199,7 @@ func TestDefaultErrorFunc(t *testing.T) { if !exists { continue } - handler.ValidateRequest(t, "/api/v1beta1/pods/foo", "GET", nil) + handler.ValidateRequest(t, "/api/"+testapi.Version()+"/pods/foo", "GET", nil) if e, a := testPod, got; !reflect.DeepEqual(e, a) { t.Errorf("Expected %v, got %v", e, a) } @@ -289,14 +290,14 @@ func TestBind(t *testing.T) { T: t, } server := httptest.NewServer(&handler) - client := client.NewOrDie(api.NewContext(), server.URL, "", nil) + client := client.NewOrDie(&client.Config{Host: server.URL, Version: testapi.Version()}) b := binder{client} if err := b.Bind(item.binding); err != nil { t.Errorf("Unexpected error: %v", err) continue } - expectedBody := runtime.EncodeOrDie(latest.Codec, item.binding) - handler.ValidateRequest(t, "/api/v1beta1/bindings", "POST", &expectedBody) + expectedBody := runtime.EncodeOrDie(testapi.CodecForVersionOrDie(), item.binding) + handler.ValidateRequest(t, "/api/"+testapi.Version()+"/bindings", "POST", &expectedBody) } } diff --git a/test/integration/client_test.go b/test/integration/client_test.go index 07640d4afb..7deca9b6cb 100644 --- a/test/integration/client_test.go +++ b/test/integration/client_test.go @@ -61,7 +61,7 @@ func TestClient(t *testing.T) { for apiVersion, values := range testCases { deleteAllEtcdKeys() s := httptest.NewServer(apiserver.Handle(values.Storage, values.Codec, fmt.Sprintf("/api/%s/", apiVersion), values.selfLinker)) - client := client.NewOrDie(api.NewContext(), s.URL, apiVersion, nil) + client := client.NewOrDie(&client.Config{Host: s.URL, Version: apiVersion}) info, err := client.ServerVersion() if err != nil {