Files
podman/pkg/machine/config.go
Brent Baude 2430fc71a0 Allow podman machine to download from oci registry
this pr represents a shift in how we download podman machine images.
these images will now be stored in oci registry and will replace the
default method of downloading an image.  you can still use a reference
to a disk image as a path or url too with the --image-path switch.

the final registry and location of the images has not been determined;
and will need to be updated in podman as well.

i dont think we need to allow --image-path to accept a registry/image
for the podman 5.0 release.  i do think there will be demand for this.

upgrades also need to be plumbed.  for example, updating from an oci
registry.

once we make decisions on final image locations/registrties as well as
some behaviors of init and the oci pull, we must update the machine-init
documentation.

Signed-off-by: Brent Baude <bbaude@redhat.com>
2024-02-12 15:19:34 -06:00

412 lines
9.6 KiB
Go

//go:build amd64 || arm64
package machine
import (
"context"
"errors"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/containers/podman/v5/pkg/machine/compression"
"github.com/containers/podman/v5/pkg/machine/define"
"github.com/containers/podman/v5/pkg/machine/vmconfigs"
"github.com/containers/storage/pkg/homedir"
"github.com/sirupsen/logrus"
)
const (
DefaultMachineName string = "podman-machine-default"
apiUpTimeout = 20 * time.Second
)
var (
ForwarderBinaryName = "gvproxy"
)
type Download struct {
Arch string
Artifact define.Artifact
CacheDir string
CompressionType compression.ImageCompression
DataDir string
Format define.ImageFormat
ImageName string
LocalPath string
LocalUncompressedFile string
Sha256sum string
Size int64
URL *url.URL
VMKind define.VMType
VMName string
}
type ListOptions struct{}
type ListResponse struct {
Name string
CreatedAt time.Time
LastUp time.Time
Running bool
Starting bool
Stream string
VMType string
CPUs uint64
Memory uint64
DiskSize uint64
Port int
RemoteUsername string
IdentityPath string
UserModeNetworking bool
}
type SSHOptions struct {
Username string
Args []string
}
type StartOptions struct {
NoInfo bool
Quiet bool
}
type StopOptions struct{}
type RemoveOptions struct {
Force bool
SaveImage bool
SaveIgnition bool
}
type InspectOptions struct{}
// TODO This can be removed when WSL is refactored into podman 5
type VM interface {
Init(opts define.InitOptions) (bool, error)
Inspect() (*InspectInfo, error)
Remove(name string, opts RemoveOptions) (string, func() error, error)
Set(name string, opts define.SetOptions) ([]error, error)
SSH(name string, opts SSHOptions) error
Start(name string, opts StartOptions) error
State(bypass bool) (define.Status, error)
Stop(name string, opts StopOptions) error
}
type DistributionDownload interface {
HasUsableCache() (bool, error)
Get() *Download
CleanCache() error
}
type InspectInfo struct {
ConfigPath define.VMFile
ConnectionInfo ConnectionConfig
Created time.Time
Image ImageConfig
LastUp time.Time
Name string
Resources vmconfigs.ResourceConfig
SSHConfig vmconfigs.SSHConfig
State define.Status
UserModeNetworking bool
Rootful bool
}
// GetCacheDir returns the dir where VM images are downloaded into when pulled
func GetCacheDir(vmType define.VMType) (string, error) {
dataDir, err := GetDataDir(vmType)
if err != nil {
return "", err
}
cacheDir := filepath.Join(dataDir, "cache")
if _, err := os.Stat(cacheDir); !errors.Is(err, os.ErrNotExist) {
return cacheDir, nil
}
return cacheDir, os.MkdirAll(cacheDir, 0755)
}
// GetDataDir returns the filepath where vm images should
// live for podman-machine.
func GetDataDir(vmType define.VMType) (string, error) {
dataDirPrefix, err := DataDirPrefix()
if err != nil {
return "", err
}
dataDir := filepath.Join(dataDirPrefix, vmType.String())
if _, err := os.Stat(dataDir); !errors.Is(err, os.ErrNotExist) {
return dataDir, nil
}
mkdirErr := os.MkdirAll(dataDir, 0755)
return dataDir, mkdirErr
}
// GetGlobalDataDir returns the root of all backends
// for shared machine data.
func GetGlobalDataDir() (string, error) {
dataDir, err := DataDirPrefix()
if err != nil {
return "", err
}
return dataDir, os.MkdirAll(dataDir, 0755)
}
func GetMachineDirs(vmType define.VMType) (*define.MachineDirs, error) {
rtDir, err := getRuntimeDir()
if err != nil {
return nil, err
}
rtDir = filepath.Join(rtDir, "podman")
configDir, err := GetConfDir(vmType)
if err != nil {
return nil, err
}
configDirFile, err := define.NewMachineFile(configDir, nil)
if err != nil {
return nil, err
}
dataDir, err := GetDataDir(vmType)
if err != nil {
return nil, err
}
dataDirFile, err := define.NewMachineFile(dataDir, nil)
if err != nil {
return nil, err
}
imageCacheDir, err := dataDirFile.AppendToNewVMFile("cache", nil)
if err != nil {
return nil, err
}
rtDirFile, err := define.NewMachineFile(rtDir, nil)
if err != nil {
return nil, err
}
dirs := define.MachineDirs{
ConfigDir: configDirFile,
DataDir: dataDirFile,
ImageCacheDir: imageCacheDir,
RuntimeDir: rtDirFile,
}
// make sure all machine dirs are present
if err := os.MkdirAll(rtDir, 0755); err != nil {
return nil, err
}
if err := os.MkdirAll(configDir, 0755); err != nil {
return nil, err
}
// Because this is a mkdirall, we make the image cache dir
// which is a subdir of datadir (so the datadir is made anyway)
err = os.MkdirAll(imageCacheDir.GetPath(), 0755)
return &dirs, err
}
// DataDirPrefix returns the path prefix for all machine data files
func DataDirPrefix() (string, error) {
data, err := homedir.GetDataHome()
if err != nil {
return "", err
}
dataDir := filepath.Join(data, "containers", "podman", "machine")
return dataDir, nil
}
// GetConfigDir returns the filepath to where configuration
// files for podman-machine should live
func GetConfDir(vmType define.VMType) (string, error) {
confDirPrefix, err := ConfDirPrefix()
if err != nil {
return "", err
}
confDir := filepath.Join(confDirPrefix, vmType.String())
if _, err := os.Stat(confDir); !errors.Is(err, os.ErrNotExist) {
return confDir, nil
}
mkdirErr := os.MkdirAll(confDir, 0755)
return confDir, mkdirErr
}
// ConfDirPrefix returns the path prefix for all machine config files
func ConfDirPrefix() (string, error) {
conf, err := homedir.GetConfigHome()
if err != nil {
return "", err
}
confDir := filepath.Join(conf, "containers", "podman", "machine")
return confDir, nil
}
// GetSSHIdentityPath returns the path to the expected SSH private key
func GetSSHIdentityPath(name string) (string, error) {
datadir, err := GetGlobalDataDir()
if err != nil {
return "", err
}
return filepath.Join(datadir, name), nil
}
// ImageConfig describes the bootable image for the VM
type ImageConfig struct {
// IgnitionFile is the path to the filesystem where the
// ignition file was written (if needs one)
IgnitionFile define.VMFile `json:"IgnitionFilePath"`
// ImageStream is the update stream for the image
ImageStream string
// ImageFile is the fq path to
ImagePath define.VMFile `json:"ImagePath"`
}
// ConnectionConfig contains connections like sockets, etc.
type ConnectionConfig struct {
// PodmanSocket is the exported podman service socket
PodmanSocket *define.VMFile `json:"PodmanSocket"`
// PodmanPipe is the exported podman service named pipe (Windows hosts only)
PodmanPipe *define.VMFile `json:"PodmanPipe"`
}
type APIForwardingState int
const (
NoForwarding APIForwardingState = iota
ClaimUnsupported
NotInstalled
MachineLocal
DockerGlobal
)
// TODO THis should be able to be removed once WSL is refactored for podman5
type Virtualization struct {
artifact define.Artifact
compression compression.ImageCompression
format define.ImageFormat
vmKind define.VMType
}
func (p *Virtualization) Artifact() define.Artifact {
return p.artifact
}
func (p *Virtualization) Compression() compression.ImageCompression {
return p.compression
}
func (p *Virtualization) Format() define.ImageFormat {
return p.format
}
func (p *Virtualization) VMType() define.VMType {
return p.vmKind
}
func (p *Virtualization) NewDownload(vmName string) (Download, error) {
cacheDir, err := GetCacheDir(p.VMType())
if err != nil {
return Download{}, err
}
dataDir, err := GetDataDir(p.VMType())
if err != nil {
return Download{}, err
}
return Download{
Artifact: p.Artifact(),
CacheDir: cacheDir,
CompressionType: p.Compression(),
DataDir: dataDir,
Format: p.Format(),
VMKind: p.VMType(),
VMName: vmName,
}, nil
}
func NewVirtualization(artifact define.Artifact, compression compression.ImageCompression, format define.ImageFormat, vmKind define.VMType) Virtualization {
return Virtualization{
artifact,
compression,
format,
vmKind,
}
}
func dialSocket(socket string, timeout time.Duration) (net.Conn, error) {
scheme := "unix"
if strings.Contains(socket, "://") {
url, err := url.Parse(socket)
if err != nil {
return nil, err
}
scheme = url.Scheme
socket = url.Path
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var dial func() (net.Conn, error)
switch scheme {
default:
fallthrough
case "unix":
dial = func() (net.Conn, error) {
var dialer net.Dialer
return dialer.DialContext(ctx, "unix", socket)
}
case "npipe":
dial = func() (net.Conn, error) {
return DialNamedPipe(ctx, socket)
}
}
backoff := 500 * time.Millisecond
for {
conn, err := dial()
if !errors.Is(err, os.ErrNotExist) {
return conn, err
}
select {
case <-time.After(backoff):
backoff *= 2
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
func WaitAndPingAPI(sock string) {
client := http.Client{
Transport: &http.Transport{
DialContext: func(context.Context, string, string) (net.Conn, error) {
con, err := dialSocket(sock, apiUpTimeout)
if err != nil {
return nil, err
}
if err := con.SetDeadline(time.Now().Add(apiUpTimeout)); err != nil {
return nil, err
}
return con, nil
},
},
}
resp, err := client.Get("http://host/_ping")
if err == nil {
defer resp.Body.Close()
}
if err != nil || resp.StatusCode != 200 {
logrus.Warn("API socket failed ping test")
}
}