Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/app/scripts/controllers/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ angular.module('openshiftConsole')

$scope.templatesByTag = {};

$scope.sourceURLPattern = /^(ftp|http|https|git):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
$scope.sourceURLPattern = /^((ftp|http|https|git):\/\/(\w+:{0,1}\w*@)|git@)?([^\s@]+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;

DataService.list("templates", $scope, function(templates) {
$scope.projectTemplates = templates.by("metadata.name");
Expand Down
70 changes: 70 additions & 0 deletions assets/test/spec/controllers/create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use strict";

describe("CreateController", function(){
var controller, form;
var $scope = {
projectTemplates: {},
openshiftTemplates: {},
templatesByTag: {}
};

beforeEach(function(){
inject(function(_$controller_){
// The injector unwraps the underscores (_) from around the parameter names when matching
controller = _$controller_("CreateController", {
$scope: $scope,
DataService: {
list: function(templates){}
}
});
});
});


it("valid http URL", function(){
var match = 'http://example.com/dir1/dir2'.match($scope.sourceURLPattern)
expect(match).not.toBeNull();
});

it("valid http URL, without http part", function(){
var match = 'example.com/dir1/dir2'.match($scope.sourceURLPattern)
expect(match).not.toBeNull();
});


it("valid http URL with user and password", function(){
var match = 'http://user:[email protected]/dir1/dir2'.match($scope.sourceURLPattern)
expect(match).not.toBeNull();
});

it("valid http URL with port", function(){
var match = 'http://example.com:8080/dir1/dir2'.match($scope.sourceURLPattern)
expect(match).not.toBeNull();
});

it("valid http URL with port, user and password", function(){
var match = 'http://user:[email protected]:8080/dir1/dir2'.match($scope.sourceURLPattern)
expect(match).not.toBeNull();
});

it("valid https URL", function(){
var match = 'https://example.com/dir1/dir2'.match($scope.sourceURLPattern)
expect(match).not.toBeNull();
});

it("valid ftp URL", function(){
var match = 'ftp://example.com/dir1/dir2'.match($scope.sourceURLPattern)
expect(match).not.toBeNull();
});

it("valid git+ssh URL", function(){
var match = '[email protected]:dir1/dir2'.match($scope.sourceURLPattern)
expect(match).not.toBeNull();
});

it("invalid git+ssh URL (double @@)", function(){
var match = 'git@@example.com:dir1/dir2'.match($scope.sourceURLPattern)
expect(match).toBeNull();
});

});
2 changes: 1 addition & 1 deletion pkg/assets/bindata.go
Original file line number Diff line number Diff line change
Expand Up @@ -16383,7 +16383,7 @@ namespace:"openshift"
}), g.info("openshift image repos", a.openshiftImageRepos);
});
} ]), angular.module("openshiftConsole").controller("CreateController", [ "$scope", "DataService", "$filter", "LabelFilter", "$location", "Logger", function(a, b, c, d, e, f) {
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) {
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) {
a.projectTemplates = b.by("metadata.name"), g(), f.info("project templates", a.projectTemplates);
}), b.list("templates", {
namespace:"openshift"
Expand Down
9 changes: 8 additions & 1 deletion pkg/build/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ type BuildSource struct {
// This allows to have buildable sources in directory other than root of
// repository.
ContextDir string `json:"contextDir,omitempty"`

// SourceSecretName is the name of a Secret that would be used for setting
// up the authentication for cloning private repository.
// The secret contains valid credentials for remote repository, where the
// data's key represent the authentication method to be used and value is
// the base64 encoded credentials. Supported auth methods are: ssh-privatekey.
SourceSecretName string
}

// SourceRevision is the revision or commit information from the source for the build
Expand Down Expand Up @@ -240,7 +247,7 @@ type BuildOutput struct {
// a Docker image repository to push to. Failure to find the To will result in a build error.
To *kapi.ObjectReference `json:"to,omitempty"`

// pushSecretName is the name of a Secret that would be used for setting
// PushSecretName is the name of a Secret that would be used for setting
// up the authentication for executing the Docker push to authentication
// enabled Docker Registry (or Docker Hub).
PushSecretName string `json:"pushSecretName,omitempty"`
Expand Down
9 changes: 8 additions & 1 deletion pkg/build/api/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ type BuildSource struct {
// This allows to have buildable sources in directory other than root of
// repository.
ContextDir string `json:"contextDir,omitempty"`

// SourceSecretName is the name of a Secret that would be used for setting
// up the authentication for cloning private repository.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more doc about what constitutes a valid secret today?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I should explain how to setup a secret over here, it'll only pollute build docs with unnecessary details, which should be covered in secrets. Of course there will be some more information around that topic in our docs, but I don't think it's necessary here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not looking for major detail, but "secret must be an SSH key for git auth" or something... our API turns out to be our docs frequently.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// The secret contains valid credentials for remote repository, where the
// secret's data key represent the authentication method to be used and value is
// the base64 encoded credentials. Supported auth methods are: ssh-privatekey.
SourceSecretName string `json:"sourceSecretName,omitempty" description:"supported auth methods are: ssh-privatekey`
}

// SourceRevision is the revision or commit information from the source for the build
Expand Down Expand Up @@ -278,7 +285,7 @@ type BuildOutput struct {
// a Docker image repository to push to.
To *kapi.ObjectReference `json:"to,omitempty"`

// pushSecretName is the name of a Secret that would be used for setting
// PushSecretName is the name of a Secret that would be used for setting
// up the authentication for executing the Docker push to authentication
// enabled Docker Registry (or Docker Hub).
PushSecretName string `json:"pushSecretName,omitempty"`
Expand Down
7 changes: 7 additions & 0 deletions pkg/build/api/v1beta3/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ type BuildSource struct {
// This allows to have buildable sources in directory other than root of
// repository.
ContextDir string `json:"contextDir,omitempty"`

// SourceSecretName is the name of a Secret that would be used for setting
// up the authentication for cloning private repository.
// The secret contains valid credentials for remote repository, where the
// data's key represent the authentication method to be used and value is
// the base64 encoded credentials. Supported auth methods are: ssh-privatekey.
SourceSecretName string `json:"sourceSecretName,omitempty" description:"supported auth methods are: ssh-privatekey`
}

// SourceRevision is the revision or commit information from the source for the build
Expand Down
86 changes: 83 additions & 3 deletions pkg/build/builder/cmd/builder.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package cmd

import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"

"github.com/fsouza/go-dockerclient"
"github.com/golang/glog"
"github.com/openshift/origin/pkg/api/latest"
"github.com/openshift/origin/pkg/build/api"
bld "github.com/openshift/origin/pkg/build/builder"
"github.com/openshift/origin/pkg/build/builder/cmd/dockercfg"
"github.com/openshift/origin/pkg/build/builder/cmd/scmauth"
dockerutil "github.com/openshift/origin/pkg/cmd/util/docker"
image "github.com/openshift/origin/pkg/image/api"
)
Expand All @@ -19,14 +24,17 @@ const DockerCfgFile = ".dockercfg"
type builder interface {
Build() error
}

type factoryFunc func(
client bld.DockerClient,
dockerSocket string,
authConfig docker.AuthConfiguration,
authPresent bool,
build *api.Build) builder

func run(builderFactory factoryFunc) {
// run is responsible for preparing environment for actual build.
// It accepts factoryFunc and an ordered array of SCMAuths.
func run(builderFactory factoryFunc, scmAuths []scmauth.SCMAuth) {
client, endpoint, err := dockerutil.NewHelper().GetClient()
if err != nil {
glog.Fatalf("Error obtaining docker client: %v", err)
Expand Down Expand Up @@ -57,6 +65,11 @@ func run(builderFactory factoryFunc) {
dockercfg.PullAuthType,
)
}
if len(build.Parameters.Source.SourceSecretName) > 0 {
if err := setupSourceSecret(build.Parameters.Source.SourceSecretName, scmAuths); err != nil {
glog.Fatalf("Cannot setup secret file for accessing private repository: %v", err)
}
}
b := builderFactory(client, endpoint, authcfg, authPresent, &build)
if err = b.Build(); err != nil {
glog.Fatalf("Build error: %v", err)
Expand All @@ -67,16 +80,83 @@ func run(builderFactory factoryFunc) {

}

// fixSecretPermissions loweres access permissions to very low acceptable level
// TODO: this method should be removed as soon as secrets permissions are fixed upstream
func fixSecretPermissions() error {
secretTmpDir, err := ioutil.TempDir("", "tmpsecret")
if err != nil {
return err
}
cmd := exec.Command("cp", "-R", ".", secretTmpDir)
cmd.Dir = os.Getenv("SOURCE_SECRET_PATH")
if err := cmd.Run(); err != nil {
return err
}
secretFiles, err := ioutil.ReadDir(secretTmpDir)
if err != nil {
return err
}
for _, file := range secretFiles {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use exec.Command here as well? You can then avoid listing files using Go and just "chmod -R 0600 dir"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was faster than chmod magic, just to change contents permissions. And passing * is a pain to exec.

if err := os.Chmod(filepath.Join(secretTmpDir, file.Name()), 0600); err != nil {
return err
}
}
os.Setenv("SOURCE_SECRET_PATH", secretTmpDir)
return nil
}

func setupSourceSecret(sourceSecretName string, scmAuths []scmauth.SCMAuth) error {
fixSecretPermissions()
sourceSecretDir := os.Getenv("SOURCE_SECRET_PATH")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you getting this via env var when you have the value stored already in 'secretTmpDir' ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TODO part will be removed as soon as secret permissions will be fixed. Then when removing the code between TODO's you wouldn't have to touch the rest of the code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@soltysh I will still remove this as it is confusing now. Move the os.Getenv() into comment with TODO.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// TODO: Replace this with the line below once the problem with secret permissions is fixed in upstream.
// sourceSecretDir := os.Getenv("SOURCE_SECRET_PATH")
sourceSecretDir := secretTmpDir

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also move the 'fixing' part into fixSecretPermissions() method and add note about removing that once the problem is fixed in upstream.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a viable option.

files, err := ioutil.ReadDir(sourceSecretDir)
if err != nil {
return err
}
found := false

SCMAuthLoop:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this necessary? can't this be done by a simple loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm iterating through two lists (files and secrets), without it break would just break the inner loop.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@soltysh you can have helper var that helps you break the outer loop

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the labeled break cleaner?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@soltysh it is harder to read the code ;-)

for _, scmAuth := range scmAuths {
glog.V(3).Infof("Checking for '%s' in secret '%s'", scmAuth.Name(), sourceSecretName)
for _, file := range files {
if file.Name() == scmAuth.Name() {
glog.Infof("Using '%s' from secret '%s'", scmAuth.Name(), sourceSecretName)
if err := scmAuth.Setup(sourceSecretDir); err != nil {
glog.Warningf("Error setting up '%s': %v", scmAuth.Name(), err)
continue
}
found = true
break SCMAuthLoop
}
}
}
if !found {
return fmt.Errorf("the provided secret '%s' did not have any of the supported keys %v",
sourceSecretName, getSCMNames(scmAuths))
}
return nil
}

func getSCMNames(scmAuths []scmauth.SCMAuth) string {
var names string
for _, scmAuth := range scmAuths {
if len(names) > 0 {
names += ", "
}
names += scmAuth.Name()
}
return names
}

// RunDockerBuild creates a docker builder and runs its build
func RunDockerBuild() {
run(func(client bld.DockerClient, sock string, auth docker.AuthConfiguration, present bool, build *api.Build) builder {
return bld.NewDockerBuilder(client, auth, present, build)
})
}, []scmauth.SCMAuth{&scmauth.SSHPrivateKey{}})
}

// RunSTIBuild creates a STI builder and runs its build
func RunSTIBuild() {
run(func(client bld.DockerClient, sock string, auth docker.AuthConfiguration, present bool, build *api.Build) builder {
return bld.NewSTIBuilder(client, sock, auth, present, build)
})
}, []scmauth.SCMAuth{&scmauth.SSHPrivateKey{}})
}
2 changes: 1 addition & 1 deletion pkg/build/builder/cmd/dockercfg/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (h *Helper) GetDockerAuth(registry, authType string) (docker.AuthConfigurat
dockercfgPath = getDockercfgFile(pathForAuthType)
}
if _, err := os.Stat(dockercfgPath); err != nil {
glog.V(3).Infof("%s: %v", dockercfgPath, err)
glog.V(3).Infof("Problem accessing %s: %v", dockercfgPath, err)
return authCfg, false
}
cfg, err := readDockercfg(dockercfgPath)
Expand Down
9 changes: 9 additions & 0 deletions pkg/build/builder/cmd/scmauth/scmauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package scmauth
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something inside me is telling me that should package should live elsewhere ("pkg/util/scm" ?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, generally speaking this will be one of those things we'll externalize when abstracting scm access, IMHO.
@smarterclayton opinions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want to externalize it, then there is no point on baking it here. externalizing this from its own package living in 'pkg/util' will be much easier ;-) also the naming seems wrong to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open for naming propositions, I suck at it 😉 As for location of that package, since this is tightly connected with builds, I've decided to put it here. As for externalization, I'm wondering how useful these auth methods will be for other SCM. I know mercurial supports SSH keys, but the setup might be different. If so then packages could be named then "pkg/util/scm/git/auth/"


// SCMAuth is an interface implemented by different authentication providers
// which are responsible for setting up the credentials to be used when accessing
// private repository.
type SCMAuth interface {
Name() string
Setup(baseDir string) error
}
40 changes: 40 additions & 0 deletions pkg/build/builder/cmd/scmauth/sshkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package scmauth
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be "pkg/util/scm/auth/key.go"


import (
"io/ioutil"
"os"
"path/filepath"
)

const SSHPrivateKeyMethodName = "ssh-privatekey"

// SSHPrivateKey implements SCMAuth interface for using SSH private keys.
type SSHPrivateKey struct{}

// Setup creates a wrapper script for SSH command to be able to use the provided
// SSH key while accessing private repository.
func (_ SSHPrivateKey) Setup(baseDir string) error {
script, err := ioutil.TempFile("", "gitssh")
if err != nil {
return err
}
defer script.Close()
if err := script.Chmod(0711); err != nil {
return err
}
if _, err := script.WriteString("#!/bin/sh\nssh -i " +
filepath.Join(baseDir, SSHPrivateKeyMethodName) +
" -o StrictHostKeyChecking=false \"$@\"\n"); err != nil {
return err
}
// set environment variable to tell git to use the SSH wrapper
if err := os.Setenv("GIT_SSH", script.Name()); err != nil {
return err
}
return nil
}

// Name returns the name of this auth method.
func (_ SSHPrivateKey) Name() string {
return SSHPrivateKeyMethodName
}
1 change: 1 addition & 0 deletions pkg/build/controller/strategy/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,6 @@ func (bs *CustomBuildStrategy) CreateBuildPod(build *buildapi.Build) (*kapi.Pod,
setupDockerSocket(pod)
setupDockerSecrets(pod, build.Parameters.Output.PushSecretName)
}
setupSourceSecrets(pod, build.Parameters.Source.SourceSecretName)
return pod, nil
}
19 changes: 9 additions & 10 deletions pkg/build/controller/strategy/custom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,19 @@ func TestCustomCreateBuildPod(t *testing.T) {
if actual.Spec.RestartPolicy != kapi.RestartPolicyNever {
t.Errorf("Expected never, got %#v", actual.Spec.RestartPolicy)
}
if len(container.VolumeMounts) != 2 {
t.Fatalf("Expected 2 volumes in container, got %d", len(container.VolumeMounts))
if len(container.VolumeMounts) != 3 {
t.Fatalf("Expected 3 volumes in container, got %d", len(container.VolumeMounts))
}
if container.VolumeMounts[0].MountPath != dockerSocketPath {
t.Fatalf("Expected %s in first VolumeMount, got %s", dockerSocketPath, container.VolumeMounts[0].MountPath)
}
if container.VolumeMounts[1].MountPath != dockerPushSecretMountPath {
t.Fatalf("Expected %s in first VolumeMount, got %s", dockerPushSecretMountPath, container.VolumeMounts[1].MountPath)
for i, expected := range []string{dockerSocketPath, dockerPushSecretMountPath, sourceSecretMountPath} {
if container.VolumeMounts[i].MountPath != expected {
t.Fatalf("Expected %s in VolumeMount[%d], got %s", expected, i, container.VolumeMounts[i].MountPath)
}
}
if !kapi.Semantic.DeepEqual(container.Resources, expected.Parameters.Resources) {
t.Fatalf("Expected actual=expected, %v != %v", container.Resources, expected.Parameters.Resources)
}

if len(actual.Spec.Volumes) != 2 {
t.Fatalf("Expected 2 volumes in Build pod, got %d", len(actual.Spec.Volumes))
if len(actual.Spec.Volumes) != 3 {
t.Fatalf("Expected 3 volumes in Build pod, got %d", len(actual.Spec.Volumes))
}
buildJSON, _ := v1beta1.Codec.Encode(expected)
errorCases := map[int][]string{
Expand Down Expand Up @@ -110,6 +108,7 @@ func mockCustomBuild() *buildapi.Build {
URI: "http://my.build.com/the/dockerbuild/Dockerfile",
Ref: "master",
},
SourceSecretName: "secretFoo",
},
Strategy: buildapi.BuildStrategy{
Type: buildapi.CustomBuildStrategyType,
Expand Down
1 change: 1 addition & 0 deletions pkg/build/controller/strategy/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,6 @@ func (bs *DockerBuildStrategy) CreateBuildPod(build *buildapi.Build) (*kapi.Pod,

setupDockerSocket(pod)
setupDockerSecrets(pod, build.Parameters.Output.PushSecretName)
setupSourceSecrets(pod, build.Parameters.Source.SourceSecretName)
return pod, nil
}
Loading