... | ... |
@@ -14,7 +14,7 @@ angular.module('openshiftConsole') |
14 | 14 |
|
15 | 15 |
$scope.templatesByTag = {}; |
16 | 16 |
|
17 |
- $scope.sourceURLPattern = /^(ftp|http|https|git):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; |
|
17 |
+ $scope.sourceURLPattern = /^((ftp|http|https|git):\/\/(\w+:{0,1}\w*@)|git@)?([^\s@]+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; |
|
18 | 18 |
|
19 | 19 |
DataService.list("templates", $scope, function(templates) { |
20 | 20 |
$scope.projectTemplates = templates.by("metadata.name"); |
21 | 21 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,70 @@ |
0 |
+"use strict"; |
|
1 |
+ |
|
2 |
+describe("CreateController", function(){ |
|
3 |
+ var controller, form; |
|
4 |
+ var $scope = { |
|
5 |
+ projectTemplates: {}, |
|
6 |
+ openshiftTemplates: {}, |
|
7 |
+ templatesByTag: {} |
|
8 |
+ }; |
|
9 |
+ |
|
10 |
+ beforeEach(function(){ |
|
11 |
+ inject(function(_$controller_){ |
|
12 |
+ // The injector unwraps the underscores (_) from around the parameter names when matching |
|
13 |
+ controller = _$controller_("CreateController", { |
|
14 |
+ $scope: $scope, |
|
15 |
+ DataService: { |
|
16 |
+ list: function(templates){} |
|
17 |
+ } |
|
18 |
+ }); |
|
19 |
+ }); |
|
20 |
+ }); |
|
21 |
+ |
|
22 |
+ |
|
23 |
+ it("valid http URL", function(){ |
|
24 |
+ var match = 'http://example.com/dir1/dir2'.match($scope.sourceURLPattern) |
|
25 |
+ expect(match).not.toBeNull(); |
|
26 |
+ }); |
|
27 |
+ |
|
28 |
+ it("valid http URL, without http part", function(){ |
|
29 |
+ var match = 'example.com/dir1/dir2'.match($scope.sourceURLPattern) |
|
30 |
+ expect(match).not.toBeNull(); |
|
31 |
+ }); |
|
32 |
+ |
|
33 |
+ |
|
34 |
+ it("valid http URL with user and password", function(){ |
|
35 |
+ var match = 'http://user:pass@example.com/dir1/dir2'.match($scope.sourceURLPattern) |
|
36 |
+ expect(match).not.toBeNull(); |
|
37 |
+ }); |
|
38 |
+ |
|
39 |
+ it("valid http URL with port", function(){ |
|
40 |
+ var match = 'http://example.com:8080/dir1/dir2'.match($scope.sourceURLPattern) |
|
41 |
+ expect(match).not.toBeNull(); |
|
42 |
+ }); |
|
43 |
+ |
|
44 |
+ it("valid http URL with port, user and password", function(){ |
|
45 |
+ var match = 'http://user:pass@example.com:8080/dir1/dir2'.match($scope.sourceURLPattern) |
|
46 |
+ expect(match).not.toBeNull(); |
|
47 |
+ }); |
|
48 |
+ |
|
49 |
+ it("valid https URL", function(){ |
|
50 |
+ var match = 'https://example.com/dir1/dir2'.match($scope.sourceURLPattern) |
|
51 |
+ expect(match).not.toBeNull(); |
|
52 |
+ }); |
|
53 |
+ |
|
54 |
+ it("valid ftp URL", function(){ |
|
55 |
+ var match = 'ftp://example.com/dir1/dir2'.match($scope.sourceURLPattern) |
|
56 |
+ expect(match).not.toBeNull(); |
|
57 |
+ }); |
|
58 |
+ |
|
59 |
+ it("valid git+ssh URL", function(){ |
|
60 |
+ var match = 'git@example.com:dir1/dir2'.match($scope.sourceURLPattern) |
|
61 |
+ expect(match).not.toBeNull(); |
|
62 |
+ }); |
|
63 |
+ |
|
64 |
+ it("invalid git+ssh URL (double @@)", function(){ |
|
65 |
+ var match = 'git@@example.com:dir1/dir2'.match($scope.sourceURLPattern) |
|
66 |
+ expect(match).toBeNull(); |
|
67 |
+ }); |
|
68 |
+ |
|
69 |
+}); |
... | ... |
@@ -16383,7 +16383,7 @@ namespace:"openshift" |
16383 | 16383 |
}), g.info("openshift image repos", a.openshiftImageRepos); |
16384 | 16384 |
}); |
16385 | 16385 |
} ]), angular.module("openshiftConsole").controller("CreateController", [ "$scope", "DataService", "$filter", "LabelFilter", "$location", "Logger", function(a, b, c, d, e, f) { |
16386 |
-a.projectTemplates = {}, a.openshiftTemplates = {}, a.templatesByTag = {}, a.sourceURLPattern = /^(ftp|http|https|git):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/, b.list("templates", a, function(b) { |
|
16386 |
+a.projectTemplates = {}, a.openshiftTemplates = {}, a.templatesByTag = {}, a.sourceURLPattern = /^((ftp|http|https|git):\/\/(\w+:{0,1}\w*@)|git@)?([^\s@]+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/, b.list("templates", a, function(b) { |
|
16387 | 16387 |
a.projectTemplates = b.by("metadata.name"), g(), f.info("project templates", a.projectTemplates); |
16388 | 16388 |
}), b.list("templates", { |
16389 | 16389 |
namespace:"openshift" |
... | ... |
@@ -111,6 +111,13 @@ type BuildSource struct { |
111 | 111 |
// This allows to have buildable sources in directory other than root of |
112 | 112 |
// repository. |
113 | 113 |
ContextDir string `json:"contextDir,omitempty"` |
114 |
+ |
|
115 |
+ // SourceSecretName is the name of a Secret that would be used for setting |
|
116 |
+ // up the authentication for cloning private repository. |
|
117 |
+ // The secret contains valid credentials for remote repository, where the |
|
118 |
+ // data's key represent the authentication method to be used and value is |
|
119 |
+ // the base64 encoded credentials. Supported auth methods are: ssh-privatekey. |
|
120 |
+ SourceSecretName string |
|
114 | 121 |
} |
115 | 122 |
|
116 | 123 |
// SourceRevision is the revision or commit information from the source for the build |
... | ... |
@@ -111,6 +111,13 @@ type BuildSource struct { |
111 | 111 |
// This allows to have buildable sources in directory other than root of |
112 | 112 |
// repository. |
113 | 113 |
ContextDir string `json:"contextDir,omitempty"` |
114 |
+ |
|
115 |
+ // SourceSecretName is the name of a Secret that would be used for setting |
|
116 |
+ // up the authentication for cloning private repository. |
|
117 |
+ // The secret contains valid credentials for remote repository, where the |
|
118 |
+ // secret's data key represent the authentication method to be used and value is |
|
119 |
+ // the base64 encoded credentials. Supported auth methods are: ssh-privatekey. |
|
120 |
+ SourceSecretName string `json:"sourceSecretName,omitempty" description:"supported auth methods are: ssh-privatekey` |
|
114 | 121 |
} |
115 | 122 |
|
116 | 123 |
// SourceRevision is the revision or commit information from the source for the build |
... | ... |
@@ -117,6 +117,13 @@ type BuildSource struct { |
117 | 117 |
// This allows to have buildable sources in directory other than root of |
118 | 118 |
// repository. |
119 | 119 |
ContextDir string `json:"contextDir,omitempty"` |
120 |
+ |
|
121 |
+ // SourceSecretName is the name of a Secret that would be used for setting |
|
122 |
+ // up the authentication for cloning private repository. |
|
123 |
+ // The secret contains valid credentials for remote repository, where the |
|
124 |
+ // data's key represent the authentication method to be used and value is |
|
125 |
+ // the base64 encoded credentials. Supported auth methods are: ssh-privatekey. |
|
126 |
+ SourceSecretName string `json:"sourceSecretName,omitempty" description:"supported auth methods are: ssh-privatekey` |
|
120 | 127 |
} |
121 | 128 |
|
122 | 129 |
// SourceRevision is the revision or commit information from the source for the build |
... | ... |
@@ -1,7 +1,11 @@ |
1 | 1 |
package cmd |
2 | 2 |
|
3 | 3 |
import ( |
4 |
+ "fmt" |
|
5 |
+ "io/ioutil" |
|
4 | 6 |
"os" |
7 |
+ "os/exec" |
|
8 |
+ "path/filepath" |
|
5 | 9 |
|
6 | 10 |
"github.com/fsouza/go-dockerclient" |
7 | 11 |
"github.com/golang/glog" |
... | ... |
@@ -9,6 +13,7 @@ import ( |
9 | 9 |
"github.com/openshift/origin/pkg/build/api" |
10 | 10 |
bld "github.com/openshift/origin/pkg/build/builder" |
11 | 11 |
"github.com/openshift/origin/pkg/build/builder/cmd/dockercfg" |
12 |
+ "github.com/openshift/origin/pkg/build/builder/cmd/scmauth" |
|
12 | 13 |
dockerutil "github.com/openshift/origin/pkg/cmd/util/docker" |
13 | 14 |
image "github.com/openshift/origin/pkg/image/api" |
14 | 15 |
) |
... | ... |
@@ -19,6 +24,7 @@ const DockerCfgFile = ".dockercfg" |
19 | 19 |
type builder interface { |
20 | 20 |
Build() error |
21 | 21 |
} |
22 |
+ |
|
22 | 23 |
type factoryFunc func( |
23 | 24 |
client bld.DockerClient, |
24 | 25 |
dockerSocket string, |
... | ... |
@@ -26,7 +32,9 @@ type factoryFunc func( |
26 | 26 |
authPresent bool, |
27 | 27 |
build *api.Build) builder |
28 | 28 |
|
29 |
-func run(builderFactory factoryFunc) { |
|
29 |
+// run is responsible for preparing environment for actual build. |
|
30 |
+// It accepts factoryFunc and an ordered array of SCMAuths. |
|
31 |
+func run(builderFactory factoryFunc, scmAuths []scmauth.SCMAuth) { |
|
30 | 32 |
client, endpoint, err := dockerutil.NewHelper().GetClient() |
31 | 33 |
if err != nil { |
32 | 34 |
glog.Fatalf("Error obtaining docker client: %v", err) |
... | ... |
@@ -57,6 +65,11 @@ func run(builderFactory factoryFunc) { |
57 | 57 |
dockercfg.PullAuthType, |
58 | 58 |
) |
59 | 59 |
} |
60 |
+ if len(build.Parameters.Source.SourceSecretName) > 0 { |
|
61 |
+ if err := setupSourceSecret(build.Parameters.Source.SourceSecretName, scmAuths); err != nil { |
|
62 |
+ glog.Fatalf("Cannot setup secret file for accessing private repository: %v", err) |
|
63 |
+ } |
|
64 |
+ } |
|
60 | 65 |
b := builderFactory(client, endpoint, authcfg, authPresent, &build) |
61 | 66 |
if err = b.Build(); err != nil { |
62 | 67 |
glog.Fatalf("Build error: %v", err) |
... | ... |
@@ -67,16 +80,83 @@ func run(builderFactory factoryFunc) { |
67 | 67 |
|
68 | 68 |
} |
69 | 69 |
|
70 |
+// fixSecretPermissions loweres access permissions to very low acceptable level |
|
71 |
+// TODO: this method should be removed as soon as secrets permissions are fixed upstream |
|
72 |
+func fixSecretPermissions() error { |
|
73 |
+ secretTmpDir, err := ioutil.TempDir("", "tmpsecret") |
|
74 |
+ if err != nil { |
|
75 |
+ return err |
|
76 |
+ } |
|
77 |
+ cmd := exec.Command("cp", "-R", ".", secretTmpDir) |
|
78 |
+ cmd.Dir = os.Getenv("SOURCE_SECRET_PATH") |
|
79 |
+ if err := cmd.Run(); err != nil { |
|
80 |
+ return err |
|
81 |
+ } |
|
82 |
+ secretFiles, err := ioutil.ReadDir(secretTmpDir) |
|
83 |
+ if err != nil { |
|
84 |
+ return err |
|
85 |
+ } |
|
86 |
+ for _, file := range secretFiles { |
|
87 |
+ if err := os.Chmod(filepath.Join(secretTmpDir, file.Name()), 0600); err != nil { |
|
88 |
+ return err |
|
89 |
+ } |
|
90 |
+ } |
|
91 |
+ os.Setenv("SOURCE_SECRET_PATH", secretTmpDir) |
|
92 |
+ return nil |
|
93 |
+} |
|
94 |
+ |
|
95 |
+func setupSourceSecret(sourceSecretName string, scmAuths []scmauth.SCMAuth) error { |
|
96 |
+ fixSecretPermissions() |
|
97 |
+ sourceSecretDir := os.Getenv("SOURCE_SECRET_PATH") |
|
98 |
+ files, err := ioutil.ReadDir(sourceSecretDir) |
|
99 |
+ if err != nil { |
|
100 |
+ return err |
|
101 |
+ } |
|
102 |
+ found := false |
|
103 |
+ |
|
104 |
+SCMAuthLoop: |
|
105 |
+ for _, scmAuth := range scmAuths { |
|
106 |
+ glog.V(3).Infof("Checking for '%s' in secret '%s'", scmAuth.Name(), sourceSecretName) |
|
107 |
+ for _, file := range files { |
|
108 |
+ if file.Name() == scmAuth.Name() { |
|
109 |
+ glog.Infof("Using '%s' from secret '%s'", scmAuth.Name(), sourceSecretName) |
|
110 |
+ if err := scmAuth.Setup(sourceSecretDir); err != nil { |
|
111 |
+ glog.Warningf("Error setting up '%s': %v", scmAuth.Name(), err) |
|
112 |
+ continue |
|
113 |
+ } |
|
114 |
+ found = true |
|
115 |
+ break SCMAuthLoop |
|
116 |
+ } |
|
117 |
+ } |
|
118 |
+ } |
|
119 |
+ if !found { |
|
120 |
+ return fmt.Errorf("the provided secret '%s' did not have any of the supported keys %v", |
|
121 |
+ sourceSecretName, getSCMNames(scmAuths)) |
|
122 |
+ } |
|
123 |
+ return nil |
|
124 |
+} |
|
125 |
+ |
|
126 |
+func getSCMNames(scmAuths []scmauth.SCMAuth) string { |
|
127 |
+ var names string |
|
128 |
+ for _, scmAuth := range scmAuths { |
|
129 |
+ if len(names) > 0 { |
|
130 |
+ names += ", " |
|
131 |
+ } |
|
132 |
+ names += scmAuth.Name() |
|
133 |
+ } |
|
134 |
+ return names |
|
135 |
+} |
|
136 |
+ |
|
70 | 137 |
// RunDockerBuild creates a docker builder and runs its build |
71 | 138 |
func RunDockerBuild() { |
72 | 139 |
run(func(client bld.DockerClient, sock string, auth docker.AuthConfiguration, present bool, build *api.Build) builder { |
73 | 140 |
return bld.NewDockerBuilder(client, auth, present, build) |
74 |
- }) |
|
141 |
+ }, []scmauth.SCMAuth{&scmauth.SSHPrivateKey{}}) |
|
75 | 142 |
} |
76 | 143 |
|
77 | 144 |
// RunSTIBuild creates a STI builder and runs its build |
78 | 145 |
func RunSTIBuild() { |
79 | 146 |
run(func(client bld.DockerClient, sock string, auth docker.AuthConfiguration, present bool, build *api.Build) builder { |
80 | 147 |
return bld.NewSTIBuilder(client, sock, auth, present, build) |
81 |
- }) |
|
148 |
+ }, []scmauth.SCMAuth{&scmauth.SSHPrivateKey{}}) |
|
82 | 149 |
} |
... | ... |
@@ -47,7 +47,7 @@ func (h *Helper) GetDockerAuth(registry, authType string) (docker.AuthConfigurat |
47 | 47 |
dockercfgPath = getDockercfgFile(pathForAuthType) |
48 | 48 |
} |
49 | 49 |
if _, err := os.Stat(dockercfgPath); err != nil { |
50 |
- glog.V(3).Infof("%s: %v", dockercfgPath, err) |
|
50 |
+ glog.V(3).Infof("Problem accessing %s: %v", dockercfgPath, err) |
|
51 | 51 |
return authCfg, false |
52 | 52 |
} |
53 | 53 |
cfg, err := readDockercfg(dockercfgPath) |
54 | 54 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,9 @@ |
0 |
+package scmauth |
|
1 |
+ |
|
2 |
+// SCMAuth is an interface implemented by different authentication providers |
|
3 |
+// which are responsible for setting up the credentials to be used when accessing |
|
4 |
+// private repository. |
|
5 |
+type SCMAuth interface { |
|
6 |
+ Name() string |
|
7 |
+ Setup(baseDir string) error |
|
8 |
+} |
0 | 9 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,40 @@ |
0 |
+package scmauth |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "io/ioutil" |
|
4 |
+ "os" |
|
5 |
+ "path/filepath" |
|
6 |
+) |
|
7 |
+ |
|
8 |
+const SSHPrivateKeyMethodName = "ssh-privatekey" |
|
9 |
+ |
|
10 |
+// SSHPrivateKey implements SCMAuth interface for using SSH private keys. |
|
11 |
+type SSHPrivateKey struct{} |
|
12 |
+ |
|
13 |
+// Setup creates a wrapper script for SSH command to be able to use the provided |
|
14 |
+// SSH key while accessing private repository. |
|
15 |
+func (_ SSHPrivateKey) Setup(baseDir string) error { |
|
16 |
+ script, err := ioutil.TempFile("", "gitssh") |
|
17 |
+ if err != nil { |
|
18 |
+ return err |
|
19 |
+ } |
|
20 |
+ defer script.Close() |
|
21 |
+ if err := script.Chmod(0711); err != nil { |
|
22 |
+ return err |
|
23 |
+ } |
|
24 |
+ if _, err := script.WriteString("#!/bin/sh\nssh -i " + |
|
25 |
+ filepath.Join(baseDir, SSHPrivateKeyMethodName) + |
|
26 |
+ " -o StrictHostKeyChecking=false \"$@\"\n"); err != nil { |
|
27 |
+ return err |
|
28 |
+ } |
|
29 |
+ // set environment variable to tell git to use the SSH wrapper |
|
30 |
+ if err := os.Setenv("GIT_SSH", script.Name()); err != nil { |
|
31 |
+ return err |
|
32 |
+ } |
|
33 |
+ return nil |
|
34 |
+} |
|
35 |
+ |
|
36 |
+// Name returns the name of this auth method. |
|
37 |
+func (_ SSHPrivateKey) Name() string { |
|
38 |
+ return SSHPrivateKeyMethodName |
|
39 |
+} |
... | ... |
@@ -79,5 +79,6 @@ func (bs *CustomBuildStrategy) CreateBuildPod(build *buildapi.Build) (*kapi.Pod, |
79 | 79 |
setupDockerSocket(pod) |
80 | 80 |
setupDockerSecrets(pod, build.Parameters.Output.PushSecretName) |
81 | 81 |
} |
82 |
+ setupSourceSecrets(pod, build.Parameters.Source.SourceSecretName) |
|
82 | 83 |
return pod, nil |
83 | 84 |
} |
... | ... |
@@ -53,21 +53,19 @@ func TestCustomCreateBuildPod(t *testing.T) { |
53 | 53 |
if actual.Spec.RestartPolicy != kapi.RestartPolicyNever { |
54 | 54 |
t.Errorf("Expected never, got %#v", actual.Spec.RestartPolicy) |
55 | 55 |
} |
56 |
- if len(container.VolumeMounts) != 2 { |
|
57 |
- t.Fatalf("Expected 2 volumes in container, got %d", len(container.VolumeMounts)) |
|
56 |
+ if len(container.VolumeMounts) != 3 { |
|
57 |
+ t.Fatalf("Expected 3 volumes in container, got %d", len(container.VolumeMounts)) |
|
58 | 58 |
} |
59 |
- if container.VolumeMounts[0].MountPath != dockerSocketPath { |
|
60 |
- t.Fatalf("Expected %s in first VolumeMount, got %s", dockerSocketPath, container.VolumeMounts[0].MountPath) |
|
61 |
- } |
|
62 |
- if container.VolumeMounts[1].MountPath != dockerPushSecretMountPath { |
|
63 |
- t.Fatalf("Expected %s in first VolumeMount, got %s", dockerPushSecretMountPath, container.VolumeMounts[1].MountPath) |
|
59 |
+ for i, expected := range []string{dockerSocketPath, dockerPushSecretMountPath, sourceSecretMountPath} { |
|
60 |
+ if container.VolumeMounts[i].MountPath != expected { |
|
61 |
+ t.Fatalf("Expected %s in VolumeMount[%d], got %s", expected, i, container.VolumeMounts[i].MountPath) |
|
62 |
+ } |
|
64 | 63 |
} |
65 | 64 |
if !kapi.Semantic.DeepEqual(container.Resources, expected.Parameters.Resources) { |
66 | 65 |
t.Fatalf("Expected actual=expected, %v != %v", container.Resources, expected.Parameters.Resources) |
67 | 66 |
} |
68 |
- |
|
69 |
- if len(actual.Spec.Volumes) != 2 { |
|
70 |
- t.Fatalf("Expected 2 volumes in Build pod, got %d", len(actual.Spec.Volumes)) |
|
67 |
+ if len(actual.Spec.Volumes) != 3 { |
|
68 |
+ t.Fatalf("Expected 3 volumes in Build pod, got %d", len(actual.Spec.Volumes)) |
|
71 | 69 |
} |
72 | 70 |
buildJSON, _ := v1beta1.Codec.Encode(expected) |
73 | 71 |
errorCases := map[int][]string{ |
... | ... |
@@ -110,6 +108,7 @@ func mockCustomBuild() *buildapi.Build { |
110 | 110 |
URI: "http://my.build.com/the/dockerbuild/Dockerfile", |
111 | 111 |
Ref: "master", |
112 | 112 |
}, |
113 |
+ SourceSecretName: "secretFoo", |
|
113 | 114 |
}, |
114 | 115 |
Strategy: buildapi.BuildStrategy{ |
115 | 116 |
Type: buildapi.CustomBuildStrategyType, |
... | ... |
@@ -55,5 +55,6 @@ func (bs *DockerBuildStrategy) CreateBuildPod(build *buildapi.Build) (*kapi.Pod, |
55 | 55 |
|
56 | 56 |
setupDockerSocket(pod) |
57 | 57 |
setupDockerSecrets(pod, build.Parameters.Output.PushSecretName) |
58 |
+ setupSourceSecrets(pod, build.Parameters.Source.SourceSecretName) |
|
58 | 59 |
return pod, nil |
59 | 60 |
} |
... | ... |
@@ -48,20 +48,19 @@ func TestDockerCreateBuildPod(t *testing.T) { |
48 | 48 |
if actual.Spec.RestartPolicy != kapi.RestartPolicyNever { |
49 | 49 |
t.Errorf("Expected never, got %#v", actual.Spec.RestartPolicy) |
50 | 50 |
} |
51 |
- if len(container.VolumeMounts) != 2 { |
|
52 |
- t.Fatalf("Expected 2 volumes in container, got %d", len(container.VolumeMounts)) |
|
51 |
+ if len(container.Env) != 4 { |
|
52 |
+ t.Fatalf("Expected 4 elements in Env table, got %d", len(container.Env)) |
|
53 | 53 |
} |
54 |
- if container.VolumeMounts[0].MountPath != dockerSocketPath { |
|
55 |
- t.Fatalf("Expected %s in first VolumeMount, got %s", dockerSocketPath, container.VolumeMounts[0].MountPath) |
|
54 |
+ if len(container.VolumeMounts) != 3 { |
|
55 |
+ t.Fatalf("Expected 3 volumes in container, got %d", len(container.VolumeMounts)) |
|
56 | 56 |
} |
57 |
- if container.VolumeMounts[1].MountPath != dockerPushSecretMountPath { |
|
58 |
- t.Fatalf("Expected %s in first VolumeMount, got %s", dockerPushSecretMountPath, container.VolumeMounts[1].MountPath) |
|
59 |
- } |
|
60 |
- if len(actual.Spec.Volumes) != 2 { |
|
61 |
- t.Fatalf("Expected 2 volumes in Build pod, got %d", len(actual.Spec.Volumes)) |
|
57 |
+ for i, expected := range []string{dockerSocketPath, dockerPushSecretMountPath, sourceSecretMountPath} { |
|
58 |
+ if container.VolumeMounts[i].MountPath != expected { |
|
59 |
+ t.Fatalf("Expected %s in VolumeMount[%d], got %s", expected, i, container.VolumeMounts[i].MountPath) |
|
60 |
+ } |
|
62 | 61 |
} |
63 |
- if len(container.Env) != 3 { |
|
64 |
- t.Fatalf("Expected 3 elements in Env table, got %d", len(container.Env)) |
|
62 |
+ if len(actual.Spec.Volumes) != 3 { |
|
63 |
+ t.Fatalf("Expected 3 volumes in Build pod, got %d", len(actual.Spec.Volumes)) |
|
65 | 64 |
} |
66 | 65 |
if !kapi.Semantic.DeepEqual(container.Resources, expected.Parameters.Resources) { |
67 | 66 |
t.Fatalf("Expected actual=expected, %v != %v", container.Resources, expected.Parameters.Resources) |
... | ... |
@@ -93,7 +92,8 @@ func mockDockerBuild() *buildapi.Build { |
93 | 93 |
Git: &buildapi.GitBuildSource{ |
94 | 94 |
URI: "http://my.build.com/the/dockerbuild/Dockerfile", |
95 | 95 |
}, |
96 |
- ContextDir: "my/test/dir", |
|
96 |
+ ContextDir: "my/test/dir", |
|
97 |
+ SourceSecretName: "secretFoo", |
|
97 | 98 |
}, |
98 | 99 |
Strategy: buildapi.BuildStrategy{ |
99 | 100 |
Type: buildapi.DockerBuildStrategyType, |
... | ... |
@@ -78,5 +78,6 @@ func (bs *STIBuildStrategy) CreateBuildPod(build *buildapi.Build) (*kapi.Pod, er |
78 | 78 |
|
79 | 79 |
setupDockerSocket(pod) |
80 | 80 |
setupDockerSecrets(pod, build.Parameters.Output.PushSecretName) |
81 |
+ setupSourceSecrets(pod, build.Parameters.Source.SourceSecretName) |
|
81 | 82 |
return pod, nil |
82 | 83 |
} |
... | ... |
@@ -56,21 +56,20 @@ func TestSTICreateBuildPod(t *testing.T) { |
56 | 56 |
t.Errorf("Expected never, got %#v", actual.Spec.RestartPolicy) |
57 | 57 |
} |
58 | 58 |
// strategy ENV is not copied into the container environment, so only |
59 |
- // expect 5 not 6 values. |
|
60 |
- if len(container.Env) != 5 { |
|
61 |
- t.Fatalf("Expected 5 elements in Env table, got %d", len(container.Env)) |
|
59 |
+ // expect 6 not 7 values. |
|
60 |
+ if len(container.Env) != 6 { |
|
61 |
+ t.Fatalf("Expected 6 elements in Env table, got %d", len(container.Env)) |
|
62 | 62 |
} |
63 |
- if len(container.VolumeMounts) != 2 { |
|
64 |
- t.Fatalf("Expected 2 volumes in container, got %d", len(container.VolumeMounts)) |
|
63 |
+ if len(container.VolumeMounts) != 3 { |
|
64 |
+ t.Fatalf("Expected 3 volumes in container, got %d", len(container.VolumeMounts)) |
|
65 | 65 |
} |
66 |
- if container.VolumeMounts[0].MountPath != dockerSocketPath { |
|
67 |
- t.Fatalf("Expected %s in first VolumeMount, got %s", dockerSocketPath, container.VolumeMounts[0].MountPath) |
|
68 |
- } |
|
69 |
- if container.VolumeMounts[1].MountPath != dockerPushSecretMountPath { |
|
70 |
- t.Fatalf("Expected %s in first VolumeMount, got %s", dockerPushSecretMountPath, container.VolumeMounts[1].MountPath) |
|
66 |
+ for i, expected := range []string{dockerSocketPath, dockerPushSecretMountPath, sourceSecretMountPath} { |
|
67 |
+ if container.VolumeMounts[i].MountPath != expected { |
|
68 |
+ t.Fatalf("Expected %s in VolumeMount[%d], got %s", expected, i, container.VolumeMounts[i].MountPath) |
|
69 |
+ } |
|
71 | 70 |
} |
72 |
- if len(actual.Spec.Volumes) != 2 { |
|
73 |
- t.Fatalf("Expected 2 volumes in Build pod, got %d", len(actual.Spec.Volumes)) |
|
71 |
+ if len(actual.Spec.Volumes) != 3 { |
|
72 |
+ t.Fatalf("Expected 3 volumes in Build pod, got %d", len(actual.Spec.Volumes)) |
|
74 | 73 |
} |
75 | 74 |
if !kapi.Semantic.DeepEqual(container.Resources, expected.Parameters.Resources) { |
76 | 75 |
t.Fatalf("Expected actual=expected, %v != %v", container.Resources, expected.Parameters.Resources) |
... | ... |
@@ -111,6 +110,7 @@ func mockSTIBuild() *buildapi.Build { |
111 | 111 |
Git: &buildapi.GitBuildSource{ |
112 | 112 |
URI: "http://my.build.com/the/stibuild/Dockerfile", |
113 | 113 |
}, |
114 |
+ SourceSecretName: "fooSecret", |
|
114 | 115 |
}, |
115 | 116 |
Strategy: buildapi.BuildStrategy{ |
116 | 117 |
Type: buildapi.STIBuildStrategyType, |
... | ... |
@@ -17,6 +17,7 @@ const ( |
17 | 17 |
// TODO: The pull secrets is the same as push secret for now. |
18 | 18 |
// This will be replaced using Service Account. |
19 | 19 |
dockerPullSecretMountPath = dockerPushSecretMountPath |
20 |
+ sourceSecretMountPath = "/var/run/secrets/source" |
|
20 | 21 |
) |
21 | 22 |
|
22 | 23 |
var whitelistEnvVarNames = []string{"BUILD_LOGLEVEL"} |
... | ... |
@@ -71,36 +72,55 @@ func setupBuildEnv(build *buildapi.Build, pod *kapi.Pod) error { |
71 | 71 |
return nil |
72 | 72 |
} |
73 | 73 |
|
74 |
-// setupDockerSecrets mounts Docker Registry secrets into Pod running the build, |
|
75 |
-// allowing Docker to authenticate against private registries or Docker Hub. |
|
76 |
-func setupDockerSecrets(pod *kapi.Pod, pushSecret string) { |
|
77 |
- if len(pushSecret) == 0 { |
|
78 |
- return |
|
79 |
- } |
|
80 |
- |
|
74 |
+// mountSecretVolume is a helper method responsible for actual mounting secret |
|
75 |
+// volumes into a pod. |
|
76 |
+func mountSecretVolume(pod *kapi.Pod, secretName, mountPath string) { |
|
81 | 77 |
volume := kapi.Volume{ |
82 |
- Name: pushSecret, |
|
78 |
+ Name: secretName, |
|
83 | 79 |
VolumeSource: kapi.VolumeSource{ |
84 | 80 |
Secret: &kapi.SecretVolumeSource{ |
85 |
- SecretName: pushSecret, |
|
81 |
+ SecretName: secretName, |
|
86 | 82 |
}, |
87 | 83 |
}, |
88 | 84 |
} |
89 | 85 |
volumeMount := kapi.VolumeMount{ |
90 |
- Name: pushSecret, |
|
91 |
- MountPath: dockerPushSecretMountPath, |
|
86 |
+ Name: secretName, |
|
87 |
+ MountPath: mountPath, |
|
92 | 88 |
ReadOnly: true, |
93 | 89 |
} |
94 |
- |
|
95 |
- glog.V(3).Infof("Installed %s as docker push secret in Pod %s", volumeMount.MountPath, pod.Name) |
|
96 | 90 |
pod.Spec.Volumes = append(pod.Spec.Volumes, volume) |
97 | 91 |
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, volumeMount) |
92 |
+} |
|
93 |
+ |
|
94 |
+// setupDockerSecrets mounts Docker Registry secrets into Pod running the build, |
|
95 |
+// allowing Docker to authenticate against private registries or Docker Hub. |
|
96 |
+func setupDockerSecrets(pod *kapi.Pod, pushSecret string) { |
|
97 |
+ if len(pushSecret) == 0 { |
|
98 |
+ return |
|
99 |
+ } |
|
100 |
+ |
|
101 |
+ mountSecretVolume(pod, pushSecret, dockerPushSecretMountPath) |
|
102 |
+ glog.V(3).Infof("Installed %s as docker push secret in Pod %s", dockerPushSecretMountPath, pod.Name) |
|
98 | 103 |
pod.Spec.Containers[0].Env = append(pod.Spec.Containers[0].Env, []kapi.EnvVar{ |
99 | 104 |
{Name: "PUSH_DOCKERCFG_PATH", Value: filepath.Join(dockerPushSecretMountPath, "dockercfg")}, |
100 | 105 |
{Name: "PULL_DOCKERCFG_PATH", Value: filepath.Join(dockerPullSecretMountPath, "dockercfg")}, |
101 | 106 |
}...) |
102 | 107 |
} |
103 | 108 |
|
109 |
+// setupSourceSecrets mounts SSH key used for accesing private SCM to clone |
|
110 |
+// application source code during build. |
|
111 |
+func setupSourceSecrets(pod *kapi.Pod, sourceSecret string) { |
|
112 |
+ if len(sourceSecret) == 0 { |
|
113 |
+ return |
|
114 |
+ } |
|
115 |
+ |
|
116 |
+ mountSecretVolume(pod, sourceSecret, sourceSecretMountPath) |
|
117 |
+ glog.V(3).Infof("Installed source secrets in %s, in Pod %s", sourceSecretMountPath, pod.Name) |
|
118 |
+ pod.Spec.Containers[0].Env = append(pod.Spec.Containers[0].Env, []kapi.EnvVar{ |
|
119 |
+ {Name: "SOURCE_SECRET_PATH", Value: sourceSecretMountPath}, |
|
120 |
+ }...) |
|
121 |
+} |
|
122 |
+ |
|
104 | 123 |
// mergeTrustedEnvWithoutDuplicates merges two environment lists without having |
105 | 124 |
// duplicate items in the output list. Only trusted environment variables |
106 | 125 |
// will be merged. |
... | ... |
@@ -173,6 +173,9 @@ func describeBuildParameters(p buildapi.BuildParameters, out *tabwriter.Writer) |
173 | 173 |
if len(p.Source.ContextDir) > 0 { |
174 | 174 |
formatString(out, "ContextDir", p.Source.ContextDir) |
175 | 175 |
} |
176 |
+ if len(p.Source.SourceSecretName) > 0 { |
|
177 |
+ formatString(out, "Source Secret", p.Source.SourceSecretName) |
|
178 |
+ } |
|
176 | 179 |
} |
177 | 180 |
if p.Output.To != nil { |
178 | 181 |
tag := imageapi.DefaultImageTag |
... | ... |
@@ -98,6 +98,15 @@ func TestValidate(t *testing.T) { |
98 | 98 |
env: map[string]string{}, |
99 | 99 |
parms: map[string]string{}, |
100 | 100 |
}, |
101 |
+ "git+sshsourcerepos": { |
|
102 |
+ cfg: AppConfig{ |
|
103 |
+ SourceRepositories: []string{"git@github.com:openshift/ruby-hello-world.git"}, |
|
104 |
+ }, |
|
105 |
+ componentValues: []string{}, |
|
106 |
+ sourceRepoLocations: []string{"git@github.com:openshift/ruby-hello-world.git"}, |
|
107 |
+ env: map[string]string{}, |
|
108 |
+ parms: map[string]string{}, |
|
109 |
+ }, |
|
101 | 110 |
"envs": { |
102 | 111 |
cfg: AppConfig{ |
103 | 112 |
Environment: util.StringList{"one=first", "two=second", "three=third"}, |
... | ... |
@@ -17,41 +17,30 @@ import ( |
17 | 17 |
// - file |
18 | 18 |
// - git |
19 | 19 |
func ParseRepository(s string) (*url.URL, error) { |
20 |
- switch { |
|
21 |
- case strings.HasPrefix(s, "git@"): |
|
22 |
- base := "git://" + strings.TrimPrefix(s, "git@") |
|
23 |
- url, err := url.Parse(base) |
|
24 |
- if err != nil { |
|
25 |
- return nil, err |
|
26 |
- } |
|
27 |
- return url, nil |
|
20 |
+ uri, err := url.Parse(s) |
|
21 |
+ if err != nil { |
|
22 |
+ return nil, err |
|
23 |
+ } |
|
28 | 24 |
|
29 |
- default: |
|
30 |
- uri, err := url.Parse(s) |
|
25 |
+ if uri.Scheme == "" && !strings.HasPrefix(uri.Path, "git@") { |
|
26 |
+ path := s |
|
27 |
+ ref := "" |
|
28 |
+ segments := strings.SplitN(path, "#", 2) |
|
29 |
+ if len(segments) == 2 { |
|
30 |
+ path, ref = segments[0], segments[1] |
|
31 |
+ } |
|
32 |
+ path, err := filepath.Abs(path) |
|
31 | 33 |
if err != nil { |
32 | 34 |
return nil, err |
33 | 35 |
} |
34 |
- |
|
35 |
- if uri.Scheme == "" { |
|
36 |
- path := s |
|
37 |
- ref := "" |
|
38 |
- segments := strings.SplitN(path, "#", 2) |
|
39 |
- if len(segments) == 2 { |
|
40 |
- path, ref = segments[0], segments[1] |
|
41 |
- } |
|
42 |
- path, err := filepath.Abs(path) |
|
43 |
- if err != nil { |
|
44 |
- return nil, err |
|
45 |
- } |
|
46 |
- uri = &url.URL{ |
|
47 |
- Scheme: "file", |
|
48 |
- Path: path, |
|
49 |
- Fragment: ref, |
|
50 |
- } |
|
36 |
+ uri = &url.URL{ |
|
37 |
+ Scheme: "file", |
|
38 |
+ Path: path, |
|
39 |
+ Fragment: ref, |
|
51 | 40 |
} |
52 |
- |
|
53 |
- return uri, nil |
|
54 | 41 |
} |
42 |
+ |
|
43 |
+ return uri, nil |
|
55 | 44 |
} |
56 | 45 |
|
57 | 46 |
// NameFromRepositoryURL suggests a name for a repository URL based on the last |