From b1d8a41049e0788d7db9fee6f573ad3395f5447b Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Sat, 21 Jun 2014 14:23:14 -0700 Subject: [PATCH 1/4] Add new api usage mechanism. --- cmd/cloudcfg/cloudcfg.go | 42 +++++----- pkg/cloudcfg/cloudcfg.go | 130 +++++++++++++++++++++++++++++++ pkg/cloudcfg/resource_printer.go | 22 ++++++ 3 files changed, 174 insertions(+), 20 deletions(-) diff --git a/cmd/cloudcfg/cloudcfg.go b/cmd/cloudcfg/cloudcfg.go index 8ed97d41790..eaad7381865 100644 --- a/cmd/cloudcfg/cloudcfg.go +++ b/cmd/cloudcfg/cloudcfg.go @@ -139,27 +139,34 @@ func executeAPIRequest(method string, auth *kube_client.AuthInfo) bool { return *httpServer + path.Join("/api/v1beta1", storage) } - var request *http.Request - var err error + verb := "" switch method { case "get", "list": - url := readUrl(parseStorage()) - if len(*selector) > 0 && method == "list" { - url = url + "?labels=" + *selector - } - request, err = http.NewRequest("GET", url, nil) + verb = "GET" case "delete": - request, err = http.NewRequest("DELETE", readUrl(parseStorage()), nil) + verb = "DELETE" case "create": - storage := parseStorage() - request, err = cloudcfg.RequestWithBodyData(readConfig(storage), readUrl(storage), "POST") + verb = "POST" case "update": - storage := parseStorage() - request, err = cloudcfg.RequestWithBodyData(readConfig(storage), readUrl(storage), "PUT") + verb = "PUT" default: return false } + s := cloudcfg.New(*httpServer, auth) + r := s.Verb(verb). + Path("api/v1beta1"). + Path(parseStorage()). + Selector(*selector) + if method == "create" || method == "update" { + r.Body(readConfig(parseStorage())) + } + obj, err := r.Do() + if err != nil { + log.Fatalf("Got request error: %v\n", err) + return false + } + var printer cloudcfg.ResourcePrinter if *json { printer = &cloudcfg.IdentityPrinter{} @@ -169,15 +176,10 @@ func executeAPIRequest(method string, auth *kube_client.AuthInfo) bool { printer = &cloudcfg.HumanReadablePrinter{} } - var body []byte - if body, err = cloudcfg.DoRequest(request, auth); err == nil { - if err = printer.Print(body, os.Stdout); err != nil { - log.Fatalf("Failed to print: %#v\nRaw received text:\n%v\n", err, string(body)) - } - fmt.Print("\n") - } else { - log.Fatalf("Error: %#v %s", err, body) + if err = printer.PrintObj(obj, os.Stdout); err != nil { + log.Fatalf("Failed to print: %#v\nRaw received text:\n%v\n", err, string(body)) } + fmt.Print("\n") return true } diff --git a/pkg/cloudcfg/cloudcfg.go b/pkg/cloudcfg/cloudcfg.go index 6925b62d5cc..b2d2df6fea1 100644 --- a/pkg/cloudcfg/cloudcfg.go +++ b/pkg/cloudcfg/cloudcfg.go @@ -21,16 +21,20 @@ import ( "crypto/tls" "encoding/json" "fmt" + "io" "io/ioutil" "log" "net/http" + "net/url" "os" + "path" "strconv" "strings" "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "gopkg.in/v1/yaml" ) @@ -90,6 +94,132 @@ func Update(name string, client client.ClientInterface, updatePeriod time.Durati return nil } +// Server contains info locating a kubernetes api server. +// Example usage: +// auth, err := LoadAuth(filename) +// s := New(url, auth) +// resp, err := s.Verb("GET"). +// Path("api/v1beta1"). +// Path("pods"). +// Selector("area=staging"). +// Timeout(10*time.Second). +// Do() +// list, ok := resp.(api.PodList) +type Server struct { + auth *client.AuthInfo + rawUrl string +} + +// Create a new server object. +func New(serverUrl string, auth *client.AuthInfo) *Server { + return &Server{ + auth: auth, + rawUrl: serverUrl, + } +} + +// Begin a request with a verb (GET, POST, PUT, DELETE) +func (s *Server) Verb(verb string) *Request { + return &Request{ + verb: verb, + s: s, + } +} + +// Request allows for building up a request to a server in a chained fashion. +type Request struct { + s *Server + err error + verb string + path string + body interface{} + selector labels.Selector + timeout time.Duration +} + +// Append an item to the request path. You must call Path at least once. +func (r *Request) Path(item string) *Request { + if r.err != nil { + return r + } + r.path = path.Join(r.path, item) + return r +} + +// Use the given item as a resource label selector. Optional. +func (r *Request) Selector(item string) *Request { + if r.err != nil { + return r + } + r.selector, r.err = labels.ParseSelector(item) + return r +} + +// Use the given duration as a timeout. Optional. +func (r *Request) Timeout(d time.Duration) *Request { + if r.err != nil { + return r + } + r.timeout = d + return r +} + +// Use obj as the body of the request. Optional. +// If obj is a string, try to read a file of that name. +// If obj is a []byte, send it directly. +// Otherwise, assume obj is an api type and marshall it correctly. +func (r *Request) Body(obj interface{}) *Request { + if r.err != nil { + return r + } + r.body = obj + return r +} + +// Format and xecute the request. Returns the API object received, or an error. +func (r *Request) Do() (interface{}, error) { + if r.err != nil { + return nil, r.err + } + finalUrl := path.Join(r.s.rawUrl, r.path) + query := url.Values{} + if r.selector != nil { + query.Add("labels", r.selector.String()) + } + if r.timeout != 0 { + query.Add("timeout", r.timeout.String()) + } + finalUrl += "?" + query.Encode() + var body io.Reader + if r.body != nil { + switch t := r.body.(type) { + case string: + data, err := ioutil.ReadFile(t) + if err != nil { + return nil, err + } + body = bytes.NewBuffer(data) + case []byte: + body = bytes.NewBuffer(t) + default: + data, err := api.Encode(r.body) + if err != nil { + return nil, err + } + body = bytes.NewBuffer(data) + } + } + req, err := http.NewRequest(r.verb, finalUrl, body) + if err != nil { + return nil, err + } + str, err := DoRequest(req, r.s.auth) + if err != nil { + return nil, err + } + return api.Decode([]byte(str)) +} + // RequestWithBody is a helper method that creates an HTTP request with the specified url, method // and a body read from 'configFile' // FIXME: need to be public API? diff --git a/pkg/cloudcfg/resource_printer.go b/pkg/cloudcfg/resource_printer.go index 6f2643cacc5..9ca587116b7 100644 --- a/pkg/cloudcfg/resource_printer.go +++ b/pkg/cloudcfg/resource_printer.go @@ -32,6 +32,7 @@ import ( type ResourcePrinter interface { // Print receives an arbitrary JSON body, formats it and prints it to a writer Print([]byte, io.Writer) error + PrintObj(interface{}, io.Writer) } // Identity printer simply copies the body out to the output stream @@ -42,6 +43,14 @@ func (i *IdentityPrinter) Print(data []byte, w io.Writer) error { return err } +func (i *IdentityPrinter) PrintObj(obj interface{}, output io.Writer) error { + data, err := api.EncodeIndent(obj) + if err != nil { + return err + } + return i.Print(data, output) +} + // YAMLPrinter parses JSON, and re-formats as YAML type YAMLPrinter struct{} @@ -58,6 +67,15 @@ func (y *YAMLPrinter) Print(data []byte, w io.Writer) error { return err } +func (y *YAMLPrinter) PrintObj(obj interface{}, w io.Writer) error { + output, err := yaml.Marshal(obj) + if err != nil { + return err + } + _, err = fmt.Fprint(w, string(output)) + return err +} + // HumanReadablePrinter attempts to provide more elegant output type HumanReadablePrinter struct{} @@ -168,6 +186,10 @@ func (h *HumanReadablePrinter) Print(data []byte, output io.Writer) error { if err != nil { return err } + return h.PrintObj(obj, output) +} + +func (h *HumanReadablePrinter) PrintObj(obj interface{}, output io.Writer) { switch o := obj.(type) { case *api.Pod: h.printHeader(podColumns, w) From 7667c7de425b4f0152bb3da954550672246125fb Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Sat, 21 Jun 2014 22:13:32 -0700 Subject: [PATCH 2/4] Add test --- pkg/cloudcfg/cloudcfg.go | 3 ++- pkg/cloudcfg/cloudcfg_test.go | 39 ++++++++++++++++++++++++++++++++ pkg/cloudcfg/resource_printer.go | 15 ++++-------- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/pkg/cloudcfg/cloudcfg.go b/pkg/cloudcfg/cloudcfg.go index b2d2df6fea1..07a6bd16cad 100644 --- a/pkg/cloudcfg/cloudcfg.go +++ b/pkg/cloudcfg/cloudcfg.go @@ -123,6 +123,7 @@ func (s *Server) Verb(verb string) *Request { return &Request{ verb: verb, s: s, + path: "/", } } @@ -181,7 +182,7 @@ func (r *Request) Do() (interface{}, error) { if r.err != nil { return nil, r.err } - finalUrl := path.Join(r.s.rawUrl, r.path) + finalUrl := r.s.rawUrl + r.path query := url.Values{} if r.selector != nil { query.Add("labels", r.selector.String()) diff --git a/pkg/cloudcfg/cloudcfg_test.go b/pkg/cloudcfg/cloudcfg_test.go index 178d983df53..9ccff33d69a 100644 --- a/pkg/cloudcfg/cloudcfg_test.go +++ b/pkg/cloudcfg/cloudcfg_test.go @@ -22,7 +22,9 @@ import ( "net/http" "net/http/httptest" "os" + "reflect" "testing" + "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" @@ -170,6 +172,43 @@ func TestDoRequest(t *testing.T) { fakeHandler.ValidateRequest(t, "/foo/bar", "GET", nil) } +func TestDoRequestNewWay(t *testing.T) { + reqBody := "request body" + expectedObj := &api.Service{Port: 12345} + expectedBody, _ := api.Encode(expectedObj) + fakeHandler := util.FakeHandler{ + StatusCode: 200, + ResponseBody: string(expectedBody), + T: t, + } + testServer := httptest.NewTLSServer(&fakeHandler) + auth := client.AuthInfo{User: "user", Password: "pass"} + s := New(testServer.URL, &auth) + obj, err := s.Verb("POST"). + Path("foo/bar"). + Path("baz"). + Selector("name=foo"). + Timeout(time.Second). + Body([]byte(reqBody)). + Do() + if err != nil { + t.Errorf("Unexpected error: %v %#v", err, err) + return + } + if obj == nil { + t.Error("nil obj") + } else if !reflect.DeepEqual(obj, expectedObj) { + t.Errorf("Expected: %#v, got %#v", expectedObj, obj) + } + fakeHandler.ValidateRequest(t, "/foo/bar/baz", "POST", &reqBody) + if fakeHandler.RequestReceived.URL.RawQuery != "labels=name%3Dfoo&timeout=1s" { + t.Errorf("Unexpected query: %v", fakeHandler.RequestReceived.URL.RawQuery) + } + if fakeHandler.RequestReceived.Header["Authorization"] == nil { + t.Errorf("Request is missing authorization header: %#v", *fakeHandler.RequestReceived) + } +} + func TestRunController(t *testing.T) { fakeClient := FakeKubeClient{} name := "name" diff --git a/pkg/cloudcfg/resource_printer.go b/pkg/cloudcfg/resource_printer.go index 9ca587116b7..58bffc48de1 100644 --- a/pkg/cloudcfg/resource_printer.go +++ b/pkg/cloudcfg/resource_printer.go @@ -165,19 +165,11 @@ func (h *HumanReadablePrinter) printStatus(status *api.Status, w io.Writer) erro // TODO replace this with something that returns a concrete printer object, rather than // having the secondary switch below. func (h *HumanReadablePrinter) Print(data []byte, output io.Writer) error { - w := tabwriter.NewWriter(output, 20, 5, 3, ' ', 0) - defer w.Flush() var mapObj map[string]interface{} if err := json.Unmarshal([]byte(data), &mapObj); err != nil { return err } - // Don't complain about empty objects returned by DELETE commands. - if len(mapObj) == 0 { - fmt.Fprint(w, "") - return nil - } - if _, contains := mapObj["kind"]; !contains { return fmt.Errorf("unexpected object with no 'kind' field: %s", data) } @@ -189,7 +181,9 @@ func (h *HumanReadablePrinter) Print(data []byte, output io.Writer) error { return h.PrintObj(obj, output) } -func (h *HumanReadablePrinter) PrintObj(obj interface{}, output io.Writer) { +func (h *HumanReadablePrinter) PrintObj(obj interface{}, output io.Writer) error { + w := tabwriter.NewWriter(output, 20, 5, 3, ' ', 0) + defer w.Flush() switch o := obj.(type) { case *api.Pod: h.printHeader(podColumns, w) @@ -212,6 +206,7 @@ func (h *HumanReadablePrinter) PrintObj(obj interface{}, output io.Writer) { case *api.Status: return h.printStatus(o, w) default: - return h.unknown(data, w) + _, err := fmt.Fprintf(w, "Error: unknown type %#v", obj) + return err } } From 6ccd9b2361af2e1b46dfd5236057a3c54325c5db Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Sun, 22 Jun 2014 12:05:34 -0700 Subject: [PATCH 3/4] Move to new file. Build and tests pass. --- cmd/cloudcfg/cloudcfg.go | 8 +- pkg/cloudcfg/cloudcfg.go | 140 +-------------------------- pkg/cloudcfg/cloudcfg_test.go | 45 +-------- pkg/cloudcfg/request.go | 158 +++++++++++++++++++++++++++++++ pkg/cloudcfg/request_test.go | 104 ++++++++++++++++++++ pkg/cloudcfg/resource_printer.go | 2 +- 6 files changed, 271 insertions(+), 186 deletions(-) create mode 100644 pkg/cloudcfg/request.go create mode 100644 pkg/cloudcfg/request_test.go diff --git a/cmd/cloudcfg/cloudcfg.go b/cmd/cloudcfg/cloudcfg.go index eaad7381865..c27d7b26a9b 100644 --- a/cmd/cloudcfg/cloudcfg.go +++ b/cmd/cloudcfg/cloudcfg.go @@ -21,10 +21,8 @@ import ( "fmt" "io/ioutil" "log" - "net/http" "net/url" "os" - "path" "strconv" "strings" "time" @@ -135,10 +133,6 @@ func executeAPIRequest(method string, auth *kube_client.AuthInfo) bool { return strings.Trim(flag.Arg(1), "/") } - readUrl := func(storage string) string { - return *httpServer + path.Join("/api/v1beta1", storage) - } - verb := "" switch method { case "get", "list": @@ -177,7 +171,7 @@ func executeAPIRequest(method string, auth *kube_client.AuthInfo) bool { } if err = printer.PrintObj(obj, os.Stdout); err != nil { - log.Fatalf("Failed to print: %#v\nRaw received text:\n%v\n", err, string(body)) + log.Fatalf("Failed to print: %#v\nRaw received object:\n%#v\n", err, obj) } fmt.Print("\n") diff --git a/pkg/cloudcfg/cloudcfg.go b/pkg/cloudcfg/cloudcfg.go index 07a6bd16cad..0d17707d7aa 100644 --- a/pkg/cloudcfg/cloudcfg.go +++ b/pkg/cloudcfg/cloudcfg.go @@ -21,20 +21,16 @@ import ( "crypto/tls" "encoding/json" "fmt" - "io" "io/ioutil" "log" "net/http" - "net/url" "os" - "path" "strconv" "strings" "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "gopkg.in/v1/yaml" ) @@ -94,137 +90,9 @@ func Update(name string, client client.ClientInterface, updatePeriod time.Durati return nil } -// Server contains info locating a kubernetes api server. -// Example usage: -// auth, err := LoadAuth(filename) -// s := New(url, auth) -// resp, err := s.Verb("GET"). -// Path("api/v1beta1"). -// Path("pods"). -// Selector("area=staging"). -// Timeout(10*time.Second). -// Do() -// list, ok := resp.(api.PodList) -type Server struct { - auth *client.AuthInfo - rawUrl string -} - -// Create a new server object. -func New(serverUrl string, auth *client.AuthInfo) *Server { - return &Server{ - auth: auth, - rawUrl: serverUrl, - } -} - -// Begin a request with a verb (GET, POST, PUT, DELETE) -func (s *Server) Verb(verb string) *Request { - return &Request{ - verb: verb, - s: s, - path: "/", - } -} - -// Request allows for building up a request to a server in a chained fashion. -type Request struct { - s *Server - err error - verb string - path string - body interface{} - selector labels.Selector - timeout time.Duration -} - -// Append an item to the request path. You must call Path at least once. -func (r *Request) Path(item string) *Request { - if r.err != nil { - return r - } - r.path = path.Join(r.path, item) - return r -} - -// Use the given item as a resource label selector. Optional. -func (r *Request) Selector(item string) *Request { - if r.err != nil { - return r - } - r.selector, r.err = labels.ParseSelector(item) - return r -} - -// Use the given duration as a timeout. Optional. -func (r *Request) Timeout(d time.Duration) *Request { - if r.err != nil { - return r - } - r.timeout = d - return r -} - -// Use obj as the body of the request. Optional. -// If obj is a string, try to read a file of that name. -// If obj is a []byte, send it directly. -// Otherwise, assume obj is an api type and marshall it correctly. -func (r *Request) Body(obj interface{}) *Request { - if r.err != nil { - return r - } - r.body = obj - return r -} - -// Format and xecute the request. Returns the API object received, or an error. -func (r *Request) Do() (interface{}, error) { - if r.err != nil { - return nil, r.err - } - finalUrl := r.s.rawUrl + r.path - query := url.Values{} - if r.selector != nil { - query.Add("labels", r.selector.String()) - } - if r.timeout != 0 { - query.Add("timeout", r.timeout.String()) - } - finalUrl += "?" + query.Encode() - var body io.Reader - if r.body != nil { - switch t := r.body.(type) { - case string: - data, err := ioutil.ReadFile(t) - if err != nil { - return nil, err - } - body = bytes.NewBuffer(data) - case []byte: - body = bytes.NewBuffer(t) - default: - data, err := api.Encode(r.body) - if err != nil { - return nil, err - } - body = bytes.NewBuffer(data) - } - } - req, err := http.NewRequest(r.verb, finalUrl, body) - if err != nil { - return nil, err - } - str, err := DoRequest(req, r.s.auth) - if err != nil { - return nil, err - } - return api.Decode([]byte(str)) -} - // RequestWithBody is a helper method that creates an HTTP request with the specified url, method // and a body read from 'configFile' -// FIXME: need to be public API? -func RequestWithBody(configFile, url, method string) (*http.Request, error) { +func requestWithBody(configFile, url, method string) (*http.Request, error) { if len(configFile) == 0 { return nil, fmt.Errorf("empty config file.") } @@ -232,19 +100,19 @@ func RequestWithBody(configFile, url, method string) (*http.Request, error) { if err != nil { return nil, err } - return RequestWithBodyData(data, url, method) + return requestWithBodyData(data, url, method) } // RequestWithBodyData is a helper method that creates an HTTP request with the specified url, method // and body data -func RequestWithBodyData(data []byte, url, method string) (*http.Request, error) { +func requestWithBodyData(data []byte, url, method string) (*http.Request, error) { request, err := http.NewRequest(method, url, bytes.NewBuffer(data)) request.ContentLength = int64(len(data)) return request, err } // Execute a request, adds authentication (if auth != nil), and HTTPS cert ignoring. -func DoRequest(request *http.Request, auth *client.AuthInfo) ([]byte, error) { +func doRequest(request *http.Request, auth *client.AuthInfo) ([]byte, error) { if auth != nil { request.SetBasicAuth(auth.User, auth.Password) } diff --git a/pkg/cloudcfg/cloudcfg_test.go b/pkg/cloudcfg/cloudcfg_test.go index 9ccff33d69a..13795309133 100644 --- a/pkg/cloudcfg/cloudcfg_test.go +++ b/pkg/cloudcfg/cloudcfg_test.go @@ -22,9 +22,7 @@ import ( "net/http" "net/http/httptest" "os" - "reflect" "testing" - "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" @@ -159,7 +157,7 @@ func TestDoRequest(t *testing.T) { testServer := httptest.NewTLSServer(&fakeHandler) request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil) auth := client.AuthInfo{User: "user", Password: "pass"} - body, err := DoRequest(request, &auth) + body, err := doRequest(request, &auth) if request.Header["Authorization"] == nil { t.Errorf("Request is missing authorization header: %#v", *request) } @@ -172,43 +170,6 @@ func TestDoRequest(t *testing.T) { fakeHandler.ValidateRequest(t, "/foo/bar", "GET", nil) } -func TestDoRequestNewWay(t *testing.T) { - reqBody := "request body" - expectedObj := &api.Service{Port: 12345} - expectedBody, _ := api.Encode(expectedObj) - fakeHandler := util.FakeHandler{ - StatusCode: 200, - ResponseBody: string(expectedBody), - T: t, - } - testServer := httptest.NewTLSServer(&fakeHandler) - auth := client.AuthInfo{User: "user", Password: "pass"} - s := New(testServer.URL, &auth) - obj, err := s.Verb("POST"). - Path("foo/bar"). - Path("baz"). - Selector("name=foo"). - Timeout(time.Second). - Body([]byte(reqBody)). - Do() - if err != nil { - t.Errorf("Unexpected error: %v %#v", err, err) - return - } - if obj == nil { - t.Error("nil obj") - } else if !reflect.DeepEqual(obj, expectedObj) { - t.Errorf("Expected: %#v, got %#v", expectedObj, obj) - } - fakeHandler.ValidateRequest(t, "/foo/bar/baz", "POST", &reqBody) - if fakeHandler.RequestReceived.URL.RawQuery != "labels=name%3Dfoo&timeout=1s" { - t.Errorf("Unexpected query: %v", fakeHandler.RequestReceived.URL.RawQuery) - } - if fakeHandler.RequestReceived.Header["Authorization"] == nil { - t.Errorf("Request is missing authorization header: %#v", *fakeHandler.RequestReceived) - } -} - func TestRunController(t *testing.T) { fakeClient := FakeKubeClient{} name := "name" @@ -323,7 +284,7 @@ func TestCloudCfgDeleteControllerWithReplicas(t *testing.T) { } func TestRequestWithBodyNoSuchFile(t *testing.T) { - request, err := RequestWithBody("non/existent/file.json", "http://www.google.com", "GET") + request, err := requestWithBody("non/existent/file.json", "http://www.google.com", "GET") if request != nil { t.Error("Unexpected non-nil result") } @@ -372,7 +333,7 @@ func TestRequestWithBody(t *testing.T) { expectNoError(t, err) _, err = file.Write(data) expectNoError(t, err) - request, err := RequestWithBody(file.Name(), "http://www.google.com", "GET") + request, err := requestWithBody(file.Name(), "http://www.google.com", "GET") if request == nil { t.Error("Unexpected nil result") } diff --git a/pkg/cloudcfg/request.go b/pkg/cloudcfg/request.go new file mode 100644 index 00000000000..cea3d98a0a3 --- /dev/null +++ b/pkg/cloudcfg/request.go @@ -0,0 +1,158 @@ +/* +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 cloudcfg + +import ( + "bytes" + "io" + "io/ioutil" + "net/http" + "net/url" + "path" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" +) + +// Server contains info locating a kubernetes api server. +// Example usage: +// auth, err := LoadAuth(filename) +// s := New(url, auth) +// resp, err := s.Verb("GET"). +// Path("api/v1beta1"). +// Path("pods"). +// Selector("area=staging"). +// Timeout(10*time.Second). +// Do() +// list, ok := resp.(api.PodList) +type Server struct { + auth *client.AuthInfo + rawUrl string +} + +// Create a new server object. +func New(serverUrl string, auth *client.AuthInfo) *Server { + return &Server{ + auth: auth, + rawUrl: serverUrl, + } +} + +// Begin a request with a verb (GET, POST, PUT, DELETE) +func (s *Server) Verb(verb string) *Request { + return &Request{ + verb: verb, + s: s, + path: "/", + } +} + +// Request allows for building up a request to a server in a chained fashion. +type Request struct { + s *Server + err error + verb string + path string + body interface{} + selector labels.Selector + timeout time.Duration +} + +// Append an item to the request path. You must call Path at least once. +func (r *Request) Path(item string) *Request { + if r.err != nil { + return r + } + r.path = path.Join(r.path, item) + return r +} + +// Use the given item as a resource label selector. Optional. +func (r *Request) Selector(item string) *Request { + if r.err != nil { + return r + } + r.selector, r.err = labels.ParseSelector(item) + return r +} + +// Use the given duration as a timeout. Optional. +func (r *Request) Timeout(d time.Duration) *Request { + if r.err != nil { + return r + } + r.timeout = d + return r +} + +// Use obj as the body of the request. Optional. +// If obj is a string, try to read a file of that name. +// If obj is a []byte, send it directly. +// Otherwise, assume obj is an api type and marshall it correctly. +func (r *Request) Body(obj interface{}) *Request { + if r.err != nil { + return r + } + r.body = obj + return r +} + +// Format and xecute the request. Returns the API object received, or an error. +func (r *Request) Do() (interface{}, error) { + if r.err != nil { + return nil, r.err + } + finalUrl := r.s.rawUrl + r.path + query := url.Values{} + if r.selector != nil { + query.Add("labels", r.selector.String()) + } + if r.timeout != 0 { + query.Add("timeout", r.timeout.String()) + } + finalUrl += "?" + query.Encode() + var body io.Reader + if r.body != nil { + switch t := r.body.(type) { + case string: + data, err := ioutil.ReadFile(t) + if err != nil { + return nil, err + } + body = bytes.NewBuffer(data) + case []byte: + body = bytes.NewBuffer(t) + default: + data, err := api.Encode(r.body) + if err != nil { + return nil, err + } + body = bytes.NewBuffer(data) + } + } + req, err := http.NewRequest(r.verb, finalUrl, body) + if err != nil { + return nil, err + } + str, err := doRequest(req, r.s.auth) + if err != nil { + return nil, err + } + return api.Decode([]byte(str)) +} diff --git a/pkg/cloudcfg/request_test.go b/pkg/cloudcfg/request_test.go new file mode 100644 index 00000000000..d05fbe63167 --- /dev/null +++ b/pkg/cloudcfg/request_test.go @@ -0,0 +1,104 @@ +/* +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 cloudcfg + +import ( + "net/http/httptest" + "reflect" + "testing" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +func TestDoRequestNewWay(t *testing.T) { + reqBody := "request body" + expectedObj := &api.Service{Port: 12345} + expectedBody, _ := api.Encode(expectedObj) + fakeHandler := util.FakeHandler{ + StatusCode: 200, + ResponseBody: string(expectedBody), + T: t, + } + testServer := httptest.NewTLSServer(&fakeHandler) + auth := client.AuthInfo{User: "user", Password: "pass"} + s := New(testServer.URL, &auth) + obj, err := s.Verb("POST"). + Path("foo/bar"). + Path("baz"). + Selector("name=foo"). + Timeout(time.Second). + Body([]byte(reqBody)). + Do() + if err != nil { + t.Errorf("Unexpected error: %v %#v", err, err) + return + } + if obj == nil { + t.Error("nil obj") + } else if !reflect.DeepEqual(obj, expectedObj) { + t.Errorf("Expected: %#v, got %#v", expectedObj, obj) + } + fakeHandler.ValidateRequest(t, "/foo/bar/baz", "POST", &reqBody) + if fakeHandler.RequestReceived.URL.RawQuery != "labels=name%3Dfoo&timeout=1s" { + t.Errorf("Unexpected query: %v", fakeHandler.RequestReceived.URL.RawQuery) + } + if fakeHandler.RequestReceived.Header["Authorization"] == nil { + t.Errorf("Request is missing authorization header: %#v", *fakeHandler.RequestReceived) + } +} + +func TestDoRequestNewWayObj(t *testing.T) { + reqObj := &api.Pod{} + reqBodyExpected, _ := api.Encode(reqObj) + expectedObj := &api.Service{Port: 12345} + expectedBody, _ := api.Encode(expectedObj) + fakeHandler := util.FakeHandler{ + StatusCode: 200, + ResponseBody: string(expectedBody), + T: t, + } + testServer := httptest.NewTLSServer(&fakeHandler) + auth := client.AuthInfo{User: "user", Password: "pass"} + s := New(testServer.URL, &auth) + obj, err := s.Verb("POST"). + Path("foo/bar"). + Path("baz"). + Selector("name=foo"). + Timeout(time.Second). + Body(reqObj). + Do() + if err != nil { + t.Errorf("Unexpected error: %v %#v", err, err) + return + } + if obj == nil { + t.Error("nil obj") + } else if !reflect.DeepEqual(obj, expectedObj) { + t.Errorf("Expected: %#v, got %#v", expectedObj, obj) + } + tmpStr := string(reqBodyExpected) + fakeHandler.ValidateRequest(t, "/foo/bar/baz", "POST", &tmpStr) + if fakeHandler.RequestReceived.URL.RawQuery != "labels=name%3Dfoo&timeout=1s" { + t.Errorf("Unexpected query: %v", fakeHandler.RequestReceived.URL.RawQuery) + } + if fakeHandler.RequestReceived.Header["Authorization"] == nil { + t.Errorf("Request is missing authorization header: %#v", *fakeHandler.RequestReceived) + } +} diff --git a/pkg/cloudcfg/resource_printer.go b/pkg/cloudcfg/resource_printer.go index 58bffc48de1..281abf74170 100644 --- a/pkg/cloudcfg/resource_printer.go +++ b/pkg/cloudcfg/resource_printer.go @@ -32,7 +32,7 @@ import ( type ResourcePrinter interface { // Print receives an arbitrary JSON body, formats it and prints it to a writer Print([]byte, io.Writer) error - PrintObj(interface{}, io.Writer) + PrintObj(interface{}, io.Writer) error } // Identity printer simply copies the body out to the output stream From f62440a65ef1c87243ff7be832c55d9046d31d56 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Mon, 23 Jun 2014 13:35:14 -0700 Subject: [PATCH 4/4] Fix call to removed function EncodeIndent --- pkg/cloudcfg/resource_printer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cloudcfg/resource_printer.go b/pkg/cloudcfg/resource_printer.go index 281abf74170..02c13791a99 100644 --- a/pkg/cloudcfg/resource_printer.go +++ b/pkg/cloudcfg/resource_printer.go @@ -44,7 +44,7 @@ func (i *IdentityPrinter) Print(data []byte, w io.Writer) error { } func (i *IdentityPrinter) PrintObj(obj interface{}, output io.Writer) error { - data, err := api.EncodeIndent(obj) + data, err := api.Encode(obj) if err != nil { return err }