diff --git a/builder/internals.go b/builder/internals.go index d8093507d3..f6083e7918 100644 --- a/builder/internals.go +++ b/builder/internals.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "io/ioutil" + "net/http" "net/url" "os" "path" @@ -254,8 +255,21 @@ func calcCopyInfo(b *Builder, cmdName string, cInfos *[]*copyInfo, origPath stri fmt.Fprintf(b.OutStream, "\n") tmpFile.Close() - // Remove the mtime of the newly created tmp file - if err := system.UtimesNano(tmpFileName, make([]syscall.Timespec, 2)); err != nil { + // Set the mtime to the Last-Modified header value if present + // Otherwise just remove atime and mtime + times := make([]syscall.Timespec, 2) + + lastMod := resp.Header.Get("Last-Modified") + if lastMod != "" { + mTime, err := http.ParseTime(lastMod) + // If we can't parse it then just let it default to 'zero' + // otherwise use the parsed time value + if err == nil { + times[1] = syscall.NsecToTimespec(mTime.UnixNano()) + } + } + + if err := system.UtimesNano(tmpFileName, times); err != nil { return err } diff --git a/docs/sources/reference/builder.md b/docs/sources/reference/builder.md index 1f18f0c63a..00d9ba3eb8 100644 --- a/docs/sources/reference/builder.md +++ b/docs/sources/reference/builder.md @@ -376,7 +376,11 @@ destination container. All new files and directories are created with a UID and GID of 0. In the case where `` is a remote file URL, the destination will -have permissions of 600. +have permissions of 600. If the remote file being retrieved has an HTTP +`Last-Modified` header, the timestamp from that header will be used +to set the `mtime` on the destination file. Then, like any other file +processed during an `ADD`, `mtime` will be included in the determination +of whether or not the file has changed and the cache should be updated. > **Note**: > If you build by passing a `Dockerfile` through STDIN (`docker diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 59d09f54d4..de60a8017f 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -7,9 +7,11 @@ import ( "io/ioutil" "os" "os/exec" + "path" "path/filepath" "regexp" "strings" + "syscall" "testing" "time" @@ -2214,6 +2216,64 @@ func TestBuildADDRemoteFileWithoutCache(t *testing.T) { logDone("build - add remote file without cache") } +func TestBuildADDRemoteFileMTime(t *testing.T) { + name := "testbuildaddremotefilemtime" + defer deleteImages(name) + + server, err := fakeStorage(map[string]string{"baz": "hello"}) + if err != nil { + t.Fatal(err) + } + defer server.Close() + + ctx, err := fakeContext(fmt.Sprintf(`FROM scratch + MAINTAINER dockerio + ADD %s/baz /usr/lib/baz/quux`, server.URL), nil) + if err != nil { + t.Fatal(err) + } + defer ctx.Close() + + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + t.Fatal(err) + } + + id2, err := buildImageFromContext(name, ctx, true) + if err != nil { + t.Fatal(err) + } + if id1 != id2 { + t.Fatal("The cache should have been used but wasn't - #1") + } + + // Now set baz's times to anything else and redo the build + // This time the cache should not be used + bazPath := path.Join(server.FakeContext.Dir, "baz") + err = syscall.UtimesNano(bazPath, make([]syscall.Timespec, 2)) + if err != nil { + t.Fatalf("Error setting mtime on %q: %v", bazPath, err) + } + + id3, err := buildImageFromContext(name, ctx, true) + if err != nil { + t.Fatal(err) + } + if id1 == id3 { + t.Fatal("The cache should not have been used but was") + } + + // And for good measure do it again and make sure cache is used this time + id4, err := buildImageFromContext(name, ctx, true) + if err != nil { + t.Fatal(err) + } + if id3 != id4 { + t.Fatal("The cache should have been used but wasn't - #2") + } + logDone("build - add remote file testing mtime") +} + func TestBuildADDLocalAndRemoteFilesWithCache(t *testing.T) { name := "testbuildaddlocalandremotefilewithcache" defer deleteImages(name)