Skip to content
This repository was archived by the owner on Aug 1, 2023. It is now read-only.

Commit d30fa8a

Browse files
committed
Support Compute image metadata API.
1 parent 70270c2 commit d30fa8a

File tree

6 files changed

+384
-0
lines changed

6 files changed

+384
-0
lines changed
+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// +build fixtures
2+
3+
package images
4+
5+
import (
6+
"net/http"
7+
"testing"
8+
9+
th "github.com/rackspace/gophercloud/testhelper"
10+
"github.com/rackspace/gophercloud/testhelper/client"
11+
)
12+
13+
var (
14+
testMetadataMap = map[string]interface{}{"foo": "bar", "true": true}
15+
testMetadataOpts = MetadatumOpts{"foo": "bar", "true": true}
16+
testMetadataString = `{"metadata": {"foo": "bar", "true": true}}`
17+
18+
testMetadataChangeOpts = MetadataOpts{"foo": "baz", "new": nil}
19+
testMetadataChangeString = `{"metadata": {"foo": "baz", "new": null}}`
20+
testMetadataResetString = testMetadataChangeString
21+
testMetadataResetMap = map[string]interface{}{"foo": "baz", "new": nil}
22+
testMetadataUpdateString = `{"metadata": {"foo": "baz", "true": true, "new": null}}`
23+
testMetadataUpdateMap = map[string]interface{}{"foo": "baz", "true": true, "new": nil}
24+
25+
testMetadatumMap = map[string]interface{}{"foo": "bar"}
26+
testMetadatumOpts = MetadatumOpts{"foo": "bar"}
27+
testMetadatumString = `{"meta": {"foo": "bar"}}`
28+
29+
testMetadatumChangeOpts = MetadatumOpts{"foo": nil}
30+
testMetadatumChangeString = `{"meta": {"foo": null}}`
31+
testMetadatumCreateMap = map[string]interface{}{"foo": nil}
32+
testMetadatumCreateString = testMetadatumChangeString
33+
)
34+
35+
// HandleMetadataGetSuccessfully sets up the test server to respond to a metadata Get request.
36+
func HandleMetadataGetSuccessfully(t *testing.T) {
37+
th.Mux.HandleFunc("/images/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) {
38+
th.TestMethod(t, r, "GET")
39+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
40+
th.TestHeader(t, r, "Accept", "application/json")
41+
42+
w.WriteHeader(http.StatusOK)
43+
w.Write([]byte(testMetadataString))
44+
})
45+
}
46+
47+
// HandleMetadataResetSuccessfully sets up the test server to respond to a metadata Create request.
48+
func HandleMetadataResetSuccessfully(t *testing.T) {
49+
th.Mux.HandleFunc("/images/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) {
50+
th.TestMethod(t, r, "PUT")
51+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
52+
th.TestJSONRequest(t, r, testMetadataResetString)
53+
54+
w.WriteHeader(http.StatusOK)
55+
w.Header().Add("Content-Type", "application/json")
56+
w.Write([]byte(testMetadataResetString))
57+
})
58+
}
59+
60+
// HandleMetadataUpdateSuccessfully sets up the test server to respond to a metadata Update request.
61+
func HandleMetadataUpdateSuccessfully(t *testing.T) {
62+
th.Mux.HandleFunc("/images/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) {
63+
th.TestMethod(t, r, "POST")
64+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
65+
th.TestJSONRequest(t, r, testMetadataResetString)
66+
67+
w.WriteHeader(http.StatusOK)
68+
w.Header().Add("Content-Type", "application/json")
69+
w.Write([]byte(testMetadataUpdateString))
70+
})
71+
}
72+
73+
// HandleMetadatumGetSuccessfully sets up the test server to respond to a metadatum Get request.
74+
func HandleMetadatumGetSuccessfully(t *testing.T) {
75+
th.Mux.HandleFunc("/images/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) {
76+
th.TestMethod(t, r, "GET")
77+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
78+
th.TestHeader(t, r, "Accept", "application/json")
79+
80+
w.WriteHeader(http.StatusOK)
81+
w.Header().Add("Content-Type", "application/json")
82+
w.Write([]byte(testMetadatumString))
83+
})
84+
}
85+
86+
// HandleMetadatumCreateSuccessfully sets up the test server to respond to a metadatum Create request.
87+
func HandleMetadatumCreateSuccessfully(t *testing.T) {
88+
th.Mux.HandleFunc("/images/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) {
89+
th.TestMethod(t, r, "PUT")
90+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
91+
th.TestJSONRequest(t, r, testMetadatumChangeString)
92+
93+
w.WriteHeader(http.StatusOK)
94+
w.Header().Add("Content-Type", "application/json")
95+
w.Write([]byte(testMetadatumChangeString))
96+
})
97+
}
98+
99+
// HandleMetadatumDeleteSuccessfully sets up the test server to respond to a metadatum Delete request.
100+
func HandleMetadatumDeleteSuccessfully(t *testing.T) {
101+
th.Mux.HandleFunc("/images/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) {
102+
th.TestMethod(t, r, "DELETE")
103+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
104+
105+
w.WriteHeader(http.StatusNoContent)
106+
})
107+
}

openstack/compute/v2/images/requests.go

+119
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package images
22

33
import (
4+
"errors"
45
"fmt"
56

67
"github.com/rackspace/gophercloud"
@@ -107,3 +108,121 @@ func IDFromName(client *gophercloud.ServiceClient, name string) (string, error)
107108
return "", fmt.Errorf("Found %d images matching %s", imageCount, name)
108109
}
109110
}
111+
112+
// Metadata requests all the metadata for the given image ID.
113+
func Metadata(client *gophercloud.ServiceClient, id string) GetMetadataResult {
114+
var res GetMetadataResult
115+
_, res.Err = client.Get(metadataURL(client, id), &res.Body, nil)
116+
return res
117+
}
118+
119+
// MetadataOpts is a map that contains key-value pairs.
120+
type MetadataOpts map[string]interface{}
121+
122+
// ToMetadataResetMap assembles a body for a Reset request based on the contents of a MetadataOpts.
123+
func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) {
124+
return map[string]interface{}{"metadata": opts}, nil
125+
}
126+
127+
// ToMetadataUpdateMap assembles a body for an Update request based on the contents of a MetadataOpts.
128+
func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) {
129+
return map[string]interface{}{"metadata": opts}, nil
130+
}
131+
132+
// ResetMetadataOptsBuilder allows extensions to add additional parameters to the
133+
// Reset request.
134+
type ResetMetadataOptsBuilder interface {
135+
ToMetadataResetMap() (map[string]interface{}, error)
136+
}
137+
138+
// ResetMetadata will create multiple new key-value pairs for the given image ID.
139+
// Note: Using this operation will erase any already-existing metadata and create
140+
// the new metadata provided. To keep any already-existing metadata, use the
141+
// UpdateMetadatas or UpdateMetadata function.
142+
func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) ResetMetadataResult {
143+
var res ResetMetadataResult
144+
metadata, err := opts.ToMetadataResetMap()
145+
if err != nil {
146+
res.Err = err
147+
return res
148+
}
149+
_, res.Err = client.Put(metadataURL(client, id), metadata, &res.Body, &gophercloud.RequestOpts{
150+
OkCodes: []int{200},
151+
})
152+
return res
153+
}
154+
155+
// UpdateMetadataOptsBuilder allows extensions to add additional parameters to the
156+
// Create request.
157+
type UpdateMetadataOptsBuilder interface {
158+
ToMetadataUpdateMap() (map[string]interface{}, error)
159+
}
160+
161+
// UpdateMetadata updates (or creates) all the metadata specified by opts for the given image ID.
162+
// This operation does not affect already-existing metadata that is not specified
163+
// by opts.
164+
func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult {
165+
var res UpdateMetadataResult
166+
metadata, err := opts.ToMetadataUpdateMap()
167+
if err != nil {
168+
res.Err = err
169+
return res
170+
}
171+
_, res.Err = client.Post(metadataURL(client, id), metadata, &res.Body, &gophercloud.RequestOpts{
172+
OkCodes: []int{200},
173+
})
174+
return res
175+
}
176+
177+
// Metadatum requests the key-value pair with the given key for the given image ID.
178+
func Metadatum(client *gophercloud.ServiceClient, id, key string) GetMetadatumResult {
179+
var res GetMetadatumResult
180+
_, res.Err = client.Request("GET", metadatumURL(client, id, key), gophercloud.RequestOpts{
181+
JSONResponse: &res.Body,
182+
})
183+
return res
184+
}
185+
186+
// MetadatumOpts is a map of length one that contains a key-value pair.
187+
type MetadatumOpts map[string]interface{}
188+
189+
// ToMetadatumCreateMap assembles a body for a Create request based on the contents of a MetadataumOpts.
190+
func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) {
191+
if len(opts) != 1 {
192+
return nil, "", errors.New("CreateMetadatum operation must have 1 and only 1 key-value pair.")
193+
}
194+
metadatum := map[string]interface{}{"meta": opts}
195+
var key string
196+
for k := range metadatum["meta"].(MetadatumOpts) {
197+
key = k
198+
}
199+
return metadatum, key, nil
200+
}
201+
202+
// MetadatumOptsBuilder allows extensions to add additional parameters to the
203+
// Create request.
204+
type MetadatumOptsBuilder interface {
205+
ToMetadatumCreateMap() (map[string]interface{}, string, error)
206+
}
207+
208+
// CreateMetadatum will create or update the key-value pair with the given key for the given image ID.
209+
func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) CreateMetadatumResult {
210+
var res CreateMetadatumResult
211+
metadatum, key, err := opts.ToMetadatumCreateMap()
212+
if err != nil {
213+
res.Err = err
214+
return res
215+
}
216+
217+
_, res.Err = client.Put(metadatumURL(client, id, key), metadatum, &res.Body, &gophercloud.RequestOpts{
218+
OkCodes: []int{200},
219+
})
220+
return res
221+
}
222+
223+
// DeleteMetadatum will delete the key-value pair with the given key for the given image ID.
224+
func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) DeleteMetadatumResult {
225+
var res DeleteMetadatumResult
226+
_, res.Err = client.Delete(metadatumURL(client, id, key), nil)
227+
return res
228+
}

openstack/compute/v2/images/requests_test.go

+70
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,73 @@ func TestDeleteImage(t *testing.T) {
209209
res := Delete(fake.ServiceClient(), "12345678")
210210
th.AssertNoErr(t, res.Err)
211211
}
212+
213+
func TestGetMetadata(t *testing.T) {
214+
th.SetupHTTP()
215+
defer th.TeardownHTTP()
216+
217+
HandleMetadataGetSuccessfully(t)
218+
219+
expected := testMetadataMap
220+
actual, err := Metadata(fake.ServiceClient(), "1234asdf").Extract()
221+
th.AssertNoErr(t, err)
222+
th.AssertDeepEquals(t, expected, actual)
223+
}
224+
225+
func TestResetMetadata(t *testing.T) {
226+
th.SetupHTTP()
227+
defer th.TeardownHTTP()
228+
229+
HandleMetadataResetSuccessfully(t)
230+
231+
expected := testMetadataResetMap
232+
actual, err := ResetMetadata(fake.ServiceClient(), "1234asdf", testMetadataChangeOpts).Extract()
233+
th.AssertNoErr(t, err)
234+
th.AssertDeepEquals(t, expected, actual)
235+
}
236+
237+
func TestUpdateMetadata(t *testing.T) {
238+
th.SetupHTTP()
239+
defer th.TeardownHTTP()
240+
241+
HandleMetadataUpdateSuccessfully(t)
242+
243+
expected := testMetadataUpdateMap
244+
actual, err := UpdateMetadata(fake.ServiceClient(), "1234asdf", testMetadataChangeOpts).Extract()
245+
th.AssertNoErr(t, err)
246+
th.AssertDeepEquals(t, expected, actual)
247+
}
248+
249+
func TestGetMetadatum(t *testing.T) {
250+
th.SetupHTTP()
251+
defer th.TeardownHTTP()
252+
253+
HandleMetadatumGetSuccessfully(t)
254+
255+
expected := testMetadatumMap
256+
actual, err := Metadatum(fake.ServiceClient(), "1234asdf", "foo").Extract()
257+
th.AssertNoErr(t, err)
258+
th.AssertDeepEquals(t, expected, actual)
259+
}
260+
261+
func TestCreateMetadatum(t *testing.T) {
262+
th.SetupHTTP()
263+
defer th.TeardownHTTP()
264+
265+
HandleMetadatumCreateSuccessfully(t)
266+
267+
expected := testMetadatumCreateMap
268+
actual, err := CreateMetadatum(fake.ServiceClient(), "1234asdf", testMetadatumChangeOpts).Extract()
269+
th.AssertNoErr(t, err)
270+
th.AssertDeepEquals(t, expected, actual)
271+
}
272+
273+
func TestDeleteMetadatum(t *testing.T) {
274+
th.SetupHTTP()
275+
defer th.TeardownHTTP()
276+
277+
HandleMetadatumDeleteSuccessfully(t)
278+
279+
err := DeleteMetadatum(fake.ServiceClient(), "1234asdf", "foo").ExtractErr()
280+
th.AssertNoErr(t, err)
281+
}

openstack/compute/v2/images/results.go

+68
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,71 @@ func ExtractImages(page pagination.Page) ([]Image, error) {
9595
err := mapstructure.Decode(casted, &results)
9696
return results.Images, err
9797
}
98+
99+
// MetadataResult contains the result of a call for (potentially) multiple key-value pairs.
100+
type MetadataResult struct {
101+
gophercloud.Result
102+
}
103+
104+
// GetMetadataResult temporarily contains the response from a metadata Get call.
105+
type GetMetadataResult struct {
106+
MetadataResult
107+
}
108+
109+
// ResetMetadataResult temporarily contains the response from a metadata Reset call.
110+
type ResetMetadataResult struct {
111+
MetadataResult
112+
}
113+
114+
// UpdateMetadataResult temporarily contains the response from a metadata Update call.
115+
type UpdateMetadataResult struct {
116+
MetadataResult
117+
}
118+
119+
// MetadatumResult contains the result of a call for individual a single key-value pair.
120+
type MetadatumResult struct {
121+
gophercloud.Result
122+
}
123+
124+
// GetMetadatumResult temporarily contains the response from a metadatum Get call.
125+
type GetMetadatumResult struct {
126+
MetadatumResult
127+
}
128+
129+
// CreateMetadatumResult temporarily contains the response from a metadatum Create call.
130+
type CreateMetadatumResult struct {
131+
MetadatumResult
132+
}
133+
134+
// DeleteMetadatumResult temporarily contains the response from a metadatum Delete call.
135+
type DeleteMetadatumResult struct {
136+
gophercloud.ErrResult
137+
}
138+
139+
// Extract interprets any MetadataResult as a Metadata, if possible.
140+
func (r MetadataResult) Extract() (map[string]interface{}, error) {
141+
if r.Err != nil {
142+
return nil, r.Err
143+
}
144+
145+
var response struct {
146+
Metadata map[string]interface{} `mapstructure:"metadata"`
147+
}
148+
149+
err := mapstructure.Decode(r.Body, &response)
150+
return response.Metadata, err
151+
}
152+
153+
// Extract interprets any MetadatumResult as a Metadatum, if possible.
154+
func (r MetadatumResult) Extract() (map[string]interface{}, error) {
155+
if r.Err != nil {
156+
return nil, r.Err
157+
}
158+
159+
var response struct {
160+
Metadatum map[string]interface{} `mapstructure:"meta"`
161+
}
162+
163+
err := mapstructure.Decode(r.Body, &response)
164+
return response.Metadatum, err
165+
}

openstack/compute/v2/images/urls.go

+8
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,11 @@ func getURL(client *gophercloud.ServiceClient, id string) string {
1313
func deleteURL(client *gophercloud.ServiceClient, id string) string {
1414
return client.ServiceURL("images", id)
1515
}
16+
17+
func metadataURL(client *gophercloud.ServiceClient, id string) string {
18+
return client.ServiceURL("images", id, "metadata")
19+
}
20+
21+
func metadatumURL(client *gophercloud.ServiceClient, id, key string) string {
22+
return client.ServiceURL("images", id, "metadata", key)
23+
}

0 commit comments

Comments
 (0)