mirror of
https://github.com/lxc/incus.git
synced 2026-02-05 09:46:19 +01:00
1093 lines
27 KiB
Go
1093 lines
27 KiB
Go
package incus
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/lxc/incus/v6/shared/api"
|
|
"github.com/lxc/incus/v6/shared/cancel"
|
|
"github.com/lxc/incus/v6/shared/ioprogress"
|
|
localtls "github.com/lxc/incus/v6/shared/tls"
|
|
"github.com/lxc/incus/v6/shared/units"
|
|
"github.com/lxc/incus/v6/shared/util"
|
|
)
|
|
|
|
// Image handling functions
|
|
|
|
// GetImages returns a list of available images as Image structs.
|
|
func (r *ProtocolIncus) GetImages() ([]api.Image, error) {
|
|
images := []api.Image{}
|
|
|
|
_, err := r.queryStruct("GET", "/images?recursion=1", nil, "", &images)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return images, nil
|
|
}
|
|
|
|
// GetImagesAllProjects returns a list of images across all projects as Image structs.
|
|
func (r *ProtocolIncus) GetImagesAllProjects() ([]api.Image, error) {
|
|
images := []api.Image{}
|
|
|
|
v := url.Values{}
|
|
v.Set("recursion", "1")
|
|
v.Set("all-projects", "true")
|
|
|
|
if !r.HasExtension("images_all_projects") {
|
|
return nil, fmt.Errorf("The server is missing the required \"images_all_projects\" API extension")
|
|
}
|
|
|
|
_, err := r.queryStruct("GET", fmt.Sprintf("/images?%s", v.Encode()), nil, "", &images)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return images, nil
|
|
}
|
|
|
|
// GetImagesAllProjectsWithFilter returns a filtered list of images across all projects as Image structs.
|
|
func (r *ProtocolIncus) GetImagesAllProjectsWithFilter(filters []string) ([]api.Image, error) {
|
|
images := []api.Image{}
|
|
|
|
v := url.Values{}
|
|
v.Set("recursion", "1")
|
|
v.Set("all-projects", "true")
|
|
v.Set("filter", parseFilters(filters))
|
|
|
|
if !r.HasExtension("images_all_projects") {
|
|
return nil, fmt.Errorf("The server is missing the required \"images_all_projects\" API extension")
|
|
}
|
|
|
|
_, err := r.queryStruct("GET", fmt.Sprintf("/images?%s", v.Encode()), nil, "", &images)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return images, nil
|
|
}
|
|
|
|
// GetImagesWithFilter returns a filtered list of available images as Image structs.
|
|
func (r *ProtocolIncus) GetImagesWithFilter(filters []string) ([]api.Image, error) {
|
|
if !r.HasExtension("api_filtering") {
|
|
return nil, fmt.Errorf("The server is missing the required \"api_filtering\" API extension")
|
|
}
|
|
|
|
images := []api.Image{}
|
|
|
|
v := url.Values{}
|
|
v.Set("recursion", "1")
|
|
v.Set("filter", parseFilters(filters))
|
|
|
|
_, err := r.queryStruct("GET", fmt.Sprintf("/images?%s", v.Encode()), nil, "", &images)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return images, nil
|
|
}
|
|
|
|
// GetImageFingerprints returns a list of available image fingerprints.
|
|
func (r *ProtocolIncus) GetImageFingerprints() ([]string, error) {
|
|
// Fetch the raw URL values.
|
|
urls := []string{}
|
|
baseURL := "/images"
|
|
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse it.
|
|
return urlsToResourceNames(baseURL, urls...)
|
|
}
|
|
|
|
// GetImage returns an Image struct for the provided fingerprint.
|
|
func (r *ProtocolIncus) GetImage(fingerprint string) (*api.Image, string, error) {
|
|
return r.GetPrivateImage(fingerprint, "")
|
|
}
|
|
|
|
// GetImageFile downloads an image from the server, returning an ImageFileRequest struct.
|
|
func (r *ProtocolIncus) GetImageFile(fingerprint string, req ImageFileRequest) (*ImageFileResponse, error) {
|
|
return r.GetPrivateImageFile(fingerprint, "", req)
|
|
}
|
|
|
|
// GetImageSecret is a helper around CreateImageSecret that returns a secret for the image.
|
|
func (r *ProtocolIncus) GetImageSecret(fingerprint string) (string, error) {
|
|
op, err := r.CreateImageSecret(fingerprint)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
opAPI := op.Get()
|
|
|
|
secret, ok := opAPI.Metadata["secret"].(string)
|
|
if !ok {
|
|
return "", errors.New("Bad secret type")
|
|
}
|
|
|
|
return secret, nil
|
|
}
|
|
|
|
// GetPrivateImage is similar to GetImage but allows passing a secret download token.
|
|
func (r *ProtocolIncus) GetPrivateImage(fingerprint string, secret string) (*api.Image, string, error) {
|
|
image := api.Image{}
|
|
|
|
// Build the API path
|
|
path := fmt.Sprintf("/images/%s", url.PathEscape(fingerprint))
|
|
var err error
|
|
path, err = r.setQueryAttributes(path)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
if secret != "" {
|
|
path, err = setQueryParam(path, "secret", secret)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
}
|
|
|
|
// Fetch the raw value
|
|
etag, err := r.queryStruct("GET", path, nil, "", &image)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
return &image, etag, nil
|
|
}
|
|
|
|
// GetPrivateImageFile is similar to GetImageFile but allows passing a secret download token.
|
|
func (r *ProtocolIncus) GetPrivateImageFile(fingerprint string, secret string, req ImageFileRequest) (*ImageFileResponse, error) {
|
|
// Quick checks.
|
|
if req.MetaFile == nil && req.RootfsFile == nil {
|
|
return nil, fmt.Errorf("No file requested")
|
|
}
|
|
|
|
uri := fmt.Sprintf("/1.0/images/%s/export", url.PathEscape(fingerprint))
|
|
|
|
var err error
|
|
uri, err = r.setQueryAttributes(uri)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Attempt to download from host
|
|
if secret == "" && util.PathExists("/dev/incus/sock") && os.Geteuid() == 0 {
|
|
unixURI := fmt.Sprintf("http://unix.socket%s", uri)
|
|
|
|
// Setup the HTTP client
|
|
devIncusHTTP, err := unixHTTPClient(nil, "/dev/incus/sock")
|
|
if err == nil {
|
|
resp, err := incusDownloadImage(fingerprint, unixURI, r.httpUserAgent, devIncusHTTP.Do, req)
|
|
if err == nil {
|
|
return resp, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build the URL
|
|
uri = fmt.Sprintf("%s%s", r.httpBaseURL.String(), uri)
|
|
if secret != "" {
|
|
uri, err = setQueryParam(uri, "secret", secret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Use relatively short response header timeout so as not to hold the image lock open too long.
|
|
// Deference client and transport in order to clone them so as to not modify timeout of base client.
|
|
httpClient := *r.http
|
|
httpTransport := httpClient.Transport.(*http.Transport).Clone()
|
|
httpTransport.ResponseHeaderTimeout = 30 * time.Second
|
|
httpClient.Transport = httpTransport
|
|
|
|
return incusDownloadImage(fingerprint, uri, r.httpUserAgent, r.DoHTTP, req)
|
|
}
|
|
|
|
func incusDownloadImage(fingerprint string, uri string, userAgent string, do func(*http.Request) (*http.Response, error), req ImageFileRequest) (*ImageFileResponse, error) {
|
|
// Prepare the response
|
|
resp := ImageFileResponse{}
|
|
|
|
// Prepare the download request
|
|
request, err := http.NewRequest("GET", uri, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if userAgent != "" {
|
|
request.Header.Set("User-Agent", userAgent)
|
|
}
|
|
|
|
// Start the request
|
|
response, doneCh, err := cancel.CancelableDownload(req.Canceler, do, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() { _ = response.Body.Close() }()
|
|
defer close(doneCh)
|
|
|
|
if response.StatusCode != http.StatusOK {
|
|
_, _, err := incusParseResponse(response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
ctype, ctypeParams, err := mime.ParseMediaType(response.Header.Get("Content-Type"))
|
|
if err != nil {
|
|
ctype = "application/octet-stream"
|
|
}
|
|
|
|
// Check the image type.
|
|
imageType := response.Header.Get("X-Incus-Type")
|
|
if imageType == "" {
|
|
imageType = "incus"
|
|
}
|
|
|
|
// Handle the data
|
|
body := response.Body
|
|
if req.ProgressHandler != nil {
|
|
reader := &ioprogress.ProgressReader{
|
|
ReadCloser: response.Body,
|
|
Tracker: &ioprogress.ProgressTracker{
|
|
Length: response.ContentLength,
|
|
},
|
|
}
|
|
|
|
if response.ContentLength > 0 {
|
|
reader.Tracker.Handler = func(percent int64, speed int64) {
|
|
req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", percent, units.GetByteSizeString(speed, 2))})
|
|
}
|
|
} else {
|
|
reader.Tracker.Handler = func(received int64, speed int64) {
|
|
req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%s (%s/s)", units.GetByteSizeString(received, 2), units.GetByteSizeString(speed, 2))})
|
|
}
|
|
}
|
|
|
|
body = reader
|
|
}
|
|
|
|
// Hashing
|
|
hash256 := sha256.New()
|
|
|
|
// Deal with split images
|
|
if ctype == "multipart/form-data" {
|
|
if req.MetaFile == nil || req.RootfsFile == nil {
|
|
return nil, fmt.Errorf("Multi-part image but only one target file provided")
|
|
}
|
|
|
|
// Parse the POST data
|
|
mr := multipart.NewReader(body, ctypeParams["boundary"])
|
|
|
|
// Get the metadata tarball
|
|
part, err := mr.NextPart()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if part.FormName() != "metadata" {
|
|
return nil, fmt.Errorf("Invalid multipart image")
|
|
}
|
|
|
|
size, err := io.Copy(io.MultiWriter(req.MetaFile, hash256), part)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp.MetaSize = size
|
|
resp.MetaName = part.FileName()
|
|
|
|
// Get the rootfs tarball
|
|
part, err = mr.NextPart()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !slices.Contains([]string{"rootfs", "rootfs.img"}, part.FormName()) {
|
|
return nil, fmt.Errorf("Invalid multipart image")
|
|
}
|
|
|
|
size, err = io.Copy(io.MultiWriter(req.RootfsFile, hash256), part)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp.RootfsSize = size
|
|
resp.RootfsName = part.FileName()
|
|
|
|
// Check the hash
|
|
hash := fmt.Sprintf("%x", hash256.Sum(nil))
|
|
if imageType != "oci" && !strings.HasPrefix(hash, fingerprint) {
|
|
return nil, fmt.Errorf("Image fingerprint doesn't match. Got %s expected %s", hash, fingerprint)
|
|
}
|
|
|
|
return &resp, nil
|
|
}
|
|
|
|
// Deal with unified images
|
|
_, cdParams, err := mime.ParseMediaType(response.Header.Get("Content-Disposition"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filename, ok := cdParams["filename"]
|
|
if !ok {
|
|
return nil, fmt.Errorf("No filename in Content-Disposition header")
|
|
}
|
|
|
|
size, err := io.Copy(io.MultiWriter(req.MetaFile, hash256), body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp.MetaSize = size
|
|
resp.MetaName = filename
|
|
|
|
// Check the hash
|
|
hash := fmt.Sprintf("%x", hash256.Sum(nil))
|
|
if imageType != "oci" && !strings.HasPrefix(hash, fingerprint) {
|
|
return nil, fmt.Errorf("Image fingerprint doesn't match. Got %s expected %s", hash, fingerprint)
|
|
}
|
|
|
|
return &resp, nil
|
|
}
|
|
|
|
// GetImageAliases returns the list of available aliases as ImageAliasesEntry structs.
|
|
func (r *ProtocolIncus) GetImageAliases() ([]api.ImageAliasesEntry, error) {
|
|
aliases := []api.ImageAliasesEntry{}
|
|
|
|
// Fetch the raw value
|
|
_, err := r.queryStruct("GET", "/images/aliases?recursion=1", nil, "", &aliases)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return aliases, nil
|
|
}
|
|
|
|
// GetImageAliasNames returns the list of available alias names.
|
|
func (r *ProtocolIncus) GetImageAliasNames() ([]string, error) {
|
|
// Fetch the raw URL values.
|
|
urls := []string{}
|
|
baseURL := "/images/aliases"
|
|
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse it.
|
|
return urlsToResourceNames(baseURL, urls...)
|
|
}
|
|
|
|
// GetImageAlias returns an existing alias as an ImageAliasesEntry struct.
|
|
func (r *ProtocolIncus) GetImageAlias(name string) (*api.ImageAliasesEntry, string, error) {
|
|
alias := api.ImageAliasesEntry{}
|
|
|
|
// Fetch the raw value
|
|
etag, err := r.queryStruct("GET", fmt.Sprintf("/images/aliases/%s", url.PathEscape(name)), nil, "", &alias)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
return &alias, etag, nil
|
|
}
|
|
|
|
// GetImageAliasType returns an existing alias as an ImageAliasesEntry struct.
|
|
func (r *ProtocolIncus) GetImageAliasType(imageType string, name string) (*api.ImageAliasesEntry, string, error) {
|
|
alias, etag, err := r.GetImageAlias(name)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
if imageType != "" {
|
|
if alias.Type == "" {
|
|
alias.Type = "container"
|
|
}
|
|
|
|
if alias.Type != imageType {
|
|
return nil, "", fmt.Errorf("Alias doesn't exist for the specified type")
|
|
}
|
|
}
|
|
|
|
return alias, etag, nil
|
|
}
|
|
|
|
// GetImageAliasArchitectures returns a map of architectures / targets.
|
|
func (r *ProtocolIncus) GetImageAliasArchitectures(imageType string, name string) (map[string]*api.ImageAliasesEntry, error) {
|
|
alias, _, err := r.GetImageAliasType(imageType, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
img, _, err := r.GetImage(alias.Target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]*api.ImageAliasesEntry{img.Architecture: alias}, nil
|
|
}
|
|
|
|
// CreateImage requests that Incus creates, copies or import a new image.
|
|
func (r *ProtocolIncus) CreateImage(image api.ImagesPost, args *ImageCreateArgs) (Operation, error) {
|
|
if image.CompressionAlgorithm != "" {
|
|
if !r.HasExtension("image_compression_algorithm") {
|
|
return nil, fmt.Errorf("The server is missing the required \"image_compression_algorithm\" API extension")
|
|
}
|
|
}
|
|
|
|
// Send the JSON based request
|
|
if args == nil {
|
|
op, _, err := r.queryOperation("POST", "/images", image, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return op, nil
|
|
}
|
|
|
|
// Prepare an image upload
|
|
if args.MetaFile == nil {
|
|
return nil, fmt.Errorf("Metadata file is required")
|
|
}
|
|
|
|
// Prepare the body
|
|
var body io.Reader
|
|
var contentType string
|
|
if args.RootfsFile == nil {
|
|
// If unified image, just pass it through
|
|
body = args.MetaFile
|
|
|
|
contentType = "application/octet-stream"
|
|
} else {
|
|
pr, pw := io.Pipe()
|
|
// Setup the multipart writer
|
|
w := multipart.NewWriter(pw)
|
|
|
|
go func() {
|
|
var ioErr error
|
|
defer func() {
|
|
cerr := w.Close()
|
|
if ioErr == nil && cerr != nil {
|
|
ioErr = cerr
|
|
}
|
|
|
|
_ = pw.CloseWithError(ioErr)
|
|
}()
|
|
|
|
// Metadata file
|
|
fw, ioErr := w.CreateFormFile("metadata", args.MetaName)
|
|
if ioErr != nil {
|
|
return
|
|
}
|
|
|
|
_, ioErr = io.Copy(fw, args.MetaFile)
|
|
if ioErr != nil {
|
|
return
|
|
}
|
|
|
|
// Rootfs file
|
|
if args.Type == "virtual-machine" {
|
|
fw, ioErr = w.CreateFormFile("rootfs.img", args.RootfsName)
|
|
} else {
|
|
fw, ioErr = w.CreateFormFile("rootfs", args.RootfsName)
|
|
}
|
|
|
|
if ioErr != nil {
|
|
return
|
|
}
|
|
|
|
_, ioErr = io.Copy(fw, args.RootfsFile)
|
|
if ioErr != nil {
|
|
return
|
|
}
|
|
|
|
// Done writing to multipart
|
|
ioErr = w.Close()
|
|
if ioErr != nil {
|
|
return
|
|
}
|
|
|
|
ioErr = pw.Close()
|
|
if ioErr != nil {
|
|
return
|
|
}
|
|
}()
|
|
|
|
// Setup progress handler
|
|
if args.ProgressHandler != nil {
|
|
body = &ioprogress.ProgressReader{
|
|
ReadCloser: pr,
|
|
Tracker: &ioprogress.ProgressTracker{
|
|
Handler: func(received int64, speed int64) {
|
|
args.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%s (%s/s)", units.GetByteSizeString(received, 2), units.GetByteSizeString(speed, 2))})
|
|
},
|
|
},
|
|
}
|
|
} else {
|
|
body = pr
|
|
}
|
|
|
|
contentType = w.FormDataContentType()
|
|
}
|
|
|
|
// Prepare the HTTP request
|
|
reqURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0/images", r.httpBaseURL.String()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", reqURL, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Setup the headers
|
|
req.Header.Set("Content-Type", contentType)
|
|
if image.Public {
|
|
req.Header.Set("X-Incus-public", "true")
|
|
}
|
|
|
|
if image.Filename != "" {
|
|
req.Header.Set("X-Incus-filename", image.Filename)
|
|
}
|
|
|
|
if len(image.Properties) > 0 {
|
|
imgProps := url.Values{}
|
|
|
|
for k, v := range image.Properties {
|
|
imgProps.Set(k, v)
|
|
}
|
|
|
|
req.Header.Set("X-Incus-properties", imgProps.Encode())
|
|
}
|
|
|
|
if len(image.Profiles) > 0 {
|
|
imgProfiles := url.Values{}
|
|
|
|
for _, v := range image.Profiles {
|
|
imgProfiles.Add("profile", v)
|
|
}
|
|
|
|
req.Header.Set("X-Incus-profiles", imgProfiles.Encode())
|
|
}
|
|
|
|
if len(image.Aliases) > 0 {
|
|
imgProfiles := url.Values{}
|
|
|
|
for _, v := range image.Aliases {
|
|
imgProfiles.Add("alias", v.Name)
|
|
}
|
|
|
|
req.Header.Set("X-Incus-aliases", imgProfiles.Encode())
|
|
}
|
|
|
|
// Set the user agent
|
|
if image.Source != nil && image.Source.Fingerprint != "" && image.Source.Secret != "" && image.Source.Mode == "push" {
|
|
// Set fingerprint
|
|
req.Header.Set("X-Incus-fingerprint", image.Source.Fingerprint)
|
|
|
|
// Set secret
|
|
req.Header.Set("X-Incus-secret", image.Source.Secret)
|
|
}
|
|
|
|
// Send the request
|
|
resp, err := r.DoHTTP(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// Handle errors
|
|
response, _, err := incusParseResponse(resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get to the operation
|
|
respOperation, err := response.MetadataAsOperation()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Setup an Operation wrapper
|
|
op := operation{
|
|
Operation: *respOperation,
|
|
r: r,
|
|
chActive: make(chan bool),
|
|
}
|
|
|
|
return &op, nil
|
|
}
|
|
|
|
// tryCopyImage iterates through the source server URLs until one lets it download the image.
|
|
func (r *ProtocolIncus) tryCopyImage(req api.ImagesPost, urls []string) (RemoteOperation, error) {
|
|
if len(urls) == 0 {
|
|
return nil, fmt.Errorf("The source server isn't listening on the network")
|
|
}
|
|
|
|
rop := remoteOperation{
|
|
chDone: make(chan bool),
|
|
}
|
|
|
|
// For older servers, apply the aliases after copy
|
|
if !r.HasExtension("image_create_aliases") && req.Aliases != nil {
|
|
rop.chPost = make(chan bool)
|
|
|
|
go func() {
|
|
defer close(rop.chPost)
|
|
|
|
// Wait for the main operation to finish
|
|
<-rop.chDone
|
|
if rop.err != nil {
|
|
return
|
|
}
|
|
|
|
var errs []remoteOperationResult
|
|
|
|
// Get the operation data
|
|
op, err := rop.GetTarget()
|
|
if err != nil {
|
|
errs = append(errs, remoteOperationResult{Error: err})
|
|
rop.err = remoteOperationError("Failed to get operation data", errs)
|
|
return
|
|
}
|
|
|
|
// Extract the fingerprint
|
|
fingerprint, ok := op.Metadata["fingerprint"].(string)
|
|
if !ok {
|
|
errs = append(errs, remoteOperationResult{Error: errors.New("Bad fingerprint")})
|
|
rop.err = remoteOperationError("Failed to get operation data", errs)
|
|
return
|
|
}
|
|
|
|
// Add the aliases
|
|
for _, entry := range req.Aliases {
|
|
alias := api.ImageAliasesPost{}
|
|
alias.Name = entry.Name
|
|
alias.Target = fingerprint
|
|
|
|
err := r.CreateImageAlias(alias)
|
|
if err != nil {
|
|
errs = append(errs, remoteOperationResult{Error: err})
|
|
rop.err = remoteOperationError("Failed to create image alias", errs)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Forward targetOp to remote op
|
|
go func() {
|
|
success := false
|
|
var errs []remoteOperationResult
|
|
for _, serverURL := range urls {
|
|
req.Source.Server = serverURL
|
|
|
|
op, err := r.CreateImage(req, nil)
|
|
if err != nil {
|
|
errs = append(errs, remoteOperationResult{URL: serverURL, Error: err})
|
|
continue
|
|
}
|
|
|
|
rop.handlerLock.Lock()
|
|
rop.targetOp = op
|
|
rop.handlerLock.Unlock()
|
|
|
|
for _, handler := range rop.handlers {
|
|
_, _ = rop.targetOp.AddHandler(handler)
|
|
}
|
|
|
|
err = rop.targetOp.Wait()
|
|
if err != nil {
|
|
errs = append(errs, remoteOperationResult{URL: serverURL, Error: err})
|
|
|
|
if localtls.IsConnectionError(err) {
|
|
continue
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
success = true
|
|
break
|
|
}
|
|
|
|
if !success {
|
|
rop.err = remoteOperationError("Failed remote image download", errs)
|
|
}
|
|
|
|
close(rop.chDone)
|
|
}()
|
|
|
|
return &rop, nil
|
|
}
|
|
|
|
// CopyImage copies an image from a remote server. Additional options can be passed using ImageCopyArgs.
|
|
func (r *ProtocolIncus) CopyImage(source ImageServer, image api.Image, args *ImageCopyArgs) (RemoteOperation, error) {
|
|
// Quick checks.
|
|
if r.isSameServer(source) {
|
|
return nil, fmt.Errorf("The source and target servers must be different")
|
|
}
|
|
|
|
// Handle profile list overrides.
|
|
if args != nil && args.Profiles != nil {
|
|
if !r.HasExtension("image_copy_profile") {
|
|
return nil, fmt.Errorf("The server is missing the required \"image_copy_profile\" API extension")
|
|
}
|
|
|
|
image.Profiles = args.Profiles
|
|
} else {
|
|
// If profiles aren't provided, clear the list on the source to
|
|
// avoid requiring the destination to have them all.
|
|
image.Profiles = nil
|
|
}
|
|
|
|
// Get source server connection information
|
|
info, err := source.GetConnectionInfo()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Push mode
|
|
if args != nil && args.Mode == "push" {
|
|
// Get certificate and URL
|
|
info, err := r.GetConnectionInfo()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
imagesPost := api.ImagesPost{
|
|
Source: &api.ImagesPostSource{
|
|
Fingerprint: image.Fingerprint,
|
|
Mode: args.Mode,
|
|
},
|
|
}
|
|
|
|
imagesPost.Aliases = args.Aliases
|
|
if args.CopyAliases {
|
|
imagesPost.Aliases = image.Aliases
|
|
if args.Aliases != nil {
|
|
imagesPost.Aliases = append(imagesPost.Aliases, args.Aliases...)
|
|
}
|
|
}
|
|
|
|
imagesPost.ExpiresAt = image.ExpiresAt
|
|
imagesPost.Properties = image.Properties
|
|
imagesPost.Public = args.Public
|
|
|
|
// Receive token from target server. This token is later passed to the source which will use
|
|
// it, together with the URL and certificate, to connect to the target.
|
|
tokenOp, err := r.CreateImage(imagesPost, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
opAPI := tokenOp.Get()
|
|
|
|
secret, ok := opAPI.Metadata["secret"]
|
|
if !ok {
|
|
return nil, fmt.Errorf("No token provided")
|
|
}
|
|
|
|
req := api.ImageExportPost{
|
|
Target: info.URL,
|
|
Certificate: info.Certificate,
|
|
Secret: secret.(string),
|
|
Project: info.Project,
|
|
Profiles: image.Profiles,
|
|
}
|
|
|
|
exportOp, err := source.ExportImage(image.Fingerprint, req)
|
|
if err != nil {
|
|
_ = tokenOp.Cancel()
|
|
return nil, err
|
|
}
|
|
|
|
rop := remoteOperation{
|
|
targetOp: exportOp,
|
|
chDone: make(chan bool),
|
|
}
|
|
|
|
// Forward targetOp to remote op
|
|
go func() {
|
|
rop.err = rop.targetOp.Wait()
|
|
_ = tokenOp.Cancel()
|
|
close(rop.chDone)
|
|
}()
|
|
|
|
return &rop, nil
|
|
}
|
|
|
|
// Relay mode
|
|
if args != nil && args.Mode == "relay" {
|
|
metaFile, err := os.CreateTemp("", "incus_image_")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() { _ = os.Remove(metaFile.Name()) }()
|
|
|
|
rootfsFile, err := os.CreateTemp("", "incus_image_")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() { _ = os.Remove(rootfsFile.Name()) }()
|
|
|
|
// Import image
|
|
req := ImageFileRequest{
|
|
MetaFile: metaFile,
|
|
RootfsFile: rootfsFile,
|
|
}
|
|
|
|
resp, err := source.GetImageFile(image.Fingerprint, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Export image
|
|
_, err = metaFile.Seek(0, io.SeekStart)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = rootfsFile.Seek(0, io.SeekStart)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
imagePost := api.ImagesPost{}
|
|
imagePost.Public = args.Public
|
|
imagePost.Profiles = image.Profiles
|
|
|
|
imagePost.Aliases = args.Aliases
|
|
if args.CopyAliases {
|
|
imagePost.Aliases = image.Aliases
|
|
if args.Aliases != nil {
|
|
imagePost.Aliases = append(imagePost.Aliases, args.Aliases...)
|
|
}
|
|
}
|
|
|
|
createArgs := &ImageCreateArgs{
|
|
MetaFile: metaFile,
|
|
MetaName: image.Filename,
|
|
Type: image.Type,
|
|
}
|
|
|
|
if resp.RootfsName != "" {
|
|
// Deal with split images
|
|
createArgs.RootfsFile = rootfsFile
|
|
createArgs.RootfsName = image.Filename
|
|
}
|
|
|
|
rop := remoteOperation{
|
|
chDone: make(chan bool),
|
|
}
|
|
|
|
go func() {
|
|
defer close(rop.chDone)
|
|
|
|
op, err := r.CreateImage(imagePost, createArgs)
|
|
if err != nil {
|
|
rop.err = remoteOperationError("Failed to copy image", nil)
|
|
return
|
|
}
|
|
|
|
rop.handlerLock.Lock()
|
|
rop.targetOp = op
|
|
rop.handlerLock.Unlock()
|
|
|
|
for _, handler := range rop.handlers {
|
|
_, _ = rop.targetOp.AddHandler(handler)
|
|
}
|
|
|
|
err = rop.targetOp.Wait()
|
|
if err != nil {
|
|
rop.err = remoteOperationError("Failed to copy image", nil)
|
|
return
|
|
}
|
|
|
|
// Apply the aliases.
|
|
for _, entry := range imagePost.Aliases {
|
|
alias := api.ImageAliasesPost{}
|
|
alias.Name = entry.Name
|
|
alias.Target = image.Fingerprint
|
|
|
|
err := r.CreateImageAlias(alias)
|
|
if err != nil {
|
|
rop.err = remoteOperationError("Failed to add alias", nil)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return &rop, nil
|
|
}
|
|
|
|
// Prepare the copy request
|
|
req := api.ImagesPost{
|
|
Source: &api.ImagesPostSource{
|
|
ImageSource: api.ImageSource{
|
|
Certificate: info.Certificate,
|
|
Protocol: info.Protocol,
|
|
},
|
|
Fingerprint: image.Fingerprint,
|
|
Mode: "pull",
|
|
Type: "image",
|
|
Project: info.Project,
|
|
},
|
|
ImagePut: api.ImagePut{
|
|
Profiles: image.Profiles,
|
|
},
|
|
}
|
|
|
|
if args != nil {
|
|
req.Source.ImageType = args.Type
|
|
}
|
|
|
|
// Generate secret token if needed
|
|
if !image.Public {
|
|
secret, err := source.GetImageSecret(image.Fingerprint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Source.Secret = secret
|
|
}
|
|
|
|
// Process the arguments
|
|
if args != nil {
|
|
req.Aliases = args.Aliases
|
|
req.AutoUpdate = args.AutoUpdate
|
|
req.Public = args.Public
|
|
|
|
if args.CopyAliases {
|
|
req.Aliases = image.Aliases
|
|
if args.Aliases != nil {
|
|
req.Aliases = append(req.Aliases, args.Aliases...)
|
|
}
|
|
}
|
|
}
|
|
|
|
return r.tryCopyImage(req, info.Addresses)
|
|
}
|
|
|
|
// UpdateImage updates the image definition.
|
|
func (r *ProtocolIncus) UpdateImage(fingerprint string, image api.ImagePut, ETag string) error {
|
|
// Send the request
|
|
_, _, err := r.query("PUT", fmt.Sprintf("/images/%s", url.PathEscape(fingerprint)), image, ETag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteImage requests that Incus removes an image from the store.
|
|
func (r *ProtocolIncus) DeleteImage(fingerprint string) (Operation, error) {
|
|
// Send the request
|
|
op, _, err := r.queryOperation("DELETE", fmt.Sprintf("/images/%s", url.PathEscape(fingerprint)), nil, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return op, nil
|
|
}
|
|
|
|
// RefreshImage requests that Incus issues an image refresh.
|
|
func (r *ProtocolIncus) RefreshImage(fingerprint string) (Operation, error) {
|
|
if !r.HasExtension("image_force_refresh") {
|
|
return nil, fmt.Errorf("The server is missing the required \"image_force_refresh\" API extension")
|
|
}
|
|
|
|
// Send the request
|
|
op, _, err := r.queryOperation("POST", fmt.Sprintf("/images/%s/refresh", url.PathEscape(fingerprint)), nil, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return op, nil
|
|
}
|
|
|
|
// CreateImageSecret requests that Incus issues a temporary image secret.
|
|
func (r *ProtocolIncus) CreateImageSecret(fingerprint string) (Operation, error) {
|
|
// Send the request
|
|
op, _, err := r.queryOperation("POST", fmt.Sprintf("/images/%s/secret", url.PathEscape(fingerprint)), nil, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return op, nil
|
|
}
|
|
|
|
// CreateImageAlias sets up a new image alias.
|
|
func (r *ProtocolIncus) CreateImageAlias(alias api.ImageAliasesPost) error {
|
|
// Send the request
|
|
_, _, err := r.query("POST", "/images/aliases", alias, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateImageAlias updates the image alias definition.
|
|
func (r *ProtocolIncus) UpdateImageAlias(name string, alias api.ImageAliasesEntryPut, ETag string) error {
|
|
// Send the request
|
|
_, _, err := r.query("PUT", fmt.Sprintf("/images/aliases/%s", url.PathEscape(name)), alias, ETag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RenameImageAlias renames an existing image alias.
|
|
func (r *ProtocolIncus) RenameImageAlias(name string, alias api.ImageAliasesEntryPost) error {
|
|
// Send the request
|
|
_, _, err := r.query("POST", fmt.Sprintf("/images/aliases/%s", url.PathEscape(name)), alias, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteImageAlias removes an alias from the Incus image store.
|
|
func (r *ProtocolIncus) DeleteImageAlias(name string) error {
|
|
// Send the request
|
|
_, _, err := r.query("DELETE", fmt.Sprintf("/images/aliases/%s", url.PathEscape(name)), nil, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ExportImage exports (copies) an image to a remote server.
|
|
func (r *ProtocolIncus) ExportImage(fingerprint string, image api.ImageExportPost) (Operation, error) {
|
|
if !r.HasExtension("images_push_relay") {
|
|
return nil, fmt.Errorf("The server is missing the required \"images_push_relay\" API extension")
|
|
}
|
|
|
|
// Send the request
|
|
op, _, err := r.queryOperation("POST", fmt.Sprintf("/images/%s/export", url.PathEscape(fingerprint)), &image, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return op, nil
|
|
}
|