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

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 #1941

Signed-off-by: Stéphane Graber <stgra...@ubuntu.com>
From d7a6b8ab42481c7204bc3a7ce6fd102f6add5035 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgra...@ubuntu.com>
Date: Fri, 17 Jun 2016 15:47:04 -0400
Subject: [PATCH] Implement the PATCH method
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #1941

Signed-off-by: Stéphane Graber <stgra...@ubuntu.com>
---
 doc/api_extensions.md  |   5 +++
 doc/rest-api.md        |  94 ++++++++++++++++++++++++++++++++++++---
 lxd/api_1.0.go         |  38 +++++++++++++++-
 lxd/certificates.go    |  20 +--------
 lxd/container_patch.go | 108 ++++++++++++++++++++++++++++++++++++++++++++
 lxd/containers.go      |   1 +
 lxd/daemon.go          |   5 +++
 lxd/images.go          | 118 ++++++++++++++++++++++++++++++++++++++++++++++++-
 lxd/profiles.go        |  71 ++++++++++++++++++++++++++++-
 9 files changed, 431 insertions(+), 29 deletions(-)
 create mode 100644 lxd/container_patch.go

diff --git a/doc/api_extensions.md b/doc/api_extensions.md
index 567c1a6..fc0ea73 100644
--- a/doc/api_extensions.md
+++ b/doc/api_extensions.md
@@ -59,3 +59,8 @@ And adds support for the following HTTP header on PUT 
requests:
 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.
+
+## patch
+Add support for the HTTP PATCH method.
+
+PATCH allows for partial update of an object in place of PUT.
diff --git a/doc/rest-api.md b/doc/rest-api.md
index b617b35..3db16a2 100644
--- a/doc/rest-api.md
+++ b/doc/rest-api.md
@@ -258,7 +258,7 @@ Return value (if guest or untrusted):
     }
 
 ### PUT (ETag supported)
- * Description: Updates the server configuration or other properties
+ * Description: Replaces the server configuration or other properties
  * Authentication: trusted
  * Operation: sync
  * Return: standard return value or standard error
@@ -272,6 +272,20 @@ Input (replaces any existing config with the provided one):
         }
     }
 
+### PATCH (ETag supported)
+ * Description: Updates the server configuration or other properties
+ * Authentication: trusted
+ * Operation: sync
+ * Return: standard return value or standard error
+
+Input (updates only the listed keys, rest remains intact):
+
+    {
+        "config": {
+            "core.trust_password": "my-new-password"
+        }
+    }
+
 ## /1.0/certificates
 ### GET
  * Description: list of trusted certificates
@@ -558,7 +572,7 @@ Output:
 
 
 ### PUT (ETag supported)
- * Description: update container configuration or restore snapshot
+ * Description: replaces container configuration or restore snapshot
  * Authentication: trusted
  * Operation: async
  * Return: background operation or standard error
@@ -595,6 +609,27 @@ Input (restore snapshot):
         "restore": "snapshot-name"
     }
 
+### PATCH
+ * Description: update container configuration
+ * Authentication: trusted
+ * Operation: sync
+ * Return: standard return value or standard error
+
+Input:
+
+    {
+        "config": {
+            "limits.cpu": "4"
+        },
+        "devices": {
+            "rootfs": {
+                "size": "5GB"
+            }
+        },
+        "ephemeral": true
+    }
+
+
 ### POST
  * Description: used to rename/migrate the container
  * Authentication: trusted
@@ -1232,7 +1267,7 @@ Input (none at present):
 HTTP code for this should be 202 (Accepted).
 
 ### PUT (ETag supported)
- * Description: Updates the image properties
+ * Description: Replaces the image properties, update information and 
visibility
  * Authentication: trusted
  * Operation: sync
  * Return: standard return value or standard error
@@ -1250,6 +1285,22 @@ Input:
         "public": true,
     }
 
+### PATCH
+ * Description: Updates the image properties, update information and visibility
+ * Authentication: trusted
+ * Operation: sync
+ * Return: standard return value or standard error
+
+Input:
+
+    {
+        "properties": {
+            "os": "ubuntu",
+            "release": "trusty"
+        },
+        "public": true,
+    }
+
 ## /1.0/images/\<fingerprint\>/export
 ### GET (optional ?secret=SECRET)
  * Description: Download the image tarball
@@ -1336,7 +1387,7 @@ Output:
     }
 
 ### PUT (ETag supported)
- * Description: Updates the alias target or description
+ * Description: Replaces the alias target or description
  * Authentication: trusted
  * Operation: sync
  * Return: standard return value or standard error
@@ -1348,6 +1399,19 @@ Input:
         "target": 
"54c8caac1f61901ed86c68f24af5f5d3672bdc62c71d04f06df3a59e95684473"
     }
 
+### PATCH
+ * Description: Updates the alias target or description
+ * Authentication: trusted
+ * Operation: sync
+ * Return: standard return value or standard error
+
+Input:
+
+    {
+        "description": "New description"
+    }
+
+
 ### POST
  * Description: rename an alias
  * Authentication: trusted
@@ -1538,7 +1602,7 @@ Output:
     }
 
 ### PUT (ETag supported)
- * Description: update the profile
+ * Description: replace the profile information
  * Authentication: trusted
  * Operation: sync
  * Return: standard return value or standard error
@@ -1561,6 +1625,26 @@ Input:
 Same dict as used for initial creation and coming from GET. The name
 property can't be changed (see POST for that).
 
+### PATCH
+ * Description: update the profile information
+ * Authentication: trusted
+ * Operation: sync
+ * Return: standard return value or standard error
+
+Input:
+
+    {
+        "config": {
+            "limits.memory": "4GB"
+        },
+        "description": "Some description string",
+        "devices": {
+            "kvm": {
+                "path": "/dev/kvm",
+                "type": "unix-char"
+            }
+        }
+    }
 
 ### POST
  * Description: rename a profile
diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go
index 45306ec..ea46212 100644
--- a/lxd/api_1.0.go
+++ b/lxd/api_1.0.go
@@ -172,6 +172,40 @@ func api10Put(d *Daemon, r *http.Request) Response {
                return BadRequest(err)
        }
 
+       return doApi10Update(d, oldConfig, req)
+}
+
+func api10Patch(d *Daemon, r *http.Request) Response {
+       oldConfig, err := dbConfigValuesGet(d.db)
+       if err != nil {
+               return InternalError(err)
+       }
+
+       err = etagCheck(r, oldConfig)
+       if err != nil {
+               return PreconditionFailed(err)
+       }
+
+       req := apiPut{}
+       if err := shared.ReadToJSON(r.Body, &req); err != nil {
+               return BadRequest(err)
+       }
+
+       if req.Config == nil {
+               return EmptySyncResponse
+       }
+
+       for k, v := range oldConfig {
+               _, ok := req.Config[k]
+               if !ok {
+                       req.Config[k] = v
+               }
+       }
+
+       return doApi10Update(d, oldConfig, req)
+}
+
+func doApi10Update(d *Daemon, oldConfig map[string]string, req apiPut) 
Response {
        // Deal with special keys
        for k, v := range req.Config {
                config := daemonConfig[k]
@@ -213,11 +247,11 @@ func api10Put(d *Daemon, r *http.Request) Response {
 
                err := confKey.Set(d, value)
                if err != nil {
-                       return BadRequest(err)
+                       return SmartError(err)
                }
        }
 
        return EmptySyncResponse
 }
 
-var api10Cmd = Command{name: "", untrustedGet: true, get: api10Get, put: 
api10Put}
+var api10Cmd = Command{name: "", untrustedGet: true, get: api10Get, put: 
api10Put, patch: api10Patch}
diff --git a/lxd/certificates.go b/lxd/certificates.go
index 021b48f..c9dec92 100644
--- a/lxd/certificates.go
+++ b/lxd/certificates.go
@@ -158,15 +158,7 @@ func certificatesPost(d *Daemon, r *http.Request) Response 
{
        return SyncResponseLocation(true, nil, 
fmt.Sprintf("/%s/certificates/%s", shared.APIVersion, fingerprint))
 }
 
-var certificatesCmd = Command{
-       "certificates",
-       false,
-       true,
-       certificatesGet,
-       nil,
-       certificatesPost,
-       nil,
-}
+var certificatesCmd = Command{name: "certificates", untrustedPost: true, get: 
certificatesGet, post: certificatesPost}
 
 func certificateFingerprintGet(d *Daemon, r *http.Request) Response {
        fingerprint := mux.Vars(r)["fingerprint"]
@@ -215,12 +207,4 @@ func certificateFingerprintDelete(d *Daemon, r 
*http.Request) Response {
        return EmptySyncResponse
 }
 
-var certificateFingerprintCmd = Command{
-       "certificates/{fingerprint}",
-       false,
-       false,
-       certificateFingerprintGet,
-       nil,
-       nil,
-       certificateFingerprintDelete,
-}
+var certificateFingerprintCmd = Command{name: "certificates/{fingerprint}", 
get: certificateFingerprintGet, delete: certificateFingerprintDelete}
diff --git a/lxd/container_patch.go b/lxd/container_patch.go
new file mode 100644
index 0000000..ede550f
--- /dev/null
+++ b/lxd/container_patch.go
@@ -0,0 +1,108 @@
+package main
+
+import (
+       "bytes"
+       "encoding/json"
+       "fmt"
+       "io/ioutil"
+       "net/http"
+
+       "github.com/gorilla/mux"
+       "github.com/lxc/lxd/shared"
+)
+
+func containerPatch(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)
+       }
+
+       body, _ := ioutil.ReadAll(r.Body)
+       rdr1 := ioutil.NopCloser(bytes.NewBuffer(body))
+       rdr2 := ioutil.NopCloser(bytes.NewBuffer(body))
+
+       reqRaw := shared.Jmap{}
+       if err := json.NewDecoder(rdr1).Decode(&reqRaw); err != nil {
+               return BadRequest(err)
+       }
+
+       req := containerPutReq{}
+       if err := json.NewDecoder(rdr2).Decode(&req); err != nil {
+               return BadRequest(err)
+       }
+
+       if req.Restore != "" {
+               return BadRequest(fmt.Errorf("Can't call PATCH in restore 
mode."))
+       }
+
+       // Check if architecture was passed
+       var architecture int
+       _, err = reqRaw.GetString("architecture")
+       if err != nil {
+               architecture = c.Architecture()
+       } else {
+               architecture, err = shared.ArchitectureId(req.Architecture)
+               if err != nil {
+                       architecture = 0
+               }
+       }
+
+       // Check if ephemeral was passed
+       _, err = reqRaw.GetBool("ephemeral")
+       if err != nil {
+               req.Ephemeral = c.IsEphemeral()
+       }
+
+       // Check if profiles was passed
+       if req.Profiles == nil {
+               req.Profiles = c.Profiles()
+       }
+
+       // Check if config was passed
+       if req.Config == nil {
+               req.Config = c.LocalConfig()
+       } else {
+               for k, v := range c.LocalConfig() {
+                       _, ok := req.Config[k]
+                       if !ok {
+                               req.Config[k] = v
+                       }
+               }
+       }
+
+       // Check if devices was passed
+       if req.Devices == nil {
+               req.Devices = c.LocalDevices()
+       } else {
+               for k, v := range c.LocalDevices() {
+                       _, ok := req.Devices[k]
+                       if !ok {
+                               req.Devices[k] = v
+                       }
+               }
+       }
+
+       // Update container configuration
+       args := containerArgs{
+               Architecture: architecture,
+               Config:       req.Config,
+               Devices:      req.Devices,
+               Ephemeral:    req.Ephemeral,
+               Profiles:     req.Profiles}
+
+       err = c.Update(args, false)
+       if err != nil {
+               return SmartError(err)
+       }
+
+       return EmptySyncResponse
+}
diff --git a/lxd/containers.go b/lxd/containers.go
index 803fb0c..15c6526 100644
--- a/lxd/containers.go
+++ b/lxd/containers.go
@@ -29,6 +29,7 @@ var containerCmd = Command{
        put:    containerPut,
        delete: containerDelete,
        post:   containerPost,
+       patch:  containerPatch,
 }
 
 var containerStateCmd = Command{
diff --git a/lxd/daemon.go b/lxd/daemon.go
index d418b45..37a2338 100644
--- a/lxd/daemon.go
+++ b/lxd/daemon.go
@@ -103,6 +103,7 @@ type Command struct {
        put           func(d *Daemon, r *http.Request) Response
        post          func(d *Daemon, r *http.Request) Response
        delete        func(d *Daemon, r *http.Request) Response
+       patch         func(d *Daemon, r *http.Request) Response
 }
 
 func (d *Daemon) httpGetSync(url string, certificate string) (*lxd.Response, 
error) {
@@ -328,6 +329,10 @@ func (d *Daemon) createCmd(version string, c Command) {
                        if c.delete != nil {
                                resp = c.delete(d, r)
                        }
+               case "PATCH":
+                       if c.patch != nil {
+                               resp = c.patch(d, r)
+                       }
                default:
                        resp = NotFound
                }
diff --git a/lxd/images.go b/lxd/images.go
index b5d8e31..a041fa8 100644
--- a/lxd/images.go
+++ b/lxd/images.go
@@ -1054,7 +1054,69 @@ func imagePut(d *Daemon, r *http.Request) Response {
        return EmptySyncResponse
 }
 
-var imageCmd = Command{name: "images/{fingerprint}", untrustedGet: true, get: 
imageGet, put: imagePut, delete: imageDelete}
+func imagePatch(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)
+       }
+
+       body, _ := ioutil.ReadAll(r.Body)
+       rdr1 := ioutil.NopCloser(bytes.NewBuffer(body))
+       rdr2 := ioutil.NopCloser(bytes.NewBuffer(body))
+
+       reqRaw := shared.Jmap{}
+       if err := json.NewDecoder(rdr1).Decode(&reqRaw); err != nil {
+               return BadRequest(err)
+       }
+
+       req := imagePutReq{}
+       if err := json.NewDecoder(rdr2).Decode(&req); err != nil {
+               return BadRequest(err)
+       }
+
+       // Get AutoUpdate
+       autoUpdate, err := reqRaw.GetBool("auto_update")
+       if err == nil {
+               info.AutoUpdate = autoUpdate
+       }
+
+       // Get Public
+       public, err := reqRaw.GetBool("public")
+       if err == nil {
+               info.Public = public
+       }
+
+       // Get Properties
+       _, ok := reqRaw["properties"]
+       if ok {
+               properties := req.Properties
+               for k, v := range info.Properties {
+                       _, ok := req.Properties[k]
+                       if !ok {
+                               properties[k] = v
+                       }
+               }
+               info.Properties = properties
+       }
+
+       err = dbImageUpdate(d.db, id, info.Filename, info.Size, info.Public, 
info.AutoUpdate, info.Architecture, info.CreationDate, info.ExpiryDate, 
info.Properties)
+       if err != nil {
+               return SmartError(err)
+       }
+
+       return EmptySyncResponse
+}
+
+var imageCmd = Command{name: "images/{fingerprint}", untrustedGet: true, get: 
imageGet, put: imagePut, delete: imageDelete, patch: imagePatch}
 
 type aliasPostReq struct {
        Name        string `json:"name"`
@@ -1193,6 +1255,58 @@ func aliasPut(d *Daemon, r *http.Request) Response {
        return EmptySyncResponse
 }
 
+func aliasPatch(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 := shared.Jmap{}
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               return BadRequest(err)
+       }
+
+       _, ok := req["target"]
+       if ok {
+               target, err := req.GetString("target")
+               if err != nil {
+                       return BadRequest(err)
+               }
+
+               alias.Target = target
+       }
+
+       _, ok = req["description"]
+       if ok {
+               description, err := req.GetString("description")
+               if err != nil {
+                       return BadRequest(err)
+               }
+
+               alias.Description = description
+       }
+
+       imageId, _, err := dbImageGet(d.db, alias.Target, false, false)
+       if err != nil {
+               return SmartError(err)
+       }
+
+       err = dbImageAliasUpdate(d.db, id, imageId, alias.Description)
+       if err != nil {
+               return SmartError(err)
+       }
+
+       return EmptySyncResponse
+}
+
 func aliasPost(d *Daemon, r *http.Request) Response {
        name := mux.Vars(r)["name"]
 
@@ -1294,4 +1408,4 @@ var imagesSecretCmd = Command{name: 
"images/{fingerprint}/secret", post: imageSe
 
 var aliasesCmd = Command{name: "images/aliases", post: aliasesPost, get: 
aliasesGet}
 
-var aliasCmd = Command{name: "images/aliases/{name:.*}", untrustedGet: true, 
get: aliasGet, delete: aliasDelete, put: aliasPut, post: aliasPost}
+var aliasCmd = Command{name: "images/aliases/{name:.*}", untrustedGet: true, 
get: aliasGet, delete: aliasDelete, put: aliasPut, post: aliasPost, patch: 
aliasPatch}
diff --git a/lxd/profiles.go b/lxd/profiles.go
index 13ac836..0b86688 100644
--- a/lxd/profiles.go
+++ b/lxd/profiles.go
@@ -1,8 +1,10 @@
 package main
 
 import (
+       "bytes"
        "encoding/json"
        "fmt"
+       "io/ioutil"
        "net/http"
        "reflect"
 
@@ -145,8 +147,73 @@ func profilePut(d *Daemon, r *http.Request) Response {
                return BadRequest(err)
        }
 
+       return doProfileUpdate(d, name, id, profile, req)
+}
+
+func profilePatch(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)
+       }
+
+       body, _ := ioutil.ReadAll(r.Body)
+       rdr1 := ioutil.NopCloser(bytes.NewBuffer(body))
+       rdr2 := ioutil.NopCloser(bytes.NewBuffer(body))
+
+       reqRaw := shared.Jmap{}
+       if err := json.NewDecoder(rdr1).Decode(&reqRaw); err != nil {
+               return BadRequest(err)
+       }
+
+       req := profilesPostReq{}
+       if err := json.NewDecoder(rdr2).Decode(&req); err != nil {
+               return BadRequest(err)
+       }
+
+       // Get Description
+       _, err = reqRaw.GetString("description")
+       if err != nil {
+               req.Description = profile.Description
+       }
+
+       // Get Config
+       if req.Config == nil {
+               req.Config = profile.Config
+       } else {
+               for k, v := range profile.Config {
+                       _, ok := req.Config[k]
+                       if !ok {
+                               req.Config[k] = v
+                       }
+               }
+       }
+
+       // Get Devices
+       if req.Devices == nil {
+               req.Devices = profile.Devices
+       } else {
+               for k, v := range profile.Devices {
+                       _, ok := req.Devices[k]
+                       if !ok {
+                               req.Devices[k] = v
+                       }
+               }
+       }
+
+       return doProfileUpdate(d, name, id, profile, req)
+}
+
+func doProfileUpdate(d *Daemon, name string, id int64, profile 
*shared.ProfileConfig, req profilesPostReq) Response {
        // Sanity checks
-       err = containerValidConfig(d, req.Config, true, false)
+       err := containerValidConfig(d, req.Config, true, false)
        if err != nil {
                return BadRequest(err)
        }
@@ -279,4 +346,4 @@ func profileDelete(d *Daemon, r *http.Request) Response {
        return EmptySyncResponse
 }
 
-var profileCmd = Command{name: "profiles/{name}", get: profileGet, put: 
profilePut, delete: profileDelete, post: profilePost}
+var profileCmd = Command{name: "profiles/{name}", get: profileGet, put: 
profilePut, delete: profileDelete, post: profilePost, patch: profilePatch}
_______________________________________________
lxc-devel mailing list
lxc-devel@lists.linuxcontainers.org
http://lists.linuxcontainers.org/listinfo/lxc-devel

Reply via email to