1
0
mirror of https://github.com/containers/podman.git synced 2026-02-05 15:45:08 +01:00

add bootc transports to os-apply

now that we use `bootc switch` for changing out-of-band updates, we can
consider also using some of their supported transports.

* containers-storage
* oci
* oci-archive
* registry

RUN-3963
Signed-off-by: Brent Baude <bbaude@redhat.com>
This commit is contained in:
Brent Baude
2026-01-30 13:09:40 -06:00
parent 2467b71c4a
commit f4138d3599
4 changed files with 279 additions and 11 deletions

View File

@@ -12,14 +12,18 @@ import (
)
var applyCmd = &cobra.Command{
Use: "apply [options] IMAGE [NAME]",
Use: "apply [options] URI [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`,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
images, _ := common.AutocompleteImages(cmd, args, toComplete)
// We also accept an URI so ignore ShellCompDirectiveNoFileComp and use the default one instead to get file paths completed by the shell.
return images, cobra.ShellCompDirectiveDefault
},
Example: `podman machine os apply myimage`,
}
var restart bool

View File

@@ -4,7 +4,7 @@
podman\-machine\-os\-apply - Apply an OCI image to a Podman Machine's OS
## SYNOPSIS
**podman machine os apply** [*options*] *image* [vm]
**podman machine os apply** [*options*] *uri* [vm]
## DESCRIPTION
@@ -19,7 +19,16 @@ customized distribution and cannot be updated with this command.
Note: WSL-based machines are upgradable by using the `podman machine ssh <machine_name>` command followed by `sudo dnf update`. This can, however, result in unexpected results in
Podman client and server version differences.
The applying of the OCI image is done by a command called `bootc`.
The applying of the OCI image is done by a command called `bootc` and specifically `bootc switch`. By default, this command
takes an OCI registry image reference like `quay.io/custom/machine-os:latest`. However, `bootc` also
understands references with different transports. At present, Podman will support the following transports:
* containers-storage
* oci
* oci-archive
* registry
Examples for these transports in URI form are provided below.
Podman machine images are stored as OCI images at `quay.io/podman/machine-os`. When applying an image using this
command, the fully qualified OCI reference name must be used including tag where the tag is the
@@ -47,15 +56,37 @@ bootable OCI image.
Note: This may result in having a newer Podman version inside the machine
than the client. Unexpected results may occur.
Update the default Podman machine to the most recent Podman 6.1 bootable
OCI image.
Apply a new custom operating system from an OCI bootable image on quay.
```
$ podman machine os apply quay.io/podman/machine-os:6.1
$ podman machine os apply quay.io/custom/machine-os:latest
```
Update the specified Podman machine to latest Podman 6.1 bootable OCI image.
Apply a new custom operating system to a specific machine from an OCI bootable image on quay.
```
$ podman machine os apply quay.io/podman/machine-os:6.1 mymachine
$ podman machine os apply quay.io/custom/machine-os:latest mymachine
```
Apply a new custom operating system that was pulled or built on your existing machine by the unprivileged user.
Note the use of brackets around the path because this command is run by the root user.
```
$ podman machine os apply containers-storage:[/home/core/.local/share/containers/storage]localhost/mycustomimage:latest
```
Apply a new custom operating system that was pulled or built on your existing machine by the privileged user. Note that
because a privileged user built or pulled the image, `bootc` will resolve that users storage and the path
is not needed.
```
$ podman machine os apply containers-storage:localhost/mycustomimage:latest
```
Apply a new custom operating system from an OCI archive in tar form.
```
$ podman machine os apply oci-archive:/tmp/oci-image.tar
```
Apply a new custom operating system from an OCI formatted directory.
```
$ podman machine os apply oci:/tmp/oci-image/
```
## SEE ALSO

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/blang/semver/v4"
"github.com/opencontainers/go-digest"
@@ -16,6 +17,7 @@ import (
"go.podman.io/image/v5/docker/reference"
"go.podman.io/image/v5/image"
"go.podman.io/image/v5/manifest"
"go.podman.io/image/v5/transports/alltransports"
"go.podman.io/image/v5/types"
)
@@ -27,7 +29,17 @@ type OSTree struct{}
// is pulled from an OCI registry. We simply pass the user
// input to bootc without any manipulation.
func (dist *OSTree) Apply(image string, _ ApplyOptions) error {
cli := []string{"bootc", "switch", image}
t, pathOrImageRef, err := parseApplyInput(image)
if err != nil {
return err
}
cli := []string{"bootc", "switch"}
// registry is the default transport and therefore not
// necessary
if t != "registry" {
cli = append(cli, "--transport", t)
}
cli = append(cli, pathOrImageRef)
cmd := exec.Command("sudo", cli...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@@ -185,3 +197,76 @@ func printJSON(out UpgradeOutput) error {
fmt.Println(string(b))
return nil
}
// parseApplyInput takes well known OCI references and splits them up. this
// function should only be used to deal with bootc transports. podman takes
// the entire reference as an oci reference but bootc requires we use
// --transport if the default transport is not used. also, bootc's default
// transport is "registry" which mimics docker in the way it is handled.
func parseApplyInput(arg string) (string, string, error) {
var (
containersStorageTransport = "containers-storage"
dockerTransport = "docker"
ociArchiveTransport = "oci-archive"
ociTransport = "oci"
registryTransport = "registry"
)
imgRef, err := alltransports.ParseImageName(arg)
if err == nil {
transportName := imgRef.Transport().Name()
if imgRef.DockerReference() != nil {
// bootc docs do not show the docker transport, but the
// code apparently does handle it and because docker is
// a well-known transport, we do as well, but we change
// it to registry for convenience.
if transportName == dockerTransport {
transportName = registryTransport
}
// docker://quay.io/fedora/fedora-bootc:40
return transportName, imgRef.DockerReference().String(), nil
}
imagePath := imgRef.StringWithinTransport()
if transportName == ociTransport { //nolint:staticcheck
// oci:/tmp/oci-image
imagePath, _, _ = strings.Cut(imagePath, ":")
} else if transportName == ociArchiveTransport {
// oci-archive:/tmp/myimage.tar
imagePath = strings.TrimSuffix(imagePath, ":")
}
return transportName, imagePath, nil
}
// quay.io/fedora/fedora-bootc:40
if _, err := reference.ParseNamed(arg); err == nil {
return registryTransport, arg, nil
}
// registry://quay.io/fedora/fedora-bootc:40
afterReg, hasRegistry := strings.CutPrefix(arg, registryTransport+"://")
if hasRegistry {
return registryTransport, afterReg, nil
}
// oci-archive:/var/tmp/fedora-bootc.tar
// oci-archive:/mnt/usb/images/myapp.tar
afterOCIArchive, hasOCIArchive := strings.CutPrefix(arg, ociArchiveTransport+":")
if hasOCIArchive {
return ociArchiveTransport, afterOCIArchive, nil
}
// oci:/tmp/oci-image
// oci:/var/mnt/usb/bootc-image
afterOCI, hasOCI := strings.CutPrefix(arg, ociTransport+":")
if hasOCI {
return ociTransport, afterOCI, nil
}
// containers-storage:/home/user:localhost/fedora-bootc:latest
afterCS, hasCS := strings.CutPrefix(arg, containersStorageTransport+":")
if hasCS {
return containersStorageTransport, afterCS, nil
}
return "", "", fmt.Errorf("unknown transport %q given", arg)
}

View File

@@ -89,3 +89,151 @@ func Test_compareMajorMinor(t *testing.T) {
})
}
}
func Test_parseApplyInput(t *testing.T) {
type args struct {
arg string
}
tests := []struct {
name string
args args
want string
want1 string
wantErr bool
}{
{
name: "bare registry reference with tag",
args: args{arg: "quay.io/fedora/fedora-bootc:40"},
want: "registry",
want1: "quay.io/fedora/fedora-bootc:40",
wantErr: false,
},
{
name: "docker transport with tag",
args: args{arg: "docker://quay.io/fedora/fedora-bootc:40"},
want: "registry",
want1: "quay.io/fedora/fedora-bootc:40",
wantErr: false,
},
{
name: "docker transport with digest",
args: args{arg: "docker://quay.io/podman/stable@sha256:9cca0703342e24806a9f64e08c053dca7f2cd90f10529af8ea872afb0a0c77d4"},
want: "registry",
want1: "quay.io/podman/stable@sha256:9cca0703342e24806a9f64e08c053dca7f2cd90f10529af8ea872afb0a0c77d4",
wantErr: false,
},
{
name: "registry transport with tag",
args: args{arg: "registry://quay.io/fedora/fedora-bootc:40"},
want: "registry",
want1: "quay.io/fedora/fedora-bootc:40",
wantErr: false,
},
{
name: "registry transport with digest",
args: args{arg: "registry://quay.io/podman/stable@sha256:9cca0703342e24806a9f64e08c053dca7f2cd90f10529af8ea872afb0a0c77d4"},
want: "registry",
want1: "quay.io/podman/stable@sha256:9cca0703342e24806a9f64e08c053dca7f2cd90f10529af8ea872afb0a0c77d4",
wantErr: false,
},
{
name: "registry transport with port",
args: args{arg: "registry://localhost:5000/myapp:latest"},
want: "registry",
want1: "localhost:5000/myapp:latest",
wantErr: false,
},
{
name: "oci transport var path",
args: args{arg: "oci:/var/mnt/usb/bootc-image"},
want: "oci",
want1: "/var/mnt/usb/bootc-image",
wantErr: false,
},
{
name: "oci transport opt path",
args: args{arg: "oci:/opt/images/mylayout"},
want: "oci",
want1: "/opt/images/mylayout",
wantErr: false,
},
{
name: "oci transport tmp path",
args: args{arg: "oci:/tmp/oci-image"},
want: "oci",
want1: "/tmp/oci-image",
wantErr: false,
},
{
name: "oci transport with reference",
args: args{arg: "oci:/path/to/oci-dir:myref"},
want: "oci",
want1: "/path/to/oci-dir:myref",
wantErr: false,
},
{
name: "oci-archive transport tmp tar",
args: args{arg: "oci-archive:/tmp/myimage.tar"},
want: "oci-archive",
want1: "/tmp/myimage.tar",
wantErr: false,
},
{
name: "oci-archive transport mnt path",
args: args{arg: "oci-archive:/mnt/usb/images/myapp.tar"},
want: "oci-archive",
want1: "/mnt/usb/images/myapp.tar",
wantErr: false,
},
{
name: "oci-archive transport with reference",
args: args{arg: "oci-archive:/home/user/archives/image.tar:myref"},
want: "oci-archive",
want1: "/home/user/archives/image.tar:myref",
wantErr: false,
},
{
name: "containers-storage transport with tag",
args: args{arg: "containers-storage:quay.io/podman/machine-os:6.0"},
want: "containers-storage",
want1: "quay.io/podman/machine-os:6.0",
wantErr: false,
},
{
name: "containers-storage transport with graph root",
args: args{arg: "containers-storage:[/home/core/.local/share/containers/storage]quay.io/podman/machine-os:6.0"},
want: "containers-storage",
want1: "[/home/core/.local/share/containers/storage]quay.io/podman/machine-os:6.0",
wantErr: false,
},
{
name: "good reference bad transport",
args: args{"foobar://quay.io/podman/machine-os:6.0"},
want: "",
want1: "",
wantErr: true,
},
{
name: "similar name but incorrect transport",
args: args{"oci-dir:/foo/bar/bar.tar"},
want: "",
want1: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := parseApplyInput(tt.args.arg)
if (err != nil) != tt.wantErr {
t.Errorf("parseApplyInput() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseApplyInput() got = '%v', want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("parseApplyInput() got1 = '%v', want %v", got1, tt.want1)
}
})
}
}