Merge pull request #203 from lavalamp/api

cloundcfg api improvements
pull/6/head
brendandburns 2014-06-23 14:06:08 -07:00
commit 11963ecf35
6 changed files with 317 additions and 43 deletions

View File

@ -21,10 +21,8 @@ import (
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
"time"
@ -135,31 +133,34 @@ 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)
}
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 +170,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 object:\n%#v\n", err, obj)
}
fmt.Print("\n")
return true
}

View File

@ -92,8 +92,7 @@ func Update(name string, client client.ClientInterface, updatePeriod time.Durati
// 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.")
}
@ -101,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)
}

View File

@ -157,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)
}
@ -284,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")
}
@ -333,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")
}

158
pkg/cloudcfg/request.go Normal file
View File

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

View File

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

View File

@ -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) error
}
// 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.Encode(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{}
@ -147,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, "<empty>")
return nil
}
if _, contains := mapObj["kind"]; !contains {
return fmt.Errorf("unexpected object with no 'kind' field: %s", data)
}
@ -168,6 +178,12 @@ 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) error {
w := tabwriter.NewWriter(output, 20, 5, 3, ' ', 0)
defer w.Flush()
switch o := obj.(type) {
case *api.Pod:
h.printHeader(podColumns, w)
@ -190,6 +206,7 @@ func (h *HumanReadablePrinter) Print(data []byte, output io.Writer) error {
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
}
}