diff --git a/hack/PACKAGERS.md b/hack/PACKAGERS.md index 57ec45fd81..11d4698f3e 100644 --- a/hack/PACKAGERS.md +++ b/hack/PACKAGERS.md @@ -90,6 +90,16 @@ You would do the users of your distro a disservice and "void the docker warranty A good comparison is Busybox: all distros package it as a statically linked binary, because it just makes sense. Docker is the same way. +If you *must* have a non-static Docker binary, please use: + +```bash +./hack/make.sh dynbinary +``` + +This will create *./bundles/$VERSION/dynbinary/docker-$VERSION* and *./bundles/$VERSION/binary/dockerinit-$VERSION*. +The first of these would usually be installed at */usr/bin/docker*, while the second must be installed +at */usr/libexec/docker/dockerinit*. + ## Testing Docker Before releasing your binary, make sure to run the tests! Run the following command with the source diff --git a/hack/make.sh b/hack/make.sh index dd2a96945c..7e81417e46 100755 --- a/hack/make.sh +++ b/hack/make.sh @@ -35,6 +35,8 @@ grep -q "$RESOLVCONF" /proc/mounts || { DEFAULT_BUNDLES=( binary test + dynbinary + dyntest ubuntu ) @@ -46,6 +48,7 @@ fi # Use these flags when compiling the tests and final binary LDFLAGS='-X main.GITCOMMIT "'$GITCOMMIT'" -X main.VERSION "'$VERSION'" -w' +LDFLAGS_STATIC='-X github.com/dotcloud/docker/utils.IAMSTATIC true -linkmode external -extldflags "-lpthread -static -Wl,--unresolved-symbols=ignore-in-object-files"' BUILDFLAGS='-tags netgo' bundle() { diff --git a/hack/make/binary b/hack/make/binary index 4bc0effc43..6363c2ab09 100644 --- a/hack/make/binary +++ b/hack/make/binary @@ -2,5 +2,5 @@ DEST=$1 -go build -o $DEST/docker-$VERSION -ldflags "$LDFLAGS -linkmode external -extldflags \"-lpthread -static -Wl,--unresolved-symbols=ignore-in-object-files\"" $BUILDFLAGS ./docker +go build -o $DEST/docker-$VERSION -ldflags "$LDFLAGS $LDFLAGS_STATIC" $BUILDFLAGS ./docker echo "Created binary: $DEST/docker-$VERSION" diff --git a/hack/make/dynbinary b/hack/make/dynbinary new file mode 100644 index 0000000000..e630c94f53 --- /dev/null +++ b/hack/make/dynbinary @@ -0,0 +1,15 @@ +#!/bin/sh + +DEST=$1 + +# dockerinit still needs to be a static binary, even if docker is dynamic +CGO_ENABLED=0 go build -a -o $DEST/dockerinit-$VERSION -ldflags "$LDFLAGS -d" $BUILDFLAGS ./dockerinit +echo "Created binary: $DEST/dockerinit-$VERSION" +ln -sf dockerinit-$VERSION $DEST/dockerinit + +# sha1 our new dockerinit to ensure separate docker and dockerinit always run in a perfect pair compiled for one another +export DOCKER_INITSHA1="$(sha1sum $DEST/dockerinit-$VERSION | cut -d' ' -f1)" +# exported so that "dyntest" can easily access it later without recalculating it + +go build -o $DEST/docker-$VERSION -ldflags "$LDFLAGS -X github.com/dotcloud/docker/utils.INITSHA1 \"$DOCKER_INITSHA1\"" $BUILDFLAGS ./docker +echo "Created binary: $DEST/docker-$VERSION" diff --git a/hack/make/dyntest b/hack/make/dyntest new file mode 100644 index 0000000000..848ed8b5cc --- /dev/null +++ b/hack/make/dyntest @@ -0,0 +1,41 @@ +#!/bin/sh + +DEST=$1 +INIT=$DEST/../dynbinary/dockerinit-$VERSION + +set -e + +if [ ! -x "$INIT" ]; then + echo >&2 'error: dynbinary must be run before dyntest' + false +fi + +# Run Docker's test suite, including sub-packages, and store their output as a bundle +# If $TESTFLAGS is set in the environment, it is passed as extra arguments to 'go test'. +# You can use this to select certain tests to run, eg. +# +# TESTFLAGS='-run ^TestBuild$' ./hack/make.sh test +# +bundle_test() { + { + date + for test_dir in $(find_test_dirs); do ( + set -x + cd $test_dir + export TEST_DOCKERINIT_PATH=$DEST/../dynbinary/dockerinit-$VERSION + go test -v -ldflags "$LDFLAGS -X github.com/dotcloud/docker/utils.INITSHA1 \"$DOCKER_INITSHA1\"" $BUILDFLAGS + ) done + } 2>&1 | tee $DEST/test.log +} + + +# This helper function walks the current directory looking for directories +# holding Go test files, and prints their paths on standard output, one per +# line. +find_test_dirs() { + find . -name '*_test.go' | grep -v '^./vendor' | + { while read f; do dirname $f; done; } | + sort -u +} + +bundle_test diff --git a/hack/make/test b/hack/make/test index eab131f71d..f905fa14a5 100644 --- a/hack/make/test +++ b/hack/make/test @@ -17,7 +17,7 @@ bundle_test() { set -x cd $test_dir go test -i - go test -v -ldflags "$LDFLAGS -linkmode external -extldflags \"-lpthread -static -Wl,--unresolved-symbols=ignore-in-object-files\"" $BUILDFLAGS $TESTFLAGS + go test -v -ldflags "$LDFLAGS $LDFLAGS_STATIC" $BUILDFLAGS $TESTFLAGS ) done } 2>&1 | tee $DEST/test.log } diff --git a/runtime.go b/runtime.go index 25f378bfb6..0a492cd77e 100644 --- a/runtime.go +++ b/runtime.go @@ -38,12 +38,6 @@ type Runtime struct { containerGraph *gograph.Database } -var sysInitPath string - -func init() { - sysInitPath = utils.SelfPath() -} - // List returns an array of all containers registered in the runtime. func (runtime *Runtime) List() []*Container { containers := new(History) @@ -335,6 +329,11 @@ func (runtime *Runtime) Create(config *Config) (*Container, []string, error) { return nil, nil, fmt.Errorf("No command specified") } + sysInitPath := utils.DockerInitPath() + if sysInitPath == "" { + return nil, nil, fmt.Errorf("Could not locate dockerinit: This usually means docker was built incorrectly. See http://docs.docker.io/en/latest/contributing/devenvironment for official build instructions.") + } + // Generate id id := GenerateID() diff --git a/runtime_test.go b/runtime_test.go index e042ae6e81..0aa2d3d543 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -9,6 +9,7 @@ import ( "log" "net" "os" + "path/filepath" "runtime" "strconv" "strings" @@ -86,6 +87,25 @@ func init() { log.Fatal("docker tests need to be run as root") } + // Copy dockerinit into our current testing directory, if provided (so we can test a separate dockerinit binary) + if dockerinit := os.Getenv("TEST_DOCKERINIT_PATH"); dockerinit != "" { + src, err := os.Open(dockerinit) + if err != nil { + log.Fatalf("Unable to open TEST_DOCKERINIT_PATH: %s\n", err) + } + defer src.Close() + dst, err := os.OpenFile(filepath.Join(filepath.Dir(utils.SelfPath()), "dockerinit"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0555) + if err != nil { + log.Fatalf("Unable to create dockerinit in test directory: %s\n", err) + } + defer dst.Close() + if _, err := io.Copy(dst, src); err != nil { + log.Fatalf("Unable to copy dockerinit to TEST_DOCKERINIT_PATH: %s\n", err) + } + dst.Close() + src.Close() + } + // Setup the base runtime, which will be duplicated for each test. // (no tests are run directly in the base) setupBaseImage() diff --git a/utils/utils.go b/utils/utils.go index 3bf2cf05be..d53094397f 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -2,6 +2,7 @@ package utils import ( "bytes" + "crypto/sha1" "crypto/sha256" "encoding/hex" "encoding/json" @@ -21,6 +22,11 @@ import ( "time" ) +var ( + IAMSTATIC bool // whether or not Docker itself was compiled statically via ./hack/make.sh binary + INITSHA1 string // sha1sum of separate static dockerinit, if Docker itself was compiled dynamically via ./hack/make.sh dynbinary +) + // ListOpts type type ListOpts []string @@ -190,6 +196,67 @@ func SelfPath() string { return path } +func dockerInitSha1(target string) string { + f, err := os.Open(target) + if err != nil { + return "" + } + defer f.Close() + h := sha1.New() + _, err = io.Copy(h, f) + if err != nil { + return "" + } + return hex.EncodeToString(h.Sum(nil)) +} + +func isValidDockerInitPath(target string, selfPath string) bool { // target and selfPath should be absolute (InitPath and SelfPath already do this) + if IAMSTATIC { + if target == selfPath { + return true + } + targetFileInfo, err := os.Lstat(target) + if err != nil { + return false + } + selfPathFileInfo, err := os.Lstat(selfPath) + if err != nil { + return false + } + return os.SameFile(targetFileInfo, selfPathFileInfo) + } + return INITSHA1 != "" && dockerInitSha1(target) == INITSHA1 +} + +// Figure out the path of our dockerinit (which may be SelfPath()) +func DockerInitPath() string { + selfPath := SelfPath() + if isValidDockerInitPath(selfPath, selfPath) { + // if we're valid, don't bother checking anything else + return selfPath + } + var possibleInits = []string{ + filepath.Join(filepath.Dir(selfPath), "dockerinit"), + // "/usr/libexec includes internal binaries that are not intended to be executed directly by users or shell scripts. Applications may use a single subdirectory under /usr/libexec." + "/usr/libexec/docker/dockerinit", + "/usr/local/libexec/docker/dockerinit", + } + for _, dockerInit := range possibleInits { + path, err := exec.LookPath(dockerInit) + if err == nil { + path, err = filepath.Abs(path) + if err != nil { + // LookPath already validated that this file exists and is executable (following symlinks), so how could Abs fail? + panic(err) + } + if isValidDockerInitPath(path, selfPath) { + return path + } + } + } + return "" +} + type NopWriter struct{} func (*NopWriter) Write(buf []byte) (int, error) {