mirror of https://github.com/portainer/portainer
feat(stack-creation): add the ability to specify git credentials (#1722)
* feat(stack-creation): add the ability to specify git credentials * docs(api): update Swaggerpull/1330/merge
parent
50ece68f35
commit
adf1ba7b47
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue