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
pull/6/head
Clayton Coleman 2014-09-29 20:15:00 -04:00
parent 88bf01b008
commit ff2eca97d9
26 changed files with 1281 additions and 704 deletions

View File

@ -27,7 +27,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
"github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
@ -127,8 +126,12 @@ func main() {
Port: *minionPort, Port: *minionPort,
} }
ctx := api.NewContext() // TODO: expose same flags as client.BindClientConfigFlags but for a server
client, err := client.New(ctx, net.JoinHostPort(*address, strconv.Itoa(int(*port))), *storageVersion, nil) clientConfig := &client.Config{
Host: net.JoinHostPort(*address, strconv.Itoa(int(*port))),
Version: *storageVersion,
}
client, err := client.New(clientConfig)
if err != nil { if err != nil {
glog.Fatalf("Invalid server address: %v", err) glog.Fatalf("Invalid server address: %v", err)
} }

View File

@ -27,8 +27,6 @@ import (
"strconv" "strconv"
"time" "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/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/controller" "github.com/GoogleCloudPlatform/kubernetes/pkg/controller"
_ "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz"
@ -39,11 +37,15 @@ import (
) )
var ( 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")
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")
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() { func main() {
flag.Parse() flag.Parse()
util.InitLogs() util.InitLogs()
@ -51,13 +53,13 @@ func main() {
verflag.PrintAndExitIfRequested() verflag.PrintAndExitIfRequested()
if len(*master) == 0 { if len(clientConfig.Host) == 0 {
glog.Fatal("usage: controller-manager -master <master>") glog.Fatal("usage: controller-manager -master <master>")
} }
ctx := api.NewContext()
kubeClient, err := client.New(ctx, *master, latest.OldestVersion, nil) kubeClient, err := client.New(clientConfig)
if err != nil { 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) go http.ListenAndServe(net.JoinHostPort(*address, strconv.Itoa(*port)), nil)

View File

@ -29,7 +29,9 @@ import (
"time" "time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "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/apiserver"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/controller" "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.PollPeriod = time.Second * 1
cl.Sync = true cl.Sync = true
@ -262,12 +264,10 @@ func runAtomicPutTest(c *client.Client) {
glog.Infof("Posting update (%s, %s)", l, v) glog.Infof("Posting update (%s, %s)", l, v)
err = c.Put().Path("services").Path(svc.ID).Body(&tmpSvc).Do().Error() err = c.Put().Path("services").Path(svc.ID).Body(&tmpSvc).Do().Error()
if err != nil { if err != nil {
if se, ok := err.(*client.StatusErr); ok { if errors.IsConflict(err) {
if se.Status.Code == http.StatusConflict { glog.Infof("Conflict: (%s, %s)", l, v)
glog.Infof("Conflict: (%s, %s)", l, v) // This is what we expect.
// This is what we expect. continue
continue
}
} }
glog.Errorf("Unexpected error putting atomicService: %v", err) glog.Errorf("Unexpected error putting atomicService: %v", err)
continue continue
@ -311,7 +311,7 @@ func main() {
// Wait for the synchronization threads to come up. // Wait for the synchronization threads to come up.
time.Sleep(time.Second * 10) 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 // Run tests in parallel
testFuncs := []testFunc{ testFuncs := []testFunc{

View File

@ -43,7 +43,6 @@ import (
var ( var (
serverVersion = verflag.Version("server_version", verflag.VersionFalse, "Print the server's version information and quit") 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.") 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") 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") selector = flag.String("l", "", "Selector (label query) to use for listing")
updatePeriod = flag.Duration("u", 60*time.Second, "Update interval period") 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") 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") 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.") 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.") clientConfig = &client.Config{}
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.")
) )
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{ var parser = kubecfg.NewParser(map[string]runtime.Object{
"pods": &api.Pod{}, "pods": &api.Pod{},
"services": &api.Service{}, "services": &api.Service{},
@ -165,43 +169,29 @@ func main() {
verflag.PrintAndExitIfRequested() verflag.PrintAndExitIfRequested()
var masterServer string // Initialize the client
if len(*httpServer) > 0 { if clientConfig.Host == "" {
masterServer = *httpServer clientConfig.Host = os.Getenv("KUBERNETES_MASTER")
} else if len(os.Getenv("KUBERNETES_MASTER")) > 0 {
masterServer = os.Getenv("KUBERNETES_MASTER")
} else {
masterServer = "http://localhost:8080"
} }
// TODO: get the namespace context when kubecfg ns is completed // 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 clientConfig.Host == "" {
if err != nil { // TODO: eventually apiserver should start on 443 and be secure by default
glog.Fatalf("Can't configure client: %v", err) clientConfig.Host = "http://localhost:8080"
} }
if client.IsConfigTransportSecure(clientConfig) {
// 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() {
auth, err := kubecfg.LoadAuthInfo(*authConfig, os.Stdin) auth, err := kubecfg.LoadAuthInfo(*authConfig, os.Stdin)
if err != nil { if err != nil {
glog.Fatalf("Error loading auth: %v", err) glog.Fatalf("Error loading auth: %v", err)
} }
if *caFile != "" { clientConfig.Username = auth.User
auth.CAFile = *caFile clientConfig.Password = auth.Password
} }
if *certFile != "" { kubeClient, err := client.New(clientConfig)
auth.CertFile = *certFile if err != nil {
} glog.Fatalf("Can't configure client: %v", err)
if *keyFile != "" {
auth.KeyFile = *keyFile
}
kubeClient, err = client.New(ctx, masterServer, *apiVersion, auth)
if err != nil {
glog.Fatalf("Can't configure client: %v", err)
}
} }
if *serverVersion != verflag.VersionFalse { if *serverVersion != verflag.VersionFalse {

View File

@ -20,8 +20,6 @@ import (
"flag" "flag"
"time" "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/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/proxy" "github.com/GoogleCloudPlatform/kubernetes/pkg/proxy"
"github.com/GoogleCloudPlatform/kubernetes/pkg/proxy/config" "github.com/GoogleCloudPlatform/kubernetes/pkg/proxy/config"
@ -33,12 +31,13 @@ import (
var ( var (
configFile = flag.String("configfile", "/tmp/proxy_config", "Configuration file for the proxy") 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 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)") 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() { func init() {
client.BindClientConfigFlags(flag.CommandLine, clientConfig)
flag.Var(&etcdServerList, "etcd_servers", "List of etcd servers to watch (http://ip:port), comma separated (optional)") 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() endpointsConfig := config.NewEndpointsConfig()
// define api config source // define api config source
if *master != "" { if clientConfig.Host != "" {
glog.Infof("Using api calls to get config %v", *master) glog.Infof("Using api calls to get config %v", clientConfig.Host)
ctx := api.NewContext() client, err := client.New(clientConfig)
//TODO: add auth info
client, err := client.New(ctx, *master, latest.OldestVersion, nil)
if err != nil { if err != nil {
glog.Fatalf("Invalid -master: %v", err) glog.Fatalf("Invalid API configuration: %v", err)
} }
config.NewSourceAPI( config.NewSourceAPI(
client, client,
@ -70,7 +67,7 @@ func main() {
} }
// Create a configuration source that handles configuration from etcd. // 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) glog.Infof("Using etcd servers %v", etcdServerList)
// Set up logger for etcd client // Set up logger for etcd client

View File

@ -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
}

View File

@ -17,20 +17,11 @@ limitations under the License.
package client package client
import ( import (
"crypto/tls"
"crypto/x509"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/version" "github.com/GoogleCloudPlatform/kubernetes/pkg/version"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
) )
@ -91,207 +82,17 @@ type MinionInterface interface {
ListMinions() (*api.MinionList, error) 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 { type Client struct {
*RESTClient *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. // 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) { func (c *Client) ListPods(selector labels.Selector) (result *api.PodList, err error) {
result = &api.PodList{} result = &api.PodList{}

View File

@ -27,8 +27,6 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
@ -38,73 +36,112 @@ import (
// TODO: Move this to a common place, it's needed in multiple tests. // TODO: Move this to a common place, it's needed in multiple tests.
const apiPath = "/api/v1beta1" const apiPath = "/api/v1beta1"
func TestChecksCodec(t *testing.T) { type testRequest struct {
testCases := map[string]struct { Method string
Err bool Path string
Prefix string Header string
Codec runtime.Codec Query url.Values
}{ Body runtime.Object
"v1beta1": {false, "/api/v1beta1/", v1beta1.Codec}, RawBody *string
"": {false, "/api/v1beta1/", v1beta1.Codec}, }
"v1beta2": {false, "/api/v1beta2/", v1beta2.Codec},
"v1beta3": {true, "", nil}, 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() if responseBody := body(c.Response.Body, c.Response.RawBody); responseBody != nil {
for version, expected := range testCases { c.handler.ResponseBody = *responseBody
client, err := New(ctx, "127.0.0.1", version, nil) }
switch { c.server = httptest.NewServer(c.handler)
case err == nil && expected.Err: if c.Client == nil {
t.Errorf("expected error but was nil") c.Client = NewOrDie(&Config{
continue Host: c.server.URL,
case err != nil && !expected.Err: Version: "v1beta1",
t.Errorf("unexpected error %v", err) })
continue }
case err != nil: c.QueryValidator = map[string]func(string, string) bool{}
continue return c
} }
if e, a := expected.Prefix, client.prefix; e != a {
t.Errorf("expected %#v, got %#v", e, a) func (c *testClient) Validate(t *testing.T, received runtime.Object, err error) {
} c.ValidateCommon(t, err)
if e, a := expected.Codec, client.Codec; e != a {
t.Errorf("expected %#v, got %#v", e, a) 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) { func (c *testClient) ValidateRaw(t *testing.T, received []byte, err error) {
testCases := map[string]struct { c.ValidateCommon(t, err)
Host string
Prefix string if c.Response.Body != nil && !reflect.DeepEqual(c.Response.Body, received) {
Err bool t.Errorf("bad response for request %#v: expected %s, got %s", c.Request, c.Response.Body, received)
}{
"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},
} }
ctx := api.NewContext() }
for k, expected := range testCases {
c, err := NewRESTClient(ctx, k, nil, "/api/v1beta1/", v1beta1.Codec) func (c *testClient) ValidateCommon(t *testing.T, err error) {
switch { defer c.server.Close()
case err == nil && expected.Err:
t.Errorf("expected error but was nil") if c.Error {
continue if err == nil {
case err != nil && !expected.Err: t.Errorf("error expected for %#v, got none", c.Request)
t.Errorf("unexpected error %v", err)
continue
case err != nil:
continue
} }
if e, a := expected.Host, c.host; e != a { return
t.Errorf("%s: expected host %s, got %s", k, e, a) }
continue 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 { observed := actualQuery.Get(key)
t.Errorf("%s: expected prefix %s, got %s", k, e, a) if !validator(values[0], observed) {
continue 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) { func TestListEmptyPods(t *testing.T) {
@ -353,109 +390,6 @@ func body(obj runtime.Object, raw *string) *string {
return raw 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) { func TestListServices(t *testing.T) {
c := &testClient{ c := &testClient{
Request: testRequest{Method: "GET", Path: "/services"}, Request: testRequest{Method: "GET", Path: "/services"},
@ -517,10 +451,10 @@ func TestGetService(t *testing.T) {
} }
func TestCreateService(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"}}}, 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"}}}, 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"}}) response, err := c.Setup().CreateService(&api.Service{JSONBase: api.JSONBase{ID: "service-1"}})
c.Validate(t, response, err) c.Validate(t, response, err)
} }
@ -571,102 +505,6 @@ func TestGetEndpoints(t *testing.T) {
c.Validate(t, response, err) 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) { func TestGetServerVersion(t *testing.T) {
expect := version.Info{ expect := version.Info{
Major: "foo", Major: "foo",
@ -683,7 +521,7 @@ func TestGetServerVersion(t *testing.T) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write(output) w.Write(output)
})) }))
client := NewOrDie(api.NewContext(), server.URL, "", nil) client := NewOrDie(&Config{Host: server.URL})
got, err := client.ServerVersion() got, err := client.ServerVersion()
if err != nil { if err != nil {

View File

@ -14,6 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License. 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 package client

29
pkg/client/flags.go Normal file
View File

@ -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")
}

231
pkg/client/helper.go Normal file
View File

@ -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
}

86
pkg/client/helper_test.go Normal file
View File

@ -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)
}
}
}

View File

@ -40,56 +40,6 @@ import (
// are therefore not allowed to set manually. // are therefore not allowed to set manually.
var specialParams = util.NewStringSet("sync", "timeout") var specialParams = util.NewStringSet("sync", "timeout")
// 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. // Request allows for building up a request to a server in a chained fashion.
// Any errors are stored until the end of your call, so you only have to // Any errors are stored until the end of your call, so you only have to
// check once. // check once.
@ -232,7 +182,8 @@ func (r *Request) PollPeriod(d time.Duration) *Request {
} }
func (r *Request) finalURL() string { func (r *Request) finalURL() string {
finalURL := r.c.host + r.path finalURL := *r.c.baseURL
finalURL.Path = r.path
query := url.Values{} query := url.Values{}
for key, value := range r.params { for key, value := range r.params {
query.Add(key, value) query.Add(key, value)
@ -245,8 +196,8 @@ func (r *Request) finalURL() string {
query.Add("timeout", r.timeout.String()) query.Add("timeout", r.timeout.String())
} }
} }
finalURL += "?" + query.Encode() finalURL.RawQuery = query.Encode()
return finalURL return finalURL.String()
} }
// Watch attempts to begin watching the requested location. // Watch attempts to begin watching the requested location.
@ -259,10 +210,11 @@ func (r *Request) Watch() (watch.Interface, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if r.c.auth != nil { client := r.c.Client
req.SetBasicAuth(r.c.auth.User, r.c.auth.Password) if client == nil {
client = http.DefaultClient
} }
response, err := r.c.httpClient.Do(req) response, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -284,10 +236,11 @@ func (r *Request) Do() Result {
} }
respBody, err := r.c.doRequest(req) respBody, err := r.c.doRequest(req)
if err != nil { if err != nil {
if statusErr, ok := err.(*StatusErr); ok { if s, ok := err.(APIStatus); ok {
if statusErr.Status.Status == api.StatusWorking && r.pollPeriod != 0 { status := s.Status()
if statusErr.Status.Details != nil { if status.Status == api.StatusWorking && r.pollPeriod != 0 {
id := statusErr.Status.Details.ID if status.Details != nil {
id := status.Details.ID
if len(id) > 0 { if len(id) > 0 {
glog.Infof("Waiting for completion of /operations/%s", id) glog.Infof("Waiting for completion of /operations/%s", id)
time.Sleep(r.pollPeriod) time.Sleep(r.pollPeriod)

View File

@ -48,9 +48,7 @@ func TestDoRequestNewWay(t *testing.T) {
T: t, T: t,
} }
testServer := httptest.NewServer(&fakeHandler) testServer := httptest.NewServer(&fakeHandler)
auth := AuthInfo{User: "user", Password: "pass"} c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta2", Username: "user", Password: "pass"})
ctx := api.NewContext()
c := NewOrDie(ctx, testServer.URL, "v1beta2", &auth)
obj, err := c.Verb("POST"). obj, err := c.Verb("POST").
Path("foo/bar"). Path("foo/bar").
Path("baz"). Path("baz").
@ -84,9 +82,7 @@ func TestDoRequestNewWayReader(t *testing.T) {
T: t, T: t,
} }
testServer := httptest.NewServer(&fakeHandler) testServer := httptest.NewServer(&fakeHandler)
auth := AuthInfo{User: "user", Password: "pass"} c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta1", Username: "user", Password: "pass"})
ctx := api.NewContext()
c := NewOrDie(ctx, testServer.URL, "v1beta1", &auth)
obj, err := c.Verb("POST"). obj, err := c.Verb("POST").
Path("foo/bar"). Path("foo/bar").
Path("baz"). Path("baz").
@ -122,9 +118,7 @@ func TestDoRequestNewWayObj(t *testing.T) {
T: t, T: t,
} }
testServer := httptest.NewServer(&fakeHandler) testServer := httptest.NewServer(&fakeHandler)
auth := AuthInfo{User: "user", Password: "pass"} c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta2", Username: "user", Password: "pass"})
ctx := api.NewContext()
c := NewOrDie(ctx, testServer.URL, "v1beta2", &auth)
obj, err := c.Verb("POST"). obj, err := c.Verb("POST").
Path("foo/bar"). Path("foo/bar").
Path("baz"). Path("baz").
@ -173,9 +167,7 @@ func TestDoRequestNewWayFile(t *testing.T) {
T: t, T: t,
} }
testServer := httptest.NewServer(&fakeHandler) testServer := httptest.NewServer(&fakeHandler)
auth := AuthInfo{User: "user", Password: "pass"} c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta1", Username: "user", Password: "pass"})
ctx := api.NewContext()
c := NewOrDie(ctx, testServer.URL, "v1beta1", &auth)
obj, err := c.Verb("POST"). obj, err := c.Verb("POST").
Path("foo/bar"). Path("foo/bar").
Path("baz"). Path("baz").
@ -200,8 +192,7 @@ func TestDoRequestNewWayFile(t *testing.T) {
} }
func TestVerbs(t *testing.T) { func TestVerbs(t *testing.T) {
ctx := api.NewContext() c := NewOrDie(&Config{})
c := NewOrDie(ctx, "localhost", "", nil)
if r := c.Post(); r.verb != "POST" { if r := c.Post(); r.verb != "POST" {
t.Errorf("Post verb is wrong") t.Errorf("Post verb is wrong")
} }
@ -217,9 +208,8 @@ func TestVerbs(t *testing.T) {
} }
func TestAbsPath(t *testing.T) { func TestAbsPath(t *testing.T) {
ctx := api.NewContext()
expectedPath := "/bar/foo" expectedPath := "/bar/foo"
c := NewOrDie(ctx, "localhost", "", nil) c := NewOrDie(&Config{})
r := c.Post().Path("/foo").AbsPath(expectedPath) r := c.Post().Path("/foo").AbsPath(expectedPath)
if r.path != expectedPath { if r.path != expectedPath {
t.Errorf("unexpected path: %s, expected %s", 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) { func TestSync(t *testing.T) {
ctx := api.NewContext() c := NewOrDie(&Config{})
c := NewOrDie(ctx, "localhost", "", nil)
r := c.Get() r := c.Get()
if r.sync { if r.sync {
t.Errorf("sync has wrong default") t.Errorf("sync has wrong default")
@ -254,9 +243,8 @@ func TestUintParam(t *testing.T) {
{"baz", 0, "http://localhost?baz=0"}, {"baz", 0, "http://localhost?baz=0"},
} }
ctx := api.NewContext()
for _, item := range table { for _, item := range table {
c := NewOrDie(ctx, "localhost", "", nil) c := NewOrDie(&Config{})
r := c.Get().AbsPath("").UintParam(item.name, item.testVal) r := c.Get().AbsPath("").UintParam(item.name, item.testVal)
if e, a := item.expectStr, r.finalURL(); e != a { if e, a := item.expectStr, r.finalURL(); e != a {
t.Errorf("expected %v, got %v", e, a) t.Errorf("expected %v, got %v", e, a)
@ -273,9 +261,9 @@ func TestUnacceptableParamNames(t *testing.T) {
{"sync", "foo", false}, {"sync", "foo", false},
{"timeout", "42", false}, {"timeout", "42", false},
} }
ctx := api.NewContext()
for _, item := range table { for _, item := range table {
c := NewOrDie(ctx, "localhost", "", nil) c := NewOrDie(&Config{})
r := c.Get().setParam(item.name, item.testVal) r := c.Get().setParam(item.name, item.testVal)
if e, a := item.expectSuccess, r.err == nil; e != a { if e, a := item.expectSuccess, r.err == nil; e != a {
t.Errorf("expected %v, got %v (%v)", e, a, r.err) 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) { func TestSetPollPeriod(t *testing.T) {
ctx := api.NewContext() c := NewOrDie(&Config{})
c := NewOrDie(ctx, "localhost", "", nil)
r := c.Get() r := c.Get()
if r.pollPeriod == 0 { if r.pollPeriod == 0 {
t.Errorf("polling should be on by default") t.Errorf("polling should be on by default")
@ -315,9 +302,7 @@ func TestPolling(t *testing.T) {
w.Write(data) w.Write(data)
})) }))
auth := AuthInfo{User: "user", Password: "pass"} c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta1", Username: "user", Password: "pass"})
ctx := api.NewContext()
c := NewOrDie(ctx, testServer.URL, "v1beta1", &auth)
trials := []func(){ trials := []func(){
func() { func() {
@ -342,7 +327,7 @@ func TestPolling(t *testing.T) {
t.Errorf("Unexpected non error: %v", obj) t.Errorf("Unexpected non error: %v", obj)
return 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) t.Errorf("Unexpected kind of error: %#v", err)
return 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"] auth, ok := r.Header["Authorization"]
if !ok { if !ok {
return nil, false return nil, false
@ -376,16 +361,16 @@ func authFromReq(r *http.Request) (*AuthInfo, bool) {
if len(parts) != 2 { if len(parts) != 2 {
return nil, false 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. // checkAuth sets errors if the auth found in r doesn't match the expectation.
// TODO: Move to util, test in more places. // 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) foundAuth, found := authFromReq(r)
if !found { if !found {
t.Errorf("no auth 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) 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"}}}, {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) { testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
checkAuth(t, auth, r) checkAuth(t, auth, r)
flusher, ok := w.(http.Flusher) flusher, ok := w.(http.Flusher)
@ -421,8 +406,12 @@ func TestWatch(t *testing.T) {
} }
})) }))
ctx := api.NewContext() s, err := New(&Config{
s, err := New(ctx, testServer.URL, "v1beta1", &auth) Host: testServer.URL,
Version: "v1beta1",
Username: "user",
Password: "pass",
})
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }

172
pkg/client/restclient.go Normal file
View File

@ -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)
}

View File

@ -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)
}
}

101
pkg/client/transport.go Normal file
View File

@ -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
}

View File

@ -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)
}
}

View File

@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"path"
"reflect" "reflect"
"sync" "sync"
"testing" "testing"
@ -28,7 +29,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
@ -38,11 +39,8 @@ import (
"github.com/coreos/go-etcd/etcd" "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 { func makeURL(suffix string) string {
return apiPath + suffix return path.Join("/api", testapi.Version(), suffix)
} }
type FakePodControl struct { type FakePodControl struct {
@ -116,8 +114,8 @@ func TestSyncReplicationControllerDoesNothing(t *testing.T) {
StatusCode: 200, StatusCode: 200,
ResponseBody: string(body), ResponseBody: string(body),
} }
testServer := httptest.NewTLSServer(&fakeHandler) testServer := httptest.NewServer(&fakeHandler)
client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()})
fakePodControl := FakePodControl{} fakePodControl := FakePodControl{}
@ -136,8 +134,8 @@ func TestSyncReplicationControllerDeletes(t *testing.T) {
StatusCode: 200, StatusCode: 200,
ResponseBody: string(body), ResponseBody: string(body),
} }
testServer := httptest.NewTLSServer(&fakeHandler) testServer := httptest.NewServer(&fakeHandler)
client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()})
fakePodControl := FakePodControl{} fakePodControl := FakePodControl{}
@ -151,13 +149,13 @@ func TestSyncReplicationControllerDeletes(t *testing.T) {
} }
func TestSyncReplicationControllerCreates(t *testing.T) { func TestSyncReplicationControllerCreates(t *testing.T) {
body, _ := latest.Codec.Encode(newPodList(0)) body := runtime.EncodeOrDie(testapi.CodecForVersionOrDie(), newPodList(0))
fakeHandler := util.FakeHandler{ fakeHandler := util.FakeHandler{
StatusCode: 200, StatusCode: 200,
ResponseBody: string(body), ResponseBody: string(body),
} }
testServer := httptest.NewTLSServer(&fakeHandler) testServer := httptest.NewServer(&fakeHandler)
client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()})
fakePodControl := FakePodControl{} fakePodControl := FakePodControl{}
@ -171,13 +169,13 @@ func TestSyncReplicationControllerCreates(t *testing.T) {
} }
func TestCreateReplica(t *testing.T) { func TestCreateReplica(t *testing.T) {
body, _ := v1beta1.Codec.Encode(&api.Pod{}) body := runtime.EncodeOrDie(testapi.CodecForVersionOrDie(), &api.Pod{})
fakeHandler := util.FakeHandler{ fakeHandler := util.FakeHandler{
StatusCode: 200, StatusCode: 200,
ResponseBody: string(body), ResponseBody: string(body),
} }
testServer := httptest.NewTLSServer(&fakeHandler) testServer := httptest.NewServer(&fakeHandler)
client := client.NewOrDie(api.NewContext(), testServer.URL, "v1beta1", nil) client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()})
podControl := RealPodControl{ podControl := RealPodControl{
kubeClient: client, kubeClient: client,
@ -211,7 +209,7 @@ func TestCreateReplica(t *testing.T) {
expectedPod := api.Pod{ expectedPod := api.Pod{
JSONBase: api.JSONBase{ JSONBase: api.JSONBase{
Kind: "Pod", Kind: "Pod",
APIVersion: latest.Version, APIVersion: testapi.Version(),
}, },
Labels: controllerSpec.DesiredState.PodTemplate.Labels, Labels: controllerSpec.DesiredState.PodTemplate.Labels,
DesiredState: controllerSpec.DesiredState.PodTemplate.DesiredState, DesiredState: controllerSpec.DesiredState.PodTemplate.DesiredState,
@ -229,7 +227,7 @@ func TestCreateReplica(t *testing.T) {
func TestSynchonize(t *testing.T) { func TestSynchonize(t *testing.T) {
controllerSpec1 := api.ReplicationController{ controllerSpec1 := api.ReplicationController{
JSONBase: api.JSONBase{APIVersion: "v1beta1"}, JSONBase: api.JSONBase{APIVersion: testapi.Version()},
DesiredState: api.ReplicationControllerState{ DesiredState: api.ReplicationControllerState{
Replicas: 4, Replicas: 4,
PodTemplate: api.PodTemplate{ PodTemplate: api.PodTemplate{
@ -250,7 +248,7 @@ func TestSynchonize(t *testing.T) {
}, },
} }
controllerSpec2 := api.ReplicationController{ controllerSpec2 := api.ReplicationController{
JSONBase: api.JSONBase{APIVersion: "v1beta1"}, JSONBase: api.JSONBase{APIVersion: testapi.Version()},
DesiredState: api.ReplicationControllerState{ DesiredState: api.ReplicationControllerState{
Replicas: 3, Replicas: 3,
PodTemplate: api.PodTemplate{ PodTemplate: api.PodTemplate{
@ -289,7 +287,7 @@ func TestSynchonize(t *testing.T) {
fakePodHandler := util.FakeHandler{ fakePodHandler := util.FakeHandler{
StatusCode: 200, StatusCode: 200,
ResponseBody: "{\"apiVersion\": \"" + latest.Version + "\", \"kind\": \"PodList\"}", ResponseBody: "{\"apiVersion\": \"" + testapi.Version() + "\", \"kind\": \"PodList\"}",
T: t, T: t,
} }
fakeControllerHandler := util.FakeHandler{ fakeControllerHandler := util.FakeHandler{
@ -303,14 +301,14 @@ func TestSynchonize(t *testing.T) {
T: t, T: t,
} }
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/api/v1beta1/pods/", &fakePodHandler) mux.Handle("/api/"+testapi.Version()+"/pods/", &fakePodHandler)
mux.Handle("/api/v1beta1/replicationControllers/", &fakeControllerHandler) mux.Handle("/api/"+testapi.Version()+"/replicationControllers/", &fakeControllerHandler)
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
t.Errorf("Unexpected request for %v", req.RequestURI) t.Errorf("Unexpected request for %v", req.RequestURI)
}) })
testServer := httptest.NewServer(mux) 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) manager := NewReplicationManager(client)
fakePodControl := FakePodControl{} fakePodControl := FakePodControl{}
manager.podControl = &fakePodControl manager.podControl = &fakePodControl

View File

@ -51,9 +51,14 @@ func promptForString(field string, r io.Reader) string {
return result 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. // 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) { func LoadAuthInfo(path string, r io.Reader) (*AuthInfo, error) {
var auth client.AuthInfo var auth AuthInfo
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
auth.User = promptForString("Username", r) auth.User = promptForString("Username", r)
auth.Password = promptForString("Password", r) auth.Password = promptForString("Password", r)

View File

@ -222,12 +222,12 @@ func TestCloudCfgDeleteControllerWithReplicas(t *testing.T) {
func TestLoadAuthInfo(t *testing.T) { func TestLoadAuthInfo(t *testing.T) {
loadAuthInfoTests := []struct { loadAuthInfoTests := []struct {
authData string authData string
authInfo *client.AuthInfo authInfo *AuthInfo
r io.Reader r io.Reader
}{ }{
{ {
`{"user": "user", "password": "pass"}`, `{"user": "user", "password": "pass"}`,
&client.AuthInfo{User: "user", Password: "pass"}, &AuthInfo{User: "user", Password: "pass"},
nil, nil,
}, },
{ {
@ -235,7 +235,7 @@ func TestLoadAuthInfo(t *testing.T) {
}, },
{ {
"missing", "missing",
&client.AuthInfo{User: "user", Password: "pass"}, &AuthInfo{User: "user", Password: "pass"},
bytes.NewBufferString("user\npass"), bytes.NewBufferString("user\npass"),
}, },
} }

View File

@ -24,7 +24,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
_ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
@ -37,7 +37,7 @@ func newPodList(count int) api.PodList {
pods = append(pods, api.Pod{ pods = append(pods, api.Pod{
JSONBase: api.JSONBase{ JSONBase: api.JSONBase{
ID: fmt.Sprintf("pod%d", i), ID: fmt.Sprintf("pod%d", i),
APIVersion: "v1beta1", APIVersion: testapi.Version(),
}, },
DesiredState: api.PodState{ DesiredState: api.PodState{
Manifest: api.ContainerManifest{ Manifest: api.ContainerManifest{
@ -58,7 +58,7 @@ func newPodList(count int) api.PodList {
}) })
} }
return api.PodList{ return api.PodList{
JSONBase: api.JSONBase{APIVersion: "v1beta1", Kind: "PodList"}, JSONBase: api.JSONBase{APIVersion: testapi.Version(), Kind: "PodList"},
Items: pods, Items: pods,
} }
} }
@ -143,10 +143,10 @@ func makeTestServer(t *testing.T, podResponse serverResponse, serviceResponse se
ResponseBody: util.EncodeJSON(endpointsResponse.obj), ResponseBody: util.EncodeJSON(endpointsResponse.obj),
} }
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/api/v1beta1/pods", &fakePodHandler) mux.Handle("/api/"+testapi.Version()+"/pods", &fakePodHandler)
mux.Handle("/api/v1beta1/services", &fakeServiceHandler) mux.Handle("/api/"+testapi.Version()+"/services", &fakeServiceHandler)
mux.Handle("/api/v1beta1/endpoints", &fakeEndpointsHandler) mux.Handle("/api/"+testapi.Version()+"/endpoints", &fakeEndpointsHandler)
mux.Handle("/api/v1beta1/endpoints/", &fakeEndpointsHandler) mux.Handle("/api/"+testapi.Version()+"/endpoints/", &fakeEndpointsHandler)
mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
t.Errorf("unexpected request: %v", req.RequestURI) t.Errorf("unexpected request: %v", req.RequestURI)
res.WriteHeader(http.StatusNotFound) res.WriteHeader(http.StatusNotFound)
@ -159,7 +159,7 @@ func TestSyncEndpointsEmpty(t *testing.T) {
serverResponse{http.StatusOK, newPodList(0)}, serverResponse{http.StatusOK, newPodList(0)},
serverResponse{http.StatusOK, api.ServiceList{}}, serverResponse{http.StatusOK, api.ServiceList{}},
serverResponse{http.StatusOK, api.Endpoints{}}) 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{} serviceRegistry := registrytest.ServiceRegistry{}
endpoints := NewEndpointController(&serviceRegistry, client) endpoints := NewEndpointController(&serviceRegistry, client)
if err := endpoints.SyncServiceEndpoints(); err != nil { if err := endpoints.SyncServiceEndpoints(); err != nil {
@ -172,7 +172,7 @@ func TestSyncEndpointsError(t *testing.T) {
serverResponse{http.StatusOK, newPodList(0)}, serverResponse{http.StatusOK, newPodList(0)},
serverResponse{http.StatusInternalServerError, api.ServiceList{}}, serverResponse{http.StatusInternalServerError, api.ServiceList{}},
serverResponse{http.StatusOK, api.Endpoints{}}) 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{ serviceRegistry := registrytest.ServiceRegistry{
Err: fmt.Errorf("test error"), Err: fmt.Errorf("test error"),
} }
@ -203,20 +203,20 @@ func TestSyncEndpointsItemsPreexisting(t *testing.T) {
}, },
Endpoints: []string{"6.7.8.9:1000"}, 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{} serviceRegistry := registrytest.ServiceRegistry{}
endpoints := NewEndpointController(&serviceRegistry, client) endpoints := NewEndpointController(&serviceRegistry, client)
if err := endpoints.SyncServiceEndpoints(); err != nil { if err := endpoints.SyncServiceEndpoints(); err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
data := runtime.EncodeOrDie(v1beta1.Codec, &api.Endpoints{ data := runtime.EncodeOrDie(testapi.CodecForVersionOrDie(), &api.Endpoints{
JSONBase: api.JSONBase{ JSONBase: api.JSONBase{
ID: "foo", ID: "foo",
ResourceVersion: 1, ResourceVersion: 1,
}, },
Endpoints: []string{"1.2.3.4:8080"}, 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) { func TestSyncEndpointsItemsPreexistingIdentical(t *testing.T) {
@ -239,13 +239,13 @@ func TestSyncEndpointsItemsPreexistingIdentical(t *testing.T) {
}, },
Endpoints: []string{"1.2.3.4:8080"}, 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{} serviceRegistry := registrytest.ServiceRegistry{}
endpoints := NewEndpointController(&serviceRegistry, client) endpoints := NewEndpointController(&serviceRegistry, client)
if err := endpoints.SyncServiceEndpoints(); err != nil { if err := endpoints.SyncServiceEndpoints(); err != nil {
t.Errorf("unexpected error: %v", err) 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) { func TestSyncEndpointsItems(t *testing.T) {
@ -263,19 +263,19 @@ func TestSyncEndpointsItems(t *testing.T) {
serverResponse{http.StatusOK, newPodList(1)}, serverResponse{http.StatusOK, newPodList(1)},
serverResponse{http.StatusOK, serviceList}, serverResponse{http.StatusOK, serviceList},
serverResponse{http.StatusOK, api.Endpoints{}}) 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{} serviceRegistry := registrytest.ServiceRegistry{}
endpoints := NewEndpointController(&serviceRegistry, client) endpoints := NewEndpointController(&serviceRegistry, client)
if err := endpoints.SyncServiceEndpoints(); err != nil { if err := endpoints.SyncServiceEndpoints(); err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
data := runtime.EncodeOrDie(v1beta1.Codec, &api.Endpoints{ data := runtime.EncodeOrDie(testapi.CodecForVersionOrDie(), &api.Endpoints{
JSONBase: api.JSONBase{ JSONBase: api.JSONBase{
ResourceVersion: 0, ResourceVersion: 0,
}, },
Endpoints: []string{"1.2.3.4:8080"}, 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) { func TestSyncEndpointsPodError(t *testing.T) {
@ -292,7 +292,7 @@ func TestSyncEndpointsPodError(t *testing.T) {
serverResponse{http.StatusInternalServerError, api.PodList{}}, serverResponse{http.StatusInternalServerError, api.PodList{}},
serverResponse{http.StatusOK, serviceList}, serverResponse{http.StatusOK, serviceList},
serverResponse{http.StatusOK, api.Endpoints{}}) 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{ serviceRegistry := registrytest.ServiceRegistry{
List: api.ServiceList{ List: api.ServiceList{
Items: []api.Service{ Items: []api.Service{

View File

@ -65,6 +65,10 @@ func (f FakeHandler) ValidateRequest(t TestInterface, expectedPath, expectedMeth
if err != nil { if err != nil {
t.Errorf("Couldn't parse %v as a URL.", expectedPath) 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 { 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) t.Errorf("Unexpected request path for request %#v, received: %q, expected: %q", f.RequestReceived, f.RequestReceived.URL.Path, expectURL.Path)
} }

View File

@ -22,8 +22,6 @@ import (
"net/http" "net/http"
"strconv" "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/client"
_ "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz"
masterPkg "github.com/GoogleCloudPlatform/kubernetes/pkg/master" masterPkg "github.com/GoogleCloudPlatform/kubernetes/pkg/master"
@ -35,11 +33,15 @@ import (
) )
var ( 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")
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")
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() { func main() {
flag.Parse() flag.Parse()
util.InitLogs() util.InitLogs()
@ -47,11 +49,9 @@ func main() {
verflag.PrintAndExitIfRequested() verflag.PrintAndExitIfRequested()
// TODO: security story for plugins! kubeClient, err := client.New(clientConfig)
ctx := api.NewContext()
kubeClient, err := client.New(ctx, *master, latest.OldestVersion, nil)
if err != nil { 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) go http.ListenAndServe(net.JoinHostPort(*address, strconv.Itoa(*port)), nil)

View File

@ -25,6 +25,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
@ -39,7 +40,7 @@ func TestCreate(t *testing.T) {
T: t, T: t,
} }
server := httptest.NewServer(&handler) 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 := ConfigFactory{client}
factory.Create() factory.Create()
} }
@ -52,17 +53,17 @@ func TestCreateLists(t *testing.T) {
}{ }{
// Minion // Minion
{ {
location: "/api/v1beta1/minions?fields=", location: "/api/" + testapi.Version() + "/minions?fields=",
factory: factory.createMinionLW, factory: factory.createMinionLW,
}, },
// Assigned pod // Assigned pod
{ {
location: "/api/v1beta1/pods?fields=DesiredState.Host!%3D", location: "/api/" + testapi.Version() + "/pods?fields=DesiredState.Host!%3D",
factory: factory.createAssignedPodLW, factory: factory.createAssignedPodLW,
}, },
// Unassigned pod // Unassigned pod
{ {
location: "/api/v1beta1/pods?fields=DesiredState.Host%3D", location: "/api/" + testapi.Version() + "/pods?fields=DesiredState.Host%3D",
factory: factory.createUnassignedPodLW, factory: factory.createUnassignedPodLW,
}, },
} }
@ -74,7 +75,7 @@ func TestCreateLists(t *testing.T) {
T: t, T: t,
} }
server := httptest.NewServer(&handler) 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. // This test merely tests that the correct request is made.
item.factory().List() item.factory().List()
handler.ValidateRequest(t, item.location, "GET", nil) handler.ValidateRequest(t, item.location, "GET", nil)
@ -91,31 +92,31 @@ func TestCreateWatches(t *testing.T) {
// Minion watch // Minion watch
{ {
rv: 0, rv: 0,
location: "/api/v1beta1/watch/minions?fields=&resourceVersion=0", location: "/api/" + testapi.Version() + "/watch/minions?fields=&resourceVersion=0",
factory: factory.createMinionLW, factory: factory.createMinionLW,
}, { }, {
rv: 42, rv: 42,
location: "/api/v1beta1/watch/minions?fields=&resourceVersion=42", location: "/api/" + testapi.Version() + "/watch/minions?fields=&resourceVersion=42",
factory: factory.createMinionLW, factory: factory.createMinionLW,
}, },
// Assigned pod watches // Assigned pod watches
{ {
rv: 0, 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, factory: factory.createAssignedPodLW,
}, { }, {
rv: 42, 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, factory: factory.createAssignedPodLW,
}, },
// Unassigned pod watches // Unassigned pod watches
{ {
rv: 0, 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, factory: factory.createUnassignedPodLW,
}, { }, {
rv: 42, 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, factory: factory.createUnassignedPodLW,
}, },
} }
@ -127,7 +128,7 @@ func TestCreateWatches(t *testing.T) {
T: t, T: t,
} }
server := httptest.NewServer(&handler) 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. // This test merely tests that the correct request is made.
item.factory().Watch(item.rv) item.factory().Watch(item.rv)
handler.ValidateRequest(t, item.location, "GET", nil) handler.ValidateRequest(t, item.location, "GET", nil)
@ -155,9 +156,9 @@ func TestPollMinions(t *testing.T) {
} }
mux := http.NewServeMux() mux := http.NewServeMux()
// FakeHandler musn't be sent requests other than the one you want to test. // 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) 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} cf := ConfigFactory{client}
ce, err := cf.pollMinions() ce, err := cf.pollMinions()
@ -165,7 +166,7 @@ func TestPollMinions(t *testing.T) {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
continue 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 { if e, a := len(item.minions), ce.Len(); e != a {
t.Errorf("Expected %v, got %v", e, a) t.Errorf("Expected %v, got %v", e, a)
@ -182,9 +183,9 @@ func TestDefaultErrorFunc(t *testing.T) {
} }
mux := http.NewServeMux() mux := http.NewServeMux()
// FakeHandler musn't be sent requests other than the one you want to test. // 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) 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() queue := cache.NewFIFO()
errFunc := factory.makeDefaultErrorFunc(queue) errFunc := factory.makeDefaultErrorFunc(queue)
@ -198,7 +199,7 @@ func TestDefaultErrorFunc(t *testing.T) {
if !exists { if !exists {
continue 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) { if e, a := testPod, got; !reflect.DeepEqual(e, a) {
t.Errorf("Expected %v, got %v", e, a) t.Errorf("Expected %v, got %v", e, a)
} }
@ -289,14 +290,14 @@ func TestBind(t *testing.T) {
T: t, T: t,
} }
server := httptest.NewServer(&handler) 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} b := binder{client}
if err := b.Bind(item.binding); err != nil { if err := b.Bind(item.binding); err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
continue continue
} }
expectedBody := runtime.EncodeOrDie(latest.Codec, item.binding) expectedBody := runtime.EncodeOrDie(testapi.CodecForVersionOrDie(), item.binding)
handler.ValidateRequest(t, "/api/v1beta1/bindings", "POST", &expectedBody) handler.ValidateRequest(t, "/api/"+testapi.Version()+"/bindings", "POST", &expectedBody)
} }
} }

View File

@ -61,7 +61,7 @@ func TestClient(t *testing.T) {
for apiVersion, values := range testCases { for apiVersion, values := range testCases {
deleteAllEtcdKeys() deleteAllEtcdKeys()
s := httptest.NewServer(apiserver.Handle(values.Storage, values.Codec, fmt.Sprintf("/api/%s/", apiVersion), values.selfLinker)) 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() info, err := client.ServerVersion()
if err != nil { if err != nil {