mirror of
https://github.com/containers/podman.git
synced 2025-05-21 00:56:36 +08:00

In #20538, I was asked to consider refactoring the new OCI pull code from within the generic machine directory. This is something I had tried when originally coding it but it became apparent that a much larger refactor to prevent circular deps was needed. Because I did not want to pollute the initial PR with that refactor, I asked for the PR to merge first. This is the refactor that needed to be done. Signed-off-by: Brent Baude <bbaude@redhat.com>
294 lines
7.4 KiB
Go
294 lines
7.4 KiB
Go
//go:build amd64 || arm64
|
|
// +build amd64 arm64
|
|
|
|
package machine
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
url2 "net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/containers/podman/v4/pkg/machine/compression"
|
|
"github.com/containers/podman/v4/pkg/machine/define"
|
|
"github.com/containers/podman/v4/pkg/machine/ocipull"
|
|
"github.com/containers/podman/v4/utils"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// GenericDownload is used when a user provides a URL
|
|
// or path for an image
|
|
type GenericDownload struct {
|
|
Download
|
|
}
|
|
|
|
// NewGenericDownloader is used when the disk image is provided by the user
|
|
func NewGenericDownloader(vmType VMType, vmName, pullPath string) (DistributionDownload, error) {
|
|
var (
|
|
imageName string
|
|
)
|
|
dataDir, err := GetDataDir(vmType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cacheDir, err := GetCacheDir(vmType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dl := Download{}
|
|
// Is pullpath a file or url?
|
|
if getURL := supportedURL(pullPath); getURL != nil {
|
|
urlSplit := strings.Split(getURL.Path, "/")
|
|
imageName = urlSplit[len(urlSplit)-1]
|
|
dl.URL = getURL
|
|
dl.LocalPath = filepath.Join(cacheDir, imageName)
|
|
} else {
|
|
// Dealing with FilePath
|
|
imageName = filepath.Base(pullPath)
|
|
dl.LocalPath = pullPath
|
|
}
|
|
dl.VMName = vmName
|
|
dl.ImageName = imageName
|
|
dl.LocalUncompressedFile = dl.GetLocalUncompressedFile(dataDir)
|
|
// The download needs to be pulled into the datadir
|
|
|
|
gd := GenericDownload{Download: dl}
|
|
return gd, nil
|
|
}
|
|
|
|
func supportedURL(path string) (url *url2.URL) {
|
|
getURL, err := url2.Parse(path)
|
|
if err != nil {
|
|
// ignore error, probably not a URL, fallback & treat as file path
|
|
return nil
|
|
}
|
|
|
|
// Check supported scheme. Since URL is passed to net.http, only http
|
|
// schemes are supported. Also, windows drive paths can resemble a
|
|
// URL, but with a single letter scheme. These values should be
|
|
// passed through for interpretation as a file path.
|
|
switch getURL.Scheme {
|
|
case "http":
|
|
fallthrough
|
|
case "https":
|
|
return getURL
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (dl Download) GetLocalUncompressedFile(dataDir string) string {
|
|
compressedFilename := dl.VMName + "_" + dl.ImageName
|
|
extension := compression.KindFromFile(compressedFilename)
|
|
uncompressedFile := strings.TrimSuffix(compressedFilename, fmt.Sprintf(".%s", extension.String()))
|
|
dl.LocalUncompressedFile = filepath.Join(dataDir, uncompressedFile)
|
|
return dl.LocalUncompressedFile
|
|
}
|
|
|
|
func (g GenericDownload) Get() *Download {
|
|
return &g.Download
|
|
}
|
|
|
|
func (g GenericDownload) HasUsableCache() (bool, error) {
|
|
// If we have a URL for this "downloader", we now pull it
|
|
return g.URL == nil, nil
|
|
}
|
|
|
|
// CleanCache cleans out downloaded uncompressed image files
|
|
func (g GenericDownload) CleanCache() error {
|
|
// Remove any image that has been downloaded via URL
|
|
// We never read from cache for generic downloads
|
|
if g.URL != nil {
|
|
if err := os.Remove(g.LocalPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DownloadImage(d DistributionDownload) error {
|
|
// check if the latest image is already present
|
|
ok, err := d.HasUsableCache()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
if err := DownloadVMImage(d.Get().URL, d.Get().ImageName, d.Get().LocalPath); err != nil {
|
|
return err
|
|
}
|
|
// Clean out old cached images, since we didn't find needed image in cache
|
|
defer func() {
|
|
if err = d.CleanCache(); err != nil {
|
|
logrus.Warnf("error cleaning machine image cache: %s", err)
|
|
}
|
|
}()
|
|
}
|
|
localPath, err := define.NewMachineFile(d.Get().LocalPath, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return compression.Decompress(localPath, d.Get().LocalUncompressedFile)
|
|
}
|
|
|
|
// DownloadVMImage downloads a VM image from url to given path
|
|
// with download status
|
|
func DownloadVMImage(downloadURL *url2.URL, imageName string, localImagePath string) error {
|
|
out, err := os.Create(localImagePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := out.Close(); err != nil {
|
|
logrus.Error(err)
|
|
}
|
|
}()
|
|
|
|
resp, err := http.Get(downloadURL.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
logrus.Error(err)
|
|
}
|
|
}()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("downloading VM image %s: %s", downloadURL, resp.Status)
|
|
}
|
|
size := resp.ContentLength
|
|
prefix := "Downloading VM image: " + imageName
|
|
onComplete := prefix + ": done"
|
|
|
|
p, bar := utils.ProgressBar(prefix, size, onComplete)
|
|
|
|
proxyReader := bar.ProxyReader(resp.Body)
|
|
defer func() {
|
|
if err := proxyReader.Close(); err != nil {
|
|
logrus.Error(err)
|
|
}
|
|
}()
|
|
|
|
if _, err := io.Copy(out, proxyReader); err != nil {
|
|
return err
|
|
}
|
|
|
|
p.Wait()
|
|
return nil
|
|
}
|
|
|
|
func RemoveImageAfterExpire(dir string, expire time.Duration) error {
|
|
now := time.Now()
|
|
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
// Delete any cache files that are older than expiry date
|
|
if !info.IsDir() && (now.Sub(info.ModTime()) > expire) {
|
|
err := os.Remove(path)
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
logrus.Warnf("unable to clean up cached image: %s", path)
|
|
} else {
|
|
logrus.Debugf("cleaning up cached image: %s", path)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return err
|
|
}
|
|
|
|
// AcquireAlternateImage downloads the alternate image the user provided, which
|
|
// can be a file path or URL
|
|
func (dl Download) AcquireAlternateImage(inputPath string) (*define.VMFile, error) {
|
|
g, err := NewGenericDownloader(dl.VMKind, dl.VMName, inputPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
imagePath, err := define.NewMachineFile(g.Get().LocalUncompressedFile, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := DownloadImage(g); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return imagePath, nil
|
|
}
|
|
|
|
func isOci(input string) (bool, *ocipull.OCIKind, error) {
|
|
inputURL, err := url2.Parse(input)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
switch inputURL.Scheme {
|
|
case ocipull.OCIDir.String():
|
|
return true, &ocipull.OCIDir, nil
|
|
case ocipull.OCIRegistry.String():
|
|
return true, &ocipull.OCIRegistry, nil
|
|
}
|
|
return false, nil, nil
|
|
}
|
|
|
|
func Pull(input, machineName string, vp VirtProvider) (*define.VMFile, FCOSStream, error) {
|
|
var (
|
|
disk ocipull.Disker
|
|
)
|
|
|
|
ociBased, ociScheme, err := isOci(input)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if !ociBased {
|
|
// Business as usual
|
|
dl, err := vp.NewDownload(machineName)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return dl.AcquireVMImage(input)
|
|
}
|
|
oopts := ocipull.OCIOpts{
|
|
Scheme: ociScheme,
|
|
}
|
|
dataDir, err := GetDataDir(vp.VMType())
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if ociScheme.IsOCIDir() {
|
|
strippedOCIDir := ocipull.StripOCIReference(input)
|
|
oopts.Dir = &strippedOCIDir
|
|
disk = ocipull.NewOCIDir(context.Background(), input, dataDir, machineName)
|
|
} else {
|
|
// a use of a containers image type here might be
|
|
// tighter
|
|
strippedInput := strings.TrimPrefix(input, "docker://")
|
|
// this is the next piece of work
|
|
if len(strippedInput) > 0 {
|
|
return nil, 0, errors.New("image names are not supported yet")
|
|
}
|
|
disk, err = ocipull.NewVersioned(context.Background(), dataDir, machineName)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
}
|
|
if err := disk.Pull(); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
unpacked, err := disk.Unpack()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer func() {
|
|
logrus.Debugf("cleaning up %q", unpacked.GetPath())
|
|
if err := unpacked.Delete(); err != nil {
|
|
logrus.Errorf("unable to delete local compressed file %q:%v", unpacked.GetPath(), err)
|
|
}
|
|
}()
|
|
imagePath, err := disk.Decompress(unpacked)
|
|
return imagePath, UnknownStream, err
|
|
}
|