harness-drone/service/hook/parser/parse.go

406 lines
12 KiB
Go

// Copyright 2019 Drone IO, Inc.
//
// 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 parser
import (
"errors"
"fmt"
"net/http"
"net/http/httputil"
"os"
"strconv"
"strings"
"time"
"github.com/drone/drone/core"
"github.com/drone/go-scm/scm"
)
// TODO(bradrydzewski): stash, push hook missing link
// TODO(bradrydzewski): stash, tag hook missing timestamp
// TODO(bradrydzewski): stash, tag hook missing commit message
// TODO(bradrydzewski): stash, tag hook missing link
// TODO(bradrydzewski): stash, pull request hook missing link
// TODO(bradrydzewski): stash, hooks missing repository clone http url
// TODO(bradrydzewski): stash, hooks missing repository clone ssh url
// TODO(bradrydzewski): stash, hooks missing repository html link
// TODO(bradrydzewski): gogs, push hook missing author avatar, using sender instead.
// TODO(bradrydzewski): gogs, pull request hook missing commit sha.
// TODO(bradrydzewski): gogs, tag hook missing commit sha.
// TODO(bradrydzewski): gogs, sender missing Name field.
// TODO(bradrydzewski): gogs, push hook missing repository html url
// TODO(bradrydzewski): gitea, push hook missing author avatar, using sender instead.
// TODO(bradrydzewski): gitea, tag hook missing commit sha.
// TODO(bradrydzewski): gitea, sender missing Name field.
// TODO(bradrydzewski): gitea, push hook missing repository html url
// TODO(bradrydzewski): bitbucket, pull request hook missing author email.
// TODO(bradrydzewski): bitbucket, hooks missing default repository branch.
// TODO(bradrydzewski): github, push hook timestamp is negative value.
// TODO(bradrydzewski): github, pull request message is empty
// represents a deleted ref in the github webhook.
const emptyCommit = "0000000000000000000000000000000000000000"
// this is intended for local testing and instructs the handler
// to print the contents of the hook to stdout.
var debugPrintHook = false
func init() {
debugPrintHook, _ = strconv.ParseBool(
os.Getenv("DRONE_DEBUG_DUMP_HOOK"),
)
}
// New returns a new HookParser.
func New(client *scm.Client) core.HookParser {
return &parser{client}
}
type parser struct {
client *scm.Client
}
func (p *parser) Parse(req *http.Request, secretFunc func(string) string) (*core.Hook, *core.Repository, error) {
if debugPrintHook {
// if DRONE_DEBUG_DUMP_HOOK=true print the http.Request
// headers and body to stdout.
out, _ := httputil.DumpRequest(req, true)
os.Stderr.Write(out)
}
// callback function provides the webhook parser with
// a per-repository secret key used to verify the webhook
// payload signature for authenticity.
fn := func(webhook scm.Webhook) (string, error) {
if webhook == nil {
// HACK(bradrydzewski) if the incoming webhook is nil
// we assume it is an unknown event or action. A more
// permanent fix is to update go-scm to return an
// scm.ErrUnknownAction error.
return "", scm.ErrUnknownEvent
}
repo := webhook.Repository()
slug := scm.Join(repo.Namespace, repo.Name)
secret := secretFunc(slug)
if secret == "" {
return secret, errors.New("Cannot find repository")
}
return secret, nil
}
payload, err := p.client.Webhooks.Parse(req, fn)
if err == scm.ErrUnknownEvent {
return nil, nil, nil
}
if err != nil {
return nil, nil, err
}
var repo *core.Repository
var hook *core.Hook
switch v := payload.(type) {
case *scm.PushHook:
// github sends push hooks when tags and branches are
// deleted. These hooks should be ignored.
if v.Commit.Sha == emptyCommit {
return nil, nil, nil
}
// github sends push hooks when tags are created. The
// push hook contains more information than the tag hook,
// so we choose to use the push hook for tags.
if strings.HasPrefix(v.Ref, "refs/tags/") {
hook = &core.Hook{
Trigger: core.TriggerHook, // core.TriggerHook
Event: core.EventTag,
Action: core.ActionCreate,
Link: v.Commit.Link,
Timestamp: v.Commit.Author.Date.Unix(),
Message: v.Commit.Message,
Before: v.Before,
After: v.Commit.Sha,
Source: scm.TrimRef(v.BaseRef),
Target: scm.TrimRef(v.BaseRef),
Ref: v.Ref,
Author: v.Commit.Author.Login,
AuthorName: v.Commit.Author.Name,
AuthorEmail: v.Commit.Author.Email,
AuthorAvatar: v.Commit.Author.Avatar,
Sender: v.Sender.Login,
}
} else {
hook = &core.Hook{
Trigger: core.TriggerHook, //core.TriggerHook,
Event: core.EventPush,
Link: v.Commit.Link,
Timestamp: v.Commit.Author.Date.Unix(),
Message: v.Commit.Message,
Before: v.Before,
After: v.Commit.Sha,
Ref: v.Ref,
Source: strings.TrimPrefix(v.Ref, "refs/heads/"),
Target: strings.TrimPrefix(v.Ref, "refs/heads/"),
Author: v.Commit.Author.Login,
AuthorName: v.Commit.Author.Name,
AuthorEmail: v.Commit.Author.Email,
AuthorAvatar: v.Commit.Author.Avatar,
Sender: v.Sender.Login,
}
}
repo = &core.Repository{
UID: v.Repo.ID,
Namespace: v.Repo.Namespace,
Name: v.Repo.Name,
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
Link: v.Repo.Link,
Branch: v.Repo.Branch,
Private: v.Repo.Private,
HTTPURL: v.Repo.Clone,
SSHURL: v.Repo.CloneSSH,
}
// gogs and gitea do not include the author avatar in
// the webhook, but they do include the sender avatar.
// use the sender avatar when necessary.
if hook.AuthorAvatar == "" {
hook.AuthorAvatar = v.Sender.Avatar
}
return hook, repo, nil
case *scm.TagHook:
if v.Action != scm.ActionCreate {
return nil, nil, nil
}
// when a tag is created github sends both a push hook
// and a tag create hook. The push hook contains more
// information, so we choose to use the push hook and
// ignore the native tag hook.
if p.client.Driver == scm.DriverGithub ||
p.client.Driver == scm.DriverGitea ||
p.client.Driver == scm.DriverGitlab {
return nil, nil, nil
}
// the tag hook does not include the commit link, message
// or timestamp. In some cases it does not event include
// the sha (gogs). Note that we may need to fetch additional
// details to augment the webhook.
hook = &core.Hook{
Trigger: core.TriggerHook, // core.TriggerHook,
Event: core.EventTag,
Action: core.ActionCreate,
Link: "",
Timestamp: 0,
Message: "",
After: v.Ref.Sha,
Ref: v.Ref.Name,
Source: v.Ref.Name,
Target: v.Ref.Name,
Author: v.Sender.Login,
AuthorName: v.Sender.Name,
AuthorEmail: v.Sender.Email,
AuthorAvatar: v.Sender.Avatar,
Sender: v.Sender.Login,
}
repo = &core.Repository{
UID: v.Repo.ID,
Namespace: v.Repo.Namespace,
Name: v.Repo.Name,
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
Link: v.Repo.Link,
Branch: v.Repo.Branch,
Private: v.Repo.Private,
HTTPURL: v.Repo.Clone,
SSHURL: v.Repo.CloneSSH,
}
// TODO(bradrydzewski) can we use scm.ExpandRef here?
if !strings.HasPrefix(hook.Ref, "refs/tags/") {
hook.Ref = fmt.Sprintf("refs/tags/%s", hook.Ref)
}
if hook.AuthorAvatar == "" {
hook.AuthorAvatar = v.Sender.Avatar
}
return hook, repo, nil
case *scm.PullRequestHook:
// TODO(bradrydzewski) cleanup the pr close hook code.
if v.Action == scm.ActionClose {
return &core.Hook{
Trigger: core.TriggerHook,
Event: core.EventPullRequest,
Action: core.ActionClose,
After: v.PullRequest.Sha,
Ref: v.PullRequest.Ref,
}, &core.Repository{
UID: v.Repo.ID,
Namespace: v.Repo.Namespace,
Name: v.Repo.Name,
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
}, nil
}
if v.Action != scm.ActionOpen && v.Action != scm.ActionSync {
return nil, nil, nil
}
// Pull Requests are not supported for Bitbucket due
// to lack of refs (e.g. refs/pull-requests/42/from).
// Please contact Bitbucket Support if you would like to
// see this feature enabled:
// https://bitbucket.org/site/master/issues/5814/repository-refs-for-pull-requests
if p.client.Driver == scm.DriverBitbucket {
return nil, nil, nil
}
hook = &core.Hook{
Trigger: core.TriggerHook, // core.TriggerHook,
Event: core.EventPullRequest,
Action: v.Action.String(),
Link: v.PullRequest.Link,
Timestamp: v.PullRequest.Created.Unix(),
Title: v.PullRequest.Title,
Message: v.PullRequest.Body,
Before: v.PullRequest.Base.Sha,
After: v.PullRequest.Sha,
Ref: v.PullRequest.Ref,
Fork: v.PullRequest.Fork,
Source: v.PullRequest.Source,
Target: v.PullRequest.Target,
Author: v.PullRequest.Author.Login,
AuthorName: v.PullRequest.Author.Name,
AuthorEmail: v.PullRequest.Author.Email,
AuthorAvatar: v.PullRequest.Author.Avatar,
Sender: v.Sender.Login,
}
// HACK this is a workaround for github. The pull
// request title is populated, but not the message.
if hook.Message == "" {
hook.Message = hook.Title
}
repo = &core.Repository{
UID: v.Repo.ID,
Namespace: v.Repo.Namespace,
Name: v.Repo.Name,
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
Link: v.Repo.Link,
Branch: v.Repo.Branch,
Private: v.Repo.Private,
HTTPURL: v.Repo.Clone,
SSHURL: v.Repo.CloneSSH,
}
if hook.AuthorAvatar == "" {
hook.AuthorAvatar = v.Sender.Avatar
}
return hook, repo, nil
case *scm.BranchHook:
// TODO(bradrydzewski) cleanup the branch hook code.
if v.Action == scm.ActionDelete {
return &core.Hook{
Trigger: core.TriggerHook,
Event: core.EventPush,
After: v.Ref.Sha,
Action: core.ActionDelete,
Target: scm.TrimRef(v.Ref.Name),
}, &core.Repository{
UID: v.Repo.ID,
Namespace: v.Repo.Namespace,
Name: v.Repo.Name,
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
}, nil
}
if v.Action != scm.ActionCreate {
return nil, nil, nil
}
if p.client.Driver != scm.DriverStash {
return nil, nil, nil
}
hook = &core.Hook{
Trigger: core.TriggerHook, // core.TriggerHook,
Event: core.EventPush,
Link: "",
Timestamp: 0,
Message: "",
After: v.Ref.Sha,
Ref: v.Ref.Name,
Source: v.Ref.Name,
Target: v.Ref.Name,
Author: v.Sender.Login,
AuthorName: v.Sender.Name,
AuthorEmail: v.Sender.Email,
AuthorAvatar: v.Sender.Avatar,
Sender: v.Sender.Login,
}
repo = &core.Repository{
UID: v.Repo.ID,
Namespace: v.Repo.Namespace,
Name: v.Repo.Name,
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
Link: v.Repo.Link,
Branch: v.Repo.Branch,
Private: v.Repo.Private,
HTTPURL: v.Repo.Clone,
SSHURL: v.Repo.CloneSSH,
}
return hook, repo, nil
case *scm.DeployHook:
hook = &core.Hook{
Trigger: core.TriggerHook,
Event: core.EventPromote,
Link: v.TargetURL,
Timestamp: time.Now().Unix(),
Message: v.Desc,
After: v.Ref.Sha,
Ref: v.Ref.Path,
Source: v.Ref.Name,
Target: v.Ref.Name,
Author: v.Sender.Login,
AuthorName: v.Sender.Name,
AuthorEmail: v.Sender.Email,
AuthorAvatar: v.Sender.Avatar,
Sender: v.Sender.Login,
Deployment: v.Target,
DeploymentID: v.Number,
Params: toMap(v.Data),
}
repo = &core.Repository{
UID: v.Repo.ID,
Namespace: v.Repo.Namespace,
Name: v.Repo.Name,
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
Link: v.Repo.Link,
Branch: v.Repo.Branch,
Private: v.Repo.Private,
HTTPURL: v.Repo.Clone,
SSHURL: v.Repo.CloneSSH,
}
return hook, repo, nil
default:
return nil, nil, nil
}
}
func toMap(src interface{}) map[string]string {
set, ok := src.(map[string]interface{})
if !ok {
return nil
}
dst := map[string]string{}
for k, v := range set {
dst[k] = fmt.Sprint(v)
}
return dst
}