Merge pull request #344 from smashwilson/cdn-updates
Improve CDN service patch updates.
diff --git a/acceptance/rackspace/cdn/v1/service_test.go b/acceptance/rackspace/cdn/v1/service_test.go
index af73dcf..ddec667 100644
--- a/acceptance/rackspace/cdn/v1/service_test.go
+++ b/acceptance/rackspace/cdn/v1/service_test.go
@@ -60,17 +60,13 @@
}
func testServiceUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) {
- updateOpts := os.UpdateOpts{
- os.UpdateOpt{
- Op: os.Add,
- Path: "/domains/-",
- Value: map[string]interface{}{
- "domain": "newDomain.com",
- "protocol": "http",
- },
+ opts := []os.Patch{
+ os.Append{
+ Value: os.Domain{Domain: "newDomain.com", Protocol: "http"},
},
}
- loc, err := services.Update(client, id, updateOpts).Extract()
+
+ loc, err := services.Update(client, id, opts).Extract()
th.AssertNoErr(t, err)
t.Logf("Successfully updated service at location: %s", loc)
}
diff --git a/openstack/cdn/v1/services/fixtures.go b/openstack/cdn/v1/services/fixtures.go
index ec1fd52..d9bc9f2 100644
--- a/openstack/cdn/v1/services/fixtures.go
+++ b/openstack/cdn/v1/services/fixtures.go
@@ -170,10 +170,10 @@
// HandleCreateCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux
// that responds with a `Create` response.
func HandleCreateCDNServiceSuccessfully(t *testing.T) {
- th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
- th.TestMethod(t, r, "POST")
- th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- th.TestJSONRequest(t, r, `
+ th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestJSONRequest(t, r, `
{
"name": "mywebsite.com",
"domains": [
@@ -212,21 +212,21 @@
"flavor_id": "cdn"
}
`)
- w.Header().Add("Location", "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
- w.WriteHeader(http.StatusAccepted)
- })
+ w.Header().Add("Location", "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
+ w.WriteHeader(http.StatusAccepted)
+ })
}
// HandleGetCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
// that responds with a `Get` response.
func HandleGetCDNServiceSuccessfully(t *testing.T) {
- th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
- th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- fmt.Fprintf(w, `
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
{
"id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
"name": "mywebsite.com",
@@ -299,44 +299,74 @@
]
}
`)
- })
+ })
}
// HandleUpdateCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
// that responds with a `Update` response.
func HandleUpdateCDNServiceSuccessfully(t *testing.T) {
- th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
- th.TestMethod(t, r, "PATCH")
- th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- th.TestJSONRequest(t, r, `
+ th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PATCH")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestJSONRequest(t, r, `
[
- {
- "op": "replace",
- "path": "/origins/0",
- "value": {
- "origin": "44.33.22.11",
- "port": 80,
- "ssl": false
- }
- },
- {
- "op": "add",
- "path": "/domains/0",
- "value": {"domain": "added.mocksite4.com"}
- }
+ {
+ "op": "add",
+ "path": "/domains/-",
+ "value": {"domain": "appended.mocksite4.com"}
+ },
+ {
+ "op": "add",
+ "path": "/domains/4",
+ "value": {"domain": "inserted.mocksite4.com"}
+ },
+ {
+ "op": "add",
+ "path": "/domains",
+ "value": [
+ {"domain": "bulkadded1.mocksite4.com"},
+ {"domain": "bulkadded2.mocksite4.com"}
+ ]
+ },
+ {
+ "op": "replace",
+ "path": "/origins/2",
+ "value": {"origin": "44.33.22.11", "port": 80, "ssl": false}
+ },
+ {
+ "op": "replace",
+ "path": "/origins",
+ "value": [
+ {"origin": "44.33.22.11", "port": 80, "ssl": false},
+ {"origin": "55.44.33.22", "port": 443, "ssl": true}
+ ]
+ },
+ {
+ "op": "remove",
+ "path": "/caching/8"
+ },
+ {
+ "op": "remove",
+ "path": "/caching"
+ },
+ {
+ "op": "replace",
+ "path": "/name",
+ "value": "differentServiceName"
+ }
]
`)
- w.Header().Add("Location", "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
- w.WriteHeader(http.StatusAccepted)
- })
+ w.Header().Add("Location", "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
+ w.WriteHeader(http.StatusAccepted)
+ })
}
// HandleDeleteCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
// that responds with a `Delete` response.
func HandleDeleteCDNServiceSuccessfully(t *testing.T) {
- th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
- th.TestMethod(t, r, "DELETE")
- th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- w.WriteHeader(http.StatusAccepted)
- })
+ th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
}
diff --git a/openstack/cdn/v1/services/requests.go b/openstack/cdn/v1/services/requests.go
index 646f63e..801fd3b 100644
--- a/openstack/cdn/v1/services/requests.go
+++ b/openstack/cdn/v1/services/requests.go
@@ -209,75 +209,142 @@
return res
}
-// UpdateOptsBuilder is the interface options structs have to satisfy in order
-// to be used in the main Update operation in this package. Since many
-// extensions decorate or modify the common logic, it is useful for them to
-// satisfy a basic interface in order for them to be used.
-type UpdateOptsBuilder interface {
- ToCDNServiceUpdateMap() ([]map[string]interface{}, error)
+// Path is a JSON pointer location that indicates which service parameter is being added, replaced,
+// or removed.
+type Path struct {
+ baseElement string
}
-// Op represents an update operation.
-type Op string
+func (p Path) renderRoot() string {
+ return "/" + p.baseElement
+}
+
+func (p Path) renderDash() string {
+ return fmt.Sprintf("/%s/-", p.baseElement)
+}
+
+func (p Path) renderIndex(index int64) string {
+ return fmt.Sprintf("/%s/%d", p.baseElement, index)
+}
var (
- // Add is a constant used for performing a "add" operation when updating.
- Add Op = "add"
- // Remove is a constant used for performing a "remove" operation when updating.
- Remove Op = "remove"
- // Replace is a constant used for performing a "replace" operation when updating.
- Replace Op = "replace"
+ // PathDomains indicates that an update operation is to be performed on a Domain.
+ PathDomains = Path{baseElement: "domains"}
+
+ // PathOrigins indicates that an update operation is to be performed on an Origin.
+ PathOrigins = Path{baseElement: "origins"}
+
+ // PathCaching indicates that an update operation is to be performed on a CacheRule.
+ PathCaching = Path{baseElement: "caching"}
)
-// UpdateOpts represents the attributes used when updating an existing CDN service.
-type UpdateOpts []UpdateOpt
-
-// UpdateOpt represents a single update to an existing service. Multiple updates
-// to a service can be submitted at the same time. See UpdateOpts.
-type UpdateOpt struct {
- // Specifies the update operation to perform.
- Op Op `json:"op"`
- // Specifies the JSON Pointer location within the service's JSON representation
- // of the service parameter being added, replaced or removed.
- Path string `json:"path"`
- // Specifies the actual value to be added or replaced. It is not required for
- // the remove operation.
- Value map[string]interface{} `json:"value,omitempty"`
+type value interface {
+ toPatchValue() interface{}
+ appropriatePath() Path
+ renderRootOr(func(p Path) string) string
}
-// ToCDNServiceUpdateMap casts an UpdateOpts struct to a map.
-func (opts UpdateOpts) ToCDNServiceUpdateMap() ([]map[string]interface{}, error) {
- s := make([]map[string]interface{}, len(opts))
+// Patch represents a single update to an existing Service. Multiple updates to a service can be
+// submitted at the same time.
+type Patch interface {
+ ToCDNServiceUpdateMap() map[string]interface{}
+}
- for i, opt := range opts {
- if opt.Op != Add && opt.Op != Remove && opt.Op != Replace {
- return nil, fmt.Errorf("Invalid Op: %v", opt.Op)
- }
- if opt.Op == "" {
- return nil, no("Op")
- }
- if opt.Path == "" {
- return nil, no("Path")
- }
- if opt.Op != Remove && opt.Value == nil {
- return nil, no("Value")
- }
- s[i] = map[string]interface{}{
- "op": opt.Op,
- "path": opt.Path,
- "value": opt.Value,
- }
+// Insertion is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to
+// a Service at a fixed index. Use an Append instead to append the new value to the end of its
+// collection. Pass it to the Update function as part of the Patch slice.
+type Insertion struct {
+ Index int64
+ Value value
+}
+
+// ToCDNServiceUpdateMap converts an Insertion into a request body fragment suitable for the
+// Update call.
+func (i Insertion) ToCDNServiceUpdateMap() map[string]interface{} {
+ return map[string]interface{}{
+ "op": "add",
+ "path": i.Value.renderRootOr(func(p Path) string { return p.renderIndex(i.Index) }),
+ "value": i.Value.toPatchValue(),
}
-
- return s, nil
}
-// Update accepts a UpdateOpts struct and updates an existing CDN service using
-// the values provided. idOrURL can be either the service's URL or its ID. For
-// example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// Append is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to a
+// Service at the end of its respective collection. Use an Insertion instead to insert the value
+// at a fixed index within the collection. Pass this to the Update function as part of its
+// Patch slice.
+type Append struct {
+ Value value
+}
+
+// ToCDNServiceUpdateMap converts an Append into a request body fragment suitable for the
+// Update call.
+func (a Append) ToCDNServiceUpdateMap() map[string]interface{} {
+ return map[string]interface{}{
+ "op": "add",
+ "path": a.Value.renderRootOr(func(p Path) string { return p.renderDash() }),
+ "value": a.Value.toPatchValue(),
+ }
+}
+
+// Replacement is a Patch that alters a specific service parameter (Domain, Origin, or CacheRule)
+// in-place by index. Pass it to the Update function as part of the Patch slice.
+type Replacement struct {
+ Value value
+ Index int64
+}
+
+// ToCDNServiceUpdateMap converts a Replacement into a request body fragment suitable for the
+// Update call.
+func (r Replacement) ToCDNServiceUpdateMap() map[string]interface{} {
+ return map[string]interface{}{
+ "op": "replace",
+ "path": r.Value.renderRootOr(func(p Path) string { return p.renderIndex(r.Index) }),
+ "value": r.Value.toPatchValue(),
+ }
+}
+
+// NameReplacement specifically updates the Service name. Pass it to the Update function as part
+// of the Patch slice.
+type NameReplacement struct {
+ NewName string
+}
+
+// ToCDNServiceUpdateMap converts a NameReplacement into a request body fragment suitable for the
+// Update call.
+func (r NameReplacement) ToCDNServiceUpdateMap() map[string]interface{} {
+ return map[string]interface{}{
+ "op": "replace",
+ "path": "/name",
+ "value": r.NewName,
+ }
+}
+
+// Removal is a Patch that requests the removal of a service parameter (Domain, Origin, or
+// CacheRule) by index. Pass it to the Update function as part of the Patch slice.
+type Removal struct {
+ Path Path
+ Index int64
+ All bool
+}
+
+// ToCDNServiceUpdateMap converts a Removal into a request body fragment suitable for the
+// Update call.
+func (r Removal) ToCDNServiceUpdateMap() map[string]interface{} {
+ result := map[string]interface{}{"op": "remove"}
+ if r.All {
+ result["path"] = r.Path.renderRoot()
+ } else {
+ result["path"] = r.Path.renderIndex(r.Index)
+ }
+ return result
+}
+
+// Update accepts a slice of Patch operations (Insertion, Append, Replacement or Removal) and
+// updates an existing CDN service using the values provided. idOrURL can be either the service's
+// URL or its ID. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
// are valid options for idOrURL.
-func Update(c *gophercloud.ServiceClient, idOrURL string, opts UpdateOptsBuilder) UpdateResult {
+func Update(c *gophercloud.ServiceClient, idOrURL string, patches []Patch) UpdateResult {
var url string
if strings.Contains(idOrURL, "/") {
url = idOrURL
@@ -285,11 +352,9 @@
url = updateURL(c, idOrURL)
}
- var res UpdateResult
- reqBody, err := opts.ToCDNServiceUpdateMap()
- if err != nil {
- res.Err = err
- return res
+ reqBody := make([]map[string]interface{}, len(patches))
+ for i, patch := range patches {
+ reqBody[i] = patch.ToCDNServiceUpdateMap()
}
resp, err := perigee.Request("PATCH", url, perigee.Options{
@@ -297,9 +362,10 @@
ReqBody: &reqBody,
OkCodes: []int{202},
})
- res.Header = resp.HttpResponse.Header
- res.Err = err
- return res
+ var result UpdateResult
+ result.Header = resp.HttpResponse.Header
+ result.Err = err
+ return result
}
// Delete accepts a service's ID or its URL and deletes the CDN service
diff --git a/openstack/cdn/v1/services/requests_test.go b/openstack/cdn/v1/services/requests_test.go
index ca7c269..2c11562 100644
--- a/openstack/cdn/v1/services/requests_test.go
+++ b/openstack/cdn/v1/services/requests_test.go
@@ -129,8 +129,8 @@
},
},
Restrictions: []Restriction{},
- FlavorID: "europe",
- Status: "deployed",
+ FlavorID: "europe",
+ Status: "deployed",
Links: []gophercloud.Link{
gophercloud.Link{
Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
@@ -160,204 +160,199 @@
}
func TestCreate(t *testing.T) {
- th.SetupHTTP()
- defer th.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- HandleCreateCDNServiceSuccessfully(t)
+ HandleCreateCDNServiceSuccessfully(t)
- createOpts := CreateOpts{
- Name: "mywebsite.com",
- Domains: []Domain{
- Domain{
- Domain: "www.mywebsite.com",
- },
- Domain{
- Domain: "blog.mywebsite.com",
- },
- },
- Origins: []Origin{
- Origin{
- Origin: "mywebsite.com",
- Port: 80,
- SSL: false,
- },
- },
- Restrictions: []Restriction{
- Restriction{
- Name: "website only",
- Rules: []RestrictionRule{
- RestrictionRule{
- Name: "mywebsite.com",
- Referrer: "www.mywebsite.com",
- },
- },
- },
- },
- Caching: []CacheRule{
- CacheRule{
- Name: "default",
- TTL: 3600,
- },
- },
- FlavorID: "cdn",
- }
+ createOpts := CreateOpts{
+ Name: "mywebsite.com",
+ Domains: []Domain{
+ Domain{
+ Domain: "www.mywebsite.com",
+ },
+ Domain{
+ Domain: "blog.mywebsite.com",
+ },
+ },
+ Origins: []Origin{
+ Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Restrictions: []Restriction{
+ Restriction{
+ Name: "website only",
+ Rules: []RestrictionRule{
+ RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ Caching: []CacheRule{
+ CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ },
+ FlavorID: "cdn",
+ }
- expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
- actual, err := Create(fake.ServiceClient(), createOpts).Extract()
- th.AssertNoErr(t, err)
- th.AssertEquals(t, expected, actual)
+ expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+ actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, expected, actual)
}
func TestGet(t *testing.T) {
- th.SetupHTTP()
- defer th.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- HandleGetCDNServiceSuccessfully(t)
+ HandleGetCDNServiceSuccessfully(t)
- expected := &Service{
- ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
- Name: "mywebsite.com",
- Domains: []Domain{
- Domain{
- Domain: "www.mywebsite.com",
- Protocol: "http",
- },
- },
- Origins: []Origin{
- Origin{
- Origin: "mywebsite.com",
- Port: 80,
- SSL: false,
- },
- },
- Caching: []CacheRule{
- CacheRule{
- Name: "default",
- TTL: 3600,
- },
- CacheRule{
- Name: "home",
- TTL: 17200,
- Rules: []TTLRule{
- TTLRule{
- Name: "index",
- RequestURL: "/index.htm",
- },
- },
- },
- CacheRule{
- Name: "images",
- TTL: 12800,
- Rules: []TTLRule{
- TTLRule{
- Name: "images",
- RequestURL: "*.png",
- },
- },
- },
- },
- Restrictions: []Restriction{
- Restriction{
- Name: "website only",
- Rules: []RestrictionRule{
- RestrictionRule{
- Name: "mywebsite.com",
- Referrer: "www.mywebsite.com",
- },
- },
- },
- },
- FlavorID: "cdn",
- Status: "deployed",
- Errors: []Error{},
- Links: []gophercloud.Link{
- gophercloud.Link{
- Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
- Rel: "self",
- },
- gophercloud.Link{
- Href: "blog.mywebsite.com.cdn1.raxcdn.com",
- Rel: "access_url",
- },
- gophercloud.Link{
- Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
- Rel: "flavor",
- },
- },
- }
+ expected := &Service{
+ ID: "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Name: "mywebsite.com",
+ Domains: []Domain{
+ Domain{
+ Domain: "www.mywebsite.com",
+ Protocol: "http",
+ },
+ },
+ Origins: []Origin{
+ Origin{
+ Origin: "mywebsite.com",
+ Port: 80,
+ SSL: false,
+ },
+ },
+ Caching: []CacheRule{
+ CacheRule{
+ Name: "default",
+ TTL: 3600,
+ },
+ CacheRule{
+ Name: "home",
+ TTL: 17200,
+ Rules: []TTLRule{
+ TTLRule{
+ Name: "index",
+ RequestURL: "/index.htm",
+ },
+ },
+ },
+ CacheRule{
+ Name: "images",
+ TTL: 12800,
+ Rules: []TTLRule{
+ TTLRule{
+ Name: "images",
+ RequestURL: "*.png",
+ },
+ },
+ },
+ },
+ Restrictions: []Restriction{
+ Restriction{
+ Name: "website only",
+ Rules: []RestrictionRule{
+ RestrictionRule{
+ Name: "mywebsite.com",
+ Referrer: "www.mywebsite.com",
+ },
+ },
+ },
+ },
+ FlavorID: "cdn",
+ Status: "deployed",
+ Errors: []Error{},
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "blog.mywebsite.com.cdn1.raxcdn.com",
+ Rel: "access_url",
+ },
+ gophercloud.Link{
+ Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+ Rel: "flavor",
+ },
+ },
+ }
-
- actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract()
- th.AssertNoErr(t, err)
- th.AssertDeepEquals(t, expected, actual)
+ actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
}
func TestSuccessfulUpdate(t *testing.T) {
- th.SetupHTTP()
- defer th.TeardownHTTP()
-
- HandleUpdateCDNServiceSuccessfully(t)
-
- expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
- updateOpts := UpdateOpts{
- UpdateOpt{
- Op: Replace,
- Path: "/origins/0",
- Value: map[string]interface{}{
- "origin": "44.33.22.11",
- "port": 80,
- "ssl": false,
- },
- },
- UpdateOpt{
- Op: Add,
- Path: "/domains/0",
- Value: map[string]interface{}{
- "domain": "added.mocksite4.com",
- },
- },
- }
- actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", updateOpts).Extract()
- th.AssertNoErr(t, err)
- th.AssertEquals(t, expected, actual)
-}
-
-func TestUnsuccessfulUpdate(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
HandleUpdateCDNServiceSuccessfully(t)
- updateOpts := UpdateOpts{
- UpdateOpt{
- Op: "Foo",
- Path: "/origins/0",
- Value: map[string]interface{}{
- "origin": "44.33.22.11",
- "port": 80,
- "ssl": false,
+ expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+ ops := []Patch{
+ // Append a single Domain
+ Append{Value: Domain{Domain: "appended.mocksite4.com"}},
+ // Insert a single Domain
+ Insertion{
+ Index: 4,
+ Value: Domain{Domain: "inserted.mocksite4.com"},
+ },
+ // Bulk addition
+ Append{
+ Value: DomainList{
+ Domain{Domain: "bulkadded1.mocksite4.com"},
+ Domain{Domain: "bulkadded2.mocksite4.com"},
},
},
- UpdateOpt{
- Op: Add,
- Path: "/domains/0",
- Value: map[string]interface{}{
- "domain": "added.mocksite4.com",
+ // Replace a single Origin
+ Replacement{
+ Index: 2,
+ Value: Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+ },
+ // Bulk replace Origins
+ Replacement{
+ Index: 0, // Ignored
+ Value: OriginList{
+ Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+ Origin{Origin: "55.44.33.22", Port: 443, SSL: true},
},
},
+ // Remove a single CacheRule
+ Removal{
+ Index: 8,
+ Path: PathCaching,
+ },
+ // Bulk removal
+ Removal{
+ All: true,
+ Path: PathCaching,
+ },
+ // Service name replacement
+ NameReplacement{
+ NewName: "differentServiceName",
+ },
}
- _, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", updateOpts).Extract()
- if err == nil {
- t.Errorf("Expected error during TestUnsuccessfulUpdate but didn't get one.")
- }
+
+ actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", ops).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, expected, actual)
}
func TestDelete(t *testing.T) {
- th.SetupHTTP()
- defer th.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- HandleDeleteCDNServiceSuccessfully(t)
+ HandleDeleteCDNServiceSuccessfully(t)
- err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr()
- th.AssertNoErr(t, err)
+ err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr()
+ th.AssertNoErr(t, err)
}
diff --git a/openstack/cdn/v1/services/results.go b/openstack/cdn/v1/services/results.go
index c64509b..33406c4 100644
--- a/openstack/cdn/v1/services/results.go
+++ b/openstack/cdn/v1/services/results.go
@@ -17,6 +17,42 @@
Protocol string `mapstructure:"protocol" json:"protocol,omitempty"`
}
+func (d Domain) toPatchValue() interface{} {
+ r := make(map[string]interface{})
+ r["domain"] = d.Domain
+ if d.Protocol != "" {
+ r["protocol"] = d.Protocol
+ }
+ return r
+}
+
+func (d Domain) appropriatePath() Path {
+ return PathDomains
+}
+
+func (d Domain) renderRootOr(render func(p Path) string) string {
+ return render(d.appropriatePath())
+}
+
+// DomainList provides a useful way to perform bulk operations in a single Patch.
+type DomainList []Domain
+
+func (list DomainList) toPatchValue() interface{} {
+ r := make([]interface{}, len(list))
+ for i, domain := range list {
+ r[i] = domain.toPatchValue()
+ }
+ return r
+}
+
+func (list DomainList) appropriatePath() Path {
+ return PathDomains
+}
+
+func (list DomainList) renderRootOr(_ func(p Path) string) string {
+ return list.appropriatePath().renderRoot()
+}
+
// OriginRule represents a rule that defines when an origin should be accessed.
type OriginRule struct {
// Specifies the name of this rule.
@@ -39,6 +75,49 @@
Rules []OriginRule `mapstructure:"rules" json:"rules,omitempty"`
}
+func (o Origin) toPatchValue() interface{} {
+ r := make(map[string]interface{})
+ r["origin"] = o.Origin
+ r["port"] = o.Port
+ r["ssl"] = o.SSL
+ if len(o.Rules) > 0 {
+ r["rules"] = make([]map[string]interface{}, len(o.Rules))
+ for index, rule := range o.Rules {
+ submap := r["rules"].([]map[string]interface{})[index]
+ submap["name"] = rule.Name
+ submap["request_url"] = rule.RequestURL
+ }
+ }
+ return r
+}
+
+func (o Origin) appropriatePath() Path {
+ return PathOrigins
+}
+
+func (o Origin) renderRootOr(render func(p Path) string) string {
+ return render(o.appropriatePath())
+}
+
+// OriginList provides a useful way to perform bulk operations in a single Patch.
+type OriginList []Origin
+
+func (list OriginList) toPatchValue() interface{} {
+ r := make([]interface{}, len(list))
+ for i, origin := range list {
+ r[i] = origin.toPatchValue()
+ }
+ return r
+}
+
+func (list OriginList) appropriatePath() Path {
+ return PathOrigins
+}
+
+func (list OriginList) renderRootOr(_ func(p Path) string) string {
+ return list.appropriatePath().renderRoot()
+}
+
// TTLRule specifies a rule that determines if a TTL should be applied to an asset.
type TTLRule struct {
// Specifies the name of this rule.
@@ -57,6 +136,46 @@
Rules []TTLRule `mapstructure:"rules" json:"rules,omitempty"`
}
+func (c CacheRule) toPatchValue() interface{} {
+ r := make(map[string]interface{})
+ r["name"] = c.Name
+ r["ttl"] = c.TTL
+ r["rules"] = make([]map[string]interface{}, len(c.Rules))
+ for index, rule := range c.Rules {
+ submap := r["rules"].([]map[string]interface{})[index]
+ submap["name"] = rule.Name
+ submap["request_url"] = rule.RequestURL
+ }
+ return r
+}
+
+func (c CacheRule) appropriatePath() Path {
+ return PathCaching
+}
+
+func (c CacheRule) renderRootOr(render func(p Path) string) string {
+ return render(c.appropriatePath())
+}
+
+// CacheRuleList provides a useful way to perform bulk operations in a single Patch.
+type CacheRuleList []CacheRule
+
+func (list CacheRuleList) toPatchValue() interface{} {
+ r := make([]interface{}, len(list))
+ for i, rule := range list {
+ r[i] = rule.toPatchValue()
+ }
+ return r
+}
+
+func (list CacheRuleList) appropriatePath() Path {
+ return PathCaching
+}
+
+func (list CacheRuleList) renderRootOr(_ func(p Path) string) string {
+ return list.appropriatePath().renderRoot()
+}
+
// RestrictionRule specifies a rule that determines if this restriction should be applied to an asset.
type RestrictionRule struct {
// Specifies the name of this rule.
diff --git a/rackspace/cdn/v1/services/delegate.go b/rackspace/cdn/v1/services/delegate.go
index 10881eb..e3f1459 100644
--- a/rackspace/cdn/v1/services/delegate.go
+++ b/rackspace/cdn/v1/services/delegate.go
@@ -27,8 +27,8 @@
// Update accepts a UpdateOpts struct and updates an existing CDN service using
// the values provided.
-func Update(c *gophercloud.ServiceClient, id string, opts os.UpdateOptsBuilder) os.UpdateResult {
- return os.Update(c, id, opts)
+func Update(c *gophercloud.ServiceClient, id string, patches []os.Patch) os.UpdateResult {
+ return os.Update(c, id, patches)
}
// Delete accepts a unique ID and deletes the CDN service associated with it.
diff --git a/rackspace/cdn/v1/services/delegate_test.go b/rackspace/cdn/v1/services/delegate_test.go
index f833cab..6c48365 100644
--- a/rackspace/cdn/v1/services/delegate_test.go
+++ b/rackspace/cdn/v1/services/delegate_test.go
@@ -299,59 +299,55 @@
os.HandleUpdateCDNServiceSuccessfully(t)
expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
- updateOpts := os.UpdateOpts{
- os.UpdateOpt{
- Op: os.Replace,
- Path: "/origins/0",
- Value: map[string]interface{}{
- "origin": "44.33.22.11",
- "port": 80,
- "ssl": false,
+ ops := []os.Patch{
+ // Append a single Domain
+ os.Append{Value: os.Domain{Domain: "appended.mocksite4.com"}},
+ // Insert a single Domain
+ os.Insertion{
+ Index: 4,
+ Value: os.Domain{Domain: "inserted.mocksite4.com"},
+ },
+ // Bulk addition
+ os.Append{
+ Value: os.DomainList{
+ os.Domain{Domain: "bulkadded1.mocksite4.com"},
+ os.Domain{Domain: "bulkadded2.mocksite4.com"},
},
},
- os.UpdateOpt{
- Op: os.Add,
- Path: "/domains/0",
- Value: map[string]interface{}{
- "domain": "added.mocksite4.com",
+ // Replace a single Origin
+ os.Replacement{
+ Index: 2,
+ Value: os.Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+ },
+ // Bulk replace Origins
+ os.Replacement{
+ Index: 0, // Ignored
+ Value: os.OriginList{
+ os.Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+ os.Origin{Origin: "55.44.33.22", Port: 443, SSL: true},
},
},
+ // Remove a single CacheRule
+ os.Removal{
+ Index: 8,
+ Path: os.PathCaching,
+ },
+ // Bulk removal
+ os.Removal{
+ All: true,
+ Path: os.PathCaching,
+ },
+ // Service name replacement
+ os.NameReplacement{
+ NewName: "differentServiceName",
+ },
}
- actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", updateOpts).Extract()
+
+ actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", ops).Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, expected, actual)
}
-func TestUnsuccessfulUpdate(t *testing.T) {
- th.SetupHTTP()
- defer th.TeardownHTTP()
-
- os.HandleUpdateCDNServiceSuccessfully(t)
-
- updateOpts := os.UpdateOpts{
- os.UpdateOpt{
- Op: "Foo",
- Path: "/origins/0",
- Value: map[string]interface{}{
- "origin": "44.33.22.11",
- "port": 80,
- "ssl": false,
- },
- },
- os.UpdateOpt{
- Op: os.Add,
- Path: "/domains/0",
- Value: map[string]interface{}{
- "domain": "added.mocksite4.com",
- },
- },
- }
- _, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", updateOpts).Extract()
- if err == nil {
- t.Errorf("Expected error during TestUnsuccessfulUpdate but didn't get one.")
- }
-}
-
func TestDelete(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()