20833b06a0
Signed-off-by: John Howard <jhoward@microsoft.com> Also fixes https://github.com/moby/moby/issues/22874 This commit is a pre-requisite to moving moby/moby on Windows to using Containerd for its runtime. The reason for this is that the interface between moby and containerd for the runtime is an OCI spec which must be unambigious. It is the responsibility of the runtime (runhcs in the case of containerd on Windows) to ensure that arguments are escaped prior to calling into HCS and onwards to the Win32 CreateProcess call. Previously, the builder was always escaping arguments which has led to several bugs in moby. Because the local runtime in libcontainerd had context of whether or not arguments were escaped, it was possible to hack around in daemon/oci_windows.go with knowledge of the context of the call (from builder or not). With a remote runtime, this is not possible as there's rightly no context of the caller passed across in the OCI spec. Put another way, as I put above, the OCI spec must be unambigious. The other previous limitation (which leads to various subtle bugs) is that moby is coded entirely from a Linux-centric point of view. Unfortunately, Windows != Linux. Windows CreateProcess uses a command line, not an array of arguments. And it has very specific rules about how to escape a command line. Some interesting reading links about this are: https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ https://stackoverflow.com/questions/31838469/how-do-i-convert-argv-to-lpcommandline-parameter-of-createprocess https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments?view=vs-2017 For this reason, the OCI spec has recently been updated to cater for more natural syntax by including a CommandLine option in Process. What does this commit do? Primary objective is to ensure that the built OCI spec is unambigious. It changes the builder so that `ArgsEscaped` as commited in a layer is only controlled by the use of CMD or ENTRYPOINT. Subsequently, when calling in to create a container from the builder, if follows a different path to both `docker run` and `docker create` using the added `ContainerCreateIgnoreImagesArgsEscaped`. This allows a RUN from the builder to control how to escape in the OCI spec. It changes the builder so that when shell form is used for RUN, CMD or ENTRYPOINT, it builds (for WCOW) a more natural command line using the original as put by the user in the dockerfile, not the parsed version as a set of args which loses fidelity. This command line is put into args[0] and `ArgsEscaped` is set to true for CMD or ENTRYPOINT. A RUN statement does not commit `ArgsEscaped` to the commited layer regardless or whether shell or exec form were used.
141 lines
5.9 KiB
Go
141 lines
5.9 KiB
Go
package dockerfile // import "github.com/docker/docker/builder/dockerfile"
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/pkg/system"
|
|
"github.com/moby/buildkit/frontend/dockerfile/instructions"
|
|
)
|
|
|
|
var pattern = regexp.MustCompile(`^[a-zA-Z]:\.$`)
|
|
|
|
// normalizeWorkdir normalizes a user requested working directory in a
|
|
// platform semantically consistent way.
|
|
func normalizeWorkdir(platform string, current string, requested string) (string, error) {
|
|
if platform == "" {
|
|
platform = "windows"
|
|
}
|
|
if platform == "windows" {
|
|
return normalizeWorkdirWindows(current, requested)
|
|
}
|
|
return normalizeWorkdirUnix(current, requested)
|
|
}
|
|
|
|
// normalizeWorkdirUnix normalizes a user requested working directory in a
|
|
// platform semantically consistent way.
|
|
func normalizeWorkdirUnix(current string, requested string) (string, error) {
|
|
if requested == "" {
|
|
return "", errors.New("cannot normalize nothing")
|
|
}
|
|
current = strings.Replace(current, string(os.PathSeparator), "/", -1)
|
|
requested = strings.Replace(requested, string(os.PathSeparator), "/", -1)
|
|
if !path.IsAbs(requested) {
|
|
return path.Join(`/`, current, requested), nil
|
|
}
|
|
return requested, nil
|
|
}
|
|
|
|
// normalizeWorkdirWindows normalizes a user requested working directory in a
|
|
// platform semantically consistent way.
|
|
func normalizeWorkdirWindows(current string, requested string) (string, error) {
|
|
if requested == "" {
|
|
return "", errors.New("cannot normalize nothing")
|
|
}
|
|
|
|
// `filepath.Clean` will replace "" with "." so skip in that case
|
|
if current != "" {
|
|
current = filepath.Clean(current)
|
|
}
|
|
if requested != "" {
|
|
requested = filepath.Clean(requested)
|
|
}
|
|
|
|
// If either current or requested in Windows is:
|
|
// C:
|
|
// C:.
|
|
// then an error will be thrown as the definition for the above
|
|
// refers to `current directory on drive C:`
|
|
// Since filepath.Clean() will automatically normalize the above
|
|
// to `C:.`, we only need to check the last format
|
|
if pattern.MatchString(current) {
|
|
return "", fmt.Errorf("%s is not a directory. If you are specifying a drive letter, please add a trailing '\\'", current)
|
|
}
|
|
if pattern.MatchString(requested) {
|
|
return "", fmt.Errorf("%s is not a directory. If you are specifying a drive letter, please add a trailing '\\'", requested)
|
|
}
|
|
|
|
// Target semantics is C:\somefolder, specifically in the format:
|
|
// UPPERCASEDriveLetter-Colon-Backslash-FolderName. We are already
|
|
// guaranteed that `current`, if set, is consistent. This allows us to
|
|
// cope correctly with any of the following in a Dockerfile:
|
|
// WORKDIR a --> C:\a
|
|
// WORKDIR c:\\foo --> C:\foo
|
|
// WORKDIR \\foo --> C:\foo
|
|
// WORKDIR /foo --> C:\foo
|
|
// WORKDIR c:\\foo \ WORKDIR bar --> C:\foo --> C:\foo\bar
|
|
// WORKDIR C:/foo \ WORKDIR bar --> C:\foo --> C:\foo\bar
|
|
// WORKDIR C:/foo \ WORKDIR \\bar --> C:\foo --> C:\bar
|
|
// WORKDIR /foo \ WORKDIR c:/bar --> C:\foo --> C:\bar
|
|
if len(current) == 0 || system.IsAbs(requested) {
|
|
if (requested[0] == os.PathSeparator) ||
|
|
(len(requested) > 1 && string(requested[1]) != ":") ||
|
|
(len(requested) == 1) {
|
|
requested = filepath.Join(`C:\`, requested)
|
|
}
|
|
} else {
|
|
requested = filepath.Join(current, requested)
|
|
}
|
|
// Upper-case drive letter
|
|
return (strings.ToUpper(string(requested[0])) + requested[1:]), nil
|
|
}
|
|
|
|
// resolveCmdLine takes a command line arg set and optionally prepends a platform-specific
|
|
// shell in front of it. It returns either an array of arguments and an indication that
|
|
// the arguments are not yet escaped; Or, an array containing a single command line element
|
|
// along with an indication that the arguments are escaped so the runtime shouldn't escape.
|
|
//
|
|
// A better solution could be made, but it would be exceptionally invasive throughout
|
|
// many parts of the daemon which are coded assuming Linux args array only only, not taking
|
|
// account of Windows-natural command line semantics and it's argv handling. Put another way,
|
|
// while what is here is good-enough, it could be improved, but would be highly invasive.
|
|
//
|
|
// The commands when this function is called are RUN, ENTRYPOINT and CMD.
|
|
func resolveCmdLine(cmd instructions.ShellDependantCmdLine, runConfig *container.Config, os, command, original string) ([]string, bool) {
|
|
|
|
// Make sure we return an empty array if there is no cmd.CmdLine
|
|
if len(cmd.CmdLine) == 0 {
|
|
return []string{}, runConfig.ArgsEscaped
|
|
}
|
|
|
|
if os == "windows" { // ie WCOW
|
|
if cmd.PrependShell {
|
|
// WCOW shell-form. Return a single-element array containing the original command line prepended with the shell.
|
|
// Also indicate that it has not been escaped (so will be passed through directly to HCS). Note that
|
|
// we go back to the original un-parsed command line in the dockerfile line, strip off both the command part of
|
|
// it (RUN/ENTRYPOINT/CMD), and also strip any leading white space. IOW, we deliberately ignore any prior parsing
|
|
// so as to ensure it is treated exactly as a command line. For those interested, `RUN mkdir "c:/foo"` is a particularly
|
|
// good example of why this is necessary if you fancy debugging how cmd.exe and its builtin mkdir works. (Windows
|
|
// doesn't have a mkdir.exe, and I'm guessing cmd.exe has some very long unavoidable and unchangeable historical
|
|
// design decisions over how both its built-in echo and mkdir are coded. Probably more too.)
|
|
original = original[len(command):] // Strip off the command
|
|
original = strings.TrimLeft(original, " \t\v\n") // Strip of leading whitespace
|
|
return []string{strings.Join(getShell(runConfig, os), " ") + " " + original}, true
|
|
}
|
|
|
|
// WCOW JSON/"exec" form.
|
|
return cmd.CmdLine, false
|
|
}
|
|
|
|
// LCOW - use args as an array, same as LCOL.
|
|
if cmd.PrependShell && cmd.CmdLine != nil {
|
|
return append(getShell(runConfig, os), cmd.CmdLine...), false
|
|
}
|
|
return cmd.CmdLine, false
|
|
}
|