mirror of
https://github.com/containers/podman.git
synced 2026-05-24 08:26:40 -04:00
Add --format to image scp
Add --format (oci-archive, docker-archive) to pass through to podman save. Default is unchanged (no --format) so podman save uses its own default. Document that scp is not storage-to-storage and only archive formats are supported. Fixes: https://github.com/containers/podman/issues/28183 Fixes: https://issues.redhat.com/browse/RUN-4403 Signed-off-by: Jan Rodák <hony.com@seznam.cz>
This commit is contained in:
@@ -38,6 +38,8 @@ var (
|
||||
LogLevels = []string{"trace", "debug", "info", "warn", "warning", "error", "fatal", "panic"}
|
||||
// ValidSaveFormats is the list of support podman save formats
|
||||
ValidSaveFormats = []string{define.OCIManifestDir, define.OCIArchive, define.V2s2ManifestDir, define.V2s2Archive}
|
||||
// ValidScpFormats is the list of formats for podman image scp (archive types only)
|
||||
ValidScpFormats = []string{define.OCIArchive, define.V2s2Archive}
|
||||
)
|
||||
|
||||
type completeType int
|
||||
@@ -1711,6 +1713,11 @@ func AutocompleteImageSaveFormat(_ *cobra.Command, _ []string, _ string) ([]stri
|
||||
return ValidSaveFormats, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
// AutocompleteImageScpFormat - Autocomplete image scp format options (oci-archive, docker-archive).
|
||||
func AutocompleteImageScpFormat(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return ValidScpFormats, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
// AutocompleteWaitCondition - Autocomplete wait condition options.
|
||||
// -> "unknown", "configured", "created", "running", "stopped", "paused", "exited", "removing"
|
||||
func AutocompleteWaitCondition(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"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/pkg/domain/entities"
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/common/pkg/ssh"
|
||||
@@ -30,6 +31,7 @@ var (
|
||||
var (
|
||||
parentFlags []string
|
||||
quiet bool
|
||||
format string
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -43,6 +45,10 @@ func init() {
|
||||
func scpFlags(cmd *cobra.Command) {
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&quiet, "quiet", "q", false, "Suppress the output")
|
||||
|
||||
formatChoice := validate.Value(&format, common.ValidScpFormats...)
|
||||
flags.Var(formatChoice, "format", "Format for `podman save` when creating the transfer archive ("+formatChoice.Choices()+"). Default is docker-archive when omitted.")
|
||||
_ = cmd.RegisterFlagCompletionFunc("format", common.AutocompleteImageScpFormat)
|
||||
}
|
||||
|
||||
func scp(_ *cobra.Command, args []string) (finalErr error) {
|
||||
@@ -76,6 +82,7 @@ func scp(_ *cobra.Command, args []string) (finalErr error) {
|
||||
scpOpts.ParentFlags = parentFlags
|
||||
scpOpts.Quiet = quiet
|
||||
scpOpts.SSHMode = sshEngine
|
||||
scpOpts.SaveFormat = format
|
||||
_, err = registry.ImageEngine().Scp(registry.Context(), src, dst, scpOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -10,6 +10,8 @@ podman-image-scp - Securely copy an image from one host to another
|
||||
**podman image scp** copies container images between hosts on a network. This command can copy images to the remote host or from the remote host as well as between two remote hosts.
|
||||
Note: `::` is used to specify the image name depending on Podman is saving or loading. Images can also be transferred from rootful to rootless storage on the same machine without using sshd. This feature is not supported on the remote client, including Mac and Windows (excluding WSL2) machines.
|
||||
|
||||
This is not a direct storage-to-storage copy. The image is saved to an archive (using **podman save**), the archive file is transferred (e.g., over SSH), and then loaded on the destination. As a result, digest references to the original compressed blobs are not preserved (e.g., **podman pull** *image*@*digest* followed by **podman image scp** and then inspecting by that digest may not work). For regular workflows, using a registry (push from source, pull on destination) is often preferable.
|
||||
|
||||
**podman image scp [GLOBAL OPTIONS]**
|
||||
|
||||
**podman image** *scp [OPTIONS] NAME[:TAG] [HOSTNAME::]*
|
||||
@@ -20,6 +22,12 @@ Note: `::` is used to specify the image name depending on Podman is saving or lo
|
||||
|
||||
## OPTIONS
|
||||
|
||||
#### **--format**=*format*
|
||||
|
||||
Format passed to **podman save** when creating the transfer archive. Allowed values are **oci-archive** and **docker-archive**. If omitted, **podman save** uses its default (docker-archive).
|
||||
|
||||
Only the **oci-archive** and **docker-archive** archive (tar) formats are supported. Directory formats (**oci-dir**, **docker-dir**) are not supported because the transfer sends a single file; the remote path does not support directory layouts.
|
||||
|
||||
#### **--help**, **-h**
|
||||
|
||||
Print usage statement
|
||||
@@ -95,6 +103,16 @@ Storing signatures
|
||||
Loaded image: docker.io/library/alpine:latest
|
||||
```
|
||||
|
||||
Copy image to rootful storage with OCI archive format:
|
||||
```
|
||||
$ podman image scp --format oci-archive quay.io/fedora/fedora:43 root@localhost::
|
||||
```
|
||||
|
||||
Copy image to remote host (uses default format when **--format** is omitted):
|
||||
```
|
||||
$ podman image scp alpine root@myserver::
|
||||
```
|
||||
|
||||
## SEE ALSO
|
||||
**[podman(1)](podman.1.md)**, **[podman-load(1)](podman-load.1.md)**, **[podman-save(1)](podman-save.1.md)**, **[podman-remote(1)](podman-remote.1.md)**, **[podman-system-connection-add(1)](podman-system-connection-add.1.md)**, **[containers.conf(5)](https://github.com/containers/container-libs/blob/main/common/docs/containers.conf.5.md)**, **[containers-transports(5)](https://github.com/containers/image/blob/main/docs/containers-transports.5.md)**
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ my %Format_Option_Is_Special = map { $_ => 1 } (
|
||||
'machine os upgrade', # " " " "
|
||||
'push', 'image push', 'manifest push', # oci | v2s*
|
||||
'save', 'image save', # image formats (oci-*, ...)
|
||||
'image scp', # image archive formats only
|
||||
'inspect', # ambiguous (container/image)
|
||||
);
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ type ScpExecuteTransferOptions struct {
|
||||
Quiet bool
|
||||
// SSHMode is the specified ssh.EngineMode which should be used
|
||||
SSHMode ssh.EngineMode
|
||||
// SaveFormat is the format for podman save (oci-archive or docker-archive). Empty means default of podman save (docker-archive).
|
||||
SaveFormat string
|
||||
}
|
||||
|
||||
type ScpExecuteTransferReport struct {
|
||||
@@ -42,11 +44,15 @@ type ScpExecuteTransferReport struct {
|
||||
Dest *ScpTransferImageOptions
|
||||
// ParentFlags are the arguments to apply to the parent podman command when called via ssh
|
||||
ParentFlags []string
|
||||
// SaveFormat is the format to use for podman save when transferring (oci-archive or docker-archive)
|
||||
SaveFormat string
|
||||
}
|
||||
|
||||
type ScpTransferOptions struct {
|
||||
// ParentFlags are the arguments to apply to the parent podman command when called.
|
||||
ParentFlags []string
|
||||
// SaveFormat is the format for podman save (oci-archive or docker-archive)
|
||||
SaveFormat string
|
||||
}
|
||||
|
||||
type ScpTransferReport struct{}
|
||||
@@ -85,6 +91,8 @@ type ScpSaveToRemoteOptions struct {
|
||||
Iden string
|
||||
// SSHMode is the specified ssh.EngineMode which should be used
|
||||
SSHMode ssh.EngineMode
|
||||
// Format is the save format (oci-archive or docker-archive). Empty means default of podman save (docker-archive).
|
||||
Format string
|
||||
}
|
||||
|
||||
type ScpSaveToRemoteReport struct{}
|
||||
|
||||
@@ -788,6 +788,7 @@ func (ir *ImageEngine) Scp(ctx context.Context, src, dst string, opts entities.I
|
||||
if report.LoadReport == nil && (report.Source != nil && report.Dest != nil) { // we need to execute the transfer
|
||||
transferOpts := entities.ScpTransferOptions{}
|
||||
transferOpts.ParentFlags = report.ParentFlags
|
||||
transferOpts.SaveFormat = report.SaveFormat
|
||||
_, err := Transfer(ctx, *report.Source, *report.Dest, transferOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -806,17 +807,20 @@ func Transfer(_ context.Context, source entities.ScpTransferImageOptions, dest e
|
||||
}
|
||||
rep := entities.ScpTransferReport{}
|
||||
if rootless.IsRootless() && (len(dest.User) == 0 || dest.User == "root") { // if we are rootless and do not have a destination user we can just use sudo
|
||||
return &rep, transferRootless(source, dest, podman, opts.ParentFlags)
|
||||
return &rep, transferRootless(source, dest, podman, opts)
|
||||
}
|
||||
return &rep, transferRootful(source, dest, podman, opts.ParentFlags)
|
||||
return &rep, transferRootful(source, dest, podman, opts)
|
||||
}
|
||||
|
||||
// TransferRootless creates new podman processes using exec.Command and sudo, transferring images between the given source and destination users
|
||||
func transferRootless(source entities.ScpTransferImageOptions, dest entities.ScpTransferImageOptions, podman string, parentFlags []string) error {
|
||||
func transferRootless(source entities.ScpTransferImageOptions, dest entities.ScpTransferImageOptions, podman string, opts entities.ScpTransferOptions) error {
|
||||
var cmdSave *exec.Cmd
|
||||
saveCommand := slices.Clone(parentFlags)
|
||||
loadCommand := slices.Clone(parentFlags)
|
||||
saveCommand = append(saveCommand, []string{"save"}...)
|
||||
saveCommand := slices.Clone(opts.ParentFlags)
|
||||
loadCommand := slices.Clone(opts.ParentFlags)
|
||||
saveCommand = append(saveCommand, "save")
|
||||
if opts.SaveFormat != "" {
|
||||
saveCommand = append(saveCommand, "--format", opts.SaveFormat)
|
||||
}
|
||||
loadCommand = append(loadCommand, []string{"load"}...)
|
||||
if source.Quiet {
|
||||
saveCommand = append(saveCommand, "-q")
|
||||
@@ -854,14 +858,17 @@ func transferRootless(source entities.ScpTransferImageOptions, dest entities.Scp
|
||||
}
|
||||
|
||||
// transferRootful creates new podman processes using exec.Command and a new uid/gid alongside a cleared environment
|
||||
func transferRootful(source entities.ScpTransferImageOptions, dest entities.ScpTransferImageOptions, podman string, parentFlags []string) error {
|
||||
basicCommand := make([]string, 0, len(parentFlags)+1)
|
||||
func transferRootful(source entities.ScpTransferImageOptions, dest entities.ScpTransferImageOptions, podman string, opts entities.ScpTransferOptions) error {
|
||||
basicCommand := make([]string, 0, len(opts.ParentFlags)+1)
|
||||
basicCommand = append(basicCommand, podman)
|
||||
basicCommand = append(basicCommand, parentFlags...)
|
||||
basicCommand = append(basicCommand, opts.ParentFlags...)
|
||||
|
||||
saveCommand := make([]string, 0, len(basicCommand)+4)
|
||||
saveCommand := make([]string, 0, len(basicCommand)+6)
|
||||
saveCommand = append(saveCommand, basicCommand...)
|
||||
saveCommand = append(saveCommand, "save")
|
||||
if opts.SaveFormat != "" {
|
||||
saveCommand = append(saveCommand, "--format", opts.SaveFormat)
|
||||
}
|
||||
|
||||
loadCommand := make([]string, 0, len(basicCommand)+3)
|
||||
loadCommand = append(loadCommand, basicCommand...)
|
||||
|
||||
@@ -97,6 +97,7 @@ func ExecuteTransfer(src, dst string, opts entities.ScpExecuteTransferOptions) (
|
||||
saveToRemoteOpts.URL = sshInfo.URI[0]
|
||||
saveToRemoteOpts.Iden = sshInfo.Identities[0]
|
||||
saveToRemoteOpts.SSHMode = opts.SSHMode
|
||||
saveToRemoteOpts.Format = opts.SaveFormat
|
||||
_, err = SaveToRemote(saveToRemoteOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -139,6 +140,9 @@ func ExecuteTransfer(src, dst string, opts entities.ScpExecuteTransferOptions) (
|
||||
saveCmd := []string{podman}
|
||||
saveCmd = append(saveCmd, opts.ParentFlags...)
|
||||
saveCmd = append(saveCmd, "save")
|
||||
if opts.SaveFormat != "" {
|
||||
saveCmd = append(saveCmd, "--format", opts.SaveFormat)
|
||||
}
|
||||
if source.Quiet {
|
||||
saveCmd = append(saveCmd, "-q")
|
||||
}
|
||||
@@ -183,6 +187,7 @@ func ExecuteTransfer(src, dst string, opts entities.ScpExecuteTransferOptions) (
|
||||
rep.Source = &source
|
||||
rep.Dest = &dest
|
||||
rep.ParentFlags = opts.ParentFlags
|
||||
rep.SaveFormat = opts.SaveFormat
|
||||
return &rep, nil // transfer needs to be done in ABI due to cross issues
|
||||
}
|
||||
|
||||
@@ -302,7 +307,14 @@ func SaveToRemote(opts entities.ScpSaveToRemoteOptions) (*entities.ScpSaveToRemo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = ssh.Exec(&ssh.ConnectionExecOptions{Host: opts.URL.String(), Identity: opts.Iden, Port: port, User: opts.URL.User, Args: []string{"podman", "image", "save", opts.Image, "--format", "oci-archive", "--output", remoteFile}}, opts.SSHMode)
|
||||
saveArgs := []string{"podman", "image", "save", opts.Image}
|
||||
if opts.Format != "" {
|
||||
saveArgs = append(saveArgs, "--format", opts.Format)
|
||||
}
|
||||
|
||||
saveArgs = append(saveArgs, "--output", remoteFile)
|
||||
|
||||
_, err = ssh.Exec(&ssh.ConnectionExecOptions{Host: opts.URL.String(), Identity: opts.Iden, Port: port, User: opts.URL.User, Args: saveArgs}, opts.SSHMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -81,6 +81,14 @@ verify_iid_and_name() {
|
||||
run_podman rmi $fqin
|
||||
}
|
||||
|
||||
|
||||
@test "podman image scp --format invalid" {
|
||||
skip_if_remote "only applicable under local podman"
|
||||
|
||||
run_podman 125 image scp --format invalid-format $IMAGE somehost::
|
||||
assert "$output" =~ "Error:.*invalid-format.*is not a valid value" "invalid --format is rejected"
|
||||
}
|
||||
|
||||
@test "podman image scp transfer" {
|
||||
skip_if_remote "only applicable under local podman"
|
||||
|
||||
@@ -123,8 +131,9 @@ verify_iid_and_name() {
|
||||
run_podman image scp $newname ${notme}@localhost::
|
||||
is "$output" "Copying blob .*Copying config.*Writing manifest"
|
||||
|
||||
# confirm that image was copied. FIXME: also try $PODMAN image inspect?
|
||||
_sudo "${PODMAN_CMD[@]}" image exists $newname
|
||||
# Confirm image was copied and manifest format is preserved (OCI).
|
||||
run _sudo "${PODMAN_CMD[@]}" image inspect --format '{{.ManifestType}}' $newname
|
||||
assert "$output" == "application/vnd.docker.distribution.manifest.v2+json" "destination image has Docker manifest type (default) after transfer"
|
||||
|
||||
# Copy it back, this time using -q
|
||||
run_podman untag $IMAGE $newname
|
||||
@@ -149,23 +158,22 @@ verify_iid_and_name() {
|
||||
|
||||
# get foobar's ID, for an ID transfer test
|
||||
run_podman image inspect --format '{{.ID}}' foobar:123
|
||||
run_podman image scp $output ${notme}@localhost::foobartwo
|
||||
run_podman image scp --format oci-archive $output ${notme}@localhost::foobartwo
|
||||
|
||||
_sudo "${PODMAN_CMD[@]}" image exists foobartwo
|
||||
run _sudo "${PODMAN_CMD[@]}" image inspect --format '{{.ManifestType}}' foobartwo
|
||||
assert "$output" == "application/vnd.oci.image.manifest.v1+json" "destination image has OCI manifest type"
|
||||
|
||||
# Clean up
|
||||
_sudo "${PODMAN_CMD[@]}" image rm foobartwo
|
||||
run_podman untag $IMAGE $newname
|
||||
|
||||
# Negative test for nonexistent image.
|
||||
# FIXME: error message is 2 lines, the 2nd being "exit status 125".
|
||||
# FIXME: is that fixable, or do we have to live with it?
|
||||
# Negative test for nonexistent image (output may include exit status on second line).
|
||||
nope="nope.nope/nonesuch:notag"
|
||||
run_podman 125 image scp ${notme}@localhost::$nope
|
||||
is "$output" "Error: $nope: image not known.*" "Pulling nonexistent image"
|
||||
assert "$output" =~ "Error:.*$nope.*image not known" "Pulling nonexistent image"
|
||||
|
||||
run_podman 125 image scp $nope ${notme}@localhost::
|
||||
is "$output" "Error: $nope: image not known.*" "Pushing nonexistent image"
|
||||
assert "$output" =~ "Error:.*$nope.*image not known" "Pushing nonexistent image"
|
||||
|
||||
run_podman rmi foobar:123
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user