diff --git a/pkg/machine/applehv/config.go b/pkg/machine/applehv/config.go index 3da8fa5581..be4dfabbe9 100644 --- a/pkg/machine/applehv/config.go +++ b/pkg/machine/applehv/config.go @@ -11,6 +11,8 @@ import ( "time" "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/compression" + "github.com/containers/podman/v4/pkg/machine/define" vfConfig "github.com/crc-org/vfkit/pkg/config" "github.com/docker/go-units" "golang.org/x/sys/unix" @@ -34,7 +36,7 @@ type MMHardwareConfig struct { func VirtualizationProvider() machine.VirtProvider { return &AppleHVVirtualization{ - machine.NewVirtualization(machine.AppleHV, machine.Xz, machine.Raw, vmtype), + machine.NewVirtualization(define.AppleHV, compression.Xz, define.Raw, vmtype), } } @@ -116,7 +118,7 @@ func (v AppleHVVirtualization) NewMachine(opts machine.InitOptions) (machine.VM, return nil, err } - configPath, err := machine.NewMachineFile(getVMConfigPath(configDir, opts.Name), nil) + configPath, err := define.NewMachineFile(getVMConfigPath(configDir, opts.Name), nil) if err != nil { return nil, err } @@ -127,7 +129,7 @@ func (v AppleHVVirtualization) NewMachine(opts machine.InitOptions) (machine.VM, return nil, err } - ignitionPath, err := machine.NewMachineFile(filepath.Join(configDir, m.Name)+".ign", nil) + ignitionPath, err := define.NewMachineFile(filepath.Join(configDir, m.Name)+".ign", nil) if err != nil { return nil, err } diff --git a/pkg/machine/applehv/ignition.go b/pkg/machine/applehv/ignition.go index cffff0187c..68e23d1596 100644 --- a/pkg/machine/applehv/ignition.go +++ b/pkg/machine/applehv/ignition.go @@ -7,13 +7,13 @@ import ( "net" "net/http" - "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/sirupsen/logrus" ) // serveIgnitionOverSock allows podman to open a small httpd instance on the vsock between the host // and guest to inject the ignitionfile into fcos -func (m *MacMachine) serveIgnitionOverSock(ignitionSocket *machine.VMFile) error { +func (m *MacMachine) serveIgnitionOverSock(ignitionSocket *define.VMFile) error { logrus.Debugf("reading ignition file: %s", m.IgnitionFile.GetPath()) ignFile, err := m.IgnitionFile.Read() if err != nil { diff --git a/pkg/machine/applehv/machine.go b/pkg/machine/applehv/machine.go index 7edbb55b36..8da6f3f74c 100644 --- a/pkg/machine/applehv/machine.go +++ b/pkg/machine/applehv/machine.go @@ -20,6 +20,7 @@ import ( "github.com/containers/common/pkg/config" gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types" "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/containers/podman/v4/pkg/strongunits" "github.com/containers/podman/v4/pkg/util" "github.com/containers/podman/v4/utils" @@ -45,13 +46,13 @@ const ( type VfkitHelper struct { LogLevel logrus.Level Endpoint string - VfkitBinaryPath *machine.VMFile + VfkitBinaryPath *define.VMFile VirtualMachine *vfConfig.VirtualMachine } type MacMachine struct { // ConfigPath is the fully qualified path to the configuration file - ConfigPath machine.VMFile + ConfigPath define.VMFile // HostUser contains info about host user machine.HostUser // ImageConfig describes the bootable image @@ -61,7 +62,7 @@ type MacMachine struct { // Name of VM Name string // ReadySocket tells host when vm is booted - ReadySocket machine.VMFile + ReadySocket define.VMFile // ResourceConfig is physical attrs of the VM machine.ResourceConfig // SSHConfig for accessing the remote vm @@ -74,9 +75,9 @@ type MacMachine struct { LastUp time.Time // The VFKit endpoint where we can interact with the VM Vfkit VfkitHelper - LogPath machine.VMFile - GvProxyPid machine.VMFile - GvProxySock machine.VMFile + LogPath define.VMFile + GvProxyPid define.VMFile + GvProxySock define.VMFile // Used at runtime for serializing write operations lock *lockfile.LockFile @@ -84,7 +85,7 @@ type MacMachine struct { // setGVProxyInfo sets the VM's gvproxy pid and socket files func (m *MacMachine) setGVProxyInfo(runtimeDir string) error { - gvProxyPid, err := machine.NewMachineFile(filepath.Join(runtimeDir, "gvproxy.pid"), nil) + gvProxyPid, err := define.NewMachineFile(filepath.Join(runtimeDir, "gvproxy.pid"), nil) if err != nil { return err } @@ -95,7 +96,7 @@ func (m *MacMachine) setGVProxyInfo(runtimeDir string) error { // setVfkitInfo stores the default devices, sets the vfkit endpoint, and // locates/stores the path to the binary -func (m *MacMachine) setVfkitInfo(cfg *config.Config, readySocket machine.VMFile) error { +func (m *MacMachine) setVfkitInfo(cfg *config.Config, readySocket define.VMFile) error { defaultDevices, err := getDefaultDevices(m.ImagePath.GetPath(), m.LogPath.GetPath(), readySocket.GetPath()) if err != nil { return err @@ -105,7 +106,7 @@ func (m *MacMachine) setVfkitInfo(cfg *config.Config, readySocket machine.VMFile if err != nil { return err } - vfkitBinaryPath, err := machine.NewMachineFile(vfkitPath, nil) + vfkitBinaryPath, err := define.NewMachineFile(vfkitPath, nil) if err != nil { return err } @@ -217,7 +218,7 @@ func (m *MacMachine) Init(opts machine.InitOptions) (bool, error) { m.ImagePath = *imagePath m.ImageStream = strm.String() - logPath, err := machine.NewMachineFile(filepath.Join(dataDir, fmt.Sprintf("%s.log", m.Name)), nil) + logPath, err := define.NewMachineFile(filepath.Join(dataDir, fmt.Sprintf("%s.log", m.Name)), nil) if err != nil { return false, err } @@ -490,7 +491,7 @@ func (m *MacMachine) SSH(name string, opts machine.SSHOptions) error { // deleteIgnitionSocket retrieves the ignition socket, deletes it, and returns a // pointer to the `VMFile` -func (m *MacMachine) deleteIgnitionSocket() (*machine.VMFile, error) { +func (m *MacMachine) deleteIgnitionSocket() (*define.VMFile, error) { ignitionSocket, err := m.getIgnitionSock() if err != nil { return nil, err @@ -556,7 +557,7 @@ func (m *MacMachine) addVolumesToVfKit() error { } func (m *MacMachine) Start(name string, opts machine.StartOptions) error { - var ignitionSocket *machine.VMFile + var ignitionSocket *define.VMFile m.lock.Lock() defer m.lock.Unlock() @@ -996,13 +997,13 @@ func (m *MacMachine) dockerSock() (string, error) { return filepath.Join(dd, "podman.sock"), nil } -func (m *MacMachine) forwardSocketPath() (*machine.VMFile, error) { +func (m *MacMachine) forwardSocketPath() (*define.VMFile, error) { sockName := "podman.sock" path, err := machine.GetDataDir(machine.AppleHvVirt) if err != nil { return nil, fmt.Errorf("Resolving data dir: %s", err.Error()) } - return machine.NewMachineFile(filepath.Join(path, sockName), &sockName) + return define.NewMachineFile(filepath.Join(path, sockName), &sockName) } // resizeDisk uses os truncate to resize (only larger) a raw disk. the input size @@ -1034,7 +1035,7 @@ func (m *MacMachine) isFirstBoot() (bool, error) { return m.LastUp == never, nil } -func (m *MacMachine) getIgnitionSock() (*machine.VMFile, error) { +func (m *MacMachine) getIgnitionSock() (*define.VMFile, error) { dataDir, err := machine.GetDataDir(machine.AppleHvVirt) if err != nil { return nil, err @@ -1044,7 +1045,7 @@ func (m *MacMachine) getIgnitionSock() (*machine.VMFile, error) { return nil, err } } - return machine.NewMachineFile(filepath.Join(dataDir, ignitionSocketName), nil) + return define.NewMachineFile(filepath.Join(dataDir, ignitionSocketName), nil) } func (m *MacMachine) getRuntimeDir() (string, error) { diff --git a/pkg/machine/compression/compression_test.go b/pkg/machine/compression/compression_test.go new file mode 100644 index 0000000000..afffce1e5b --- /dev/null +++ b/pkg/machine/compression/compression_test.go @@ -0,0 +1,91 @@ +package compression + +import "testing" + +func Test_compressionFromFile(t *testing.T) { + type args struct { + path string + } + var tests = []struct { + name string + args args + want ImageCompression + }{ + { + name: "xz", + args: args{ + path: "/tmp/foo.xz", + }, + want: Xz, + }, + { + name: "gzip", + args: args{ + path: "/tmp/foo.gz", + }, + want: Gz, + }, + { + name: "bz2", + args: args{ + path: "/tmp/foo.bz2", + }, + want: Bz2, + }, + { + name: "default is xz", + args: args{ + path: "/tmp/foo", + }, + want: Xz, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := KindFromFile(tt.args.path); got != tt.want { + t.Errorf("KindFromFile() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestImageCompression_String(t *testing.T) { + tests := []struct { + name string + c ImageCompression + want string + }{ + { + name: "xz", + c: Xz, + want: "xz", + }, + { + name: "gz", + c: Gz, + want: "gz", + }, + { + name: "bz2", + c: Bz2, + want: "bz2", + }, + { + name: "zip", + c: Zip, + want: "zip", + }, + { + name: "xz is default", + c: 99, + want: "xz", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.c.String(); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/machine/compression/config.go b/pkg/machine/compression/config.go new file mode 100644 index 0000000000..0201f75331 --- /dev/null +++ b/pkg/machine/compression/config.go @@ -0,0 +1,36 @@ +package compression + +import "strings" + +type ImageCompression int64 + +const ( + Xz ImageCompression = iota + Zip + Gz + Bz2 +) + +func KindFromFile(path string) ImageCompression { + switch { + case strings.HasSuffix(path, Bz2.String()): + return Bz2 + case strings.HasSuffix(path, Gz.String()): + return Gz + case strings.HasSuffix(path, Zip.String()): + return Zip + } + return Xz +} + +func (c ImageCompression) String() string { + switch c { + case Gz: + return "gz" + case Zip: + return "zip" + case Bz2: + return "bz2" + } + return "xz" +} diff --git a/pkg/machine/compression/decompress.go b/pkg/machine/compression/decompress.go new file mode 100644 index 0000000000..f5b2a16cc4 --- /dev/null +++ b/pkg/machine/compression/decompress.go @@ -0,0 +1,184 @@ +package compression + +import ( + "archive/zip" + "bufio" + "errors" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/containers/image/v5/pkg/compression" + "github.com/containers/podman/v4/pkg/machine/define" + "github.com/containers/podman/v4/utils" + "github.com/containers/storage/pkg/archive" + "github.com/sirupsen/logrus" + "github.com/ulikunitz/xz" +) + +func Decompress(localPath *define.VMFile, uncompressedPath string) error { + var isZip bool + uncompressedFileWriter, err := os.OpenFile(uncompressedPath, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return err + } + sourceFile, err := localPath.Read() + if err != nil { + return err + } + if strings.HasSuffix(localPath.GetPath(), ".zip") { + isZip = true + } + prefix := "Copying uncompressed file" + compressionType := archive.DetectCompression(sourceFile) + if compressionType != archive.Uncompressed || isZip { + prefix = "Extracting compressed file" + } + prefix += ": " + filepath.Base(uncompressedPath) + if compressionType == archive.Xz { + return decompressXZ(prefix, localPath.GetPath(), uncompressedFileWriter) + } + if isZip && runtime.GOOS == "windows" { + return decompressZip(prefix, localPath.GetPath(), uncompressedFileWriter) + } + return decompressEverythingElse(prefix, localPath.GetPath(), uncompressedFileWriter) +} + +// Will error out if file without .Xz already exists +// Maybe extracting then renaming is a good idea here.. +// depends on Xz: not pre-installed on mac, so it becomes a brew dependency +func decompressXZ(prefix string, src string, output io.WriteCloser) error { + var read io.Reader + var cmd *exec.Cmd + + stat, err := os.Stat(src) + if err != nil { + return err + } + file, err := os.Open(src) + if err != nil { + return err + } + defer file.Close() + + p, bar := utils.ProgressBar(prefix, stat.Size(), prefix+": done") + proxyReader := bar.ProxyReader(file) + defer func() { + if err := proxyReader.Close(); err != nil { + logrus.Error(err) + } + }() + + // Prefer Xz utils for fastest performance, fallback to go xi2 impl + if _, err := exec.LookPath("xz"); err == nil { + cmd = exec.Command("xz", "-d", "-c") + cmd.Stdin = proxyReader + read, err = cmd.StdoutPipe() + if err != nil { + return err + } + cmd.Stderr = os.Stderr + } else { + // This XZ implementation is reliant on buffering. It is also 3x+ slower than XZ utils. + // Consider replacing with a faster implementation (e.g. xi2) if podman machine is + // updated with a larger image for the distribution base. + buf := bufio.NewReader(proxyReader) + read, err = xz.NewReader(buf) + if err != nil { + return err + } + } + + done := make(chan bool) + go func() { + if _, err := io.Copy(output, read); err != nil { + logrus.Error(err) + } + output.Close() + done <- true + }() + + if cmd != nil { + err := cmd.Start() + if err != nil { + return err + } + p.Wait() + return cmd.Wait() + } + <-done + p.Wait() + return nil +} + +func decompressEverythingElse(prefix string, src string, output io.WriteCloser) error { + stat, err := os.Stat(src) + if err != nil { + return err + } + f, err := os.Open(src) + if err != nil { + return err + } + p, bar := utils.ProgressBar(prefix, stat.Size(), prefix+": done") + proxyReader := bar.ProxyReader(f) + defer func() { + if err := proxyReader.Close(); err != nil { + logrus.Error(err) + } + }() + uncompressStream, _, err := compression.AutoDecompress(proxyReader) + if err != nil { + return err + } + defer func() { + if err := uncompressStream.Close(); err != nil { + logrus.Error(err) + } + if err := output.Close(); err != nil { + logrus.Error(err) + } + }() + + _, err = io.Copy(output, uncompressStream) + p.Wait() + return err +} + +func decompressZip(prefix string, src string, output io.WriteCloser) error { + zipReader, err := zip.OpenReader(src) + if err != nil { + return err + } + if len(zipReader.File) != 1 { + return errors.New("machine image files should consist of a single compressed file") + } + f, err := zipReader.File[0].Open() + if err != nil { + return err + } + defer func() { + if err := f.Close(); err != nil { + logrus.Error(err) + } + }() + defer func() { + if err := output.Close(); err != nil { + logrus.Error(err) + } + }() + size := int64(zipReader.File[0].CompressedSize64) + p, bar := utils.ProgressBar(prefix, size, prefix+": done") + proxyReader := bar.ProxyReader(f) + defer func() { + if err := proxyReader.Close(); err != nil { + logrus.Error(err) + } + }() + _, err = io.Copy(output, proxyReader) + p.Wait() + return err +} diff --git a/pkg/machine/config.go b/pkg/machine/config.go index b65e8d581e..96c283b7cc 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -15,6 +15,8 @@ import ( "strings" "time" + "github.com/containers/podman/v4/pkg/machine/compression" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/containers/storage/pkg/homedir" "github.com/containers/storage/pkg/lockfile" "github.com/sirupsen/logrus" @@ -64,11 +66,11 @@ var ( type Download struct { Arch string - Artifact Artifact + Artifact define.Artifact CacheDir string - CompressionType ImageCompression + CompressionType compression.ImageCompression DataDir string - Format ImageFormat + Format define.ImageFormat ImageName string LocalPath string LocalUncompressedFile string @@ -162,7 +164,7 @@ type DistributionDownload interface { CleanCache() error } type InspectInfo struct { - ConfigPath VMFile + ConfigPath define.VMFile ConnectionInfo ConnectionConfig Created time.Time Image ImageConfig @@ -269,15 +271,6 @@ func ConfDirPrefix() (string, error) { return confDir, nil } -// GuardedRemoveAll functions much like os.RemoveAll but -// will not delete certain catastrophic paths. -func GuardedRemoveAll(path string) error { - if path == "" || path == "/" { - return fmt.Errorf("refusing to recursively delete `%s`", path) - } - return os.RemoveAll(path) -} - // ResourceConfig describes physical attributes of the machine type ResourceConfig struct { // CPUs to be assigned to the VM @@ -288,77 +281,6 @@ type ResourceConfig struct { Memory uint64 } -const maxSocketPathLength int = 103 - -type VMFile struct { - // Path is the fully qualified path to a file - Path string - // Symlink is a shortened version of Path by using - // a symlink - Symlink *string `json:"symlink,omitempty"` -} - -// GetPath returns the working path for a machinefile. it returns -// the symlink unless one does not exist -func (m *VMFile) GetPath() string { - if m.Symlink == nil { - return m.Path - } - return *m.Symlink -} - -// Delete removes the machinefile symlink (if it exists) and -// the actual path -func (m *VMFile) Delete() error { - if m.Symlink != nil { - if err := os.Remove(*m.Symlink); err != nil && !errors.Is(err, os.ErrNotExist) { - logrus.Errorf("unable to remove symlink %q", *m.Symlink) - } - } - if err := os.Remove(m.Path); err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - return nil -} - -// Read the contents of a given file and return in []bytes -func (m *VMFile) Read() ([]byte, error) { - return os.ReadFile(m.GetPath()) -} - -// NewMachineFile is a constructor for VMFile -func NewMachineFile(path string, symlink *string) (*VMFile, error) { - if len(path) < 1 { - return nil, errors.New("invalid machine file path") - } - if symlink != nil && len(*symlink) < 1 { - return nil, errors.New("invalid symlink path") - } - mf := VMFile{Path: path} - if symlink != nil && len(path) > maxSocketPathLength { - if err := mf.makeSymlink(symlink); err != nil && !errors.Is(err, os.ErrExist) { - return nil, err - } - } - return &mf, nil -} - -// makeSymlink for macOS creates a symlink in $HOME/.podman/ -// for a machinefile like a socket -func (m *VMFile) makeSymlink(symlink *string) error { - homeDir, err := os.UserHomeDir() - if err != nil { - return err - } - sl := filepath.Join(homeDir, ".podman", *symlink) - // make the symlink dir and throw away if it already exists - if err := os.MkdirAll(filepath.Dir(sl), 0700); err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - m.Symlink = &sl - return os.Symlink(m.Path, sl) -} - type Mount struct { ReadOnly bool Source string @@ -371,11 +293,11 @@ type Mount struct { type ImageConfig struct { // IgnitionFile is the path to the filesystem where the // ignition file was written (if needs one) - IgnitionFile VMFile `json:"IgnitionFilePath"` + IgnitionFile define.VMFile `json:"IgnitionFilePath"` // ImageStream is the update stream for the image ImageStream string // ImageFile is the fq path to - ImagePath VMFile `json:"ImagePath"` + ImagePath define.VMFile `json:"ImagePath"` } // HostUser describes the host user @@ -401,9 +323,9 @@ type SSHConfig struct { // ConnectionConfig contains connections like sockets, etc. type ConnectionConfig struct { // PodmanSocket is the exported podman service socket - PodmanSocket *VMFile `json:"PodmanSocket"` + PodmanSocket *define.VMFile `json:"PodmanSocket"` // PodmanPipe is the exported podman service named pipe (Windows hosts only) - PodmanPipe *VMFile `json:"PodmanPipe"` + PodmanPipe *define.VMFile `json:"PodmanPipe"` } type VMType int64 @@ -455,10 +377,10 @@ func ParseVMType(input string, emptyFallback VMType) (VMType, error) { } type VirtProvider interface { //nolint:interfacebloat - Artifact() Artifact + Artifact() define.Artifact CheckExclusiveActiveVM() (bool, string, error) - Compression() ImageCompression - Format() ImageFormat + Compression() compression.ImageCompression + Format() define.ImageFormat IsValidVMName(name string) (bool, error) List(opts ListOptions) ([]*ListResponse, error) LoadVMByName(name string) (VM, error) @@ -469,21 +391,21 @@ type VirtProvider interface { //nolint:interfacebloat } type Virtualization struct { - artifact Artifact - compression ImageCompression - format ImageFormat + artifact define.Artifact + compression compression.ImageCompression + format define.ImageFormat vmKind VMType } -func (p *Virtualization) Artifact() Artifact { +func (p *Virtualization) Artifact() define.Artifact { return p.artifact } -func (p *Virtualization) Compression() ImageCompression { +func (p *Virtualization) Compression() compression.ImageCompression { return p.compression } -func (p *Virtualization) Format() ImageFormat { +func (p *Virtualization) Format() define.ImageFormat { return p.format } @@ -513,7 +435,7 @@ func (p *Virtualization) NewDownload(vmName string) (Download, error) { }, nil } -func NewVirtualization(artifact Artifact, compression ImageCompression, format ImageFormat, vmKind VMType) Virtualization { +func NewVirtualization(artifact define.Artifact, compression compression.ImageCompression, format define.ImageFormat, vmKind VMType) Virtualization { return Virtualization{ artifact, compression, @@ -579,10 +501,10 @@ func (dl Download) NewFcosDownloader(imageStream FCOSStream) (DistributionDownlo // AcquireVMImage determines if the image is already in a FCOS stream. If so, // retrieves the image path of the uncompressed file. Otherwise, the user has // provided an alternative image, so we set the image path and download the image. -func (dl Download) AcquireVMImage(imagePath string) (*VMFile, FCOSStream, error) { +func (dl Download) AcquireVMImage(imagePath string) (*define.VMFile, FCOSStream, error) { var ( err error - imageLocation *VMFile + imageLocation *define.VMFile fcosStream FCOSStream ) @@ -600,7 +522,7 @@ func (dl Download) AcquireVMImage(imagePath string) (*VMFile, FCOSStream, error) return nil, 0, err } - imageLocation, err = NewMachineFile(dd.Get().LocalUncompressedFile, nil) + imageLocation, err = define.NewMachineFile(dd.Get().LocalUncompressedFile, nil) if err != nil { return nil, 0, err } diff --git a/pkg/machine/define/image_format.go b/pkg/machine/define/image_format.go new file mode 100644 index 0000000000..9b2766d638 --- /dev/null +++ b/pkg/machine/define/image_format.go @@ -0,0 +1,34 @@ +package define + +type ImageFormat int64 + +const ( + Qcow ImageFormat = iota + Vhdx + Tar + Raw +) + +func (imf ImageFormat) Kind() string { + switch imf { + case Vhdx: + return "vhdx" + case Tar: + return "tar" + case Raw: + return "raw" + } + return "qcow2" +} + +func (imf ImageFormat) KindWithCompression() string { + switch imf { + case Vhdx: + return "vhdx.zip" + case Tar: + return "tar.xz" + case Raw: + return "raw.gz" + } + return "qcow2.xz" +} diff --git a/pkg/machine/define/image_format_test.go b/pkg/machine/define/image_format_test.go new file mode 100644 index 0000000000..50ca6b55d2 --- /dev/null +++ b/pkg/machine/define/image_format_test.go @@ -0,0 +1,74 @@ +package define + +import "testing" + +func TestImageFormat_Kind(t *testing.T) { + tests := []struct { + name string + imf ImageFormat + want string + }{ + { + name: "vhdx", + imf: Vhdx, + want: "vhdx", + }, + { + name: "qcow2", + imf: Qcow, + want: "qcow2", + }, + { + name: "raw", + imf: Raw, + want: "raw", + }, + { + name: "tar", + imf: Tar, + want: "tar", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.imf.Kind(); got != tt.want { + t.Errorf("Kind() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestImageFormat_KindWithCompression(t *testing.T) { + tests := []struct { + name string + imf ImageFormat + want string + }{ + { + name: "vhdx.zip", + imf: Vhdx, + want: "vhdx.zip", + }, + { + name: "qcow2", + imf: Qcow, + want: "qcow2.xz", + }, + { + name: "raw.gz", + imf: Raw, + want: "raw.gz", + }, { + name: "tar.xz", + imf: Tar, + want: "tar.xz", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.imf.KindWithCompression(); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/machine/define/machine_artifact.go b/pkg/machine/define/machine_artifact.go new file mode 100644 index 0000000000..c714e5dd51 --- /dev/null +++ b/pkg/machine/define/machine_artifact.go @@ -0,0 +1,20 @@ +package define + +type Artifact int64 + +const ( + Qemu Artifact = iota + HyperV + AppleHV + None +) + +func (a Artifact) String() string { + switch a { + case HyperV: + return "hyperv" + case AppleHV: + return "applehv" + } + return "qemu" +} diff --git a/pkg/machine/define/machine_artifact_test.go b/pkg/machine/define/machine_artifact_test.go new file mode 100644 index 0000000000..187bf14502 --- /dev/null +++ b/pkg/machine/define/machine_artifact_test.go @@ -0,0 +1,35 @@ +package define + +import ( + "testing" +) + +func Test_artifact_String(t *testing.T) { + tests := []struct { + name string + a Artifact + want string + }{ + { + name: "qemu", + a: Qemu, + want: "qemu", + }, + { + name: "hyperv", + a: HyperV, + want: "hyperv", + }, { + name: "applehv", + a: AppleHV, + want: "applehv", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.a.String(); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/machine/define/vmfile.go b/pkg/machine/define/vmfile.go new file mode 100644 index 0000000000..b5ee45f8b5 --- /dev/null +++ b/pkg/machine/define/vmfile.go @@ -0,0 +1,80 @@ +package define + +import ( + "errors" + "os" + "path/filepath" + + "github.com/sirupsen/logrus" +) + +const MaxSocketPathLength int = 103 + +type VMFile struct { + // Path is the fully qualified path to a file + Path string + // Symlink is a shortened version of Path by using + // a symlink + Symlink *string `json:"symlink,omitempty"` +} + +// GetPath returns the working path for a machinefile. it returns +// the symlink unless one does not exist +func (m *VMFile) GetPath() string { + if m.Symlink == nil { + return m.Path + } + return *m.Symlink +} + +// Delete removes the machinefile symlink (if it exists) and +// the actual path +func (m *VMFile) Delete() error { + if m.Symlink != nil { + if err := os.Remove(*m.Symlink); err != nil && !errors.Is(err, os.ErrNotExist) { + logrus.Errorf("unable to remove symlink %q", *m.Symlink) + } + } + if err := os.Remove(m.Path); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + return nil +} + +// Read the contents of a given file and return in []bytes +func (m *VMFile) Read() ([]byte, error) { + return os.ReadFile(m.GetPath()) +} + +// NewMachineFile is a constructor for VMFile +func NewMachineFile(path string, symlink *string) (*VMFile, error) { + if len(path) < 1 { + return nil, errors.New("invalid machine file path") + } + if symlink != nil && len(*symlink) < 1 { + return nil, errors.New("invalid symlink path") + } + mf := VMFile{Path: path} + if symlink != nil && len(path) > MaxSocketPathLength { + if err := mf.makeSymlink(symlink); err != nil && !errors.Is(err, os.ErrExist) { + return nil, err + } + } + return &mf, nil +} + +// makeSymlink for macOS creates a symlink in $HOME/.podman/ +// for a machinefile like a socket +func (m *VMFile) makeSymlink(symlink *string) error { + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + sl := filepath.Join(homeDir, ".podman", *symlink) + // make the symlink dir and throw away if it already exists + if err := os.MkdirAll(filepath.Dir(sl), 0700); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + m.Symlink = &sl + return os.Symlink(m.Path, sl) +} diff --git a/pkg/machine/qemu/config_test.go b/pkg/machine/define/vmfile_test.go similarity index 89% rename from pkg/machine/qemu/config_test.go rename to pkg/machine/define/vmfile_test.go index 72cb3ed900..2f4f5948fe 100644 --- a/pkg/machine/qemu/config_test.go +++ b/pkg/machine/define/vmfile_test.go @@ -1,7 +1,7 @@ //go:build (amd64 && !windows) || (arm64 && !windows) // +build amd64,!windows arm64,!windows -package qemu +package define import ( "os" @@ -9,7 +9,6 @@ import ( "reflect" "testing" - "github.com/containers/podman/v4/pkg/machine" "github.com/containers/podman/v4/test/utils" ) @@ -41,7 +40,7 @@ func TestMachineFile_GetPath(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - m := &machine.VMFile{ + m := &VMFile{ Path: tt.fields.Path, //nolint: scopelint Symlink: tt.fields.Symlink, //nolint: scopelint } @@ -75,7 +74,7 @@ func TestNewMachineFile(t *testing.T) { sym := "my.sock" longSym := filepath.Join(homedir, ".podman", sym) - m := machine.VMFile{ + m := VMFile{ Path: p, Symlink: nil, } @@ -86,7 +85,7 @@ func TestNewMachineFile(t *testing.T) { tests := []struct { name string args args - want *machine.VMFile + want *VMFile wantErr bool }{ { @@ -98,7 +97,7 @@ func TestNewMachineFile(t *testing.T) { { name: "Good with short symlink", args: args{p, &sym}, - want: &machine.VMFile{Path: p}, + want: &VMFile{Path: p}, wantErr: false, }, { @@ -116,14 +115,14 @@ func TestNewMachineFile(t *testing.T) { { name: "Good with long symlink", args: args{longp, &sym}, - want: &machine.VMFile{Path: longp, Symlink: &longSym}, + want: &VMFile{Path: longp, Symlink: &longSym}, wantErr: false, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - got, err := machine.NewMachineFile(tt.args.path, tt.args.symlink) + got, err := NewMachineFile(tt.args.path, tt.args.symlink) if (err != nil) != tt.wantErr { t.Errorf("NewMachineFile() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/machine/e2e/init_test.go b/pkg/machine/e2e/init_test.go index 85afbdf4d0..317e60c5f5 100644 --- a/pkg/machine/e2e/init_test.go +++ b/pkg/machine/e2e/init_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/utils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gexec" @@ -188,7 +189,7 @@ var _ = Describe("podman machine init", func() { _, err = os.CreateTemp(tmpDir, "example") Expect(err).ToNot(HaveOccurred()) mount := tmpDir + ":/testmountdir" - defer func() { _ = machine.GuardedRemoveAll(tmpDir) }() + defer func() { _ = utils.GuardedRemoveAll(tmpDir) }() name := randomString() i := new(initMachine) diff --git a/pkg/machine/e2e/machine_test.go b/pkg/machine/e2e/machine_test.go index ad26a42147..fd70eccfe8 100644 --- a/pkg/machine/e2e/machine_test.go +++ b/pkg/machine/e2e/machine_test.go @@ -12,7 +12,10 @@ import ( "time" "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/compression" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/containers/podman/v4/pkg/machine/provider" + "github.com/containers/podman/v4/utils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -77,11 +80,11 @@ var _ = BeforeSuite(func() { Fail(fmt.Sprintf("unable to download machine image: %q", err)) } GinkgoWriter.Println("Download took: ", time.Since(now).String()) - diskImage, err := machine.NewMachineFile(fqImageName+compressionExtension, nil) + diskImage, err := define.NewMachineFile(fqImageName+compressionExtension, nil) if err != nil { Fail(fmt.Sprintf("unable to create vmfile %q: %v", fqImageName+compressionExtension, err)) } - if err := machine.Decompress(diskImage, fqImageName); err != nil { + if err := compression.Decompress(diskImage, fqImageName); err != nil { Fail(fmt.Sprintf("unable to decompress image file: %q", err)) } } else { @@ -149,7 +152,7 @@ func teardown(origHomeDir string, testDir string, mb *machineTestBuilder) { GinkgoWriter.Printf("error occurred rm'ing machine: %q\n", err) } } - if err := machine.GuardedRemoveAll(testDir); err != nil { + if err := utils.GuardedRemoveAll(testDir); err != nil { Fail(fmt.Sprintf("failed to remove test dir: %q", err)) } // this needs to be last in teardown diff --git a/pkg/machine/fcos.go b/pkg/machine/fcos.go index a7cd8eb1c4..cf90c88608 100644 --- a/pkg/machine/fcos.go +++ b/pkg/machine/fcos.go @@ -11,9 +11,10 @@ import ( url2 "net/url" "os" "runtime" - "strings" "time" + "github.com/containers/podman/v4/pkg/machine/compression" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/coreos/stream-metadata-go/fedoracoreos" "github.com/coreos/stream-metadata-go/release" "github.com/coreos/stream-metadata-go/stream" @@ -21,31 +22,12 @@ import ( "github.com/sirupsen/logrus" ) -type ImageCompression int64 -type Artifact int64 -type ImageFormat int64 - const ( // Used for testing the latest podman in fcos // special builds podmanTesting = "podman-testing" PodmanTestingHost = "fedorapeople.org" PodmanTestingURL = "groups/podman/testing" - - Xz ImageCompression = iota - Zip - Gz - Bz2 - - Qemu Artifact = iota - HyperV - AppleHV - None - - Qcow ImageFormat = iota - Vhdx - Tar - Raw ) // @@ -56,64 +38,6 @@ const ( // typed strongly // -func (a Artifact) String() string { - switch a { - case HyperV: - return "hyperv" - case AppleHV: - return "applehv" - } - return "qemu" -} - -func (imf ImageFormat) String() string { - switch imf { - case Vhdx: - return "vhdx.zip" - case Tar: - return "tar.xz" - case Raw: - return "raw.gz" - } - return "qcow2.xz" -} - -func (imf ImageFormat) string() string { - switch imf { - case Vhdx: - return "vhdx" - case Tar: - return "tar" - case Raw: - return "raw" - } - return "qcow2" -} - -func (c ImageCompression) String() string { - switch c { - case Gz: - return "gz" - case Zip: - return "zip" - case Bz2: - return "bz2" - } - return "xz" -} - -func compressionFromFile(path string) ImageCompression { - switch { - case strings.HasSuffix(path, Bz2.String()): - return Bz2 - case strings.HasSuffix(path, Gz.String()): - return Gz - case strings.HasSuffix(path, Zip.String()): - return Zip - } - return Xz -} - type FcosDownload struct { Download } @@ -123,7 +47,7 @@ func (f FcosDownload) Get() *Download { } type FcosDownloadInfo struct { - CompressionType ImageCompression + CompressionType compression.ImageCompression Location string Release string Sha256Sum string @@ -219,7 +143,7 @@ func (dl Download) GetFCOSDownload(imageStream FCOSStream) (*FcosDownloadInfo, e if !ok { return nil, fmt.Errorf("unable to pull VM image: no targetArch in stream") } - qcow2, ok := arches.Media.Qemu.Artifacts[Qcow.String()] + qcow2, ok := arches.Media.Qemu.Artifacts[define.Qcow.KindWithCompression()] if !ok { return nil, fmt.Errorf("unable to pull VM image: no qcow2.xz format in stream") } @@ -251,9 +175,9 @@ func (dl Download) GetFCOSDownload(imageStream FCOSStream) (*FcosDownloadInfo, e if formats == nil { return nil, fmt.Errorf("unable to pull VM image: no formats in stream") } - formatType, ok := formats[dl.Format.String()] + formatType, ok := formats[dl.Format.KindWithCompression()] if !ok { - return nil, fmt.Errorf("unable to pull VM image: no %s format in stream", dl.Format.String()) + return nil, fmt.Errorf("unable to pull VM image: no %s format in stream", dl.Format.KindWithCompression()) } disk := formatType.Disk if disk == nil { diff --git a/pkg/machine/fcos_test.go b/pkg/machine/fcos_test.go index a629478204..efb3d017ac 100644 --- a/pkg/machine/fcos_test.go +++ b/pkg/machine/fcos_test.go @@ -7,146 +7,6 @@ import ( "testing" ) -func Test_compressionFromFile(t *testing.T) { - type args struct { - path string - } - var tests = []struct { - name string - args args - want ImageCompression - }{ - { - name: "xz", - args: args{ - path: "/tmp/foo.xz", - }, - want: Xz, - }, - { - name: "gzip", - args: args{ - path: "/tmp/foo.gz", - }, - want: Gz, - }, - { - name: "bz2", - args: args{ - path: "/tmp/foo.bz2", - }, - want: Bz2, - }, - { - name: "default is xz", - args: args{ - path: "/tmp/foo", - }, - want: Xz, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := compressionFromFile(tt.args.path); got != tt.want { - t.Errorf("compressionFromFile() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestImageCompression_String(t *testing.T) { - tests := []struct { - name string - c ImageCompression - want string - }{ - { - name: "xz", - c: Xz, - want: "xz", - }, - { - name: "gz", - c: Gz, - want: "gz", - }, - { - name: "bz2", - c: Bz2, - want: "bz2", - }, - { - name: "zip", - c: Zip, - want: "zip", - }, - { - name: "xz is default", - c: 99, - want: "xz", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.c.String(); got != tt.want { - t.Errorf("String() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestImageFormat_String(t *testing.T) { - tests := []struct { - name string - imf ImageFormat - want string - }{ - { - name: "vhdx.zip", - imf: Vhdx, - want: "vhdx.zip", - }, - { - name: "qcow2", - imf: Qcow, - want: "qcow2.xz", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.imf.String(); got != tt.want { - t.Errorf("String() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_artifact_String(t *testing.T) { - tests := []struct { - name string - a Artifact - want string - }{ - { - name: "qemu", - a: Qemu, - want: "qemu", - }, - { - name: "hyperv", - a: HyperV, - want: "hyperv", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.a.String(); got != tt.want { - t.Errorf("String() = %v, want %v", got, tt.want) - } - }) - } -} - func TestFCOSStream_String(t *testing.T) { tests := []struct { name string diff --git a/pkg/machine/gvproxy.go b/pkg/machine/gvproxy.go index ab7a826a86..5786b564b5 100644 --- a/pkg/machine/gvproxy.go +++ b/pkg/machine/gvproxy.go @@ -8,6 +8,7 @@ import ( "syscall" "time" + "github.com/containers/podman/v4/pkg/machine/define" psutil "github.com/shirou/gopsutil/v3/process" "github.com/sirupsen/logrus" ) @@ -82,7 +83,7 @@ func waitOnProcess(processID int) error { } // CleanupGVProxy reads the --pid-file for gvproxy attempts to stop it -func CleanupGVProxy(f VMFile) error { +func CleanupGVProxy(f define.VMFile) error { gvPid, err := f.Read() if err != nil { return fmt.Errorf("unable to read gvproxy pid file %s: %v", f.GetPath(), err) diff --git a/pkg/machine/hyperv/config.go b/pkg/machine/hyperv/config.go index 39b4f58273..223241d9f4 100644 --- a/pkg/machine/hyperv/config.go +++ b/pkg/machine/hyperv/config.go @@ -14,6 +14,8 @@ import ( "github.com/containers/libhvee/pkg/hypervctl" "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/compression" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/docker/go-units" "github.com/sirupsen/logrus" ) @@ -24,7 +26,7 @@ type HyperVVirtualization struct { func VirtualizationProvider() machine.VirtProvider { return &HyperVVirtualization{ - machine.NewVirtualization(machine.HyperV, machine.Zip, machine.Vhdx, vmtype), + machine.NewVirtualization(define.HyperV, compression.Zip, define.Vhdx, vmtype), } } @@ -117,14 +119,14 @@ func (v HyperVVirtualization) NewMachine(opts machine.InitOptions) (machine.VM, return nil, err } - configPath, err := machine.NewMachineFile(getVMConfigPath(configDir, opts.Name), nil) + configPath, err := define.NewMachineFile(getVMConfigPath(configDir, opts.Name), nil) if err != nil { return nil, err } m.ConfigPath = *configPath - ignitionPath, err := machine.NewMachineFile(filepath.Join(configDir, m.Name)+".ign", nil) + ignitionPath, err := define.NewMachineFile(filepath.Join(configDir, m.Name)+".ign", nil) if err != nil { return nil, err } @@ -139,7 +141,7 @@ func (v HyperVVirtualization) NewMachine(opts machine.InitOptions) (machine.VM, } // Set the proxy pid file - gvProxyPid, err := machine.NewMachineFile(filepath.Join(dataDir, "gvproxy.pid"), nil) + gvProxyPid, err := define.NewMachineFile(filepath.Join(dataDir, "gvproxy.pid"), nil) if err != nil { return nil, err } diff --git a/pkg/machine/hyperv/machine.go b/pkg/machine/hyperv/machine.go index 5bdba9bb2a..ffacfa4ebb 100644 --- a/pkg/machine/hyperv/machine.go +++ b/pkg/machine/hyperv/machine.go @@ -20,6 +20,7 @@ import ( gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types" "github.com/containers/libhvee/pkg/hypervctl" "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/containers/podman/v4/pkg/strongunits" "github.com/containers/podman/v4/pkg/util" "github.com/containers/podman/v4/utils" @@ -44,7 +45,7 @@ const ( type HyperVMachine struct { // ConfigPath is the fully qualified path to the configuration file - ConfigPath machine.VMFile + ConfigPath define.VMFile // HostUser contains info about host user machine.HostUser // ImageConfig describes the bootable image @@ -68,7 +69,7 @@ type HyperVMachine struct { // LastUp contains the last recorded uptime LastUp time.Time // GVProxy will write its PID here - GvProxyPid machine.VMFile + GvProxyPid define.VMFile // MountVsocks contains the currently-active vsocks, mapped to the // directory they should be mounted on. MountVsocks map[string]uint64 @@ -873,13 +874,13 @@ func (m *HyperVMachine) dockerSock() (string, error) { return filepath.Join(dd, "podman.sock"), nil } -func (m *HyperVMachine) forwardSocketPath() (*machine.VMFile, error) { +func (m *HyperVMachine) forwardSocketPath() (*define.VMFile, error) { sockName := "podman.sock" path, err := machine.GetDataDir(machine.HyperVVirt) if err != nil { return nil, fmt.Errorf("Resolving data dir: %s", err.Error()) } - return machine.NewMachineFile(filepath.Join(path, sockName), &sockName) + return define.NewMachineFile(filepath.Join(path, sockName), &sockName) } func (m *HyperVMachine) writeConfig() error { diff --git a/pkg/machine/oci.go b/pkg/machine/ocipull/oci.go similarity index 86% rename from pkg/machine/oci.go rename to pkg/machine/ocipull/oci.go index 7d59c5325f..abc7d42342 100644 --- a/pkg/machine/oci.go +++ b/pkg/machine/ocipull/oci.go @@ -1,4 +1,4 @@ -package machine +package ocipull import ( "fmt" @@ -6,14 +6,13 @@ import ( "path/filepath" "strings" - "github.com/containers/image/v5/types" - + "github.com/blang/semver/v4" "github.com/containers/image/v5/pkg/compression" + "github.com/containers/image/v5/types" + "github.com/containers/podman/v4/pkg/machine/define" + "github.com/containers/podman/v4/version" "github.com/containers/storage/pkg/archive" "github.com/sirupsen/logrus" - - "github.com/blang/semver/v4" - "github.com/containers/podman/v4/version" ) // quay.io/libpod/podman-machine-images:4.6 @@ -30,9 +29,9 @@ type OSVersion struct { type Disker interface { Pull() error - Decompress(compressedFile *VMFile) (*VMFile, error) + Decompress(compressedFile *define.VMFile) (*define.VMFile, error) DiskEndpoint() string - Unpack() (*VMFile, error) + Unpack() (*define.VMFile, error) } type OCIOpts struct { @@ -81,11 +80,11 @@ func (o *OSVersion) majorMinor() string { return fmt.Sprintf("%d.%d", o.Major, o.Minor) } -func (o *OSVersion) diskImage(diskFlavor ImageFormat) string { - return fmt.Sprintf("%s/%s/%s:%s-%s", registry, repo, diskImages, o.majorMinor(), diskFlavor.string()) +func (o *OSVersion) diskImage(diskFlavor define.ImageFormat) string { + return fmt.Sprintf("%s/%s/%s:%s-%s", registry, repo, diskImages, o.majorMinor(), diskFlavor.Kind()) } -func unpackOCIDir(ociTb, machineImageDir string) (*VMFile, error) { +func unpackOCIDir(ociTb, machineImageDir string) (*define.VMFile, error) { imageFileName, err := findTarComponent(ociTb) if err != nil { return nil, err @@ -121,7 +120,7 @@ func unpackOCIDir(ociTb, machineImageDir string) (*VMFile, error) { return nil, err } - return NewMachineFile(unpackedFileName, nil) + return define.NewMachineFile(unpackedFileName, nil) } func localOCIDiskImageDir(blobDirPath string, localBlob *types.BlobInfo) string { diff --git a/pkg/machine/ocidir.go b/pkg/machine/ocipull/ocidir.go similarity index 82% rename from pkg/machine/ocidir.go rename to pkg/machine/ocipull/ocidir.go index b513bf119d..ceda9e5e06 100644 --- a/pkg/machine/ocidir.go +++ b/pkg/machine/ocipull/ocidir.go @@ -1,4 +1,4 @@ -package machine +package ocipull import ( "archive/tar" @@ -11,7 +11,8 @@ import ( "github.com/containers/image/v5/pkg/compression" "github.com/containers/image/v5/types" - "github.com/containers/podman/v4/pkg/machine/ocipull" + diskcompression "github.com/containers/podman/v4/pkg/machine/compression" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/sirupsen/logrus" ) @@ -38,7 +39,7 @@ func NewOCIDir(ctx context.Context, inputDir, machineImageDir, vmName string) *L } func (l *LocalBlobDir) Pull() error { - localBlob, err := ocipull.GetLocalBlob(l.ctx, l.DiskEndpoint()) + localBlob, err := GetLocalBlob(l.ctx, l.DiskEndpoint()) if err != nil { return err } @@ -46,15 +47,15 @@ func (l *LocalBlobDir) Pull() error { return nil } -func (l *LocalBlobDir) Decompress(compressedFile *VMFile) (*VMFile, error) { +func (l *LocalBlobDir) Decompress(compressedFile *define.VMFile) (*define.VMFile, error) { finalName := finalFQImagePathName(l.vmName, l.imageName) - if err := Decompress(compressedFile, finalName); err != nil { + if err := diskcompression.Decompress(compressedFile, finalName); err != nil { return nil, err } - return NewMachineFile(finalName, nil) + return define.NewMachineFile(finalName, nil) } -func (l *LocalBlobDir) Unpack() (*VMFile, error) { +func (l *LocalBlobDir) Unpack() (*define.VMFile, error) { tbPath := localOCIDiskImageDir(l.blobDirPath, l.blob) unPackedFile, err := unpackOCIDir(tbPath, l.machineImageDir) if err != nil { diff --git a/pkg/machine/default.go b/pkg/machine/ocipull/versioned.go similarity index 76% rename from pkg/machine/default.go rename to pkg/machine/ocipull/versioned.go index 6d0298d2e6..9cc5e630aa 100644 --- a/pkg/machine/default.go +++ b/pkg/machine/ocipull/versioned.go @@ -1,4 +1,4 @@ -package machine +package ocipull import ( "context" @@ -8,7 +8,9 @@ import ( "strings" "github.com/containers/image/v5/types" - "github.com/containers/podman/v4/pkg/machine/ocipull" + "github.com/containers/podman/v4/pkg/machine/compression" + "github.com/containers/podman/v4/pkg/machine/define" + "github.com/containers/podman/v4/utils" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" ) @@ -18,14 +20,14 @@ type Versioned struct { blobDirPath string cacheDir string ctx context.Context - imageFormat ImageFormat + imageFormat define.ImageFormat imageName string machineImageDir string machineVersion *OSVersion vmName string } -func newVersioned(ctx context.Context, machineImageDir, vmName string) (*Versioned, error) { +func NewVersioned(ctx context.Context, machineImageDir, vmName string) (*Versioned, error) { imageCacheDir := filepath.Join(machineImageDir, "cache") if err := os.MkdirAll(imageCacheDir, 0777); err != nil { return nil, err @@ -47,7 +49,7 @@ func (d *Versioned) versionedOCICacheDir() string { } func (d *Versioned) identifyImageNameFromOCIDir() (string, error) { - imageManifest, err := ocipull.ReadImageManifestFromOCIPath(d.ctx, d.versionedOCICacheDir()) + imageManifest, err := ReadImageManifestFromOCIPath(d.ctx, d.versionedOCICacheDir()) if err != nil { return "", err } @@ -61,7 +63,7 @@ func (d *Versioned) identifyImageNameFromOCIDir() (string, error) { func (d *Versioned) pull(path string) error { fmt.Printf("Pulling %s\n", d.DiskEndpoint()) logrus.Debugf("pulling %s to %s", d.DiskEndpoint(), path) - return ocipull.Pull(d.ctx, d.DiskEndpoint(), path, ocipull.PullOptions{}) + return Pull(d.ctx, d.DiskEndpoint(), path, PullOptions{}) } func (d *Versioned) Pull() error { @@ -72,7 +74,7 @@ func (d *Versioned) Pull() error { remoteDescriptor *v1.Descriptor ) - remoteDiskImage := d.machineVersion.diskImage(Qcow) + remoteDiskImage := d.machineVersion.diskImage(define.Qcow) logrus.Debugf("podman disk image name: %s", remoteDiskImage) // is there a valid oci dir in our cache @@ -80,12 +82,12 @@ func (d *Versioned) Pull() error { if hasCache { logrus.Debug("checking remote registry") - remoteDescriptor, err = ocipull.GetRemoteDescriptor(d.ctx, remoteDiskImage) + remoteDescriptor, err = GetRemoteDescriptor(d.ctx, remoteDiskImage) if err != nil { return err } logrus.Debugf("working with local cache: %s", d.versionedOCICacheDir()) - localBlob, err = ocipull.GetLocalBlob(d.ctx, d.versionedOCICacheDir()) + localBlob, err = GetLocalBlob(d.ctx, d.versionedOCICacheDir()) if err != nil { return err } @@ -97,7 +99,7 @@ func (d *Versioned) Pull() error { } if !hasCache || isUpdatable { if hasCache { - if err := GuardedRemoveAll(d.versionedOCICacheDir()); err != nil { + if err := utils.GuardedRemoveAll(d.versionedOCICacheDir()); err != nil { return err } } @@ -113,7 +115,7 @@ func (d *Versioned) Pull() error { d.imageName = imageName if localBlob == nil { - localBlob, err = ocipull.GetLocalBlob(d.ctx, d.versionedOCICacheDir()) + localBlob, err = GetLocalBlob(d.ctx, d.versionedOCICacheDir()) if err != nil { return err } @@ -124,7 +126,7 @@ func (d *Versioned) Pull() error { return nil } -func (d *Versioned) Unpack() (*VMFile, error) { +func (d *Versioned) Unpack() (*define.VMFile, error) { tbPath := localOCIDiskImageDir(d.blobDirPath, d.blob) unpackedFile, err := unpackOCIDir(tbPath, d.machineImageDir) if err != nil { @@ -134,14 +136,14 @@ func (d *Versioned) Unpack() (*VMFile, error) { return unpackedFile, nil } -func (d *Versioned) Decompress(compressedFile *VMFile) (*VMFile, error) { - imageCompression := compressionFromFile(d.imageName) +func (d *Versioned) Decompress(compressedFile *define.VMFile) (*define.VMFile, error) { + imageCompression := compression.KindFromFile(d.imageName) strippedImageName := strings.TrimSuffix(d.imageName, fmt.Sprintf(".%s", imageCompression.String())) finalName := finalFQImagePathName(d.vmName, strippedImageName) - if err := Decompress(compressedFile, finalName); err != nil { + if err := compression.Decompress(compressedFile, finalName); err != nil { return nil, err } - return NewMachineFile(finalName, nil) + return define.NewMachineFile(finalName, nil) } func (d *Versioned) localOCIDiskImageDir(localBlob *types.BlobInfo) string { diff --git a/pkg/machine/pull.go b/pkg/machine/pull.go index 153cbe3db1..83ae506747 100644 --- a/pkg/machine/pull.go +++ b/pkg/machine/pull.go @@ -4,8 +4,6 @@ package machine import ( - "archive/zip" - "bufio" "context" "errors" "fmt" @@ -13,18 +11,15 @@ import ( "net/http" url2 "net/url" "os" - "os/exec" "path/filepath" - "runtime" "strings" "time" - "github.com/containers/image/v5/pkg/compression" - "github.com/containers/storage/pkg/archive" + "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" - "github.com/ulikunitz/xz" - "github.com/vbauerster/mpb/v8" - "github.com/vbauerster/mpb/v8/decor" ) // GenericDownload is used when a user provides a URL @@ -90,7 +85,7 @@ func supportedURL(path string) (url *url2.URL) { func (dl Download) GetLocalUncompressedFile(dataDir string) string { compressedFilename := dl.VMName + "_" + dl.ImageName - extension := compressionFromFile(compressedFilename) + extension := compression.KindFromFile(compressedFilename) uncompressedFile := strings.TrimSuffix(compressedFilename, fmt.Sprintf(".%s", extension.String())) dl.LocalUncompressedFile = filepath.Join(dataDir, uncompressedFile) return dl.LocalUncompressedFile @@ -134,33 +129,11 @@ func DownloadImage(d DistributionDownload) error { } }() } - localPath, err := NewMachineFile(d.Get().LocalPath, nil) + localPath, err := define.NewMachineFile(d.Get().LocalPath, nil) if err != nil { return err } - return Decompress(localPath, d.Get().LocalUncompressedFile) -} - -func progressBar(prefix string, size int64, onComplete string) (*mpb.Progress, *mpb.Bar) { - p := mpb.New( - mpb.WithWidth(80), // Do not go below 80, see bug #17718 - mpb.WithRefreshRate(180*time.Millisecond), - ) - - bar := p.AddBar(size, - mpb.BarFillerClearOnComplete(), - mpb.PrependDecorators( - decor.OnComplete(decor.Name(prefix), onComplete), - ), - mpb.AppendDecorators( - decor.OnComplete(decor.CountersKibiByte("%.1f / %.1f"), ""), - ), - ) - if size == 0 { - bar.SetTotal(0, true) - } - - return p, bar + return compression.Decompress(localPath, d.Get().LocalUncompressedFile) } // DownloadVMImage downloads a VM image from url to given path @@ -193,7 +166,7 @@ func DownloadVMImage(downloadURL *url2.URL, imageName string, localImagePath str prefix := "Downloading VM image: " + imageName onComplete := prefix + ": done" - p, bar := progressBar(prefix, size, onComplete) + p, bar := utils.ProgressBar(prefix, size, onComplete) proxyReader := bar.ProxyReader(resp.Body) defer func() { @@ -210,170 +183,6 @@ func DownloadVMImage(downloadURL *url2.URL, imageName string, localImagePath str return nil } -func Decompress(localPath *VMFile, uncompressedPath string) error { - var isZip bool - uncompressedFileWriter, err := os.OpenFile(uncompressedPath, os.O_CREATE|os.O_RDWR, 0600) - if err != nil { - return err - } - sourceFile, err := localPath.Read() - if err != nil { - return err - } - if strings.HasSuffix(localPath.GetPath(), ".zip") { - isZip = true - } - prefix := "Copying uncompressed file" - compressionType := archive.DetectCompression(sourceFile) - if compressionType != archive.Uncompressed || isZip { - prefix = "Extracting compressed file" - } - prefix += ": " + filepath.Base(uncompressedPath) - if compressionType == archive.Xz { - return decompressXZ(prefix, localPath.GetPath(), uncompressedFileWriter) - } - if isZip && runtime.GOOS == "windows" { - return decompressZip(prefix, localPath.GetPath(), uncompressedFileWriter) - } - return decompressEverythingElse(prefix, localPath.GetPath(), uncompressedFileWriter) -} - -// Will error out if file without .Xz already exists -// Maybe extracting then renaming is a good idea here.. -// depends on Xz: not pre-installed on mac, so it becomes a brew dependency -func decompressXZ(prefix string, src string, output io.WriteCloser) error { - var read io.Reader - var cmd *exec.Cmd - - stat, err := os.Stat(src) - if err != nil { - return err - } - file, err := os.Open(src) - if err != nil { - return err - } - defer file.Close() - - p, bar := progressBar(prefix, stat.Size(), prefix+": done") - proxyReader := bar.ProxyReader(file) - defer func() { - if err := proxyReader.Close(); err != nil { - logrus.Error(err) - } - }() - - // Prefer Xz utils for fastest performance, fallback to go xi2 impl - if _, err := exec.LookPath("xz"); err == nil { - cmd = exec.Command("xz", "-d", "-c") - cmd.Stdin = proxyReader - read, err = cmd.StdoutPipe() - if err != nil { - return err - } - cmd.Stderr = os.Stderr - } else { - // This XZ implementation is reliant on buffering. It is also 3x+ slower than XZ utils. - // Consider replacing with a faster implementation (e.g. xi2) if podman machine is - // updated with a larger image for the distribution base. - buf := bufio.NewReader(proxyReader) - read, err = xz.NewReader(buf) - if err != nil { - return err - } - } - - done := make(chan bool) - go func() { - if _, err := io.Copy(output, read); err != nil { - logrus.Error(err) - } - output.Close() - done <- true - }() - - if cmd != nil { - err := cmd.Start() - if err != nil { - return err - } - p.Wait() - return cmd.Wait() - } - <-done - p.Wait() - return nil -} - -func decompressEverythingElse(prefix string, src string, output io.WriteCloser) error { - stat, err := os.Stat(src) - if err != nil { - return err - } - f, err := os.Open(src) - if err != nil { - return err - } - p, bar := progressBar(prefix, stat.Size(), prefix+": done") - proxyReader := bar.ProxyReader(f) - defer func() { - if err := proxyReader.Close(); err != nil { - logrus.Error(err) - } - }() - uncompressStream, _, err := compression.AutoDecompress(proxyReader) - if err != nil { - return err - } - defer func() { - if err := uncompressStream.Close(); err != nil { - logrus.Error(err) - } - if err := output.Close(); err != nil { - logrus.Error(err) - } - }() - - _, err = io.Copy(output, uncompressStream) - p.Wait() - return err -} - -func decompressZip(prefix string, src string, output io.WriteCloser) error { - zipReader, err := zip.OpenReader(src) - if err != nil { - return err - } - if len(zipReader.File) != 1 { - return errors.New("machine image files should consist of a single compressed file") - } - f, err := zipReader.File[0].Open() - if err != nil { - return err - } - defer func() { - if err := f.Close(); err != nil { - logrus.Error(err) - } - }() - defer func() { - if err := output.Close(); err != nil { - logrus.Error(err) - } - }() - size := int64(zipReader.File[0].CompressedSize64) - p, bar := progressBar(prefix, size, prefix+": done") - proxyReader := bar.ProxyReader(f) - defer func() { - if err := proxyReader.Close(); err != nil { - logrus.Error(err) - } - }() - _, err = io.Copy(output, proxyReader) - p.Wait() - return err -} - func RemoveImageAfterExpire(dir string, expire time.Duration) error { now := time.Now() err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { @@ -393,13 +202,13 @@ func RemoveImageAfterExpire(dir string, expire time.Duration) error { // AcquireAlternateImage downloads the alternate image the user provided, which // can be a file path or URL -func (dl Download) AcquireAlternateImage(inputPath string) (*VMFile, error) { +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 := NewMachineFile(g.Get().LocalUncompressedFile, nil) + imagePath, err := define.NewMachineFile(g.Get().LocalUncompressedFile, nil) if err != nil { return nil, err } @@ -411,23 +220,23 @@ func (dl Download) AcquireAlternateImage(inputPath string) (*VMFile, error) { return imagePath, nil } -func isOci(input string) (bool, *OCIKind, error) { +func isOci(input string) (bool, *ocipull.OCIKind, error) { inputURL, err := url2.Parse(input) if err != nil { return false, nil, err } switch inputURL.Scheme { - case OCIDir.String(): - return true, &OCIDir, nil - case OCIRegistry.String(): - return true, &OCIRegistry, nil + 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) (*VMFile, FCOSStream, error) { +func Pull(input, machineName string, vp VirtProvider) (*define.VMFile, FCOSStream, error) { var ( - disk Disker + disk ocipull.Disker ) ociBased, ociScheme, err := isOci(input) @@ -442,7 +251,7 @@ func Pull(input, machineName string, vp VirtProvider) (*VMFile, FCOSStream, erro } return dl.AcquireVMImage(input) } - oopts := OCIOpts{ + oopts := ocipull.OCIOpts{ Scheme: ociScheme, } dataDir, err := GetDataDir(vp.VMType()) @@ -450,9 +259,9 @@ func Pull(input, machineName string, vp VirtProvider) (*VMFile, FCOSStream, erro return nil, 0, err } if ociScheme.IsOCIDir() { - strippedOCIDir := StripOCIReference(input) + strippedOCIDir := ocipull.StripOCIReference(input) oopts.Dir = &strippedOCIDir - disk = NewOCIDir(context.Background(), input, dataDir, machineName) + disk = ocipull.NewOCIDir(context.Background(), input, dataDir, machineName) } else { // a use of a containers image type here might be // tighter @@ -461,7 +270,7 @@ func Pull(input, machineName string, vp VirtProvider) (*VMFile, FCOSStream, erro if len(strippedInput) > 0 { return nil, 0, errors.New("image names are not supported yet") } - disk, err = newVersioned(context.Background(), dataDir, machineName) + disk, err = ocipull.NewVersioned(context.Background(), dataDir, machineName) if err != nil { return nil, 0, err } diff --git a/pkg/machine/qemu/command.go b/pkg/machine/qemu/command.go index 12b0bd10af..6e6c8f31be 100644 --- a/pkg/machine/qemu/command.go +++ b/pkg/machine/qemu/command.go @@ -4,7 +4,7 @@ import ( "fmt" "strconv" - "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/define" ) // QemuCmd is an alias around a string slice to prevent the need to migrate the @@ -30,7 +30,7 @@ func (q *QemuCmd) SetCPUs(c uint64) { } // SetIgnitionFile specifies the machine's ignition file -func (q *QemuCmd) SetIgnitionFile(file machine.VMFile) { +func (q *QemuCmd) SetIgnitionFile(file define.VMFile) { *q = append(*q, "-fw_cfg", "name=opt/com.coreos/config,file="+file.GetPath()) } @@ -47,7 +47,7 @@ func (q *QemuCmd) SetNetwork() { } // SetSerialPort adds a serial port to the machine for readiness -func (q *QemuCmd) SetSerialPort(readySocket, vmPidFile machine.VMFile, name string) { +func (q *QemuCmd) SetSerialPort(readySocket, vmPidFile define.VMFile, name string) { *q = append(*q, "-device", "virtio-serial", // qemu needs to establish the long name; other connections can use the symlink'd diff --git a/pkg/machine/qemu/config.go b/pkg/machine/qemu/config.go index 57066c10cb..eb776097f2 100644 --- a/pkg/machine/qemu/config.go +++ b/pkg/machine/qemu/config.go @@ -11,6 +11,8 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/compression" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/containers/podman/v4/utils" "github.com/docker/go-units" "github.com/sirupsen/logrus" @@ -77,14 +79,14 @@ func (p *QEMUVirtualization) NewMachine(opts machine.InitOptions) (machine.VM, e } // set VM ignition file - ignitionFile, err := machine.NewMachineFile(filepath.Join(vmConfigDir, vm.Name+".ign"), nil) + ignitionFile, err := define.NewMachineFile(filepath.Join(vmConfigDir, vm.Name+".ign"), nil) if err != nil { return nil, err } vm.IgnitionFile = *ignitionFile // set VM image file - imagePath, err := machine.NewMachineFile(opts.ImagePath, nil) + imagePath, err := define.NewMachineFile(opts.ImagePath, nil) if err != nil { return nil, err } @@ -299,7 +301,7 @@ func (p *QEMUVirtualization) RemoveAndCleanMachines() error { } prevErr = err } else { - err := machine.GuardedRemoveAll(dataDir) + err := utils.GuardedRemoveAll(dataDir) if err != nil { if prevErr != nil { logrus.Error(prevErr) @@ -316,7 +318,7 @@ func (p *QEMUVirtualization) RemoveAndCleanMachines() error { } prevErr = err } else { - err := machine.GuardedRemoveAll(confDir) + err := utils.GuardedRemoveAll(confDir) if err != nil { if prevErr != nil { logrus.Error(prevErr) @@ -333,7 +335,7 @@ func (p *QEMUVirtualization) VMType() machine.VMType { func VirtualizationProvider() machine.VirtProvider { return &QEMUVirtualization{ - machine.NewVirtualization(machine.Qemu, machine.Xz, machine.Qcow, vmtype), + machine.NewVirtualization(define.Qemu, compression.Xz, define.Qcow, vmtype), } } diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index bcbda58f7d..f7fd7ef38b 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -24,6 +24,7 @@ import ( "github.com/containers/common/pkg/config" gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types" "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/containers/podman/v4/pkg/rootless" "github.com/containers/podman/v4/pkg/util" "github.com/containers/storage/pkg/lockfile" @@ -46,7 +47,7 @@ const ( type MachineVM struct { // ConfigPath is the path to the configuration file - ConfigPath machine.VMFile + ConfigPath define.VMFile // The command line representation of the qemu command CmdLine QemuCmd // HostUser contains info about host user @@ -58,13 +59,13 @@ type MachineVM struct { // Name of VM Name string // PidFilePath is the where the Proxy PID file lives - PidFilePath machine.VMFile + PidFilePath define.VMFile // VMPidFilePath is the where the VM PID file lives - VMPidFilePath machine.VMFile + VMPidFilePath define.VMFile // QMPMonitor is the qemu monitor object for sending commands QMPMonitor Monitor // ReadySocket tells host when vm is booted - ReadySocket machine.VMFile + ReadySocket define.VMFile // ResourceConfig is physical attrs of the VM machine.ResourceConfig // SSHConfig for accessing the remote vm @@ -82,7 +83,7 @@ type MachineVM struct { type Monitor struct { // Address portion of the qmp monitor (/tmp/tmp.sock) - Address machine.VMFile + Address define.VMFile // Network portion of the qmp monitor (unix) Network string // Timeout in seconds for qmp monitor transactions @@ -105,9 +106,9 @@ func migrateVM(configPath string, config []byte, vm *MachineVM) error { return err } - pidFilePath := machine.VMFile{Path: pidFile} + pidFilePath := define.VMFile{Path: pidFile} qmpMonitor := Monitor{ - Address: machine.VMFile{Path: old.QMPMonitor.Address}, + Address: define.VMFile{Path: old.QMPMonitor.Address}, Network: old.QMPMonitor.Network, Timeout: old.QMPMonitor.Timeout, } @@ -116,18 +117,18 @@ func migrateVM(configPath string, config []byte, vm *MachineVM) error { return err } virtualSocketPath := filepath.Join(socketPath, "podman", vm.Name+"_ready.sock") - readySocket := machine.VMFile{Path: virtualSocketPath} + readySocket := define.VMFile{Path: virtualSocketPath} vm.HostUser = machine.HostUser{} vm.ImageConfig = machine.ImageConfig{} vm.ResourceConfig = machine.ResourceConfig{} vm.SSHConfig = machine.SSHConfig{} - ignitionFilePath, err := machine.NewMachineFile(old.IgnitionFilePath, nil) + ignitionFilePath, err := define.NewMachineFile(old.IgnitionFilePath, nil) if err != nil { return err } - imagePath, err := machine.NewMachineFile(old.ImagePath, nil) + imagePath, err := define.NewMachineFile(old.ImagePath, nil) if err != nil { return err } @@ -1030,7 +1031,7 @@ func NewQMPMonitor(network, name string, timeout time.Duration) (Monitor, error) if timeout == 0 { timeout = defaultQMPTimeout } - address, err := machine.NewMachineFile(filepath.Join(rtDir, "qmp_"+name+".sock"), nil) + address, err := define.NewMachineFile(filepath.Join(rtDir, "qmp_"+name+".sock"), nil) if err != nil { return Monitor{}, err } @@ -1350,14 +1351,14 @@ func (v *MachineVM) userGlobalSocketLink() (string, error) { return filepath.Join(filepath.Dir(path), "podman.sock"), err } -func (v *MachineVM) forwardSocketPath() (*machine.VMFile, error) { +func (v *MachineVM) forwardSocketPath() (*define.VMFile, error) { sockName := "podman.sock" path, err := machine.GetDataDir(machine.QemuVirt) if err != nil { logrus.Errorf("Resolving data dir: %s", err.Error()) return nil, err } - return machine.NewMachineFile(filepath.Join(path, sockName), &sockName) + return define.NewMachineFile(filepath.Join(path, sockName), &sockName) } func (v *MachineVM) setConfigPath() error { @@ -1366,7 +1367,7 @@ func (v *MachineVM) setConfigPath() error { return err } - configPath, err := machine.NewMachineFile(filepath.Join(vmConfigDir, v.Name)+".json", nil) + configPath, err := define.NewMachineFile(filepath.Join(vmConfigDir, v.Name)+".json", nil) if err != nil { return err } @@ -1385,11 +1386,11 @@ func (v *MachineVM) setPIDSocket() error { socketDir := filepath.Join(rtPath, "podman") vmPidFileName := fmt.Sprintf("%s_vm.pid", v.Name) proxyPidFileName := fmt.Sprintf("%s_proxy.pid", v.Name) - vmPidFilePath, err := machine.NewMachineFile(filepath.Join(socketDir, vmPidFileName), &vmPidFileName) + vmPidFilePath, err := define.NewMachineFile(filepath.Join(socketDir, vmPidFileName), &vmPidFileName) if err != nil { return err } - proxyPidFilePath, err := machine.NewMachineFile(filepath.Join(socketDir, proxyPidFileName), &proxyPidFileName) + proxyPidFilePath, err := define.NewMachineFile(filepath.Join(socketDir, proxyPidFileName), &proxyPidFileName) if err != nil { return err } diff --git a/pkg/machine/qemu/qemu_command_test.go b/pkg/machine/qemu/qemu_command_test.go index 3a8848a07d..5041dcb156 100644 --- a/pkg/machine/qemu/qemu_command_test.go +++ b/pkg/machine/qemu/qemu_command_test.go @@ -7,22 +7,22 @@ import ( "fmt" "testing" - "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestQemuCmd(t *testing.T) { - ignFile, err := machine.NewMachineFile(t.TempDir()+"demo-ignition-file.ign", nil) + ignFile, err := define.NewMachineFile(t.TempDir()+"demo-ignition-file.ign", nil) assert.NoError(t, err) - machineAddrFile, err := machine.NewMachineFile(t.TempDir()+"tmp.sock", nil) + machineAddrFile, err := define.NewMachineFile(t.TempDir()+"tmp.sock", nil) assert.NoError(t, err) - readySocket, err := machine.NewMachineFile(t.TempDir()+"readySocket.sock", nil) + readySocket, err := define.NewMachineFile(t.TempDir()+"readySocket.sock", nil) assert.NoError(t, err) - vmPidFile, err := machine.NewMachineFile(t.TempDir()+"vmpidfile.pid", nil) + vmPidFile, err := define.NewMachineFile(t.TempDir()+"vmpidfile.pid", nil) assert.NoError(t, err) monitor := Monitor{ diff --git a/pkg/machine/sockets.go b/pkg/machine/sockets.go index 8c53448340..131127094d 100644 --- a/pkg/machine/sockets.go +++ b/pkg/machine/sockets.go @@ -7,12 +7,14 @@ import ( "net" "path/filepath" "time" + + "github.com/containers/podman/v4/pkg/machine/define" ) // SetSocket creates a new machine file for the socket and assigns it to // `socketLoc` -func SetSocket(socketLoc *VMFile, path string, symlink *string) error { - socket, err := NewMachineFile(path, symlink) +func SetSocket(socketLoc *define.VMFile, path string, symlink *string) error { + socket, err := define.NewMachineFile(path, symlink) if err != nil { return err } diff --git a/pkg/machine/wsl/config.go b/pkg/machine/wsl/config.go index e20be7b8d3..001d7c675a 100644 --- a/pkg/machine/wsl/config.go +++ b/pkg/machine/wsl/config.go @@ -10,6 +10,9 @@ import ( "time" "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/compression" + "github.com/containers/podman/v4/pkg/machine/define" + "github.com/containers/podman/v4/utils" "github.com/sirupsen/logrus" ) @@ -19,7 +22,7 @@ type WSLVirtualization struct { func VirtualizationProvider() machine.VirtProvider { return &WSLVirtualization{ - machine.NewVirtualization(machine.None, machine.Xz, machine.Tar, vmtype), + machine.NewVirtualization(define.None, compression.Xz, define.Tar, vmtype), } } @@ -196,7 +199,7 @@ func (p *WSLVirtualization) RemoveAndCleanMachines() error { } prevErr = err } else { - err := machine.GuardedRemoveAll(dataDir) + err := utils.GuardedRemoveAll(dataDir) if err != nil { if prevErr != nil { logrus.Error(prevErr) @@ -213,7 +216,7 @@ func (p *WSLVirtualization) RemoveAndCleanMachines() error { } prevErr = err } else { - err := machine.GuardedRemoveAll(confDir) + err := utils.GuardedRemoveAll(confDir) if err != nil { if prevErr != nil { logrus.Error(prevErr) diff --git a/pkg/machine/wsl/fedora.go b/pkg/machine/wsl/fedora.go index 8df9e8716d..43b4f50ef8 100644 --- a/pkg/machine/wsl/fedora.go +++ b/pkg/machine/wsl/fedora.go @@ -16,6 +16,7 @@ import ( "time" "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/define" ) const ( @@ -43,9 +44,9 @@ func NewFedoraDownloader(vmType machine.VMType, vmName, releaseStream string) (m f := FedoraDownload{ Download: machine.Download{ Arch: machine.GetFcosArch(), - Artifact: machine.None, + Artifact: define.None, CacheDir: cacheDir, - Format: machine.Tar, + Format: define.Tar, ImageName: imageName, LocalPath: filepath.Join(cacheDir, imageName), URL: downloadURL, diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index d29aa02365..51d96e7434 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -19,8 +19,10 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/containers/podman/v4/pkg/machine/wsl/wutil" "github.com/containers/podman/v4/pkg/util" + "github.com/containers/podman/v4/utils" "github.com/containers/storage/pkg/homedir" "github.com/containers/storage/pkg/lockfile" "github.com/sirupsen/logrus" @@ -480,22 +482,22 @@ func (v *MachineVM) unprovisionWSL() error { } distDir := filepath.Join(vmDataDir, "wsldist") distTarget := filepath.Join(distDir, v.Name) - return machine.GuardedRemoveAll(distTarget) + return utils.GuardedRemoveAll(distTarget) } func (v *MachineVM) removeMachineConfig() error { - return machine.GuardedRemoveAll(v.ConfigPath) + return utils.GuardedRemoveAll(v.ConfigPath) } func (v *MachineVM) removeMachineImage() error { - return machine.GuardedRemoveAll(v.ImagePath) + return utils.GuardedRemoveAll(v.ImagePath) } func (v *MachineVM) removeSSHKeys() error { - if err := machine.GuardedRemoveAll(fmt.Sprintf("%s.pub", v.IdentityPath)); err != nil { + if err := utils.GuardedRemoveAll(fmt.Sprintf("%s.pub", v.IdentityPath)); err != nil { logrus.Error(err) } - return machine.GuardedRemoveAll(v.IdentityPath) + return utils.GuardedRemoveAll(v.IdentityPath) } func (v *MachineVM) removeSystemConnections() error { @@ -1635,7 +1637,7 @@ func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (string, fun logrus.Error(err) } for _, f := range files { - if err := machine.GuardedRemoveAll(f); err != nil { + if err := utils.GuardedRemoveAll(f); err != nil { logrus.Error(err) } } @@ -1804,15 +1806,15 @@ func (v *MachineVM) Inspect() (*machine.InspectInfo, error) { connInfo := new(machine.ConnectionConfig) machinePipe := toDist(v.Name) - connInfo.PodmanPipe = &machine.VMFile{Path: `\\.\pipe\` + machinePipe} + connInfo.PodmanPipe = &define.VMFile{Path: `\\.\pipe\` + machinePipe} created, lastUp, _ := v.updateTimeStamps(state == machine.Running) return &machine.InspectInfo{ - ConfigPath: machine.VMFile{Path: v.ConfigPath}, + ConfigPath: define.VMFile{Path: v.ConfigPath}, ConnectionInfo: *connInfo, Created: created, Image: machine.ImageConfig{ - ImagePath: machine.VMFile{Path: v.ImagePath}, + ImagePath: define.VMFile{Path: v.ImagePath}, ImageStream: v.ImageStream, }, LastUp: lastUp, diff --git a/utils/utils.go b/utils/utils.go index 81b77e544a..f73672c7a4 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -10,12 +10,15 @@ import ( "strconv" "strings" "sync" + "time" "github.com/containers/common/pkg/cgroups" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/chrootarchive" "github.com/godbus/dbus/v5" "github.com/sirupsen/logrus" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) // ExecCmd executes a command with args and returns its output as a string along @@ -242,3 +245,34 @@ func MaybeMoveToSubCgroup() error { }) return maybeMoveToSubCgroupSyncErr } + +// GuardedRemoveAll functions much like os.RemoveAll but +// will not delete certain catastrophic paths. +func GuardedRemoveAll(path string) error { + if path == "" || path == "/" { + return fmt.Errorf("refusing to recursively delete `%s`", path) + } + return os.RemoveAll(path) +} + +func ProgressBar(prefix string, size int64, onComplete string) (*mpb.Progress, *mpb.Bar) { + p := mpb.New( + mpb.WithWidth(80), // Do not go below 80, see bug #17718 + mpb.WithRefreshRate(180*time.Millisecond), + ) + + bar := p.AddBar(size, + mpb.BarFillerClearOnComplete(), + mpb.PrependDecorators( + decor.OnComplete(decor.Name(prefix), onComplete), + ), + mpb.AppendDecorators( + decor.OnComplete(decor.CountersKibiByte("%.1f / %.1f"), ""), + ), + ) + if size == 0 { + bar.SetTotal(0, true) + } + + return p, bar +}