diff --git a/internal/services/gateway/action/project.go b/internal/services/gateway/action/project.go index 5143e83..d8e061a 100644 --- a/internal/services/gateway/action/project.go +++ b/internal/services/gateway/action/project.go @@ -21,6 +21,7 @@ import ( "net/url" "path" + gitsource "github.com/sorintlab/agola/internal/gitsources" csapi "github.com/sorintlab/agola/internal/services/configstore/api" "github.com/sorintlab/agola/internal/services/types" "github.com/sorintlab/agola/internal/util" @@ -120,12 +121,12 @@ func (h *ActionHandler) CreateProject(ctx context.Context, req *CreateProjectReq 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) + 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) + repo, err := gitSource.GetRepoInfo(req.RepoPath) if err != nil { return nil, errors.Errorf("failed to get repository info from gitsource: %w", err) } @@ -350,3 +351,165 @@ func (h *ActionHandler) DeleteProject(ctx context.Context, projectRef string) er } 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)) + } + h.log.Infof("rs: %s", util.Dump(rs)) + var la *types.LinkedAccount + for _, v := range user.LinkedAccounts { + if v.RemoteSourceID == rs.ID { + la = v + break + } + } + h.log.Infof("la: %s", util.Dump(la)) + 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) +} diff --git a/internal/services/gateway/action/run.go b/internal/services/gateway/action/run.go index 328b73d..02a00b7 100644 --- a/internal/services/gateway/action/run.go +++ b/internal/services/gateway/action/run.go @@ -430,7 +430,7 @@ func (h *ActionHandler) CreateRuns(ctx context.Context, req *CreateRunRequest) e data, filename, err := h.fetchConfigFiles(req.GitSource, req.RepoPath, req.CommitSHA) if err != nil { - return errors.Errorf("failed to fetch config file: %w", err) + return util.NewErrInternal(errors.Errorf("failed to fetch config file: %w", err)) } h.log.Debug("data: %s", data) diff --git a/internal/services/gateway/api/project.go b/internal/services/gateway/api/project.go index 215ee9a..35c9c05 100644 --- a/internal/services/gateway/api/project.go +++ b/internal/services/gateway/api/project.go @@ -260,3 +260,46 @@ func createProjectResponse(r *csapi.Project) *ProjectResponse { return res } + +type ProjectCreateRunRequest struct { + Branch string `json:"branch,omitempty"` + Tag string `json:"tag,omitempty"` + Ref string `json:"ref,omitempty"` + CommitSHA string `json:"commit_sha,omitempty"` +} + +type ProjectCreateRunHandler struct { + log *zap.SugaredLogger + ah *action.ActionHandler +} + +func NewProjectCreateRunHandler(logger *zap.Logger, ah *action.ActionHandler) *ProjectCreateRunHandler { + return &ProjectCreateRunHandler{log: logger.Sugar(), ah: ah} +} + +func (h *ProjectCreateRunHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + projectRef, err := url.PathUnescape(vars["projectref"]) + if err != nil { + httpError(w, util.NewErrBadRequest(err)) + return + } + + var req ProjectCreateRunRequest + d := json.NewDecoder(r.Body) + if err := d.Decode(&req); err != nil { + httpError(w, util.NewErrBadRequest(err)) + return + } + + err = h.ah.ProjectCreateRun(ctx, projectRef, req.Branch, req.Tag, req.Ref, req.CommitSHA) + if httpError(w, err) { + h.log.Errorf("err: %+v", err) + return + } + + if err := httpResponse(w, http.StatusCreated, nil); err != nil { + h.log.Errorf("err: %+v", err) + } +} diff --git a/internal/services/gateway/gateway.go b/internal/services/gateway/gateway.go index b21416a..f559cc9 100644 --- a/internal/services/gateway/gateway.go +++ b/internal/services/gateway/gateway.go @@ -162,6 +162,7 @@ func (g *Gateway) Run(ctx context.Context) error { deleteProjectHandler := api.NewDeleteProjectHandler(logger, g.ah) projectReconfigHandler := api.NewProjectReconfigHandler(logger, g.ah) projectUpdateRepoLinkedAccountHandler := api.NewProjectUpdateRepoLinkedAccountHandler(logger, g.ah) + projectCreateRunHandler := api.NewProjectCreateRunHandler(logger, g.ah) secretHandler := api.NewSecretHandler(logger, g.ah) createSecretHandler := api.NewCreateSecretHandler(logger, g.ah) @@ -242,6 +243,7 @@ func (g *Gateway) Run(ctx context.Context) error { apirouter.Handle("/projects/{projectref}", authForcedHandler(deleteProjectHandler)).Methods("DELETE") apirouter.Handle("/projects/{projectref}/reconfig", authForcedHandler(projectReconfigHandler)).Methods("PUT") apirouter.Handle("/projects/{projectref}/updaterepolinkedaccount", authForcedHandler(projectUpdateRepoLinkedAccountHandler)).Methods("PUT") + apirouter.Handle("/projects/{projectref}/createrun", authForcedHandler(projectCreateRunHandler)).Methods("PUT") apirouter.Handle("/projectgroups/{projectgroupref}/secrets", authForcedHandler(secretHandler)).Methods("GET") apirouter.Handle("/projects/{projectref}/secrets", authForcedHandler(secretHandler)).Methods("GET") diff --git a/internal/services/types/run.go b/internal/services/types/run.go index 37a185c..67f841e 100644 --- a/internal/services/types/run.go +++ b/internal/services/types/run.go @@ -33,4 +33,5 @@ type RunCreationTriggerType string const ( RunCreationTriggerTypeWebhook RunCreationTriggerType = "webhook" + RunCreationTriggerTypeManual RunCreationTriggerType = "manual" )