diff --git a/client/README.md b/client/README.md index fededef1..19d50eb9 100644 --- a/client/README.md +++ b/client/README.md @@ -30,7 +30,7 @@ This method returns a cadvisor/info.MachineInfo struct with all the fields fille }) ``` -You can see the full specification of the [MachineInfo struct in the source](../info/container.go) +You can see the full specification of the [MachineInfo struct in the source](../info/v1/machine.go) ### ContainerInfo diff --git a/client/client_test.go b/client/client_test.go index 40b4c64f..d6ec0708 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -61,8 +61,6 @@ func cadvisorTestClient(path string, expectedPostObj *info.ContainerInfoRequest, } encoder := json.NewEncoder(w) encoder.Encode(replyObj) - } else if r.URL.Path == "/api/v1.2/machine" { - fmt.Fprint(w, `{"num_cores":8,"memory_capacity":31625871360, "disk_map":["8:0":{"name":"sda","major":8,"minor":0,"size":10737418240}]}`) } else { w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "Page not found.") diff --git a/client/v2/README.md b/client/v2/README.md new file mode 100644 index 00000000..d49c3094 --- /dev/null +++ b/client/v2/README.md @@ -0,0 +1,69 @@ +# Example REST API Client + +This is an implementation of a cAdvisor REST API in Go. You can use it like this: + +```go +client, err := client.NewClient("http://192.168.59.103:8080/") +``` + +Obviously, replace the URL with the path to your actual cAdvisor REST endpoint. + + +### MachineInfo + +```go +client.MachineInfo() +``` + +This method returns a cadvisor/info.MachineInfo struct with all the fields filled in. Here is an example return value: + +``` +(*info.MachineInfo)(0xc208022b10)({ + NumCores: (int) 4, + MemoryCapacity: (int64) 2106028032, + Filesystems: ([]info.FsInfo) (len=1 cap=4) { + (info.FsInfo) { + Device: (string) (len=9) "/dev/sda1", + Capacity: (uint64) 19507089408 + } + } +}) +``` + +You can see the full specification of the [MachineInfo struct in the source](../../info/v1/machine.go) + +### VersionInfo + +```go +client.VersionInfo() +``` + +This method returns the cAdvisor version. + +### Attributes + +```go +client.Attributes() +``` + +This method returns a [cadvisor/info/v2/Attributes](../../info/v2/machine.go) struct with all the fields filled in. Attributes includes hardware attributes (as returned by MachineInfo) as well as software attributes (eg. software versions). Here is an example return value: + +``` +(*v2.Attributes)({ + KernelVersion: (string) (len=17) "3.13.0-44-generic" + ContainerOsVersion: (string) (len=18) "Ubuntu 14.04.1 LTS" + DockerVersion: (string) (len=9) "1.5.0-rc4" + CadvisorVersion: (string) (len=6) "0.10.1" + NumCores: (int) 4, + MemoryCapacity: (int64) 2106028032, + Filesystems: ([]info.FsInfo) (len=1 cap=4) { + (info.FsInfo) { + Device: (string) (len=9) "/dev/sda1", + Capacity: (uint64) 19507089408 + } + } +}) +``` + +You can see the full specification of the [Attributes struct in the source](../../info/v2/machine.go) + diff --git a/client/v2/client.go b/client/v2/client.go new file mode 100644 index 00000000..44ddd28a --- /dev/null +++ b/client/v2/client.go @@ -0,0 +1,139 @@ +// Copyright 2015 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. + +// Client library to programmatically access cAdvisor API. +package v2 + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "path" + "strings" + + v1 "github.com/google/cadvisor/info/v1" + info "github.com/google/cadvisor/info/v2" +) + +// Client represents the base URL for a cAdvisor client. +type Client struct { + baseUrl string +} + +// NewClient returns a new client with the specified base URL. +func NewClient(url string) (*Client, error) { + if !strings.HasSuffix(url, "/") { + url += "/" + } + + return &Client{ + baseUrl: fmt.Sprintf("%sapi/v2.0/", url), + }, nil +} + +// MachineInfo returns the JSON machine information for this client. +// A non-nil error result indicates a problem with obtaining +// the JSON machine information data. +func (self *Client) MachineInfo() (minfo *v1.MachineInfo, err error) { + u := self.machineInfoUrl() + ret := new(v1.MachineInfo) + if err = self.httpGetJsonData(ret, nil, u, "machine info"); err != nil { + return + } + minfo = ret + return +} + +// VersionInfo returns the version info for cAdvisor. +func (self *Client) VersionInfo() (version string, err error) { + u := self.versionInfoUrl() + version, err = self.httpGetString(u, "version info") + return +} + +// Attributes returns hardware and software attributes of the machine. +func (self *Client) Attributes() (attr *info.Attributes, err error) { + u := self.attributesUrl() + ret := new(info.Attributes) + if err = self.httpGetJsonData(ret, nil, u, "attributes"); err != nil { + return + } + attr = ret + return +} + +func (self *Client) machineInfoUrl() string { + return self.baseUrl + path.Join("machine") +} + +func (self *Client) versionInfoUrl() string { + return self.baseUrl + path.Join("version") +} + +func (self *Client) attributesUrl() string { + return self.baseUrl + path.Join("attributes") +} + +func (self *Client) httpGetResponse(postData interface{}, url, infoName string) ([]byte, error) { + var resp *http.Response + var err error + + if postData != nil { + data, err := json.Marshal(postData) + if err != nil { + return nil, fmt.Errorf("unable to marshal data: %v", err) + } + resp, err = http.Post(url, "application/json", bytes.NewBuffer(data)) + } else { + resp, err = http.Get(url) + } + if err != nil { + return nil, fmt.Errorf("unable to get %q from %q: %v", infoName, url, err) + } + if resp == nil { + return nil, fmt.Errorf("received empty response for %q from %q", infoName, url) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + err = fmt.Errorf("unable to read all %q from %q: %v", infoName, url, err) + return nil, err + } + if resp.StatusCode != 200 { + return nil, fmt.Errorf("request %q failed with error: %q", url, strings.TrimSpace(string(body))) + } + return body, nil +} + +func (self *Client) httpGetString(url, infoName string) (string, error) { + body, err := self.httpGetResponse(nil, url, infoName) + if err != nil { + return "", err + } + return string(body), nil +} + +func (self *Client) httpGetJsonData(data, postData interface{}, url, infoName string) error { + body, err := self.httpGetResponse(postData, url, infoName) + if err != nil { + return err + } + if err = json.Unmarshal(body, data); err != nil { + err = fmt.Errorf("unable to unmarshal %q (Body: %q) from %q with error: %v", infoName, string(body), url, err) + return err + } + return nil +} diff --git a/client/v2/client_test.go b/client/v2/client_test.go new file mode 100644 index 00000000..12aae1cc --- /dev/null +++ b/client/v2/client_test.go @@ -0,0 +1,178 @@ +// Copyright 2015 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 v2 + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" + + info "github.com/google/cadvisor/info/v1" + infoV2 "github.com/google/cadvisor/info/v2" + "github.com/kr/pretty" +) + +func testGetJsonData( + expected interface{}, + f func() (interface{}, error), +) error { + reply, err := f() + if err != nil { + return fmt.Errorf("unable to retrieve data: %v", err) + } + if !reflect.DeepEqual(reply, expected) { + return pretty.Errorf("retrieved wrong data: %# v != %# v", reply, expected) + } + return nil +} + +func cadvisorTestClient(path string, expectedPostObj *info.ContainerInfoRequest, replyObj interface{}, t *testing.T) (*Client, *httptest.Server, error) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == path { + if expectedPostObj != nil { + expectedPostObjEmpty := new(info.ContainerInfoRequest) + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(expectedPostObjEmpty); err != nil { + t.Errorf("Received invalid object: %v", err) + } + if expectedPostObj.NumStats != expectedPostObjEmpty.NumStats || + expectedPostObj.Start.Unix() != expectedPostObjEmpty.Start.Unix() || + expectedPostObj.End.Unix() != expectedPostObjEmpty.End.Unix() { + t.Errorf("Received unexpected object: %+v, expected: %+v", expectedPostObjEmpty, expectedPostObj) + } + } + encoder := json.NewEncoder(w) + encoder.Encode(replyObj) + } else if r.URL.Path == "/api/v2.0/version" { + fmt.Fprintf(w, "0.1.2") + } else { + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "Page not found.") + } + })) + client, err := NewClient(ts.URL) + if err != nil { + ts.Close() + return nil, nil, err + } + return client, ts, err +} + +// TestGetMachineInfo performs one test to check if MachineInfo() +// in a cAdvisor client returns the correct result. +func TestGetMachineinfo(t *testing.T) { + minfo := &info.MachineInfo{ + NumCores: 8, + MemoryCapacity: 31625871360, + DiskMap: map[string]info.DiskInfo{ + "8:0": { + Name: "sda", + Major: 8, + Minor: 0, + Size: 10737418240, + }, + }, + } + client, server, err := cadvisorTestClient("/api/v2.0/machine", nil, minfo, t) + if err != nil { + t.Fatalf("unable to get a client %v", err) + } + defer server.Close() + returned, err := client.MachineInfo() + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(returned, minfo) { + t.Fatalf("received unexpected machine info") + } +} + +// TestGetVersionInfo performs one test to check if VersionInfo() +// in a cAdvisor client returns the correct result. +func TestGetVersioninfo(t *testing.T) { + version := "0.1.2" + client, server, err := cadvisorTestClient("", nil, version, t) + if err != nil { + t.Fatalf("unable to get a client %v", err) + } + defer server.Close() + returned, err := client.VersionInfo() + if err != nil { + t.Fatal(err) + } + if returned != version { + t.Fatalf("received unexpected version info") + } +} + +// TestAttributes performs one test to check if Attributes() +// in a cAdvisor client returns the correct result. +func TestGetAttributes(t *testing.T) { + attr := &infoV2.Attributes{ + KernelVersion: "3.3.0", + ContainerOsVersion: "Ubuntu 14.4", + DockerVersion: "Docker 1.5", + CadvisorVersion: "0.1.2", + NumCores: 8, + MemoryCapacity: 31625871360, + DiskMap: map[string]info.DiskInfo{ + "8:0": { + Name: "sda", + Major: 8, + Minor: 0, + Size: 10737418240, + }, + }, + } + client, server, err := cadvisorTestClient("/api/v2.0/attributes", nil, attr, t) + if err != nil { + t.Fatalf("unable to get a client %v", err) + } + defer server.Close() + returned, err := client.Attributes() + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(returned, attr) { + t.Fatalf("received unexpected attributes") + } +} + +func TestRequestFails(t *testing.T) { + errorText := "there was an error" + // Setup a server that simply fails. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, errorText, 500) + })) + client, err := NewClient(ts.URL) + if err != nil { + ts.Close() + t.Fatal(err) + } + defer ts.Close() + + _, err = client.MachineInfo() + if err == nil { + t.Fatalf("Expected non-nil error") + } + expectedError := fmt.Sprintf("request failed with error: %q", errorText) + if strings.Contains(err.Error(), expectedError) { + t.Fatalf("Expected error %q but received %q", expectedError, err) + } +}