package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"gopkg.in/yaml.v2"
internalInstance "github.com/lxc/incus/v6/internal/instance"
"github.com/lxc/incus/v6/internal/server/instance"
"github.com/lxc/incus/v6/internal/server/lifecycle"
"github.com/lxc/incus/v6/internal/server/request"
"github.com/lxc/incus/v6/internal/server/response"
"github.com/lxc/incus/v6/internal/server/state"
storagePools "github.com/lxc/incus/v6/internal/server/storage"
localUtil "github.com/lxc/incus/v6/internal/server/util"
"github.com/lxc/incus/v6/shared/api"
"github.com/lxc/incus/v6/shared/logger"
"github.com/lxc/incus/v6/shared/util"
)
func instanceMetadataGet(d *Daemon, r *http.Request) response.Response {
s := d.State()
instanceType, err := urlInstanceTypeDetect(r)
if err != nil {
return response.SmartError(err)
}
projectName := request.ProjectParam(r)
name, err := url.PathUnescape(mux.Vars(r)["name"])
if err != nil {
return response.SmartError(err)
}
if internalInstance.IsSnapshot(name) {
return response.BadRequest(fmt.Errorf("Invalid instance name"))
}
resp, err := forwardedResponseIfInstanceIsRemote(s, r, projectName, name, instanceType)
if err != nil {
return response.SmartError(err)
}
if resp != nil {
return resp
}
c, err := instance.LoadByProjectAndName(s, projectName, name)
if err != nil {
return response.SmartError(err)
}
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 func() { _ = storagePools.InstanceUnmount(pool, c, nil) }()
metadataPath := filepath.Join(c.Path(), "metadata.yaml")
if !util.PathExists(metadataPath) {
return response.SyncResponse(true, api.ImageMetadata{})
}
metadataFile, err := os.Open(metadataPath)
if err != nil {
return response.InternalError(err)
}
defer func() { _ = metadataFile.Close() }()
data, err := io.ReadAll(metadataFile)
if err != nil {
return response.InternalError(err)
}
metadata := api.ImageMetadata{}
err = yaml.Unmarshal(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)
}
func instanceMetadataPatch(d *Daemon, r *http.Request) response.Response {
s := d.State()
instanceType, err := urlInstanceTypeDetect(r)
if err != nil {
return response.SmartError(err)
}
projectName := request.ProjectParam(r)
name, err := url.PathUnescape(mux.Vars(r)["name"])
if err != nil {
return response.SmartError(err)
}
if internalInstance.IsSnapshot(name) {
return response.BadRequest(fmt.Errorf("Invalid instance name"))
}
resp, err := forwardedResponseIfInstanceIsRemote(s, r, projectName, name, instanceType)
if err != nil {
return response.SmartError(err)
}
if resp != nil {
return resp
}
inst, err := instance.LoadByProjectAndName(s, projectName, name)
if err != nil {
return response.SmartError(err)
}
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 func() { _ = storagePools.InstanceUnmount(pool, inst, nil) }()
metadataPath := filepath.Join(inst.Path(), "metadata.yaml")
metadata := api.ImageMetadata{}
if util.PathExists(metadataPath) {
metadataFile, err := os.Open(metadataPath)
if err != nil {
return response.InternalError(err)
}
defer func() { _ = metadataFile.Close() }()
data, err := io.ReadAll(metadataFile)
if err != nil {
return response.InternalError(err)
}
err = yaml.Unmarshal(data, &metadata)
if err != nil {
return response.SmartError(err)
}
}
err = localUtil.EtagCheck(r, metadata)
if err != nil {
return response.PreconditionFailed(err)
}
err = json.NewDecoder(r.Body).Decode(&metadata)
if err != nil {
return response.BadRequest(err)
}
return doInstanceMetadataUpdate(s, inst, metadata, r)
}
func instanceMetadataPut(d *Daemon, r *http.Request) response.Response {
s := d.State()
instanceType, err := urlInstanceTypeDetect(r)
if err != nil {
return response.SmartError(err)
}
projectName := request.ProjectParam(r)
name, err := url.PathUnescape(mux.Vars(r)["name"])
if err != nil {
return response.SmartError(err)
}
if internalInstance.IsSnapshot(name) {
return response.BadRequest(fmt.Errorf("Invalid instance name"))
}
resp, err := forwardedResponseIfInstanceIsRemote(s, r, projectName, name, instanceType)
if err != nil {
return response.SmartError(err)
}
if resp != nil {
return resp
}
metadata := api.ImageMetadata{}
err = json.NewDecoder(r.Body).Decode(&metadata)
if err != nil {
return response.BadRequest(err)
}
inst, err := instance.LoadByProjectAndName(s, projectName, name)
if err != nil {
return response.SmartError(err)
}
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 func() { _ = storagePools.InstanceUnmount(pool, inst, nil) }()
return doInstanceMetadataUpdate(s, inst, metadata, r)
}
func doInstanceMetadataUpdate(s *state.State, inst instance.Instance, metadata api.ImageMetadata, r *http.Request) response.Response {
data, err := yaml.Marshal(metadata)
if err != nil {
return response.BadRequest(err)
}
metadataPath := filepath.Join(inst.Path(), "metadata.yaml")
err = os.WriteFile(metadataPath, data, 0644)
if err != nil {
return response.InternalError(err)
}
s.Events.SendLifecycle(inst.Project().Name, lifecycle.InstanceMetadataUpdated.Event(inst, request.CreateRequestor(r), nil))
return response.EmptySyncResponse
}
func instanceMetadataTemplatesGet(d *Daemon, r *http.Request) response.Response {
s := d.State()
instanceType, err := urlInstanceTypeDetect(r)
if err != nil {
return response.SmartError(err)
}
projectName := request.ProjectParam(r)
name, err := url.PathUnescape(mux.Vars(r)["name"])
if err != nil {
return response.SmartError(err)
}
if internalInstance.IsSnapshot(name) {
return response.BadRequest(fmt.Errorf("Invalid instance name"))
}
resp, err := forwardedResponseIfInstanceIsRemote(s, r, projectName, name, instanceType)
if err != nil {
return response.SmartError(err)
}
if resp != nil {
return resp
}
c, err := instance.LoadByProjectAndName(s, projectName, name)
if err != nil {
return response.SmartError(err)
}
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 func() { _ = storagePools.InstanceUnmount(pool, c, nil) }()
templateName := r.FormValue("path")
if templateName == "" {
templates := []string{}
if !util.PathExists(filepath.Join(c.Path(), "templates")) {
return response.SyncResponse(true, templates)
}
templatesPath := filepath.Join(c.Path(), "templates")
entries, err := os.ReadDir(templatesPath)
if err != nil {
return response.InternalError(err)
}
for _, entry := range entries {
if !entry.IsDir() {
templates = append(templates, entry.Name())
}
}
return response.SyncResponse(true, templates)
}
templatePath, err := getContainerTemplatePath(c, templateName)
if err != nil {
return response.SmartError(err)
}
if !util.PathExists(templatePath) {
return response.NotFound(fmt.Errorf("Template %q not found", templateName))
}
template, err := os.Open(templatePath)
if err != nil {
return response.SmartError(err)
}
defer func() { _ = template.Close() }()
tempfile, err := os.CreateTemp("", "incus_template")
if err != nil {
return response.SmartError(err)
}
_, err = io.Copy(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)
}
func instanceMetadataTemplatesPost(d *Daemon, r *http.Request) response.Response {
s := d.State()
instanceType, err := urlInstanceTypeDetect(r)
if err != nil {
return response.SmartError(err)
}
projectName := request.ProjectParam(r)
name, err := url.PathUnescape(mux.Vars(r)["name"])
if err != nil {
return response.SmartError(err)
}
if internalInstance.IsSnapshot(name) {
return response.BadRequest(fmt.Errorf("Invalid instance name"))
}
resp, err := forwardedResponseIfInstanceIsRemote(s, r, projectName, name, instanceType)
if err != nil {
return response.SmartError(err)
}
if resp != nil {
return resp
}
c, err := instance.LoadByProjectAndName(s, projectName, name)
if err != nil {
return response.SmartError(err)
}
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 func() { _ = storagePools.InstanceUnmount(pool, c, nil) }()
templateName := r.FormValue("path")
if templateName == "" {
return response.BadRequest(fmt.Errorf("missing path argument"))
}
if !util.PathExists(filepath.Join(c.Path(), "templates")) {
err := os.MkdirAll(filepath.Join(c.Path(), "templates"), 0711)
if err != nil {
return response.SmartError(err)
}
}
templatePath, err := getContainerTemplatePath(c, templateName)
if err != nil {
return response.SmartError(err)
}
template, err := os.OpenFile(templatePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return response.SmartError(err)
}
_, err = io.Copy(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
}
func instanceMetadataTemplatesDelete(d *Daemon, r *http.Request) response.Response {
s := d.State()
instanceType, err := urlInstanceTypeDetect(r)
if err != nil {
return response.SmartError(err)
}
projectName := request.ProjectParam(r)
name, err := url.PathUnescape(mux.Vars(r)["name"])
if err != nil {
return response.SmartError(err)
}
if internalInstance.IsSnapshot(name) {
return response.BadRequest(fmt.Errorf("Invalid instance name"))
}
resp, err := forwardedResponseIfInstanceIsRemote(s, r, projectName, name, instanceType)
if err != nil {
return response.SmartError(err)
}
if resp != nil {
return resp
}
c, err := instance.LoadByProjectAndName(s, projectName, name)
if err != nil {
return response.SmartError(err)
}
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 func() { _ = storagePools.InstanceUnmount(pool, c, nil) }()
templateName := r.FormValue("path")
if templateName == "" {
return response.BadRequest(fmt.Errorf("missing path argument"))
}
templatePath, err := getContainerTemplatePath(c, templateName)
if err != nil {
return response.SmartError(err)
}
if !util.PathExists(templatePath) {
return response.NotFound(fmt.Errorf("Template %q not found", templateName))
}
err = os.Remove(templatePath)
if err != nil {
return response.InternalError(err)
}
s.Events.SendLifecycle(projectName, lifecycle.InstanceMetadataTemplateDeleted.Event(c, request.CreateRequestor(r), logger.Ctx{"path": templateName}))
return response.EmptySyncResponse
}
func getContainerTemplatePath(c instance.Instance, filename string) (string, error) {
if strings.Contains(filename, "/") {
return "", fmt.Errorf("Invalid template filename")
}
return filepath.Join(c.Path(), "templates", filename), nil
}