iclient/incus_image.go
Rama develop · réplica de lectura del repositorio original.
package iclient
import (
"context"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// incusImagesPath is the collection every image call hangs off.
const incusImagesPath = "/images"
// GetImage returns one image and its ETag.
func (c *Connection) GetImage(ctx context.Context, project string, fingerprint string, args *GetImageArgs) (*api.Image, string, error) {
if args == nil {
args = &GetImageArgs{}
}
var query url.Values
if args.Secret != "" {
query = url.Values{}
query.Set("secret", args.Secret)
}
image := api.Image{}
etag, err := c.getStruct(ctx, project, incusImagesPath+"/"+url.PathEscape(fingerprint), query, &image)
if err != nil {
return nil, "", err
}
return &image, etag, nil
}
// GetImageAlias resolves an alias to the image behind it.
func (c *Connection) GetImageAlias(ctx context.Context, project string, name string, args *GetImageAliasArgs) (*api.ImageAliasesEntry, string, error) {
if args == nil {
args = &GetImageAliasArgs{}
}
path := incusImagesPath + "/aliases/" + url.PathEscape(name)
if args.Type != "" {
path = incusImagesPath + "/aliases/" + url.PathEscape(args.Type) + "/" + url.PathEscape(name)
}
alias := api.ImageAliasesEntry{}
etag, err := c.getStruct(ctx, project, path, nil, &alias)
if err != nil {
return nil, "", err
}
return &alias, etag, nil
}
// CreateImage adds an image, and is also how one is copied in: give
// image.Source a server, a protocol and an alias and incusd fetches it.
//
// With args the tarballs are uploaded instead, and image carries only the
// aliases and properties to record.
func (c *Connection) CreateImage(ctx context.Context, project string, image api.ImagesPost, args *ImageCreateArgs) (<-chan api.Operation, error) {
if args == nil {
return c.asyncOperation(ctx, project, http.MethodPost, incusImagesPath, nil, image, "")
}
if args.MetaFile == nil {
return nil, errors.New("uploading an image: the metadata file is required")
}
header := imageUploadHeader(image)
// A unified image is one tarball; a split one needs the two parts named.
if args.RootfsFile == nil {
return c.asyncUpload(ctx, project, incusImagesPath, args.MetaFile, "application/octet-stream", header)
}
reader, writer := io.Pipe()
form := multipart.NewWriter(writer)
go func() {
// A read error aborts the request rather than uploading a truncated image.
_ = writer.CloseWithError(writeImageParts(form, args))
}()
return c.asyncUpload(ctx, project, incusImagesPath, reader, form.FormDataContentType(), header)
}
// imageUploadHeader carries what an upload cannot put in the body, the body
// being the tarballs. Leaving it out silently drops the image's aliases.
func imageUploadHeader(image api.ImagesPost) http.Header {
header := http.Header{}
if image.Public {
header.Set("X-Incus-public", "true")
}
if image.Filename != "" {
header.Set("X-Incus-filename", image.Filename)
}
if len(image.Properties) > 0 {
properties := url.Values{}
for key, value := range image.Properties {
properties.Set(key, value)
}
header.Set("X-Incus-properties", properties.Encode())
}
if len(image.Profiles) > 0 {
profiles := url.Values{}
for _, profile := range image.Profiles {
profiles.Add("profile", profile)
}
header.Set("X-Incus-profiles", profiles.Encode())
}
if len(image.Aliases) > 0 {
aliases := url.Values{}
for _, alias := range image.Aliases {
aliases.Add("alias", alias.Name)
}
header.Set("X-Incus-aliases", aliases.Encode())
}
return header
}
// writeImageParts streams the metadata and rootfs tarballs into the form.
func writeImageParts(form *multipart.Writer, args *ImageCreateArgs) error {
part, err := form.CreateFormFile("metadata", args.MetaName)
if err != nil {
return err
}
_, err = io.Copy(part, args.MetaFile)
if err != nil {
return err
}
// The field name is how the server tells a container rootfs from a disk image.
field := "rootfs"
if args.Type == "virtual-machine" {
field = "rootfs.img"
}
part, err = form.CreateFormFile(field, args.RootfsName)
if err != nil {
return err
}
_, err = io.Copy(part, args.RootfsFile)
if err != nil {
return err
}
return form.Close()
}
// UpdateImage replaces an image's configuration.
func (c *Connection) UpdateImage(ctx context.Context, project string, fingerprint string, image api.ImagePut, etag string) error {
_, _, err := c.do(ctx, project, http.MethodPut, incusImagesPath+"/"+url.PathEscape(fingerprint), nil, image, etag)
return err
}
// DeleteImage removes an image and follows the operation.
func (c *Connection) DeleteImage(ctx context.Context, project string, fingerprint string) (<-chan api.Operation, error) {
return c.asyncOperation(ctx, project, http.MethodDelete, incusImagesPath+"/"+url.PathEscape(fingerprint), nil, nil, "")
}
// imageSecret mints the one-time token that fetching a non-public image needs.
//
// A token operation never reaches a terminal state, so following it would
// subscribe to events for something that never reports: the response to the
// request already carries the secret.
func (c *Connection) imageSecret(ctx context.Context, project string, fingerprint string) (string, error) {
path := incusImagesPath + "/" + url.PathEscape(fingerprint) + "/secret"
resp, _, err := c.do(ctx, project, http.MethodPost, path, nil, nil, "")
if err != nil {
return "", fmt.Errorf("minting a secret for %q: %w", fingerprint, err)
}
op := api.Operation{}
err = resp.MetadataAsStruct(&op)
if err != nil {
return "", fmt.Errorf("minting a secret for %q: decoding the operation: %w", fingerprint, err)
}
secret, ok := op.Metadata["secret"].(string)
if !ok {
return "", fmt.Errorf("minting a secret for %q: no secret in the operation metadata", fingerprint)
}
return secret, nil
}
// CopyImage copies an image out of sourceProject on source into project here.
func (c *Connection) CopyImage(ctx context.Context, project string, source *Connection, sourceProject string, fingerprint string, args *ImageCopyArgs) (<-chan api.Operation, error) {
if args == nil {
args = &ImageCopyArgs{}
}
image, _, err := source.GetImage(ctx, sourceProject, fingerprint, nil)
if err != nil {
return nil, fmt.Errorf("reading the source image %q: %w", fingerprint, err)
}
info, err := source.GetConnectionInfo(ctx)
if err != nil {
return nil, err
}
if len(info.Addresses) == 0 {
// incusd dials the source over the network even when it is itself.
return nil, fmt.Errorf("copying %q: %w", fingerprint, ErrConnectionNoAddress)
}
mode := args.Mode
if mode == "" {
mode = "pull"
}
post := api.ImagesPost{
Aliases: args.Aliases,
ImagePut: api.ImagePut{
Public: args.Public,
AutoUpdate: args.AutoUpdate,
Profiles: args.Profiles,
},
Source: &api.ImagesPostSource{
ImageSource: api.ImageSource{
Server: info.Addresses[0],
Protocol: "incus",
Certificate: info.Certificate,
ImageType: args.Type,
},
Type: "image",
Mode: mode,
Fingerprint: fingerprint,
Project: sourceProject,
},
}
// A private image is only fetchable with a token.
if !image.Public {
post.Source.Secret, err = source.imageSecret(ctx, sourceProject, fingerprint)
if err != nil {
return nil, err
}
}
return c.CreateImage(ctx, project, post, nil)
}