package handler import ( "database/sql" "fmt" "io/ioutil" "net/http" "strconv" "strings" "time" "github.com/drone/drone/pkg/build/script" "github.com/drone/drone/pkg/database" . "github.com/drone/drone/pkg/model" "github.com/drone/drone/pkg/queue" "github.com/plouc/go-gitlab-client" ) type GitlabHandler struct { queue *queue.Queue apiPath string } func NewGitlabHandler(queue *queue.Queue) *GitlabHandler { return &GitlabHandler{ queue: queue, apiPath: "/api/v3", } } func (g *GitlabHandler) Add(w http.ResponseWriter, r *http.Request, u *User) error { settings := database.SettingsMust() teams, err := database.ListTeams(u.ID) if err != nil { return err } data := struct { User *User Teams []*Team Settings *Settings }{u, teams, settings} // if the user hasn't linked their GitLab account // render a different template if len(u.GitlabToken) == 0 { return RenderTemplate(w, "gitlab_link.html", &data) } // otherwise display the template for adding // a new GitLab repository. return RenderTemplate(w, "gitlab_add.html", &data) } func (g *GitlabHandler) Link(w http.ResponseWriter, r *http.Request, u *User) error { token := strings.TrimSpace(r.FormValue("token")) if len(u.GitlabToken) == 0 || token != u.GitlabToken && len(token) > 0 { u.GitlabToken = token settings := database.SettingsMust() gl := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, u.GitlabToken) _, err := gl.CurrentUser() if err != nil { return fmt.Errorf("Private Token is not valid: %q", err) } if err := database.SaveUser(u); err != nil { return RenderError(w, err, http.StatusBadRequest) } } http.Redirect(w, r, "/new/gitlab", http.StatusSeeOther) return nil } func (g *GitlabHandler) ReLink(w http.ResponseWriter, r *http.Request, u *User) error { data := struct { User *User }{u} return RenderTemplate(w, "gitlab_link.html", &data) } func (g *GitlabHandler) Create(w http.ResponseWriter, r *http.Request, u *User) error { teamName := r.FormValue("team") owner := r.FormValue("owner") name := r.FormValue("name") repo, err := g.newGitlabRepo(u, owner, name) if err != nil { return err } if len(teamName) > 0 { team, err := database.GetTeamSlug(teamName) if err != nil { return fmt.Errorf("Unable to find Team %s.", teamName) } // user must be an admin member of the team if ok, _ := database.IsMemberAdmin(u.ID, team.ID); !ok { return fmt.Errorf("Invalid permission to access Team %s.", teamName) } repo.TeamID = team.ID } // Save to the database if err := database.SaveRepo(repo); err != nil { return fmt.Errorf("Error saving repository to the database. %s", err) } return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) } func (g *GitlabHandler) newGitlabRepo(u *User, owner, name string) (*Repo, error) { settings := database.SettingsMust() gl := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, u.GitlabToken) project, err := gl.Project(ns(owner, name)) if err != nil { return nil, err } var cloneUrl string if project.Public { cloneUrl = project.HttpRepoUrl } else { cloneUrl = project.SshRepoUrl } repo, err := NewRepo(settings.GitlabDomain, owner, name, ScmGit, cloneUrl) if err != nil { return nil, err } repo.UserID = u.ID repo.Private = !project.Public if repo.Private { // name the key keyName := fmt.Sprintf("%s@%s", repo.Owner, settings.Domain) // TODO: (fudanchii) check if we already opted to use UserKey // create the github key, or update if one already exists if err := gl.AddProjectDeployKey(ns(owner, name), keyName, repo.PublicKey); err != nil { return nil, fmt.Errorf("Unable to add Public Key to your GitLab repository.") } } link := fmt.Sprintf("%s://%s/hook/gitlab?id=%s", settings.Scheme, settings.Domain, repo.Slug) if err := gl.AddProjectHook(ns(owner, name), link, true, false, true); err != nil { return nil, fmt.Errorf("Unable to add Hook to your GitLab repository.") } return repo, err } func (g *GitlabHandler) Hook(w http.ResponseWriter, r *http.Request) error { rID := r.FormValue("id") repo, err := database.GetRepoSlug(rID) if err != nil { return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) } user, err := database.GetUser(repo.UserID) if err != nil { return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) } payload, _ := ioutil.ReadAll(r.Body) parsed, err := gogitlab.ParseHook(payload) if err != nil { return err } if parsed.ObjectKind == "merge_request" { fmt.Println(string(payload)) if err := g.PullRequestHook(parsed, repo, user); err != nil { return err } return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) } if len(parsed.After) == 0 { return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) } _, err = database.GetCommitHash(parsed.After, repo.ID) if err != nil && err != sql.ErrNoRows { fmt.Println("commit already exists") return RenderText(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway) } commit := &Commit{} commit.RepoID = repo.ID commit.Branch = parsed.Branch() commit.Hash = parsed.After commit.Status = "Pending" commit.Created = time.Now().UTC() head := parsed.Head() commit.Message = head.Message commit.Timestamp = head.Timestamp if head.Author != nil { commit.SetAuthor(head.Author.Email) } else { commit.SetAuthor(parsed.UserName) } // get the github settings from the database settings := database.SettingsMust() // get the drone.yml file from GitHub client := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, user.GitlabToken) content, err := client.RepoRawFile(ns(repo.Owner, repo.Name), commit.Hash, ".drone.yml") if err != nil { msg := "No .drone.yml was found in this repository. You need to add one.\n" if err := saveFailedBuild(commit, msg); err != nil { return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } // parse the build script buildscript, err := script.ParseBuild(content, repo.Params) if err != nil { msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n" if err := saveFailedBuild(commit, msg); err != nil { return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } // save the commit to the database if err := database.SaveCommit(commit); err != nil { return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } // save the build to the database build := &Build{} build.Slug = "1" // TODO build.CommitID = commit.ID build.Created = time.Now().UTC() build.Status = "Pending" if err := database.SaveBuild(build); err != nil { return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } g.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript}) // OK! return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) } func (g *GitlabHandler) PullRequestHook(p *gogitlab.HookPayload, repo *Repo, user *User) error { obj := p.ObjectAttributes // Gitlab may trigger multiple hooks upon updating merge requests status // only build when it was just opened and the merge hasn't been checked yet. if !(obj.State == "opened" && obj.MergeStatus == "unchecked") { fmt.Println("Ignore GitLab Merge Requests") return nil } settings := database.SettingsMust() client := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, user.GitlabToken) // GitLab merge-requests hook doesn't include repository data. // Have to fetch it manually src, err := client.RepoBranch(strconv.Itoa(obj.SourceProjectId), obj.SourceBranch) if err != nil { return err } _, err = database.GetCommitHash(src.Commit.Id, repo.ID) if err != nil && err != sql.ErrNoRows { fmt.Println("commit already exists") return err } commit := &Commit{} commit.RepoID = repo.ID commit.Branch = src.Name commit.Hash = src.Commit.Id commit.Status = "Pending" commit.Created = time.Now().UTC() commit.PullRequest = strconv.Itoa(obj.IId) commit.Message = src.Commit.Message commit.Timestamp = src.Commit.AuthoredDateRaw commit.SetAuthor(src.Commit.Author.Email) content, err := client.RepoRawFile(strconv.Itoa(obj.SourceProjectId), commit.Hash, ".drone.yml") if err != nil { msg := "No .drone.yml was found in this repository. You need to add one.\n" if err := saveFailedBuild(commit, msg); err != nil { return fmt.Errorf("Failed to save build: %q", err) } return fmt.Errorf("Error to fetch build script: %q", err) } // parse the build script buildscript, err := script.ParseBuild(content, repo.Params) if err != nil { msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n" if err := saveFailedBuild(commit, msg); err != nil { return fmt.Errorf("Failed to save build: %q", err) } return fmt.Errorf("Failed to parse build script: %q", err) } // save the commit to the database if err := database.SaveCommit(commit); err != nil { return fmt.Errorf("Failed to save commit: %q", err) } // save the build to the database build := &Build{} build.Slug = "1" // TODO build.CommitID = commit.ID build.Created = time.Now().UTC() build.Status = "Pending" if err := database.SaveBuild(build); err != nil { return fmt.Errorf("Failed to save build: %q", err) } g.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript}) return nil } // ns namespaces user and repo. // Returns user%2Frepo func ns(user, repo string) string { return fmt.Sprintf("%s%%2F%s", user, repo) }