349 lines
12 KiB
Go
349 lines
12 KiB
Go
package images
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
urlP "net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/containers/common/pkg/config"
|
|
"github.com/containers/podman/v3/cmd/podman/common"
|
|
"github.com/containers/podman/v3/cmd/podman/parse"
|
|
"github.com/containers/podman/v3/cmd/podman/registry"
|
|
"github.com/containers/podman/v3/cmd/podman/system/connection"
|
|
"github.com/containers/podman/v3/libpod/define"
|
|
"github.com/containers/podman/v3/pkg/domain/entities"
|
|
"github.com/docker/distribution/reference"
|
|
scpD "github.com/dtylman/scp"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
var (
|
|
saveScpDescription = `Securely copy an image from one host to another.`
|
|
imageScpCommand = &cobra.Command{
|
|
Use: "scp [options] IMAGE [HOST::]",
|
|
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
|
|
Long: saveScpDescription,
|
|
Short: "securely copy images",
|
|
RunE: scp,
|
|
Args: cobra.RangeArgs(1, 2),
|
|
ValidArgsFunction: common.AutocompleteScp,
|
|
Example: `podman image scp myimage:latest otherhost::`,
|
|
}
|
|
)
|
|
|
|
var (
|
|
scpOpts entities.ImageScpOptions
|
|
)
|
|
|
|
func init() {
|
|
registry.Commands = append(registry.Commands, registry.CliCommand{
|
|
Command: imageScpCommand,
|
|
Parent: imageCmd,
|
|
})
|
|
scpFlags(imageScpCommand)
|
|
}
|
|
|
|
func scpFlags(cmd *cobra.Command) {
|
|
flags := cmd.Flags()
|
|
flags.BoolVarP(&scpOpts.Save.Quiet, "quiet", "q", false, "Suppress the output")
|
|
}
|
|
|
|
func scp(cmd *cobra.Command, args []string) (finalErr error) {
|
|
var (
|
|
// TODO add tag support for images
|
|
err error
|
|
)
|
|
if scpOpts.Save.Quiet { // set quiet for both load and save
|
|
scpOpts.Load.Quiet = true
|
|
}
|
|
f, err := ioutil.TempFile("", "podman") // open temp file for load/save output
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(f.Name())
|
|
|
|
scpOpts.Save.Output = f.Name()
|
|
scpOpts.Load.Input = scpOpts.Save.Output
|
|
if err := parse.ValidateFileName(saveOpts.Output); err != nil {
|
|
return err
|
|
}
|
|
confR, err := config.NewConfig("") // create a hand made config for the remote engine since we might use remote and native at once
|
|
if err != nil {
|
|
return errors.Wrapf(err, "could not make config")
|
|
}
|
|
abiEng, err := registry.NewImageEngine(cmd, args) // abi native engine
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg, err := config.ReadCustomConfig() // get ready to set ssh destination if necessary
|
|
if err != nil {
|
|
return err
|
|
}
|
|
serv, err := parseArgs(args, cfg) // parses connection data and "which way" we are loading and saving
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// TODO: Add podman remote support
|
|
confR.Engine = config.EngineConfig{Remote: true, CgroupManager: "cgroupfs", ServiceDestinations: serv} // pass the service dest (either remote or something else) to engine
|
|
switch {
|
|
case scpOpts.FromRemote: // if we want to load FROM the remote
|
|
err = saveToRemote(scpOpts.SourceImageName, scpOpts.Save.Output, "", scpOpts.URI[0], scpOpts.Iden[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if scpOpts.ToRemote { // we want to load remote -> remote
|
|
rep, err := loadToRemote(scpOpts.Save.Output, "", scpOpts.URI[1], scpOpts.Iden[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(rep)
|
|
break
|
|
}
|
|
report, err := abiEng.Load(context.Background(), scpOpts.Load)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println("Loaded image(s): " + strings.Join(report.Names, ","))
|
|
case scpOpts.ToRemote: // remote host load
|
|
scpOpts.Save.Format = "oci-archive"
|
|
abiErr := abiEng.Save(context.Background(), scpOpts.SourceImageName, []string{}, scpOpts.Save) // save the image locally before loading it on remote, local, or ssh
|
|
if abiErr != nil {
|
|
errors.Wrapf(abiErr, "could not save image as specified")
|
|
}
|
|
rep, err := loadToRemote(scpOpts.Save.Output, "", scpOpts.URI[0], scpOpts.Iden[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(rep)
|
|
// TODO: Add podman remote support
|
|
default: // else native load
|
|
if scpOpts.Tag != "" {
|
|
return errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported")
|
|
}
|
|
scpOpts.Save.Format = "oci-archive"
|
|
abiErr := abiEng.Save(context.Background(), scpOpts.SourceImageName, []string{}, scpOpts.Save) // save the image locally before loading it on remote, local, or ssh
|
|
if abiErr != nil {
|
|
errors.Wrapf(abiErr, "could not save image as specified")
|
|
}
|
|
rep, err := abiEng.Load(context.Background(), scpOpts.Load)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println("Loaded image(s): " + strings.Join(rep.Names, ","))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// loadToRemote takes image and remote connection information. it connects to the specified client
|
|
// and copies the saved image dir over to the remote host and then loads it onto the machine
|
|
// returns a string containing output or an error
|
|
func loadToRemote(localFile string, tag string, url *urlP.URL, iden string) (string, error) {
|
|
dial, remoteFile, err := createConnection(url, iden)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer dial.Close()
|
|
|
|
n, err := scpD.CopyTo(dial, localFile, remoteFile)
|
|
if err != nil {
|
|
errOut := (strconv.Itoa(int(n)) + " Bytes copied before error")
|
|
return " ", errors.Wrapf(err, errOut)
|
|
}
|
|
run := ""
|
|
if tag != "" {
|
|
return "", errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported")
|
|
}
|
|
podman := os.Args[0]
|
|
run = podman + " image load --input=" + remoteFile + ";rm " + remoteFile // run ssh image load of the file copied via scp
|
|
out, err := connection.ExecRemoteCommand(dial, run)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSuffix(out, "\n"), nil
|
|
}
|
|
|
|
// saveToRemote takes image information and remote connection information. it connects to the specified client
|
|
// and saves the specified image on the remote machine and then copies it to the specified local location
|
|
// returns an error if one occurs.
|
|
func saveToRemote(image, localFile string, tag string, uri *urlP.URL, iden string) error {
|
|
dial, remoteFile, err := createConnection(uri, iden)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dial.Close()
|
|
|
|
if tag != "" {
|
|
return errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported")
|
|
}
|
|
podman := os.Args[0]
|
|
run := podman + " image save " + image + " --format=oci-archive --output=" + remoteFile // run ssh image load of the file copied via scp. Files are reverse in thie case...
|
|
_, err = connection.ExecRemoteCommand(dial, run)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
n, err := scpD.CopyFrom(dial, remoteFile, localFile)
|
|
connection.ExecRemoteCommand(dial, "rm "+remoteFile)
|
|
if err != nil {
|
|
errOut := (strconv.Itoa(int(n)) + " Bytes copied before error")
|
|
return errors.Wrapf(err, errOut)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// makeRemoteFile creates the necessary remote file on the host to
|
|
// save or load the image to. returns a string with the file name or an error
|
|
func makeRemoteFile(dial *ssh.Client) (string, error) {
|
|
run := "mktemp"
|
|
remoteFile, err := connection.ExecRemoteCommand(dial, run)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
remoteFile = strings.TrimSuffix(remoteFile, "\n")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return remoteFile, nil
|
|
}
|
|
|
|
// createConnections takes a boolean determining which ssh client to dial
|
|
// and returns the dials client, its newly opened remote file, and an error if applicable.
|
|
func createConnection(url *urlP.URL, iden string) (*ssh.Client, string, error) {
|
|
cfg, err := connection.ValidateAndConfigure(url, iden)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
dialAdd, err := ssh.Dial("tcp", url.Host, cfg) // dial the client
|
|
if err != nil {
|
|
return nil, "", errors.Wrapf(err, "failed to connect")
|
|
}
|
|
file, err := makeRemoteFile(dialAdd)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
return dialAdd, file, nil
|
|
}
|
|
|
|
// validateImageName makes sure that the image given is valid and no injections are occurring
|
|
// we simply use this for error checking, bot setting the image
|
|
func validateImageName(input string) error {
|
|
// ParseNormalizedNamed transforms a shortname image into its
|
|
// full name reference so busybox => docker.io/library/busybox
|
|
// we want to keep our shortnames, so only return an error if
|
|
// we cannot parse what th euser has given us
|
|
_, err := reference.ParseNormalizedNamed(input)
|
|
return err
|
|
}
|
|
|
|
// remoteArgLength is a helper function to simplify the extracting of host argument data
|
|
// returns an int which contains the length of a specified index in a host::image string
|
|
func remoteArgLength(input string, side int) int {
|
|
return len((strings.Split(input, "::"))[side])
|
|
}
|
|
|
|
// parseArgs returns the valid connection data based off of the information provided by the user
|
|
// args is an array of the command arguments and cfg is tooling configuration used to get service destinations
|
|
// returned is serv and an error if applicable. serv is a map of service destinations with the connection name as the index
|
|
// this connection name is intended to be used as EngineConfig.ServiceDestinations
|
|
// this function modifies the global scpOpt entities: FromRemote, ToRemote, Connections, and SourceImageName
|
|
func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination, error) {
|
|
serv := map[string]config.Destination{}
|
|
cliConnections := []string{}
|
|
switch len(args) {
|
|
case 1:
|
|
if strings.Contains(args[0], "::") {
|
|
scpOpts.FromRemote = true
|
|
cliConnections = append(cliConnections, args[0])
|
|
} else {
|
|
err := validateImageName(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scpOpts.SourceImageName = args[0]
|
|
}
|
|
case 2:
|
|
if strings.Contains(args[0], "::") {
|
|
if !(strings.Contains(args[1], "::")) && remoteArgLength(args[0], 1) == 0 { // if an image is specified, this mean we are loading to our client
|
|
cliConnections = append(cliConnections, args[0])
|
|
scpOpts.ToRemote = true
|
|
scpOpts.SourceImageName = args[1]
|
|
} else if strings.Contains(args[1], "::") { // both remote clients
|
|
scpOpts.FromRemote = true
|
|
scpOpts.ToRemote = true
|
|
if remoteArgLength(args[0], 1) == 0 { // is save->load w/ one image name
|
|
cliConnections = append(cliConnections, args[0])
|
|
cliConnections = append(cliConnections, args[1])
|
|
} else if remoteArgLength(args[0], 1) > 0 && remoteArgLength(args[1], 1) > 0 {
|
|
//in the future, this function could, instead of rejecting renames, also set a DestImageName field
|
|
return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
|
|
} else { // else its a load save (order of args)
|
|
cliConnections = append(cliConnections, args[1])
|
|
cliConnections = append(cliConnections, args[0])
|
|
}
|
|
} else {
|
|
//in the future, this function could, instead of rejecting renames, also set a DestImageName field
|
|
return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
|
|
}
|
|
} else if strings.Contains(args[1], "::") { // if we are given image host::
|
|
if remoteArgLength(args[1], 1) > 0 {
|
|
//in the future, this function could, instead of rejecting renames, also set a DestImageName field
|
|
return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
|
|
}
|
|
err := validateImageName(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scpOpts.SourceImageName = args[0]
|
|
scpOpts.ToRemote = true
|
|
cliConnections = append(cliConnections, args[1])
|
|
} else {
|
|
//in the future, this function could, instead of rejecting renames, also set a DestImageName field
|
|
return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
|
|
}
|
|
}
|
|
var url string
|
|
var iden string
|
|
for i, val := range cliConnections {
|
|
splitEnv := strings.SplitN(val, "::", 2)
|
|
scpOpts.Connections = append(scpOpts.Connections, splitEnv[0])
|
|
if len(splitEnv[1]) != 0 {
|
|
err := validateImageName(splitEnv[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scpOpts.SourceImageName = splitEnv[1]
|
|
//TODO: actually use the new name given by the user
|
|
}
|
|
conn, found := cfg.Engine.ServiceDestinations[scpOpts.Connections[i]]
|
|
if found {
|
|
url = conn.URI
|
|
iden = conn.Identity
|
|
} else { // no match, warn user and do a manual connection.
|
|
url = "ssh://" + scpOpts.Connections[i]
|
|
iden = ""
|
|
logrus.Warnf("Unknown connection name given. Please use system connection add to specify the default remote socket location")
|
|
}
|
|
urlT, err := urlP.Parse(url) // create an actual url to pass to exec command
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if urlT.User.Username() == "" {
|
|
if urlT.User, err = connection.GetUserInfo(urlT); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
scpOpts.URI = append(scpOpts.URI, urlT)
|
|
scpOpts.Iden = append(scpOpts.Iden, iden)
|
|
}
|
|
return serv, nil
|
|
}
|