Skip to content

Commit

Permalink
factor out gitlab entities
Browse files Browse the repository at this point in the history
  • Loading branch information
miku committed May 27, 2020
1 parent 04b8002 commit 3f683a2
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 173 deletions.
203 changes: 31 additions & 172 deletions cmd/span-webhookd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import (
"github.com/ilyakaznacheev/cleanenv"
"github.com/miku/span"
"github.com/miku/span/configutil"
"github.com/miku/span/gitlab"
"github.com/miku/span/reviewutil"
log "github.com/sirupsen/logrus"
)
Expand All @@ -65,6 +66,11 @@ var (

// Parsed configuration options.
config configutil.Config

// IndexReviewQueue takes requests for index reviews, add some buffering, so we
// can accept a few requests at a time, although this is improbable.
IndexReviewQueue = make(chan IndexReviewRequest, 8)
done = make(chan bool)
)

// IndexReviewRequest contains information for run an index review.
Expand All @@ -73,25 +79,22 @@ type IndexReviewRequest struct {
}

// PeekTicketNumber will fish out the ticket number from the YAML review
// configuration.
func (irr *IndexReviewRequest) PeekTicketNumber() (ticket string, err error) {
reviewConfig := &reviewutil.ReviewConfig{}
f, err := os.Open(irr.ReviewConfigFile)
// configuration. Returns the ticket number (e.g. 1234) and an error.
func (irr *IndexReviewRequest) PeekTicketNumber() (string, error) {
var (
reviewConfig = &reviewutil.ReviewConfig{}
f, err = os.Open(irr.ReviewConfigFile)
)
if err != nil {
return ticket, err
return "", err
}
defer f.Close()
if _, err := reviewConfig.ReadFrom(f); err != nil {
return ticket, err
return "", err
}
return reviewConfig.Ticket, nil
}

// IndexReviewQueue takes requests for index reviews, add some buffering, so we
// can accept a few requests at a time, although this is improbable.
var IndexReviewQueue = make(chan IndexReviewRequest, 8)
var done = make(chan bool)

// Worker hangs in there, checks for any new review requests on the index
// review queue and starts review, if requested.
func Worker(done chan bool) {
Expand All @@ -116,151 +119,6 @@ func Worker(done chan bool) {
done <- true
}

// Repo points to a local clone containing the review configuration we want. A
// personal access token is required to clone the repo from GitLab.
type Repo struct {
URL string
Dir string
Token string
}

// AuthURL returns an authenticated repository URL, if no token is supplied,
// just return the repo URL as is.
func (r Repo) AuthURL() string {
if r.Token == "" {
return r.URL
}
return strings.Replace(r.URL, "https://", fmt.Sprintf("https://oauth2:%s@", r.Token), 1)
}

// String representation.
func (r Repo) String() string {
return fmt.Sprintf("git repo from %s at %s", r.URL, r.Dir)
}

// Update runs a git pull (or clone), as per strong convention, this will
// always be a fast forward. If repo does not exist yet, clone.
// gitlab/profile/personal_access_tokens: You can also use personal access
// tokens to authenticate against Git over HTTP. They are the only accepted
// password when you have Two-Factor Authentication (2FA) enabled.
func (r Repo) Update() error {
log.Printf("updating %s", r)
if r.Token == "" {
log.Printf("warning: no gitlab.token found, checkout might fail (%s)", *spanConfigFile)
}
if _, err := os.Stat(path.Dir(r.Dir)); os.IsNotExist(err) {
if err := os.MkdirAll(path.Dir(r.Dir), 0755); err != nil {
return err
}
}
var (
cmd string
args []string
)
if _, err := os.Stat(r.Dir); os.IsNotExist(err) {
cmd, args = "git", []string{"clone", r.AuthURL(), r.Dir}
} else {
cmd, args = "git", []string{"-C", r.Dir, "pull", "origin", "master"}
}
// XXX: black out token for logs.
log.Printf("[cmd] %s %s", cmd, strings.Join(args, " "))
// XXX: exit code handling, https://stackoverflow.com/a/10385867.
return exec.Command(cmd, args...).Run()
}

// PushPayload delivered on push and web edits. This is the whole response, we
// are mainly interested in the modified files in a commit.
type PushPayload struct {
After string `json:"after"`
Before string `json:"before"`
CheckoutSha string `json:"checkout_sha"`
Commits []struct {
Added []interface{} `json:"added"`
Author struct {
Email string `json:"email"`
Name string `json:"name"`
} `json:"author"`
Id string `json:"id"`
Message string `json:"message"`
Modified []string `json:"modified"`
Removed []interface{} `json:"removed"`
Timestamp string `json:"timestamp"`
Url string `json:"url"`
} `json:"commits"`
EventName string `json:"event_name"`
Message interface{} `json:"message"`
ObjectKind string `json:"object_kind"`
Project struct {
AvatarUrl interface{} `json:"avatar_url"`
CiConfigPath interface{} `json:"ci_config_path"`
DefaultBranch string `json:"default_branch"`
Description string `json:"description"`
GitHttpUrl string `json:"git_http_url"`
GitSshUrl string `json:"git_ssh_url"`
Homepage string `json:"homepage"`
HttpUrl string `json:"http_url"`
Id int64 `json:"id"`
Name string `json:"name"`
Namespace string `json:"namespace"`
PathWithNamespace string `json:"path_with_namespace"`
SshUrl string `json:"ssh_url"`
Url string `json:"url"`
VisibilityLevel int64 `json:"visibility_level"`
WebUrl string `json:"web_url"`
} `json:"project"`
ProjectId int64 `json:"project_id"`
Ref string `json:"ref"`
Repository struct {
Description string `json:"description"`
GitHttpUrl string `json:"git_http_url"`
GitSshUrl string `json:"git_ssh_url"`
Homepage string `json:"homepage"`
Name string `json:"name"`
Url string `json:"url"`
VisibilityLevel int64 `json:"visibility_level"`
} `json:"repository"`
TotalCommitsCount int64 `json:"total_commits_count"`
UserAvatar string `json:"user_avatar"`
UserEmail string `json:"user_email"`
UserId int64 `json:"user_id"`
UserName string `json:"user_name"`
UserUsername string `json:"user_username"`
}

// ModifiedFiles returns all modified files across all commits in this payload.
func (p PushPayload) ModifiedFiles() (filenames []string) {
for _, commit := range p.Commits {
for _, modified := range commit.Modified {
filenames = append(filenames, modified)
}
}
return
}

// IsFileModified returns true, if given file has been modified.
func (p PushPayload) IsFileModified(filename string) bool {
for _, modified := range p.ModifiedFiles() {
if modified == filename {
return true
}
}
return false
}

// MatchModified returns a list of paths matching a pattern (match against the
// full path in repo, e.g. docs/review.*).
func (p PushPayload) MatchModified(re *regexp.Regexp) (filenames []string) {
for _, modified := range p.ModifiedFiles() {
if re.MatchString(modified) {
filenames = append(filenames, modified)
log.Printf("%s matches %s", modified, re)
} else {
log.Printf("%s ignored", modified)
}
}
return
}

// HookHandler can act as webhook receiver. The hook we use at the moment is
// the Push Hook. Other types are Issue, Note or Tag Push Hook.
func HookHandler(w http.ResponseWriter, r *http.Request) {
Expand All @@ -269,45 +127,41 @@ func HookHandler(w http.ResponseWriter, r *http.Request) {
// We care a bit, because gitlab wants us to return ASAP.
log.Printf("request completed after %s", time.Since(started))
}()

log.Printf("request from %s", r.RemoteAddr)
if r.Header.Get("X-FORWARDED-FOR") != "" {
log.Printf("X-FORWARDED-FOR: %s", r.Header.Get("X-FORWARDED-FOR"))
}

gitlabEvent := strings.TrimSpace(r.Header.Get("X-Gitlab-Event"))

switch gitlabEvent {
case "Push Hook":
var payload PushPayload
var payload gitlab.PushPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
log.Printf("gitlab payload: %v", payload)

pattern := "^docs/review.*yaml"
reviewFiles := payload.MatchModified(regexp.MustCompile(pattern))
var (
pattern = "^docs/review.*yaml"
reviewFiles = payload.MatchModified(regexp.MustCompile(pattern))
repo = gitlab.Repo{
URL: payload.Project.GitHttpUrl,
Dir: config.GitLabCloneDir,
Token: config.GitLabToken,
}
)
if len(reviewFiles) == 0 {
log.Printf("%s matched nothing, hook done", pattern)
return
}
repo := Repo{
URL: payload.Project.GitHttpUrl,
Dir: *repoDir,
Token: config.GitLabToken,
}
if err := repo.Update(); err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}

// XXX: exit code handling, non-portable, https://stackoverflow.com/a/10385867.
// TODO: exit code handling, non-portable, https://stackoverflow.com/a/10385867.
log.Printf("successfully updated repo at %s", repo.Dir)

// We can have multiple review files, issue a request for each of them.
// XXX: the same file might appear multiple times.
// TODO: the same file might appear multiple times.
for _, reviewFile := range reviewFiles {
rr := IndexReviewRequest{
ReviewConfigFile: path.Join(repo.Dir, reviewFile),
Expand Down Expand Up @@ -372,6 +226,11 @@ func main() {
if *addr != "" {
config.WebhookdHostPort = *addr
}
// Keep flag compatibility.
if *repoDir != "" {
config.GitLabCloneDir = *repoDir
}

// Dump config.
b, err := json.Marshal(config)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion configutil/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ package configutil

// Config is application configuration of span and its subcommands.
type Config struct {
GitLabCloneDir string `yaml:"gitlab.clonedir" env:"SPAN_GITLAB_CLONEDIR" env-default:"/tmp/span-webhookd-clone"`
GitLabToken string `yaml:"gitlab.token" env:"SPAN_GITLAB_TOKEN"`
RedmineToken string `yaml:"redmine.token" env:"SPAN_REDMINE_TOKEN"`
RedmineURL string `yaml:"redmine.url" env:"SPAN_REDMINE_URL"`
WebhookdHostPort string `yaml:"webhookd.listen" env:"SPAN_WEBHOOKD_LISTEN" env-default:"0.0.0.0:8080"`
WebhookdPath string `yaml:"webhookd.path" env:"SPAN_WEBHOOKD_PATH" env-default:"trigger"`
WebhookdLogfile string `yaml:"webhookd.logfile" env:"SPAN_WEBHOOKD_LOGFILE"`
WebhookdPath string `yaml:"webhookd.path" env:"SPAN_WEBHOOKD_PATH" env-default:"trigger"`
WhatIsLiveURL string `yaml:"whatislive.url" env:"SPAN_WHATISLIVE_URL"`
}
96 changes: 96 additions & 0 deletions gitlab/gitlab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Package gitlab contains support types for gitlab interaction.
package gitlab

import (
"regexp"
)

// PushPayload delivered on push and web edits. This is the whole response, we
// are mainly interested in the modified files in a commit.
type PushPayload struct {
After string `json:"after"`
Before string `json:"before"`
CheckoutSha string `json:"checkout_sha"`
Commits []struct {
Added []interface{} `json:"added"`
Author struct {
Email string `json:"email"`
Name string `json:"name"`
} `json:"author"`
Id string `json:"id"`
Message string `json:"message"`
Modified []string `json:"modified"`
Removed []interface{} `json:"removed"`
Timestamp string `json:"timestamp"`
Url string `json:"url"`
} `json:"commits"`
EventName string `json:"event_name"`
Message interface{} `json:"message"`
ObjectKind string `json:"object_kind"`
Project struct {
AvatarUrl interface{} `json:"avatar_url"`
CiConfigPath interface{} `json:"ci_config_path"`
DefaultBranch string `json:"default_branch"`
Description string `json:"description"`
GitHttpUrl string `json:"git_http_url"`
GitSshUrl string `json:"git_ssh_url"`
Homepage string `json:"homepage"`
HttpUrl string `json:"http_url"`
Id int64 `json:"id"`
Name string `json:"name"`
Namespace string `json:"namespace"`
PathWithNamespace string `json:"path_with_namespace"`
SshUrl string `json:"ssh_url"`
Url string `json:"url"`
VisibilityLevel int64 `json:"visibility_level"`
WebUrl string `json:"web_url"`
} `json:"project"`
ProjectId int64 `json:"project_id"`
Ref string `json:"ref"`
Repository struct {
Description string `json:"description"`
GitHttpUrl string `json:"git_http_url"`
GitSshUrl string `json:"git_ssh_url"`
Homepage string `json:"homepage"`
Name string `json:"name"`
Url string `json:"url"`
VisibilityLevel int64 `json:"visibility_level"`
} `json:"repository"`
TotalCommitsCount int64 `json:"total_commits_count"`
UserAvatar string `json:"user_avatar"`
UserEmail string `json:"user_email"`
UserId int64 `json:"user_id"`
UserName string `json:"user_name"`
UserUsername string `json:"user_username"`
}

// ModifiedFiles returns all modified files across all commits in this payload.
func (p PushPayload) ModifiedFiles() (filenames []string) {
for _, commit := range p.Commits {
for _, modified := range commit.Modified {
filenames = append(filenames, modified)
}
}
return
}

// IsFileModified returns true, if given file has been modified.
func (p PushPayload) IsFileModified(filename string) bool {
for _, modified := range p.ModifiedFiles() {
if modified == filename {
return true
}
}
return false
}

// MatchModified returns a list of paths matching a pattern (match against the
// full path in repo, e.g. docs/review.*).
func (p PushPayload) MatchModified(re *regexp.Regexp) (filenames []string) {
for _, modified := range p.ModifiedFiles() {
if re.MatchString(modified) {
filenames = append(filenames, modified)
}
}
return
}
Loading

0 comments on commit 3f683a2

Please sign in to comment.