Browse code

Card origin_devexp_286 - Added ssh key-based access to private git repository.

Maciej Szulik authored on 2015/05/08 01:36:23
Showing 22 changed files
... ...
@@ -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.
... ...
@@ -57,7 +57,6 @@ func TestSetupDockerSocketHostSocket(t *testing.T) {
57 57
 }
58 58
 
59 59
 func isVolumeSourceEmpty(volumeSource kapi.VolumeSource) bool {
60
-
61 60
 	if volumeSource.EmptyDir == nil &&
62 61
 		volumeSource.HostPath == nil &&
63 62
 		volumeSource.GCEPersistentDisk == nil &&
... ...
@@ -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"},
... ...
@@ -46,6 +46,7 @@ func NewSourceRepository(s string) (*SourceRepository, error) {
46 46
 	if err != nil {
47 47
 		return nil, err
48 48
 	}
49
+
49 50
 	return &SourceRepository{
50 51
 		location: s,
51 52
 		url:      *location,
... ...
@@ -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