diff --git a/openstack/compute/v2/images/fixtures.go b/openstack/compute/v2/images/fixtures.go new file mode 100644 index 00000000..e8f13c45 --- /dev/null +++ b/openstack/compute/v2/images/fixtures.go @@ -0,0 +1,107 @@ +// +build fixtures + +package images + +import ( + "net/http" + "testing" + + th "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +var ( + testMetadataMap = map[string]string{"foo": "bar"} + testMetadataOpts = MetadatumOpts{"foo": "bar"} + testMetadataString = `{"metadata": {"foo": "bar"}}` + + testMetadataChangeOpts = MetadataOpts{"foo": "baz"} + testMetadataChangeString = `{"metadata": {"foo": "baz"}}` + testMetadataResetString = testMetadataChangeString + testMetadataResetMap = map[string]string{"foo": "baz"} + testMetadataUpdateString = `{"metadata": {"foo": "baz"}}` + testMetadataUpdateMap = map[string]string{"foo": "baz"} + + testMetadatumMap = map[string]string{"foo": "bar"} + testMetadatumOpts = MetadatumOpts{"foo": "bar"} + testMetadatumString = `{"meta": {"foo": "bar"}}` + + testMetadatumChangeOpts = MetadatumOpts{"foo": "bar"} + testMetadatumChangeString = `{"meta": {"foo": "bar"}}` + testMetadatumCreateMap = map[string]string{"foo": "bar"} + testMetadatumCreateString = testMetadatumChangeString +) + +// HandleMetadataGetSuccessfully sets up the test server to respond to a metadata Get request. +func HandleMetadataGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.WriteHeader(http.StatusOK) + w.Write([]byte(testMetadataString)) + }) +} + +// HandleMetadataResetSuccessfully sets up the test server to respond to a metadata Create request. +func HandleMetadataResetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, testMetadataResetString) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(testMetadataResetString)) + }) +} + +// HandleMetadataUpdateSuccessfully sets up the test server to respond to a metadata Update request. +func HandleMetadataUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, testMetadataResetString) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(testMetadataUpdateString)) + }) +} + +// HandleMetadatumGetSuccessfully sets up the test server to respond to a metadatum Get request. +func HandleMetadatumGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(testMetadatumString)) + }) +} + +// HandleMetadatumCreateSuccessfully sets up the test server to respond to a metadatum Create request. +func HandleMetadatumCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, testMetadatumChangeString) + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + w.Write([]byte(testMetadatumChangeString)) + }) +} + +// HandleMetadatumDeleteSuccessfully sets up the test server to respond to a metadatum Delete request. +func HandleMetadatumDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/images/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/openstack/compute/v2/images/requests.go b/openstack/compute/v2/images/requests.go index 1e021ad4..6ba180f7 100644 --- a/openstack/compute/v2/images/requests.go +++ b/openstack/compute/v2/images/requests.go @@ -1,6 +1,7 @@ package images import ( + "errors" "fmt" "github.com/rackspace/gophercloud" @@ -59,7 +60,7 @@ func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) paginat } // Get acquires additional detail about a specific image by ID. -// Use ExtractImage() to interpret the result as an openstack Image. +// Use GetResult.Extract() to interpret the result as an openstack Image. func Get(client *gophercloud.ServiceClient, id string) GetResult { var result GetResult _, result.Err = client.Get(getURL(client, id), &result.Body, nil) @@ -107,3 +108,121 @@ func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) return "", fmt.Errorf("Found %d images matching %s", imageCount, name) } } + +// Metadata requests all the metadata for the given image ID. +func Metadata(client *gophercloud.ServiceClient, id string) GetMetadataResult { + var res GetMetadataResult + _, res.Err = client.Get(metadataURL(client, id), &res.Body, nil) + return res +} + +// MetadataOpts is a map that contains key-value pairs. +type MetadataOpts map[string]string + +// ToMetadataResetMap assembles a body for a Reset request based on the contents of a MetadataOpts. +func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ToMetadataUpdateMap assembles a body for an Update request based on the contents of a MetadataOpts. +func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ResetMetadataOptsBuilder allows extensions to add additional parameters to the +// Reset request. +type ResetMetadataOptsBuilder interface { + ToMetadataResetMap() (map[string]interface{}, error) +} + +// ResetMetadata will create multiple new key-value pairs for the given image ID. +// Note: Using this operation will erase any already-existing metadata and create +// the new metadata provided. To keep any already-existing metadata, use the +// UpdateMetadatas or UpdateMetadata function. +func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) ResetMetadataResult { + var res ResetMetadataResult + metadata, err := opts.ToMetadataResetMap() + if err != nil { + res.Err = err + return res + } + _, res.Err = client.Put(metadataURL(client, id), metadata, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to the +// Create request. +type UpdateMetadataOptsBuilder interface { + ToMetadataUpdateMap() (map[string]interface{}, error) +} + +// UpdateMetadata updates (or creates) all the metadata specified by opts for the given image ID. +// This operation does not affect already-existing metadata that is not specified +// by opts. +func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult { + var res UpdateMetadataResult + metadata, err := opts.ToMetadataUpdateMap() + if err != nil { + res.Err = err + return res + } + _, res.Err = client.Post(metadataURL(client, id), metadata, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// Metadatum requests the key-value pair with the given key for the given image ID. +func Metadatum(client *gophercloud.ServiceClient, id, key string) GetMetadatumResult { + var res GetMetadatumResult + _, res.Err = client.Request("GET", metadatumURL(client, id, key), gophercloud.RequestOpts{ + JSONResponse: &res.Body, + }) + return res +} + +// MetadatumOpts is a map of length one that contains a key-value pair. +type MetadatumOpts map[string]interface{} + +// ToMetadatumCreateMap assembles a body for a Create request based on the contents of a MetadataumOpts. +func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) { + if len(opts) != 1 { + return nil, "", errors.New("CreateMetadatum operation must have 1 and only 1 key-value pair.") + } + metadatum := map[string]interface{}{"meta": opts} + var key string + for k := range metadatum["meta"].(MetadatumOpts) { + key = k + } + return metadatum, key, nil +} + +// MetadatumOptsBuilder allows extensions to add additional parameters to the +// Create request. +type MetadatumOptsBuilder interface { + ToMetadatumCreateMap() (map[string]interface{}, string, error) +} + +// CreateMetadatum will create or update the key-value pair with the given key for the given image ID. +func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) CreateMetadatumResult { + var res CreateMetadatumResult + metadatum, key, err := opts.ToMetadatumCreateMap() + if err != nil { + res.Err = err + return res + } + + _, res.Err = client.Put(metadatumURL(client, id, key), metadatum, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res +} + +// DeleteMetadatum will delete the key-value pair with the given key for the given image ID. +func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) DeleteMetadatumResult { + var res DeleteMetadatumResult + _, res.Err = client.Delete(metadatumURL(client, id, key), nil) + return res +} diff --git a/openstack/compute/v2/images/requests_test.go b/openstack/compute/v2/images/requests_test.go index 21e82969..dbf20ec5 100644 --- a/openstack/compute/v2/images/requests_test.go +++ b/openstack/compute/v2/images/requests_test.go @@ -186,3 +186,73 @@ func TestDeleteImage(t *testing.T) { res := Delete(fake.ServiceClient(), "12345678") th.AssertNoErr(t, res.Err) } + +func TestGetMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadataGetSuccessfully(t) + + expected := testMetadataMap + actual, err := Metadata(fake.ServiceClient(), "1234asdf").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestResetMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadataResetSuccessfully(t) + + expected := testMetadataResetMap + actual, err := ResetMetadata(fake.ServiceClient(), "1234asdf", testMetadataChangeOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestUpdateMetadata(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadataUpdateSuccessfully(t) + + expected := testMetadataUpdateMap + actual, err := UpdateMetadata(fake.ServiceClient(), "1234asdf", testMetadataChangeOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestGetMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadatumGetSuccessfully(t) + + expected := testMetadatumMap + actual, err := Metadatum(fake.ServiceClient(), "1234asdf", "foo").Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestCreateMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadatumCreateSuccessfully(t) + + expected := testMetadatumCreateMap + actual, err := CreateMetadatum(fake.ServiceClient(), "1234asdf", testMetadatumChangeOpts).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, expected, actual) +} + +func TestDeleteMetadatum(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleMetadatumDeleteSuccessfully(t) + + err := DeleteMetadatum(fake.ServiceClient(), "1234asdf", "foo").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/openstack/compute/v2/images/results.go b/openstack/compute/v2/images/results.go index 482e7d6f..44b86826 100644 --- a/openstack/compute/v2/images/results.go +++ b/openstack/compute/v2/images/results.go @@ -51,7 +51,7 @@ type Image struct { Status string Updated string - + Metadata map[string]string } @@ -95,3 +95,71 @@ func ExtractImages(page pagination.Page) ([]Image, error) { err := mapstructure.Decode(casted, &results) return results.Images, err } + +// MetadataResult contains the result of a call for (potentially) multiple key-value pairs. +type MetadataResult struct { + gophercloud.Result +} + +// GetMetadataResult temporarily contains the response from a metadata Get call. +type GetMetadataResult struct { + MetadataResult +} + +// ResetMetadataResult temporarily contains the response from a metadata Reset call. +type ResetMetadataResult struct { + MetadataResult +} + +// UpdateMetadataResult temporarily contains the response from a metadata Update call. +type UpdateMetadataResult struct { + MetadataResult +} + +// MetadatumResult contains the result of a call for individual a single key-value pair. +type MetadatumResult struct { + gophercloud.Result +} + +// GetMetadatumResult temporarily contains the response from a metadatum Get call. +type GetMetadatumResult struct { + MetadatumResult +} + +// CreateMetadatumResult temporarily contains the response from a metadatum Create call. +type CreateMetadatumResult struct { + MetadatumResult +} + +// DeleteMetadatumResult temporarily contains the response from a metadatum Delete call. +type DeleteMetadatumResult struct { + gophercloud.ErrResult +} + +// Extract interprets any MetadataResult as a Metadata, if possible. +func (r MetadataResult) Extract() (map[string]string, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Metadata map[string]string `mapstructure:"metadata"` + } + + err := mapstructure.Decode(r.Body, &response) + return response.Metadata, err +} + +// Extract interprets any MetadatumResult as a Metadatum, if possible. +func (r MetadatumResult) Extract() (map[string]string, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Metadatum map[string]string `mapstructure:"meta"` + } + + err := mapstructure.Decode(r.Body, &response) + return response.Metadatum, err +} diff --git a/openstack/compute/v2/images/urls.go b/openstack/compute/v2/images/urls.go index b1bf1038..ea92cadc 100644 --- a/openstack/compute/v2/images/urls.go +++ b/openstack/compute/v2/images/urls.go @@ -13,3 +13,11 @@ func getURL(client *gophercloud.ServiceClient, id string) string { func deleteURL(client *gophercloud.ServiceClient, id string) string { return client.ServiceURL("images", id) } + +func metadataURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("images", id, "metadata") +} + +func metadatumURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("images", id, "metadata", key) +} diff --git a/openstack/compute/v2/images/urls_test.go b/openstack/compute/v2/images/urls_test.go index b1ab3d67..4b95f67c 100644 --- a/openstack/compute/v2/images/urls_test.go +++ b/openstack/compute/v2/images/urls_test.go @@ -24,3 +24,15 @@ func TestListDetailURL(t *testing.T) { expected := endpoint + "images/detail" th.CheckEquals(t, expected, actual) } + +func TestMetadataURL(t *testing.T) { + actual := metadataURL(endpointClient(), "foo") + expected := endpoint + "images/foo/metadata" + th.CheckEquals(t, expected, actual) +} + +func TestMetadatumURL(t *testing.T) { + actual := metadatumURL(endpointClient(), "foo", "bar") + expected := endpoint + "images/foo/metadata/bar" + th.CheckEquals(t, expected, actual) +}