diff --git a/doc/metadata.txt b/doc/metadata.txt index cc0167094c00..ff12f8a61978 100644 --- a/doc/metadata.txt +++ b/doc/metadata.txt @@ -2112,7 +2112,6 @@ See {ref}`dev-lxd` for more information. ``` ```{config:option} security.devlxd.images instance-security -:condition: "container" :defaultdesc: "`false`" :liveupdate: "no" :shortdesc: "Controls the availability of the `/1.0/images` API over `devlxd`" diff --git a/lxd-agent/devlxd.go b/lxd-agent/devlxd.go index c22910de0e1a..6922e8903ddd 100644 --- a/lxd-agent/devlxd.go +++ b/lxd-agent/devlxd.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io" "net" "net/http" "net/url" @@ -18,8 +19,11 @@ import ( "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" "github.com/canonical/lxd/shared/logger" + "github.com/canonical/lxd/shared/version" ) +type devLXDHandlerFunc func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse + // DevLxdServer creates an http.Server capable of handling requests against the // /dev/lxd Unix socket endpoint created inside VMs. func devLxdServer(d *Daemon) *http.Server { @@ -37,7 +41,7 @@ type devLxdHandler struct { * server side right now either, I went the simple route to avoid * needless noise. */ - f func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse + handlerFunc devLXDHandlerFunc } func getVsockClient(d *Daemon) (lxd.InstanceServer, error) { @@ -55,7 +59,12 @@ func getVsockClient(d *Daemon) (lxd.InstanceServer, error) { return server, nil } -var devlxdConfigGet = devLxdHandler{"/1.0/config", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { +var devlxdConfigGet = devLxdHandler{ + path: "/1.0/config", + handlerFunc: devlxdConfigGetHandler, +} + +func devlxdConfigGetHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { client, err := getVsockClient(d) if err != nil { return smartResponse(fmt.Errorf("Failed connecting to LXD over vsock: %w", err)) @@ -82,9 +91,14 @@ var devlxdConfigGet = devLxdHandler{"/1.0/config", func(d *Daemon, w http.Respon } } return okResponse(filtered, "json") -}} +} + +var devlxdConfigKeyGet = devLxdHandler{ + path: "/1.0/config/{key}", + handlerFunc: devlxdConfigKeyGetHandler, +} -var devlxdConfigKeyGet = devLxdHandler{"/1.0/config/{key}", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { +func devlxdConfigKeyGetHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { key, err := url.PathUnescape(mux.Vars(r)["key"]) if err != nil { return &devLxdResponse{"bad request", http.StatusBadRequest, "raw"} @@ -114,9 +128,14 @@ var devlxdConfigKeyGet = devLxdHandler{"/1.0/config/{key}", func(d *Daemon, w ht } return okResponse(value, "raw") -}} +} + +var devlxdMetadataGet = devLxdHandler{ + path: "/1.0/meta-data", + handlerFunc: devlxdMetadataGetHandler, +} -var devlxdMetadataGet = devLxdHandler{"/1.0/meta-data", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { +func devlxdMetadataGetHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { var client lxd.InstanceServer var err error @@ -148,18 +167,28 @@ var devlxdMetadataGet = devLxdHandler{"/1.0/meta-data", func(d *Daemon, w http.R } return okResponse(metaData, "raw") -}} +} + +var devLxdEventsGet = devLxdHandler{ + path: "/1.0/events", + handlerFunc: devlxdEventsGetHandler, +} -var devLxdEventsGet = devLxdHandler{"/1.0/events", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { +func devlxdEventsGetHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { err := eventsGet(d, r).Render(w) if err != nil { return smartResponse(err) } return okResponse("", "raw") -}} +} + +var devlxdAPIGet = devLxdHandler{ + path: "/1.0", + handlerFunc: devlxdAPIGetHandler, +} -var devlxdAPIGet = devLxdHandler{"/1.0", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { +func devlxdAPIGetHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { client, err := getVsockClient(d) if err != nil { return smartResponse(fmt.Errorf("Failed connecting to LXD over vsock: %w", err)) @@ -191,9 +220,14 @@ var devlxdAPIGet = devLxdHandler{"/1.0", func(d *Daemon, w http.ResponseWriter, } return &devLxdResponse{fmt.Sprintf("method %q not allowed", r.Method), http.StatusBadRequest, "raw"} -}} +} + +var devlxdDevicesGet = devLxdHandler{ + path: "/1.0/devices", + handlerFunc: devlxdDevicesGetHandler, +} -var devlxdDevicesGet = devLxdHandler{"/1.0/devices", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { +func devlxdDevicesGetHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { client, err := getVsockClient(d) if err != nil { return smartResponse(fmt.Errorf("Failed connecting to LXD over vsock: %w", err)) @@ -214,23 +248,79 @@ var devlxdDevicesGet = devLxdHandler{"/1.0/devices", func(d *Daemon, w http.Resp } return okResponse(devices, "json") -}} +} + +var devlxdImageExport = devLxdHandler{ + path: "/1.0/images/{fingerprint}/export", + handlerFunc: devlxdImageExportHandler, +} + +func devlxdImageExportHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { + // Extract the fingerprint. + fingerprint, err := url.PathUnescape(mux.Vars(r)["fingerprint"]) + if err != nil { + return smartResponse(err) + } + + // Get a http.Client. + client, err := getClient(d.serverCID, int(d.serverPort), d.serverCertificate) + if err != nil { + return smartResponse(fmt.Errorf("Failed connecting to LXD over vsock: %w", err)) + } + + // Remove the request URI, this cannot be set on requests. + r.RequestURI = "" + + // Set up the request URL with the correct host. + r.URL = &api.NewURL().Scheme("https").Host("custom.socket").Path(version.APIVersion, "images", fingerprint, "export").URL + + // Proxy the request. + resp, err := client.Do(r) + if err != nil { + return errorResponse(http.StatusInternalServerError, err.Error()) + } + + // Set headers from the host LXD. + for k, vv := range resp.Header { + for _, v := range vv { + w.Header().Set(k, v) + } + } + + // Copy headers and response body. + w.WriteHeader(resp.StatusCode) + _, err = io.Copy(w, resp.Body) + if err != nil { + return smartResponse(err) + } + + return nil +} var handlers = []devLxdHandler{ - {"/", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { - return okResponse([]string{"/1.0"}, "json") - }}, + { + path: "/", + handlerFunc: func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse { + return okResponse([]string{"/1.0"}, "json") + }, + }, devlxdAPIGet, devlxdConfigGet, devlxdConfigKeyGet, devlxdMetadataGet, devLxdEventsGet, devlxdDevicesGet, + devlxdImageExport, } func hoistReq(f func(*Daemon, http.ResponseWriter, *http.Request) *devLxdResponse, d *Daemon) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { resp := f(d, w, r) + if resp == nil { + // The handler has already written the response. + return + } + if resp.code != http.StatusOK { http.Error(w, fmt.Sprintf("%s", resp.content), resp.code) } else if resp.ctype == "json" { @@ -249,7 +339,7 @@ func devLxdAPI(d *Daemon) http.Handler { m.UseEncodedPath() // Allow encoded values in path segments. for _, handler := range handlers { - m.HandleFunc(handler.path, hoistReq(handler.f, d)) + m.HandleFunc(handler.path, hoistReq(handler.handlerFunc, d)) } return m diff --git a/lxd/instance/instancetype/instance.go b/lxd/instance/instancetype/instance.go index a63ced508a77..c57075f5a398 100644 --- a/lxd/instance/instancetype/instance.go +++ b/lxd/instance/instancetype/instance.go @@ -302,6 +302,15 @@ var InstanceConfigKeysAny = map[string]func(value string) error{ // shortdesc: Whether `/dev/lxd` is present in the instance "security.devlxd": validate.Optional(validate.IsBool), + // lxdmeta:generate(entities=instance; group=security; key=security.devlxd.images) + // + // --- + // type: bool + // defaultdesc: `false` + // liveupdate: no + // shortdesc: Controls the availability of the `/1.0/images` API over `devlxd` + "security.devlxd.images": validate.Optional(validate.IsBool), + // lxdmeta:generate(entities=instance; group=security; key=security.protection.delete) // // --- @@ -676,16 +685,6 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{ // shortdesc: Raw Seccomp configuration "raw.seccomp": validate.IsAny, - // lxdmeta:generate(entities=instance; group=security; key=security.devlxd.images) - // - // --- - // type: bool - // defaultdesc: `false` - // liveupdate: no - // condition: container - // shortdesc: Controls the availability of the `/1.0/images` API over `devlxd` - "security.devlxd.images": validate.Optional(validate.IsBool), - // lxdmeta:generate(entities=instance; group=security; key=security.idmap.base) // Setting this option overrides auto-detection. // --- diff --git a/lxd/metadata/configuration.json b/lxd/metadata/configuration.json index ec38ceab98fe..2f948ed3eefc 100644 --- a/lxd/metadata/configuration.json +++ b/lxd/metadata/configuration.json @@ -2392,7 +2392,6 @@ }, { "security.devlxd.images": { - "condition": "container", "defaultdesc": "`false`", "liveupdate": "no", "longdesc": "",