diff --git a/pkg/client/helper.go b/pkg/client/helper.go index a0689073586..560e959ef39 100644 --- a/pkg/client/helper.go +++ b/pkg/client/helper.go @@ -21,8 +21,10 @@ import ( "io/ioutil" "net/http" "net/url" + "os" "path" "reflect" + gruntime "runtime" "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" @@ -68,6 +70,9 @@ type Config struct { // certificate. For testing only. Insecure bool + // UserAgent is an optional field that specifies the caller of this request. + UserAgent string + // Transport may be used for custom HTTP behavior. This attribute may not // be specified with the TLS client certificate options. Transport http.RoundTripper @@ -151,6 +156,9 @@ func SetKubernetesDefaults(config *Config) error { if config.Prefix == "" { config.Prefix = "/api" } + if len(config.UserAgent) == 0 { + config.UserAgent = DefaultKubernetesUserAgent() + } if len(config.Version) == 0 { config.Version = defaultVersionFor(config) } @@ -252,6 +260,9 @@ func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTrip case hasBasicAuth: rt = NewBasicAuthRoundTripper(config.Username, config.Password, rt) } + if len(config.UserAgent) > 0 { + rt = NewUserAgentRoundTripper(config.UserAgent, rt) + } return rt, nil } @@ -353,3 +364,18 @@ func defaultVersionFor(config *Config) string { } return version } + +// DefaultKubernetesUserAgent returns the default user agent that clients can use. +func DefaultKubernetesUserAgent() string { + commit := version.Get().GitCommit + if len(commit) > 7 { + commit = commit[:7] + } + if len(commit) == 0 { + commit = "unknown" + } + version := version.Get().GitVersion + seg := strings.SplitN(version, "-", 2) + version = seg[0] + return fmt.Sprintf("%s/%s (%s/%s) kubernetes/%s", path.Base(os.Args[0]), version, gruntime.GOOS, gruntime.GOARCH, commit) +} diff --git a/pkg/client/helper_test.go b/pkg/client/helper_test.go index c15e961a51b..19efee7434f 100644 --- a/pkg/client/helper_test.go +++ b/pkg/client/helper_test.go @@ -18,7 +18,11 @@ package client import ( "net/http" + "reflect" + "strings" "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" ) const ( @@ -255,3 +259,57 @@ func TestIsConfigTransportTLS(t *testing.T) { } } } + +func TestSetKubernetesDefaults(t *testing.T) { + testCases := []struct { + Config Config + After Config + Err bool + }{ + { + Config{}, + Config{ + Prefix: "/api", + Version: latest.Version, + Codec: latest.Codec, + LegacyBehavior: (latest.Version == "v1beta1" || latest.Version == "v1beta2"), + }, + false, + }, + { + Config{ + Version: "not_an_api", + }, + Config{}, + true, + }, + } + for _, testCase := range testCases { + val := &testCase.Config + err := SetKubernetesDefaults(val) + val.UserAgent = "" + switch { + case err == nil && testCase.Err: + t.Errorf("expected error but was nil") + continue + case err != nil && !testCase.Err: + t.Errorf("unexpected error %v", err) + continue + case err != nil: + continue + } + if !reflect.DeepEqual(*val, testCase.After) { + t.Errorf("unexpected result object: %#v", val) + } + } +} + +func TestSetKubernetesDefaultsUserAgent(t *testing.T) { + config := &Config{} + if err := SetKubernetesDefaults(config); err != nil { + t.Errorf("unexpected error: %v") + } + if !strings.Contains(config.UserAgent, "kubernetes/") { + t.Errorf("no user agent set: %#v", config) + } +} diff --git a/pkg/client/restclient_test.go b/pkg/client/restclient_test.go index 7f50bd8d371..e9c3ede61ce 100644 --- a/pkg/client/restclient_test.go +++ b/pkg/client/restclient_test.go @@ -65,49 +65,6 @@ func TestSetsCodec(t *testing.T) { } } -func TestSetDefaults(t *testing.T) { - testCases := []struct { - Config Config - After Config - Err bool - }{ - { - Config{}, - Config{ - Prefix: "/api", - Version: latest.Version, - Codec: latest.Codec, - LegacyBehavior: (latest.Version == "v1beta1" || latest.Version == "v1beta2"), - }, - false, - }, - { - Config{ - Version: "not_an_api", - }, - Config{}, - true, - }, - } - for _, testCase := range testCases { - val := &testCase.Config - err := SetKubernetesDefaults(val) - switch { - case err == nil && testCase.Err: - t.Errorf("expected error but was nil") - continue - case err != nil && !testCase.Err: - t.Errorf("unexpected error %v", err) - continue - case err != nil: - continue - } - if !reflect.DeepEqual(*val, testCase.After) { - t.Errorf("unexpected result object: %#v", val) - } - } -} - func TestRESTClientRequires(t *testing.T) { if _, err := RESTClientFor(&Config{Host: "127.0.0.1", Version: "", Codec: testapi.Codec()}); err == nil { t.Errorf("unexpected non-error") diff --git a/pkg/client/transport.go b/pkg/client/transport.go index 750b0f42312..62dd0964872 100644 --- a/pkg/client/transport.go +++ b/pkg/client/transport.go @@ -23,6 +23,24 @@ import ( "net/http" ) +type userAgentRoundTripper struct { + agent string + rt http.RoundTripper +} + +func NewUserAgentRoundTripper(agent string, rt http.RoundTripper) http.RoundTripper { + return &userAgentRoundTripper{agent, rt} +} + +func (rt *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if len(req.Header.Get("User-Agent")) != 0 { + return rt.rt.RoundTrip(req) + } + req = cloneRequest(req) + req.Header.Set("User-Agent", rt.agent) + return rt.rt.RoundTrip(req) +} + type basicAuthRoundTripper struct { username string password string diff --git a/pkg/client/transport_test.go b/pkg/client/transport_test.go index f31445c0501..aba38a3b2fd 100644 --- a/pkg/client/transport_test.go +++ b/pkg/client/transport_test.go @@ -69,3 +69,33 @@ func TestBasicAuthRoundTripper(t *testing.T) { t.Errorf("unexpected authorization header: %#v", rt.Request) } } + +func TestUserAgentRoundTripper(t *testing.T) { + rt := &testRoundTripper{} + req := &http.Request{ + Header: make(http.Header), + } + req.Header.Set("User-Agent", "other") + NewUserAgentRoundTripper("test", rt).RoundTrip(req) + if rt.Request == nil { + t.Fatalf("unexpected nil request: %v", rt) + } + if rt.Request != req { + t.Fatalf("round tripper should not have copied request object: %#v", rt.Request) + } + if rt.Request.Header.Get("User-Agent") != "other" { + t.Errorf("unexpected user agent header: %#v", rt.Request) + } + + req = &http.Request{} + NewUserAgentRoundTripper("test", rt).RoundTrip(req) + if rt.Request == nil { + t.Fatalf("unexpected nil request: %v", rt) + } + if rt.Request == req { + t.Fatalf("round tripper should have copied request object: %#v", rt.Request) + } + if rt.Request.Header.Get("User-Agent") != "test" { + t.Errorf("unexpected user agent header: %#v", rt.Request) + } +} diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go index 517ff7ddf35..64fb136b41b 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -18,79 +18,21 @@ limitations under the License. package kubectl import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "reflect" "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util" - "github.com/GoogleCloudPlatform/kubernetes/pkg/version" ) var apiVersionToUse = "v1beta1" const kubectlAnnotationPrefix = "kubectl.kubernetes.io/" -func GetKubeClient(config *client.Config, matchVersion bool) (*client.Client, error) { - // TODO: get the namespace context when kubectl ns is completed - c, err := client.New(config) - if err != nil { - return nil, err - } - - if matchVersion { - clientVersion := version.Get() - serverVersion, err := c.ServerVersion() - if err != nil { - return nil, fmt.Errorf("couldn't read version from server: %v\n", err) - } - if s := *serverVersion; !reflect.DeepEqual(clientVersion, s) { - return nil, fmt.Errorf("server version (%#v) differs from client version (%#v)!\n", s, clientVersion) - } - } - - return c, nil -} - type NamespaceInfo struct { Namespace string } -// LoadNamespaceInfo parses a NamespaceInfo object from a file path. It creates a file at the specified path if it doesn't exist with the default namespace. -func LoadNamespaceInfo(path string) (*NamespaceInfo, error) { - var ns NamespaceInfo - if _, err := os.Stat(path); os.IsNotExist(err) { - ns.Namespace = api.NamespaceDefault - err = SaveNamespaceInfo(path, &ns) - return &ns, err - } - data, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - err = json.Unmarshal(data, &ns) - if err != nil { - return nil, err - } - return &ns, err -} - -// SaveNamespaceInfo saves a NamespaceInfo object at the specified file path. -func SaveNamespaceInfo(path string, ns *NamespaceInfo) error { - if !util.IsDNSLabel(ns.Namespace) { - return fmt.Errorf("namespace %s is not a valid DNS Label", ns.Namespace) - } - data, err := json.Marshal(ns) - err = ioutil.WriteFile(path, data, 0600) - return err -} - // TODO Move to labels package. func formatLabels(labelMap map[string]string) string { l := labels.Set(labelMap).String() diff --git a/pkg/kubectl/kubectl_test.go b/pkg/kubectl/kubectl_test.go deleted file mode 100644 index 11ec81f572c..00000000000 --- a/pkg/kubectl/kubectl_test.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -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 kubectl - -import ( - "io/ioutil" - "os" - "reflect" - "testing" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" -) - -func validateAction(expectedAction, actualAction client.FakeAction, t *testing.T) { - if !reflect.DeepEqual(expectedAction, actualAction) { - t.Errorf("Unexpected Action: %#v, expected: %#v", actualAction, expectedAction) - } -} - -func TestLoadNamespaceInfo(t *testing.T) { - loadNamespaceInfoTests := []struct { - nsData string - nsInfo *NamespaceInfo - }{ - { - `{"Namespace":"test"}`, - &NamespaceInfo{Namespace: "test"}, - }, - { - "", nil, - }, - { - "missing", - &NamespaceInfo{Namespace: "default"}, - }, - } - for _, loadNamespaceInfoTest := range loadNamespaceInfoTests { - tt := loadNamespaceInfoTest - nsfile, err := ioutil.TempFile("", "testNamespaceInfo") - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - if tt.nsData != "missing" { - defer os.Remove(nsfile.Name()) - defer nsfile.Close() - _, err := nsfile.WriteString(tt.nsData) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - } else { - nsfile.Close() - os.Remove(nsfile.Name()) - } - nsInfo, err := LoadNamespaceInfo(nsfile.Name()) - if len(tt.nsData) == 0 && tt.nsData != "missing" { - if err == nil { - t.Error("LoadNamespaceInfo didn't fail on an empty file") - } - continue - } - if tt.nsData != "missing" { - if err != nil { - t.Errorf("Unexpected error: %v, %v", tt.nsData, err) - } - if !reflect.DeepEqual(nsInfo, tt.nsInfo) { - t.Errorf("Expected %v, got %v", tt.nsInfo, nsInfo) - } - } - } -}