diff options
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=" |