summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/podman/common/create.go16
-rw-r--r--cmd/podman/common/netflags.go53
-rw-r--r--cmd/podman/images/scp.go309
-rw-r--r--cmd/podman/images/scp_test.go46
-rw-r--r--cmd/podman/images/scp_utils.go87
-rw-r--r--cmd/podman/main.go4
-rw-r--r--contrib/cirrus/lib.sh1
-rwxr-xr-xcontrib/cirrus/setup_environment.sh3
-rw-r--r--docs/source/_static/api.html2
-rw-r--r--docs/source/markdown/podman-create.1.md17
-rw-r--r--docs/source/markdown/podman-pod-create.1.md43
-rw-r--r--docs/source/markdown/podman-run.1.md17
-rw-r--r--docs/source/markdown/podman-search.1.md2
-rw-r--r--docs/tutorials/mac_experimental.md2
-rw-r--r--libpod/container_config.go11
-rw-r--r--libpod/container_inspect.go36
-rw-r--r--libpod/container_internal.go2
-rw-r--r--libpod/define/pod_inspect.go2
-rw-r--r--libpod/healthcheck_linux.go10
-rw-r--r--libpod/network/internal/util/util.go2
-rw-r--r--libpod/options.go19
-rw-r--r--libpod/pod_api.go3
-rw-r--r--libpod/runtime_volume_linux.go42
-rw-r--r--libpod/volume_internal.go2
-rw-r--r--libpod/volume_internal_linux.go35
-rw-r--r--pkg/api/handlers/compat/containers.go11
-rw-r--r--pkg/api/handlers/compat/events.go2
-rw-r--r--pkg/api/handlers/compat/images.go4
-rw-r--r--pkg/api/handlers/compat/images_build.go4
-rw-r--r--pkg/api/handlers/compat/images_push.go4
-rw-r--r--pkg/api/handlers/compat/images_search.go4
-rw-r--r--pkg/api/handlers/libpod/images.go4
-rw-r--r--pkg/api/handlers/libpod/images_pull.go4
-rw-r--r--pkg/api/handlers/libpod/manifests.go4
-rw-r--r--pkg/api/handlers/libpod/play.go4
-rw-r--r--pkg/api/handlers/libpod/pods.go1
-rw-r--r--pkg/api/server/docs.go4
-rw-r--r--pkg/api/server/register_networks.go4
-rw-r--r--pkg/api/server/register_volumes.go4
-rw-r--r--pkg/auth/auth.go228
-rw-r--r--pkg/auth/auth_test.go377
-rw-r--r--pkg/bindings/images/build.go10
-rw-r--r--pkg/bindings/images/images.go5
-rw-r--r--pkg/bindings/images/pull.go3
-rw-r--r--pkg/bindings/play/play.go3
-rw-r--r--pkg/domain/entities/engine_image.go2
-rw-r--r--pkg/domain/entities/images.go36
-rw-r--r--pkg/domain/entities/pods.go4
-rw-r--r--pkg/domain/infra/abi/images.go179
-rw-r--r--pkg/domain/infra/tunnel/images.go2
-rw-r--r--pkg/machine/ignition.go53
-rw-r--r--pkg/rootless/rootless_linux.c2
-rw-r--r--pkg/specgen/generate/container_create.go111
-rw-r--r--pkg/specgen/generate/oci.go37
-rw-r--r--pkg/specgen/podspecgen.go5
-rw-r--r--pkg/util/utils.go4
-rw-r--r--test/apiv2/20-containers.at4
-rw-r--r--test/apiv2/27-containersEvents.at27
-rw-r--r--test/e2e/common_test.go28
-rw-r--r--test/e2e/image_scp_test.go38
-rw-r--r--test/e2e/libpod_suite_remote_test.go6
-rw-r--r--test/e2e/pod_create_test.go62
-rw-r--r--test/e2e/pod_initcontainers_test.go2
-rw-r--r--test/e2e/run_staticip_test.go15
-rw-r--r--test/e2e/run_test.go2
-rw-r--r--test/system/030-run.bats19
-rw-r--r--test/system/120-load.bats29
-rw-r--r--test/system/160-volumes.bats25
-rw-r--r--test/system/180-blkio.bats2
-rw-r--r--test/system/520-checkpoint.bats2
-rw-r--r--test/system/helpers.bash22
-rw-r--r--test/utils/utils.go5
-rw-r--r--utils/utils.go9
73 files changed, 1574 insertions, 608 deletions
diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go
index f02c5713b..32d227e65 100644
--- a/cmd/podman/common/create.go
+++ b/cmd/podman/common/create.go
@@ -540,14 +540,6 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions,
)
_ = cmd.RegisterFlagCompletionFunc(secretFlagName, AutocompleteSecrets)
- securityOptFlagName := "security-opt"
- createFlags.StringArrayVar(
- &cf.SecurityOpt,
- securityOptFlagName, []string{},
- "Security Options",
- )
- _ = cmd.RegisterFlagCompletionFunc(securityOptFlagName, AutocompleteSecurityOption)
-
shmSizeFlagName := "shm-size"
createFlags.String(
shmSizeFlagName, shmSize(),
@@ -720,6 +712,13 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions,
`If a container with the same name exists, replace it`,
)
}
+ securityOptFlagName := "security-opt"
+ createFlags.StringArrayVar(
+ &cf.SecurityOpt,
+ securityOptFlagName, []string{},
+ "Security Options",
+ )
+ _ = cmd.RegisterFlagCompletionFunc(securityOptFlagName, AutocompleteSecurityOption)
subgidnameFlagName := "subgidname"
createFlags.StringVar(
@@ -890,6 +889,7 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions,
"Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb)",
)
_ = cmd.RegisterFlagCompletionFunc(deviceReadBpsFlagName, completion.AutocompleteDefault)
+
volumesFromFlagName := "volumes-from"
createFlags.StringArrayVar(
&cf.VolumesFrom,
diff --git a/cmd/podman/common/netflags.go b/cmd/podman/common/netflags.go
index ba8ab7a8b..425d85c9d 100644
--- a/cmd/podman/common/netflags.go
+++ b/cmd/podman/common/netflags.go
@@ -53,6 +53,13 @@ func DefineNetFlags(cmd *cobra.Command) {
)
_ = cmd.RegisterFlagCompletionFunc(ipFlagName, completion.AutocompleteNone)
+ ip6FlagName := "ip6"
+ netFlags.String(
+ ip6FlagName, "",
+ "Specify a static IPv6 address for the container",
+ )
+ _ = cmd.RegisterFlagCompletionFunc(ip6FlagName, completion.AutocompleteNone)
+
macAddressFlagName := "mac-address"
netFlags.String(
macAddressFlagName, "",
@@ -185,7 +192,7 @@ func NetFlagsToNetOptions(opts *entities.NetOptions, flags pflag.FlagSet) (*enti
opts.Networks = networks
}
- if flags.Changed("ip") || flags.Changed("mac-address") || flags.Changed("network-alias") {
+ if flags.Changed("ip") || flags.Changed("ip6") || flags.Changed("mac-address") || flags.Changed("network-alias") {
// if there is no network we add the default
if len(opts.Networks) == 0 {
opts.Networks = map[string]types.PerNetworkOptions{
@@ -193,29 +200,31 @@ func NetFlagsToNetOptions(opts *entities.NetOptions, flags pflag.FlagSet) (*enti
}
}
- ip, err := flags.GetString("ip")
- if err != nil {
- return nil, err
- }
- if ip != "" {
- // if pod create --infra=false
- if infra, err := flags.GetBool("infra"); err == nil && !infra {
- return nil, errors.Wrap(define.ErrInvalidArg, "cannot set --ip without infra container")
+ for _, ipFlagName := range []string{"ip", "ip6"} {
+ ip, err := flags.GetString(ipFlagName)
+ if err != nil {
+ return nil, err
}
+ if ip != "" {
+ // if pod create --infra=false
+ if infra, err := flags.GetBool("infra"); err == nil && !infra {
+ return nil, errors.Wrapf(define.ErrInvalidArg, "cannot set --%s without infra container", ipFlagName)
+ }
- staticIP := net.ParseIP(ip)
- if staticIP == nil {
- return nil, errors.Errorf("%s is not an ip address", ip)
- }
- if !opts.Network.IsBridge() && !opts.Network.IsDefault() {
- return nil, errors.Wrap(define.ErrInvalidArg, "--ip can only be set when the network mode is bridge")
- }
- if len(opts.Networks) != 1 {
- return nil, errors.Wrap(define.ErrInvalidArg, "--ip can only be set for a single network")
- }
- for name, netOpts := range opts.Networks {
- netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP)
- opts.Networks[name] = netOpts
+ staticIP := net.ParseIP(ip)
+ if staticIP == nil {
+ return nil, errors.Errorf("%q is not an ip address", ip)
+ }
+ if !opts.Network.IsBridge() && !opts.Network.IsDefault() {
+ return nil, errors.Wrapf(define.ErrInvalidArg, "--%s can only be set when the network mode is bridge", ipFlagName)
+ }
+ if len(opts.Networks) != 1 {
+ return nil, errors.Wrapf(define.ErrInvalidArg, "--%s can only be set for a single network", ipFlagName)
+ }
+ for name, netOpts := range opts.Networks {
+ netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP)
+ opts.Networks[name] = netOpts
+ }
}
}
diff --git a/cmd/podman/images/scp.go b/cmd/podman/images/scp.go
index 5c9cadc7a..f02a3c15e 100644
--- a/cmd/podman/images/scp.go
+++ b/cmd/podman/images/scp.go
@@ -6,18 +6,19 @@ import (
"io/ioutil"
urlP "net/url"
"os"
+ "os/exec"
+ "os/user"
"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/containers/podman/v3/pkg/rootless"
- "github.com/docker/distribution/reference"
+ "github.com/containers/podman/v3/utils"
scpD "github.com/dtylman/scp"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -28,8 +29,12 @@ import (
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},
+ Use: "scp [options] IMAGE [HOST::]",
+ Annotations: map[string]string{
+ registry.UnshareNSRequired: "",
+ registry.ParentNSRequired: "",
+ registry.EngineMode: registry.ABIMode,
+ },
Long: saveScpDescription,
Short: "securely copy images",
RunE: scp,
@@ -40,7 +45,10 @@ var (
)
var (
- scpOpts entities.ImageScpOptions
+ parentFlags []string
+ source entities.ImageScpOptions
+ dest entities.ImageScpOptions
+ sshInfo entities.ImageScpConnections
)
func init() {
@@ -53,7 +61,7 @@ func init() {
func scpFlags(cmd *cobra.Command) {
flags := cmd.Flags()
- flags.BoolVarP(&scpOpts.Save.Quiet, "quiet", "q", false, "Suppress the output")
+ flags.BoolVarP(&source.Quiet, "quiet", "q", false, "Suppress the output")
}
func scp(cmd *cobra.Command, args []string) (finalErr error) {
@@ -61,24 +69,31 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) {
// TODO add tag support for images
err error
)
- if scpOpts.Save.Quiet { // set quiet for both load and save
- scpOpts.Load.Quiet = true
+ for i, val := range os.Args {
+ if val == "image" {
+ break
+ }
+ if i == 0 {
+ continue
+ }
+ if strings.Contains(val, "CIRRUS") { // need to skip CIRRUS flags for testing suite purposes
+ continue
+ }
+ parentFlags = append(parentFlags, val)
}
- f, err := ioutil.TempFile("", "podman") // open temp file for load/save output
+ podman, err := os.Executable()
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 {
+ f, err := ioutil.TempFile("", "podman") // open temp file for load/save output
+ if 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
@@ -88,77 +103,115 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) {
if err != nil {
return err
}
- serv, err := parseArgs(args, cfg) // parses connection data and "which way" we are loading and saving
+ locations := []*entities.ImageScpOptions{}
+ cliConnections := []string{}
+ flipConnections := false
+ for _, arg := range args {
+ loc, connect, err := parseImageSCPArg(arg)
+ if err != nil {
+ return err
+ }
+ locations = append(locations, loc)
+ cliConnections = append(cliConnections, connect...)
+ }
+ source = *locations[0]
+ switch {
+ case len(locations) > 1:
+ if flipConnections, err = validateSCPArgs(locations); err != nil {
+ return err
+ }
+ if flipConnections { // the order of cliConnections matters, we need to flip both arrays since the args are parsed separately sometimes.
+ connect := cliConnections[0]
+ cliConnections[0] = cliConnections[1]
+ cliConnections[1] = connect
+
+ loc := locations[0]
+ locations[0] = locations[1]
+ locations[1] = loc
+ }
+ dest = *locations[1]
+ case len(locations) == 1:
+ switch {
+ case len(locations[0].Image) == 0:
+ return errors.Wrapf(define.ErrInvalidArg, "no source image specified")
+ case len(locations[0].Image) > 0 && !locations[0].Remote && len(locations[0].User) == 0: // if we have podman image scp $IMAGE
+ return errors.Wrapf(define.ErrInvalidArg, "must specify a destination")
+ }
+ }
+
+ source.File = f.Name() // after parsing the arguments, set the file for the save/load
+ dest.File = source.File
+ if err = os.Remove(source.File); err != nil { // remove the file and simply use its name so podman creates the file upon save. avoids umask errors
+ return err
+ }
+
+ var serv map[string]config.Destination
+ serv, err = GetServiceInformation(cliConnections, cfg)
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
+ saveCmd, loadCmd := createCommands(podman)
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])
+ case source.Remote: // if we want to load FROM the remote, dest can either be local or remote in this case
+ err = saveToRemote(source.Image, source.File, "", sshInfo.URI[0], sshInfo.Identities[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 dest.Remote { // we want to load remote -> remote, both source and dest are remote
+ rep, err := loadToRemote(dest.File, "", sshInfo.URI[1], sshInfo.Identities[1])
if err != nil {
return err
}
fmt.Println(rep)
break
}
- report, err := abiEng.Load(context.Background(), scpOpts.Load)
+ err = execPodman(podman, loadCmd)
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])
+ case dest.Remote: // remote host load, implies source is local
+ err = execPodman(podman, saveCmd)
if err != nil {
return err
}
- fmt.Println(rep)
- // TODO: Add podman remote support
- default: // else native load
- scpOpts.Save.Format = "oci-archive"
- _, err := os.Open(scpOpts.Save.Output)
+ rep, err := loadToRemote(source.File, "", sshInfo.URI[0], sshInfo.Identities[0])
if err != nil {
return err
}
- 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 {
- return errors.Wrapf(abiErr, "could not save image as specified")
+ fmt.Println(rep)
+ if err = os.Remove(source.File); err != nil {
+ return err
}
- if !rootless.IsRootless() && scpOpts.Rootless {
- if scpOpts.User == "" {
- scpOpts.User = os.Getenv("SUDO_USER")
- if scpOpts.User == "" {
- return errors.New("could not obtain root user, make sure the environmental variable SUDO_USER is set, and that this command is being run as root")
+ // TODO: Add podman remote support
+ default: // else native load, both source and dest are local and transferring between users
+ if source.User == "" { // source user has to be set, destination does not
+ source.User = os.Getenv("USER")
+ if source.User == "" {
+ u, err := user.Current()
+ if err != nil {
+ return errors.Wrapf(err, "could not obtain user, make sure the environmental variable $USER is set")
}
+ source.User = u.Username
}
- err := abiEng.Transfer(context.Background(), scpOpts)
- if err != nil {
- return err
- }
- } else {
- rep, err := abiEng.Load(context.Background(), scpOpts.Load)
- if err != nil {
- return err
- }
- fmt.Println("Loaded image(s): " + strings.Join(rep.Names, ","))
+ }
+ err := abiEng.Transfer(context.Background(), source, dest, parentFlags)
+ if err != nil {
+ return err
}
}
+ src, err := json.MarshalIndent(source, "", " ")
+ if err != nil {
+ return err
+ }
+ dst, err := json.MarshalIndent(dest, "", " ")
+ if err != nil {
+ return err
+ }
+ fmt.Printf("SOURCE: %s\nDEST: %s\n", string(src), string(dst))
return nil
}
@@ -249,119 +302,28 @@ func createConnection(url *urlP.URL, iden string) (*ssh.Client, string, error) {
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], "localhost") {
- if strings.Split(args[0], "@")[0] != "root" {
- return nil, errors.Wrapf(define.ErrInvalidArg, "cannot transfer images from any user besides root using sudo")
- }
- scpOpts.Rootless = true
- scpOpts.SourceImageName = strings.Split(args[0], "::")[1]
- } else 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], "localhost") || strings.Contains(args[1], "localhost") { // only supporting root to local using sudo at the moment
- if strings.Split(args[0], "@")[0] != "root" {
- return nil, errors.Wrapf(define.ErrInvalidArg, "currently, transferring images to a user account is not supported")
- }
- if len(strings.Split(args[0], "::")) > 1 {
- scpOpts.Rootless = true
- scpOpts.User = strings.Split(args[1], "@")[0]
- scpOpts.SourceImageName = strings.Split(args[0], "::")[1]
- } else {
- return nil, errors.Wrapf(define.ErrInvalidArg, "currently, you cannot rename images during the transfer or transfer them to a user account")
- }
- } else 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")
- }
- }
+// GetSerivceInformation takes the parsed list of hosts to connect to and validates the information
+func GetServiceInformation(cliConnections []string, cfg *config.Config) (map[string]config.Destination, error) {
+ var serv map[string]config.Destination
var url string
var iden string
for i, val := range cliConnections {
splitEnv := strings.SplitN(val, "::", 2)
- scpOpts.Connections = append(scpOpts.Connections, splitEnv[0])
+ sshInfo.Connections = append(sshInfo.Connections, splitEnv[0])
if len(splitEnv[1]) != 0 {
err := validateImageName(splitEnv[1])
if err != nil {
return nil, err
}
- scpOpts.SourceImageName = splitEnv[1]
+ source.Image = splitEnv[1]
//TODO: actually use the new name given by the user
}
- conn, found := cfg.Engine.ServiceDestinations[scpOpts.Connections[i]]
+ conn, found := cfg.Engine.ServiceDestinations[sshInfo.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]
+ url = "ssh://" + sshInfo.Connections[i]
iden = ""
logrus.Warnf("Unknown connection name given. Please use system connection add to specify the default remote socket location")
}
@@ -374,8 +336,45 @@ func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination
return nil, err
}
}
- scpOpts.URI = append(scpOpts.URI, urlT)
- scpOpts.Iden = append(scpOpts.Iden, iden)
+ sshInfo.URI = append(sshInfo.URI, urlT)
+ sshInfo.Identities = append(sshInfo.Identities, iden)
}
return serv, nil
}
+
+// execPodman executes the podman save/load command given the podman binary
+func execPodman(podman string, command []string) error {
+ if rootless.IsRootless() {
+ cmd := exec.Command(podman)
+ utils.CreateSCPCommand(cmd, command[1:])
+ logrus.Debug("Executing podman command")
+ return cmd.Run()
+ }
+ machinectl, err := exec.LookPath("machinectl")
+ if err != nil {
+ cmd := exec.Command("su", "-l", "root", "--command")
+ cmd = utils.CreateSCPCommand(cmd, []string{strings.Join(command, " ")})
+ return cmd.Run()
+ }
+ cmd := exec.Command(machinectl, "shell", "-q", "root@.host")
+ cmd = utils.CreateSCPCommand(cmd, command)
+ logrus.Debug("Executing load command machinectl")
+ return cmd.Run()
+}
+
+// createCommands forms the podman save and load commands used by SCP
+func createCommands(podman string) ([]string, []string) {
+ var parentString string
+ quiet := ""
+ if source.Quiet {
+ quiet = "-q "
+ }
+ if len(parentFlags) > 0 {
+ parentString = strings.Join(parentFlags, " ") + " " // if there are parent args, an extra space needs to be added
+ } else {
+ parentString = strings.Join(parentFlags, " ")
+ }
+ loadCmd := strings.Split(fmt.Sprintf("%s %sload %s--input %s", podman, parentString, quiet, dest.File), " ")
+ saveCmd := strings.Split(fmt.Sprintf("%s %vsave %s--output %s %s", podman, parentString, quiet, source.File, source.Image), " ")
+ return saveCmd, loadCmd
+}
diff --git a/cmd/podman/images/scp_test.go b/cmd/podman/images/scp_test.go
new file mode 100644
index 000000000..d4d8f8e58
--- /dev/null
+++ b/cmd/podman/images/scp_test.go
@@ -0,0 +1,46 @@
+package images
+
+import (
+ "testing"
+
+ "github.com/containers/podman/v3/pkg/domain/entities"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParseSCPArgs(t *testing.T) {
+ args := []string{"alpine", "root@localhost::"}
+ var source *entities.ImageScpOptions
+ var dest *entities.ImageScpOptions
+ var err error
+ source, _, err = parseImageSCPArg(args[0])
+ assert.Nil(t, err)
+ assert.Equal(t, source.Image, "alpine")
+
+ dest, _, err = parseImageSCPArg(args[1])
+ assert.Nil(t, err)
+ assert.Equal(t, dest.Image, "")
+ assert.Equal(t, dest.User, "root")
+
+ args = []string{"root@localhost::alpine"}
+ source, _, err = parseImageSCPArg(args[0])
+ assert.Nil(t, err)
+ assert.Equal(t, source.User, "root")
+ assert.Equal(t, source.Image, "alpine")
+
+ args = []string{"charliedoern@192.168.68.126::alpine", "foobar@192.168.68.126::"}
+ source, _, err = parseImageSCPArg(args[0])
+ assert.Nil(t, err)
+ assert.True(t, source.Remote)
+ assert.Equal(t, source.Image, "alpine")
+
+ dest, _, err = parseImageSCPArg(args[1])
+ assert.Nil(t, err)
+ assert.True(t, dest.Remote)
+ assert.Equal(t, dest.Image, "")
+
+ args = []string{"charliedoern@192.168.68.126::alpine"}
+ source, _, err = parseImageSCPArg(args[0])
+ assert.Nil(t, err)
+ assert.True(t, source.Remote)
+ assert.Equal(t, source.Image, "alpine")
+}
diff --git a/cmd/podman/images/scp_utils.go b/cmd/podman/images/scp_utils.go
new file mode 100644
index 000000000..ebb874c1c
--- /dev/null
+++ b/cmd/podman/images/scp_utils.go
@@ -0,0 +1,87 @@
+package images
+
+import (
+ "strings"
+
+ "github.com/containers/image/v5/docker/reference"
+ "github.com/containers/podman/v3/libpod/define"
+ "github.com/containers/podman/v3/pkg/domain/entities"
+ "github.com/pkg/errors"
+)
+
+// parseImageSCPArg returns the valid connection, and source/destination data based off of the information provided by the user
+// arg is a string containing one of the cli arguments returned is a filled out source/destination options structs as well as a connections array and an error if applicable
+func parseImageSCPArg(arg string) (*entities.ImageScpOptions, []string, error) {
+ location := entities.ImageScpOptions{}
+ var err error
+ cliConnections := []string{}
+
+ switch {
+ case strings.Contains(arg, "@localhost"): // image transfer between users
+ location.User = strings.Split(arg, "@")[0]
+ location, err = validateImagePortion(location, arg)
+ if err != nil {
+ return nil, nil, err
+ }
+ case strings.Contains(arg, "::"):
+ location, err = validateImagePortion(location, arg)
+ if err != nil {
+ return nil, nil, err
+ }
+ location.Remote = true
+ cliConnections = append(cliConnections, arg)
+ default:
+ location.Image = arg
+ }
+ return &location, cliConnections, nil
+}
+
+// validateImagePortion is a helper function to validate the image name in an SCP argument
+func validateImagePortion(location entities.ImageScpOptions, arg string) (entities.ImageScpOptions, error) {
+ if remoteArgLength(arg, 1) > 0 {
+ err := validateImageName(strings.Split(arg, "::")[1])
+ if err != nil {
+ return location, err
+ }
+ location.Image = strings.Split(arg, "::")[1] // this will get checked/set again once we validate connections
+ }
+ return location, nil
+}
+
+// validateSCPArgs takes the array of source and destination options and checks for common errors
+func validateSCPArgs(locations []*entities.ImageScpOptions) (bool, error) {
+ if len(locations) > 2 {
+ return false, errors.Wrapf(define.ErrInvalidArg, "cannot specify more than two arguments")
+ }
+ switch {
+ case len(locations[0].Image) > 0 && len(locations[1].Image) > 0:
+ return false, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
+ case len(locations[0].Image) == 0 && len(locations[1].Image) == 0:
+ return false, errors.Wrapf(define.ErrInvalidArg, "a source image must be specified")
+ case len(locations[0].Image) == 0 && len(locations[1].Image) != 0:
+ if locations[0].Remote && locations[1].Remote {
+ return true, nil // we need to flip the cliConnections array so the save/load connections are in the right place
+ }
+ }
+ return false, 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 the user 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 {
+ if strings.Contains(input, "::") {
+ return len((strings.Split(input, "::"))[side])
+ }
+ return -1
+}
diff --git a/cmd/podman/main.go b/cmd/podman/main.go
index b7f5f1720..b38734617 100644
--- a/cmd/podman/main.go
+++ b/cmd/podman/main.go
@@ -52,14 +52,14 @@ func parseCommands() *cobra.Command {
// Command cannot be run rootless
_, found := c.Command.Annotations[registry.UnshareNSRequired]
if found {
- if rootless.IsRootless() && os.Getuid() != 0 {
+ if rootless.IsRootless() && os.Getuid() != 0 && c.Command.Name() != "scp" {
c.Command.RunE = func(cmd *cobra.Command, args []string) error {
return fmt.Errorf("cannot run command %q in rootless mode, must execute `podman unshare` first", cmd.CommandPath())
}
}
} else {
_, found = c.Command.Annotations[registry.ParentNSRequired]
- if rootless.IsRootless() && found {
+ if rootless.IsRootless() && found && c.Command.Name() != "scp" {
c.Command.RunE = func(cmd *cobra.Command, args []string) error {
return fmt.Errorf("cannot run command %q in rootless mode", cmd.CommandPath())
}
diff --git a/contrib/cirrus/lib.sh b/contrib/cirrus/lib.sh
index cff8f4b3f..c7352106a 100644
--- a/contrib/cirrus/lib.sh
+++ b/contrib/cirrus/lib.sh
@@ -165,6 +165,7 @@ setup_rootless() {
groupadd -g $rootless_gid $ROOTLESS_USER
useradd -g $rootless_gid -u $rootless_uid --no-user-group --create-home $ROOTLESS_USER
chown -R $ROOTLESS_USER:$ROOTLESS_USER "$GOPATH" "$GOSRC"
+ echo "$ROOTLESS_USER ALL=(root) NOPASSWD: ALL" > /etc/sudoers.d/ci-rootless
mkdir -p "$HOME/.ssh" "/home/$ROOTLESS_USER/.ssh"
diff --git a/contrib/cirrus/setup_environment.sh b/contrib/cirrus/setup_environment.sh
index 43c709228..ee80a209c 100755
--- a/contrib/cirrus/setup_environment.sh
+++ b/contrib/cirrus/setup_environment.sh
@@ -121,8 +121,9 @@ case "$OS_RELEASE_ID" in
# Force a crun version that has this fix: https://github.com/containers/crun/pull/819
# FIXME: Remove once a fixed crun made its way into Fedora
if test "$OS_RELEASE_VER" == "35"; then
- yum upgrade -y https://kojipkgs.fedoraproject.org//work/tasks/684/80280684/crun-1.3-2.fc35.x86_64.rpm
+ yum upgrade -y crun
fi
+
if ((CONTAINER==0)); then
# All SELinux distros need this for systemd-in-a-container
msg "Enabling container_manage_cgroup"
diff --git a/docs/source/_static/api.html b/docs/source/_static/api.html
index fbc945d87..6d467d099 100644
--- a/docs/source/_static/api.html
+++ b/docs/source/_static/api.html
@@ -18,7 +18,7 @@
</style>
</head>
<body>
- <redoc spec-url='https://storage.googleapis.com/libpod-master-releases/swagger-latest.yaml' sort-props-alphabetically></redoc>
+ <redoc spec-url='https://storage.googleapis.com/libpod-master-releases/swagger-latest.yaml' sort-props-alphabetically sort-operations-alphabetically></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
</body>
</html>
diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md
index 3d4b867d4..e3647b194 100644
--- a/docs/source/markdown/podman-create.1.md
+++ b/docs/source/markdown/podman-create.1.md
@@ -474,19 +474,24 @@ Path to the container-init binary.
Keep STDIN open even if not attached. The default is *false*.
-#### **--ip6**=*ip*
+#### **--ip**=*ipv4*
-Not implemented
-
-#### **--ip**=*ip*
-
-Specify a static IP address for the container, for example **10.88.64.128**.
+Specify a static IPv4 address for the container, for example **10.88.64.128**.
This option can only be used if the container is joined to only a single network - i.e., **--network=network-name** is used at most once -
and if the container is not joining another container's network namespace via **--network=container:_id_**.
The address must be within the network's IP address pool (default **10.88.0.0/16**).
To specify multiple static IP addresses per container, set multiple networks using the **--network** option with a static IP address specified for each using the `ip` mode for that option.
+#### **--ip6**=*ipv6*
+
+Specify a static IPv6 address for the container, for example **fd46:db93:aa76:ac37::10**.
+This option can only be used if the container is joined to only a single network - i.e., **--network=network-name** is used at most once -
+and if the container is not joining another container's network namespace via **--network=container:_id_**.
+The address must be within the network's IPv6 address pool.
+
+To specify multiple static IPv6 addresses per container, set multiple networks using the **--network** option with a static IPv6 address specified for each using the `ip6` mode for that option.
+
#### **--ipc**=*ipc*
diff --git a/docs/source/markdown/podman-pod-create.1.md b/docs/source/markdown/podman-pod-create.1.md
index b1b029429..b2e16e051 100644
--- a/docs/source/markdown/podman-pod-create.1.md
+++ b/docs/source/markdown/podman-pod-create.1.md
@@ -127,6 +127,15 @@ The address must be within the network's IP address pool (default **10.88.0.0/16
To specify multiple static IP addresses per pod, set multiple networks using the **--network** option with a static IP address specified for each using the `ip` mode for that option.
+#### **--ip6**=*ipv6*
+
+Specify a static IPv6 address for the pod, for example **fd46:db93:aa76:ac37::10**.
+This option can only be used if the pod is joined to only a single network - i.e., **--network=network-name** is used at most once -
+and if the pod is not joining another container's network namespace via **--network=container:_id_**.
+The address must be within the network's IPv6 address pool.
+
+To specify multiple static IPv6 addresses per pod, set multiple networks using the **--network** option with a static IPv6 address specified for each using the `ip6` mode for that option.
+
#### **--label**=*label*, **-l**
Add metadata to a pod (e.g., --label com.example.key=value).
@@ -222,6 +231,38 @@ NOTE: This cannot be modified once the pod is created.
If another pod with the same name already exists, replace and remove it. The default is **false**.
+#### **--security-opt**=*option*
+
+Security Options
+
+- `apparmor=unconfined` : Turn off apparmor confinement for the pod
+- `apparmor=your-profile` : Set the apparmor confinement profile for the pod
+
+- `label=user:USER` : Set the label user for the pod processes
+- `label=role:ROLE` : Set the label role for the pod processes
+- `label=type:TYPE` : Set the label process type for the pod processes
+- `label=level:LEVEL` : Set the label level for the pod processes
+- `label=filetype:TYPE` : Set the label file type for the pod files
+- `label=disable` : Turn off label separation for the pod
+
+Note: Labeling can be disabled for all pods/containers by setting label=false in the **containers.conf** (`/etc/containers/containers.conf` or `$HOME/.config/containers/containers.conf`) file.
+
+- `mask=/path/1:/path/2` : The paths to mask separated by a colon. A masked path
+ cannot be accessed inside the containers within the pod.
+
+- `no-new-privileges` : Disable container processes from gaining additional privileges
+
+- `seccomp=unconfined` : Turn off seccomp confinement for the pod
+- `seccomp=profile.json` : Whitelisted syscalls seccomp Json file to be used as a seccomp filter
+
+- `proc-opts=OPTIONS` : Comma-separated list of options to use for the /proc mount. More details for the
+ possible mount options are specified in the **proc(5)** man page.
+
+- **unmask**=_ALL_ or _/path/1:/path/2_, or shell expanded paths (/proc/*): Paths to unmask separated by a colon. If set to **ALL**, it will unmask all the paths that are masked or made read only by default.
+ The default masked paths are **/proc/acpi, /proc/kcore, /proc/keys, /proc/latency_stats, /proc/sched_debug, /proc/scsi, /proc/timer_list, /proc/timer_stats, /sys/firmware, and /sys/fs/selinux.** The default paths that are read only are **/proc/asound, /proc/bus, /proc/fs, /proc/irq, /proc/sys, /proc/sysrq-trigger, /sys/fs/cgroup**.
+
+Note: Labeling can be disabled for all containers by setting label=false in the **containers.conf** (`/etc/containers/containers.conf` or `$HOME/.config/containers/containers.conf`) file.
+
#### **--share**=*namespace*
A comma-separated list of kernel namespaces to share. If none or "" is specified, no namespaces will be shared. The namespaces to choose from are ipc, net, pid, uts.
@@ -462,7 +503,7 @@ $ podman pod create --network net1:ip=10.89.1.5 --network net2:ip=10.89.10.10
```
## SEE ALSO
-**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**
+**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **containers.conf(1)**
## HISTORY
diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md
index 6c565e3d1..b98e563ef 100644
--- a/docs/source/markdown/podman-run.1.md
+++ b/docs/source/markdown/podman-run.1.md
@@ -497,19 +497,24 @@ Path to the container-init binary.
When set to **true**, keep stdin open even if not attached. The default is **false**.
-#### **--ip6**=*ip*
+#### **--ip**=*ipv4*
-Not implemented.
-
-#### **--ip**=*ip*
-
-Specify a static IP address for the container, for example **10.88.64.128**.
+Specify a static IPv4 address for the container, for example **10.88.64.128**.
This option can only be used if the container is joined to only a single network - i.e., **--network=network-name** is used at most once -
and if the container is not joining another container's network namespace via **--network=container:_id_**.
The address must be within the network's IP address pool (default **10.88.0.0/16**).
To specify multiple static IP addresses per container, set multiple networks using the **--network** option with a static IP address specified for each using the `ip` mode for that option.
+#### **--ip6**=*ipv6*
+
+Specify a static IPv6 address for the container, for example **fd46:db93:aa76:ac37::10**.
+This option can only be used if the container is joined to only a single network - i.e., **--network=network-name** is used at most once -
+and if the container is not joining another container's network namespace via **--network=container:_id_**.
+The address must be within the network's IPv6 address pool.
+
+To specify multiple static IPv6 addresses per container, set multiple networks using the **--network** option with a static IPv6 address specified for each using the `ip6` mode for that option.
+
#### **--ipc**=*mode*
Set the IPC namespace mode for a container. The default is to create
diff --git a/docs/source/markdown/podman-search.1.md b/docs/source/markdown/podman-search.1.md
index 9e166fcc2..9c075a1e0 100644
--- a/docs/source/markdown/podman-search.1.md
+++ b/docs/source/markdown/podman-search.1.md
@@ -62,7 +62,7 @@ Valid placeholders for the Go template are listed below:
| --------------- | ---------------------------- |
| .Index | Registry |
| .Name | Image name |
-| .Descriptions | Image description |
+| .Description | Image description |
| .Stars | Star count of image |
| .Official | "[OK]" if image is official |
| .Automated | "[OK]" if image is automated |
diff --git a/docs/tutorials/mac_experimental.md b/docs/tutorials/mac_experimental.md
index 8df64dc99..b5b815fe5 100644
--- a/docs/tutorials/mac_experimental.md
+++ b/docs/tutorials/mac_experimental.md
@@ -90,7 +90,7 @@ that you were given. It will be used in two of the steps below.
## Test podman
-1. podman machine init --image-path /path/to/image
+1. podman machine init --image-path /path/to/image --cpus 2
2. podman machine start
3. podman images
4. git clone http://github.com/baude/alpine_nginx && cd alpine_nginx
diff --git a/libpod/container_config.go b/libpod/container_config.go
index a43fd632b..288524dbd 100644
--- a/libpod/container_config.go
+++ b/libpod/container_config.go
@@ -400,3 +400,14 @@ type ContainerMiscConfig struct {
// and if so, what type: always or once are possible non-nil entries
InitContainerType string `json:"init_container_type,omitempty"`
}
+
+type InfraInherit struct {
+ InfraSecurity ContainerSecurityConfig
+ InfraLabels []string `json:"labelopts,omitempty"`
+ InfraVolumes []*ContainerNamedVolume `json:"namedVolumes,omitempty"`
+ InfraOverlay []*ContainerOverlayVolume `json:"overlayVolumes,omitempty"`
+ InfraImageVolumes []*ContainerImageVolume `json:"ctrImageVolumes,omitempty"`
+ InfraUserVolumes []string `json:"userVolumes,omitempty"`
+ InfraResources *spec.LinuxResources `json:"resources,omitempty"`
+ InfraDevices []spec.LinuxDevice `json:"device_host_src,omitempty"`
+}
diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go
index f72700ab6..792dfc58e 100644
--- a/libpod/container_inspect.go
+++ b/libpod/container_inspect.go
@@ -273,6 +273,27 @@ func (c *Container) GetInspectMounts(namedVolumes []*ContainerNamedVolume, image
return inspectMounts, nil
}
+// GetSecurityOptions retrives and returns the security related annotations and process information upon inspection
+func (c *Container) GetSecurityOptions() []string {
+ ctrSpec := c.config.Spec
+ SecurityOpt := []string{}
+ if ctrSpec.Process != nil {
+ if ctrSpec.Process.NoNewPrivileges {
+ SecurityOpt = append(SecurityOpt, "no-new-privileges")
+ }
+ }
+ if label, ok := ctrSpec.Annotations[define.InspectAnnotationLabel]; ok {
+ SecurityOpt = append(SecurityOpt, fmt.Sprintf("label=%s", label))
+ }
+ if seccomp, ok := ctrSpec.Annotations[define.InspectAnnotationSeccomp]; ok {
+ SecurityOpt = append(SecurityOpt, fmt.Sprintf("seccomp=%s", seccomp))
+ }
+ if apparmor, ok := ctrSpec.Annotations[define.InspectAnnotationApparmor]; ok {
+ SecurityOpt = append(SecurityOpt, fmt.Sprintf("apparmor=%s", apparmor))
+ }
+ return SecurityOpt
+}
+
// Parse mount options so we can populate them in the mount structure.
// The mount passed in will be modified.
func parseMountOptionsForInspect(options []string, mount *define.InspectMount) {
@@ -422,16 +443,14 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named
hostConfig.GroupAdd = make([]string, 0, len(c.config.Groups))
hostConfig.GroupAdd = append(hostConfig.GroupAdd, c.config.Groups...)
- hostConfig.SecurityOpt = []string{}
if ctrSpec.Process != nil {
if ctrSpec.Process.OOMScoreAdj != nil {
hostConfig.OomScoreAdj = *ctrSpec.Process.OOMScoreAdj
}
- if ctrSpec.Process.NoNewPrivileges {
- hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, "no-new-privileges")
- }
}
+ hostConfig.SecurityOpt = c.GetSecurityOptions()
+
hostConfig.ReadonlyRootfs = ctrSpec.Root.Readonly
hostConfig.ShmSize = c.config.ShmSize
hostConfig.Runtime = "oci"
@@ -456,15 +475,6 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named
if ctrSpec.Annotations[define.InspectAnnotationInit] == define.InspectResponseTrue {
hostConfig.Init = true
}
- if label, ok := ctrSpec.Annotations[define.InspectAnnotationLabel]; ok {
- hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, fmt.Sprintf("label=%s", label))
- }
- if seccomp, ok := ctrSpec.Annotations[define.InspectAnnotationSeccomp]; ok {
- hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, fmt.Sprintf("seccomp=%s", seccomp))
- }
- if apparmor, ok := ctrSpec.Annotations[define.InspectAnnotationApparmor]; ok {
- hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, fmt.Sprintf("apparmor=%s", apparmor))
- }
}
// Resource limits
diff --git a/libpod/container_internal.go b/libpod/container_internal.go
index 7ae9daefa..2d12a90d1 100644
--- a/libpod/container_internal.go
+++ b/libpod/container_internal.go
@@ -762,7 +762,7 @@ func (c *Container) export(path string) error {
if !c.state.Mounted {
containerMount, err := c.runtime.store.Mount(c.ID(), c.config.MountLabel)
if err != nil {
- return errors.Wrapf(err, "error mounting container %q", c.ID())
+ return errors.Wrapf(err, "mounting container %q", c.ID())
}
mountPoint = containerMount
defer func() {
diff --git a/libpod/define/pod_inspect.go b/libpod/define/pod_inspect.go
index 97e7ffdfb..e7adc8700 100644
--- a/libpod/define/pod_inspect.go
+++ b/libpod/define/pod_inspect.go
@@ -65,6 +65,8 @@ type InspectPodData struct {
BlkioDeviceReadBps []InspectBlkioThrottleDevice `json:"device_read_bps,omitempty"`
// VolumesFrom contains the containers that the pod inherits mounts from
VolumesFrom []string `json:"volumes_from,omitempty"`
+ // SecurityOpt contains the specified security labels and related SELinux information
+ SecurityOpts []string `json:"security_opt,omitempty"`
}
// InspectPodInfraConfig contains the configuration of the pod's infra
diff --git a/libpod/healthcheck_linux.go b/libpod/healthcheck_linux.go
index 2c19e0a61..a1f3e8491 100644
--- a/libpod/healthcheck_linux.go
+++ b/libpod/healthcheck_linux.go
@@ -73,6 +73,16 @@ func (c *Container) removeTransientFiles(ctx context.Context) error {
defer conn.Close()
timerFile := fmt.Sprintf("%s.timer", c.ID())
serviceFile := fmt.Sprintf("%s.service", c.ID())
+
+ // If the service has failed (the healthcheck has failed), then
+ // the .service file is not removed on stopping the unit file. If
+ // we check the properties of the service, it will automatically
+ // reset the state. But checking the state takes msecs vs usecs to
+ // blindly call reset.
+ if err := conn.ResetFailedUnitContext(ctx, serviceFile); err != nil {
+ logrus.Debugf("failed to reset unit file: %q", err)
+ }
+
// We want to ignore errors where the timer unit and/or service unit has already
// been removed. The error return is generic so we have to check against the
// string in the error
diff --git a/libpod/network/internal/util/util.go b/libpod/network/internal/util/util.go
index bf9d70aba..d9b9a8dc0 100644
--- a/libpod/network/internal/util/util.go
+++ b/libpod/network/internal/util/util.go
@@ -78,7 +78,7 @@ func GetUsedSubnets(n NetUtil) ([]*net.IPNet, error) {
return append(subnets, liveSubnets...), nil
}
-// GetFreeIPv6NetworkSubnet returns a unused ipv4 subnet
+// GetFreeIPv4NetworkSubnet returns a unused ipv4 subnet
func GetFreeIPv4NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) {
// the default podman network is 10.88.0.0/16
// start locking for free /24 networks
diff --git a/libpod/options.go b/libpod/options.go
index 204f2a457..6edb9972b 100644
--- a/libpod/options.go
+++ b/libpod/options.go
@@ -1816,6 +1816,25 @@ func WithSelectedPasswordManagement(passwd *bool) CtrCreateOption {
}
}
+// WithInfraConfig allows for inheritance of compatible config entities from the infra container
+func WithInfraConfig(compatibleOptions InfraInherit) CtrCreateOption {
+ return func(ctr *Container) error {
+ if ctr.valid {
+ return define.ErrCtrFinalized
+ }
+ compatMarshal, err := json.Marshal(compatibleOptions)
+ if err != nil {
+ return errors.New("Could not marshal compatible options")
+ }
+
+ err = json.Unmarshal(compatMarshal, ctr.config)
+ if err != nil {
+ return errors.New("Could not unmarshal compatible options into contrainer config")
+ }
+ return nil
+ }
+}
+
// Pod Creation Options
// WithPodCreateCommand adds the full command plus arguments of the current
diff --git a/libpod/pod_api.go b/libpod/pod_api.go
index 95a82721e..526e0c28b 100644
--- a/libpod/pod_api.go
+++ b/libpod/pod_api.go
@@ -586,6 +586,7 @@ func (p *Pod) Inspect() (*define.InspectPodData, error) {
var inspectMounts []define.InspectMount
var devices []define.InspectDevice
var deviceLimits []define.InspectBlkioThrottleDevice
+ var infraSecurity []string
if p.state.InfraContainerID != "" {
infra, err := p.runtime.GetContainer(p.state.InfraContainerID)
if err != nil {
@@ -603,6 +604,7 @@ func (p *Pod) Inspect() (*define.InspectPodData, error) {
infraConfig.UserNS = p.UserNSMode()
namedVolumes, mounts := infra.sortUserVolumes(infra.config.Spec)
inspectMounts, err = infra.GetInspectMounts(namedVolumes, infra.config.ImageVolumes, mounts)
+ infraSecurity = infra.GetSecurityOptions()
if err != nil {
return nil, err
}
@@ -678,6 +680,7 @@ func (p *Pod) Inspect() (*define.InspectPodData, error) {
Devices: devices,
BlkioDeviceReadBps: deviceLimits,
VolumesFrom: p.VolumesFrom(),
+ SecurityOpts: infraSecurity,
}
return &inspectData, nil
diff --git a/libpod/runtime_volume_linux.go b/libpod/runtime_volume_linux.go
index ed3cc971c..d4d9a4438 100644
--- a/libpod/runtime_volume_linux.go
+++ b/libpod/runtime_volume_linux.go
@@ -35,7 +35,7 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption)
volume := newVolume(r)
for _, option := range options {
if err := option(volume); err != nil {
- return nil, errors.Wrapf(err, "error running volume create option")
+ return nil, errors.Wrapf(err, "running volume create option")
}
}
@@ -50,7 +50,7 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption)
// Check if volume with given name exists.
exists, err := r.state.HasVolume(volume.config.Name)
if err != nil {
- return nil, errors.Wrapf(err, "error checking if volume with name %s exists", volume.config.Name)
+ return nil, errors.Wrapf(err, "checking if volume with name %s exists", volume.config.Name)
}
if exists {
return nil, errors.Wrapf(define.ErrVolumeExists, "volume with name %s already exists", volume.config.Name)
@@ -67,9 +67,15 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption)
if volume.config.Driver == define.VolumeDriverLocal {
logrus.Debugf("Validating options for local driver")
// Validate options
- for key := range volume.config.Options {
- switch key {
- case "device", "o", "type", "UID", "GID", "SIZE", "INODES":
+ for key, val := range volume.config.Options {
+ switch strings.ToLower(key) {
+ case "device":
+ if strings.ToLower(volume.config.Options["type"]) == "bind" {
+ if _, err := os.Stat(val); err != nil {
+ return nil, errors.Wrapf(err, "invalid volume option %s for driver 'local'", key)
+ }
+ }
+ case "o", "type", "uid", "gid", "size", "inodes":
// Do nothing, valid keys
default:
return nil, errors.Wrapf(define.ErrInvalidArg, "invalid mount option %s for driver 'local'", key)
@@ -92,17 +98,17 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption)
// Create the mountpoint of this volume
volPathRoot := filepath.Join(r.config.Engine.VolumePath, volume.config.Name)
if err := os.MkdirAll(volPathRoot, 0700); err != nil {
- return nil, errors.Wrapf(err, "error creating volume directory %q", volPathRoot)
+ return nil, errors.Wrapf(err, "creating volume directory %q", volPathRoot)
}
if err := os.Chown(volPathRoot, volume.config.UID, volume.config.GID); err != nil {
- return nil, errors.Wrapf(err, "error chowning volume directory %q to %d:%d", volPathRoot, volume.config.UID, volume.config.GID)
+ return nil, errors.Wrapf(err, "chowning volume directory %q to %d:%d", volPathRoot, volume.config.UID, volume.config.GID)
}
fullVolPath := filepath.Join(volPathRoot, "_data")
if err := os.MkdirAll(fullVolPath, 0755); err != nil {
- return nil, errors.Wrapf(err, "error creating volume directory %q", fullVolPath)
+ return nil, errors.Wrapf(err, "creating volume directory %q", fullVolPath)
}
if err := os.Chown(fullVolPath, volume.config.UID, volume.config.GID); err != nil {
- return nil, errors.Wrapf(err, "error chowning volume directory %q to %d:%d", fullVolPath, volume.config.UID, volume.config.GID)
+ return nil, errors.Wrapf(err, "chowning volume directory %q to %d:%d", fullVolPath, volume.config.UID, volume.config.GID)
}
if err := LabelVolumePath(fullVolPath); err != nil {
return nil, err
@@ -132,7 +138,7 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption)
lock, err := r.lockManager.AllocateLock()
if err != nil {
- return nil, errors.Wrapf(err, "error allocating lock for new volume")
+ return nil, errors.Wrapf(err, "allocating lock for new volume")
}
volume.lock = lock
volume.config.LockID = volume.lock.ID()
@@ -149,7 +155,7 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption)
// Add the volume to state
if err := r.state.AddVolume(volume); err != nil {
- return nil, errors.Wrapf(err, "error adding volume to state")
+ return nil, errors.Wrapf(err, "adding volume to state")
}
defer volume.newVolumeEvent(events.Create)
return volume, nil
@@ -181,7 +187,7 @@ func makeVolumeInPluginIfNotExist(name string, options map[string]string, plugin
createReq.Name = name
createReq.Options = options
if err := plugin.CreateVolume(createReq); err != nil {
- return errors.Wrapf(err, "error creating volume %q in plugin %s", name, plugin.Name)
+ return errors.Wrapf(err, "creating volume %q in plugin %s", name, plugin.Name)
}
}
@@ -225,13 +231,13 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeo
continue
}
- return errors.Wrapf(err, "error removing container %s that depends on volume %s", dep, v.Name())
+ return errors.Wrapf(err, "removing container %s that depends on volume %s", dep, v.Name())
}
logrus.Debugf("Removing container %s (depends on volume %q)", ctr.ID(), v.Name())
if err := r.removeContainer(ctx, ctr, force, false, false, timeout); err != nil {
- return errors.Wrapf(err, "error removing container %s that depends on volume %s", ctr.ID(), v.Name())
+ return errors.Wrapf(err, "removing container %s that depends on volume %s", ctr.ID(), v.Name())
}
}
}
@@ -244,7 +250,7 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeo
// them.
logrus.Errorf("Unmounting volume %s: %v", v.Name(), err)
} else {
- return errors.Wrapf(err, "error unmounting volume %s", v.Name())
+ return errors.Wrapf(err, "unmounting volume %s", v.Name())
}
}
@@ -288,13 +294,13 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeo
if removalErr != nil {
logrus.Errorf("Removing volume %s from plugin %s: %v", v.Name(), v.Driver(), removalErr)
}
- return errors.Wrapf(err, "error removing volume %s", v.Name())
+ return errors.Wrapf(err, "removing volume %s", v.Name())
}
// Free the volume's lock
if err := v.lock.Free(); err != nil {
if removalErr == nil {
- removalErr = errors.Wrapf(err, "error freeing lock for volume %s", v.Name())
+ removalErr = errors.Wrapf(err, "freeing lock for volume %s", v.Name())
} else {
logrus.Errorf("Freeing lock for volume %q: %v", v.Name(), err)
}
@@ -304,7 +310,7 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeo
// from /var/lib/containers/storage/volumes
if err := v.teardownStorage(); err != nil {
if removalErr == nil {
- removalErr = errors.Wrapf(err, "error cleaning up volume storage for %q", v.Name())
+ removalErr = errors.Wrapf(err, "cleaning up volume storage for %q", v.Name())
} else {
logrus.Errorf("Cleaning up volume storage for volume %q: %v", v.Name(), err)
}
diff --git a/libpod/volume_internal.go b/libpod/volume_internal.go
index f69f1c044..f9e1ea87d 100644
--- a/libpod/volume_internal.go
+++ b/libpod/volume_internal.go
@@ -81,7 +81,7 @@ func (v *Volume) save() error {
func (v *Volume) refresh() error {
lock, err := v.runtime.lockManager.AllocateAndRetrieveLock(v.config.LockID)
if err != nil {
- return errors.Wrapf(err, "error acquiring lock %d for volume %s", v.config.LockID, v.Name())
+ return errors.Wrapf(err, "acquiring lock %d for volume %s", v.config.LockID, v.Name())
}
v.lock = lock
diff --git a/libpod/volume_internal_linux.go b/libpod/volume_internal_linux.go
index 45cd22385..abd31df0f 100644
--- a/libpod/volume_internal_linux.go
+++ b/libpod/volume_internal_linux.go
@@ -7,7 +7,6 @@ import (
"strings"
"github.com/containers/podman/v3/libpod/define"
- "github.com/containers/podman/v3/pkg/rootless"
pluginapi "github.com/docker/go-plugins-helpers/volume"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -32,13 +31,6 @@ func (v *Volume) mount() error {
return nil
}
- // We cannot mount 'local' volumes as rootless.
- if !v.UsesVolumeDriver() && rootless.IsRootless() {
- // This check should only be applied to 'local' driver
- // so Volume Drivers must be excluded
- return errors.Wrapf(define.ErrRootless, "cannot mount volumes without root privileges")
- }
-
// Update the volume from the DB to get an accurate mount counter.
if err := v.update(); err != nil {
return err
@@ -90,22 +82,27 @@ func (v *Volume) mount() error {
// TODO: might want to cache this path in the runtime?
mountPath, err := exec.LookPath("mount")
if err != nil {
- return errors.Wrapf(err, "error locating 'mount' binary")
+ return errors.Wrapf(err, "locating 'mount' binary")
}
mountArgs := []string{}
if volOptions != "" {
mountArgs = append(mountArgs, "-o", volOptions)
}
- if volType != "" {
+ switch volType {
+ case "":
+ case "bind":
+ mountArgs = append(mountArgs, "-o", volType)
+ default:
mountArgs = append(mountArgs, "-t", volType)
}
+
mountArgs = append(mountArgs, volDevice, v.config.MountPoint)
mountCmd := exec.Command(mountPath, mountArgs...)
logrus.Debugf("Running mount command: %s %s", mountPath, strings.Join(mountArgs, " "))
if output, err := mountCmd.CombinedOutput(); err != nil {
logrus.Debugf("Mount %v failed with %v", mountCmd, err)
- return errors.Wrapf(errors.Errorf(string(output)), "error mounting volume %s", v.Name())
+ return errors.Errorf(string(output))
}
logrus.Debugf("Mounted volume %s", v.Name())
@@ -139,20 +136,6 @@ func (v *Volume) unmount(force bool) error {
return nil
}
- // We cannot unmount 'local' volumes as rootless.
- if !v.UsesVolumeDriver() && rootless.IsRootless() {
- // If force is set, just clear the counter and bail without
- // error, so we can remove volumes from the state if they are in
- // an awkward configuration.
- if force {
- logrus.Errorf("Volume %s is mounted despite being rootless - state is not sane", v.Name())
- v.state.MountCount = 0
- return v.save()
- }
-
- return errors.Wrapf(define.ErrRootless, "cannot mount or unmount volumes without root privileges")
- }
-
if !force {
v.state.MountCount--
} else {
@@ -184,7 +167,7 @@ func (v *Volume) unmount(force bool) error {
// Ignore EINVAL - the mount no longer exists.
return nil
}
- return errors.Wrapf(err, "error unmounting volume %s", v.Name())
+ return errors.Wrapf(err, "unmounting volume %s", v.Name())
}
logrus.Debugf("Unmounted volume %s", v.Name())
}
diff --git a/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go
index 5a06722ec..ad341c3ab 100644
--- a/pkg/api/handlers/compat/containers.go
+++ b/pkg/api/handlers/compat/containers.go
@@ -356,6 +356,15 @@ func LibpodToContainer(l *libpod.Container, sz bool) (*handlers.Container, error
return nil, err
}
+ m, err := json.Marshal(inspect.Mounts)
+ if err != nil {
+ return nil, err
+ }
+ mounts := []types.MountPoint{}
+ if err := json.Unmarshal(m, &mounts); err != nil {
+ return nil, err
+ }
+
return &handlers.Container{Container: types.Container{
ID: l.ID(),
Names: []string{fmt.Sprintf("/%s", l.Name())},
@@ -374,7 +383,7 @@ func LibpodToContainer(l *libpod.Container, sz bool) (*handlers.Container, error
}{
"host"},
NetworkSettings: &networkSettings,
- Mounts: nil,
+ Mounts: mounts,
},
ContainerCreateConfig: types.ContainerCreateConfig{},
}, nil
diff --git a/pkg/api/handlers/compat/events.go b/pkg/api/handlers/compat/events.go
index 901acdac4..bc31a36c4 100644
--- a/pkg/api/handlers/compat/events.go
+++ b/pkg/api/handlers/compat/events.go
@@ -91,6 +91,8 @@ func GetEvents(w http.ResponseWriter, r *http.Request) {
e := entities.ConvertToEntitiesEvent(*evt)
if !utils.IsLibpodRequest(r) && e.Status == "died" {
e.Status = "die"
+ e.Action = "die"
+ e.Actor.Attributes["exitCode"] = e.Actor.Attributes["containerExitCode"]
}
if err := coder.Encode(e); err != nil {
diff --git a/pkg/api/handlers/compat/images.go b/pkg/api/handlers/compat/images.go
index 4533fddeb..c1cc99da4 100644
--- a/pkg/api/handlers/compat/images.go
+++ b/pkg/api/handlers/compat/images.go
@@ -270,9 +270,9 @@ func CreateImageFromImage(w http.ResponseWriter, r *http.Request) {
return
}
- authConf, authfile, key, err := auth.GetCredentials(r)
+ authConf, authfile, err := auth.GetCredentials(r)
if err != nil {
- utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
+ utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err)
return
}
defer auth.RemoveAuthfile(authfile)
diff --git a/pkg/api/handlers/compat/images_build.go b/pkg/api/handlers/compat/images_build.go
index 45e4543a9..0fcac5330 100644
--- a/pkg/api/handlers/compat/images_build.go
+++ b/pkg/api/handlers/compat/images_build.go
@@ -453,10 +453,10 @@ func BuildImage(w http.ResponseWriter, r *http.Request) {
}
}
- creds, authfile, key, err := auth.GetCredentials(r)
+ creds, authfile, err := auth.GetCredentials(r)
if err != nil {
// Credential value(s) not returned as their value is not human readable
- utils.BadRequest(w, key.String(), "n/a", err)
+ utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err)
return
}
defer auth.RemoveAuthfile(authfile)
diff --git a/pkg/api/handlers/compat/images_push.go b/pkg/api/handlers/compat/images_push.go
index 3a84b5799..04cad204d 100644
--- a/pkg/api/handlers/compat/images_push.go
+++ b/pkg/api/handlers/compat/images_push.go
@@ -85,9 +85,9 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
return
}
- authconf, authfile, key, err := auth.GetCredentials(r)
+ authconf, authfile, err := auth.GetCredentials(r)
if err != nil {
- utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
+ utils.Error(w, "Something went wrong.", http.StatusBadRequest, err)
return
}
defer auth.RemoveAuthfile(authfile)
diff --git a/pkg/api/handlers/compat/images_search.go b/pkg/api/handlers/compat/images_search.go
index e9cc3e2b6..f6ad86a04 100644
--- a/pkg/api/handlers/compat/images_search.go
+++ b/pkg/api/handlers/compat/images_search.go
@@ -34,9 +34,9 @@ func SearchImages(w http.ResponseWriter, r *http.Request) {
return
}
- _, authfile, key, err := auth.GetCredentials(r)
+ _, authfile, err := auth.GetCredentials(r)
if err != nil {
- utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
+ utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err)
return
}
defer auth.RemoveAuthfile(authfile)
diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go
index f2f93434a..6e23845f0 100644
--- a/pkg/api/handlers/libpod/images.go
+++ b/pkg/api/handlers/libpod/images.go
@@ -497,9 +497,9 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
return
}
- authconf, authfile, key, err := auth.GetCredentials(r)
+ authconf, authfile, err := auth.GetCredentials(r)
if err != nil {
- utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
+ utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err)
return
}
defer auth.RemoveAuthfile(authfile)
diff --git a/pkg/api/handlers/libpod/images_pull.go b/pkg/api/handlers/libpod/images_pull.go
index fabdb326b..518e7cc65 100644
--- a/pkg/api/handlers/libpod/images_pull.go
+++ b/pkg/api/handlers/libpod/images_pull.go
@@ -68,9 +68,9 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) {
}
// Do the auth dance.
- authConf, authfile, key, err := auth.GetCredentials(r)
+ authConf, authfile, err := auth.GetCredentials(r)
if err != nil {
- utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
+ utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err)
return
}
defer auth.RemoveAuthfile(authfile)
diff --git a/pkg/api/handlers/libpod/manifests.go b/pkg/api/handlers/libpod/manifests.go
index 869c83fa3..eb0b6827f 100644
--- a/pkg/api/handlers/libpod/manifests.go
+++ b/pkg/api/handlers/libpod/manifests.go
@@ -176,9 +176,9 @@ func ManifestPush(w http.ResponseWriter, r *http.Request) {
}
source := utils.GetName(r)
- authconf, authfile, key, err := auth.GetCredentials(r)
+ authconf, authfile, err := auth.GetCredentials(r)
if err != nil {
- utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
+ utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err)
return
}
defer auth.RemoveAuthfile(authfile)
diff --git a/pkg/api/handlers/libpod/play.go b/pkg/api/handlers/libpod/play.go
index 312aa32de..6ef83ad92 100644
--- a/pkg/api/handlers/libpod/play.go
+++ b/pkg/api/handlers/libpod/play.go
@@ -86,9 +86,9 @@ func PlayKube(w http.ResponseWriter, r *http.Request) {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error closing temporary file"))
return
}
- authConf, authfile, key, err := auth.GetCredentials(r)
+ authConf, authfile, err := auth.GetCredentials(r)
if err != nil {
- utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
+ utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err)
return
}
defer auth.RemoveAuthfile(authfile)
diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go
index 3d18406a5..1b29831b4 100644
--- a/pkg/api/handlers/libpod/pods.go
+++ b/pkg/api/handlers/libpod/pods.go
@@ -42,6 +42,7 @@ func PodCreate(w http.ResponseWriter, r *http.Request) {
infraOptions := entities.NewInfraContainerCreateOptions() // options for pulling the image and FillOutSpec
infraOptions.Net = &entities.NetOptions{}
infraOptions.Devices = psg.Devices
+ infraOptions.SecurityOpt = psg.SecurityOpt
err = specgenutil.FillOutSpecGen(psg.InfraContainerSpec, &infraOptions, []string{}) // necessary for default values in many cases (userns, idmappings)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error filling out specgen"))
diff --git a/pkg/api/server/docs.go b/pkg/api/server/docs.go
index 83d9ef160..2127e7d82 100644
--- a/pkg/api/server/docs.go
+++ b/pkg/api/server/docs.go
@@ -1,4 +1,4 @@
-// Package api Provides an API for the Libpod library
+// Package api Provides an API for the Libpod library
//
// This documentation describes the Podman v2.0 RESTful API.
// It replaces the Podman v1.0 API and was initially delivered
@@ -45,7 +45,7 @@
// Schemes: http, https
// Host: podman.io
// BasePath: /
-// Version: 3.2.0
+// Version: 4.0.0
// License: Apache-2.0 https://opensource.org/licenses/Apache-2.0
// Contact: Podman <podman@lists.podman.io> https://podman.io/community/
//
diff --git a/pkg/api/server/register_networks.go b/pkg/api/server/register_networks.go
index 344486299..77e8a80fd 100644
--- a/pkg/api/server/register_networks.go
+++ b/pkg/api/server/register_networks.go
@@ -101,7 +101,7 @@ func (s *APIServer) registerNetworkHandlers(r *mux.Router) error {
// parameters:
// - in: body
// name: create
- // description: attributes for creating a container
+ // description: attributes for creating a network
// schema:
// $ref: "#/definitions/NetworkCreateRequest"
// responses:
@@ -312,7 +312,7 @@ func (s *APIServer) registerNetworkHandlers(r *mux.Router) error {
// parameters:
// - in: body
// name: create
- // description: attributes for creating a container
+ // description: attributes for creating a network
// schema:
// $ref: "#/definitions/NetworkCreateLibpod"
// responses:
diff --git a/pkg/api/server/register_volumes.go b/pkg/api/server/register_volumes.go
index fb02cffcf..d1c1d5024 100644
--- a/pkg/api/server/register_volumes.go
+++ b/pkg/api/server/register_volumes.go
@@ -17,7 +17,7 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error {
// parameters:
// - in: body
// name: create
- // description: attributes for creating a container
+ // description: attributes for creating a volume
// schema:
// $ref: "#/definitions/VolumeCreate"
// produces:
@@ -188,7 +188,7 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error {
// - in: body
// name: create
// description: |
- // attributes for creating a container.
+ // attributes for creating a volume.
// Note: If a volume by the same name exists, a 201 response with that volume's information will be generated.
// schema:
// $ref: "#/definitions/DockerVolumeCreate"
diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go
index 070e222ad..f423c011d 100644
--- a/pkg/auth/auth.go
+++ b/pkg/auth/auth.go
@@ -3,7 +3,6 @@ package auth
import (
"encoding/base64"
"encoding/json"
- "fmt"
"io/ioutil"
"net/http"
"os"
@@ -16,52 +15,70 @@ import (
"github.com/sirupsen/logrus"
)
-type HeaderAuthName string
-
-func (h HeaderAuthName) String() string { return string(h) }
-
-// XRegistryAuthHeader is the key to the encoded registry authentication configuration in an http-request header.
-// This header supports one registry per header occurrence. To support N registries provided N headers, one per registry.
+// xRegistryAuthHeader is the key to the encoded registry authentication configuration in an http-request header.
+// This header supports one registry per header occurrence. To support N registries provide N headers, one per registry.
// As of Docker API 1.40 and Libpod API 1.0.0, this header is supported by all endpoints.
-const XRegistryAuthHeader HeaderAuthName = "X-Registry-Auth"
+const xRegistryAuthHeader = "X-Registry-Auth"
-// XRegistryConfigHeader is the key to the encoded registry authentication configuration in an http-request header.
+// xRegistryConfigHeader is the key to the encoded registry authentication configuration in an http-request header.
// This header supports N registries in one header via a Base64 encoded, JSON map.
// As of Docker API 1.40 and Libpod API 2.0.0, this header is supported by build endpoints.
-const XRegistryConfigHeader HeaderAuthName = "X-Registry-Config"
+const xRegistryConfigHeader = "X-Registry-Config"
// GetCredentials queries the http.Request for X-Registry-.* headers and extracts
-// the necessary authentication information for libpod operations
-func GetCredentials(r *http.Request) (*types.DockerAuthConfig, string, HeaderAuthName, error) {
- has := func(key HeaderAuthName) bool { hdr, found := r.Header[string(key)]; return found && len(hdr) > 0 }
- switch {
- case has(XRegistryConfigHeader):
- c, f, err := getConfigCredentials(r)
- return c, f, XRegistryConfigHeader, err
- case has(XRegistryAuthHeader):
- c, f, err := getAuthCredentials(r)
- return c, f, XRegistryAuthHeader, err
- }
- return nil, "", "", nil
+// the necessary authentication information for libpod operations, possibly
+// creating a config file. If that is the case, the caller must call RemoveAuthFile.
+func GetCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) {
+ nonemptyHeaderValue := func(key string) ([]string, bool) {
+ hdr := r.Header.Values(key)
+ return hdr, len(hdr) > 0
+ }
+ var override *types.DockerAuthConfig
+ var fileContents map[string]types.DockerAuthConfig
+ var headerName string
+ var err error
+ if hdr, ok := nonemptyHeaderValue(xRegistryConfigHeader); ok {
+ headerName = xRegistryConfigHeader
+ override, fileContents, err = getConfigCredentials(r, hdr)
+ } else if hdr, ok := nonemptyHeaderValue(xRegistryAuthHeader); ok {
+ headerName = xRegistryAuthHeader
+ override, fileContents, err = getAuthCredentials(hdr)
+ } else {
+ return nil, "", nil
+ }
+ if err != nil {
+ return nil, "", errors.Wrapf(err, "failed to parse %q header for %s", headerName, r.URL.String())
+ }
+
+ var authFile string
+ if fileContents == nil {
+ authFile = ""
+ } else {
+ authFile, err = authConfigsToAuthFile(fileContents)
+ if err != nil {
+ return nil, "", errors.Wrapf(err, "failed to parse %q header for %s", headerName, r.URL.String())
+ }
+ }
+ return override, authFile, nil
}
-// getConfigCredentials extracts one or more docker.AuthConfig from the request's
-// header. An empty key will be used as default while a named registry will be
+// getConfigCredentials extracts one or more docker.AuthConfig from a request and its
+// xRegistryConfigHeader value. An empty key will be used as default while a named registry will be
// returned as types.DockerAuthConfig
-func getConfigCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) {
+func getConfigCredentials(r *http.Request, headers []string) (*types.DockerAuthConfig, map[string]types.DockerAuthConfig, error) {
var auth *types.DockerAuthConfig
configs := make(map[string]types.DockerAuthConfig)
- for _, h := range r.Header[string(XRegistryConfigHeader)] {
+ for _, h := range headers {
param, err := base64.URLEncoding.DecodeString(h)
if err != nil {
- return nil, "", errors.Wrapf(err, "failed to decode %q", XRegistryConfigHeader)
+ return nil, nil, errors.Wrapf(err, "failed to decode %q", xRegistryConfigHeader)
}
ac := make(map[string]dockerAPITypes.AuthConfig)
err = json.Unmarshal(param, &ac)
if err != nil {
- return nil, "", errors.Wrapf(err, "failed to unmarshal %q", XRegistryConfigHeader)
+ return nil, nil, errors.Wrapf(err, "failed to unmarshal %q", xRegistryConfigHeader)
}
for k, v := range ac {
@@ -91,79 +108,45 @@ func getConfigCredentials(r *http.Request) (*types.DockerAuthConfig, string, err
if auth == nil {
logrus.Debugf("%q header found in request, but \"registry=%v\" query parameter not provided",
- XRegistryConfigHeader, registries)
+ xRegistryConfigHeader, registries)
} else {
- logrus.Debugf("%q header found in request for username %q", XRegistryConfigHeader, auth.Username)
+ logrus.Debugf("%q header found in request for username %q", xRegistryConfigHeader, auth.Username)
}
}
- authfile, err := authConfigsToAuthFile(configs)
- return auth, authfile, err
+ return auth, configs, nil
}
-// getAuthCredentials extracts one or more DockerAuthConfigs from the request's
-// header. The header could specify a single-auth config in which case the
+// getAuthCredentials extracts one or more DockerAuthConfigs from an xRegistryAuthHeader
+// value. The header could specify a single-auth config in which case the
// first return value is set. In case of a multi-auth header, the contents are
-// stored in a temporary auth file (2nd return value). Note that the auth file
-// should be removed after usage.
-func getAuthCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) {
+// returned in the second return value.
+func getAuthCredentials(headers []string) (*types.DockerAuthConfig, map[string]types.DockerAuthConfig, error) {
+ authHeader := headers[0]
+
// First look for a multi-auth header (i.e., a map).
- authConfigs, err := multiAuthHeader(r)
+ authConfigs, err := parseMultiAuthHeader(authHeader)
if err == nil {
- authfile, err := authConfigsToAuthFile(authConfigs)
- return nil, authfile, err
+ return nil, authConfigs, nil
}
// Fallback to looking for a single-auth header (i.e., one config).
- authConfigs, err = singleAuthHeader(r)
- if err != nil {
- return nil, "", err
- }
- var conf *types.DockerAuthConfig
- for k := range authConfigs {
- c := authConfigs[k]
- conf = &c
- break
- }
- return conf, "", nil
-}
-
-// Header builds the requested Authentication Header
-func Header(sys *types.SystemContext, headerName HeaderAuthName, authfile, username, password string) (map[string]string, error) {
- var (
- content string
- err error
- )
- switch headerName {
- case XRegistryAuthHeader:
- content, err = headerAuth(sys, authfile, username, password)
- case XRegistryConfigHeader:
- content, err = headerConfig(sys, authfile, username, password)
- default:
- err = fmt.Errorf("unsupported authentication header: %q", headerName)
- }
+ authConfig, err := parseSingleAuthHeader(authHeader)
if err != nil {
- return nil, err
+ return nil, nil, err
}
-
- if len(content) > 0 {
- return map[string]string{string(headerName): content}, nil
- }
- return nil, nil
+ return &authConfig, nil, nil
}
-// headerConfig returns a map with the XRegistryConfigHeader set which can
+// MakeXRegistryConfigHeader returns a map with the "X-Registry-Config" header set, which can
// conveniently be used in the http stack.
-func headerConfig(sys *types.SystemContext, authfile, username, password string) (string, error) {
+func MakeXRegistryConfigHeader(sys *types.SystemContext, username, password string) (map[string]string, error) {
if sys == nil {
sys = &types.SystemContext{}
}
- if authfile != "" {
- sys.AuthFilePath = authfile
- }
authConfigs, err := imageAuth.GetAllCredentials(sys)
if err != nil {
- return "", err
+ return nil, err
}
if username != "" {
@@ -174,29 +157,38 @@ func headerConfig(sys *types.SystemContext, authfile, username, password string)
}
if len(authConfigs) == 0 {
- return "", nil
+ return nil, nil
}
- return encodeMultiAuthConfigs(authConfigs)
+ content, err := encodeMultiAuthConfigs(authConfigs)
+ if err != nil {
+ return nil, err
+ }
+ return map[string]string{xRegistryConfigHeader: content}, nil
}
-// headerAuth returns a base64 encoded map with the XRegistryAuthHeader set which can
+// MakeXRegistryAuthHeader returns a map with the "X-Registry-Auth" header set, which can
// conveniently be used in the http stack.
-func headerAuth(sys *types.SystemContext, authfile, username, password string) (string, error) {
+func MakeXRegistryAuthHeader(sys *types.SystemContext, username, password string) (map[string]string, error) {
if username != "" {
- return encodeSingleAuthConfig(types.DockerAuthConfig{Username: username, Password: password})
+ content, err := encodeSingleAuthConfig(types.DockerAuthConfig{Username: username, Password: password})
+ if err != nil {
+ return nil, err
+ }
+ return map[string]string{xRegistryAuthHeader: content}, nil
}
if sys == nil {
sys = &types.SystemContext{}
}
- if authfile != "" {
- sys.AuthFilePath = authfile
- }
authConfigs, err := imageAuth.GetAllCredentials(sys)
if err != nil {
- return "", err
+ return nil, err
+ }
+ content, err := encodeMultiAuthConfigs(authConfigs)
+ if err != nil {
+ return nil, err
}
- return encodeMultiAuthConfigs(authConfigs)
+ return map[string]string{xRegistryAuthHeader: content}, nil
}
// RemoveAuthfile is a convenience function that is meant to be called in a
@@ -258,34 +250,38 @@ func authConfigsToAuthFile(authConfigs map[string]types.DockerAuthConfig) (strin
// Now use the c/image packages to store the credentials. It's battle
// tested, and we make sure to use the same code as the image backend.
sys := types.SystemContext{AuthFilePath: authFilePath}
- for server, config := range authConfigs {
- server = normalize(server)
+ for authFileKey, config := range authConfigs {
+ key := normalizeAuthFileKey(authFileKey)
// Note that we do not validate the credentials here. We assume
// that all credentials are valid. They'll be used on demand
// later.
- if err := imageAuth.SetAuthentication(&sys, server, config.Username, config.Password); err != nil {
- return "", errors.Wrapf(err, "error storing credentials in temporary auth file (server: %q, user: %q)", server, config.Username)
+ if err := imageAuth.SetAuthentication(&sys, key, config.Username, config.Password); err != nil {
+ return "", errors.Wrapf(err, "error storing credentials in temporary auth file (key: %q / %q, user: %q)", authFileKey, key, config.Username)
}
}
return authFilePath, nil
}
-// normalize takes a server and removes the leading "http[s]://" prefix as well
-// as removes path suffixes from docker registries.
-func normalize(server string) string {
- stripped := strings.TrimPrefix(server, "http://")
+// normalizeAuthFileKey takes an auth file key and converts it into a new-style credential key
+// in the canonical format, as interpreted by c/image/pkg/docker/config.
+func normalizeAuthFileKey(authFileKey string) string {
+ stripped := strings.TrimPrefix(authFileKey, "http://")
stripped = strings.TrimPrefix(stripped, "https://")
- /// Normalize docker registries
- if strings.HasPrefix(stripped, "index.docker.io/") ||
- strings.HasPrefix(stripped, "registry-1.docker.io/") ||
- strings.HasPrefix(stripped, "docker.io/") {
+ if stripped != authFileKey { // URLs are interpreted to mean complete registries
stripped = strings.SplitN(stripped, "/", 2)[0]
}
- return stripped
+ // Only non-namespaced registry names (or URLs) need to be normalized; repo namespaces
+ // always use the simple format.
+ switch stripped {
+ case "registry-1.docker.io", "index.docker.io":
+ return "docker.io"
+ default:
+ return stripped
+ }
}
// dockerAuthToImageAuth converts a docker auth config to one we're using
@@ -309,28 +305,26 @@ func imageAuthToDockerAuth(authConfig types.DockerAuthConfig) dockerAPITypes.Aut
}
}
-// singleAuthHeader extracts a DockerAuthConfig from the request's header.
+// parseSingleAuthHeader extracts a DockerAuthConfig from an xRegistryAuthHeader value.
// The header content is a single DockerAuthConfig.
-func singleAuthHeader(r *http.Request) (map[string]types.DockerAuthConfig, error) {
- authHeader := r.Header.Get(string(XRegistryAuthHeader))
- authConfig := dockerAPITypes.AuthConfig{}
+func parseSingleAuthHeader(authHeader string) (types.DockerAuthConfig, error) {
// Accept "null" and handle it as empty value for compatibility reason with Docker.
// Some java docker clients pass this value, e.g. this one used in Eclipse.
- if len(authHeader) > 0 && authHeader != "null" {
- authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader))
- if err := json.NewDecoder(authJSON).Decode(&authConfig); err != nil {
- return nil, err
- }
+ if len(authHeader) == 0 || authHeader == "null" {
+ return types.DockerAuthConfig{}, nil
}
- authConfigs := make(map[string]types.DockerAuthConfig)
- authConfigs["0"] = dockerAuthToImageAuth(authConfig)
- return authConfigs, nil
+
+ authConfig := dockerAPITypes.AuthConfig{}
+ authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader))
+ if err := json.NewDecoder(authJSON).Decode(&authConfig); err != nil {
+ return types.DockerAuthConfig{}, err
+ }
+ return dockerAuthToImageAuth(authConfig), nil
}
-// multiAuthHeader extracts a DockerAuthConfig from the request's header.
+// parseMultiAuthHeader extracts a DockerAuthConfig from an xRegistryAuthHeader value.
// The header content is a map[string]DockerAuthConfigs.
-func multiAuthHeader(r *http.Request) (map[string]types.DockerAuthConfig, error) {
- authHeader := r.Header.Get(string(XRegistryAuthHeader))
+func parseMultiAuthHeader(authHeader string) (map[string]types.DockerAuthConfig, error) {
// Accept "null" and handle it as empty value for compatibility reason with Docker.
// Some java docker clients pass this value, e.g. this one used in Eclipse.
if len(authHeader) == 0 || authHeader == "null" {
diff --git a/pkg/auth/auth_test.go b/pkg/auth/auth_test.go
index da2d9a5c5..f7e6e4ef6 100644
--- a/pkg/auth/auth_test.go
+++ b/pkg/auth/auth_test.go
@@ -1,13 +1,302 @@
package auth
import (
+ "encoding/base64"
+ "encoding/json"
"io/ioutil"
+ "net/http"
+ "os"
"testing"
+ "github.com/containers/image/v5/pkg/docker/config"
"github.com/containers/image/v5/types"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
+const largeAuthFile = `{"auths":{
+ "docker.io/vendor": {"auth": "ZG9ja2VyOnZlbmRvcg=="},
+ "https://index.docker.io/v1": {"auth": "ZG9ja2VyOnRvcA=="},
+ "quay.io/libpod": {"auth": "cXVheTpsaWJwb2Q="},
+ "quay.io": {"auth": "cXVheTp0b3A="}
+}}`
+
+// Semantics of largeAuthFile
+var largeAuthFileValues = map[string]types.DockerAuthConfig{
+ "docker.io/vendor": {Username: "docker", Password: "vendor"},
+ "docker.io": {Username: "docker", Password: "top"},
+ "quay.io/libpod": {Username: "quay", Password: "libpod"},
+ "quay.io": {Username: "quay", Password: "top"},
+}
+
+// systemContextForAuthFile returns a types.SystemContext with AuthFilePath pointing
+// to a temporary file with fileContents, or nil if fileContents is empty; and a cleanup
+// function the calle rmust arrange to call.
+func systemContextForAuthFile(t *testing.T, fileContents string) (*types.SystemContext, func()) {
+ if fileContents == "" {
+ return nil, func() {}
+ }
+
+ f, err := ioutil.TempFile("", "auth.json")
+ require.NoError(t, err)
+ path := f.Name()
+ err = ioutil.WriteFile(path, []byte(fileContents), 0700)
+ require.NoError(t, err)
+ return &types.SystemContext{AuthFilePath: path}, func() { os.Remove(path) }
+}
+
+// Test that GetCredentials() correctly parses what MakeXRegistryConfigHeader() produces
+func TestMakeXRegistryConfigHeaderGetCredentialsRoundtrip(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ fileContents string
+ username, password string
+ expectedOverride *types.DockerAuthConfig
+ expectedFileValues map[string]types.DockerAuthConfig
+ }{
+ {
+ name: "no data",
+ fileContents: "",
+ username: "",
+ password: "",
+ expectedOverride: nil,
+ expectedFileValues: nil,
+ },
+ {
+ name: "file data",
+ fileContents: largeAuthFile,
+ username: "",
+ password: "",
+ expectedOverride: nil,
+ expectedFileValues: largeAuthFileValues,
+ },
+ {
+ name: "file data + override",
+ fileContents: largeAuthFile,
+ username: "override-user",
+ password: "override-pass",
+ expectedOverride: &types.DockerAuthConfig{Username: "override-user", Password: "override-pass"},
+ expectedFileValues: largeAuthFileValues,
+ },
+ } {
+ sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
+ defer cleanup()
+ headers, err := MakeXRegistryConfigHeader(sys, tc.username, tc.password)
+ require.NoError(t, err)
+ req, err := http.NewRequest(http.MethodPost, "/", nil)
+ require.NoError(t, err, tc.name)
+ for k, v := range headers {
+ req.Header.Set(k, v)
+ }
+
+ override, resPath, err := GetCredentials(req)
+ require.NoError(t, err, tc.name)
+ defer RemoveAuthfile(resPath)
+ if tc.expectedOverride == nil {
+ assert.Nil(t, override, tc.name)
+ } else {
+ require.NotNil(t, override, tc.name)
+ assert.Equal(t, *tc.expectedOverride, *override, tc.name)
+ }
+ for key, expectedAuth := range tc.expectedFileValues {
+ auth, err := config.GetCredentials(&types.SystemContext{AuthFilePath: resPath}, key)
+ require.NoError(t, err, tc.name)
+ assert.Equal(t, expectedAuth, auth, "%s, key %s", tc.name, key)
+ }
+ }
+}
+
+// Test that GetCredentials() correctly parses what MakeXRegistryAuthHeader() produces
+func TestMakeXRegistryAuthHeaderGetCredentialsRoundtrip(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ fileContents string
+ username, password string
+ expectedOverride *types.DockerAuthConfig
+ expectedFileValues map[string]types.DockerAuthConfig
+ }{
+ {
+ name: "override",
+ fileContents: "",
+ username: "override-user",
+ password: "override-pass",
+ expectedOverride: &types.DockerAuthConfig{Username: "override-user", Password: "override-pass"},
+ expectedFileValues: nil,
+ },
+ {
+ name: "file data",
+ fileContents: largeAuthFile,
+ username: "",
+ password: "",
+ expectedFileValues: largeAuthFileValues,
+ },
+ } {
+ sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
+ defer cleanup()
+ headers, err := MakeXRegistryAuthHeader(sys, tc.username, tc.password)
+ require.NoError(t, err)
+ req, err := http.NewRequest(http.MethodPost, "/", nil)
+ require.NoError(t, err, tc.name)
+ for k, v := range headers {
+ req.Header.Set(k, v)
+ }
+
+ override, resPath, err := GetCredentials(req)
+ require.NoError(t, err, tc.name)
+ defer RemoveAuthfile(resPath)
+ if tc.expectedOverride == nil {
+ assert.Nil(t, override, tc.name)
+ } else {
+ require.NotNil(t, override, tc.name)
+ assert.Equal(t, *tc.expectedOverride, *override, tc.name)
+ }
+ for key, expectedAuth := range tc.expectedFileValues {
+ auth, err := config.GetCredentials(&types.SystemContext{AuthFilePath: resPath}, key)
+ require.NoError(t, err, tc.name)
+ assert.Equal(t, expectedAuth, auth, "%s, key %s", tc.name, key)
+ }
+ }
+}
+
+func TestMakeXRegistryConfigHeader(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ fileContents string
+ username, password string
+ shouldErr bool
+ expectedContents string
+ }{
+ {
+ name: "no data",
+ fileContents: "",
+ username: "",
+ password: "",
+ expectedContents: "",
+ },
+ {
+ name: "invalid JSON",
+ fileContents: "@invalid JSON",
+ username: "",
+ password: "",
+ shouldErr: true,
+ },
+ {
+ name: "file data",
+ fileContents: largeAuthFile,
+ username: "",
+ password: "",
+ expectedContents: `{
+ "docker.io/vendor": {"username": "docker", "password": "vendor"},
+ "docker.io": {"username": "docker", "password": "top"},
+ "quay.io/libpod": {"username": "quay", "password": "libpod"},
+ "quay.io": {"username": "quay", "password": "top"}
+ }`,
+ },
+ {
+ name: "file data + override",
+ fileContents: largeAuthFile,
+ username: "override-user",
+ password: "override-pass",
+ expectedContents: `{
+ "docker.io/vendor": {"username": "docker", "password": "vendor"},
+ "docker.io": {"username": "docker", "password": "top"},
+ "quay.io/libpod": {"username": "quay", "password": "libpod"},
+ "quay.io": {"username": "quay", "password": "top"},
+ "": {"username": "override-user", "password": "override-pass"}
+ }`,
+ },
+ } {
+ sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
+ defer cleanup()
+ res, err := MakeXRegistryConfigHeader(sys, tc.username, tc.password)
+ if tc.shouldErr {
+ assert.Error(t, err, tc.name)
+ } else {
+ require.NoError(t, err, tc.name)
+ if tc.expectedContents == "" {
+ assert.Empty(t, res, tc.name)
+ } else {
+ require.Len(t, res, 1, tc.name)
+ header, ok := res[xRegistryConfigHeader]
+ require.True(t, ok, tc.name)
+ decodedHeader, err := base64.URLEncoding.DecodeString(header)
+ require.NoError(t, err, tc.name)
+ // Don't test for a specific JSON representation, just for the expected contents.
+ expected := map[string]interface{}{}
+ actual := map[string]interface{}{}
+ err = json.Unmarshal([]byte(tc.expectedContents), &expected)
+ require.NoError(t, err, tc.name)
+ err = json.Unmarshal(decodedHeader, &actual)
+ require.NoError(t, err, tc.name)
+ assert.Equal(t, expected, actual, tc.name)
+ }
+ }
+ }
+}
+
+func TestMakeXRegistryAuthHeader(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ fileContents string
+ username, password string
+ shouldErr bool
+ expectedContents string
+ }{
+ {
+ name: "override",
+ fileContents: "",
+ username: "override-user",
+ password: "override-pass",
+ expectedContents: `{"username": "override-user", "password": "override-pass"}`,
+ },
+ {
+ name: "invalid JSON",
+ fileContents: "@invalid JSON",
+ username: "",
+ password: "",
+ shouldErr: true,
+ },
+ {
+ name: "file data",
+ fileContents: largeAuthFile,
+ username: "",
+ password: "",
+ expectedContents: `{
+ "docker.io/vendor": {"username": "docker", "password": "vendor"},
+ "docker.io": {"username": "docker", "password": "top"},
+ "quay.io/libpod": {"username": "quay", "password": "libpod"},
+ "quay.io": {"username": "quay", "password": "top"}
+ }`,
+ },
+ } {
+ sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
+ defer cleanup()
+ res, err := MakeXRegistryAuthHeader(sys, tc.username, tc.password)
+ if tc.shouldErr {
+ assert.Error(t, err, tc.name)
+ } else {
+ require.NoError(t, err, tc.name)
+ if tc.expectedContents == "" {
+ assert.Empty(t, res, tc.name)
+ } else {
+ require.Len(t, res, 1, tc.name)
+ header, ok := res[xRegistryAuthHeader]
+ require.True(t, ok, tc.name)
+ decodedHeader, err := base64.URLEncoding.DecodeString(header)
+ require.NoError(t, err, tc.name)
+ // Don't test for a specific JSON representation, just for the expected contents.
+ expected := map[string]interface{}{}
+ actual := map[string]interface{}{}
+ err = json.Unmarshal([]byte(tc.expectedContents), &expected)
+ require.NoError(t, err, tc.name)
+ err = json.Unmarshal(decodedHeader, &actual)
+ require.NoError(t, err, tc.name)
+ assert.Equal(t, expected, actual, tc.name)
+ }
+ }
+ }
+}
+
func TestAuthConfigsToAuthFile(t *testing.T) {
for _, tc := range []struct {
name string
@@ -22,28 +311,28 @@ func TestAuthConfigsToAuthFile(t *testing.T) {
expectedContains: "{}",
},
{
- name: "registry with prefix",
+ name: "registry with a namespace prefix",
server: "my-registry.local/username",
shouldErr: false,
expectedContains: `"my-registry.local/username":`,
},
{
- name: "normalize https:// prefix",
+ name: "URLs are interpreted as full registries",
server: "http://my-registry.local/username",
shouldErr: false,
- expectedContains: `"my-registry.local/username":`,
+ expectedContains: `"my-registry.local":`,
},
{
- name: "normalize docker registry with https prefix",
+ name: "the old-style docker registry URL is normalized",
server: "http://index.docker.io/v1/",
shouldErr: false,
- expectedContains: `"index.docker.io":`,
+ expectedContains: `"docker.io":`,
},
{
- name: "normalize docker registry without https prefix",
- server: "docker.io/v2/",
+ name: "docker.io vendor namespace",
+ server: "docker.io/vendor",
shouldErr: false,
- expectedContains: `"docker.io":`,
+ expectedContains: `"docker.io/vendor":`,
},
} {
configs := map[string]types.DockerAuthConfig{}
@@ -54,13 +343,79 @@ func TestAuthConfigsToAuthFile(t *testing.T) {
filePath, err := authConfigsToAuthFile(configs)
if tc.shouldErr {
- assert.NotNil(t, err)
+ assert.Error(t, err)
assert.Empty(t, filePath)
} else {
- assert.Nil(t, err)
+ assert.NoError(t, err)
content, err := ioutil.ReadFile(filePath)
- assert.Nil(t, err)
+ require.NoError(t, err)
assert.Contains(t, string(content), tc.expectedContains)
+ os.Remove(filePath)
+ }
+ }
+}
+
+func TestParseSingleAuthHeader(t *testing.T) {
+ for _, tc := range []struct {
+ input string
+ shouldErr bool
+ expected types.DockerAuthConfig
+ }{
+ {
+ input: "", // An empty (or missing) header
+ expected: types.DockerAuthConfig{},
+ },
+ {
+ input: "null",
+ expected: types.DockerAuthConfig{},
+ },
+ // Invalid JSON
+ {input: "@", shouldErr: true},
+ // Success
+ {
+ input: base64.URLEncoding.EncodeToString([]byte(`{"username":"u1","password":"p1"}`)),
+ expected: types.DockerAuthConfig{Username: "u1", Password: "p1"},
+ },
+ } {
+ res, err := parseSingleAuthHeader(tc.input)
+ if tc.shouldErr {
+ assert.Error(t, err, tc.input)
+ } else {
+ require.NoError(t, err, tc.input)
+ assert.Equal(t, tc.expected, res, tc.input)
+ }
+ }
+}
+
+func TestParseMultiAuthHeader(t *testing.T) {
+ for _, tc := range []struct {
+ input string
+ shouldErr bool
+ expected map[string]types.DockerAuthConfig
+ }{
+ // Empty header
+ {input: "", expected: nil},
+ // "null"
+ {input: "null", expected: nil},
+ // Invalid JSON
+ {input: "@", shouldErr: true},
+ // Success
+ {
+ input: base64.URLEncoding.EncodeToString([]byte(
+ `{"https://index.docker.io/v1/":{"username":"u1","password":"p1"},` +
+ `"quay.io/libpod":{"username":"u2","password":"p2"}}`)),
+ expected: map[string]types.DockerAuthConfig{
+ "https://index.docker.io/v1/": {Username: "u1", Password: "p1"},
+ "quay.io/libpod": {Username: "u2", Password: "p2"},
+ },
+ },
+ } {
+ res, err := parseMultiAuthHeader(tc.input)
+ if tc.shouldErr {
+ assert.Error(t, err, tc.input)
+ } else {
+ require.NoError(t, err, tc.input)
+ assert.Equal(t, tc.expected, res, tc.input)
}
}
}
diff --git a/pkg/bindings/images/build.go b/pkg/bindings/images/build.go
index be6e5ab55..7bca43132 100644
--- a/pkg/bindings/images/build.go
+++ b/pkg/bindings/images/build.go
@@ -293,14 +293,10 @@ func Build(ctx context.Context, containerFiles []string, options entities.BuildO
headers map[string]string
err error
)
- if options.SystemContext == nil {
- headers, err = auth.Header(options.SystemContext, auth.XRegistryConfigHeader, "", "", "")
+ if options.SystemContext != nil && options.SystemContext.DockerAuthConfig != nil {
+ headers, err = auth.MakeXRegistryAuthHeader(options.SystemContext, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password)
} else {
- if options.SystemContext.DockerAuthConfig != nil {
- headers, err = auth.Header(options.SystemContext, auth.XRegistryAuthHeader, options.SystemContext.AuthFilePath, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password)
- } else {
- headers, err = auth.Header(options.SystemContext, auth.XRegistryConfigHeader, options.SystemContext.AuthFilePath, "", "")
- }
+ headers, err = auth.MakeXRegistryConfigHeader(options.SystemContext, "", "")
}
if err != nil {
return nil, err
diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go
index dfb500772..152ff0cde 100644
--- a/pkg/bindings/images/images.go
+++ b/pkg/bindings/images/images.go
@@ -8,6 +8,7 @@ import (
"net/url"
"strconv"
+ imageTypes "github.com/containers/image/v5/types"
"github.com/containers/podman/v3/pkg/api/handlers/types"
"github.com/containers/podman/v3/pkg/auth"
"github.com/containers/podman/v3/pkg/bindings"
@@ -280,7 +281,7 @@ func Push(ctx context.Context, source string, destination string, options *PushO
return err
}
// TODO: have a global system context we can pass around (1st argument)
- header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.GetAuthfile(), options.GetUsername(), options.GetPassword())
+ header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword())
if err != nil {
return err
}
@@ -329,7 +330,7 @@ func Search(ctx context.Context, term string, options *SearchOptions) ([]entitie
}
// TODO: have a global system context we can pass around (1st argument)
- header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.GetAuthfile(), "", "")
+ header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, "", "")
if err != nil {
return nil, err
}
diff --git a/pkg/bindings/images/pull.go b/pkg/bindings/images/pull.go
index be21aa593..ac583973f 100644
--- a/pkg/bindings/images/pull.go
+++ b/pkg/bindings/images/pull.go
@@ -10,6 +10,7 @@ import (
"os"
"strconv"
+ "github.com/containers/image/v5/types"
"github.com/containers/podman/v3/pkg/auth"
"github.com/containers/podman/v3/pkg/bindings"
"github.com/containers/podman/v3/pkg/domain/entities"
@@ -42,7 +43,7 @@ func Pull(ctx context.Context, rawImage string, options *PullOptions) ([]string,
}
// TODO: have a global system context we can pass around (1st argument)
- header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.GetAuthfile(), options.GetUsername(), options.GetPassword())
+ header, err := auth.MakeXRegistryAuthHeader(&types.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword())
if err != nil {
return nil, err
}
diff --git a/pkg/bindings/play/play.go b/pkg/bindings/play/play.go
index 2cd7c3997..111a25cac 100644
--- a/pkg/bindings/play/play.go
+++ b/pkg/bindings/play/play.go
@@ -6,6 +6,7 @@ import (
"os"
"strconv"
+ "github.com/containers/image/v5/types"
"github.com/containers/podman/v3/pkg/auth"
"github.com/containers/podman/v3/pkg/bindings"
"github.com/containers/podman/v3/pkg/domain/entities"
@@ -40,7 +41,7 @@ func Kube(ctx context.Context, path string, options *KubeOptions) (*entities.Pla
}
// TODO: have a global system context we can pass around (1st argument)
- header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.GetAuthfile(), options.GetUsername(), options.GetPassword())
+ header, err := auth.MakeXRegistryAuthHeader(&types.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword())
if err != nil {
return nil, err
}
diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go
index d72f64b5e..bec505163 100644
--- a/pkg/domain/entities/engine_image.go
+++ b/pkg/domain/entities/engine_image.go
@@ -27,7 +27,7 @@ type ImageEngine interface {
ShowTrust(ctx context.Context, args []string, options ShowTrustOptions) (*ShowTrustReport, error)
Shutdown(ctx context.Context)
Tag(ctx context.Context, nameOrID string, tags []string, options ImageTagOptions) error
- Transfer(ctx context.Context, scpOpts ImageScpOptions) error
+ Transfer(ctx context.Context, source ImageScpOptions, dest ImageScpOptions, parentFlags []string) error
Tree(ctx context.Context, nameOrID string, options ImageTreeOptions) (*ImageTreeReport, error)
Unmount(ctx context.Context, images []string, options ImageUnmountOptions) ([]*ImageUnmountReport, error)
Untag(ctx context.Context, nameOrID string, tags []string, options ImageUntagOptions) error
diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go
index 8b0fd2b85..62e7f67c8 100644
--- a/pkg/domain/entities/images.go
+++ b/pkg/domain/entities/images.go
@@ -311,30 +311,28 @@ type ImageSaveOptions struct {
Quiet bool
}
-// ImageScpOptions provide options for securely copying images to podman remote
+// ImageScpOptions provide options for securely copying images to and from a remote host
type ImageScpOptions struct {
- // SoureImageName is the image the user is providing to load on a remote machine
- SourceImageName string
- // Tag allows for a new image to be created under the given name
- Tag string
- // ToRemote specifies that we are loading to the remote host
- ToRemote bool
- // FromRemote specifies that we are loading from the remote host
- FromRemote bool
+ // Remote determines if this entity is operating on a remote machine
+ Remote bool `json:"remote,omitempty"`
+ // File is the input/output file for the save and load Operation
+ File string `json:"file,omitempty"`
+ // Quiet Determines if the save and load operation will be done quietly
+ Quiet bool `json:"quiet,omitempty"`
+ // Image is the image the user is providing to save and load
+ Image string `json:"image,omitempty"`
+ // User is used in conjunction with Transfer to determine if a valid user was given to save from/load into
+ User string `json:"user,omitempty"`
+}
+
+// ImageScpConnections provides the ssh related information used in remote image transfer
+type ImageScpConnections struct {
// Connections holds the raw string values for connections (ssh or unix)
Connections []string
// URI contains the ssh connection URLs to be used by the client
URI []*url.URL
- // Iden contains ssh identity keys to be used by the client
- Iden []string
- // Save Options used for first half of the scp operation
- Save ImageSaveOptions
- // Load options used for the second half of the scp operation
- Load ImageLoadOptions
- // Rootless determines whether we are loading locally from root storage to rootless storage
- Rootless bool
- // User is used in conjunction with Rootless to determine which user to use to obtain the uid
- User string
+ // Identities contains ssh identity keys to be used by the client
+ Identities []string
}
// ImageTreeOptions provides options for ImageEngine.Tree()
diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go
index f9850e5a8..1b5a1be51 100644
--- a/pkg/domain/entities/pods.go
+++ b/pkg/domain/entities/pods.go
@@ -138,6 +138,7 @@ type PodCreateOptions struct {
Userns specgen.Namespace `json:"-"`
Volume []string `json:"volume,omitempty"`
VolumesFrom []string `json:"volumes_from,omitempty"`
+ SecurityOpt []string `json:"security_opt,omitempty"`
}
// PodLogsOptions describes the options to extract pod logs.
@@ -230,7 +231,7 @@ type ContainerCreateOptions struct {
Rm bool
RootFS bool
Secrets []string
- SecurityOpt []string
+ SecurityOpt []string `json:"security_opt,omitempty"`
SdNotifyMode string
ShmSize string
SignaturePolicy string
@@ -312,6 +313,7 @@ func ToPodSpecGen(s specgen.PodSpecGenerator, p *PodCreateOptions) (*specgen.Pod
s.Hostname = p.Hostname
s.Labels = p.Labels
s.Devices = p.Devices
+ s.SecurityOpt = p.SecurityOpt
s.NoInfra = !p.Infra
if p.InfraCommand != nil && len(*p.InfraCommand) > 0 {
s.InfraCommand = strings.Split(*p.InfraCommand, " ")
diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go
index 4346182d6..84c83ea8e 100644
--- a/pkg/domain/infra/abi/images.go
+++ b/pkg/domain/infra/abi/images.go
@@ -28,6 +28,7 @@ import (
domainUtils "github.com/containers/podman/v3/pkg/domain/utils"
"github.com/containers/podman/v3/pkg/errorhandling"
"github.com/containers/podman/v3/pkg/rootless"
+ "github.com/containers/podman/v3/utils"
"github.com/containers/storage"
dockerRef "github.com/docker/distribution/reference"
"github.com/opencontainers/go-digest"
@@ -351,65 +352,19 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
return pushError
}
-// Transfer moves images from root to rootless storage so the user specified in the scp call can access and use the image modified by root
-func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error {
- if scpOpts.User == "" {
+// Transfer moves images between root and rootless storage so the user specified in the scp call can access and use the image modified by root
+func (ir *ImageEngine) Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error {
+ if source.User == "" {
return errors.Wrapf(define.ErrInvalidArg, "you must define a user when transferring from root to rootless storage")
}
- var u *user.User
- scpOpts.User = strings.Split(scpOpts.User, ":")[0] // split in case provided with uid:gid
- _, err := strconv.Atoi(scpOpts.User)
- if err != nil {
- u, err = user.Lookup(scpOpts.User)
- if err != nil {
- return err
- }
- } else {
- u, err = user.LookupId(scpOpts.User)
- if err != nil {
- return err
- }
- }
- uid, err := strconv.Atoi(u.Uid)
- if err != nil {
- return err
- }
- gid, err := strconv.Atoi(u.Gid)
- if err != nil {
- return err
- }
- err = os.Chown(scpOpts.Save.Output, uid, gid) // chown the output because was created by root so we need to give th euser read access
- if err != nil {
- return err
- }
-
podman, err := os.Executable()
if err != nil {
return err
}
- machinectl, err := exec.LookPath("machinectl")
- if err != nil {
- logrus.Warn("defaulting to su since machinectl is not available, su will fail if no user session is available")
- cmd := exec.Command("su", "-l", u.Username, "--command", podman+" --log-level="+logrus.GetLevel().String()+" --cgroup-manager=cgroupfs load --input="+scpOpts.Save.Output) // load the new image to the rootless storage
- cmd.Stderr = os.Stderr
- cmd.Stdout = os.Stdout
- logrus.Debug("Executing load command su")
- err = cmd.Run()
- if err != nil {
- return err
- }
- } else {
- cmd := exec.Command(machinectl, "shell", "-q", u.Username+"@.host", podman, "--log-level="+logrus.GetLevel().String(), "--cgroup-manager=cgroupfs", "load", "--input", scpOpts.Save.Output) // load the new image to the rootless storage
- cmd.Stderr = os.Stderr
- cmd.Stdout = os.Stdout
- logrus.Debug("Executing load command machinectl")
- err = cmd.Run()
- if err != nil {
- return err
- }
+ 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 transferRootless(source, dest, podman, parentFlags)
}
-
- return nil
+ return transferRootful(source, dest, podman, parentFlags)
}
func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error {
@@ -786,3 +741,123 @@ func putSignature(manifestBlob []byte, mech signature.SigningMechanism, sigStore
}
return nil
}
+
+// TransferRootless creates new podman processes using exec.Command and sudo, transferring images between the given source and destination users
+func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error {
+ var cmdSave *exec.Cmd
+ saveCommand := parentFlags
+ saveCommand = append(saveCommand, []string{"save", "--output", source.File, source.Image}...)
+
+ loadCommand := parentFlags
+ loadCommand = append(loadCommand, []string{"load", "--input", dest.File}...)
+
+ if source.User == "root" {
+ cmdSave = exec.Command("sudo", podman)
+ } else {
+ cmdSave = exec.Command(podman)
+ }
+ cmdSave = utils.CreateSCPCommand(cmdSave, saveCommand)
+ logrus.Debug("Executing save command")
+ err := cmdSave.Run()
+ if err != nil {
+ return err
+ }
+
+ var cmdLoad *exec.Cmd
+ if source.User != "root" {
+ cmdLoad = exec.Command("sudo", podman)
+ } else {
+ cmdLoad = exec.Command(podman)
+ }
+ cmdLoad = utils.CreateSCPCommand(cmdLoad, loadCommand)
+ logrus.Debug("Executing load command")
+ err = cmdLoad.Run()
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// TransferRootless creates new podman processes using exec.Command and su/machinectl, transferring images between the given source and destination users
+func transferRootful(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error {
+ basicCommand := []string{podman}
+ basicCommand = append(basicCommand, parentFlags...)
+ saveCommand := append(basicCommand, []string{"save", "--output", source.File, source.Image}...)
+ loadCommand := append(basicCommand, []string{"load", "--input", dest.File}...)
+ save := []string{strings.Join(saveCommand, " ")}
+ load := []string{strings.Join(loadCommand, " ")}
+
+ // if executing using sudo or transferring between two users, the TransferRootless approach will not work, default to using machinectl or su as necessary.
+ // the approach using sudo is preferable and more straightforward. There is no reason for using sudo in these situations
+ // since the feature is meant to transfer from root to rootless an vice versa without explicit sudo evocaiton.
+ var uSave *user.User
+ var uLoad *user.User
+ var err error
+ source.User = strings.Split(source.User, ":")[0] // split in case provided with uid:gid
+ dest.User = strings.Split(dest.User, ":")[0]
+ uSave, err = lookupUser(source.User)
+ if err != nil {
+ return err
+ }
+ switch {
+ case dest.User != "": // if we are given a destination user, check that first
+ uLoad, err = lookupUser(dest.User)
+ if err != nil {
+ return err
+ }
+ case uSave.Name != "root": // else if we have no destination user, and source is not root that means we should be root
+ uLoad, err = user.LookupId("0")
+ if err != nil {
+ return err
+ }
+ default: // else if we have no dest user, and source user IS root, we want to be the default user.
+ uString := os.Getenv("SUDO_USER")
+ if uString == "" {
+ return errors.New("$SUDO_USER must be defined to find the default rootless user")
+ }
+ uLoad, err = user.Lookup(uString)
+ if err != nil {
+ return err
+ }
+ }
+ machinectl, err := exec.LookPath("machinectl")
+ if err != nil {
+ logrus.Warn("defaulting to su since machinectl is not available, su will fail if no user session is available")
+ err = execSu(uSave, save)
+ if err != nil {
+ return err
+ }
+ return execSu(uLoad, load)
+ }
+ err = execMachine(uSave, saveCommand, machinectl)
+ if err != nil {
+ return err
+ }
+ return execMachine(uLoad, loadCommand, machinectl)
+}
+
+func lookupUser(u string) (*user.User, error) {
+ if u, err := user.LookupId(u); err == nil {
+ return u, nil
+ }
+ return user.Lookup(u)
+}
+
+func execSu(execUser *user.User, command []string) error {
+ cmd := exec.Command("su", "-l", execUser.Username, "--command")
+ cmd = utils.CreateSCPCommand(cmd, command)
+ logrus.Debug("Executing command su")
+ return cmd.Run()
+}
+
+func execMachine(execUser *user.User, command []string, machinectl string) error {
+ var cmd *exec.Cmd
+ if execUser.Uid == "0" {
+ cmd = exec.Command("sudo", machinectl, "shell", "-q", execUser.Username+"@.host")
+ } else {
+ cmd = exec.Command(machinectl, "shell", "-q", execUser.Username+"@.host")
+ }
+ cmd = utils.CreateSCPCommand(cmd, command)
+ logrus.Debug("Executing command machinectl")
+ return cmd.Run()
+}
diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go
index 2feb9d7ad..f26a489e6 100644
--- a/pkg/domain/infra/tunnel/images.go
+++ b/pkg/domain/infra/tunnel/images.go
@@ -123,7 +123,7 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, opts entities.
return &entities.ImagePullReport{Images: pulledImages}, nil
}
-func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error {
+func (ir *ImageEngine) Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error {
return errors.Wrapf(define.ErrNotImplemented, "cannot use the remote client to transfer images between root and rootless storage")
}
diff --git a/pkg/machine/ignition.go b/pkg/machine/ignition.go
index 139318977..84d3be296 100644
--- a/pkg/machine/ignition.go
+++ b/pkg/machine/ignition.go
@@ -7,7 +7,10 @@ import (
"fmt"
"io/ioutil"
"net/url"
+ "os"
"path/filepath"
+
+ "github.com/sirupsen/logrus"
)
/*
@@ -355,6 +358,56 @@ machine_enabled=true
},
})
+ // get certs for current user
+ userHome, err := os.UserHomeDir()
+ if err != nil {
+ logrus.Warnf("Unable to copy certs via ignition %s", err.Error())
+ return files
+ }
+
+ certFiles := getCerts(filepath.Join(userHome, ".config/containers/certs.d"))
+ files = append(files, certFiles...)
+
+ certFiles = getCerts(filepath.Join(userHome, ".config/docker/certs.d"))
+ files = append(files, certFiles...)
+
+ return files
+}
+
+func getCerts(certsDir string) []File {
+ var (
+ files []File
+ )
+
+ certs, err := ioutil.ReadDir(certsDir)
+ if err == nil {
+ for _, cert := range certs {
+ b, err := ioutil.ReadFile(filepath.Join(certsDir, cert.Name()))
+ if err != nil {
+ logrus.Warnf("Unable to read cert file %s", err.Error())
+ continue
+ }
+ files = append(files, File{
+ Node: Node{
+ Group: getNodeGrp("root"),
+ Path: filepath.Join("/etc/containers/certs.d/", cert.Name()),
+ User: getNodeUsr("root"),
+ },
+ FileEmbedded1: FileEmbedded1{
+ Append: nil,
+ Contents: Resource{
+ Source: encodeDataURLPtr(string(b)),
+ },
+ Mode: intToPtr(0644),
+ },
+ })
+ }
+ } else {
+ if !os.IsNotExist(err) {
+ logrus.Warnf("Unable to copy certs via ignition, error while reading certs from %s: %s", certsDir, err.Error())
+ }
+ }
+
return files
}
diff --git a/pkg/rootless/rootless_linux.c b/pkg/rootless/rootless_linux.c
index 92f331ce4..94bd40f86 100644
--- a/pkg/rootless/rootless_linux.c
+++ b/pkg/rootless/rootless_linux.c
@@ -244,7 +244,7 @@ can_use_shortcut ()
if (argv[argc+1] != NULL && (strcmp (argv[argc], "container") == 0 ||
strcmp (argv[argc], "image") == 0) &&
- strcmp (argv[argc+1], "mount") == 0)
+ (strcmp (argv[argc+1], "mount") == 0 || strcmp (argv[argc+1], "scp") == 0))
{
ret = false;
break;
diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go
index 7ab9d1b29..7d792b3b1 100644
--- a/pkg/specgen/generate/container_create.go
+++ b/pkg/specgen/generate/container_create.go
@@ -2,13 +2,14 @@ package generate
import (
"context"
- "fmt"
+ "encoding/json"
"path/filepath"
"strings"
cdi "github.com/container-orchestrated-devices/container-device-interface/pkg"
"github.com/containers/common/libimage"
"github.com/containers/podman/v3/libpod"
+ "github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/pkg/namespaces"
"github.com/containers/podman/v3/pkg/specgen"
"github.com/containers/podman/v3/pkg/util"
@@ -29,43 +30,30 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener
// If joining a pod, retrieve the pod for use, and its infra container
var pod *libpod.Pod
- var infraConfig *libpod.ContainerConfig
+ var infra *libpod.Container
if s.Pod != "" {
pod, err = rt.LookupPod(s.Pod)
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "error retrieving pod %s", s.Pod)
}
if pod.HasInfraContainer() {
- infra, err := pod.InfraContainer()
+ infra, err = pod.InfraContainer()
if err != nil {
return nil, nil, nil, err
}
- infraConfig = infra.Config()
}
}
- if infraConfig != nil && (len(infraConfig.NamedVolumes) > 0 || len(infraConfig.UserVolumes) > 0 || len(infraConfig.ImageVolumes) > 0 || len(infraConfig.OverlayVolumes) > 0) {
- s.VolumesFrom = append(s.VolumesFrom, infraConfig.ID)
- }
-
- if infraConfig != nil && len(infraConfig.Spec.Linux.Devices) > 0 {
- s.DevicesFrom = append(s.DevicesFrom, infraConfig.ID)
- }
- if infraConfig != nil && infraConfig.Spec.Linux.Resources != nil && infraConfig.Spec.Linux.Resources.BlockIO != nil && len(infraConfig.Spec.Linux.Resources.BlockIO.ThrottleReadBpsDevice) > 0 {
- tempDev := make(map[string]spec.LinuxThrottleDevice)
- for _, val := range infraConfig.Spec.Linux.Resources.BlockIO.ThrottleReadBpsDevice {
- nodes, err := util.FindDeviceNodes()
- if err != nil {
- return nil, nil, nil, err
- }
- key := fmt.Sprintf("%d:%d", val.Major, val.Minor)
- tempDev[nodes[key]] = spec.LinuxThrottleDevice{Rate: uint64(val.Rate)}
- }
- for i, dev := range s.ThrottleReadBpsDevice {
- tempDev[i] = dev
+ options := []libpod.CtrCreateOption{}
+ compatibleOptions := &libpod.InfraInherit{}
+ var infraSpec *spec.Spec
+ if infra != nil {
+ options, infraSpec, compatibleOptions, err = Inherit(*infra)
+ if err != nil {
+ return nil, nil, nil, err
}
- s.ThrottleReadBpsDevice = tempDev
}
+
if err := FinishThrottleDevices(s); err != nil {
return nil, nil, nil, err
}
@@ -119,8 +107,6 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener
s.CgroupNS = defaultNS
}
- options := []libpod.CtrCreateOption{}
-
if s.ContainerCreateCommand != nil {
options = append(options, libpod.WithCreateCommand(s.ContainerCreateCommand))
}
@@ -165,7 +151,8 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener
return nil, nil, nil, err
}
- opts, err := createContainerOptions(ctx, rt, s, pod, finalVolumes, finalOverlays, imageData, command)
+ infraVolumes := (len(compatibleOptions.InfraVolumes) > 0 || len(compatibleOptions.InfraUserVolumes) > 0 || len(compatibleOptions.InfraImageVolumes) > 0)
+ opts, err := createContainerOptions(ctx, rt, s, pod, finalVolumes, finalOverlays, imageData, command, infraVolumes, *compatibleOptions)
if err != nil {
return nil, nil, nil, err
}
@@ -178,27 +165,29 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener
logrus.Debugf("setting container name %s", s.Name)
options = append(options, libpod.WithName(s.Name))
}
- if len(s.DevicesFrom) > 0 {
- for _, dev := range s.DevicesFrom {
- ctr, err := rt.GetContainer(dev)
- if err != nil {
- return nil, nil, nil, err
- }
- devices := ctr.DeviceHostSrc()
- s.Devices = append(s.Devices, devices...)
- }
- }
if len(s.Devices) > 0 {
- opts = extractCDIDevices(s)
+ opts = ExtractCDIDevices(s)
options = append(options, opts...)
}
- runtimeSpec, err := SpecGenToOCI(ctx, s, rt, rtc, newImage, finalMounts, pod, command)
+ runtimeSpec, err := SpecGenToOCI(ctx, s, rt, rtc, newImage, finalMounts, pod, command, compatibleOptions)
if err != nil {
return nil, nil, nil, err
}
if len(s.HostDeviceList) > 0 {
options = append(options, libpod.WithHostDevice(s.HostDeviceList))
}
+ if infraSpec != nil && infraSpec.Linux != nil { // if we are inheriting Linux info from a pod...
+ // Pass Security annotations
+ if len(infraSpec.Annotations[define.InspectAnnotationLabel]) > 0 && len(runtimeSpec.Annotations[define.InspectAnnotationLabel]) == 0 {
+ runtimeSpec.Annotations[define.InspectAnnotationLabel] = infraSpec.Annotations[define.InspectAnnotationLabel]
+ }
+ if len(infraSpec.Annotations[define.InspectAnnotationSeccomp]) > 0 && len(runtimeSpec.Annotations[define.InspectAnnotationSeccomp]) == 0 {
+ runtimeSpec.Annotations[define.InspectAnnotationSeccomp] = infraSpec.Annotations[define.InspectAnnotationSeccomp]
+ }
+ if len(infraSpec.Annotations[define.InspectAnnotationApparmor]) > 0 && len(runtimeSpec.Annotations[define.InspectAnnotationApparmor]) == 0 {
+ runtimeSpec.Annotations[define.InspectAnnotationApparmor] = infraSpec.Annotations[define.InspectAnnotationApparmor]
+ }
+ }
return runtimeSpec, s, options, err
}
func ExecuteCreate(ctx context.Context, rt *libpod.Runtime, runtimeSpec *spec.Spec, s *specgen.SpecGenerator, infra bool, options ...libpod.CtrCreateOption) (*libpod.Container, error) {
@@ -210,7 +199,7 @@ func ExecuteCreate(ctx context.Context, rt *libpod.Runtime, runtimeSpec *spec.Sp
return ctr, rt.PrepareVolumeOnCreateContainer(ctx, ctr)
}
-func extractCDIDevices(s *specgen.SpecGenerator) []libpod.CtrCreateOption {
+func ExtractCDIDevices(s *specgen.SpecGenerator) []libpod.CtrCreateOption {
devs := make([]spec.LinuxDevice, 0, len(s.Devices))
var cdiDevs []string
var options []libpod.CtrCreateOption
@@ -224,19 +213,16 @@ func extractCDIDevices(s *specgen.SpecGenerator) []libpod.CtrCreateOption {
cdiDevs = append(cdiDevs, device.Path)
continue
}
-
devs = append(devs, device)
}
-
s.Devices = devs
if len(cdiDevs) > 0 {
options = append(options, libpod.WithCDI(cdiDevs))
}
-
return options
}
-func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod, volumes []*specgen.NamedVolume, overlays []*specgen.OverlayVolume, imageData *libimage.ImageData, command []string) ([]libpod.CtrCreateOption, error) {
+func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod, volumes []*specgen.NamedVolume, overlays []*specgen.OverlayVolume, imageData *libimage.ImageData, command []string, infraVolumes bool, compatibleOptions libpod.InfraInherit) ([]libpod.CtrCreateOption, error) {
var options []libpod.CtrCreateOption
var err error
@@ -317,7 +303,10 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.
for _, imageVolume := range s.ImageVolumes {
destinations = append(destinations, imageVolume.Destination)
}
- options = append(options, libpod.WithUserVolumes(destinations))
+
+ if len(destinations) > 0 || !infraVolumes {
+ options = append(options, libpod.WithUserVolumes(destinations))
+ }
if len(volumes) != 0 {
var vols []*libpod.ContainerNamedVolume
@@ -405,7 +394,7 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.
if len(s.SelinuxOpts) > 0 {
options = append(options, libpod.WithSecLabels(s.SelinuxOpts))
} else {
- if pod != nil {
+ if pod != nil && len(compatibleOptions.InfraLabels) == 0 {
// duplicate the security options from the pod
processLabel, err := pod.ProcessLabel()
if err != nil {
@@ -498,3 +487,33 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.
return options, nil
}
+
+func Inherit(infra libpod.Container) (opts []libpod.CtrCreateOption, infraS *spec.Spec, compat *libpod.InfraInherit, err error) {
+ options := []libpod.CtrCreateOption{}
+ compatibleOptions := &libpod.InfraInherit{}
+ infraConf := infra.Config()
+ infraSpec := infraConf.Spec
+
+ config, err := json.Marshal(infraConf)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ err = json.Unmarshal(config, compatibleOptions)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ if infraSpec.Linux != nil && infraSpec.Linux.Resources != nil {
+ resources, err := json.Marshal(infraSpec.Linux.Resources)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ err = json.Unmarshal(resources, &compatibleOptions.InfraResources)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ }
+ if compatibleOptions != nil {
+ options = append(options, libpod.WithInfraConfig(*compatibleOptions))
+ }
+ return options, infraSpec, compatibleOptions, nil
+}
diff --git a/pkg/specgen/generate/oci.go b/pkg/specgen/generate/oci.go
index efac53104..ee3a990fc 100644
--- a/pkg/specgen/generate/oci.go
+++ b/pkg/specgen/generate/oci.go
@@ -2,6 +2,7 @@ package generate
import (
"context"
+ "encoding/json"
"path"
"strings"
@@ -174,7 +175,7 @@ func getCGroupPermissons(unmask []string) string {
}
// SpecGenToOCI returns the base configuration for the container.
-func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runtime, rtc *config.Config, newImage *libimage.Image, mounts []spec.Mount, pod *libpod.Pod, finalCmd []string) (*spec.Spec, error) {
+func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runtime, rtc *config.Config, newImage *libimage.Image, mounts []spec.Mount, pod *libpod.Pod, finalCmd []string, compatibleOptions *libpod.InfraInherit) (*spec.Spec, error) {
cgroupPerm := getCGroupPermissons(s.Unmask)
g, err := generate.New("linux")
@@ -299,9 +300,32 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt
g.AddAnnotation(key, val)
}
- g.Config.Linux.Resources = s.ResourceLimits
+ if compatibleOptions.InfraResources == nil && s.ResourceLimits != nil {
+ g.Config.Linux.Resources = s.ResourceLimits
+ } else if s.ResourceLimits != nil { // if we have predefined resource limits we need to make sure we keep the infra and container limits
+ originalResources, err := json.Marshal(s.ResourceLimits)
+ if err != nil {
+ return nil, err
+ }
+ infraResources, err := json.Marshal(compatibleOptions.InfraResources)
+ if err != nil {
+ return nil, err
+ }
+ err = json.Unmarshal(infraResources, s.ResourceLimits) // put infra's resource limits in the container
+ if err != nil {
+ return nil, err
+ }
+ err = json.Unmarshal(originalResources, s.ResourceLimits) // make sure we did not override anything
+ if err != nil {
+ return nil, err
+ }
+ g.Config.Linux.Resources = s.ResourceLimits
+ } else {
+ g.Config.Linux.Resources = compatibleOptions.InfraResources
+ }
// Devices
+ var userDevices []spec.LinuxDevice
if s.Privileged {
// If privileged, we need to add all the host devices to the
// spec. We do not add the user provided ones because we are
@@ -316,14 +340,19 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt
return nil, err
}
}
+ if len(compatibleOptions.InfraDevices) > 0 && len(s.Devices) == 0 {
+ userDevices = compatibleOptions.InfraDevices
+ } else {
+ userDevices = s.Devices
+ }
// add default devices specified by caller
- for _, device := range s.Devices {
+ for _, device := range userDevices {
if err = DevicesFromPath(&g, device.Path); err != nil {
return nil, err
}
}
}
- s.HostDeviceList = s.Devices
+ s.HostDeviceList = userDevices
// set the devices cgroup when not running in a user namespace
if !inUserNS && !s.Privileged {
diff --git a/pkg/specgen/podspecgen.go b/pkg/specgen/podspecgen.go
index e59d11c0a..33e8422fd 100644
--- a/pkg/specgen/podspecgen.go
+++ b/pkg/specgen/podspecgen.go
@@ -196,6 +196,7 @@ type PodSpecGenerator struct {
PodCgroupConfig
PodResourceConfig
PodStorageConfig
+ PodSecurityConfig
InfraContainerSpec *SpecGenerator `json:"-"`
}
@@ -210,6 +211,10 @@ type PodResourceConfig struct {
ThrottleReadBpsDevice map[string]spec.LinuxThrottleDevice `json:"throttleReadBpsDevice,omitempty"`
}
+type PodSecurityConfig struct {
+ SecurityOpt []string `json:"security_opt,omitempty"`
+}
+
// NewPodSpecGenerator creates a new pod spec
func NewPodSpecGenerator() *PodSpecGenerator {
return &PodSpecGenerator{}
diff --git a/pkg/util/utils.go b/pkg/util/utils.go
index 390057c32..11edf265f 100644
--- a/pkg/util/utils.go
+++ b/pkg/util/utils.go
@@ -665,8 +665,8 @@ func CreateCidFile(cidfile string, id string) error {
return nil
}
-// DefaultCPUPeriod is the default CPU period is 100us, which is the same default
-// as Kubernetes.
+// DefaultCPUPeriod is the default CPU period (100ms) in microseconds, which is
+// the same default as Kubernetes.
const DefaultCPUPeriod uint64 = 100000
// CoresToPeriodAndQuota converts a fraction of cores to the equivalent
diff --git a/test/apiv2/20-containers.at b/test/apiv2/20-containers.at
index 5a02ca3cb..554a905d4 100644
--- a/test/apiv2/20-containers.at
+++ b/test/apiv2/20-containers.at
@@ -18,7 +18,7 @@ podman rm -a -f &>/dev/null
t GET "libpod/containers/json (at start: clean slate)" 200 length=0
-podman run $IMAGE true
+podman run -v /tmp:/tmp $IMAGE true
t GET libpod/containers/json 200 length=0
@@ -33,6 +33,7 @@ t GET libpod/containers/json?all=true 200 \
.[0].Command[0]="true" \
.[0].State~\\\(exited\\\|stopped\\\) \
.[0].ExitCode=0 \
+ .[0].Mounts~.*/tmp \
.[0].IsInfra=false
# Test compat API for Network Settings (.Network is N/A when rootless)
@@ -44,6 +45,7 @@ t GET /containers/json?all=true 200 \
length=1 \
.[0].Id~[0-9a-f]\\{64\\} \
.[0].Image=$IMAGE \
+ .[0].Mounts~.*/tmp \
$network_expect
# compat API imageid with sha256: prefix
diff --git a/test/apiv2/27-containersEvents.at b/test/apiv2/27-containersEvents.at
new file mode 100644
index 000000000..a86f2e353
--- /dev/null
+++ b/test/apiv2/27-containersEvents.at
@@ -0,0 +1,27 @@
+# -*- sh -*-
+#
+# test container-related events
+#
+
+podman pull $IMAGE &>/dev/null
+
+# Ensure clean slate
+podman rm -a -f &>/dev/null
+
+START=$(date +%s)
+
+podman run $IMAGE false || true
+
+# libpod api
+t GET "libpod/events?stream=false&since=$START" 200 \
+ 'select(.status | contains("start")).Action=start' \
+ 'select(.status | contains("died")).Action=died' \
+ 'select(.status | contains("died")).Actor.Attributes.containerExitCode=1'
+
+# compat api, uses status=die (#12643)
+t GET "events?stream=false&since=$START" 200 \
+ 'select(.status | contains("start")).Action=start' \
+ 'select(.status | contains("die")).Action=die' \
+ 'select(.status | contains("die")).Actor.Attributes.exitCode=1'
+
+# vim: filetype=sh
diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go
index 6e1a62b99..bd744aa78 100644
--- a/test/e2e/common_test.go
+++ b/test/e2e/common_test.go
@@ -274,14 +274,32 @@ func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration {
}
if remote {
- uuid := stringid.GenerateNonCryptoID()
+ var pathPrefix string
if !rootless.IsRootless() {
- p.RemoteSocket = fmt.Sprintf("unix:/run/podman/podman-%s.sock", uuid)
+ pathPrefix = "/run/podman/podman"
} else {
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
- socket := fmt.Sprintf("podman-%s.sock", uuid)
- fqpath := filepath.Join(runtimeDir, socket)
- p.RemoteSocket = fmt.Sprintf("unix:%s", fqpath)
+ pathPrefix = filepath.Join(runtimeDir, "podman")
+ }
+ // We want to avoid collisions in socket paths, but using the
+ // socket directly for a collision check doesn’t work; bind(2) on AF_UNIX
+ // creates the file, and we need to pass a unique path now before the bind(2)
+ // happens. So, use a podman-%s.sock-lock empty file as a marker.
+ tries := 0
+ for {
+ uuid := stringid.GenerateNonCryptoID()
+ lockPath := fmt.Sprintf("%s-%s.sock-lock", pathPrefix, uuid)
+ lockFile, err := os.OpenFile(lockPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0700)
+ if err == nil {
+ lockFile.Close()
+ p.RemoteSocketLock = lockPath
+ p.RemoteSocket = fmt.Sprintf("unix:%s-%s.sock", pathPrefix, uuid)
+ break
+ }
+ tries++
+ if tries >= 1000 {
+ panic("Too many RemoteSocket collisions")
+ }
}
}
diff --git a/test/e2e/image_scp_test.go b/test/e2e/image_scp_test.go
index 6651a04b5..767b355d9 100644
--- a/test/e2e/image_scp_test.go
+++ b/test/e2e/image_scp_test.go
@@ -29,7 +29,6 @@ var _ = Describe("podman image scp", func() {
panic(err)
}
os.Setenv("CONTAINERS_CONF", conf.Name())
-
tempdir, err = CreateTempDirInTempDir()
if err != nil {
os.Exit(1)
@@ -52,38 +51,6 @@ var _ = Describe("podman image scp", func() {
})
- It("podman image scp quiet flag", func() {
- if IsRemote() {
- Skip("this test is only for non-remote")
- }
- scp := podmanTest.Podman([]string{"image", "scp", "-q", ALPINE})
- scp.WaitWithDefaultTimeout()
- Expect(scp).To(Exit(0))
- })
-
- It("podman image scp root to rootless transfer", func() {
- SkipIfNotRootless("this is a rootless only test, transferring from root to rootless using PodmanAsUser")
- if IsRemote() {
- Skip("this test is only for non-remote")
- }
- env := os.Environ()
- img := podmanTest.PodmanAsUser([]string{"image", "pull", ALPINE}, 0, 0, "", env) // pull image to root
- img.WaitWithDefaultTimeout()
- Expect(img).To(Exit(0))
- scp := podmanTest.PodmanAsUser([]string{"image", "scp", "root@localhost::" + ALPINE, "1000:1000@localhost::"}, 0, 0, "", env) //transfer from root to rootless (us)
- scp.WaitWithDefaultTimeout()
- Expect(scp).To(Exit(0))
-
- list := podmanTest.Podman([]string{"image", "list"}) // our image should now contain alpine loaded in from root
- list.WaitWithDefaultTimeout()
- Expect(list).To(Exit(0))
- Expect(list.OutputToStringArray()).To(ContainElement(HavePrefix("quay.io/libpod/alpine")))
-
- scp = podmanTest.PodmanAsUser([]string{"image", "scp", "root@localhost::" + ALPINE}, 0, 0, "", env) //transfer from root to rootless (us)
- scp.WaitWithDefaultTimeout()
- Expect(scp).To(Exit(0))
- })
-
It("podman image scp bogus image", func() {
if IsRemote() {
Skip("this test is only for non-remote")
@@ -119,11 +86,8 @@ var _ = Describe("podman image scp", func() {
scp.Wait(45)
// exit with error because we cannot make an actual ssh connection
// This tests that the input we are given is validated and prepared correctly
- // Error: failed to connect: dial tcp: address foo: missing port in address
+ // The error given should either be a missing image (due to testing suite complications) or a i/o timeout on ssh
Expect(scp).To(ExitWithError())
- Expect(scp.ErrorToString()).To(ContainSubstring(
- "Error: failed to connect: dial tcp 66.151.147.142:2222: i/o timeout",
- ))
})
diff --git a/test/e2e/libpod_suite_remote_test.go b/test/e2e/libpod_suite_remote_test.go
index d60383029..4644e3748 100644
--- a/test/e2e/libpod_suite_remote_test.go
+++ b/test/e2e/libpod_suite_remote_test.go
@@ -1,3 +1,4 @@
+//go:build remote
// +build remote
package integration
@@ -143,6 +144,11 @@ func (p *PodmanTestIntegration) StopRemoteService() {
if err := os.Remove(socket); err != nil {
fmt.Println(err)
}
+ if p.RemoteSocketLock != "" {
+ if err := os.Remove(p.RemoteSocketLock); err != nil {
+ fmt.Println(err)
+ }
+ }
}
//MakeOptions assembles all the podman main options
diff --git a/test/e2e/pod_create_test.go b/test/e2e/pod_create_test.go
index 41a017a52..fab107af8 100644
--- a/test/e2e/pod_create_test.go
+++ b/test/e2e/pod_create_test.go
@@ -9,6 +9,8 @@ import (
"strconv"
"strings"
+ "github.com/containers/common/pkg/apparmor"
+ "github.com/containers/common/pkg/seccomp"
"github.com/containers/common/pkg/sysinfo"
"github.com/containers/podman/v3/pkg/rootless"
"github.com/containers/podman/v3/pkg/util"
@@ -16,6 +18,7 @@ import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gexec"
+ "github.com/opencontainers/selinux/go-selinux"
)
var _ = Describe("Podman pod create", func() {
@@ -967,4 +970,63 @@ ENTRYPOINT ["sleep","99999"]
Expect(inspect).Should(Exit(0))
Expect(inspect.OutputToString()).Should(Equal("host"))
})
+
+ It("podman pod create --security-opt", func() {
+ if !selinux.GetEnabled() {
+ Skip("SELinux not enabled")
+ }
+ podCreate := podmanTest.Podman([]string{"pod", "create", "--security-opt", "label=type:spc_t", "--security-opt", "seccomp=unconfined"})
+ podCreate.WaitWithDefaultTimeout()
+ Expect(podCreate).Should(Exit(0))
+
+ ctrCreate := podmanTest.Podman([]string{"container", "create", "--pod", podCreate.OutputToString(), ALPINE})
+ ctrCreate.WaitWithDefaultTimeout()
+ Expect(ctrCreate).Should(Exit(0))
+
+ ctrInspect := podmanTest.InspectContainer(ctrCreate.OutputToString())
+ Expect(ctrInspect[0].HostConfig.SecurityOpt).To(Equal([]string{"label=type:spc_t", "seccomp=unconfined"}))
+
+ podCreate = podmanTest.Podman([]string{"pod", "create", "--security-opt", "label=disable"})
+ podCreate.WaitWithDefaultTimeout()
+ Expect(podCreate).Should(Exit(0))
+
+ ctrCreate = podmanTest.Podman([]string{"container", "run", "-it", "--pod", podCreate.OutputToString(), ALPINE, "cat", "/proc/self/attr/current"})
+ ctrCreate.WaitWithDefaultTimeout()
+ Expect(ctrCreate).Should(Exit(0))
+ match, _ := ctrCreate.GrepString("spc_t")
+ Expect(match).Should(BeTrue())
+ })
+
+ It("podman pod create --security-opt seccomp", func() {
+ if !seccomp.IsEnabled() {
+ Skip("seccomp is not enabled")
+ }
+ podCreate := podmanTest.Podman([]string{"pod", "create", "--security-opt", "seccomp=unconfined"})
+ podCreate.WaitWithDefaultTimeout()
+ Expect(podCreate).Should(Exit(0))
+
+ ctrCreate := podmanTest.Podman([]string{"container", "create", "--pod", podCreate.OutputToString(), ALPINE})
+ ctrCreate.WaitWithDefaultTimeout()
+ Expect(ctrCreate).Should(Exit(0))
+
+ ctrInspect := podmanTest.InspectContainer(ctrCreate.OutputToString())
+ Expect(ctrInspect[0].HostConfig.SecurityOpt).To(Equal([]string{"seccomp=unconfined"}))
+ })
+
+ It("podman pod create --security-opt apparmor test", func() {
+ if !apparmor.IsEnabled() {
+ Skip("Apparmor is not enabled")
+ }
+ podCreate := podmanTest.Podman([]string{"pod", "create", "--security-opt", fmt.Sprintf("apparmor=%s", apparmor.Profile)})
+ podCreate.WaitWithDefaultTimeout()
+ Expect(podCreate).Should(Exit(0))
+
+ ctrCreate := podmanTest.Podman([]string{"container", "create", "--pod", podCreate.OutputToString(), ALPINE})
+ ctrCreate.WaitWithDefaultTimeout()
+ Expect(ctrCreate).Should(Exit(0))
+
+ inspect := podmanTest.InspectContainer(ctrCreate.OutputToString())
+ Expect(inspect[0].AppArmorProfile).To(Equal(apparmor.Profile))
+
+ })
})
diff --git a/test/e2e/pod_initcontainers_test.go b/test/e2e/pod_initcontainers_test.go
index 11e7ca400..e73f28a7a 100644
--- a/test/e2e/pod_initcontainers_test.go
+++ b/test/e2e/pod_initcontainers_test.go
@@ -135,7 +135,7 @@ var _ = Describe("Podman init containers", func() {
filename := filepath.Join("/dev/shm", RandomString(12))
// Write the date to a file
- session := podmanTest.Podman([]string{"create", "--init-ctr", "always", "--pod", "new:foobar", ALPINE, "bin/sh", "-c", fmt.Sprintf("date > %s", filename)})
+ session := podmanTest.Podman([]string{"create", "--init-ctr", "always", "--pod", "new:foobar", fedoraMinimal, "bin/sh", "-c", "date +%T.%N > " + filename})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
verify := podmanTest.Podman([]string{"create", "--pod", "foobar", "-t", ALPINE, "top"})
diff --git a/test/e2e/run_staticip_test.go b/test/e2e/run_staticip_test.go
index eb7dc9d11..2f3c3025a 100644
--- a/test/e2e/run_staticip_test.go
+++ b/test/e2e/run_staticip_test.go
@@ -7,6 +7,7 @@ import (
"time"
. "github.com/containers/podman/v3/test/utils"
+ "github.com/containers/storage/pkg/stringid"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gexec"
@@ -65,6 +66,20 @@ var _ = Describe("Podman run with --ip flag", func() {
Expect(result.OutputToString()).To(ContainSubstring(ip + "/16"))
})
+ It("Podman run with specified static IPv6 has correct IP", func() {
+ netName := "ipv6-" + stringid.GenerateNonCryptoID()
+ ipv6 := "fd46:db93:aa76:ac37::10"
+ net := podmanTest.Podman([]string{"network", "create", "--subnet", "fd46:db93:aa76:ac37::/64", netName})
+ net.WaitWithDefaultTimeout()
+ defer podmanTest.removeCNINetwork(netName)
+ Expect(net).To(Exit(0))
+
+ result := podmanTest.Podman([]string{"run", "-ti", "--network", netName, "--ip6", ipv6, ALPINE, "ip", "addr"})
+ result.WaitWithDefaultTimeout()
+ Expect(result).Should(Exit(0))
+ Expect(result.OutputToString()).To(ContainSubstring(ipv6 + "/64"))
+ })
+
It("Podman run with --network bridge:ip=", func() {
ip := GetRandomIPAddress()
result := podmanTest.Podman([]string{"run", "-ti", "--network", "bridge:ip=" + ip, ALPINE, "ip", "addr"})
diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go
index 8db23080e..e98f2c999 100644
--- a/test/e2e/run_test.go
+++ b/test/e2e/run_test.go
@@ -1315,7 +1315,7 @@ USER mail`, BB)
Expect(err).To(BeNil())
file.Close()
- session := podmanTest.Podman([]string{"run", "-dt", "--restart", "always", "-v", fmt.Sprintf("%s:/tmp/runroot:Z", testDir), ALPINE, "sh", "-c", "date +%N > /tmp/runroot/ran && while test -r /tmp/runroot/running; do sleep 0.1s; done"})
+ session := podmanTest.Podman([]string{"run", "-dt", "--restart", "always", "-v", fmt.Sprintf("%s:/tmp/runroot:Z", testDir), ALPINE, "sh", "-c", "touch /tmp/runroot/ran && while test -r /tmp/runroot/running; do sleep 0.1s; done"})
found := false
testFile := filepath.Join(testDir, "ran")
diff --git a/test/system/030-run.bats b/test/system/030-run.bats
index 130cf5492..d81a0758c 100644
--- a/test/system/030-run.bats
+++ b/test/system/030-run.bats
@@ -778,6 +778,25 @@ EOF
is "$output" "1.2.3.4 foo.com.*" "users can add hosts even without /etc/hosts"
}
+# rhbz#1854566 : $IMAGE has incorrect permission 555 on the root '/' filesystem
+@test "podman run image with filesystem permission" {
+ # make sure the IMAGE image have permissiong of 555 like filesystem RPM expects
+ run_podman run --rm $IMAGE stat -c %a /
+ is "$output" "555" "directory permissions on /"
+}
+
+# rhbz#1763007 : the --log-opt for podman run does not work as expected
+@test "podman run with log-opt option" {
+ # Pseudorandom size of the form N.NNN. The '| 1' handles '0.NNN' or 'N.NN0',
+ # which podman displays as 'NNN kB' or 'N.NN MB' respectively.
+ size=$(printf "%d.%03d" $(($RANDOM % 10 | 1)) $(($RANDOM % 100 | 1)))
+ run_podman run -d --rm --log-opt max-size=${size}m $IMAGE sleep 5
+ cid=$output
+ run_podman inspect --format "{{ .HostConfig.LogConfig.Size }}" $cid
+ is "$output" "${size}MB"
+ run_podman rm -t 0 -f $cid
+}
+
@test "podman run --kernel-memory warning" {
# Not sure what situations this fails in, but want to make sure warning shows.
run_podman '?' run --rm --kernel-memory 100 $IMAGE false
diff --git a/test/system/120-load.bats b/test/system/120-load.bats
index a5508b2f4..541095764 100644
--- a/test/system/120-load.bats
+++ b/test/system/120-load.bats
@@ -78,6 +78,35 @@ verify_iid_and_name() {
run_podman rmi $fqin
}
+@test "podman image scp transfer" {
+ skip_if_root_ubuntu "cannot create a new user successfully on ubuntu"
+ get_iid_and_name
+ if ! is_remote; then
+ if is_rootless; then
+ whoami=$(id -un)
+ run_podman image scp $whoami@localhost::$iid root@localhost::
+ if [ "$status" -ne 0 ]; then
+ die "Command failed: podman image scp transfer"
+ fi
+ whoami=$(id -un)
+ run_podman image scp -q $whoami@localhost::$iid root@localhost::
+ if [ "$status" -ne 0 ]; then
+ die "Command failed: podman image scp quiet transfer failed"
+ fi
+ fi
+ if ! is_rootless; then
+ id -u 1000 &>/dev/null || useradd -u 1000 -g 1000 testingUsr
+ if [ "$status" -ne 0 ]; then
+ die "Command failed: useradd 1000"
+ fi
+ run_podman image scp root@localhost::$iid 1000:1000@localhost::
+ if [ "$status" -ne 0 ]; then
+ die "Command failed: podman image scp transfer"
+ fi
+ fi
+ fi
+}
+
@test "podman load - by image ID" {
# FIXME: how to build a simple archive instead?
diff --git a/test/system/160-volumes.bats b/test/system/160-volumes.bats
index 43462de36..1271b7c0b 100644
--- a/test/system/160-volumes.bats
+++ b/test/system/160-volumes.bats
@@ -319,5 +319,30 @@ EOF
is "$output" "" "no more volumes to prune"
}
+@test "podman volume type=bind" {
+ myvoldir=${PODMAN_TMPDIR}/volume_$(random_string)
+ mkdir $myvoldir
+ touch $myvoldir/myfile
+
+ myvolume=myvol$(random_string)
+ run_podman 125 volume create -o type=bind -o device=/bogus $myvolume
+ is "$output" "Error: invalid volume option device for driver 'local': stat /bogus: no such file or directory" "should fail with bogus directory not existing"
+
+ run_podman volume create -o type=bind -o device=/$myvoldir $myvolume
+ is "$output" "$myvolume" "should successfully create myvolume"
+
+ run_podman run --rm -v $myvolume:/vol:z $IMAGE \
+ stat -c "%u:%s" /vol/myfile
+ is "$output" "0:0" "w/o keep-id: stat(file in container) == root"
+}
+
+@test "podman volume type=tmpfs" {
+ myvolume=myvol$(random_string)
+ run_podman volume create -o type=tmpfs -o device=tmpfs $myvolume
+ is "$output" "$myvolume" "should successfully create myvolume"
+
+ run_podman run --rm -v $myvolume:/vol $IMAGE stat -f -c "%T" /vol
+ is "$output" "tmpfs" "volume should be tmpfs"
+}
# vim: filetype=sh
diff --git a/test/system/180-blkio.bats b/test/system/180-blkio.bats
index 68449681a..7999c9ec5 100644
--- a/test/system/180-blkio.bats
+++ b/test/system/180-blkio.bats
@@ -8,7 +8,7 @@ load helpers
function teardown() {
lofile=${PODMAN_TMPDIR}/disk.img
if [ -f ${lofile} ]; then
- run_podman '?' rm -t 0 --all --force
+ run_podman '?' rm -t 0 --all --force --ignore
while read path dev; do
if [[ "$path" == "$lofile" ]]; then
diff --git a/test/system/520-checkpoint.bats b/test/system/520-checkpoint.bats
index 723a20cc4..046dfd126 100644
--- a/test/system/520-checkpoint.bats
+++ b/test/system/520-checkpoint.bats
@@ -11,7 +11,7 @@ function setup() {
# TL;DR they keep fixing it then breaking it again. There's a test we
# could run to see if it's fixed, but it's way too complicated. Since
# integration tests also skip checkpoint tests on Ubuntu, do the same here.
- if grep -qiw ubuntu /etc/os-release; then
+ if is_ubuntu; then
skip "FIXME: checkpointing broken in Ubuntu 2004, 2104, 2110, ..."
fi
diff --git a/test/system/helpers.bash b/test/system/helpers.bash
index 415c9010e..36a88fc10 100644
--- a/test/system/helpers.bash
+++ b/test/system/helpers.bash
@@ -56,14 +56,14 @@ fi
# Setup helper: establish a test environment with exactly the images needed
function basic_setup() {
# Clean up all containers
- run_podman rm -t 0 --all --force
+ run_podman rm -t 0 --all --force --ignore
# ...including external (buildah) ones
run_podman ps --all --external --format '{{.ID}} {{.Names}}'
for line in "${lines[@]}"; do
set $line
echo "# setup(): removing stray external container $1 ($2)" >&3
- run_podman rm $1
+ run_podman rm -f $1
done
# Clean up all images except those desired
@@ -109,8 +109,8 @@ function basic_setup() {
# Basic teardown: remove all pods and containers
function basic_teardown() {
echo "# [teardown]" >&2
- run_podman '?' pod rm -t 0 --all --force
- run_podman '?' rm -t 0 --all --force
+ run_podman '?' pod rm -t 0 --all --force --ignore
+ run_podman '?' rm -t 0 --all --force --ignore
command rm -rf $PODMAN_TMPDIR
}
@@ -318,6 +318,10 @@ function wait_for_port() {
# BEGIN miscellaneous tools
# Shortcuts for common needs:
+function is_ubuntu() {
+ grep -qiw ubuntu /etc/os-release
+}
+
function is_rootless() {
[ "$(id -u)" -ne 0 ]
}
@@ -459,6 +463,16 @@ function skip_if_journald_unavailable {
fi
}
+function skip_if_root_ubuntu {
+ if is_ubuntu; then
+ if ! is_remote; then
+ if ! is_rootless; then
+ skip "Cannot run this test on rootful ubuntu, usually due to user errors"
+ fi
+ fi
+ fi
+}
+
#########
# die # Abort with helpful message
#########
diff --git a/test/utils/utils.go b/test/utils/utils.go
index f41024072..1f5067950 100644
--- a/test/utils/utils.go
+++ b/test/utils/utils.go
@@ -41,6 +41,7 @@ type PodmanTest struct {
RemotePodmanBinary string
RemoteSession *os.Process
RemoteSocket string
+ RemoteSocketLock string // If not "", should be removed _after_ RemoteSocket is removed
RemoteCommand *exec.Cmd
ImageCacheDir string
ImageCacheFS string
@@ -469,10 +470,6 @@ func Containerized() bool {
return strings.Contains(string(b), "docker")
}
-func init() {
- rand.Seed(GinkgoRandomSeed())
-}
-
var randomLetters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
// RandomString returns a string of given length composed of random characters
diff --git a/utils/utils.go b/utils/utils.go
index 45cec2c5f..4c04b939d 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -229,3 +229,12 @@ func MovePauseProcessToScope(pausePidPath string) {
}
}
}
+
+// CreateSCPCommand takes an existing command, appends the given arguments and returns a configured podman command for image scp
+func CreateSCPCommand(cmd *exec.Cmd, command []string) *exec.Cmd {
+ cmd.Args = append(cmd.Args, command...)
+ cmd.Env = os.Environ()
+ cmd.Stderr = os.Stderr
+ cmd.Stdout = os.Stdout
+ return cmd
+}