feat(stack): Introduce SSH Key Based Deployment

pull/2972/head
ssbkang 2019-07-02 22:41:01 +12:00
parent 5d169ad827
commit ef800d5a7d
6 changed files with 74 additions and 70 deletions

View File

@ -47,10 +47,6 @@ func cloneRepository(repositoryURL, referenceName string, destination string) er
} }
func (service *Service) ClonePrivateRepositoryWithDeploymentKey(repositoryURL, referenceName string, destination string, privateKeyPem []byte) error { func (service *Service) ClonePrivateRepositoryWithDeploymentKey(repositoryURL, referenceName string, destination string, privateKeyPem []byte) error {
// url := "git@github.com:ssbkang/personal-development.git"
// directory := "personal-development"
// https://github.com/portainer/portainer-compose
signer, _ := ssh.ParsePrivateKey(privateKeyPem) signer, _ := ssh.ParsePrivateKey(privateKeyPem)
auth := &gitSsh.PublicKeys{ auth := &gitSsh.PublicKeys{
User: "git", User: "git",

View File

@ -36,8 +36,6 @@ func (handler *Handler) deploymentkeyCreate(w http.ResponseWriter, r *http.Reque
return &httperror.HandlerError{http.StatusConflict, "A deploymentkey for this resource already exists", portainer.ErrDeploymentkeyAlreadyExists} return &httperror.HandlerError{http.StatusConflict, "A deploymentkey for this resource already exists", portainer.ErrDeploymentkeyAlreadyExists}
} }
// Add a function to call and create public key and private key
private, public, err := handler.SignatureService.GenerateDeploymentKeyPair() private, public, err := handler.SignatureService.GenerateDeploymentKeyPair()
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create private and public key pairs", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create private and public key pairs", err}

View File

@ -26,16 +26,12 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(), Router: mux.NewRouter(),
} }
h.Handle("/deployment_keys", h.Handle("/deployment_keys",
// bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploymentkeyCreate))).Methods(http.MethodPost) bouncer.AuthorizedAccess(httperror.LoggerHandler(h.deploymentkeyCreate))).Methods(http.MethodPost)
bouncer.PublicAccess(httperror.LoggerHandler(h.deploymentkeyCreate))).Methods(http.MethodPost)
h.Handle("/deployment_keys", h.Handle("/deployment_keys",
// bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploymentkeyList))).Methods(http.MethodGet) bouncer.AuthorizedAccess(httperror.LoggerHandler(h.deploymentkeyList))).Methods(http.MethodGet)
bouncer.PublicAccess(httperror.LoggerHandler(h.deploymentkeyList))).Methods(http.MethodGet)
h.Handle("/deployment_keys/{id}", h.Handle("/deployment_keys/{id}",
// bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploymentkeyList))).Methods(http.MethodGet) bouncer.AuthorizedAccess(httperror.LoggerHandler(h.deploymentkeyInspect))).Methods(http.MethodGet)
bouncer.PublicAccess(httperror.LoggerHandler(h.deploymentkeyInspect))).Methods(http.MethodGet)
h.Handle("/deployment_keys/{id}", h.Handle("/deployment_keys/{id}",
// bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploymentkeyDelete))).Methods(http.MethodDelete) bouncer.AuthorizedAccess(httperror.LoggerHandler(h.deploymentkeyDelete))).Methods(http.MethodDelete)
bouncer.PublicAccess(httperror.LoggerHandler(h.deploymentkeyDelete))).Methods(http.MethodDelete)
return h return h
} }

View File

@ -87,17 +87,16 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
return response.JSON(w, stack) return response.JSON(w, stack)
} }
// Update struc to cater for deployment key
type composeStackFromGitRepositoryPayload struct { type composeStackFromGitRepositoryPayload struct {
Name string Name string
RepositoryURL string RepositoryURL string
RepositoryReferenceName string RepositoryReferenceName string
RepositoryAuthentication bool RepositoryAuthenticationType string
RepositoryDeploymentKey string RepositoryDeploymentKey string
RepositoryUsername string RepositoryUsername string
RepositoryPassword string RepositoryPassword string
ComposeFilePathInRepository string ComposeFilePathInRepository string
Env []portainer.Pair Env []portainer.Pair
} }
func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) error { func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) error {
@ -107,8 +106,11 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) { if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return portainer.Error("Invalid repository URL. Must correspond to a valid URL format") return portainer.Error("Invalid repository URL. Must correspond to a valid URL format")
} }
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) { if payload.RepositoryAuthenticationType == "BasicAuth" && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
return portainer.Error("Invalid repository credentials. Username and password must be specified when authentication is enabled") return portainer.Error("Invalid repository credentials or deploymenet key. Either username & password must be specified when authentication type is BasicAuth")
}
if payload.RepositoryAuthenticationType == "DeploymentKey" && govalidator.IsNull(payload.RepositoryDeploymentKey) {
return portainer.Error("Invalid repository deploymenet key. A deployment key must be specified when authentication type is DeploymentKey")
} }
if govalidator.IsNull(payload.ComposeFilePathInRepository) { if govalidator.IsNull(payload.ComposeFilePathInRepository) {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
@ -147,15 +149,22 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID))) projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath stack.ProjectPath = projectPath
// Add Deployment Key
gitCloneParams := &cloneRepositoryParameters{ gitCloneParams := &cloneRepositoryParameters{
url: payload.RepositoryURL, url: payload.RepositoryURL,
referenceName: payload.RepositoryReferenceName, referenceName: payload.RepositoryReferenceName,
path: projectPath, path: projectPath,
authentication: payload.RepositoryAuthentication, authenticationType: payload.RepositoryAuthenticationType,
deploymentKey: payload.RepositoryDeploymentKey, deploymentKey: nil,
username: payload.RepositoryUsername, username: payload.RepositoryUsername,
password: payload.RepositoryPassword, password: payload.RepositoryPassword,
}
if payload.RepositoryDeploymentKey != "" {
deploymentKey, err := handler.DeploymentKeyService.DeploymentKeyByName(payload.Name)
if err != nil && err != portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "An error occurred retrieving deploymentkey from the database", err}
}
gitCloneParams.deploymentKey = deploymentKey.PrivateKey
} }
doCleanUp := true doCleanUp := true

View File

@ -93,16 +93,16 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
// Update struc to cater for deployment key // Update struc to cater for deployment key
type swarmStackFromGitRepositoryPayload struct { type swarmStackFromGitRepositoryPayload struct {
Name string Name string
SwarmID string SwarmID string
Env []portainer.Pair Env []portainer.Pair
RepositoryURL string RepositoryURL string
RepositoryReferenceName string RepositoryReferenceName string
RepositoryAuthentication bool RepositoryAuthenticationType string
RepositoryDeploymentKey string RepositoryDeploymentKey string
RepositoryUsername string RepositoryUsername string
RepositoryPassword string RepositoryPassword string
ComposeFilePathInRepository string ComposeFilePathInRepository string
} }
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error { func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
@ -112,18 +112,15 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
if govalidator.IsNull(payload.SwarmID) { if govalidator.IsNull(payload.SwarmID) {
return portainer.Error("Invalid Swarm ID") return portainer.Error("Invalid Swarm ID")
} }
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) { if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return portainer.Error("Invalid repository URL. Must correspond to a valid URL format") return portainer.Error("Invalid repository URL. Must correspond to a valid URL format")
} }
if payload.RepositoryAuthenticationType == "BasicAuth" && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
// Need to improve validators for SSH based URL and deployment key type return portainer.Error("Invalid repository credentials or deploymenet key. Either username & password must be specified when authentication type is BasicAuth")
/* }
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryDeploymentKey)) || (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) { if payload.RepositoryAuthenticationType == "DeploymentKey" && govalidator.IsNull(payload.RepositoryDeploymentKey) {
return portainer.Error("Invalid repository credentials or deploymenet key. Either Username & password or a deployment key must be specified when authentication is enabled") return portainer.Error("Invalid repository deploymenet key. A deployment key must be specified when authentication type is DeploymentKey")
} }
*/
if govalidator.IsNull(payload.ComposeFilePathInRepository) { if govalidator.IsNull(payload.ComposeFilePathInRepository) {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
} }
@ -163,13 +160,22 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
stack.ProjectPath = projectPath stack.ProjectPath = projectPath
gitCloneParams := &cloneRepositoryParameters{ gitCloneParams := &cloneRepositoryParameters{
url: payload.RepositoryURL, url: payload.RepositoryURL,
referenceName: payload.RepositoryReferenceName, referenceName: payload.RepositoryReferenceName,
path: projectPath, path: projectPath,
authentication: payload.RepositoryAuthentication, authenticationType: payload.RepositoryAuthenticationType,
deploymentKey: payload.RepositoryDeploymentKey, deploymentKey: nil,
username: payload.RepositoryUsername, username: payload.RepositoryUsername,
password: payload.RepositoryPassword, password: payload.RepositoryPassword,
}
if payload.RepositoryDeploymentKey != "" {
deploymentKey, err := handler.DeploymentKeyService.DeploymentKeyByName(payload.RepositoryDeploymentKey)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "An error occurred retrieving deploymentkey from the database", err}
}
gitCloneParams.deploymentKey = deploymentKey.PrivateKey
} }
doCleanUp := true doCleanUp := true

View File

@ -1,22 +1,21 @@
package stacks package stacks
type cloneRepositoryParameters struct { type cloneRepositoryParameters struct {
url string url string
referenceName string referenceName string
path string path string
authentication bool authenticationType string
deploymentKey string deploymentKey []byte
username string username string
password string password string
} }
func (handler *Handler) cloneGitRepository(parameters *cloneRepositoryParameters) error { func (handler *Handler) cloneGitRepository(parameters *cloneRepositoryParameters) error {
if parameters.authentication && parameters.username != "" && parameters.password != "" { if parameters.authenticationType != "" && parameters.username != "" && parameters.password != "" {
return handler.GitService.ClonePrivateRepositoryWithBasicAuth(parameters.url, parameters.referenceName, parameters.path, parameters.username, parameters.password) return handler.GitService.ClonePrivateRepositoryWithBasicAuth(parameters.url, parameters.referenceName, parameters.path, parameters.username, parameters.password)
} }
if parameters.authentication && parameters.deploymentKey != "" { if parameters.authenticationType != "" && parameters.deploymentKey != nil {
deploymentKey, _ := handler.DeploymentKeyService.DeploymentKeyByName(parameters.deploymentKey) return handler.GitService.ClonePrivateRepositoryWithDeploymentKey(parameters.url, parameters.referenceName, parameters.path, parameters.deploymentKey)
return handler.GitService.ClonePrivateRepositoryWithDeploymentKey(parameters.url, parameters.referenceName, parameters.path, deploymentKey.PrivateKey)
} }
return handler.GitService.ClonePublicRepository(parameters.url, parameters.referenceName, parameters.path) return handler.GitService.ClonePublicRepository(parameters.url, parameters.referenceName, parameters.path)
} }