The following pull request was submitted through Github.
It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/2129

This e-mail was sent by the LXC bot, direct replies will not reach the author
unless they happen to be subscribed to this list.

=== Description (from pull-request) ===
Closes #120

Signed-off-by: Stéphane Graber <stgra...@ubuntu.com>
From 771194b6d95a7b913565bce8a6314eb15d21abab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgra...@ubuntu.com>
Date: Thu, 16 Jun 2016 12:17:52 -0400
Subject: [PATCH] Implement ETag support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #120

Signed-off-by: Stéphane Graber <stgra...@ubuntu.com>
---
 doc/api_extensions.md     | 13 +++++++++++++
 doc/rest-api.md           | 10 +++++-----
 lxd/api_1.0.go            |  9 +++++++--
 lxd/container.go          |  2 +-
 lxd/container_get.go      |  4 ++--
 lxd/container_lxc.go      | 13 ++++++++-----
 lxd/container_put.go      |  8 ++++++++
 lxd/container_snapshot.go |  4 ++--
 lxd/containers_get.go     |  2 +-
 lxd/images.go             | 38 ++++++++++++++++++++++++++------------
 lxd/profiles.go           | 20 +++++++++++++-------
 lxd/response.go           | 28 +++++++++++++++++++++++-----
 lxd/util.go               | 30 ++++++++++++++++++++++++++++++
 13 files changed, 139 insertions(+), 42 deletions(-)

diff --git a/doc/api_extensions.md b/doc/api_extensions.md
index 7f05af5..567c1a6 100644
--- a/doc/api_extensions.md
+++ b/doc/api_extensions.md
@@ -46,3 +46,16 @@ It is a timestamp of the last time the container was started.
 
 If a container has been created but not started yet, last\_used\_at field
 will be 1970-01-01T00:00:00Z
+
+## etag
+Add support for the ETag header on all relevant endpoints.
+
+This adds the following HTTP header on answers to GET:
+ - ETag (SHA-256 of user modifiable content)
+
+And adds support for the following HTTP header on PUT requests:
+ - If-Match (ETag value retrieved through previous GET)
+
+This makes it possible to GET a LXD object, modify it and PUT it without
+risking to hit a race condition where LXD or another client modified the
+object in the mean time.
diff --git a/doc/rest-api.md b/doc/rest-api.md
index a1390cc..b617b35 100644
--- a/doc/rest-api.md
+++ b/doc/rest-api.md
@@ -257,7 +257,7 @@ Return value (if guest or untrusted):
         "public": false,                        # Whether the server should be 
treated as a public (read-only) remote by the client
     }
 
-### PUT
+### PUT (ETag supported)
  * Description: Updates the server configuration or other properties
  * Authentication: trusted
  * Operation: sync
@@ -557,7 +557,7 @@ Output:
     }
 
 
-### PUT
+### PUT (ETag supported)
  * Description: update container configuration or restore snapshot
  * Authentication: trusted
  * Operation: async
@@ -1231,7 +1231,7 @@ Input (none at present):
 
 HTTP code for this should be 202 (Accepted).
 
-### PUT
+### PUT (ETag supported)
  * Description: Updates the image properties
  * Authentication: trusted
  * Operation: sync
@@ -1335,7 +1335,7 @@ Output:
         "target": 
"c9b6e738fae75286d52f497415463a8ecc61bbcb046536f220d797b0e500a41f"
     }
 
-### PUT
+### PUT (ETag supported)
  * Description: Updates the alias target or description
  * Authentication: trusted
  * Operation: sync
@@ -1537,7 +1537,7 @@ Output:
         }
     }
 
-### PUT
+### PUT (ETag supported)
  * Description: update the profile
  * Authentication: trusted
  * Operation: sync
diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go
index 6ede473..45306ec 100644
--- a/lxd/api_1.0.go
+++ b/lxd/api_1.0.go
@@ -60,6 +60,7 @@ func api10Get(d *Daemon, r *http.Request) Response {
                        "container_syscall_filtering",
                        "auth_pki",
                        "container_last_used_at",
+                       "etag",
                },
 
                "api_status":  "stable",
@@ -148,7 +149,7 @@ func api10Get(d *Daemon, r *http.Request) Response {
                body["public"] = false
        }
 
-       return SyncResponse(true, body)
+       return SyncResponseETag(true, body, body["config"])
 }
 
 type apiPut struct {
@@ -161,8 +162,12 @@ func api10Put(d *Daemon, r *http.Request) Response {
                return InternalError(err)
        }
 
-       req := apiPut{}
+       err = etagCheck(r, oldConfig)
+       if err != nil {
+               return PreconditionFailed(err)
+       }
 
+       req := apiPut{}
        if err := shared.ReadToJSON(r.Body, &req); err != nil {
                return BadRequest(err)
        }
diff --git a/lxd/container.go b/lxd/container.go
index f4a6307..ecd74e1 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -402,7 +402,7 @@ type container interface {
        Exec(command []string, env map[string]string, stdin *os.File, stdout 
*os.File, stderr *os.File) (int, error)
 
        // Status
-       Render() (interface{}, error)
+       Render() (interface{}, interface{}, error)
        RenderState() (*shared.ContainerState, error)
        IsPrivileged() bool
        IsRunning() bool
diff --git a/lxd/container_get.go b/lxd/container_get.go
index 3df94ea..a1db3a4 100644
--- a/lxd/container_get.go
+++ b/lxd/container_get.go
@@ -13,10 +13,10 @@ func containerGet(d *Daemon, r *http.Request) Response {
                return SmartError(err)
        }
 
-       state, err := c.Render()
+       state, etag, err := c.Render()
        if err != nil {
                return InternalError(err)
        }
 
-       return SyncResponse(true, state)
+       return SyncResponseETag(true, state, etag)
 }
diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 2623174..c75d29c 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -1612,16 +1612,19 @@ func (c *containerLXC) getLxcState() (lxc.State, error) 
{
        }
 }
 
-func (c *containerLXC) Render() (interface{}, error) {
+func (c *containerLXC) Render() (interface{}, interface{}, error) {
        // Load the go-lxc struct
        err := c.initLXC()
        if err != nil {
-               return nil, err
+               return nil, nil, err
        }
 
        // Ignore err as the arch string on error is correct (unknown)
        architectureName, _ := shared.ArchitectureName(c.architecture)
 
+       // Prepare the ETag
+       etag := []interface{}{c.architecture, c.localConfig, c.localDevices, 
c.ephemeral, c.profiles}
+
        if c.IsSnapshot() {
                return &shared.SnapshotInfo{
                        Architecture:    architectureName,
@@ -1635,12 +1638,12 @@ func (c *containerLXC) Render() (interface{}, error) {
                        Name:            c.name,
                        Profiles:        c.profiles,
                        Stateful:        c.stateful,
-               }, nil
+               }, etag, nil
        } else {
                // FIXME: Render shouldn't directly access the go-lxc struct
                cState, err := c.getLxcState()
                if err != nil {
-                       return nil, err
+                       return nil, nil, err
                }
                statusCode := shared.FromLXCState(int(cState))
 
@@ -1658,7 +1661,7 @@ func (c *containerLXC) Render() (interface{}, error) {
                        Status:          statusCode.String(),
                        StatusCode:      statusCode,
                        Stateful:        c.stateful,
-               }, nil
+               }, etag, nil
        }
 }
 
diff --git a/lxd/container_put.go b/lxd/container_put.go
index 747a234..1b19cea 100644
--- a/lxd/container_put.go
+++ b/lxd/container_put.go
@@ -26,12 +26,20 @@ type containerPutReq struct {
  * the named snapshot
  */
 func containerPut(d *Daemon, r *http.Request) Response {
+       // Get the container
        name := mux.Vars(r)["name"]
        c, err := containerLoadByName(d, name)
        if err != nil {
                return NotFound
        }
 
+       // Validate the ETag
+       etag := []interface{}{c.Architecture(), c.LocalConfig(), 
c.LocalDevices(), c.IsEphemeral(), c.Profiles()}
+       err = etagCheck(r, etag)
+       if err != nil {
+               return PreconditionFailed(err)
+       }
+
        configRaw := containerPutReq{}
        if err := json.NewDecoder(r.Body).Decode(&configRaw); err != nil {
                return BadRequest(err)
diff --git a/lxd/container_snapshot.go b/lxd/container_snapshot.go
index 803ac11..7d5e409 100644
--- a/lxd/container_snapshot.go
+++ b/lxd/container_snapshot.go
@@ -44,7 +44,7 @@ func containerSnapshotsGet(d *Daemon, r *http.Request) 
Response {
                        url := fmt.Sprintf("/%s/containers/%s/snapshots/%s", 
shared.APIVersion, cname, snapName)
                        resultString = append(resultString, url)
                } else {
-                       render, err := snap.Render()
+                       render, _, err := snap.Render()
                        if err != nil {
                                continue
                        }
@@ -183,7 +183,7 @@ func snapshotHandler(d *Daemon, r *http.Request) Response {
 }
 
 func snapshotGet(sc container, name string) Response {
-       render, err := sc.Render()
+       render, _, err := sc.Render()
        if err != nil {
                return SmartError(err)
        }
diff --git a/lxd/containers_get.go b/lxd/containers_get.go
index 750782d..cb0be7e 100644
--- a/lxd/containers_get.go
+++ b/lxd/containers_get.go
@@ -69,7 +69,7 @@ func doContainerGet(d *Daemon, cname string) 
(*shared.ContainerInfo, error) {
                return nil, err
        }
 
-       cts, err := c.Render()
+       cts, _, err := c.Render()
        if err != nil {
                return nil, err
        }
diff --git a/lxd/images.go b/lxd/images.go
index 3410b6b..8118d8f 100644
--- a/lxd/images.go
+++ b/lxd/images.go
@@ -1016,7 +1016,8 @@ func imageGet(d *Daemon, r *http.Request) Response {
                return response
        }
 
-       return SyncResponse(true, info)
+       etag := []interface{}{info.Public, info.AutoUpdate, info.Properties}
+       return SyncResponseETag(true, info, etag)
 }
 
 type imagePutReq struct {
@@ -1026,18 +1027,25 @@ type imagePutReq struct {
 }
 
 func imagePut(d *Daemon, r *http.Request) Response {
+       // Get current value
        fingerprint := mux.Vars(r)["fingerprint"]
+       id, info, err := dbImageGet(d.db, fingerprint, false, false)
+       if err != nil {
+               return SmartError(err)
+       }
+
+       // Validate ETag
+       etag := []interface{}{info.Public, info.AutoUpdate, info.Properties}
+       err = etagCheck(r, etag)
+       if err != nil {
+               return PreconditionFailed(err)
+       }
 
        req := imagePutReq{}
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
                return BadRequest(err)
        }
 
-       id, info, err := dbImageGet(d.db, fingerprint, false, false)
-       if err != nil {
-               return SmartError(err)
-       }
-
        err = dbImageUpdate(d.db, id, info.Filename, info.Size, req.Public, 
req.AutoUpdate, info.Architecture, info.CreationDate, info.ExpiryDate, 
req.Properties)
        if err != nil {
                return SmartError(err)
@@ -1131,7 +1139,7 @@ func aliasGet(d *Daemon, r *http.Request) Response {
                return SmartError(err)
        }
 
-       return SyncResponse(true, alias)
+       return SyncResponseETag(true, alias, alias)
 }
 
 func aliasDelete(d *Daemon, r *http.Request) Response {
@@ -1150,7 +1158,18 @@ func aliasDelete(d *Daemon, r *http.Request) Response {
 }
 
 func aliasPut(d *Daemon, r *http.Request) Response {
+       // Get current value
        name := mux.Vars(r)["name"]
+       id, alias, err := dbImageAliasGet(d.db, name, true)
+       if err != nil {
+               return SmartError(err)
+       }
+
+       // Validate ETag
+       err = etagCheck(r, alias)
+       if err != nil {
+               return PreconditionFailed(err)
+       }
 
        req := aliasPutReq{}
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -1161,11 +1180,6 @@ func aliasPut(d *Daemon, r *http.Request) Response {
                return BadRequest(fmt.Errorf("The target field is required"))
        }
 
-       id, _, err := dbImageAliasGet(d.db, name, true)
-       if err != nil {
-               return SmartError(err)
-       }
-
        imageId, _, err := dbImageGet(d.db, req.Target, false, false)
        if err != nil {
                return SmartError(err)
diff --git a/lxd/profiles.go b/lxd/profiles.go
index 17bb997..32a498a 100644
--- a/lxd/profiles.go
+++ b/lxd/profiles.go
@@ -104,7 +104,7 @@ func profileGet(d *Daemon, r *http.Request) Response {
                return SmartError(err)
        }
 
-       return SyncResponse(true, resp)
+       return SyncResponseETag(true, resp, resp)
 }
 
 func getRunningContainersWithProfile(d *Daemon, profile string) []container {
@@ -127,7 +127,18 @@ func getRunningContainersWithProfile(d *Daemon, profile 
string) []container {
 }
 
 func profilePut(d *Daemon, r *http.Request) Response {
+       // Get the profile
        name := mux.Vars(r)["name"]
+       id, profile, err := dbProfileGet(d.db, name)
+       if err != nil {
+               return InternalError(fmt.Errorf("Failed to retrieve 
profile='%s'", name))
+       }
+
+       // Validate the ETag
+       err = etagCheck(r, profile)
+       if err != nil {
+               return PreconditionFailed(err)
+       }
 
        req := profilesPostReq{}
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -135,7 +146,7 @@ func profilePut(d *Daemon, r *http.Request) Response {
        }
 
        // Sanity checks
-       err := containerValidConfig(d, req.Config, true, false)
+       err = containerValidConfig(d, req.Config, true, false)
        if err != nil {
                return BadRequest(err)
        }
@@ -157,11 +168,6 @@ func profilePut(d *Daemon, r *http.Request) Response {
        }
 
        // Update the database
-       id, profile, err := dbProfileGet(d.db, name)
-       if err != nil {
-               return InternalError(fmt.Errorf("Failed to retrieve 
profile='%s'", name))
-       }
-
        tx, err := dbBegin(d.db)
        if err != nil {
                return InternalError(err)
diff --git a/lxd/response.go b/lxd/response.go
index 59a7a66..2aa9a05 100644
--- a/lxd/response.go
+++ b/lxd/response.go
@@ -39,10 +39,20 @@ type Response interface {
 // Sync response
 type syncResponse struct {
        success  bool
+       etag     interface{}
        metadata interface{}
 }
 
 func (r *syncResponse) Render(w http.ResponseWriter) error {
+       // Set an appropriate ETag header
+       if r.etag != nil {
+               etag, err := etagHash(r.etag)
+               if err == nil {
+                       w.Header().Set("ETag", etag)
+               }
+       }
+
+       // Prepare the JSON response
        status := shared.Success
        if !r.success {
                status = shared.Failure
@@ -52,10 +62,6 @@ func (r *syncResponse) Render(w http.ResponseWriter) error {
        return WriteJSON(w, resp)
 }
 
-func SyncResponse(success bool, metadata interface{}) Response {
-       return &syncResponse{success, metadata}
-}
-
 func (r *syncResponse) String() string {
        if r.success {
                return "success"
@@ -64,7 +70,15 @@ func (r *syncResponse) String() string {
        return "failure"
 }
 
-var EmptySyncResponse = &syncResponse{true, make(map[string]interface{})}
+func SyncResponse(success bool, metadata interface{}) Response {
+       return &syncResponse{success: success, metadata: metadata}
+}
+
+func SyncResponseETag(success bool, metadata interface{}, etag interface{}) 
Response {
+       return &syncResponse{success: success, metadata: metadata, etag: etag}
+}
+
+var EmptySyncResponse = &syncResponse{success: true, metadata: 
make(map[string]interface{})}
 
 // File transfer response
 type fileResponseEntry struct {
@@ -253,6 +267,10 @@ func InternalError(err error) Response {
        return &errorResponse{http.StatusInternalServerError, err.Error()}
 }
 
+func PreconditionFailed(err error) Response {
+       return &errorResponse{http.StatusPreconditionFailed, err.Error()}
+}
+
 /*
  * SmartError returns the right error message based on err.
  */
diff --git a/lxd/util.go b/lxd/util.go
index 331d284..a496f82 100644
--- a/lxd/util.go
+++ b/lxd/util.go
@@ -2,7 +2,9 @@ package main
 
 import (
        "bytes"
+       "crypto/sha256"
        "encoding/json"
+       "fmt"
        "io"
        "net/http"
 
@@ -27,3 +29,31 @@ func WriteJSON(w http.ResponseWriter, body interface{}) 
error {
 
        return err
 }
+
+func etagHash(data interface{}) (string, error) {
+       etag := sha256.New()
+       err := json.NewEncoder(etag).Encode(data)
+       if err != nil {
+               return "", err
+       }
+
+       return fmt.Sprintf("%x", etag.Sum(nil)), nil
+}
+
+func etagCheck(r *http.Request, data interface{}) error {
+       match := r.Header.Get("If-Match")
+       if match == "" {
+               return nil
+       }
+
+       hash, err := etagHash(data)
+       if err != nil {
+               return err
+       }
+
+       if hash != match {
+               return fmt.Errorf("ETag doesn't match: %s vs %s", hash, match)
+       }
+
+       return nil
+}
_______________________________________________
lxc-devel mailing list
lxc-devel@lists.linuxcontainers.org
http://lists.linuxcontainers.org/listinfo/lxc-devel

Reply via email to