[lxc-devel] [lxd/master] Implement the PATCH method

stgraber on Github lxc-bot at linuxcontainers.org
Tue Jun 21 22:28:45 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 370 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160621/2091a4c0/attachment.bin>
-------------- next part --------------
From d7a6b8ab42481c7204bc3a7ce6fd102f6add5035 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at 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 <stgraber at 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}


More information about the lxc-devel mailing list