feat(stack-creation): add the ability to specify git credentials (#1722)

* feat(stack-creation): add the ability to specify git credentials

* docs(api): update Swagger
pull/1330/merge
Anthony Lapenna 2018-03-16 07:22:05 +10:00 committed by GitHub
parent 50ece68f35
commit adf1ba7b47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 106 additions and 45 deletions

View File

@ -1,6 +1,9 @@
package git package git
import ( import (
"net/url"
"strings"
"gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4"
) )
@ -14,12 +17,23 @@ func NewService(dataStorePath string) (*Service, error) {
return service, nil 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. // destination folder.
func (service *Service) CloneRepository(url, destination string) error { func (service *Service) ClonePublicRepository(repositoryURL, destination string) error {
_, err := git.PlainClone(destination, false, &git.CloneOptions{ return cloneRepository(repositoryURL, destination)
URL: url, }
})
// 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 return err
} }

View File

@ -70,12 +70,15 @@ func NewStackHandler(bouncer *security.RequestBouncer) *StackHandler {
type ( type (
postStacksRequest struct { postStacksRequest struct {
Name string `valid:"required"` Name string `valid:"required"`
SwarmID string `valid:"required"` SwarmID string `valid:"required"`
StackFileContent string `valid:""` StackFileContent string `valid:""`
GitRepository string `valid:""` RepositoryURL string `valid:""`
PathInRepository string `valid:""` RepositoryAuthentication bool `valid:""`
Env []portainer.Pair `valid:""` RepositoryUsername string `valid:""`
RepositoryPassword string `valid:""`
ComposeFilePathInRepository string `valid:""`
Env []portainer.Pair `valid:""`
} }
postStacksResponse struct { postStacksResponse struct {
ID string `json:"Id"` ID string `json:"Id"`
@ -263,24 +266,20 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri
} }
stackName := req.Name stackName := req.Name
if stackName == "" {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
swarmID := req.SwarmID swarmID := req.SwarmID
if swarmID == "" {
if stackName == "" || swarmID == "" || req.RepositoryURL == "" {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return return
} }
if req.GitRepository == "" { if req.RepositoryAuthentication && (req.RepositoryUsername == "" || req.RepositoryPassword == "") {
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return return
} }
if req.PathInRepository == "" { if req.ComposeFilePathInRepository == "" {
req.PathInRepository = filesystem.ComposeFileDefaultName req.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
} }
stacks, err := handler.StackService.Stacks() stacks, err := handler.StackService.Stacks()
@ -300,7 +299,7 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri
ID: portainer.StackID(stackName + "_" + swarmID), ID: portainer.StackID(stackName + "_" + swarmID),
Name: stackName, Name: stackName,
SwarmID: swarmID, SwarmID: swarmID,
EntryPoint: req.PathInRepository, EntryPoint: req.ComposeFilePathInRepository,
Env: req.Env, Env: req.Env,
} }
@ -314,7 +313,11 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri
return 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 { if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return return

View File

@ -375,7 +375,8 @@ type (
// GitService represents a service for managing Git. // GitService represents a service for managing Git.
GitService interface { 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. // EndpointWatcher represents a service to synchronize the endpoints via an external source.

View File

@ -2904,14 +2904,26 @@ definitions:
type: "string" type: "string"
example: "version: 3\n services:\n web:\n image:nginx" example: "version: 3\n services:\n web:\n image:nginx"
description: "Content of the Stack file. Required when using the 'string' deployment method." description: "Content of the Stack file. Required when using the 'string' deployment method."
GitRepository: RepositoryURL:
type: "string" type: "string"
example: "https://github.com/openfaas/faas" example: "https://github.com/openfaas/faas"
description: "URL of a public Git repository hosting the Stack file. Required when using the 'repository' deployment method." description: "URL of a Git repository hosting the Stack file. Required when using the 'repository' deployment method."
PathInRepository: ComposeFilePathInRepository:
type: "string" type: "string"
example: "docker-compose.yml" example: "docker-compose.yml"
description: "Path to the Stack file inside the Git repository. Required when using the 'repository' deployment method." 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: Env:
type: "array" type: "array"
description: "A list of environment variables used during stack deployment" description: "A list of environment variables used during stack deployment"

View File

@ -124,7 +124,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
return deferred.promise; return deferred.promise;
}; };
service.createStackFromGitRepository = function(name, gitRepository, pathInRepository, env) { service.createStackFromGitRepository = function(name, repositoryOptions, env) {
var deferred = $q.defer(); var deferred = $q.defer();
SwarmService.swarm() SwarmService.swarm()
@ -133,8 +133,11 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
var payload = { var payload = {
Name: name, Name: name,
SwarmID: swarm.Id, SwarmID: swarm.Id,
GitRepository: gitRepository, RepositoryURL: repositoryOptions.RepositoryURL,
PathInRepository: pathInRepository, ComposeFilePathInRepository: repositoryOptions.ComposeFilePathInRepository,
RepositoryAuthentication: repositoryOptions.RepositoryAuthentication,
RepositoryUsername: repositoryOptions.RepositoryUsername,
RepositoryPassword: repositoryOptions.RepositoryPassword,
Env: env Env: env
}; };
return Stack.create({ method: 'repository' }, payload).$promise; return Stack.create({ method: 'repository' }, payload).$promise;

View File

@ -7,8 +7,11 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid
StackFileContent: '', StackFileContent: '',
StackFile: null, StackFile: null,
RepositoryURL: '', RepositoryURL: '',
RepositoryAuthentication: false,
RepositoryUsername: '',
RepositoryPassword: '',
Env: [], Env: [],
RepositoryPath: 'docker-compose.yml', ComposeFilePathInRepository: 'docker-compose.yml',
AccessControlData: new AccessControlFormData() AccessControlData: new AccessControlFormData()
}; };
@ -48,9 +51,14 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid
var stackFile = $scope.formValues.StackFile; var stackFile = $scope.formValues.StackFile;
return StackService.createStackFromFileUpload(name, stackFile, env); return StackService.createStackFromFileUpload(name, stackFile, env);
} else if (method === 'repository') { } else if (method === 'repository') {
var gitRepository = $scope.formValues.RepositoryURL; var repositoryOptions = {
var pathInRepository = $scope.formValues.RepositoryPath; RepositoryURL: $scope.formValues.RepositoryURL,
return StackService.createStackFromGitRepository(name, gitRepository, pathInRepository, env); 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) createStack(name, method)
.then(function success(data) { .then(function success(data) {
Notifications.success('Stack successfully deployed'); 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) { .catch(function error(err) {
Notifications.warning('Deployment error', err.err.data.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() { .finally(function final() {
$scope.state.actionInProgress = false; $scope.state.actionInProgress = false;
}); });

View File

@ -113,7 +113,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<span class="col-sm-12 text-muted small"> <span class="col-sm-12 text-muted small">
You can use the URL of a public git repository. You can use the URL of a git repository.
</span> </span>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -130,7 +130,29 @@
<div class="form-group"> <div class="form-group">
<label for="stack_repository_path" class="col-sm-2 control-label text-left">Compose path</label> <label for="stack_repository_path" class="col-sm-2 control-label text-left">Compose path</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" class="form-control" ng-model="formValues.RepositoryPath" id="stack_repository_path" placeholder="docker-compose.yml"> <input type="text" class="form-control" ng-model="formValues.ComposeFilePathInRepository" id="stack_repository_path" placeholder="docker-compose.yml">
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Authentication
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.RepositoryAuthentication"><i></i>
</label>
</div>
</div>
<div class="form-group" ng-if="formValues.RepositoryAuthentication">
<label for="repository_username" class="col-sm-1 control-label text-left">Username</label>
<div class="col-sm-11 col-md-5">
<input type="text" class="form-control" ng-model="formValues.RepositoryUsername" name="repository_username" placeholder="myGitUser">
</div>
<label for="repository_password" class="col-sm-1 margin-sm-top control-label text-left">
Password
</label>
<div class="col-sm-11 col-md-5 margin-sm-top">
<input type="password" class="form-control" ng-model="formValues.RepositoryPassword" name="repository_password" placeholder="myPassword">
</div> </div>
</div> </div>
</div> </div>
@ -174,7 +196,7 @@
<div class="col-sm-12"> <div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress <button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress
|| (state.Method === 'upload' && !formValues.StackFile) || (state.Method === 'upload' && !formValues.StackFile)
|| (state.Method === 'repository' && (!formValues.RepositoryURL || !formValues.RepositoryPath)) || (state.Method === 'repository' && ((!formValues.RepositoryURL || !formValues.ComposeFilePathInRepository) || (formValues.RepositoryAuthentication && (!formValues.RepositoryUsername || !formValues.RepositoryPassword))))
|| !formValues.Name" ng-click="deployStack()" button-spinner="state.actionInProgress"> || !formValues.Name" ng-click="deployStack()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress">Deploy the stack</span> <span ng-hide="state.actionInProgress">Deploy the stack</span>
<span ng-show="state.actionInProgress">Deployment in progress...</span> <span ng-show="state.actionInProgress">Deployment in progress...</span>