Making server action result types more consistent
diff --git a/_site/openstack/compute/v2/images/requests.go b/_site/openstack/compute/v2/images/requests.go
new file mode 100644
index 0000000..603909c
--- /dev/null
+++ b/_site/openstack/compute/v2/images/requests.go
@@ -0,0 +1,71 @@
+package images
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToImageListQuery() (string, error)
+}
+
+// ListOpts contain options for limiting the number of Images returned from a call to ListDetail.
+type ListOpts struct {
+ // When the image last changed status (in date-time format).
+ ChangesSince string `q:"changes-since"`
+ // The number of Images to return.
+ Limit int `q:"limit"`
+ // UUID of the Image at which to set a marker.
+ Marker string `q:"marker"`
+ // The name of the Image.
+ Name string `q:"name:"`
+ // The name of the Server (in URL format).
+ Server string `q:"server"`
+ // The current status of the Image.
+ Status string `q:"status"`
+ // The value of the type of image (e.g. BASE, SERVER, ALL)
+ Type string `q:"type"`
+}
+
+// ToImageListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToImageListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// ListDetail enumerates the available images.
+func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listDetailURL(client)
+ if opts != nil {
+ query, err := opts.ToImageListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ImagePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ }
+
+ return pagination.NewPager(client, url, createPage)
+}
+
+// Get acquires additional detail about a specific image by ID.
+// Use ExtractImage() to intepret the result as an openstack Image.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var result GetResult
+ _, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ Results: &result.Resp,
+ OkCodes: []int{200},
+ })
+ return result
+}
diff --git a/_site/openstack/compute/v2/images/requests_test.go b/_site/openstack/compute/v2/images/requests_test.go
new file mode 100644
index 0000000..9a05f97
--- /dev/null
+++ b/_site/openstack/compute/v2/images/requests_test.go
@@ -0,0 +1,175 @@
+package images
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "reflect"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListImages(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, `
+ {
+ "images": [
+ {
+ "status": "ACTIVE",
+ "updated": "2014-09-23T12:54:56Z",
+ "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+ "OS-EXT-IMG-SIZE:size": 476704768,
+ "name": "F17-x86_64-cfntools",
+ "created": "2014-09-23T12:54:52Z",
+ "minDisk": 0,
+ "progress": 100,
+ "minRam": 0,
+ "metadata": {}
+ },
+ {
+ "status": "ACTIVE",
+ "updated": "2014-09-23T12:51:43Z",
+ "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "OS-EXT-IMG-SIZE:size": 13167616,
+ "name": "cirros-0.3.2-x86_64-disk",
+ "created": "2014-09-23T12:51:42Z",
+ "minDisk": 0,
+ "progress": 100,
+ "minRam": 0,
+ "metadata": {}
+ }
+ ]
+ }
+ `)
+ case "2":
+ fmt.Fprintf(w, `{ "images": [] }`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ pages := 0
+ options := &ListOpts{Limit: 2}
+ err := ListDetail(fake.ServiceClient(), options).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractImages(page)
+ if err != nil {
+ return false, err
+ }
+
+ expected := []Image{
+ Image{
+ ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+ Name: "F17-x86_64-cfntools",
+ Created: "2014-09-23T12:54:52Z",
+ Updated: "2014-09-23T12:54:56Z",
+ MinDisk: 0,
+ MinRAM: 0,
+ Progress: 100,
+ Status: "ACTIVE",
+ },
+ Image{
+ ID: "f90f6034-2570-4974-8351-6b49732ef2eb",
+ Name: "cirros-0.3.2-x86_64-disk",
+ Created: "2014-09-23T12:51:42Z",
+ Updated: "2014-09-23T12:51:43Z",
+ MinDisk: 0,
+ MinRAM: 0,
+ Progress: 100,
+ Status: "ACTIVE",
+ },
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Unexpected page contents: expected %#v, got %#v", expected, actual)
+ }
+
+ return false, nil
+ })
+
+ if err != nil {
+ t.Fatalf("EachPage error: %v", err)
+ }
+ if pages != 1 {
+ t.Errorf("Expected one page, got %d", pages)
+ }
+}
+
+func TestGetImage(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "image": {
+ "status": "ACTIVE",
+ "updated": "2014-09-23T12:54:56Z",
+ "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+ "OS-EXT-IMG-SIZE:size": 476704768,
+ "name": "F17-x86_64-cfntools",
+ "created": "2014-09-23T12:54:52Z",
+ "minDisk": 0,
+ "progress": 100,
+ "minRam": 0,
+ "metadata": {}
+ }
+ }
+ `)
+ })
+
+ actual, err := Get(fake.ServiceClient(), "12345678").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected error from Get: %v", err)
+ }
+
+ expected := &Image{
+ Status: "ACTIVE",
+ Updated: "2014-09-23T12:54:56Z",
+ ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+ Name: "F17-x86_64-cfntools",
+ Created: "2014-09-23T12:54:52Z",
+ MinDisk: 0,
+ Progress: 100,
+ MinRAM: 0,
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, but got %#v", expected, actual)
+ }
+}
+
+func TestNextPageURL(t *testing.T) {
+ var page ImagePage
+ var body map[string]interface{}
+ bodyString := []byte(`{"images":{"links":[{"href":"http://192.154.23.87/12345/images/image3","rel":"bookmark"}]}, "images_links":[{"href":"http://192.154.23.87/12345/images/image4","rel":"next"}]}`)
+ err := json.Unmarshal(bodyString, &body)
+ if err != nil {
+ t.Fatalf("Error unmarshaling data into page body: %v", err)
+ }
+ page.Body = body
+
+ expected := "http://192.154.23.87/12345/images/image4"
+ actual, err := page.NextPageURL()
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, expected, actual)
+}
diff --git a/_site/openstack/compute/v2/images/results.go b/_site/openstack/compute/v2/images/results.go
new file mode 100644
index 0000000..3c22eeb
--- /dev/null
+++ b/_site/openstack/compute/v2/images/results.go
@@ -0,0 +1,90 @@
+package images
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// GetResult temporarily stores a Get response.
+type GetResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult as an Image.
+func (gr GetResult) Extract() (*Image, error) {
+ if gr.Err != nil {
+ return nil, gr.Err
+ }
+
+ var decoded struct {
+ Image Image `mapstructure:"image"`
+ }
+
+ err := mapstructure.Decode(gr.Resp, &decoded)
+ return &decoded.Image, err
+}
+
+// Image is used for JSON (un)marshalling.
+// It provides a description of an OS image.
+type Image struct {
+ // ID contains the image's unique identifier.
+ ID string
+
+ Created string
+
+ // MinDisk and MinRAM specify the minimum resources a server must provide to be able to install the image.
+ MinDisk int
+ MinRAM int
+
+ // Name provides a human-readable moniker for the OS image.
+ Name string
+
+ // The Progress and Status fields indicate image-creation status.
+ // Any usable image will have 100% progress.
+ Progress int
+ Status string
+
+ Updated string
+}
+
+// ImagePage contains a single page of results from a List operation.
+// Use ExtractImages to convert it into a slice of usable structs.
+type ImagePage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no Image results.
+func (page ImagePage) IsEmpty() (bool, error) {
+ images, err := ExtractImages(page)
+ if err != nil {
+ return true, err
+ }
+ return len(images) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page ImagePage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"images_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractImages converts a page of List results into a slice of usable Image structs.
+func ExtractImages(page pagination.Page) ([]Image, error) {
+ casted := page.(ImagePage).Body
+ var results struct {
+ Images []Image `mapstructure:"images"`
+ }
+
+ err := mapstructure.Decode(casted, &results)
+ return results.Images, err
+}
diff --git a/_site/openstack/compute/v2/images/urls.go b/_site/openstack/compute/v2/images/urls.go
new file mode 100644
index 0000000..9b3c86d
--- /dev/null
+++ b/_site/openstack/compute/v2/images/urls.go
@@ -0,0 +1,11 @@
+package images
+
+import "github.com/rackspace/gophercloud"
+
+func listDetailURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("images", "detail")
+}
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("images", id)
+}
diff --git a/_site/openstack/compute/v2/images/urls_test.go b/_site/openstack/compute/v2/images/urls_test.go
new file mode 100644
index 0000000..b1ab3d6
--- /dev/null
+++ b/_site/openstack/compute/v2/images/urls_test.go
@@ -0,0 +1,26 @@
+package images
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "images/foo"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestListDetailURL(t *testing.T) {
+ actual := listDetailURL(endpointClient())
+ expected := endpoint + "images/detail"
+ th.CheckEquals(t, expected, actual)
+}