Merge pull request #17494 from ashley-cui/osapply

Introduce podman machine os apply
This commit is contained in:
OpenShift Merge Robot
2023-02-15 17:14:17 -05:00
committed by GitHub
16 changed files with 493 additions and 43 deletions

View File

@ -396,15 +396,6 @@ bin/rootlessport: $(SOURCES) go.mod go.sum
.PHONY: rootlessport
rootlessport: bin/rootlessport
.PHONY: podman-remote-experimental
podman-remote-experimental: $(SRCBINDIR)/experimental/podman$(BINSFX)
$(SRCBINDIR)/experimental/podman$(BINSFX): $(SOURCES) go.mod go.sum | $(SRCBINDIR)
$(GOCMD) build \
$(BUILDFLAGS) \
$(GO_LDFLAGS) '$(LDFLAGS_PODMAN)' \
-tags "${REMOTETAGS} experimental" \
-o $@ ./cmd/podman
###
### Secondary binary-build targets
###

View File

@ -1,6 +1,5 @@
//go:build (amd64 || arm64) && experimental
//go:build amd64 || arm64
// +build amd64 arm64
// +build experimental
package machine
@ -13,8 +12,8 @@ import (
var (
OSCmd = &cobra.Command{
Use: "os",
Short: "Manage a virtual machine's os",
Long: "Manage a virtual machine's operating system",
Short: "Manage a Podman virtual machine's OS",
Long: "Manage a Podman virtual machine's operating system",
PersistentPreRunE: validate.NoOp,
RunE: validate.SubCommandExists,
}

View File

@ -1,38 +1,60 @@
//go:build (amd64 || arm64) && experimental
//go:build amd64 || arm64
// +build amd64 arm64
// +build experimental
package machineos
package os
import (
"fmt"
"github.com/containers/podman/v4/cmd/podman/common"
"github.com/containers/podman/v4/cmd/podman/machine"
"github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/cmd/podman/validate"
"github.com/containers/podman/v4/pkg/machine/os"
"github.com/spf13/cobra"
)
var (
applyCmd = &cobra.Command{
Use: "apply",
Short: "Apply OCI image to existing VM",
Long: "Apply custom layers from a containerized Fedora CoreOS image on top of an existing VM",
Use: "apply [options] IMAGE [NAME]",
Short: "Apply an OCI image to a Podman Machine's OS",
Long: "Apply custom layers from a containerized Fedora CoreOS OCI image on top of an existing VM",
PersistentPreRunE: validate.NoOp,
Args: cobra.RangeArgs(1, 2),
RunE: apply,
ValidArgsFunction: common.AutocompleteImages,
Example: `podman machine os apply myimage`,
}
)
var restart bool
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: applyCmd,
Parent: machine.OSCmd,
})
flags := applyCmd.Flags()
restartFlagName := "restart"
flags.BoolVar(&restart, restartFlagName, false, "Restart VM to apply changes")
}
func apply(cmd *cobra.Command, args []string) error {
fmt.Println("Applying..")
return nil
vmName := ""
if len(args) == 2 {
vmName = args[1]
}
managerOpts := ManagerOpts{
VMName: vmName,
CLIArgs: args,
Restart: restart,
}
osManager, err := NewOSManager(managerOpts)
if err != nil {
return err
}
applyOpts := os.ApplyOptions{
Image: args[0],
}
return osManager.Apply(args[0], applyOpts)
}

View File

@ -0,0 +1,91 @@
//go:build amd64 || arm64
// +build amd64 arm64
package os
import (
"bufio"
"errors"
"os"
"strings"
machineconfig "github.com/containers/common/pkg/machine"
"github.com/containers/podman/v4/cmd/podman/machine"
pkgMachine "github.com/containers/podman/v4/pkg/machine"
pkgOS "github.com/containers/podman/v4/pkg/machine/os"
)
type ManagerOpts struct {
VMName string
CLIArgs []string
Restart bool
}
// NewOSManager creates a new OSManager depending on the mode of the call
func NewOSManager(opts ManagerOpts) (pkgOS.Manager, error) {
// If a VM name is specified, then we know that we are not inside a
// Podman VM, but rather outside of it.
if machineconfig.IsPodmanMachine() && opts.VMName == "" {
return guestOSManager()
}
return machineOSManager(opts)
}
// guestOSManager returns an OSmanager for inside-VM operations
func guestOSManager() (pkgOS.Manager, error) {
dist := GetDistribution()
switch {
case dist.Name == "fedora" && dist.Variant == "coreos":
return &pkgOS.OSTree{}, nil
default:
return nil, errors.New("unsupported OS")
}
}
// machineOSManager returns an os manager that manages outside the VM.
func machineOSManager(opts ManagerOpts) (pkgOS.Manager, error) {
vmName := opts.VMName
if opts.VMName == "" {
vmName = pkgMachine.DefaultMachineName
}
provider := machine.GetSystemDefaultProvider()
vm, err := provider.LoadVMByName(vmName)
if err != nil {
return nil, err
}
return &pkgOS.MachineOS{
VM: vm,
Args: opts.CLIArgs,
VMName: vmName,
Restart: opts.Restart,
}, nil
}
type Distribution struct {
Name string
Variant string
}
// GetDistribution checks the OS distribution
func GetDistribution() Distribution {
dist := Distribution{
Name: "unknown",
Variant: "unknown",
}
f, err := os.Open("/etc/os-release")
if err != nil {
return dist
}
defer f.Close()
l := bufio.NewScanner(f)
for l.Scan() {
if strings.HasPrefix(l.Text(), "ID=") {
dist.Name = strings.TrimPrefix(l.Text(), "ID=")
}
if strings.HasPrefix(l.Text(), "VARIANT_ID=") {
dist.Variant = strings.Trim(strings.TrimPrefix(l.Text(), "VARIANT_ID="), "\"")
}
}
return dist
}

View File

@ -0,0 +1,7 @@
//go:build !amd64 && !arm64
// +build !amd64,!arm64
package os
// init do not register _podman machine os_ command on unsupported platforms
func init() {}

View File

@ -11,6 +11,7 @@ import (
_ "github.com/containers/podman/v4/cmd/podman/images"
_ "github.com/containers/podman/v4/cmd/podman/kube"
_ "github.com/containers/podman/v4/cmd/podman/machine"
_ "github.com/containers/podman/v4/cmd/podman/machine/os"
_ "github.com/containers/podman/v4/cmd/podman/manifest"
_ "github.com/containers/podman/v4/cmd/podman/networks"
_ "github.com/containers/podman/v4/cmd/podman/pods"

View File

@ -1,8 +0,0 @@
//go:build experimental
// +build experimental
package main
import (
_ "github.com/containers/podman/v4/cmd/podman/machine/os"
)

View File

@ -4,7 +4,7 @@
podman\-machine\-inspect - Inspect one or more virtual machines
## SYNOPSIS
**podman machine inspect** [*options] *name* ...
**podman machine inspect** [*options*] *name* ...
## DESCRIPTION

View File

@ -0,0 +1,41 @@
% podman-machine-os-apply 1
## NAME
podman\-machine\-os\-apply - Apply an OCI image to a Podman Machine's OS
## SYNOPSIS
**podman machine os apply** [*options*] *image* [vm]
## DESCRIPTION
Apply machine OS changes from an OCI image.
VM's that use OS's that use rpm-ostreee have the capability to rebase itself from the content of an OCI image.
`podman machine image apply` takes an OCI image with container native ostree functionality and rebases itself on that image.
By default, Podman machines on Mac and Linux use an rpm-ostree based distrubition (Fedora CoreOS).
For more information, please see the [rpm-ostree docs](https://coreos.github.io/rpm-ostree/container/).
## OPTIONS
#### **--help**
Print usage statement.
#### **--restart**
Restart VM after applying changes.
## EXAMPLES
```
$ podman machine os apply quay.io/podman_next
$ podman machine os apply quay.io/podman_next podman-machine-defualt
```
## SEE ALSO
**[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)**, **[podman-machine-os(1)](podman-machine-os.1.md)**
## HISTORY
February 2023, Originally compiled by Ashley Cui <acui@redhat.com>

View File

@ -0,0 +1,22 @@
% podman-machine-os 1
## NAME
podman\-machine\-os - Manage a Podman virtual machine's OS
## SYNOPSIS
**podman machine os** *subcommand*
## DESCRIPTION
`podman machine os` is a set of subcommands that manage a Podman virtual machine's operating system.
## SUBCOMMANDS
| Command | Man Page | Description |
|---------|--------------------------------------------------------------|----------------------------------------------|
| apply | [podman-machine-os-apply(1)](podman-machine-os-apply.1.md) | Apply an OCI image to a Podman Machine's OS |
## SEE ALSO
**[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)**, **[podman-machine-os-apply(1)](podman-machine-os-apply.1.md)**
## HISTORY
February 2023, Originally compiled by Ashley Cui <acui@redhat.com>

View File

@ -22,20 +22,21 @@ environment variable while the machines are running can lead to unexpected behav
## SUBCOMMANDS
| Command | Man Page | Description |
|---------|------------------------------------------------------|-----------------------------------|
| info | [podman-machine-info(1)](podman-machine-info.1.md) | Display machine host info |
| init | [podman-machine-init(1)](podman-machine-init.1.md) | Initialize a new virtual machine |
| Command | Man Page | Description |
|---------|-----------------------------------------------------------|--------------------------------------|
| info | [podman-machine-info(1)](podman-machine-info.1.md) | Display machine host info |
| init | [podman-machine-init(1)](podman-machine-init.1.md) | Initialize a new virtual machine |
| inspect | [podman-machine-inspect(1)](podman-machine-inspect.1.md) | Inspect one or more virtual machines |
| list | [podman-machine-list(1)](podman-machine-list.1.md) | List virtual machines |
| rm | [podman-machine-rm(1)](podman-machine-rm.1.md) | Remove a virtual machine |
| set | [podman-machine-set(1)](podman-machine-set.1.md) | Sets a virtual machine setting |
| ssh | [podman-machine-ssh(1)](podman-machine-ssh.1.md) | SSH into a virtual machine |
| start | [podman-machine-start(1)](podman-machine-start.1.md) | Start a virtual machine |
| stop | [podman-machine-stop(1)](podman-machine-stop.1.md) | Stop a virtual machine |
| list | [podman-machine-list(1)](podman-machine-list.1.md) | List virtual machines |
| os | [podman-machine-os(1)](podman-machine-os.1.md) | Manage a Podman virtual machine's OS |
| rm | [podman-machine-rm(1)](podman-machine-rm.1.md) | Remove a virtual machine |
| set | [podman-machine-set(1)](podman-machine-set.1.md) | Sets a virtual machine setting |
| ssh | [podman-machine-ssh(1)](podman-machine-ssh.1.md) | SSH into a virtual machine |
| start | [podman-machine-start(1)](podman-machine-start.1.md) | Start a virtual machine |
| stop | [podman-machine-stop(1)](podman-machine-stop.1.md) | Stop a virtual machine |
## SEE ALSO
**[podman(1)](podman.1.md)**, **[podman-machine-info(1)](podman-machine-info.1.md)**, **[podman-machine-init(1)](podman-machine-init.1.md)**, **[podman-machine-list(1)](podman-machine-list.1.md)**, **[podman-machine-rm(1)](podman-machine-rm.1.md)**, **[podman-machine-ssh(1)](podman-machine-ssh.1.md)**, **[podman-machine-start(1)](podman-machine-start.1.md)**, **[podman-machine-stop(1)](podman-machine-stop.1.md)**, **[podman-machine-inspect(1)](podman-machine-inspect.1.md)**
**[podman(1)](podman.1.md)**, **[podman-machine-info(1)](podman-machine-info.1.md)**, **[podman-machine-init(1)](podman-machine-init.1.md)**, **[podman-machine-list(1)](podman-machine-list.1.md)**, **[podman-machine-os(1)](podman-machine-os.1.md)**, **[podman-machine-rm(1)](podman-machine-rm.1.md)**, **[podman-machine-ssh(1)](podman-machine-ssh.1.md)**, **[podman-machine-start(1)](podman-machine-start.1.md)**, **[podman-machine-stop(1)](podman-machine-stop.1.md)**, **[podman-machine-inspect(1)](podman-machine-inspect.1.md)**
## HISTORY
March 2021, Originally compiled by Ashley Cui <acui@redhat.com>

View File

@ -0,0 +1,27 @@
package e2e_test
// type applyMachineOS struct {
// restart bool
// cmd []string
// }
// func (a *applyMachineOS) buildCmd(m *machineTestBuilder) []string {
// cmd := []string{"machine", "os", "apply"}
// if a.restart {
// cmd = append(cmd, "--restart")
// }
// a.cmd = cmd
// return cmd
// }
// func (a *applyMachineOS) withRestart() *applyMachineOS {
// a.restart = true
// return a
// }
// func (a *applyMachineOS) args(cmd []string) *applyMachineOS {
// a.cmd = cmd
// return a
// }

View File

@ -0,0 +1,59 @@
package e2e_test
// import (
// . "github.com/onsi/ginkgo"
// . "github.com/onsi/gomega"
// . "github.com/onsi/gomega/gexec"
// )
// var _ = Describe("podman machine os apply", func() {
// var (
// mb *machineTestBuilder
// testDir string
// )
// BeforeEach(func() {
// testDir, mb = setup()
// })
// AfterEach(func() {
// teardown(originalHomeDir, testDir, mb)
// })
// It("apply machine", func() {
// i := new(initMachine)
// foo1, err := mb.setName("foo1").setCmd(i.withImagePath(mb.imagePath)).run()
// Expect(err).ToNot(HaveOccurred())
// Expect(foo1).To(Exit(0))
// apply := new(applyMachineOS)
// applySession, err := mb.setName("foo1").setCmd(apply.args([]string{"quay.io/baude/podman_next"})).run()
// Expect(err).ToNot(HaveOccurred())
// Expect(applySession).To(Exit(0))
// })
// It("apply machine from containers-storage", func() {
// i := new(initMachine)
// foo1, err := mb.setName("foo1").setCmd(i.withImagePath(mb.imagePath)).run()
// Expect(err).ToNot(HaveOccurred())
// Expect(foo1).To(Exit(0))
// ssh := sshMachine{}
// sshSession, err := mb.setName("foo1").setCmd(ssh.withSSHComand([]string{"podman", "pull", "quay.io/baude/podman_next"})).run()
// Expect(err).ToNot(HaveOccurred())
// Expect(sshSession).To(Exit(0))
// apply := new(applyMachineOS)
// applySession, err := mb.setName("foo1").setCmd(apply.args([]string{"quay.io/baude/podman_next"})).run()
// Expect(err).ToNot(HaveOccurred())
// Expect(applySession).To(Exit(0))
// Expect(applySession.outputToString()).To(ContainSubstring("Pulling from: containers-storage"))
// })
// It("apply machine not exist", func() {
// apply := new(applyMachineOS)
// applySession, err := mb.setName("foo1").setCmd(apply.args([]string{"quay.io/baude/podman_next", "notamachine"})).run()
// Expect(err).ToNot(HaveOccurred())
// Expect(applySession).To(Exit(125))
// Expect(applySession.errorToString()).To(ContainSubstring("not exist"))
// })
// })

15
pkg/machine/os/config.go Normal file
View File

@ -0,0 +1,15 @@
//go:build amd64 || arm64
// +build amd64 arm64
package os
// Manager is the interface for operations on a Podman machine's OS
type Manager interface {
// Apply machine OS changes from an OCI image.
Apply(image string, opts ApplyOptions) error
}
// ApplyOptions are the options for applying an image into a Podman machine VM
type ApplyOptions struct {
Image string
}

View File

@ -0,0 +1,40 @@
//go:build amd64 || arm64
// +build amd64 arm64
package os
import (
"fmt"
"github.com/containers/podman/v4/pkg/machine"
)
// MachineOS manages machine OS's from outside the machine.
type MachineOS struct {
Args []string
VM machine.VM
VMName string
Restart bool
}
// Apply applies the image by sshing into the machine and running apply from inside the VM.
func (m *MachineOS) Apply(image string, opts ApplyOptions) error {
sshOpts := machine.SSHOptions{
Args: []string{"podman", "machine", "os", "apply", image},
}
if err := m.VM.SSH(m.VMName, sshOpts); err != nil {
return err
}
if m.Restart {
if err := m.VM.Stop(m.VMName, machine.StopOptions{}); err != nil {
return err
}
if err := m.VM.Start(m.VMName, machine.StartOptions{NoInfo: true}); err != nil {
return err
}
fmt.Printf("Machine %q restarted successfully\n", m.VMName)
}
return nil
}

142
pkg/machine/os/ostree.go Normal file
View File

@ -0,0 +1,142 @@
//go:build amd64 || arm64
// +build amd64 arm64
package os
import (
"errors"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"github.com/containers/image/v5/transports/alltransports"
"github.com/sirupsen/logrus"
)
// OSTree deals with operations on ostree based os's
type OSTree struct { //nolint:revive
}
// Apply takes an OCI image and does an rpm-ostree rebase on the image
// If no containers-transport is specified,
// apply will first check if the image exists locally, then default to pulling.
// Exec-ing out to rpm-ostree rebase requires sudo, so this means apply cannot
// be called within podman's user namespace if run as rootless.
// This means that we need to export images in containers-storage to oci-dirs
// We also need to do this via an exec, because if we tried to use the ABI functions,
// we would enter the user namespace, the rebase command would fail.
// The pull portion of this function essentially is a work-around for two things:
// 1. rpm-ostree requires you to specify the containers-transport when pulling.
// The pull in podman allows the behavior of os apply to match other podman commands,
// where you only pull if the image does not exist in storage already.
// 2. This works around the root/rootless issue.
// Podman machines are by default set up using a rootless connection.
// rpm-ostree needs to be run as root. If a user wants to use an image in containers-storage,
// rpm-ostree will look at the root storage, and not the user storage, which is unexpected behavior.
// Exporting to an oci-dir works around this, without nagging the user to configure the machine in rootful mode.
func (dist *OSTree) Apply(image string, opts ApplyOptions) error {
imageWithTransport := image
transport := alltransports.TransportFromImageName(image)
switch {
// no transport was specified
case transport == nil:
exists, err := execPodmanImageExists(image)
if err != nil {
return err
}
if exists {
fmt.Println("Pulling from", "containers-storage"+":", imageWithTransport)
dir, err := os.MkdirTemp("", pathSafeString(imageWithTransport))
if err != nil {
return err
}
if err := os.Chmod(dir, 0755); err != nil {
return err
}
defer func() {
if err := os.RemoveAll(dir); err != nil {
logrus.Errorf("failed to remove temporary pull file: %v", err)
}
}()
if err := execPodmanSave(dir, image); err != nil {
return err
}
imageWithTransport = "oci:" + dir
} else {
// if image doesn't exist locally, assume that we want to pull and use docker transport
imageWithTransport = "docker://" + image
}
// containers-transport specified
case transport.Name() == "containers-storage":
fmt.Println("Pulling from", image)
dir, err := os.MkdirTemp("", pathSafeString(strings.TrimPrefix(image, "containers-storage"+":")))
if err != nil {
return err
}
if err := os.Chmod(dir, 0755); err != nil {
return err
}
defer func() {
if err := os.RemoveAll(dir); err != nil {
logrus.Errorf("failed to remove temporary pull file: %v", err)
}
}()
if err := execPodmanSave(dir, image); err != nil {
return err
}
imageWithTransport = "oci:" + dir
}
ostreeCli := []string{"rpm-ostree", "--bypass-driver", "rebase", fmt.Sprintf("ostree-unverified-image:%s", imageWithTransport)}
cmd := exec.Command("sudo", ostreeCli...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// pathSafeString creates a path-safe name for our tmpdirs
func pathSafeString(str string) string {
alphanumOnly := regexp.MustCompile(`[^a-zA-Z0-9]+`)
return alphanumOnly.ReplaceAllString(str, "")
}
// execPodmanSave execs out to podman save
func execPodmanSave(dir, image string) error {
saveArgs := []string{"image", "save", "--format", "oci-dir", "-o", dir, image}
saveCmd := exec.Command("podman", saveArgs...)
saveCmd.Stdout = os.Stdout
saveCmd.Stderr = os.Stderr
return saveCmd.Run()
}
// execPodmanSave execs out to podman image exists
func execPodmanImageExists(image string) (bool, error) {
existsArgs := []string{"image", "exists", image}
existsCmd := exec.Command("podman", existsArgs...)
if err := existsCmd.Run(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
switch exitCode := exitError.ExitCode(); exitCode {
case 1:
return false, nil
default:
return false, errors.New("unable to access local image store")
}
}
}
return true, nil
}