agola/internal/services/gateway/action/project.go
Simone Gotti c1ff28ef9f *: export clients and related types
Export clients and related packages.

The main rule is to not import internal packages from exported packages.

The gateway client and related types are totally decoupled from the gateway
service (not shared types between the client and the server).

Instead the configstore and the runservice client currently share many types
that are now exported (decoupling them will require that a lot of types must be
duplicated and the need of functions to convert between them, this will be done
in future when the APIs will be declared as stable).
2019-08-02 12:02:01 +02:00

593 lines
19 KiB
Go

// Copyright 2019 Sorint.lab
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied
// See the License for the specific language governing permissions and
// limitations under the License.
package action
import (
"context"
"fmt"
"net/http"
"net/url"
"path"
gitsource "agola.io/agola/internal/gitsources"
"agola.io/agola/internal/services/types"
"agola.io/agola/internal/util"
csapitypes "agola.io/agola/services/configstore/api/types"
cstypes "agola.io/agola/services/configstore/types"
errors "golang.org/x/xerrors"
)
func (h *ActionHandler) GetProject(ctx context.Context, projectRef string) (*csapitypes.Project, error) {
project, resp, err := h.configstoreClient.GetProject(ctx, projectRef)
if err != nil {
return nil, ErrFromRemote(resp, err)
}
isProjectMember, err := h.IsProjectMember(ctx, project.OwnerType, project.OwnerID)
if err != nil {
return nil, errors.Errorf("failed to determine ownership: %w", err)
}
if project.GlobalVisibility == cstypes.VisibilityPublic {
return project, nil
}
if !isProjectMember {
return nil, util.NewErrForbidden(errors.Errorf("user not authorized"))
}
return project, nil
}
type CreateProjectRequest struct {
Name string
ParentRef string
Visibility cstypes.Visibility
RemoteSourceName string
RepoPath string
SkipSSHHostKeyCheck bool
}
func (h *ActionHandler) CreateProject(ctx context.Context, req *CreateProjectRequest) (*csapitypes.Project, error) {
curUserID := h.CurrentUserID(ctx)
user, resp, err := h.configstoreClient.GetUser(ctx, curUserID)
if err != nil {
return nil, errors.Errorf("failed to get user %q: %w", curUserID, ErrFromRemote(resp, err))
}
parentRef := req.ParentRef
if parentRef == "" {
// create project in current user namespace
parentRef = path.Join("user", user.Name)
}
pg, resp, err := h.configstoreClient.GetProjectGroup(ctx, parentRef)
if err != nil {
return nil, errors.Errorf("failed to get project group %q: %w", parentRef, ErrFromRemote(resp, err))
}
isProjectOwner, err := h.IsProjectOwner(ctx, pg.OwnerType, pg.OwnerID)
if err != nil {
return nil, errors.Errorf("failed to determine ownership: %w", err)
}
if !isProjectOwner {
return nil, util.NewErrForbidden(errors.Errorf("user not authorized"))
}
if !util.ValidateName(req.Name) {
return nil, util.NewErrBadRequest(errors.Errorf("invalid project name %q", req.Name))
}
if req.RemoteSourceName == "" {
return nil, util.NewErrBadRequest(errors.Errorf("empty remote source name"))
}
if req.RepoPath == "" {
return nil, util.NewErrBadRequest(errors.Errorf("empty remote repo path"))
}
projectPath := path.Join(pg.Path, req.Name)
_, resp, err = h.configstoreClient.GetProject(ctx, projectPath)
if err != nil {
if resp != nil && resp.StatusCode != http.StatusNotFound {
return nil, errors.Errorf("failed to get project %q: %w", req.Name, ErrFromRemote(resp, err))
}
} else {
return nil, util.NewErrBadRequest(errors.Errorf("project %q already exists", projectPath))
}
rs, resp, err := h.configstoreClient.GetRemoteSource(ctx, req.RemoteSourceName)
if err != nil {
return nil, errors.Errorf("failed to get remote source %q: %w", req.RemoteSourceName, ErrFromRemote(resp, err))
}
var la *cstypes.LinkedAccount
for _, v := range user.LinkedAccounts {
if v.RemoteSourceID == rs.ID {
la = v
break
}
}
if la == nil {
return nil, errors.Errorf("user doesn't have a linked account for remote source %q", rs.Name)
}
gitSource, err := h.GetGitSource(ctx, rs, user.Name, la)
if err != nil {
return nil, errors.Errorf("failed to create gitsource client: %w", err)
}
repo, err := gitSource.GetRepoInfo(req.RepoPath)
if err != nil {
return nil, errors.Errorf("failed to get repository info from gitsource: %w", err)
}
h.log.Infof("generating ssh key pairs")
privateKey, _, err := util.GenSSHKeyPair(4096)
if err != nil {
return nil, errors.Errorf("failed to generate ssh key pair: %w", err)
}
p := &cstypes.Project{
Name: req.Name,
Parent: cstypes.Parent{
Type: cstypes.ConfigTypeProjectGroup,
ID: parentRef,
},
Visibility: req.Visibility,
RemoteRepositoryConfigType: cstypes.RemoteRepositoryConfigTypeRemoteSource,
RemoteSourceID: rs.ID,
LinkedAccountID: la.ID,
RepositoryID: repo.ID,
RepositoryPath: req.RepoPath,
SkipSSHHostKeyCheck: req.SkipSSHHostKeyCheck,
SSHPrivateKey: string(privateKey),
}
h.log.Infof("creating project")
rp, resp, err := h.configstoreClient.CreateProject(ctx, p)
if err != nil {
return nil, errors.Errorf("failed to create project: %w", ErrFromRemote(resp, err))
}
h.log.Infof("project %s created, ID: %s", rp.Name, rp.ID)
if serr := h.setupGitSourceRepo(ctx, rs, user, la, rp); serr != nil {
var err error
h.log.Errorf("failed to setup git source repo, trying to cleanup: %+v", ErrFromRemote(resp, err))
// try to cleanup gitsource configs and remove project
// we'll log but ignore errors
h.log.Infof("deleting project with ID: %q", rp.ID)
resp, err := h.configstoreClient.DeleteProject(ctx, rp.ID)
if err != nil {
h.log.Errorf("failed to delete project: %+v", ErrFromRemote(resp, err))
}
h.log.Infof("cleanup git source repo")
if err := h.cleanupGitSourceRepo(ctx, rs, user, la, rp); err != nil {
h.log.Errorf("failed to cleanup git source repo: %+v", ErrFromRemote(resp, err))
}
return nil, errors.Errorf("failed to setup git source repo: %w", serr)
}
return rp, nil
}
type UpdateProjectRequest struct {
Name string
Visibility cstypes.Visibility
}
func (h *ActionHandler) UpdateProject(ctx context.Context, projectRef string, req *UpdateProjectRequest) (*csapitypes.Project, error) {
p, resp, err := h.configstoreClient.GetProject(ctx, projectRef)
if err != nil {
return nil, errors.Errorf("failed to get project %q: %w", projectRef, ErrFromRemote(resp, err))
}
isProjectOwner, err := h.IsProjectOwner(ctx, p.OwnerType, p.OwnerID)
if err != nil {
return nil, errors.Errorf("failed to determine ownership: %w", err)
}
if !isProjectOwner {
return nil, util.NewErrForbidden(errors.Errorf("user not authorized"))
}
p.Name = req.Name
p.Visibility = req.Visibility
h.log.Infof("updating project")
rp, resp, err := h.configstoreClient.UpdateProject(ctx, p.ID, p.Project)
if err != nil {
return nil, errors.Errorf("failed to update project: %w", ErrFromRemote(resp, err))
}
h.log.Infof("project %s updated, ID: %s", p.Name, p.ID)
return rp, nil
}
func (h *ActionHandler) ProjectUpdateRepoLinkedAccount(ctx context.Context, projectRef string) (*csapitypes.Project, error) {
curUserID := h.CurrentUserID(ctx)
user, resp, err := h.configstoreClient.GetUser(ctx, curUserID)
if err != nil {
return nil, errors.Errorf("failed to get user %q: %w", curUserID, ErrFromRemote(resp, err))
}
p, resp, err := h.configstoreClient.GetProject(ctx, projectRef)
if err != nil {
return nil, errors.Errorf("failed to get project %q: %w", projectRef, ErrFromRemote(resp, err))
}
isProjectOwner, err := h.IsProjectOwner(ctx, p.OwnerType, p.OwnerID)
if err != nil {
return nil, errors.Errorf("failed to determine ownership: %w", err)
}
if !isProjectOwner {
return nil, util.NewErrForbidden(errors.Errorf("user not authorized"))
}
rs, resp, err := h.configstoreClient.GetRemoteSource(ctx, p.RemoteSourceID)
if err != nil {
return nil, errors.Errorf("failed to get remote source %q: %w", p.RemoteSourceID, ErrFromRemote(resp, err))
}
var la *cstypes.LinkedAccount
for _, v := range user.LinkedAccounts {
if v.RemoteSourceID == rs.ID {
la = v
break
}
}
if la == nil {
return nil, util.NewErrBadRequest(errors.Errorf("user doesn't have a linked account for remote source %q", rs.Name))
}
gitsource, err := h.GetGitSource(ctx, rs, user.Name, la)
if err != nil {
return nil, errors.Errorf("failed to create gitsource client: %w", err)
}
// check user has access to the repository
_, err = gitsource.GetRepoInfo(p.RepositoryPath)
if err != nil {
return nil, errors.Errorf("failed to get repository info from gitsource: %w", err)
}
p.LinkedAccountID = la.ID
h.log.Infof("updating project")
rp, resp, err := h.configstoreClient.UpdateProject(ctx, p.ID, p.Project)
if err != nil {
return nil, errors.Errorf("failed to update project: %w", ErrFromRemote(resp, err))
}
h.log.Infof("project %s updated, ID: %s", p.Name, p.ID)
return rp, nil
}
func (h *ActionHandler) setupGitSourceRepo(ctx context.Context, rs *cstypes.RemoteSource, user *cstypes.User, la *cstypes.LinkedAccount, project *csapitypes.Project) error {
gitsource, err := h.GetGitSource(ctx, rs, user.Name, la)
if err != nil {
return errors.Errorf("failed to create gitsource client: %w", err)
}
pubKey, err := util.ExtractPublicKey([]byte(project.SSHPrivateKey))
if err != nil {
return errors.Errorf("failed to extract public key: %w", err)
}
webhookURL, err := h.genWebhookURL(project)
if err != nil {
return errors.Errorf("failed to generate webhook url: %w", err)
}
// generate deploy keys and webhooks containing the agola project id so we
// can have multiple projects referencing the same remote repository and this
// will trigger multiple different runs
deployKeyName := fmt.Sprintf("agola deploy key - %s", project.ID)
h.log.Infof("creating/updating deploy key: %s", deployKeyName)
if err := gitsource.UpdateDeployKey(project.RepositoryPath, deployKeyName, string(pubKey), true); err != nil {
return errors.Errorf("failed to create deploy key: %w", err)
}
h.log.Infof("deleting existing webhooks")
if err := gitsource.DeleteRepoWebhook(project.RepositoryPath, webhookURL); err != nil {
return errors.Errorf("failed to delete repository webhook: %w", err)
}
h.log.Infof("creating webhook to url: %s", webhookURL)
if err := gitsource.CreateRepoWebhook(project.RepositoryPath, webhookURL, project.WebhookSecret); err != nil {
return errors.Errorf("failed to create repository webhook: %w", err)
}
return nil
}
func (h *ActionHandler) cleanupGitSourceRepo(ctx context.Context, rs *cstypes.RemoteSource, user *cstypes.User, la *cstypes.LinkedAccount, project *csapitypes.Project) error {
gitsource, err := h.GetGitSource(ctx, rs, user.Name, la)
if err != nil {
return errors.Errorf("failed to create gitsource client: %w", err)
}
webhookURL, err := h.genWebhookURL(project)
if err != nil {
return errors.Errorf("failed to generate webhook url: %w", err)
}
// generate deploy keys and webhooks containing the agola project id so we
// can have multiple projects referencing the same remote repository and this
// will trigger multiple different runs
deployKeyName := fmt.Sprintf("agola deploy key - %s", project.ID)
h.log.Infof("deleting deploy key: %s", deployKeyName)
if err := gitsource.DeleteDeployKey(project.RepositoryPath, deployKeyName); err != nil {
return errors.Errorf("failed to create deploy key: %w", err)
}
h.log.Infof("deleting existing webhooks")
if err := gitsource.DeleteRepoWebhook(project.RepositoryPath, webhookURL); err != nil {
return errors.Errorf("failed to delete repository webhook: %w", err)
}
return nil
}
func (h *ActionHandler) genWebhookURL(project *csapitypes.Project) (string, error) {
baseWebhookURL := fmt.Sprintf("%s/webhooks", h.apiExposedURL)
webhookURL, err := url.Parse(baseWebhookURL)
if err != nil {
return "", errors.Errorf("failed to parse base webhook url %q: %w", baseWebhookURL, err)
}
q := url.Values{}
q.Add("projectid", project.ID)
q.Add("agolaid", h.agolaID)
webhookURL.RawQuery = q.Encode()
return webhookURL.String(), nil
}
func (h *ActionHandler) ReconfigProject(ctx context.Context, projectRef string) error {
p, resp, err := h.configstoreClient.GetProject(ctx, projectRef)
if err != nil {
return errors.Errorf("failed to get project %q: %w", projectRef, ErrFromRemote(resp, err))
}
isProjectOwner, err := h.IsProjectOwner(ctx, p.OwnerType, p.OwnerID)
if err != nil {
return errors.Errorf("failed to determine ownership: %w", err)
}
if !isProjectOwner {
return util.NewErrForbidden(errors.Errorf("user not authorized"))
}
user, rs, la, err := h.getRemoteRepoAccessData(ctx, p.LinkedAccountID)
if err != nil {
return errors.Errorf("failed to get remote repo access data: %w", err)
}
// TODO(sgotti) update project repo path if the remote let us query by repository id
return h.setupGitSourceRepo(ctx, rs, user, la, p)
}
func (h *ActionHandler) DeleteProject(ctx context.Context, projectRef string) error {
p, resp, err := h.configstoreClient.GetProject(ctx, projectRef)
if err != nil {
return errors.Errorf("failed to get project %q: %w", projectRef, ErrFromRemote(resp, err))
}
isProjectOwner, err := h.IsProjectOwner(ctx, p.OwnerType, p.OwnerID)
if err != nil {
return errors.Errorf("failed to determine ownership: %w", err)
}
if !isProjectOwner {
return util.NewErrForbidden(errors.Errorf("user not authorized"))
}
// get data needed for repo cleanup
// we'll log but ignore errors
canDoRepCleanup := true
user, rs, la, err := h.getRemoteRepoAccessData(ctx, p.LinkedAccountID)
if err != nil {
canDoRepCleanup = false
h.log.Errorf("failed to get remote repo access data: %+v", err)
}
h.log.Infof("deleting project with ID: %q", p.ID)
resp, err = h.configstoreClient.DeleteProject(ctx, projectRef)
if err != nil {
return ErrFromRemote(resp, err)
}
// try to cleanup gitsource configs
// we'll log but ignore errors
if canDoRepCleanup {
h.log.Infof("cleanup git source repo")
if err := h.cleanupGitSourceRepo(ctx, rs, user, la, p); err != nil {
h.log.Errorf("failed to cleanup git source repo: %+v", ErrFromRemote(resp, err))
}
}
return nil
}
func (h *ActionHandler) ProjectCreateRun(ctx context.Context, projectRef, branch, tag, refName, commitSHA string) error {
curUserID := h.CurrentUserID(ctx)
user, resp, err := h.configstoreClient.GetUser(ctx, curUserID)
if err != nil {
return errors.Errorf("failed to get user %q: %w", curUserID, ErrFromRemote(resp, err))
}
p, resp, err := h.configstoreClient.GetProject(ctx, projectRef)
if err != nil {
return errors.Errorf("failed to get project %q: %w", projectRef, ErrFromRemote(resp, err))
}
isProjectOwner, err := h.IsProjectOwner(ctx, p.OwnerType, p.OwnerID)
if err != nil {
return errors.Errorf("failed to determine ownership: %w", err)
}
if !isProjectOwner {
return util.NewErrForbidden(errors.Errorf("user not authorized"))
}
rs, resp, err := h.configstoreClient.GetRemoteSource(ctx, p.RemoteSourceID)
if err != nil {
return errors.Errorf("failed to get remote source %q: %w", p.RemoteSourceID, ErrFromRemote(resp, err))
}
var la *cstypes.LinkedAccount
for _, v := range user.LinkedAccounts {
if v.RemoteSourceID == rs.ID {
la = v
break
}
}
if la == nil {
return util.NewErrBadRequest(errors.Errorf("user doesn't have a linked account for remote source %q", rs.Name))
}
gitSource, err := h.GetGitSource(ctx, rs, user.Name, la)
if err != nil {
return errors.Errorf("failed to create gitsource client: %w", err)
}
// check user has access to the repository
repoInfo, err := gitSource.GetRepoInfo(p.RepositoryPath)
if err != nil {
return errors.Errorf("failed to get repository info from gitsource: %w", err)
}
set := 0
if branch != "" {
set++
}
if tag != "" {
set++
}
if refName != "" {
set++
}
if set == 0 {
return util.NewErrBadRequest(errors.Errorf("one of branch, tag or ref is required"))
}
if set > 1 {
return util.NewErrBadRequest(errors.Errorf("only one of branch, tag or ref can be provided"))
}
var refType types.RunRefType
var message string
var branchLink, tagLink string
var refCommitSHA string
if refName == "" {
if branch != "" {
refName = gitSource.BranchRef(branch)
}
if tag != "" {
refName = gitSource.TagRef(tag)
}
}
gitRefType, name, err := gitSource.RefType(refName)
if err != nil {
return util.NewErrBadRequest(errors.Errorf("failed to get refType for ref %q: %w", refName, err))
}
ref, err := gitSource.GetRef(p.RepositoryPath, refName)
if err != nil {
return errors.Errorf("failed to get ref information from git source for ref %q: %w", refName, err)
}
refCommitSHA = ref.CommitSHA
switch gitRefType {
case gitsource.RefTypeBranch:
branch = name
case gitsource.RefTypeTag:
tag = name
// TODO(sgotti) implement manual run creation on a pull request if really needed
default:
return errors.Errorf("unsupported ref %q for manual run creation", refName)
}
// TODO(sgotti) check that the provided ref contains the provided commitSHA
// if no commitSHA has been provided use the ref commit sha
if commitSHA == "" && refCommitSHA != "" {
commitSHA = refCommitSHA
}
commit, err := gitSource.GetCommit(p.RepositoryPath, commitSHA)
if err != nil {
return errors.Errorf("failed to get commit information from git source for commit sha %q: %w", commitSHA, err)
}
// use the commit full sha since the user could have provided a short commit sha
commitSHA = commit.SHA
if branch != "" {
refType = types.RunRefTypeBranch
message = commit.Message
branchLink = gitSource.BranchLink(repoInfo, branch)
}
if tag != "" {
refType = types.RunRefTypeBranch
message = fmt.Sprintf("Tag %s", tag)
tagLink = gitSource.TagLink(repoInfo, tag)
}
// use remotesource skipSSHHostKeyCheck config and override with project config if set to true there
skipSSHHostKeyCheck := rs.SkipSSHHostKeyCheck
if p.SkipSSHHostKeyCheck {
skipSSHHostKeyCheck = p.SkipSSHHostKeyCheck
}
req := &CreateRunRequest{
RunType: types.RunTypeProject,
RefType: refType,
RunCreationTrigger: types.RunCreationTriggerTypeManual,
Project: p.Project,
RepoPath: p.RepositoryPath,
GitSource: gitSource,
CommitSHA: commitSHA,
Message: message,
Branch: branch,
Tag: tag,
PullRequestID: "",
Ref: refName,
SSHPrivKey: p.SSHPrivateKey,
SSHHostKey: rs.SSHHostKey,
SkipSSHHostKeyCheck: skipSSHHostKeyCheck,
CloneURL: repoInfo.SSHCloneURL,
CommitLink: gitSource.CommitLink(repoInfo, commitSHA),
BranchLink: branchLink,
TagLink: tagLink,
PullRequestLink: "",
}
return h.CreateRuns(ctx, req)
}
func (h *ActionHandler) getRemoteRepoAccessData(ctx context.Context, linkedAccountID string) (*cstypes.User, *cstypes.RemoteSource, *cstypes.LinkedAccount, error) {
user, resp, err := h.configstoreClient.GetUserByLinkedAccount(ctx, linkedAccountID)
if err != nil {
return nil, nil, nil, errors.Errorf("failed to get user with linked account id %q: %w", linkedAccountID, ErrFromRemote(resp, err))
}
la := user.LinkedAccounts[linkedAccountID]
if la == nil {
return nil, nil, nil, errors.Errorf("linked account %q in user %q doesn't exist", linkedAccountID, user.Name)
}
rs, resp, err := h.configstoreClient.GetRemoteSource(ctx, la.RemoteSourceID)
if err != nil {
return nil, nil, nil, errors.Errorf("failed to get remote source %q: %w", la.RemoteSourceID, ErrFromRemote(resp, err))
}
return user, rs, la, nil
}