diff --git a/api/git/git.go b/api/git/git.go index 8758363b9..5e58363c7 100644 --- a/api/git/git.go +++ b/api/git/git.go @@ -1,6 +1,9 @@ package git import ( + "net/url" + "strings" + "gopkg.in/src-d/go-git.v4" ) @@ -14,12 +17,23 @@ func NewService(dataStorePath string) (*Service, error) { return service, nil } -// CloneRepository clones a git repository using the specified URL in the specified +// ClonePublicRepository clones a public git repository using the specified URL in the specified // destination folder. -func (service *Service) CloneRepository(url, destination string) error { - _, err := git.PlainClone(destination, false, &git.CloneOptions{ - URL: url, - }) +func (service *Service) ClonePublicRepository(repositoryURL, destination string) error { + return cloneRepository(repositoryURL, destination) +} +// ClonePrivateRepositoryWithBasicAuth clones a private git repository using the specified URL in the specified +// destination folder. It will use the specified username and password for basic HTTP authentication. +func (service *Service) ClonePrivateRepositoryWithBasicAuth(repositoryURL, destination, username, password string) error { + credentials := username + ":" + url.PathEscape(password) + repositoryURL = strings.Replace(repositoryURL, "://", "://"+credentials+"@", 1) + return cloneRepository(repositoryURL, destination) +} + +func cloneRepository(repositoryURL, destination string) error { + _, err := git.PlainClone(destination, false, &git.CloneOptions{ + URL: repositoryURL, + }) return err } diff --git a/api/http/handler/stack.go b/api/http/handler/stack.go index ff872896d..5b9e53187 100644 --- a/api/http/handler/stack.go +++ b/api/http/handler/stack.go @@ -70,12 +70,15 @@ func NewStackHandler(bouncer *security.RequestBouncer) *StackHandler { type ( postStacksRequest struct { - Name string `valid:"required"` - SwarmID string `valid:"required"` - StackFileContent string `valid:""` - GitRepository string `valid:""` - PathInRepository string `valid:""` - Env []portainer.Pair `valid:""` + Name string `valid:"required"` + SwarmID string `valid:"required"` + StackFileContent string `valid:""` + RepositoryURL string `valid:""` + RepositoryAuthentication bool `valid:""` + RepositoryUsername string `valid:""` + RepositoryPassword string `valid:""` + ComposeFilePathInRepository string `valid:""` + Env []portainer.Pair `valid:""` } postStacksResponse struct { ID string `json:"Id"` @@ -263,24 +266,20 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri } stackName := req.Name - if stackName == "" { - httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) - return - } - swarmID := req.SwarmID - if swarmID == "" { + + if stackName == "" || swarmID == "" || req.RepositoryURL == "" { httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) return } - if req.GitRepository == "" { + if req.RepositoryAuthentication && (req.RepositoryUsername == "" || req.RepositoryPassword == "") { httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) return } - if req.PathInRepository == "" { - req.PathInRepository = filesystem.ComposeFileDefaultName + if req.ComposeFilePathInRepository == "" { + req.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName } stacks, err := handler.StackService.Stacks() @@ -300,7 +299,7 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri ID: portainer.StackID(stackName + "_" + swarmID), Name: stackName, SwarmID: swarmID, - EntryPoint: req.PathInRepository, + EntryPoint: req.ComposeFilePathInRepository, Env: req.Env, } @@ -314,7 +313,11 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri return } - err = handler.GitService.CloneRepository(req.GitRepository, projectPath) + if req.RepositoryAuthentication { + err = handler.GitService.ClonePrivateRepositoryWithBasicAuth(req.RepositoryURL, projectPath, req.RepositoryUsername, req.RepositoryPassword) + } else { + err = handler.GitService.ClonePublicRepository(req.RepositoryURL, projectPath) + } if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return diff --git a/api/portainer.go b/api/portainer.go index 8c60b9973..fc98a6b36 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -375,7 +375,8 @@ type ( // GitService represents a service for managing Git. GitService interface { - CloneRepository(url, destination string) error + ClonePublicRepository(repositoryURL, destination string) error + ClonePrivateRepositoryWithBasicAuth(repositoryURL, destination, username, password string) error } // EndpointWatcher represents a service to synchronize the endpoints via an external source. diff --git a/api/swagger.yaml b/api/swagger.yaml index 4d2546919..d326d53a6 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -2904,14 +2904,26 @@ definitions: type: "string" example: "version: 3\n services:\n web:\n image:nginx" description: "Content of the Stack file. Required when using the 'string' deployment method." - GitRepository: + RepositoryURL: type: "string" example: "https://github.com/openfaas/faas" - description: "URL of a public Git repository hosting the Stack file. Required when using the 'repository' deployment method." - PathInRepository: + description: "URL of a Git repository hosting the Stack file. Required when using the 'repository' deployment method." + ComposeFilePathInRepository: type: "string" example: "docker-compose.yml" description: "Path to the Stack file inside the Git repository. Required when using the 'repository' deployment method." + RepositoryAuthentication: + type: "boolean" + example: true + description: "Use basic authentication to clone the Git repository." + RepositoryUsername: + type: "string" + example: "myGitUsername" + description: "Username used in basic authentication. Required when RepositoryAuthentication is true." + RepositoryPassword: + type: "string" + example: "myGitPassword" + description: "Password used in basic authentication. Required when RepositoryAuthentication is true." Env: type: "array" description: "A list of environment variables used during stack deployment" diff --git a/app/docker/services/stackService.js b/app/docker/services/stackService.js index 1d3e65411..9d0a08e13 100644 --- a/app/docker/services/stackService.js +++ b/app/docker/services/stackService.js @@ -124,7 +124,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic return deferred.promise; }; - service.createStackFromGitRepository = function(name, gitRepository, pathInRepository, env) { + service.createStackFromGitRepository = function(name, repositoryOptions, env) { var deferred = $q.defer(); SwarmService.swarm() @@ -133,8 +133,11 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic var payload = { Name: name, SwarmID: swarm.Id, - GitRepository: gitRepository, - PathInRepository: pathInRepository, + RepositoryURL: repositoryOptions.RepositoryURL, + ComposeFilePathInRepository: repositoryOptions.ComposeFilePathInRepository, + RepositoryAuthentication: repositoryOptions.RepositoryAuthentication, + RepositoryUsername: repositoryOptions.RepositoryUsername, + RepositoryPassword: repositoryOptions.RepositoryPassword, Env: env }; return Stack.create({ method: 'repository' }, payload).$promise; diff --git a/app/docker/views/stacks/create/createStackController.js b/app/docker/views/stacks/create/createStackController.js index cc11a680b..18c32be8f 100644 --- a/app/docker/views/stacks/create/createStackController.js +++ b/app/docker/views/stacks/create/createStackController.js @@ -7,8 +7,11 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid StackFileContent: '', StackFile: null, RepositoryURL: '', + RepositoryAuthentication: false, + RepositoryUsername: '', + RepositoryPassword: '', Env: [], - RepositoryPath: 'docker-compose.yml', + ComposeFilePathInRepository: 'docker-compose.yml', AccessControlData: new AccessControlFormData() }; @@ -48,9 +51,14 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid var stackFile = $scope.formValues.StackFile; return StackService.createStackFromFileUpload(name, stackFile, env); } else if (method === 'repository') { - var gitRepository = $scope.formValues.RepositoryURL; - var pathInRepository = $scope.formValues.RepositoryPath; - return StackService.createStackFromGitRepository(name, gitRepository, pathInRepository, env); + var repositoryOptions = { + RepositoryURL: $scope.formValues.RepositoryURL, + ComposeFilePathInRepository: $scope.formValues.ComposeFilePathInRepository, + RepositoryAuthentication: $scope.formValues.RepositoryAuthentication, + RepositoryUsername: $scope.formValues.RepositoryUsername, + RepositoryPassword: $scope.formValues.RepositoryPassword + }; + return StackService.createStackFromGitRepository(name, repositoryOptions, env); } } @@ -76,19 +84,17 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid createStack(name, method) .then(function success(data) { Notifications.success('Stack successfully deployed'); + return ResourceControlService.applyResourceControl('stack', name, userId, accessControlData, []) + .then(function success() { + $state.go('docker.stacks'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to apply resource control on the stack'); + }); }) .catch(function error(err) { Notifications.warning('Deployment error', err.err.data.err); }) - .then(function success(data) { - return ResourceControlService.applyResourceControl('stack', name, userId, accessControlData, []); - }) - .then(function success() { - $state.go('docker.stacks'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to apply resource control on the stack'); - }) .finally(function final() { $scope.state.actionInProgress = false; }); diff --git a/app/docker/views/stacks/create/createstack.html b/app/docker/views/stacks/create/createstack.html index 943277edc..887dd2228 100644 --- a/app/docker/views/stacks/create/createstack.html +++ b/app/docker/views/stacks/create/createstack.html @@ -113,7 +113,7 @@