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