diff --git a/acceptance/openstack/restclient/restclient_test.go b/acceptance/openstack/restclient/restclient_test.go new file mode 100644 index 00000000..c8602c87 --- /dev/null +++ b/acceptance/openstack/restclient/restclient_test.go @@ -0,0 +1,91 @@ +// +build acceptance restclient + +package restclient + +import ( + "testing" + + acc_clients "github.com/gophercloud/gophercloud/acceptance/clients" + acc_tools "github.com/gophercloud/gophercloud/acceptance/tools" + + th "github.com/gophercloud/gophercloud/testhelper" + cc "github.com/gophercloud/utils/openstack/clientconfig" + "github.com/gophercloud/utils/openstack/restclient" +) + +func TestRESTClient(t *testing.T) { + acc_clients.RequireAdmin(t) + + // This will be populated by environment variables. + clientOpts := &cc.ClientOpts{} + + computeClient, err := cc.NewServiceClient("compute", clientOpts) + th.AssertNoErr(t, err) + + // Test creating a flavor + flavorName := acc_tools.RandomString("TESTACC-", 8) + flavorID := acc_tools.RandomString("TESTACC-", 8) + flavorOpts := map[string]interface{}{ + "name": flavorName, + "ram": 512, + "vcpus": 1, + "disk": 5, + "id": flavorID, + } + + postOpts := &restclient.PostOpts{ + Params: map[string]interface{}{"flavor": flavorOpts}, + } + + postURL := computeClient.ServiceURL("flavors") + postRes := restclient.Post(computeClient, postURL, postOpts) + th.AssertNoErr(t, postRes.Err) + flavorResult, err := postRes.Extract() + th.AssertNoErr(t, postRes.Err) + acc_tools.PrintResource(t, flavorResult) + + // Test deleting a flavor + defer func() { + deleteURL := computeClient.ServiceURL("flavors", flavorID) + deleteRes := restclient.Delete(computeClient, deleteURL, nil) + th.AssertNoErr(t, deleteRes.Err) + err = deleteRes.ExtractErr() + th.AssertNoErr(t, err) + }() + + // Test retrieving a flavor + getURL := computeClient.ServiceURL("flavors", flavorID) + getRes := restclient.Get(computeClient, getURL, nil) + th.AssertNoErr(t, getRes.Err) + + flavorResult, err = getRes.Extract() + th.AssertNoErr(t, err) + + flavor := flavorResult["flavor"].(map[string]interface{}) + + acc_tools.PrintResource(t, flavor) + + th.AssertEquals(t, flavor["disk"], float64(5)) + th.AssertEquals(t, flavor["id"], flavorID) + th.AssertEquals(t, flavor["name"], flavorName) + th.AssertEquals(t, flavor["ram"], float64(512)) + th.AssertEquals(t, flavor["swap"], "") + th.AssertEquals(t, flavor["vcpus"], float64(1)) + + // Test listing flavors + getOpts := &restclient.GetOpts{ + Query: map[string]interface{}{ + "limit": 2, + }, + } + + getURL = computeClient.ServiceURL("flavors") + getRes = restclient.Get(computeClient, getURL, getOpts) + th.AssertNoErr(t, getRes.Err) + flavorResult, err = getRes.Extract() + th.AssertNoErr(t, err) + + flavors := flavorResult["flavors"].([]interface{}) + acc_tools.PrintResource(t, flavors) + th.AssertEquals(t, len(flavors), 2) +} diff --git a/openstack/restclient/doc.go b/openstack/restclient/doc.go new file mode 100644 index 00000000..debcd339 --- /dev/null +++ b/openstack/restclient/doc.go @@ -0,0 +1,59 @@ +/* Package restclient provides generic REST functions. + +Example of a GET request + + getURL := computeClient.ServiceURL("flavors", flavorID) + getRes := restclient.Get(computeClient, getURL, nil) + if err != nil { + panic(err) + } + + flavorResult, err = getRes.Extract() + if err != nil { + panic(err) + } + + flavor := flavorResult["flavor"].(map[string]interface{}) + fmt.Printf("%v\n", flavor) + +Example of a POST request + + flavorOpts := map[string]interface{}{ + "name": "some-name", + "ram": 512, + "vcpus": 1, + "disk": 5, + "id": "some-id", + } + + postOpts := &restclient.PostOpts{ + Params: map[string]interface{}{"flavor": flavorOpts}, + } + + postURL := computeClient.ServiceURL("flavors") + postRes := restclient.Post(computeClient, postURL, postOpts) + if err != nil { + panic(err) + } + + flavorResult, err := postRes.Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%v\n", flavor) + +Example of a DELETE Request + + deleteURL := computeClient.ServiceURL("flavors", "flavor-id") + deleteRes := restclient.Delete(computeClient, deleteURL, nil) + if err != nil { + panic(err) + } + + err = deleteRes.ExtractErr() + if err != nil { + panic(err) + } +*/ +package restclient diff --git a/openstack/restclient/requests.go b/openstack/restclient/requests.go new file mode 100644 index 00000000..829ffc2c --- /dev/null +++ b/openstack/restclient/requests.go @@ -0,0 +1,253 @@ +package restclient + +import ( + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + + "github.com/gophercloud/gophercloud" +) + +// GetOpts represents options used in the Get request. +type GetOpts struct { + Headers map[string]string + Query map[string]interface{} +} + +// Get performs a generic GET request to the specified URL. +func Get(client *gophercloud.ServiceClient, url string, opts *GetOpts) (r GetResult) { + requestOpts := new(gophercloud.RequestOpts) + + if opts != nil { + query, err := BuildQueryString(opts.Query) + if err != nil { + r.Err = err + return + } + url += query.String() + + requestOpts.MoreHeaders = opts.Headers + } + + // Allow a wide range of statuses + requestOpts.OkCodes = []int{200, 201, 202, 204, 206} + + _, err := client.Get(url, &r.Body, requestOpts) + if err != nil { + if err.Error() != "EOF" { + r.Err = err + return + } + + err = nil + r.Body = nil + } + + return +} + +// PostOpts represents options used in a Post request. +type PostOpts struct { + Headers map[string]string + Params map[string]interface{} + Query map[string]interface{} +} + +// Post performs a generic POST request to the specified URL. +func Post(client *gophercloud.ServiceClient, url string, opts *PostOpts) (r PostResult) { + var b map[string]interface{} + requestOpts := new(gophercloud.RequestOpts) + + if opts != nil { + query, err := BuildQueryString(opts.Query) + if err != nil { + r.Err = err + return + } + url += query.String() + + b = opts.Params + + requestOpts.MoreHeaders = opts.Headers + } + + // Allow a wide range of statuses + requestOpts.OkCodes = []int{200, 201, 202, 204, 206} + + _, err := client.Post(url, &b, &r.Body, requestOpts) + if err != nil { + if err.Error() != "EOF" { + r.Err = err + return + } + + err = nil + r.Body = nil + } + + return +} + +// PutOpts represents options used in a Put request. +type PutOpts struct { + Headers map[string]string + Params map[string]interface{} + Query map[string]interface{} +} + +// Put performs a generic PUT request to the specified URL. +func Put(client *gophercloud.ServiceClient, url string, opts *PutOpts) (r PostResult) { + var b map[string]interface{} + requestOpts := new(gophercloud.RequestOpts) + + if opts != nil { + query, err := BuildQueryString(opts.Query) + if err != nil { + r.Err = err + return + } + url += query.String() + + b = opts.Params + } + + // Allow a wide range of statuses + requestOpts.OkCodes = []int{200, 201, 202, 204, 206} + + _, err := client.Put(url, &b, &r.Body, requestOpts) + if err != nil { + if err.Error() != "EOF" { + r.Err = err + return + } + + err = nil + r.Body = nil + } + + return +} + +// PatchOpts represents options used in a Patch request. +type PatchOpts struct { + Headers map[string]string + Params map[string]interface{} + Query map[string]interface{} +} + +// Patch performs a generic PATCH request to the specified URL. +func Patch(client *gophercloud.ServiceClient, url string, opts *PatchOpts) (r PatchResult) { + var b map[string]interface{} + requestOpts := new(gophercloud.RequestOpts) + + if opts != nil { + query, err := BuildQueryString(opts.Query) + if err != nil { + r.Err = err + return + } + url += query.String() + + b = opts.Params + } + + // Allow a wide range of statuses + requestOpts.OkCodes = []int{200, 201, 202, 204, 206} + + _, err := client.Patch(url, &b, &r.Body, requestOpts) + if err != nil { + if err.Error() != "EOF" { + r.Err = err + return + } + + err = nil + r.Body = nil + } + + return +} + +// DeleteOpts represents options used in a Delete request. +type DeleteOpts struct { + Headers map[string]string + Query map[string]interface{} +} + +// Delete performs a generic DELETE request to the specified URL. +func Delete(client *gophercloud.ServiceClient, url string, opts *DeleteOpts) (r DeleteResult) { + requestOpts := new(gophercloud.RequestOpts) + + if opts != nil { + query, err := BuildQueryString(opts.Query) + if err != nil { + r.Err = err + return + } + url += query.String() + + requestOpts.MoreHeaders = opts.Headers + } + + // Allow a wide range of statuses + requestOpts.OkCodes = []int{200, 201, 202, 204, 206} + + _, err := client.Delete(url, requestOpts) + if err != nil { + if err.Error() != "EOF" { + r.Err = err + return + } + + err = nil + r.Body = nil + } + + return +} + +// BuildQueryString will take a map[string]interface and convert it +// to a URL encoded string. This is a watered-down version of Gophercloud's +// BuildQueryString. +func BuildQueryString(q map[string]interface{}) (*url.URL, error) { + params := url.Values{} + + for key, value := range q { + v := reflect.ValueOf(value) + + switch v.Kind() { + case reflect.String: + params.Add(key, v.String()) + case reflect.Int: + params.Add(key, strconv.FormatInt(v.Int(), 10)) + case reflect.Bool: + params.Add(key, strconv.FormatBool(v.Bool())) + case reflect.Slice: + switch v.Type().Elem() { + case reflect.TypeOf(0): + for i := 0; i < v.Len(); i++ { + params.Add(key, strconv.FormatInt(v.Index(i).Int(), 10)) + } + default: + for i := 0; i < v.Len(); i++ { + params.Add(key, v.Index(i).String()) + } + } + case reflect.Map: + if v.Type().Key().Kind() == reflect.String && v.Type().Elem().Kind() == reflect.String { + var s []string + for _, k := range v.MapKeys() { + value := v.MapIndex(k).String() + s = append(s, fmt.Sprintf("'%s':'%s'", k.String(), value)) + } + params.Add(key, fmt.Sprintf("{%s}", strings.Join(s, ", "))) + } + default: + return nil, fmt.Errorf("Unsupported type: %s", v.Kind()) + } + } + + return &url.URL{RawQuery: params.Encode()}, nil +} diff --git a/openstack/restclient/results.go b/openstack/restclient/results.go new file mode 100644 index 00000000..88ec9a8c --- /dev/null +++ b/openstack/restclient/results.go @@ -0,0 +1,35 @@ +package restclient + +import ( + "github.com/gophercloud/gophercloud" +) + +type commonResult struct { + gophercloud.Result +} + +func (r commonResult) Extract() (map[string]interface{}, error) { + var s map[string]interface{} + err := r.ExtractInto(&s) + return s, err +} + +type GetResult struct { + commonResult +} + +type PostResult struct { + commonResult +} + +type PatchResult struct { + commonResult +} + +type PutResult struct { + commonResult +} + +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/openstack/restclient/testing/requests_test.go b/openstack/restclient/testing/requests_test.go new file mode 100644 index 00000000..e4d52f88 --- /dev/null +++ b/openstack/restclient/testing/requests_test.go @@ -0,0 +1,168 @@ +package testing + +import ( + "fmt" + "net/http" + "net/url" + "testing" + + "github.com/gophercloud/gophercloud" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/utils/openstack/restclient" +) + +func TestBuildQueryString(t *testing.T) { + testCases := map[string]interface{}{ + "a": 2, + "b": "foo", + "c": true, + "d": []string{"one", "two", "three"}, + "e": []int{1, 2, 3}, + "f": map[string]string{"foo": "bar"}, + "g": false, + } + + expected := &url.URL{RawQuery: "a=2&b=foo&c=true&d=one&d=two&d=three&e=1&e=2&e=3&f=%7B%27foo%27%3A%27bar%27%7D&g=false"} + + actual, err := restclient.BuildQueryString(testCases) + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, expected, actual) +} + +func TestBasic(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{"foo": "bar"}`) + }) + + url := fmt.Sprintf("%s/route", th.Endpoint()) + + c := new(gophercloud.ServiceClient) + c.ProviderClient = new(gophercloud.ProviderClient) + + expected := map[string]interface{}{ + "foo": "bar", + } + + // shared params + params := map[string]interface{}{ + "bar": "baz", + } + + // Get + getRes := restclient.Get(c, url, nil) + th.AssertNoErr(t, getRes.Err) + actual, err := getRes.Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) + + // Post + postOpts := &restclient.PostOpts{ + Params: params, + } + + postRes := restclient.Post(c, url, postOpts) + th.AssertNoErr(t, postRes.Err) + actual, err = postRes.Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) + + // Patch + patchOpts := &restclient.PatchOpts{ + Params: params, + } + + patchRes := restclient.Patch(c, url, patchOpts) + th.AssertNoErr(t, patchRes.Err) + actual, err = patchRes.Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) + + // Put + putOpts := &restclient.PutOpts{ + Params: params, + } + + putRes := restclient.Put(c, url, putOpts) + th.AssertNoErr(t, putRes.Err) + actual, err = putRes.Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) + + // Delete + deleteRes := restclient.Delete(c, url, nil) + th.AssertNoErr(t, deleteRes.Err) + err = deleteRes.ExtractErr() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) +} + +func TestNoContent(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + url := fmt.Sprintf("%s/route", th.Endpoint()) + + c := new(gophercloud.ServiceClient) + c.ProviderClient = new(gophercloud.ProviderClient) + + expected := map[string]interface{}(nil) + + // shared params + params := map[string]interface{}{ + "bar": "baz", + } + + // Get + getRes := restclient.Get(c, url, nil) + th.AssertNoErr(t, getRes.Err) + actual, err := getRes.Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) + + // Post + postOpts := &restclient.PostOpts{ + Params: params, + } + + postRes := restclient.Post(c, url, postOpts) + th.AssertNoErr(t, postRes.Err) + actual, err = postRes.Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) + + // Patch + patchOpts := &restclient.PatchOpts{ + Params: params, + } + + patchRes := restclient.Patch(c, url, patchOpts) + th.AssertNoErr(t, patchRes.Err) + actual, err = patchRes.Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) + + // Put + putOpts := &restclient.PutOpts{ + Params: params, + } + + putRes := restclient.Put(c, url, putOpts) + th.AssertNoErr(t, putRes.Err) + actual, err = putRes.Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) + + // Delete + deleteRes := restclient.Delete(c, url, nil) + th.AssertNoErr(t, deleteRes.Err) + err = deleteRes.ExtractErr() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, actual, expected) +}