Signed-off-by: David Calavera <david.calavera@gmail.com>
| ... | ... |
@@ -637,13 +637,36 @@ an [*ADD*](/reference/builder/#add) instruction to reference a file in the |
| 637 | 637 |
context. |
| 638 | 638 |
|
| 639 | 639 |
The `URL` parameter can specify the location of a Git repository; |
| 640 |
-the repository acts as the build context. The system recursively clones the repository |
|
| 640 |
+the repository acts as the build context. The system recursively clones the repository |
|
| 641 | 641 |
and its submodules using a `git clone --depth 1 --recursive` command. |
| 642 | 642 |
This command runs in a temporary directory on your local host. |
| 643 | 643 |
After the command succeeds, the directory is sent to the Docker daemon as the context. |
| 644 | 644 |
Local clones give you the ability to access private repositories using |
| 645 | 645 |
local user credentials, VPN's, and so forth. |
| 646 | 646 |
|
| 647 |
+Git URLs accept context configuration in their fragment section, separated by a colon `:`. |
|
| 648 |
+The first part represents the reference that Git will check out, this can be either |
|
| 649 |
+a branch, a tag, or a commit SHA. The second part represents a subdirectory |
|
| 650 |
+inside the repository that will be used as a build context. |
|
| 651 |
+ |
|
| 652 |
+For example, run this command to use a directory called `docker` in the branch `container`: |
|
| 653 |
+ |
|
| 654 |
+ $ docker build https://github.com/docker/rootfs.git#container:docker |
|
| 655 |
+ |
|
| 656 |
+The following table represents all the valid suffixes with their build contexts: |
|
| 657 |
+ |
|
| 658 |
+Build Syntax Suffix | Commit Used | Build Context Used |
|
| 659 |
+--------------------|-------------|------------------- |
|
| 660 |
+`myrepo.git` | `refs/heads/master` | `/` |
|
| 661 |
+`myrepo.git#mytag` | `refs/tags/mytag` | `/` |
|
| 662 |
+`myrepo.git#mybranch` | `refs/heads/mybranch` | `/` |
|
| 663 |
+`myrepo.git#abcdef` | `sha1 = abcdef` | `/` |
|
| 664 |
+`myrepo.git#:myfolder` | `refs/heads/master` | `/myfolder` |
|
| 665 |
+`myrepo.git#master:myfolder` | `refs/heads/master` | `/myfolder` |
|
| 666 |
+`myrepo.git#mytag:myfolder` | `refs/tags/mytag` | `/myfolder` |
|
| 667 |
+`myrepo.git#mybranch:myfolder` | `refs/heads/mybranch` | `/myfolder` |
|
| 668 |
+`myrepo.git#abcdef:myfolder` | `sha1 = abcdef` | `/myfolder` |
|
| 669 |
+ |
|
| 647 | 670 |
Instead of specifying a context, you can pass a single Dockerfile in the |
| 648 | 671 |
`URL` or pipe the file in via `STDIN`. To pipe a Dockerfile from `STDIN`: |
| 649 | 672 |
|
| ... | ... |
@@ -4221,6 +4221,35 @@ func (s *DockerSuite) TestBuildFromGIT(c *check.C) {
|
| 4221 | 4221 |
} |
| 4222 | 4222 |
} |
| 4223 | 4223 |
|
| 4224 |
+func (s *DockerSuite) TestBuildFromGITWithContext(c *check.C) {
|
|
| 4225 |
+ name := "testbuildfromgit" |
|
| 4226 |
+ defer deleteImages(name) |
|
| 4227 |
+ git, err := fakeGIT("repo", map[string]string{
|
|
| 4228 |
+ "docker/Dockerfile": `FROM busybox |
|
| 4229 |
+ ADD first /first |
|
| 4230 |
+ RUN [ -f /first ] |
|
| 4231 |
+ MAINTAINER docker`, |
|
| 4232 |
+ "docker/first": "test git data", |
|
| 4233 |
+ }, true) |
|
| 4234 |
+ if err != nil {
|
|
| 4235 |
+ c.Fatal(err) |
|
| 4236 |
+ } |
|
| 4237 |
+ defer git.Close() |
|
| 4238 |
+ |
|
| 4239 |
+ u := fmt.Sprintf("%s#master:docker", git.RepoURL)
|
|
| 4240 |
+ _, err = buildImageFromPath(name, u, true) |
|
| 4241 |
+ if err != nil {
|
|
| 4242 |
+ c.Fatal(err) |
|
| 4243 |
+ } |
|
| 4244 |
+ res, err := inspectField(name, "Author") |
|
| 4245 |
+ if err != nil {
|
|
| 4246 |
+ c.Fatal(err) |
|
| 4247 |
+ } |
|
| 4248 |
+ if res != "docker" {
|
|
| 4249 |
+ c.Fatalf("Maintainer should be docker, got %s", res)
|
|
| 4250 |
+ } |
|
| 4251 |
+} |
|
| 4252 |
+ |
|
| 4224 | 4253 |
func (s *DockerSuite) TestBuildCleanupCmdOnEntrypoint(c *check.C) {
|
| 4225 | 4254 |
name := "testbuildcmdcleanuponentrypoint" |
| 4226 | 4255 |
defer deleteImages(name) |
| ... | ... |
@@ -1,6 +1,9 @@ |
| 1 | 1 |
package urlutil |
| 2 | 2 |
|
| 3 |
-import "strings" |
|
| 3 |
+import ( |
|
| 4 |
+ "regexp" |
|
| 5 |
+ "strings" |
|
| 6 |
+) |
|
| 4 | 7 |
|
| 5 | 8 |
var ( |
| 6 | 9 |
validPrefixes = []string{
|
| ... | ... |
@@ -8,11 +11,13 @@ var ( |
| 8 | 8 |
"github.com/", |
| 9 | 9 |
"git@", |
| 10 | 10 |
} |
| 11 |
+ |
|
| 12 |
+ urlPathWithFragmentSuffix = regexp.MustCompile(".git(?:#.+)?$")
|
|
| 11 | 13 |
) |
| 12 | 14 |
|
| 13 | 15 |
// IsGitURL returns true if the provided str is a git repository URL. |
| 14 | 16 |
func IsGitURL(str string) bool {
|
| 15 |
- if IsURL(str) && strings.HasSuffix(str, ".git") {
|
|
| 17 |
+ if IsURL(str) && urlPathWithFragmentSuffix.MatchString(str) {
|
|
| 16 | 18 |
return true |
| 17 | 19 |
} |
| 18 | 20 |
for _, prefix := range validPrefixes {
|
| ... | ... |
@@ -9,10 +9,15 @@ var ( |
| 9 | 9 |
"git@bitbucket.org:atlassianlabs/atlassian-docker.git", |
| 10 | 10 |
"https://github.com/docker/docker.git", |
| 11 | 11 |
"http://github.com/docker/docker.git", |
| 12 |
+ "http://github.com/docker/docker.git#branch", |
|
| 13 |
+ "http://github.com/docker/docker.git#:dir", |
|
| 12 | 14 |
} |
| 13 | 15 |
incompleteGitUrls = []string{
|
| 14 | 16 |
"github.com/docker/docker", |
| 15 | 17 |
} |
| 18 |
+ invalidGitUrls = []string{
|
|
| 19 |
+ "http://github.com/docker/docker.git:#branch", |
|
| 20 |
+ } |
|
| 16 | 21 |
) |
| 17 | 22 |
|
| 18 | 23 |
func TestValidGitTransport(t *testing.T) {
|
| ... | ... |
@@ -35,9 +40,16 @@ func TestIsGIT(t *testing.T) {
|
| 35 | 35 |
t.Fatalf("%q should be detected as valid Git url", url)
|
| 36 | 36 |
} |
| 37 | 37 |
} |
| 38 |
+ |
|
| 38 | 39 |
for _, url := range incompleteGitUrls {
|
| 39 | 40 |
if IsGitURL(url) == false {
|
| 40 | 41 |
t.Fatalf("%q should be detected as valid Git url", url)
|
| 41 | 42 |
} |
| 42 | 43 |
} |
| 44 |
+ |
|
| 45 |
+ for _, url := range invalidGitUrls {
|
|
| 46 |
+ if IsGitURL(url) == true {
|
|
| 47 |
+ t.Fatalf("%q should not be detected as valid Git prefix", url)
|
|
| 48 |
+ } |
|
| 49 |
+ } |
|
| 43 | 50 |
} |
| ... | ... |
@@ -4,7 +4,10 @@ import ( |
| 4 | 4 |
"fmt" |
| 5 | 5 |
"io/ioutil" |
| 6 | 6 |
"net/http" |
| 7 |
+ "net/url" |
|
| 8 |
+ "os" |
|
| 7 | 9 |
"os/exec" |
| 10 |
+ "path/filepath" |
|
| 8 | 11 |
"strings" |
| 9 | 12 |
|
| 10 | 13 |
"github.com/docker/docker/pkg/urlutil" |
| ... | ... |
@@ -19,20 +22,26 @@ func GitClone(remoteURL string) (string, error) {
|
| 19 | 19 |
return "", err |
| 20 | 20 |
} |
| 21 | 21 |
|
| 22 |
- clone := cloneArgs(remoteURL, root) |
|
| 22 |
+ u, err := url.Parse(remoteURL) |
|
| 23 |
+ if err != nil {
|
|
| 24 |
+ return "", err |
|
| 25 |
+ } |
|
| 23 | 26 |
|
| 24 |
- if output, err := exec.Command("git", clone...).CombinedOutput(); err != nil {
|
|
| 27 |
+ fragment := u.Fragment |
|
| 28 |
+ clone := cloneArgs(u, root) |
|
| 29 |
+ |
|
| 30 |
+ if output, err := git(clone...); err != nil {
|
|
| 25 | 31 |
return "", fmt.Errorf("Error trying to use git: %s (%s)", err, output)
|
| 26 | 32 |
} |
| 27 | 33 |
|
| 28 |
- return root, nil |
|
| 34 |
+ return checkoutGit(fragment, root) |
|
| 29 | 35 |
} |
| 30 | 36 |
|
| 31 |
-func cloneArgs(remoteURL, root string) []string {
|
|
| 37 |
+func cloneArgs(remoteURL *url.URL, root string) []string {
|
|
| 32 | 38 |
args := []string{"clone", "--recursive"}
|
| 33 |
- shallow := true |
|
| 39 |
+ shallow := len(remoteURL.Fragment) == 0 |
|
| 34 | 40 |
|
| 35 |
- if strings.HasPrefix(remoteURL, "http") {
|
|
| 41 |
+ if shallow && strings.HasPrefix(remoteURL.Scheme, "http") {
|
|
| 36 | 42 |
res, err := http.Head(fmt.Sprintf("%s/info/refs?service=git-upload-pack", remoteURL))
|
| 37 | 43 |
if err != nil || res.Header.Get("Content-Type") != "application/x-git-upload-pack-advertisement" {
|
| 38 | 44 |
shallow = false |
| ... | ... |
@@ -43,5 +52,42 @@ func cloneArgs(remoteURL, root string) []string {
|
| 43 | 43 |
args = append(args, "--depth", "1") |
| 44 | 44 |
} |
| 45 | 45 |
|
| 46 |
- return append(args, remoteURL, root) |
|
| 46 |
+ if remoteURL.Fragment != "" {
|
|
| 47 |
+ remoteURL.Fragment = "" |
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ return append(args, remoteURL.String(), root) |
|
| 51 |
+} |
|
| 52 |
+ |
|
| 53 |
+func checkoutGit(fragment, root string) (string, error) {
|
|
| 54 |
+ refAndDir := strings.SplitN(fragment, ":", 2) |
|
| 55 |
+ |
|
| 56 |
+ if len(refAndDir[0]) != 0 {
|
|
| 57 |
+ if output, err := gitWithinDir(root, "checkout", refAndDir[0]); err != nil {
|
|
| 58 |
+ return "", fmt.Errorf("Error trying to use git: %s (%s)", err, output)
|
|
| 59 |
+ } |
|
| 60 |
+ } |
|
| 61 |
+ |
|
| 62 |
+ if len(refAndDir) > 1 && len(refAndDir[1]) != 0 {
|
|
| 63 |
+ newCtx := filepath.Join(root, refAndDir[1]) |
|
| 64 |
+ fi, err := os.Stat(newCtx) |
|
| 65 |
+ if err != nil {
|
|
| 66 |
+ return "", err |
|
| 67 |
+ } |
|
| 68 |
+ if !fi.IsDir() {
|
|
| 69 |
+ return "", fmt.Errorf("Error setting git context, not a directory: %s", newCtx)
|
|
| 70 |
+ } |
|
| 71 |
+ root = newCtx |
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+ return root, nil |
|
| 75 |
+} |
|
| 76 |
+ |
|
| 77 |
+func gitWithinDir(dir string, args ...string) ([]byte, error) {
|
|
| 78 |
+ a := []string{"--work-tree", dir, "--git-dir", filepath.Join(dir, ".git")}
|
|
| 79 |
+ return git(append(a, args...)...) |
|
| 80 |
+} |
|
| 81 |
+ |
|
| 82 |
+func git(args ...string) ([]byte, error) {
|
|
| 83 |
+ return exec.Command("git", args...).CombinedOutput()
|
|
| 47 | 84 |
} |
| ... | ... |
@@ -2,9 +2,12 @@ package utils |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 |
+ "io/ioutil" |
|
| 5 | 6 |
"net/http" |
| 6 | 7 |
"net/http/httptest" |
| 7 | 8 |
"net/url" |
| 9 |
+ "os" |
|
| 10 |
+ "path/filepath" |
|
| 8 | 11 |
"reflect" |
| 9 | 12 |
"testing" |
| 10 | 13 |
) |
| ... | ... |
@@ -22,7 +25,7 @@ func TestCloneArgsSmartHttp(t *testing.T) {
|
| 22 | 22 |
w.Header().Set("Content-Type", fmt.Sprintf("application/x-%s-advertisement", q))
|
| 23 | 23 |
}) |
| 24 | 24 |
|
| 25 |
- args := cloneArgs(gitURL, "/tmp") |
|
| 25 |
+ args := cloneArgs(serverURL, "/tmp") |
|
| 26 | 26 |
exp := []string{"clone", "--recursive", "--depth", "1", gitURL, "/tmp"}
|
| 27 | 27 |
if !reflect.DeepEqual(args, exp) {
|
| 28 | 28 |
t.Fatalf("Expected %v, got %v", exp, args)
|
| ... | ... |
@@ -41,16 +44,132 @@ func TestCloneArgsDumbHttp(t *testing.T) {
|
| 41 | 41 |
w.Header().Set("Content-Type", "text/plain")
|
| 42 | 42 |
}) |
| 43 | 43 |
|
| 44 |
- args := cloneArgs(gitURL, "/tmp") |
|
| 44 |
+ args := cloneArgs(serverURL, "/tmp") |
|
| 45 | 45 |
exp := []string{"clone", "--recursive", gitURL, "/tmp"}
|
| 46 | 46 |
if !reflect.DeepEqual(args, exp) {
|
| 47 | 47 |
t.Fatalf("Expected %v, got %v", exp, args)
|
| 48 | 48 |
} |
| 49 | 49 |
} |
| 50 |
+ |
|
| 50 | 51 |
func TestCloneArgsGit(t *testing.T) {
|
| 51 |
- args := cloneArgs("git://github.com/docker/docker", "/tmp")
|
|
| 52 |
+ u, _ := url.Parse("git://github.com/docker/docker")
|
|
| 53 |
+ args := cloneArgs(u, "/tmp") |
|
| 52 | 54 |
exp := []string{"clone", "--recursive", "--depth", "1", "git://github.com/docker/docker", "/tmp"}
|
| 53 | 55 |
if !reflect.DeepEqual(args, exp) {
|
| 54 | 56 |
t.Fatalf("Expected %v, got %v", exp, args)
|
| 55 | 57 |
} |
| 56 | 58 |
} |
| 59 |
+ |
|
| 60 |
+func TestCloneArgsStripFragment(t *testing.T) {
|
|
| 61 |
+ u, _ := url.Parse("git://github.com/docker/docker#test")
|
|
| 62 |
+ args := cloneArgs(u, "/tmp") |
|
| 63 |
+ exp := []string{"clone", "--recursive", "git://github.com/docker/docker", "/tmp"}
|
|
| 64 |
+ if !reflect.DeepEqual(args, exp) {
|
|
| 65 |
+ t.Fatalf("Expected %v, got %v", exp, args)
|
|
| 66 |
+ } |
|
| 67 |
+} |
|
| 68 |
+ |
|
| 69 |
+func TestCheckoutGit(t *testing.T) {
|
|
| 70 |
+ root, err := ioutil.TempDir("", "docker-build-git-checkout")
|
|
| 71 |
+ if err != nil {
|
|
| 72 |
+ t.Fatal(err) |
|
| 73 |
+ } |
|
| 74 |
+ defer os.RemoveAll(root) |
|
| 75 |
+ |
|
| 76 |
+ gitDir := filepath.Join(root, "repo") |
|
| 77 |
+ _, err = git("init", gitDir)
|
|
| 78 |
+ if err != nil {
|
|
| 79 |
+ t.Fatal(err) |
|
| 80 |
+ } |
|
| 81 |
+ |
|
| 82 |
+ if _, err = gitWithinDir(gitDir, "config", "user.email", "test@docker.com"); err != nil {
|
|
| 83 |
+ t.Fatal(err) |
|
| 84 |
+ } |
|
| 85 |
+ |
|
| 86 |
+ if _, err = gitWithinDir(gitDir, "config", "user.name", "Docker test"); err != nil {
|
|
| 87 |
+ t.Fatal(err) |
|
| 88 |
+ } |
|
| 89 |
+ |
|
| 90 |
+ if err = ioutil.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM scratch"), 0644); err != nil {
|
|
| 91 |
+ t.Fatal(err) |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+ subDir := filepath.Join(gitDir, "subdir") |
|
| 95 |
+ if err = os.Mkdir(subDir, 0755); err != nil {
|
|
| 96 |
+ t.Fatal(err) |
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ if err = ioutil.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM scratch\nEXPOSE 5000"), 0644); err != nil {
|
|
| 100 |
+ t.Fatal(err) |
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ if _, err = gitWithinDir(gitDir, "add", "-A"); err != nil {
|
|
| 104 |
+ t.Fatal(err) |
|
| 105 |
+ } |
|
| 106 |
+ |
|
| 107 |
+ if _, err = gitWithinDir(gitDir, "commit", "-am", "First commit"); err != nil {
|
|
| 108 |
+ t.Fatal(err) |
|
| 109 |
+ } |
|
| 110 |
+ |
|
| 111 |
+ if _, err = gitWithinDir(gitDir, "checkout", "-b", "test"); err != nil {
|
|
| 112 |
+ t.Fatal(err) |
|
| 113 |
+ } |
|
| 114 |
+ |
|
| 115 |
+ if err = ioutil.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM scratch\nEXPOSE 3000"), 0644); err != nil {
|
|
| 116 |
+ t.Fatal(err) |
|
| 117 |
+ } |
|
| 118 |
+ |
|
| 119 |
+ if err = ioutil.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM busybox\nEXPOSE 5000"), 0644); err != nil {
|
|
| 120 |
+ t.Fatal(err) |
|
| 121 |
+ } |
|
| 122 |
+ |
|
| 123 |
+ if _, err = gitWithinDir(gitDir, "add", "-A"); err != nil {
|
|
| 124 |
+ t.Fatal(err) |
|
| 125 |
+ } |
|
| 126 |
+ |
|
| 127 |
+ if _, err = gitWithinDir(gitDir, "commit", "-am", "Branch commit"); err != nil {
|
|
| 128 |
+ t.Fatal(err) |
|
| 129 |
+ } |
|
| 130 |
+ |
|
| 131 |
+ if _, err = gitWithinDir(gitDir, "checkout", "master"); err != nil {
|
|
| 132 |
+ t.Fatal(err) |
|
| 133 |
+ } |
|
| 134 |
+ |
|
| 135 |
+ cases := []struct {
|
|
| 136 |
+ frag string |
|
| 137 |
+ exp string |
|
| 138 |
+ fail bool |
|
| 139 |
+ }{
|
|
| 140 |
+ {"", "FROM scratch", false},
|
|
| 141 |
+ {"master", "FROM scratch", false},
|
|
| 142 |
+ {":subdir", "FROM scratch\nEXPOSE 5000", false},
|
|
| 143 |
+ {":nosubdir", "", true}, // missing directory error
|
|
| 144 |
+ {":Dockerfile", "", true}, // not a directory error
|
|
| 145 |
+ {"master:nosubdir", "", true},
|
|
| 146 |
+ {"master:subdir", "FROM scratch\nEXPOSE 5000", false},
|
|
| 147 |
+ {"test", "FROM scratch\nEXPOSE 3000", false},
|
|
| 148 |
+ {"test:", "FROM scratch\nEXPOSE 3000", false},
|
|
| 149 |
+ {"test:subdir", "FROM busybox\nEXPOSE 5000", false},
|
|
| 150 |
+ } |
|
| 151 |
+ |
|
| 152 |
+ for _, c := range cases {
|
|
| 153 |
+ r, err := checkoutGit(c.frag, gitDir) |
|
| 154 |
+ |
|
| 155 |
+ fail := err != nil |
|
| 156 |
+ if fail != c.fail {
|
|
| 157 |
+ t.Fatalf("Expected %v failure, error was %v\n", c.fail, err)
|
|
| 158 |
+ } |
|
| 159 |
+ if c.fail {
|
|
| 160 |
+ continue |
|
| 161 |
+ } |
|
| 162 |
+ |
|
| 163 |
+ b, err := ioutil.ReadFile(filepath.Join(r, "Dockerfile")) |
|
| 164 |
+ if err != nil {
|
|
| 165 |
+ t.Fatal(err) |
|
| 166 |
+ } |
|
| 167 |
+ |
|
| 168 |
+ if string(b) != c.exp {
|
|
| 169 |
+ t.Fatalf("Expected %v, was %v\n", c.exp, string(b))
|
|
| 170 |
+ } |
|
| 171 |
+ } |
|
| 172 |
+} |