mirror of
https://github.com/lxc/incus
synced 2026-08-02 05:26:46 +00:00
838 lines
21 KiB
Go
838 lines
21 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"go.yaml.in/yaml/v4"
|
|
|
|
internalInstance "github.com/lxc/incus/v7/internal/instance"
|
|
"github.com/lxc/incus/v7/internal/server/instance"
|
|
"github.com/lxc/incus/v7/internal/server/lifecycle"
|
|
"github.com/lxc/incus/v7/internal/server/request"
|
|
"github.com/lxc/incus/v7/internal/server/response"
|
|
"github.com/lxc/incus/v7/internal/server/state"
|
|
storagePools "github.com/lxc/incus/v7/internal/server/storage"
|
|
localUtil "github.com/lxc/incus/v7/internal/server/util"
|
|
"github.com/lxc/incus/v7/shared/api"
|
|
"github.com/lxc/incus/v7/shared/logger"
|
|
"github.com/lxc/incus/v7/shared/util"
|
|
)
|
|
|
|
// swagger:operation GET /1.0/instances/{name}/metadata instances instance_metadata_get
|
|
//
|
|
// Get the instance image metadata
|
|
//
|
|
// Gets the image metadata for the instance.
|
|
//
|
|
// ---
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - in: path
|
|
// name: name
|
|
// description: Instance name
|
|
// type: string
|
|
// required: true
|
|
// - in: query
|
|
// name: project
|
|
// description: Project name
|
|
// type: string
|
|
// example: default
|
|
// responses:
|
|
// "200":
|
|
// description: Image metadata
|
|
// schema:
|
|
// type: object
|
|
// description: Sync response
|
|
// properties:
|
|
// type:
|
|
// type: string
|
|
// description: Response type
|
|
// example: sync
|
|
// status:
|
|
// type: string
|
|
// description: Status description
|
|
// example: Success
|
|
// status_code:
|
|
// type: integer
|
|
// description: Status code
|
|
// example: 200
|
|
// metadata:
|
|
// $ref: "#/definitions/ImageMetadata"
|
|
// "400":
|
|
// $ref: "#/responses/BadRequest"
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "404":
|
|
// $ref: "#/responses/NotFound"
|
|
// "409":
|
|
// $ref: "#/responses/Conflict"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
func instanceMetadataGet(d *Daemon, r *http.Request) response.Response {
|
|
s := d.State()
|
|
|
|
projectName := request.ProjectParam(r)
|
|
name, err := pathVar(r, "name")
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
if internalInstance.IsSnapshot(name) {
|
|
return response.BadRequest(errors.New("Invalid instance name"))
|
|
}
|
|
|
|
// Handle requests targeted to a container on a different node
|
|
resp, err := forwardedResponseIfInstanceIsRemote(s, r, projectName, name)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
if resp != nil {
|
|
return resp
|
|
}
|
|
|
|
// Load the container
|
|
c, err := instance.LoadByProjectAndName(s, projectName, name)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Start the storage if needed
|
|
pool, err := storagePools.LoadByInstance(s, c)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
_, err = storagePools.InstanceMount(pool, c, nil)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(func() error { return storagePools.InstanceUnmount(pool, c, nil) }, "Failed to unmount instance")
|
|
|
|
// Confine metadata access to the instance directory.
|
|
root, err := os.OpenRoot(c.Path())
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(root.Close, "Failed to close instance root")
|
|
|
|
// Read the metadata (if missing, just return empty result).
|
|
metadataFile, err := root.Open("metadata.yaml")
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return response.SyncResponse(true, api.ImageMetadata{})
|
|
}
|
|
|
|
return response.InternalError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(metadataFile.Close, "Failed to close metadata file")
|
|
|
|
data, err := io.ReadAll(metadataFile)
|
|
if err != nil {
|
|
return response.InternalError(err)
|
|
}
|
|
|
|
// Parse into the API struct
|
|
metadata := api.ImageMetadata{}
|
|
err = yaml.Load(data, &metadata)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
s.Events.SendLifecycle(projectName, lifecycle.InstanceMetadataRetrieved.Event(c, request.CreateRequestor(r), nil))
|
|
|
|
return response.SyncResponseETag(true, metadata, metadata)
|
|
}
|
|
|
|
// swagger:operation PATCH /1.0/instances/{name}/metadata instances instance_metadata_patch
|
|
//
|
|
// Partially update the image metadata
|
|
//
|
|
// Updates a subset of the instance image metadata.
|
|
//
|
|
// ---
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - in: path
|
|
// name: name
|
|
// description: Instance name
|
|
// type: string
|
|
// required: true
|
|
// - in: query
|
|
// name: project
|
|
// description: Project name
|
|
// type: string
|
|
// example: default
|
|
// - in: body
|
|
// name: metadata
|
|
// description: Image metadata
|
|
// required: true
|
|
// schema:
|
|
// $ref: "#/definitions/ImageMetadata"
|
|
// responses:
|
|
// "200":
|
|
// $ref: "#/responses/EmptySyncResponse"
|
|
// "400":
|
|
// $ref: "#/responses/BadRequest"
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "404":
|
|
// $ref: "#/responses/NotFound"
|
|
// "409":
|
|
// $ref: "#/responses/Conflict"
|
|
// "412":
|
|
// $ref: "#/responses/PreconditionFailed"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
func instanceMetadataPatch(d *Daemon, r *http.Request) response.Response {
|
|
s := d.State()
|
|
|
|
projectName := request.ProjectParam(r)
|
|
name, err := pathVar(r, "name")
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
if internalInstance.IsSnapshot(name) {
|
|
return response.BadRequest(errors.New("Invalid instance name"))
|
|
}
|
|
|
|
// Handle requests targeted to an instance on a different node.
|
|
resp, err := forwardedResponseIfInstanceIsRemote(s, r, projectName, name)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
if resp != nil {
|
|
return resp
|
|
}
|
|
|
|
// Load the instance.
|
|
inst, err := instance.LoadByProjectAndName(s, projectName, name)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Start the storage if needed.
|
|
pool, err := storagePools.LoadByInstance(s, inst)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
_, err = storagePools.InstanceMount(pool, inst, nil)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(func() error { return storagePools.InstanceUnmount(pool, inst, nil) }, "Failed to unmount instance")
|
|
|
|
// Confine metadata access to the instance directory.
|
|
root, err := os.OpenRoot(inst.Path())
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(root.Close, "Failed to close instance root")
|
|
|
|
// Read the existing data.
|
|
metadata := api.ImageMetadata{}
|
|
metadataFile, err := root.Open("metadata.yaml")
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return response.InternalError(err)
|
|
}
|
|
|
|
if err == nil {
|
|
defer logger.WarnOnError(metadataFile.Close, "Failed to close metadata file")
|
|
|
|
data, err := io.ReadAll(metadataFile)
|
|
if err != nil {
|
|
return response.InternalError(err)
|
|
}
|
|
|
|
// Parse into the API struct
|
|
err = yaml.Load(data, &metadata)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
}
|
|
|
|
// Validate ETag
|
|
err = localUtil.EtagCheck(r, metadata)
|
|
if err != nil {
|
|
return response.PreconditionFailed(err)
|
|
}
|
|
|
|
// Apply the new metadata on top.
|
|
err = json.NewDecoder(r.Body).Decode(&metadata)
|
|
if err != nil {
|
|
return response.BadRequest(err)
|
|
}
|
|
|
|
// Update the file.
|
|
return doInstanceMetadataUpdate(s, inst, metadata, r)
|
|
}
|
|
|
|
// swagger:operation PUT /1.0/instances/{name}/metadata instances instance_metadata_put
|
|
//
|
|
// Update the image metadata
|
|
//
|
|
// Updates the instance image metadata.
|
|
//
|
|
// ---
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - in: path
|
|
// name: name
|
|
// description: Instance name
|
|
// type: string
|
|
// required: true
|
|
// - in: query
|
|
// name: project
|
|
// description: Project name
|
|
// type: string
|
|
// example: default
|
|
// - in: body
|
|
// name: metadata
|
|
// description: Image metadata
|
|
// required: true
|
|
// schema:
|
|
// $ref: "#/definitions/ImageMetadata"
|
|
// responses:
|
|
// "200":
|
|
// $ref: "#/responses/EmptySyncResponse"
|
|
// "400":
|
|
// $ref: "#/responses/BadRequest"
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "404":
|
|
// $ref: "#/responses/NotFound"
|
|
// "409":
|
|
// $ref: "#/responses/Conflict"
|
|
// "412":
|
|
// $ref: "#/responses/PreconditionFailed"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
func instanceMetadataPut(d *Daemon, r *http.Request) response.Response {
|
|
s := d.State()
|
|
|
|
projectName := request.ProjectParam(r)
|
|
name, err := pathVar(r, "name")
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
if internalInstance.IsSnapshot(name) {
|
|
return response.BadRequest(errors.New("Invalid instance name"))
|
|
}
|
|
|
|
// Handle requests targeted to an instance on a different node.
|
|
resp, err := forwardedResponseIfInstanceIsRemote(s, r, projectName, name)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
if resp != nil {
|
|
return resp
|
|
}
|
|
|
|
// Read the new metadata.
|
|
metadata := api.ImageMetadata{}
|
|
err = json.NewDecoder(r.Body).Decode(&metadata)
|
|
if err != nil {
|
|
return response.BadRequest(err)
|
|
}
|
|
|
|
// Load the instance.
|
|
inst, err := instance.LoadByProjectAndName(s, projectName, name)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Start the storage if needed.
|
|
pool, err := storagePools.LoadByInstance(s, inst)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
_, err = storagePools.InstanceMount(pool, inst, nil)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(func() error { return storagePools.InstanceUnmount(pool, inst, nil) }, "Failed to unmount instance")
|
|
|
|
return doInstanceMetadataUpdate(s, inst, metadata, r)
|
|
}
|
|
|
|
func doInstanceMetadataUpdate(s *state.State, inst instance.Instance, metadata api.ImageMetadata, r *http.Request) response.Response {
|
|
// Convert YAML.
|
|
data, err := yaml.Dump(metadata, yaml.WithV2Defaults())
|
|
if err != nil {
|
|
return response.BadRequest(err)
|
|
}
|
|
|
|
// Update the metadata (confined to the instance directory).
|
|
root, err := os.OpenRoot(inst.Path())
|
|
if err != nil {
|
|
return response.InternalError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(root.Close, "Failed to close instance root")
|
|
|
|
err = root.WriteFile("metadata.yaml", data, 0o644)
|
|
if err != nil {
|
|
return response.InternalError(err)
|
|
}
|
|
|
|
s.Events.SendLifecycle(inst.Project().Name, lifecycle.InstanceMetadataUpdated.Event(inst, request.CreateRequestor(r), nil))
|
|
|
|
return response.EmptySyncResponse
|
|
}
|
|
|
|
// swagger:operation GET /1.0/instances/{name}/metadata/templates instances instance_metadata_templates_get
|
|
//
|
|
// Get the template file names or a specific
|
|
//
|
|
// If no path specified, returns a list of template file names.
|
|
// If a path is specified, returns the file content.
|
|
//
|
|
// ---
|
|
// produces:
|
|
// - application/json
|
|
// - application/octet-stream
|
|
// parameters:
|
|
// - in: path
|
|
// name: name
|
|
// description: Instance name
|
|
// type: string
|
|
// required: true
|
|
// - in: query
|
|
// name: project
|
|
// description: Project name
|
|
// type: string
|
|
// example: default
|
|
// - in: query
|
|
// name: path
|
|
// description: Template name
|
|
// type: string
|
|
// example: hostname.tpl
|
|
// responses:
|
|
// "200":
|
|
// description: Raw template file or file listing
|
|
// content:
|
|
// application/octet-stream:
|
|
// schema:
|
|
// type: string
|
|
// example: some-text
|
|
// application/json:
|
|
// schema:
|
|
// type: array
|
|
// items:
|
|
// type: string
|
|
// example: |-
|
|
// [
|
|
// "hostname.tpl",
|
|
// "hosts.tpl"
|
|
// ]
|
|
// "400":
|
|
// $ref: "#/responses/BadRequest"
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "404":
|
|
// $ref: "#/responses/NotFound"
|
|
// "409":
|
|
// $ref: "#/responses/Conflict"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
func instanceMetadataTemplatesGet(d *Daemon, r *http.Request) response.Response {
|
|
s := d.State()
|
|
|
|
projectName := request.ProjectParam(r)
|
|
name, err := pathVar(r, "name")
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
if internalInstance.IsSnapshot(name) {
|
|
return response.BadRequest(errors.New("Invalid instance name"))
|
|
}
|
|
|
|
// Handle requests targeted to a container on a different node
|
|
resp, err := forwardedResponseIfInstanceIsRemote(s, r, projectName, name)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
if resp != nil {
|
|
return resp
|
|
}
|
|
|
|
// Load the container
|
|
c, err := instance.LoadByProjectAndName(s, projectName, name)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Start the storage if needed
|
|
pool, err := storagePools.LoadByInstance(s, c)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
_, err = storagePools.InstanceMount(pool, c, nil)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(func() error { return storagePools.InstanceUnmount(pool, c, nil) }, "Failed to unmount instance")
|
|
|
|
// Confine all template access to the instance directory.
|
|
root, err := os.OpenRoot(c.Path())
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(root.Close, "Failed to close instance root")
|
|
|
|
// Look at the request
|
|
templateName := r.FormValue("path")
|
|
if templateName == "" {
|
|
templates := []string{}
|
|
|
|
// List templates
|
|
entries, err := fs.ReadDir(root.FS(), "templates")
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return response.SyncResponse(true, templates)
|
|
}
|
|
|
|
return response.InternalError(err)
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
templates = append(templates, entry.Name())
|
|
}
|
|
}
|
|
|
|
return response.SyncResponse(true, templates)
|
|
}
|
|
|
|
// Check if the template exists
|
|
templatePath, err := getContainerTemplatePath(templateName)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Create a temporary file with the template content (since the container
|
|
// storage might not be available when the file is read from FileResponse)
|
|
template, err := root.Open(templatePath)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return response.NotFound(fmt.Errorf("Template %q not found", templateName))
|
|
}
|
|
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(template.Close, "Failed to close template file")
|
|
|
|
tempfile, err := os.CreateTemp("", "incus_template")
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
_, err = util.SafeCopy(tempfile, template)
|
|
if err != nil {
|
|
return response.InternalError(err)
|
|
}
|
|
|
|
err = tempfile.Close()
|
|
if err != nil {
|
|
return response.InternalError(err)
|
|
}
|
|
|
|
files := make([]response.FileResponseEntry, 1)
|
|
files[0].Identifier = templateName
|
|
files[0].Path = tempfile.Name()
|
|
files[0].Filename = templateName
|
|
files[0].Cleanup = func() { _ = os.Remove(tempfile.Name()) }
|
|
|
|
s.Events.SendLifecycle(projectName, lifecycle.InstanceMetadataTemplateRetrieved.Event(c, request.CreateRequestor(r), logger.Ctx{"path": templateName}))
|
|
|
|
return response.FileResponse(r, files, nil)
|
|
}
|
|
|
|
// swagger:operation POST /1.0/instances/{name}/metadata/templates instances instance_metadata_templates_post
|
|
//
|
|
// Create or replace a template file
|
|
//
|
|
// Creates a new image template file for the instance.
|
|
//
|
|
// ---
|
|
// consumes:
|
|
// - application/octet-stream
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - in: path
|
|
// name: name
|
|
// description: Instance name
|
|
// type: string
|
|
// required: true
|
|
// - in: query
|
|
// name: path
|
|
// description: Template name
|
|
// type: string
|
|
// example: default
|
|
// - in: query
|
|
// name: project
|
|
// description: Project name
|
|
// type: string
|
|
// example: default
|
|
// - in: body
|
|
// name: raw_file
|
|
// description: Raw file content
|
|
// responses:
|
|
// "200":
|
|
// $ref: "#/responses/EmptySyncResponse"
|
|
// "400":
|
|
// $ref: "#/responses/BadRequest"
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "404":
|
|
// $ref: "#/responses/NotFound"
|
|
// "409":
|
|
// $ref: "#/responses/Conflict"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
func instanceMetadataTemplatesPost(d *Daemon, r *http.Request) response.Response {
|
|
s := d.State()
|
|
|
|
projectName := request.ProjectParam(r)
|
|
name, err := pathVar(r, "name")
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
if internalInstance.IsSnapshot(name) {
|
|
return response.BadRequest(errors.New("Invalid instance name"))
|
|
}
|
|
|
|
// Handle requests targeted to a container on a different node
|
|
resp, err := forwardedResponseIfInstanceIsRemote(s, r, projectName, name)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
if resp != nil {
|
|
return resp
|
|
}
|
|
|
|
// Load the container
|
|
c, err := instance.LoadByProjectAndName(s, projectName, name)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Start the storage if needed
|
|
pool, err := storagePools.LoadByInstance(s, c)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
_, err = storagePools.InstanceMount(pool, c, nil)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(func() error { return storagePools.InstanceUnmount(pool, c, nil) }, "Failed to unmount instance")
|
|
|
|
// Confine all template access to the instance directory.
|
|
root, err := os.OpenRoot(c.Path())
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(root.Close, "Failed to close instance root")
|
|
|
|
// Look at the request
|
|
templateName := r.FormValue("path")
|
|
if templateName == "" {
|
|
return response.BadRequest(errors.New("missing path argument"))
|
|
}
|
|
|
|
err = root.Mkdir("templates", 0o711)
|
|
if err != nil && !errors.Is(err, fs.ErrExist) {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Check if the template already exists
|
|
templatePath, err := getContainerTemplatePath(templateName)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Write the new template
|
|
template, err := root.OpenFile(templatePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
_, err = util.SafeCopy(template, r.Body)
|
|
if err != nil {
|
|
return response.InternalError(err)
|
|
}
|
|
|
|
err = template.Close()
|
|
if err != nil {
|
|
return response.InternalError(err)
|
|
}
|
|
|
|
s.Events.SendLifecycle(projectName, lifecycle.InstanceMetadataTemplateCreated.Event(c, request.CreateRequestor(r), logger.Ctx{"path": templateName}))
|
|
|
|
return response.EmptySyncResponse
|
|
}
|
|
|
|
// swagger:operation DELETE /1.0/instances/{name}/metadata/templates instances instance_metadata_templates_delete
|
|
//
|
|
// Delete a template file
|
|
//
|
|
// Removes the template file.
|
|
//
|
|
// ---
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - in: path
|
|
// name: name
|
|
// description: Instance name
|
|
// type: string
|
|
// required: true
|
|
// - in: query
|
|
// name: path
|
|
// description: Template name
|
|
// type: string
|
|
// example: default
|
|
// - in: query
|
|
// name: project
|
|
// description: Project name
|
|
// type: string
|
|
// example: default
|
|
// responses:
|
|
// "200":
|
|
// $ref: "#/responses/EmptySyncResponse"
|
|
// "400":
|
|
// $ref: "#/responses/BadRequest"
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "404":
|
|
// $ref: "#/responses/NotFound"
|
|
// "409":
|
|
// $ref: "#/responses/Conflict"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
func instanceMetadataTemplatesDelete(d *Daemon, r *http.Request) response.Response {
|
|
s := d.State()
|
|
|
|
projectName := request.ProjectParam(r)
|
|
|
|
name, err := pathVar(r, "name")
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
if internalInstance.IsSnapshot(name) {
|
|
return response.BadRequest(errors.New("Invalid instance name"))
|
|
}
|
|
|
|
// Handle requests targeted to a container on a different node
|
|
resp, err := forwardedResponseIfInstanceIsRemote(s, r, projectName, name)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
if resp != nil {
|
|
return resp
|
|
}
|
|
|
|
// Load the container
|
|
c, err := instance.LoadByProjectAndName(s, projectName, name)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Start the storage if needed
|
|
pool, err := storagePools.LoadByInstance(s, c)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
_, err = storagePools.InstanceMount(pool, c, nil)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(func() error { return storagePools.InstanceUnmount(pool, c, nil) }, "Failed to unmount instance")
|
|
|
|
// Confine all template access to the instance directory.
|
|
root, err := os.OpenRoot(c.Path())
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
defer logger.WarnOnError(root.Close, "Failed to close instance root")
|
|
|
|
// Look at the request
|
|
templateName := r.FormValue("path")
|
|
if templateName == "" {
|
|
return response.BadRequest(errors.New("missing path argument"))
|
|
}
|
|
|
|
templatePath, err := getContainerTemplatePath(templateName)
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Delete the template
|
|
err = root.Remove(templatePath)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return response.NotFound(fmt.Errorf("Template %q not found", templateName))
|
|
}
|
|
|
|
return response.InternalError(err)
|
|
}
|
|
|
|
s.Events.SendLifecycle(projectName, lifecycle.InstanceMetadataTemplateDeleted.Event(c, request.CreateRequestor(r), logger.Ctx{"path": templateName}))
|
|
|
|
return response.EmptySyncResponse
|
|
}
|
|
|
|
// Return the template path relative to the instance root.
|
|
func getContainerTemplatePath(filename string) (string, error) {
|
|
if strings.Contains(filename, "/") {
|
|
return "", errors.New("Invalid template filename")
|
|
}
|
|
|
|
return filepath.Join("templates", filename), nil
|
|
}
|