diff --git a/cmd/cloudcfg/cloudcfg.go b/cmd/cloudcfg/cloudcfg.go index 628f2f83214..5a952529f0a 100644 --- a/cmd/cloudcfg/cloudcfg.go +++ b/cmd/cloudcfg/cloudcfg.go @@ -18,12 +18,14 @@ package main import ( "flag" "fmt" + "io/ioutil" "log" "net/http" "net/url" "os" "path" "strconv" + "strings" "time" kube_client "github.com/GoogleCloudPlatform/kubernetes/pkg/client" @@ -50,6 +52,23 @@ func usage() { log.Fatal("Usage: cloudcfg -h [-c config/file.json] [-p :,..., : ") } +// Reads & parses config file. On error, calls log.Fatal(). +func readConfig(storage string) []byte { + if len(*config) == 0 { + log.Fatal("Need config file (-c)") + } + data, err := ioutil.ReadFile(*config) + if err != nil { + log.Fatalf("Unable to read %v: %#v\n", *config, err) + } + data, err = cloudcfg.ToWireFormat(data, storage) + if err != nil { + log.Fatalf("Error parsing %v as an object for %v: %#v\n", *config, storage, err) + } + log.Printf("Parsed config file successfully; sending:\n%v\n", string(data)) + return data +} + // CloudCfg command line tool. func main() { flag.Parse() // Scan the arguments list @@ -71,7 +90,8 @@ func main() { if parsedUrl.Scheme != "" && parsedUrl.Scheme != "https" { secure = false } - url := *httpServer + path.Join("/api/v1beta1", flag.Arg(1)) + storage := strings.Trim(flag.Arg(1), "/") + url := *httpServer + path.Join("/api/v1beta1", storage) var request *http.Request var printer cloudcfg.ResourcePrinter @@ -100,9 +120,9 @@ func main() { case "delete": request, err = http.NewRequest("DELETE", url, nil) case "create": - request, err = cloudcfg.RequestWithBody(*config, url, "POST") + request, err = cloudcfg.RequestWithBodyData(readConfig(storage), url, "POST") case "update": - request, err = cloudcfg.RequestWithBody(*config, url, "PUT") + request, err = cloudcfg.RequestWithBodyData(readConfig(storage), url, "PUT") case "rollingupdate": client := &kube_client.Client{ Host: *httpServer, @@ -149,7 +169,7 @@ func main() { } err = printer.Print(body, os.Stdout) if err != nil { - log.Fatalf("Failed to print: %#v", err) + log.Fatalf("Failed to print: %#v\nRaw received text:\n%v\n", err, string(body)) } fmt.Print("\n") } diff --git a/hooks/prepare-commit-msg b/hooks/prepare-commit-msg index 0ce78c6ebe5..6ade9aab2d4 100755 --- a/hooks/prepare-commit-msg +++ b/hooks/prepare-commit-msg @@ -10,7 +10,7 @@ for file in $(git diff --cached --name-only | grep "\.go"); do done if [[ $errors == "1" ]]; then - echo "# To fix these errors, run gofmt -w ." >> $1 + echo "# To fix these errors, run gofmt -s -w ." >> $1 echo "# If you want to commit in spite of these format errors," >> $1 echo "# then delete this line. Otherwise, your commit will be" >> $1 echo "# aborted." >> $1 diff --git a/pkg/api/types.go b/pkg/api/types.go index 38179c4c7ec..eeeb36d5716 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -91,13 +91,13 @@ type PodState struct { } type PodList struct { - JSONBase - Items []Pod `json:"items" yaml:"items,omitempty"` + JSONBase `json:",inline" yaml:",inline"` + Items []Pod `json:"items" yaml:"items,omitempty"` } // Pod is a collection of containers, used as either input (create, update) or as output (list, get) type Pod struct { - JSONBase + JSONBase `json:",inline" yaml:",inline"` Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` DesiredState PodState `json:"desiredState,omitempty" yaml:"desiredState,omitempty"` CurrentState PodState `json:"currentState,omitempty" yaml:"currentState,omitempty"` @@ -111,13 +111,13 @@ type ReplicationControllerState struct { } type ReplicationControllerList struct { - JSONBase - Items []ReplicationController `json:"items,omitempty" yaml:"items,omitempty"` + JSONBase `json:",inline" yaml:",inline"` + Items []ReplicationController `json:"items,omitempty" yaml:"items,omitempty"` } // ReplicationController represents the configuration of a replication controller type ReplicationController struct { - JSONBase + JSONBase `json:",inline" yaml:",inline"` DesiredState ReplicationControllerState `json:"desiredState,omitempty" yaml:"desiredState,omitempty"` Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` } @@ -130,16 +130,16 @@ type PodTemplate struct { // ServiceList holds a list of services type ServiceList struct { - JSONBase - Items []Service `json:"items" yaml:"items"` + JSONBase `json:",inline" yaml:",inline"` + Items []Service `json:"items" yaml:"items"` } // Defines a service abstraction by a name (for example, mysql) consisting of local port // (for example 3306) that the proxy listens on, and the labels that define the service. type Service struct { - JSONBase - Port int `json:"port,omitempty" yaml:"port,omitempty"` - Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + JSONBase `json:",inline" yaml:",inline"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` } // Defines the endpoints that implement the actual service, for example: diff --git a/pkg/cloudcfg/cloudcfg.go b/pkg/cloudcfg/cloudcfg.go index 0245fe5f7ef..2f589408eb2 100644 --- a/pkg/cloudcfg/cloudcfg.go +++ b/pkg/cloudcfg/cloudcfg.go @@ -102,12 +102,12 @@ 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 +// 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 @@ -250,7 +250,7 @@ func DeleteController(name string, client client.ClientInterface) error { return err } if controller.DesiredState.Replicas != 0 { - return fmt.Errorf("controller has non-zero replicas (%d)", controller.DesiredState.Replicas) + return fmt.Errorf("controller has non-zero replicas (%d), please stop it first", controller.DesiredState.Replicas) } return client.DeleteReplicationController(name) } diff --git a/pkg/cloudcfg/cloudcfg_test.go b/pkg/cloudcfg/cloudcfg_test.go index 04dd8dd800a..f4bffb2857c 100644 --- a/pkg/cloudcfg/cloudcfg_test.go +++ b/pkg/cloudcfg/cloudcfg_test.go @@ -150,6 +150,7 @@ func TestDoRequest(t *testing.T) { fakeHandler := util.FakeHandler{ StatusCode: 200, ResponseBody: expectedBody, + T: t, } testServer := httptest.NewTLSServer(&fakeHandler) request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil) diff --git a/pkg/cloudcfg/parse.go b/pkg/cloudcfg/parse.go new file mode 100644 index 00000000000..4e1f4d0e173 --- /dev/null +++ b/pkg/cloudcfg/parse.go @@ -0,0 +1,33 @@ +package cloudcfg + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "gopkg.in/v1/yaml" +) + +var storageToType = map[string]reflect.Type{ + "pods": reflect.TypeOf(api.Pod{}), + "services": reflect.TypeOf(api.Service{}), + "replicationControllers": reflect.TypeOf(api.ReplicationController{}), +} + +// Takes input 'data' as either json or yaml, checks that it parses as the +// appropriate object type, and returns json for sending to the API or an +// error. +func ToWireFormat(data []byte, storage string) ([]byte, error) { + prototypeType, found := storageToType[storage] + if !found { + return nil, fmt.Errorf("unknown storage type: %v", storage) + } + + obj := reflect.New(prototypeType).Interface() + err := yaml.Unmarshal(data, obj) + if err != nil { + return nil, err + } + return json.Marshal(obj) +} diff --git a/pkg/cloudcfg/parse_test.go b/pkg/cloudcfg/parse_test.go new file mode 100644 index 00000000000..1884466de6e --- /dev/null +++ b/pkg/cloudcfg/parse_test.go @@ -0,0 +1,88 @@ +package cloudcfg + +import ( + "encoding/json" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "gopkg.in/v1/yaml" +) + +func TestParseBadStorage(t *testing.T) { + _, err := ToWireFormat([]byte("{}"), "badstorage") + if err == nil { + t.Errorf("Expected error, received none") + } +} + +func DoParseTest(t *testing.T, storage string, obj interface{}) { + json_data, _ := json.Marshal(obj) + yaml_data, _ := yaml.Marshal(obj) + t.Logf("Intermediate yaml:\n%v\n", string(yaml_data)) + + json_got, json_err := ToWireFormat(json_data, storage) + yaml_got, yaml_err := ToWireFormat(yaml_data, storage) + + if json_err != nil { + t.Errorf("json err: %#v", json_err) + } + if yaml_err != nil { + t.Errorf("yaml err: %#v", yaml_err) + } + if string(json_got) != string(json_data) { + t.Errorf("json output didn't match:\nGot:\n%v\n\nWanted:\n%v\n", + string(json_got), string(json_data)) + } + if string(yaml_got) != string(json_data) { + t.Errorf("yaml parsed output didn't match:\nGot:\n%v\n\nWanted:\n%v\n", + string(yaml_got), string(json_data)) + } +} + +func TestParsePod(t *testing.T) { + DoParseTest(t, "pods", api.Pod{ + JSONBase: api.JSONBase{ID: "test pod"}, + DesiredState: api.PodState{ + Manifest: api.ContainerManifest{ + Id: "My manifest", + Containers: []api.Container{ + {Name: "my container"}, + }, + Volumes: []api.Volume{ + {Name: "volume"}, + }, + }, + }, + }) +} + +func TestParseService(t *testing.T) { + DoParseTest(t, "services", api.Service{ + JSONBase: api.JSONBase{ID: "my service"}, + Port: 8080, + Labels: map[string]string{ + "area": "staging", + }, + }) +} + +func TestParseController(t *testing.T) { + DoParseTest(t, "replicationControllers", api.ReplicationController{ + DesiredState: api.ReplicationControllerState{ + Replicas: 9001, + PodTemplate: api.PodTemplate{ + DesiredState: api.PodState{ + Manifest: api.ContainerManifest{ + Id: "My manifest", + Containers: []api.Container{ + {Name: "my container"}, + }, + Volumes: []api.Volume{ + {Name: "volume"}, + }, + }, + }, + }, + }, + }) +} diff --git a/pkg/util/fake_handler.go b/pkg/util/fake_handler.go index fa6a698e2f7..f6b4adb7253 100644 --- a/pkg/util/fake_handler.go +++ b/pkg/util/fake_handler.go @@ -17,7 +17,6 @@ package util import ( "io/ioutil" - "log" "net/http" ) @@ -26,12 +25,18 @@ import ( type TestInterface interface { Errorf(format string, args ...interface{}) } +type LogInterface interface { + Logf(format string, args ...interface{}) +} // FakeHandler is to assist in testing HTTP requests. type FakeHandler struct { RequestReceived *http.Request StatusCode int ResponseBody string + // For logging - you can use a *testing.T + // This will keep log messages associated with the test. + T LogInterface } func (f *FakeHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) { @@ -40,8 +45,8 @@ func (f *FakeHandler) ServeHTTP(response http.ResponseWriter, request *http.Requ response.Write([]byte(f.ResponseBody)) bodyReceived, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Printf("Received read error: %#v", err) + if err != nil && f.T != nil { + f.T.Logf("Received read error: %#v", err) } f.ResponseBody = string(bodyReceived) }