After a nice long brainstorming session with @shykes on IRC, we decided on using a SHA1 hash of dockerinit compiled into the dynamic docker binary to ensure that we always use the two in a perfect pair, and never mix and match.
| ... | ... |
@@ -90,6 +90,16 @@ You would do the users of your distro a disservice and "void the docker warranty |
| 90 | 90 |
A good comparison is Busybox: all distros package it as a statically linked binary, because it just |
| 91 | 91 |
makes sense. Docker is the same way. |
| 92 | 92 |
|
| 93 |
+If you *must* have a non-static Docker binary, please use: |
|
| 94 |
+ |
|
| 95 |
+```bash |
|
| 96 |
+./hack/make.sh dynbinary |
|
| 97 |
+``` |
|
| 98 |
+ |
|
| 99 |
+This will create *./bundles/$VERSION/dynbinary/docker-$VERSION* and *./bundles/$VERSION/binary/dockerinit-$VERSION*. |
|
| 100 |
+The first of these would usually be installed at */usr/bin/docker*, while the second must be installed |
|
| 101 |
+at */usr/libexec/docker/dockerinit*. |
|
| 102 |
+ |
|
| 93 | 103 |
## Testing Docker |
| 94 | 104 |
|
| 95 | 105 |
Before releasing your binary, make sure to run the tests! Run the following command with the source |
| ... | ... |
@@ -35,6 +35,8 @@ grep -q "$RESOLVCONF" /proc/mounts || {
|
| 35 | 35 |
DEFAULT_BUNDLES=( |
| 36 | 36 |
binary |
| 37 | 37 |
test |
| 38 |
+ dynbinary |
|
| 39 |
+ dyntest |
|
| 38 | 40 |
ubuntu |
| 39 | 41 |
) |
| 40 | 42 |
|
| ... | ... |
@@ -46,6 +48,7 @@ fi |
| 46 | 46 |
|
| 47 | 47 |
# Use these flags when compiling the tests and final binary |
| 48 | 48 |
LDFLAGS='-X main.GITCOMMIT "'$GITCOMMIT'" -X main.VERSION "'$VERSION'" -w' |
| 49 |
+LDFLAGS_STATIC='-X github.com/dotcloud/docker/utils.IAMSTATIC true -linkmode external -extldflags "-lpthread -static -Wl,--unresolved-symbols=ignore-in-object-files"' |
|
| 49 | 50 |
BUILDFLAGS='-tags netgo' |
| 50 | 51 |
|
| 51 | 52 |
bundle() {
|
| ... | ... |
@@ -2,5 +2,5 @@ |
| 2 | 2 |
|
| 3 | 3 |
DEST=$1 |
| 4 | 4 |
|
| 5 |
-go build -o $DEST/docker-$VERSION -ldflags "$LDFLAGS -linkmode external -extldflags \"-lpthread -static -Wl,--unresolved-symbols=ignore-in-object-files\"" $BUILDFLAGS ./docker |
|
| 5 |
+go build -o $DEST/docker-$VERSION -ldflags "$LDFLAGS $LDFLAGS_STATIC" $BUILDFLAGS ./docker |
|
| 6 | 6 |
echo "Created binary: $DEST/docker-$VERSION" |
| 7 | 7 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,15 @@ |
| 0 |
+#!/bin/sh |
|
| 1 |
+ |
|
| 2 |
+DEST=$1 |
|
| 3 |
+ |
|
| 4 |
+# dockerinit still needs to be a static binary, even if docker is dynamic |
|
| 5 |
+CGO_ENABLED=0 go build -a -o $DEST/dockerinit-$VERSION -ldflags "$LDFLAGS -d" $BUILDFLAGS ./dockerinit |
|
| 6 |
+echo "Created binary: $DEST/dockerinit-$VERSION" |
|
| 7 |
+ln -sf dockerinit-$VERSION $DEST/dockerinit |
|
| 8 |
+ |
|
| 9 |
+# sha1 our new dockerinit to ensure separate docker and dockerinit always run in a perfect pair compiled for one another |
|
| 10 |
+export DOCKER_INITSHA1="$(sha1sum $DEST/dockerinit-$VERSION | cut -d' ' -f1)" |
|
| 11 |
+# exported so that "dyntest" can easily access it later without recalculating it |
|
| 12 |
+ |
|
| 13 |
+go build -o $DEST/docker-$VERSION -ldflags "$LDFLAGS -X github.com/dotcloud/docker/utils.INITSHA1 \"$DOCKER_INITSHA1\"" $BUILDFLAGS ./docker |
|
| 14 |
+echo "Created binary: $DEST/docker-$VERSION" |
| 0 | 15 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,41 @@ |
| 0 |
+#!/bin/sh |
|
| 1 |
+ |
|
| 2 |
+DEST=$1 |
|
| 3 |
+INIT=$DEST/../dynbinary/dockerinit-$VERSION |
|
| 4 |
+ |
|
| 5 |
+set -e |
|
| 6 |
+ |
|
| 7 |
+if [ ! -x "$INIT" ]; then |
|
| 8 |
+ echo >&2 'error: dynbinary must be run before dyntest' |
|
| 9 |
+ false |
|
| 10 |
+fi |
|
| 11 |
+ |
|
| 12 |
+# Run Docker's test suite, including sub-packages, and store their output as a bundle |
|
| 13 |
+# If $TESTFLAGS is set in the environment, it is passed as extra arguments to 'go test'. |
|
| 14 |
+# You can use this to select certain tests to run, eg. |
|
| 15 |
+# |
|
| 16 |
+# TESTFLAGS='-run ^TestBuild$' ./hack/make.sh test |
|
| 17 |
+# |
|
| 18 |
+bundle_test() {
|
|
| 19 |
+ {
|
|
| 20 |
+ date |
|
| 21 |
+ for test_dir in $(find_test_dirs); do ( |
|
| 22 |
+ set -x |
|
| 23 |
+ cd $test_dir |
|
| 24 |
+ export TEST_DOCKERINIT_PATH=$DEST/../dynbinary/dockerinit-$VERSION |
|
| 25 |
+ go test -v -ldflags "$LDFLAGS -X github.com/dotcloud/docker/utils.INITSHA1 \"$DOCKER_INITSHA1\"" $BUILDFLAGS |
|
| 26 |
+ ) done |
|
| 27 |
+ } 2>&1 | tee $DEST/test.log |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+ |
|
| 31 |
+# This helper function walks the current directory looking for directories |
|
| 32 |
+# holding Go test files, and prints their paths on standard output, one per |
|
| 33 |
+# line. |
|
| 34 |
+find_test_dirs() {
|
|
| 35 |
+ find . -name '*_test.go' | grep -v '^./vendor' | |
|
| 36 |
+ { while read f; do dirname $f; done; } |
|
|
| 37 |
+ sort -u |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+bundle_test |
| ... | ... |
@@ -17,7 +17,7 @@ bundle_test() {
|
| 17 | 17 |
set -x |
| 18 | 18 |
cd $test_dir |
| 19 | 19 |
go test -i |
| 20 |
- go test -v -ldflags "$LDFLAGS -linkmode external -extldflags \"-lpthread -static -Wl,--unresolved-symbols=ignore-in-object-files\"" $BUILDFLAGS $TESTFLAGS |
|
| 20 |
+ go test -v -ldflags "$LDFLAGS $LDFLAGS_STATIC" $BUILDFLAGS $TESTFLAGS |
|
| 21 | 21 |
) done |
| 22 | 22 |
} 2>&1 | tee $DEST/test.log |
| 23 | 23 |
} |
| ... | ... |
@@ -38,12 +38,6 @@ type Runtime struct {
|
| 38 | 38 |
containerGraph *gograph.Database |
| 39 | 39 |
} |
| 40 | 40 |
|
| 41 |
-var sysInitPath string |
|
| 42 |
- |
|
| 43 |
-func init() {
|
|
| 44 |
- sysInitPath = utils.SelfPath() |
|
| 45 |
-} |
|
| 46 |
- |
|
| 47 | 41 |
// List returns an array of all containers registered in the runtime. |
| 48 | 42 |
func (runtime *Runtime) List() []*Container {
|
| 49 | 43 |
containers := new(History) |
| ... | ... |
@@ -335,6 +329,11 @@ func (runtime *Runtime) Create(config *Config) (*Container, []string, error) {
|
| 335 | 335 |
return nil, nil, fmt.Errorf("No command specified")
|
| 336 | 336 |
} |
| 337 | 337 |
|
| 338 |
+ sysInitPath := utils.DockerInitPath() |
|
| 339 |
+ if sysInitPath == "" {
|
|
| 340 |
+ 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.")
|
|
| 341 |
+ } |
|
| 342 |
+ |
|
| 338 | 343 |
// Generate id |
| 339 | 344 |
id := GenerateID() |
| 340 | 345 |
|
| ... | ... |
@@ -9,6 +9,7 @@ import ( |
| 9 | 9 |
"log" |
| 10 | 10 |
"net" |
| 11 | 11 |
"os" |
| 12 |
+ "path/filepath" |
|
| 12 | 13 |
"runtime" |
| 13 | 14 |
"strconv" |
| 14 | 15 |
"strings" |
| ... | ... |
@@ -86,6 +87,25 @@ func init() {
|
| 86 | 86 |
log.Fatal("docker tests need to be run as root")
|
| 87 | 87 |
} |
| 88 | 88 |
|
| 89 |
+ // Copy dockerinit into our current testing directory, if provided (so we can test a separate dockerinit binary) |
|
| 90 |
+ if dockerinit := os.Getenv("TEST_DOCKERINIT_PATH"); dockerinit != "" {
|
|
| 91 |
+ src, err := os.Open(dockerinit) |
|
| 92 |
+ if err != nil {
|
|
| 93 |
+ log.Fatalf("Unable to open TEST_DOCKERINIT_PATH: %s\n", err)
|
|
| 94 |
+ } |
|
| 95 |
+ defer src.Close() |
|
| 96 |
+ dst, err := os.OpenFile(filepath.Join(filepath.Dir(utils.SelfPath()), "dockerinit"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0555) |
|
| 97 |
+ if err != nil {
|
|
| 98 |
+ log.Fatalf("Unable to create dockerinit in test directory: %s\n", err)
|
|
| 99 |
+ } |
|
| 100 |
+ defer dst.Close() |
|
| 101 |
+ if _, err := io.Copy(dst, src); err != nil {
|
|
| 102 |
+ log.Fatalf("Unable to copy dockerinit to TEST_DOCKERINIT_PATH: %s\n", err)
|
|
| 103 |
+ } |
|
| 104 |
+ dst.Close() |
|
| 105 |
+ src.Close() |
|
| 106 |
+ } |
|
| 107 |
+ |
|
| 89 | 108 |
// Setup the base runtime, which will be duplicated for each test. |
| 90 | 109 |
// (no tests are run directly in the base) |
| 91 | 110 |
setupBaseImage() |
| ... | ... |
@@ -2,6 +2,7 @@ package utils |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"bytes" |
| 5 |
+ "crypto/sha1" |
|
| 5 | 6 |
"crypto/sha256" |
| 6 | 7 |
"encoding/hex" |
| 7 | 8 |
"encoding/json" |
| ... | ... |
@@ -21,6 +22,11 @@ import ( |
| 21 | 21 |
"time" |
| 22 | 22 |
) |
| 23 | 23 |
|
| 24 |
+var ( |
|
| 25 |
+ IAMSTATIC bool // whether or not Docker itself was compiled statically via ./hack/make.sh binary |
|
| 26 |
+ INITSHA1 string // sha1sum of separate static dockerinit, if Docker itself was compiled dynamically via ./hack/make.sh dynbinary |
|
| 27 |
+) |
|
| 28 |
+ |
|
| 24 | 29 |
// ListOpts type |
| 25 | 30 |
type ListOpts []string |
| 26 | 31 |
|
| ... | ... |
@@ -190,6 +196,67 @@ func SelfPath() string {
|
| 190 | 190 |
return path |
| 191 | 191 |
} |
| 192 | 192 |
|
| 193 |
+func dockerInitSha1(target string) string {
|
|
| 194 |
+ f, err := os.Open(target) |
|
| 195 |
+ if err != nil {
|
|
| 196 |
+ return "" |
|
| 197 |
+ } |
|
| 198 |
+ defer f.Close() |
|
| 199 |
+ h := sha1.New() |
|
| 200 |
+ _, err = io.Copy(h, f) |
|
| 201 |
+ if err != nil {
|
|
| 202 |
+ return "" |
|
| 203 |
+ } |
|
| 204 |
+ return hex.EncodeToString(h.Sum(nil)) |
|
| 205 |
+} |
|
| 206 |
+ |
|
| 207 |
+func isValidDockerInitPath(target string, selfPath string) bool { // target and selfPath should be absolute (InitPath and SelfPath already do this)
|
|
| 208 |
+ if IAMSTATIC {
|
|
| 209 |
+ if target == selfPath {
|
|
| 210 |
+ return true |
|
| 211 |
+ } |
|
| 212 |
+ targetFileInfo, err := os.Lstat(target) |
|
| 213 |
+ if err != nil {
|
|
| 214 |
+ return false |
|
| 215 |
+ } |
|
| 216 |
+ selfPathFileInfo, err := os.Lstat(selfPath) |
|
| 217 |
+ if err != nil {
|
|
| 218 |
+ return false |
|
| 219 |
+ } |
|
| 220 |
+ return os.SameFile(targetFileInfo, selfPathFileInfo) |
|
| 221 |
+ } |
|
| 222 |
+ return INITSHA1 != "" && dockerInitSha1(target) == INITSHA1 |
|
| 223 |
+} |
|
| 224 |
+ |
|
| 225 |
+// Figure out the path of our dockerinit (which may be SelfPath()) |
|
| 226 |
+func DockerInitPath() string {
|
|
| 227 |
+ selfPath := SelfPath() |
|
| 228 |
+ if isValidDockerInitPath(selfPath, selfPath) {
|
|
| 229 |
+ // if we're valid, don't bother checking anything else |
|
| 230 |
+ return selfPath |
|
| 231 |
+ } |
|
| 232 |
+ var possibleInits = []string{
|
|
| 233 |
+ filepath.Join(filepath.Dir(selfPath), "dockerinit"), |
|
| 234 |
+ // "/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." |
|
| 235 |
+ "/usr/libexec/docker/dockerinit", |
|
| 236 |
+ "/usr/local/libexec/docker/dockerinit", |
|
| 237 |
+ } |
|
| 238 |
+ for _, dockerInit := range possibleInits {
|
|
| 239 |
+ path, err := exec.LookPath(dockerInit) |
|
| 240 |
+ if err == nil {
|
|
| 241 |
+ path, err = filepath.Abs(path) |
|
| 242 |
+ if err != nil {
|
|
| 243 |
+ // LookPath already validated that this file exists and is executable (following symlinks), so how could Abs fail? |
|
| 244 |
+ panic(err) |
|
| 245 |
+ } |
|
| 246 |
+ if isValidDockerInitPath(path, selfPath) {
|
|
| 247 |
+ return path |
|
| 248 |
+ } |
|
| 249 |
+ } |
|
| 250 |
+ } |
|
| 251 |
+ return "" |
|
| 252 |
+} |
|
| 253 |
+ |
|
| 193 | 254 |
type NopWriter struct{}
|
| 194 | 255 |
|
| 195 | 256 |
func (*NopWriter) Write(buf []byte) (int, error) {
|