From f4138d3599bce78631eed79d0944bc47ccde3244 Mon Sep 17 00:00:00 2001 From: Brent Baude Date: Fri, 30 Jan 2026 13:09:40 -0600 Subject: [PATCH 1/2] 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 --- cmd/podman/machine/os/apply.go | 10 +- .../markdown/podman-machine-os-apply.1.md | 45 +++++- pkg/machine/os/ostree.go | 87 +++++++++- pkg/machine/os/ostree_test.go | 148 ++++++++++++++++++ 4 files changed, 279 insertions(+), 11 deletions(-) diff --git a/cmd/podman/machine/os/apply.go b/cmd/podman/machine/os/apply.go index da011c29a1..45ccdfe540 100644 --- a/cmd/podman/machine/os/apply.go +++ b/cmd/podman/machine/os/apply.go @@ -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 diff --git a/docs/source/markdown/podman-machine-os-apply.1.md b/docs/source/markdown/podman-machine-os-apply.1.md index 35a5536b68..0179bcef69 100644 --- a/docs/source/markdown/podman-machine-os-apply.1.md +++ b/docs/source/markdown/podman-machine-os-apply.1.md @@ -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 ` 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 diff --git a/pkg/machine/os/ostree.go b/pkg/machine/os/ostree.go index 37d33f50a2..176a19d8a3 100644 --- a/pkg/machine/os/ostree.go +++ b/pkg/machine/os/ostree.go @@ -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) +} diff --git a/pkg/machine/os/ostree_test.go b/pkg/machine/os/ostree_test.go index cb9b948fe2..61c842b4f0 100644 --- a/pkg/machine/os/ostree_test.go +++ b/pkg/machine/os/ostree_test.go @@ -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) + } + }) + } +} From 40b2a585f9ae6356c9fc9353903e8fefc7786080 Mon Sep 17 00:00:00 2001 From: Brent Baude Date: Mon, 2 Feb 2026 10:18:05 -0600 Subject: [PATCH 2/2] Autocomplete machine fixes Fixups for autocomplete for machine commands. This was authored by Paul Holzinger. Thank you very much! Signed-off-by: Brent Baude --- cmd/podman/common/completion.go | 70 ++++++++++++++++----------------- cmd/podman/machine/inspect.go | 2 +- cmd/podman/machine/machine.go | 7 ++-- cmd/podman/machine/os/apply.go | 15 +++++-- cmd/podman/machine/rm.go | 2 +- cmd/podman/machine/start.go | 2 +- cmd/podman/machine/stop.go | 2 +- pkg/machine/os/ostree.go | 13 +++--- test/system/600-completion.bats | 5 ++- 9 files changed, 66 insertions(+), 52 deletions(-) diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 0c7226e82f..228da909e2 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -504,12 +504,12 @@ func simplePathJoinUnix(p1, p2 string) string { return p1 + "/" + p2 } -// validCurrentCmdLine validates the current cmd line +// ValidCurrentCmdLine validates the current cmd line // It utilizes the Args function from the cmd struct // In most cases the Args function validates the args length but it // is also used to verify that --latest is not given with an argument. // This function helps to makes sure we only complete valid arguments. -func validCurrentCmdLine(cmd *cobra.Command, args []string, toComplete string) bool { +func ValidCurrentCmdLine(cmd *cobra.Command, args []string, toComplete string) bool { if cmd.Args == nil { // Without an Args function we cannot check so assume it's correct return true @@ -592,14 +592,14 @@ func getBoolCompletion(_ string) ([]string, cobra.ShellCompDirective) { /* Autocomplete Functions for cobra ValidArgsFunction */ func AutocompleteArtifacts(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getArtifacts(cmd, toComplete) } func AutocompleteArtifactAdd(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } if len(args) == 0 { @@ -611,7 +611,7 @@ func AutocompleteArtifactAdd(cmd *cobra.Command, args []string, toComplete strin // AutocompleteContainers - Autocomplete all container names. func AutocompleteContainers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getContainers(cmd, toComplete, completeDefault) @@ -619,7 +619,7 @@ func AutocompleteContainers(cmd *cobra.Command, args []string, toComplete string // AutocompleteContainersCreated - Autocomplete only created container names. func AutocompleteContainersCreated(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getContainers(cmd, toComplete, completeDefault, "created") @@ -627,7 +627,7 @@ func AutocompleteContainersCreated(cmd *cobra.Command, args []string, toComplete // AutocompleteContainersExited - Autocomplete only exited container names. func AutocompleteContainersExited(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getContainers(cmd, toComplete, completeDefault, "exited") @@ -635,7 +635,7 @@ func AutocompleteContainersExited(cmd *cobra.Command, args []string, toComplete // AutocompleteContainersPaused - Autocomplete only paused container names. func AutocompleteContainersPaused(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getContainers(cmd, toComplete, completeDefault, "paused") @@ -643,7 +643,7 @@ func AutocompleteContainersPaused(cmd *cobra.Command, args []string, toComplete // AutocompleteContainersRunning - Autocomplete only running container names. func AutocompleteContainersRunning(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getContainers(cmd, toComplete, completeDefault, "running") @@ -651,7 +651,7 @@ func AutocompleteContainersRunning(cmd *cobra.Command, args []string, toComplete // AutocompleteContainersStartable - Autocomplete only created and exited container names. func AutocompleteContainersStartable(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getContainers(cmd, toComplete, completeDefault, "created", "exited") @@ -659,7 +659,7 @@ func AutocompleteContainersStartable(cmd *cobra.Command, args []string, toComple // AutocompletePods - Autocomplete all pod names. func AutocompletePods(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getPods(cmd, toComplete, completeDefault) @@ -668,7 +668,7 @@ func AutocompletePods(cmd *cobra.Command, args []string, toComplete string) ([]s // AutocompletePodsRunning - Autocomplete only running pod names. // It considers degraded as running. func AutocompletePodsRunning(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getPods(cmd, toComplete, completeDefault, "running", "degraded") @@ -678,7 +678,7 @@ func AutocompletePodsRunning(cmd *cobra.Command, args []string, toComplete strin // When a pod has a few containers paused, that ends up in degraded state // So autocomplete degraded pod names as well func AutoCompletePodsPause(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getPods(cmd, toComplete, completeDefault, "paused", "degraded") @@ -686,7 +686,7 @@ func AutoCompletePodsPause(cmd *cobra.Command, args []string, toComplete string) // AutocompleteForKube - Autocomplete all Podman objects supported by kube generate. func AutocompleteForKube(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } containers, _ := getContainers(cmd, toComplete, completeDefault) @@ -704,7 +704,7 @@ func AutocompleteForGenerate(cmd *cobra.Command, args []string, toComplete strin // AutocompleteContainersAndPods - Autocomplete container names and pod names. func AutocompleteContainersAndPods(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } containers, _ := getContainers(cmd, toComplete, completeDefault) @@ -714,7 +714,7 @@ func AutocompleteContainersAndPods(cmd *cobra.Command, args []string, toComplete // AutocompleteContainersAndImages - Autocomplete container names and pod names. func AutocompleteContainersAndImages(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } containers, _ := getContainers(cmd, toComplete, completeDefault) @@ -724,7 +724,7 @@ func AutocompleteContainersAndImages(cmd *cobra.Command, args []string, toComple // AutocompleteVolumes - Autocomplete volumes. func AutocompleteVolumes(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getVolumes(cmd, toComplete) @@ -732,7 +732,7 @@ func AutocompleteVolumes(cmd *cobra.Command, args []string, toComplete string) ( // AutocompleteSecrets - Autocomplete secrets. func AutocompleteSecrets(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getSecrets(cmd, toComplete, completeDefault) @@ -747,7 +747,7 @@ func AutocompleteSecretCreate(_ *cobra.Command, args []string, _ string) ([]stri // AutocompleteImages - Autocomplete images. func AutocompleteImages(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getImages(cmd, toComplete) @@ -755,7 +755,7 @@ func AutocompleteImages(cmd *cobra.Command, args []string, toComplete string) ([ // AutocompleteQuadlets - Autocomplete quadlets. func AutocompleteQuadlets(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getQuadlets(cmd, toComplete) @@ -763,7 +763,7 @@ func AutocompleteQuadlets(cmd *cobra.Command, args []string, toComplete string) // AutocompleteManifestListAndMember - Autocomplete names of manifest lists and digests of items in them. func AutocompleteManifestListAndMember(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } if len(args) == 0 { @@ -787,7 +787,7 @@ func AutocompletePodExitPolicy(_ *cobra.Command, _ []string, _ string) ([]string // AutocompleteCreateRun - Autocomplete only the fist argument as image and then do file completion. func AutocompleteCreateRun(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } if len(args) < 1 { @@ -829,7 +829,7 @@ func AutocompleteCreateRun(cmd *cobra.Command, args []string, toComplete string) // AutocompleteRegistries - Autocomplete registries. func AutocompleteRegistries(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getRegistries() @@ -837,7 +837,7 @@ func AutocompleteRegistries(cmd *cobra.Command, args []string, toComplete string // AutocompleteNetworks - Autocomplete networks. func AutocompleteNetworks(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return getNetworks(cmd, toComplete, completeDefault) @@ -860,7 +860,7 @@ func AutocompleteDefaultOneArg(_ *cobra.Command, args []string, _ string) ([]str // AutocompleteCommitCommand - Autocomplete podman commit command args. func AutocompleteCommitCommand(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } if len(args) == 0 { @@ -875,7 +875,7 @@ func AutocompleteCommitCommand(cmd *cobra.Command, args []string, toComplete str // AutocompleteCpCommand - Autocomplete podman cp command args. func AutocompleteCpCommand(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } if len(args) < 2 { @@ -921,7 +921,7 @@ func AutocompleteCpCommand(cmd *cobra.Command, args []string, toComplete string) // AutocompleteExecCommand - Autocomplete podman exec command args. func AutocompleteExecCommand(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } if len(args) == 0 { @@ -932,7 +932,7 @@ func AutocompleteExecCommand(cmd *cobra.Command, args []string, toComplete strin // AutocompleteRunlabelCommand - Autocomplete podman container runlabel command args. func AutocompleteRunlabelCommand(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } if len(args) == 0 { @@ -949,7 +949,7 @@ func AutocompleteRunlabelCommand(cmd *cobra.Command, args []string, toComplete s // AutocompleteContainerOneArg - Autocomplete containers as fist arg. func AutocompleteContainerOneArg(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } if len(args) == 0 { @@ -991,7 +991,7 @@ func AutocompleteTopCmd(cmd *cobra.Command, args []string, toComplete string) ([ // AutocompleteInspect - Autocomplete podman inspect. func AutocompleteInspect(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } containers, _ := getContainers(cmd, toComplete, completeDefault) @@ -1010,7 +1010,7 @@ func AutocompleteInspect(cmd *cobra.Command, args []string, toComplete string) ( } func AutoCompleteFarms(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } farms, err := podmanConfig.ContainersConfDefaultsRO.GetAllFarms() @@ -1029,7 +1029,7 @@ func AutoCompleteFarms(cmd *cobra.Command, args []string, toComplete string) ([] // AutocompleteSystemConnections - Autocomplete system connections. func AutocompleteSystemConnections(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } @@ -1050,7 +1050,7 @@ func AutocompleteSystemConnections(cmd *cobra.Command, args []string, toComplete // AutocompleteScp returns a list of connections, images, or both, depending on the amount of arguments func AutocompleteScp(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } switch len(args) { @@ -1957,7 +1957,7 @@ func AutocompleteCompressionFormat(_ *cobra.Command, _ []string, _ string) ([]st // AutocompleteClone - Autocomplete container and image names func AutocompleteClone(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } switch len(args) { @@ -1977,7 +1977,7 @@ func AutocompleteClone(cmd *cobra.Command, args []string, toComplete string) ([] // AutocompleteSSH - Autocomplete ssh modes func AutocompleteSSH(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if !validCurrentCmdLine(cmd, args, toComplete) { + if !ValidCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } return []string{string(ssh.GolangMode), string(ssh.NativeMode)}, cobra.ShellCompDirectiveNoFileComp diff --git a/cmd/podman/machine/inspect.go b/cmd/podman/machine/inspect.go index 2c31581b58..82eef1a7c2 100644 --- a/cmd/podman/machine/inspect.go +++ b/cmd/podman/machine/inspect.go @@ -23,7 +23,7 @@ var ( PersistentPreRunE: machinePreRunE, RunE: inspect, Example: `podman machine inspect myvm`, - ValidArgsFunction: autocompleteMachine, + ValidArgsFunction: AutocompleteMachine, } inspectFlag = inspectFlagType{} ) diff --git a/cmd/podman/machine/machine.go b/cmd/podman/machine/machine.go index 06ce22dc6f..200c064054 100644 --- a/cmd/podman/machine/machine.go +++ b/cmd/podman/machine/machine.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/containers/podman/v6/cmd/podman/common" "github.com/containers/podman/v6/cmd/podman/registry" "github.com/containers/podman/v6/cmd/podman/validate" "github.com/containers/podman/v6/libpod/events" @@ -93,9 +94,9 @@ func autocompleteMachineCp(_ *cobra.Command, args []string, toComplete string) ( return nil, cobra.ShellCompDirectiveNoFileComp } -// autocompleteMachine - Autocomplete machines. -func autocompleteMachine(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) == 0 { +// AutocompleteMachine - Autocomplete machines. +func AutocompleteMachine(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if common.ValidCurrentCmdLine(cmd, args, toComplete) { return getMachines(toComplete) } return nil, cobra.ShellCompDirectiveNoFileComp diff --git a/cmd/podman/machine/os/apply.go b/cmd/podman/machine/os/apply.go index 45ccdfe540..f01bbb23de 100644 --- a/cmd/podman/machine/os/apply.go +++ b/cmd/podman/machine/os/apply.go @@ -12,16 +12,23 @@ import ( ) var applyCmd = &cobra.Command{ - Use: "apply [options] URI [NAME]", + Use: "apply [options] URI|IMAGE [MACHINE]", 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: 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 + switch len(args) { + case 0: + 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 + case 1: + return machine.AutocompleteMachine(cmd, args, toComplete) + default: + return nil, cobra.ShellCompDirectiveNoFileComp + } }, Example: `podman machine os apply myimage`, } diff --git a/cmd/podman/machine/rm.go b/cmd/podman/machine/rm.go index 2dd59402df..6f763943b6 100644 --- a/cmd/podman/machine/rm.go +++ b/cmd/podman/machine/rm.go @@ -18,7 +18,7 @@ var rmCmd = &cobra.Command{ RunE: rm, Args: cobra.MaximumNArgs(1), Example: `podman machine rm podman-machine-default`, - ValidArgsFunction: autocompleteMachine, + ValidArgsFunction: AutocompleteMachine, } var destroyOptions machine.RemoveOptions diff --git a/cmd/podman/machine/start.go b/cmd/podman/machine/start.go index 9a43509bb3..8c1b5fd7c2 100644 --- a/cmd/podman/machine/start.go +++ b/cmd/podman/machine/start.go @@ -21,7 +21,7 @@ var ( RunE: start, Args: cobra.MaximumNArgs(1), Example: `podman machine start podman-machine-default`, - ValidArgsFunction: autocompleteMachine, + ValidArgsFunction: AutocompleteMachine, } startOpts = machine.StartOptions{} setDefaultSystemConn bool diff --git a/cmd/podman/machine/stop.go b/cmd/podman/machine/stop.go index 83bfd239b1..873ef64d11 100644 --- a/cmd/podman/machine/stop.go +++ b/cmd/podman/machine/stop.go @@ -19,7 +19,7 @@ var stopCmd = &cobra.Command{ RunE: stop, Args: cobra.MaximumNArgs(1), Example: `podman machine stop podman-machine-default`, - ValidArgsFunction: autocompleteMachine, + ValidArgsFunction: AutocompleteMachine, } func init() { diff --git a/pkg/machine/os/ostree.go b/pkg/machine/os/ostree.go index 176a19d8a3..37cfd9f850 100644 --- a/pkg/machine/os/ostree.go +++ b/pkg/machine/os/ostree.go @@ -212,6 +212,14 @@ func parseApplyInput(arg string) (string, string, error) { registryTransport = "registry" ) + // The order of this parsing matters. Be careful when you edit. + + // containers-storage:/home/user:localhost/fedora-bootc:latest + afterCS, hasCS := strings.CutPrefix(arg, containersStorageTransport+":") + if hasCS { + return containersStorageTransport, afterCS, nil + } + imgRef, err := alltransports.ParseImageName(arg) if err == nil { transportName := imgRef.Transport().Name() @@ -262,11 +270,6 @@ func parseApplyInput(arg string) (string, string, error) { 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) } diff --git a/test/system/600-completion.bats b/test/system/600-completion.bats index 0afc166078..203383beb5 100644 --- a/test/system/600-completion.bats +++ b/test/system/600-completion.bats @@ -196,7 +196,10 @@ function check_shell_completion() { _check_completion_end NoSpace else _check_completion_end Default - _check_no_suggestions + # machine os apply is special and offers images and normal shell completion + if [[ "$cmd" != "apply" ]]; then + _check_no_suggestions + fi fi ;;