summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/podman/common/create.go8
-rw-r--r--cmd/podman/common/create_opts.go9
-rw-r--r--cmd/podman/common/default.go9
-rw-r--r--cmd/podman/inspect/inspect.go12
-rw-r--r--cmd/podman/machine/init.go8
-rw-r--r--docs/source/markdown/podman-machine-init.1.md15
-rw-r--r--libpod/define/healthchecks.go13
-rw-r--r--libpod/oci_attach_linux.go2
-rw-r--r--libpod/oci_conmon_exec_linux.go14
-rw-r--r--libpod/oci_conmon_linux.go10
-rw-r--r--libpod/oci_util.go14
-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/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/machine/config.go2
-rw-r--r--pkg/machine/qemu/config.go10
-rw-r--r--pkg/machine/qemu/machine.go96
-rw-r--r--pkg/specgen/generate/container.go8
-rw-r--r--test/e2e/healthcheck_run_test.go38
-rw-r--r--test/system/030-run.bats4
31 files changed, 730 insertions, 200 deletions
diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go
index 32d227e65..b60169990 100644
--- a/cmd/podman/common/create.go
+++ b/cmd/podman/common/create.go
@@ -257,7 +257,7 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions,
healthIntervalFlagName := "health-interval"
createFlags.StringVar(
&cf.HealthInterval,
- healthIntervalFlagName, DefaultHealthCheckInterval,
+ healthIntervalFlagName, define.DefaultHealthCheckInterval,
"set an interval for the healthchecks (a value of disable results in no automatic timer setup)",
)
_ = cmd.RegisterFlagCompletionFunc(healthIntervalFlagName, completion.AutocompleteNone)
@@ -265,7 +265,7 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions,
healthRetriesFlagName := "health-retries"
createFlags.UintVar(
&cf.HealthRetries,
- healthRetriesFlagName, DefaultHealthCheckRetries,
+ healthRetriesFlagName, define.DefaultHealthCheckRetries,
"the number of retries allowed before a healthcheck is considered to be unhealthy",
)
_ = cmd.RegisterFlagCompletionFunc(healthRetriesFlagName, completion.AutocompleteNone)
@@ -273,7 +273,7 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions,
healthStartPeriodFlagName := "health-start-period"
createFlags.StringVar(
&cf.HealthStartPeriod,
- healthStartPeriodFlagName, DefaultHealthCheckStartPeriod,
+ healthStartPeriodFlagName, define.DefaultHealthCheckStartPeriod,
"the initialization time needed for a container to bootstrap",
)
_ = cmd.RegisterFlagCompletionFunc(healthStartPeriodFlagName, completion.AutocompleteNone)
@@ -281,7 +281,7 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions,
healthTimeoutFlagName := "health-timeout"
createFlags.StringVar(
&cf.HealthTimeout,
- healthTimeoutFlagName, DefaultHealthCheckTimeout,
+ healthTimeoutFlagName, define.DefaultHealthCheckTimeout,
"the maximum time allowed to complete the healthcheck before an interval is considered failed",
)
_ = cmd.RegisterFlagCompletionFunc(healthTimeoutFlagName, completion.AutocompleteNone)
diff --git a/cmd/podman/common/create_opts.go b/cmd/podman/common/create_opts.go
index f2335a2be..297188a45 100644
--- a/cmd/podman/common/create_opts.go
+++ b/cmd/podman/common/create_opts.go
@@ -11,6 +11,7 @@ import (
"github.com/containers/common/pkg/cgroups"
"github.com/containers/common/pkg/config"
"github.com/containers/podman/v3/cmd/podman/registry"
+ "github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/pkg/api/handlers"
"github.com/containers/podman/v3/pkg/domain/entities"
@@ -304,10 +305,10 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *c
VolumesFrom: cc.HostConfig.VolumesFrom,
Workdir: cc.Config.WorkingDir,
Net: &netInfo,
- HealthInterval: DefaultHealthCheckInterval,
- HealthRetries: DefaultHealthCheckRetries,
- HealthTimeout: DefaultHealthCheckTimeout,
- HealthStartPeriod: DefaultHealthCheckStartPeriod,
+ HealthInterval: define.DefaultHealthCheckInterval,
+ HealthRetries: define.DefaultHealthCheckRetries,
+ HealthTimeout: define.DefaultHealthCheckTimeout,
+ HealthStartPeriod: define.DefaultHealthCheckStartPeriod,
}
if !rootless.IsRootless() {
var ulimits []string
diff --git a/cmd/podman/common/default.go b/cmd/podman/common/default.go
index 7e025c449..7997e761c 100644
--- a/cmd/podman/common/default.go
+++ b/cmd/podman/common/default.go
@@ -5,14 +5,7 @@ import (
)
var (
- // DefaultHealthCheckInterval default value
- DefaultHealthCheckInterval = "30s"
- // DefaultHealthCheckRetries default value
- DefaultHealthCheckRetries uint = 3
- // DefaultHealthCheckStartPeriod default value
- DefaultHealthCheckStartPeriod = "0s"
- // DefaultHealthCheckTimeout default value
- DefaultHealthCheckTimeout = "30s"
+
// DefaultImageVolume default value
DefaultImageVolume = "bind"
// Pull in configured json library
diff --git a/cmd/podman/inspect/inspect.go b/cmd/podman/inspect/inspect.go
index c982b1b7f..482b616af 100644
--- a/cmd/podman/inspect/inspect.go
+++ b/cmd/podman/inspect/inspect.go
@@ -237,12 +237,12 @@ func (i *inspector) inspect(namesOrIDs []string) error {
}
func printJSON(data []interface{}) error {
- buf, err := json.MarshalIndent(data, "", " ")
- if err != nil {
- return err
- }
- _, err = fmt.Println(string(buf))
- return err
+ enc := json.NewEncoder(os.Stdout)
+ // by default, json marshallers will force utf=8 from
+ // a string. this breaks healthchecks that use <,>, &&.
+ enc.SetEscapeHTML(false)
+ enc.SetIndent("", " ")
+ return enc.Encode(data)
}
func printTmpl(typ, row string, data []interface{}) error {
diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go
index 14e87c201..ed04239c4 100644
--- a/cmd/podman/machine/init.go
+++ b/cmd/podman/machine/init.go
@@ -88,6 +88,14 @@ func init() {
flags.StringVar(&initOpts.ImagePath, ImagePathFlagName, cfg.Machine.Image, "Path to qcow image")
_ = initCmd.RegisterFlagCompletionFunc(ImagePathFlagName, completion.AutocompleteDefault)
+ VolumeFlagName := "volume"
+ flags.StringArrayVarP(&initOpts.Volumes, VolumeFlagName, "v", []string{}, "Volumes to mount, source:target")
+ _ = initCmd.RegisterFlagCompletionFunc(VolumeFlagName, completion.AutocompleteDefault)
+
+ VolumeDriverFlagName := "volume-driver"
+ flags.StringVar(&initOpts.VolumeDriver, VolumeDriverFlagName, "", "Optional volume driver")
+ _ = initCmd.RegisterFlagCompletionFunc(VolumeDriverFlagName, completion.AutocompleteDefault)
+
IgnitionPathFlagName := "ignition-path"
flags.StringVar(&initOpts.IgnitionPath, IgnitionPathFlagName, "", "Path to ignition file")
_ = initCmd.RegisterFlagCompletionFunc(IgnitionPathFlagName, completion.AutocompleteDefault)
diff --git a/docs/source/markdown/podman-machine-init.1.md b/docs/source/markdown/podman-machine-init.1.md
index aead6c695..b515e8763 100644
--- a/docs/source/markdown/podman-machine-init.1.md
+++ b/docs/source/markdown/podman-machine-init.1.md
@@ -61,6 +61,20 @@ Set the timezone for the machine and containers. Valid values are `local` or
a `timezone` such as `America/Chicago`. A value of `local`, which is the default,
means to use the timezone of the machine host.
+#### **--volume**, **-v**=*source:target*
+
+Mounts a volume from source to target.
+
+Create a mount. If /host-dir:/machine-dir is specified as the `*source:target*`,
+Podman mounts _host-dir_ in the host to _machine-dir_ in the Podman machine.
+
+The root filesystem is mounted read-only in the default operating system,
+so mounts must be created under the /mnt directory.
+
+#### **--volume-driver**
+
+Driver to use for mounting volumes from the host, such as `virtfs`.
+
#### **--help**
Print usage statement.
@@ -72,6 +86,7 @@ $ podman machine init
$ podman machine init myvm
$ podman machine init --disk-size 50
$ podman machine init --memory=1024 myvm
+$ podman machine init -v /Users:/mnt/Users
```
## SEE ALSO
diff --git a/libpod/define/healthchecks.go b/libpod/define/healthchecks.go
index 4114262b6..bde449d30 100644
--- a/libpod/define/healthchecks.go
+++ b/libpod/define/healthchecks.go
@@ -34,3 +34,16 @@ const (
// HealthCheckDefined means the healthcheck was found on the container
HealthCheckDefined HealthCheckStatus = iota
)
+
+// Healthcheck defaults. These are used both in the cli as well in
+// libpod and were moved from cmd/podman/common
+const (
+ // DefaultHealthCheckInterval default value
+ DefaultHealthCheckInterval = "30s"
+ // DefaultHealthCheckRetries default value
+ DefaultHealthCheckRetries uint = 3
+ // DefaultHealthCheckStartPeriod default value
+ DefaultHealthCheckStartPeriod = "0s"
+ // DefaultHealthCheckTimeout default value
+ DefaultHealthCheckTimeout = "30s"
+)
diff --git a/libpod/oci_attach_linux.go b/libpod/oci_attach_linux.go
index 1f2a28ead..a382f6043 100644
--- a/libpod/oci_attach_linux.go
+++ b/libpod/oci_attach_linux.go
@@ -143,7 +143,7 @@ func (c *Container) attachToExec(streams *define.AttachStreams, keys *string, se
}
// 2: read from attachFd that the parent process has set up the console socket
- if _, err := readConmonPipeData(attachFd, ""); err != nil {
+ if _, err := readConmonPipeData(c.ociRuntime.Name(), attachFd, ""); err != nil {
return err
}
diff --git a/libpod/oci_conmon_exec_linux.go b/libpod/oci_conmon_exec_linux.go
index 654306f92..e58d4d6f9 100644
--- a/libpod/oci_conmon_exec_linux.go
+++ b/libpod/oci_conmon_exec_linux.go
@@ -76,7 +76,7 @@ func (r *ConmonOCIRuntime) ExecContainer(c *Container, sessionID string, options
return -1, nil, errors.Wrapf(err, "cannot run conmon")
}
- pid, err := readConmonPipeData(pipes.syncPipe, ociLog)
+ pid, err := readConmonPipeData(r.name, pipes.syncPipe, ociLog)
return pid, attachChan, err
}
@@ -134,7 +134,7 @@ func (r *ConmonOCIRuntime) ExecContainerHTTP(ctr *Container, sessionID string, o
conmonPipeDataChan := make(chan conmonPipeData)
go func() {
// attachToExec is responsible for closing pipes
- attachChan <- attachExecHTTP(ctr, sessionID, req, w, streams, pipes, detachKeys, options.Terminal, cancel, hijackDone, holdConnOpen, execCmd, conmonPipeDataChan, ociLog, newSize)
+ attachChan <- attachExecHTTP(ctr, sessionID, req, w, streams, pipes, detachKeys, options.Terminal, cancel, hijackDone, holdConnOpen, execCmd, conmonPipeDataChan, ociLog, newSize, r.name)
close(attachChan)
}()
@@ -176,7 +176,7 @@ func (r *ConmonOCIRuntime) ExecContainerDetached(ctr *Container, sessionID strin
// Wait for Conmon to tell us we're ready to attach.
// We aren't actually *going* to attach, but this means that we're good
// to proceed.
- if _, err := readConmonPipeData(pipes.attachPipe, ""); err != nil {
+ if _, err := readConmonPipeData(r.name, pipes.attachPipe, ""); err != nil {
return -1, err
}
@@ -190,7 +190,7 @@ func (r *ConmonOCIRuntime) ExecContainerDetached(ctr *Container, sessionID strin
return -1, errors.Wrapf(err, "cannot run conmon")
}
- pid, err := readConmonPipeData(pipes.syncPipe, ociLog)
+ pid, err := readConmonPipeData(r.name, pipes.syncPipe, ociLog)
return pid, err
}
@@ -486,7 +486,7 @@ func (r *ConmonOCIRuntime) startExec(c *Container, sessionID string, options *Ex
}
// Attach to a container over HTTP
-func attachExecHTTP(c *Container, sessionID string, r *http.Request, w http.ResponseWriter, streams *HTTPAttachStreams, pipes *execPipes, detachKeys []byte, isTerminal bool, cancel <-chan bool, hijackDone chan<- bool, holdConnOpen <-chan bool, execCmd *exec.Cmd, conmonPipeDataChan chan<- conmonPipeData, ociLog string, newSize *define.TerminalSize) (deferredErr error) {
+func attachExecHTTP(c *Container, sessionID string, r *http.Request, w http.ResponseWriter, streams *HTTPAttachStreams, pipes *execPipes, detachKeys []byte, isTerminal bool, cancel <-chan bool, hijackDone chan<- bool, holdConnOpen <-chan bool, execCmd *exec.Cmd, conmonPipeDataChan chan<- conmonPipeData, ociLog string, newSize *define.TerminalSize, runtimeName string) (deferredErr error) {
// NOTE: As you may notice, the attach code is quite complex.
// Many things happen concurrently and yet are interdependent.
// If you ever change this function, make sure to write to the
@@ -519,7 +519,7 @@ func attachExecHTTP(c *Container, sessionID string, r *http.Request, w http.Resp
}
// 2: read from attachFd that the parent process has set up the console socket
- if _, err := readConmonPipeData(pipes.attachPipe, ""); err != nil {
+ if _, err := readConmonPipeData(runtimeName, pipes.attachPipe, ""); err != nil {
conmonPipeDataChan <- conmonPipeData{-1, err}
return err
}
@@ -582,7 +582,7 @@ func attachExecHTTP(c *Container, sessionID string, r *http.Request, w http.Resp
if err := execCmd.Wait(); err != nil {
conmonPipeDataChan <- conmonPipeData{-1, err}
} else {
- pid, err := readConmonPipeData(pipes.syncPipe, ociLog)
+ pid, err := readConmonPipeData(runtimeName, pipes.syncPipe, ociLog)
if err != nil {
hijackWriteError(err, c.ID(), isTerminal, httpBuf)
conmonPipeDataChan <- conmonPipeData{pid, err}
diff --git a/libpod/oci_conmon_linux.go b/libpod/oci_conmon_linux.go
index 5446a8f8a..3440507ed 100644
--- a/libpod/oci_conmon_linux.go
+++ b/libpod/oci_conmon_linux.go
@@ -1262,7 +1262,7 @@ func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *Co
return 0, err
}
- pid, err := readConmonPipeData(parentSyncPipe, ociLog)
+ pid, err := readConmonPipeData(r.name, parentSyncPipe, ociLog)
if err != nil {
if err2 := r.DeleteContainer(ctr); err2 != nil {
logrus.Errorf("Removing container %s from runtime after creation failed", ctr.ID())
@@ -1564,7 +1564,7 @@ func readConmonPidFile(pidFile string) (int, error) {
}
// readConmonPipeData attempts to read a syncInfo struct from the pipe
-func readConmonPipeData(pipe *os.File, ociLog string) (int, error) {
+func readConmonPipeData(runtimeName string, pipe *os.File, ociLog string) (int, error) {
// syncInfo is used to return data from monitor process to daemon
type syncInfo struct {
Data int `json:"data"`
@@ -1600,7 +1600,7 @@ func readConmonPipeData(pipe *os.File, ociLog string) (int, error) {
if err == nil {
var ociErr ociError
if err := json.Unmarshal(ociLogData, &ociErr); err == nil {
- return -1, getOCIRuntimeError(ociErr.Msg)
+ return -1, getOCIRuntimeError(runtimeName, ociErr.Msg)
}
}
}
@@ -1613,13 +1613,13 @@ func readConmonPipeData(pipe *os.File, ociLog string) (int, error) {
if err == nil {
var ociErr ociError
if err := json.Unmarshal(ociLogData, &ociErr); err == nil {
- return ss.si.Data, getOCIRuntimeError(ociErr.Msg)
+ return ss.si.Data, getOCIRuntimeError(runtimeName, ociErr.Msg)
}
}
}
// If we failed to parse the JSON errors, then print the output as it is
if ss.si.Message != "" {
- return ss.si.Data, getOCIRuntimeError(ss.si.Message)
+ return ss.si.Data, getOCIRuntimeError(runtimeName, ss.si.Message)
}
return ss.si.Data, errors.Wrapf(define.ErrInternal, "container create failed")
}
diff --git a/libpod/oci_util.go b/libpod/oci_util.go
index 6d99d5836..06d1dc652 100644
--- a/libpod/oci_util.go
+++ b/libpod/oci_util.go
@@ -136,7 +136,7 @@ func bindPort(protocol, hostIP string, port uint16, isV6 bool, sctpWarning *bool
return file, nil
}
-func getOCIRuntimeError(runtimeMsg string) error {
+func getOCIRuntimeError(name, runtimeMsg string) error {
includeFullOutput := logrus.GetLevel() == logrus.DebugLevel
if match := regexp.MustCompile("(?i).*permission denied.*|.*operation not permitted.*").FindString(runtimeMsg); match != "" {
@@ -144,14 +144,14 @@ func getOCIRuntimeError(runtimeMsg string) error {
if includeFullOutput {
errStr = runtimeMsg
}
- return errors.Wrapf(define.ErrOCIRuntimePermissionDenied, "%s", strings.Trim(errStr, "\n"))
+ return errors.Wrapf(define.ErrOCIRuntimePermissionDenied, "%s: %s", name, strings.Trim(errStr, "\n"))
}
if match := regexp.MustCompile("(?i).*executable file not found in.*|.*no such file or directory.*").FindString(runtimeMsg); match != "" {
errStr := match
if includeFullOutput {
errStr = runtimeMsg
}
- return errors.Wrapf(define.ErrOCIRuntimeNotFound, "%s", strings.Trim(errStr, "\n"))
+ return errors.Wrapf(define.ErrOCIRuntimeNotFound, "%s: %s", name, strings.Trim(errStr, "\n"))
}
if match := regexp.MustCompile("`/proc/[a-z0-9-].+/attr.*`").FindString(runtimeMsg); match != "" {
errStr := match
@@ -159,11 +159,11 @@ func getOCIRuntimeError(runtimeMsg string) error {
errStr = runtimeMsg
}
if strings.HasSuffix(match, "/exec`") {
- return errors.Wrapf(define.ErrSetSecurityAttribute, "%s", strings.Trim(errStr, "\n"))
+ return errors.Wrapf(define.ErrSetSecurityAttribute, "%s: %s", name, strings.Trim(errStr, "\n"))
} else if strings.HasSuffix(match, "/current`") {
- return errors.Wrapf(define.ErrGetSecurityAttribute, "%s", strings.Trim(errStr, "\n"))
+ return errors.Wrapf(define.ErrGetSecurityAttribute, "%s: %s", name, strings.Trim(errStr, "\n"))
}
- return errors.Wrapf(define.ErrSecurityAttribute, "%s", strings.Trim(errStr, "\n"))
+ return errors.Wrapf(define.ErrSecurityAttribute, "%s: %s", name, strings.Trim(errStr, "\n"))
}
- return errors.Wrapf(define.ErrOCIRuntime, "%s", strings.Trim(runtimeMsg, "\n"))
+ return errors.Wrapf(define.ErrOCIRuntime, "%s: %s", name, strings.Trim(runtimeMsg, "\n"))
}
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/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/machine/config.go b/pkg/machine/config.go
index 4f2947ac0..33a352898 100644
--- a/pkg/machine/config.go
+++ b/pkg/machine/config.go
@@ -18,6 +18,8 @@ type InitOptions struct {
DiskSize uint64
IgnitionPath string
ImagePath string
+ Volumes []string
+ VolumeDriver string
IsDefault bool
Memory uint64
Name string
diff --git a/pkg/machine/qemu/config.go b/pkg/machine/qemu/config.go
index 8404079a2..e76509bb1 100644
--- a/pkg/machine/qemu/config.go
+++ b/pkg/machine/qemu/config.go
@@ -11,6 +11,8 @@ type MachineVM struct {
CPUs uint64
// The command line representation of the qemu command
CmdLine []string
+ // Mounts is the list of remote filesystems to mount
+ Mounts []Mount
// IdentityPath is the fq path to the ssh priv key
IdentityPath string
// IgnitionFilePath is the fq path to the .ign file
@@ -33,6 +35,14 @@ type MachineVM struct {
RemoteUsername string
}
+type Mount struct {
+ Type string
+ Tag string
+ Source string
+ Target string
+ ReadOnly bool
+}
+
type Monitor struct {
// Address portion of the qmp monitor (/tmp/tmp.sock)
Address string
diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go
index a80a11573..f09107c71 100644
--- a/pkg/machine/qemu/machine.go
+++ b/pkg/machine/qemu/machine.go
@@ -36,6 +36,11 @@ func GetQemuProvider() machine.Provider {
return qemuProvider
}
+const (
+ VolumeTypeVirtfs = "virtfs"
+ MountType9p = "9p"
+)
+
// NewMachine initializes an instance of a virtual machine based on the qemu
// virtualization.
func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) {
@@ -167,6 +172,53 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) {
// Add arch specific options including image location
v.CmdLine = append(v.CmdLine, v.addArchOptions()...)
+ var volumeType string
+ switch opts.VolumeDriver {
+ case "virtfs":
+ volumeType = VolumeTypeVirtfs
+ case "": // default driver
+ volumeType = VolumeTypeVirtfs
+ default:
+ err := fmt.Errorf("unknown volume driver: %s", opts.VolumeDriver)
+ return false, err
+ }
+
+ mounts := []Mount{}
+ for i, volume := range opts.Volumes {
+ tag := fmt.Sprintf("vol%d", i)
+ paths := strings.SplitN(volume, ":", 3)
+ source := paths[0]
+ target := source
+ readonly := false
+ if len(paths) > 1 {
+ target = paths[1]
+ }
+ if len(paths) > 2 {
+ options := paths[2]
+ volopts := strings.Split(options, ",")
+ for _, o := range volopts {
+ switch o {
+ case "rw":
+ readonly = false
+ case "ro":
+ readonly = true
+ default:
+ fmt.Printf("Unknown option: %s\n", o)
+ }
+ }
+ }
+ switch volumeType {
+ case VolumeTypeVirtfs:
+ virtfsOptions := fmt.Sprintf("local,path=%s,mount_tag=%s,security_model=mapped-xattr", source, tag)
+ if readonly {
+ virtfsOptions += ",readonly"
+ }
+ v.CmdLine = append(v.CmdLine, []string{"-virtfs", virtfsOptions}...)
+ mounts = append(mounts, Mount{Type: MountType9p, Tag: tag, Source: source, Target: target, ReadOnly: readonly})
+ }
+ }
+ v.Mounts = mounts
+
// Add location of bootable image
v.CmdLine = append(v.CmdLine, "-drive", "if=virtio,file="+v.ImagePath)
// This kind of stinks but no other way around this r/n
@@ -329,7 +381,39 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
return err
}
_, err = bufio.NewReader(conn).ReadString('\n')
- return err
+ if err != nil {
+ return err
+ }
+
+ if len(v.Mounts) > 0 {
+ for !v.isRunning() || !v.isListening() {
+ time.Sleep(100 * time.Millisecond)
+ }
+ }
+ for _, mount := range v.Mounts {
+ fmt.Printf("Mounting volume... %s:%s\n", mount.Source, mount.Target)
+ // create mountpoint directory if it doesn't exist
+ err = v.SSH(name, machine.SSHOptions{Args: []string{"-q", "--", "sudo", "mkdir", "-p", mount.Target}})
+ if err != nil {
+ return err
+ }
+ switch mount.Type {
+ case MountType9p:
+ mountOptions := []string{"-t", "9p"}
+ mountOptions = append(mountOptions, []string{"-o", "trans=virtio", mount.Tag, mount.Target}...)
+ mountOptions = append(mountOptions, []string{"-o", "version=9p2000.L,msize=131072"}...)
+ if mount.ReadOnly {
+ mountOptions = append(mountOptions, []string{"-o", "ro"}...)
+ }
+ err = v.SSH(name, machine.SSHOptions{Args: append([]string{"-q", "--", "sudo", "mount"}, mountOptions...)})
+ if err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("unknown mount type: %s", mount.Type)
+ }
+ }
+ return nil
}
// Stop uses the qmp monitor to call a system_powerdown
@@ -506,6 +590,16 @@ func (v *MachineVM) isRunning() bool {
return true
}
+func (v *MachineVM) isListening() bool {
+ // Check if we can dial it
+ conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", "localhost", v.Port), 10*time.Millisecond)
+ if err != nil {
+ return false
+ }
+ conn.Close()
+ return true
+}
+
// SSH opens an interactive SSH session to the vm specified.
// Added ssh function to VM interface: pkg/machine/config/go : line 58
func (v *MachineVM) SSH(name string, opts machine.SSHOptions) error {
diff --git a/pkg/specgen/generate/container.go b/pkg/specgen/generate/container.go
index 57676db10..5ec7c7b03 100644
--- a/pkg/specgen/generate/container.go
+++ b/pkg/specgen/generate/container.go
@@ -4,6 +4,7 @@ import (
"context"
"os"
"strings"
+ "time"
"github.com/containers/common/libimage"
"github.com/containers/podman/v3/libpod"
@@ -64,6 +65,13 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat
// NOTE: the health check is only set for Docker images
// but inspect will take care of it.
s.HealthConfig = inspectData.HealthCheck
+ if s.HealthConfig != nil && s.HealthConfig.Timeout == 0 {
+ hct, err := time.ParseDuration(define.DefaultHealthCheckTimeout)
+ if err != nil {
+ return nil, err
+ }
+ s.HealthConfig.Timeout = hct
+ }
}
// Image stop signal
diff --git a/test/e2e/healthcheck_run_test.go b/test/e2e/healthcheck_run_test.go
index c2084a6fd..6a79006b6 100644
--- a/test/e2e/healthcheck_run_test.go
+++ b/test/e2e/healthcheck_run_test.go
@@ -2,7 +2,9 @@ package integration
import (
"fmt"
+ "io/ioutil"
"os"
+ "path/filepath"
"time"
define "github.com/containers/podman/v3/libpod/define"
@@ -258,4 +260,40 @@ var _ = Describe("Podman healthcheck run", func() {
Expect(startAgain.OutputToString()).To(Equal("hc"))
Expect(startAgain.ErrorToString()).To(Equal(""))
})
+
+ It("Verify default time is used and no utf-8 escapes", func() {
+ cwd, err := os.Getwd()
+ Expect(err).To(BeNil())
+
+ podmanTest.AddImageToRWStore(ALPINE)
+ // Write target and fake files
+ targetPath, err := CreateTempDirInTempDir()
+ Expect(err).To(BeNil())
+ containerfile := fmt.Sprintf(`FROM %s
+HEALTHCHECK CMD ls -l / 2>&1`, ALPINE)
+ containerfilePath := filepath.Join(targetPath, "Containerfile")
+ err = ioutil.WriteFile(containerfilePath, []byte(containerfile), 0644)
+ Expect(err).To(BeNil())
+ defer func() {
+ Expect(os.Chdir(cwd)).To(BeNil())
+ Expect(os.RemoveAll(targetPath)).To(BeNil())
+ }()
+
+ // make cwd as context root path
+ Expect(os.Chdir(targetPath)).To(BeNil())
+
+ session := podmanTest.Podman([]string{"build", "--format", "docker", "-t", "test", "."})
+ session.WaitWithDefaultTimeout()
+ Expect(session).Should(Exit(0))
+
+ run := podmanTest.Podman([]string{"run", "-dt", "--name", "hctest", "test", "ls"})
+ run.WaitWithDefaultTimeout()
+ Expect(run).Should(Exit(0))
+
+ inspect := podmanTest.InspectContainer("hctest")
+ // Check to make sure a default time value was added
+ Expect(inspect[0].Config.Healthcheck.Timeout).To(BeNumerically("==", 30000000000))
+ // Check to make sure characters were not coerced to utf8
+ Expect(inspect[0].Config.Healthcheck.Test).To(Equal([]string{"CMD-SHELL", "ls -l / 2>&1"}))
+ })
})
diff --git a/test/system/030-run.bats b/test/system/030-run.bats
index d81a0758c..317026710 100644
--- a/test/system/030-run.bats
+++ b/test/system/030-run.bats
@@ -14,8 +14,8 @@ load helpers
# ...but check the configured runtime engine, and switch to crun as needed
run_podman info --format '{{ .Host.OCIRuntime.Path }}'
if expr "$output" : ".*/crun"; then
- err_no_such_cmd="Error: executable file.* not found in \$PATH: No such file or directory: OCI runtime attempted to invoke a command that was not found"
- err_no_exec_dir="Error: open executable: Operation not permitted: OCI permission denied"
+ err_no_such_cmd="Error:.*executable file.* not found in \$PATH: No such file or directory: OCI runtime attempted to invoke a command that was not found"
+ err_no_exec_dir="Error:.*open executable: Operation not permitted: OCI permission denied"
fi
tests="