[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