494 lines
13 KiB
Go
494 lines
13 KiB
Go
|
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||
|
// Use of this source code is governed by the Drone Non-Commercial License
|
||
|
// that can be found in the LICENSE file.
|
||
|
|
||
|
package trigger
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"runtime/debug"
|
||
|
"time"
|
||
|
|
||
|
"github.com/drone/drone-yaml/yaml"
|
||
|
"github.com/drone/drone-yaml/yaml/converter"
|
||
|
"github.com/drone/drone-yaml/yaml/linter"
|
||
|
"github.com/drone/drone-yaml/yaml/signer"
|
||
|
|
||
|
"github.com/drone/drone/core"
|
||
|
|
||
|
"github.com/sirupsen/logrus"
|
||
|
)
|
||
|
|
||
|
type triggerer struct {
|
||
|
config core.ConfigService
|
||
|
commits core.CommitService
|
||
|
status core.StatusService
|
||
|
builds core.BuildStore
|
||
|
sched core.Scheduler
|
||
|
repos core.RepositoryStore
|
||
|
users core.UserStore
|
||
|
hooks core.WebhookSender
|
||
|
}
|
||
|
|
||
|
// New returns a new build triggerer.
|
||
|
func New(
|
||
|
config core.ConfigService,
|
||
|
commits core.CommitService,
|
||
|
status core.StatusService,
|
||
|
builds core.BuildStore,
|
||
|
sched core.Scheduler,
|
||
|
repos core.RepositoryStore,
|
||
|
users core.UserStore,
|
||
|
hooks core.WebhookSender,
|
||
|
) core.Triggerer {
|
||
|
return &triggerer{
|
||
|
config: config,
|
||
|
commits: commits,
|
||
|
status: status,
|
||
|
builds: builds,
|
||
|
sched: sched,
|
||
|
repos: repos,
|
||
|
users: users,
|
||
|
hooks: hooks,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (t *triggerer) Trigger(ctx context.Context, repo *core.Repository, base *core.Hook) (*core.Build, error) {
|
||
|
logger := logrus.WithFields(
|
||
|
logrus.Fields{
|
||
|
"repo": repo.Slug,
|
||
|
"ref": base.Ref,
|
||
|
"event": base.Event,
|
||
|
"commit": base.After,
|
||
|
},
|
||
|
)
|
||
|
|
||
|
logger.Debugln("trigger: received")
|
||
|
defer func() {
|
||
|
// taking the paranoid approach to recover from
|
||
|
// a panic that should absolutely never happen.
|
||
|
if r := recover(); r != nil {
|
||
|
logger.Errorf("runner: unexpected panic: %s", r)
|
||
|
debug.PrintStack()
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
if skipMessage(base) {
|
||
|
logger.Infoln("trigger: skipping hook. found skip directive")
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
user, err := t.users.Find(ctx, repo.UserID)
|
||
|
if err != nil {
|
||
|
logger = logger.WithError(err)
|
||
|
logger.Warnln("trigger: cannot find repository owner")
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if user.Active == false {
|
||
|
logger.Infoln("trigger: skipping hook. repository owner is inactive")
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
// if the commit message is not included we should
|
||
|
// make an optional API call to the version control
|
||
|
// system to augment the available information.
|
||
|
if base.Message == "" && base.After != "" {
|
||
|
commit, err := t.commits.Find(ctx, user, repo.Slug, base.After)
|
||
|
if err == nil && commit != nil {
|
||
|
base.Message = commit.Message
|
||
|
if base.AuthorEmail == "" {
|
||
|
base.AuthorEmail = commit.Author.Email
|
||
|
}
|
||
|
if base.AuthorName == "" {
|
||
|
base.AuthorName = commit.Author.Name
|
||
|
}
|
||
|
if base.AuthorAvatar == "" {
|
||
|
base.AuthorAvatar = commit.Author.Avatar
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// // some tag hooks provide the tag but do not provide the sha.
|
||
|
// // this may be important if we want to fetch the .core.yml
|
||
|
// if base.After == "" && base.Event == core.EventTag {
|
||
|
// tag, _, err := t.client.Git.FindTag(ctx, repo.Slug, base.Ref)
|
||
|
// if err != nil {
|
||
|
// logger.Error().Err(err).
|
||
|
// Msg("cannot find tag")
|
||
|
// return nil, err
|
||
|
// }
|
||
|
// base.After = tag.Sha
|
||
|
// }
|
||
|
|
||
|
// TODO: do a better job of documenting this
|
||
|
// obj := base.After
|
||
|
// if len(obj) == 0 {
|
||
|
// if strings.HasPrefix(base.Ref, "refs/pull/") {
|
||
|
// obj = base.Target
|
||
|
// } else {
|
||
|
// obj = base.Ref
|
||
|
// }
|
||
|
// }
|
||
|
|
||
|
req := &core.ConfigArgs{
|
||
|
User: user,
|
||
|
Repo: repo,
|
||
|
// TODO this is duplicated
|
||
|
Build: &core.Build{
|
||
|
RepoID: repo.ID,
|
||
|
Trigger: base.Trigger,
|
||
|
Parent: base.Parent,
|
||
|
Status: core.StatusPending,
|
||
|
Event: base.Event,
|
||
|
Action: base.Action,
|
||
|
Link: base.Link,
|
||
|
// Timestamp: base.Timestamp,
|
||
|
Title: base.Title,
|
||
|
Message: base.Message,
|
||
|
Before: base.Before,
|
||
|
After: base.After,
|
||
|
Ref: base.Ref,
|
||
|
Fork: base.Fork,
|
||
|
Source: base.Source,
|
||
|
Target: base.Target,
|
||
|
Author: base.Author,
|
||
|
AuthorName: base.AuthorName,
|
||
|
AuthorEmail: base.AuthorEmail,
|
||
|
AuthorAvatar: base.AuthorAvatar,
|
||
|
Params: base.Params,
|
||
|
Deploy: base.Deployment,
|
||
|
Sender: base.Sender,
|
||
|
Created: time.Now().Unix(),
|
||
|
Updated: time.Now().Unix(),
|
||
|
},
|
||
|
}
|
||
|
|
||
|
raw, err := t.config.Find(ctx, req)
|
||
|
if err != nil {
|
||
|
logger = logger.WithError(err)
|
||
|
logger.Warnln("trigger: cannot find yaml")
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// this code is temporarily in place to detect and convert
|
||
|
// the legacy yaml configuration file to the new format.
|
||
|
raw.Data, err = converter.ConvertString(raw.Data, converter.Metadata{
|
||
|
Filename: repo.Config,
|
||
|
Ref: base.Ref,
|
||
|
})
|
||
|
if err != nil {
|
||
|
logger = logger.WithError(err)
|
||
|
logger.Warnln("trigger: cannot convert yaml")
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
manifest, err := yaml.ParseString(raw.Data)
|
||
|
if err != nil {
|
||
|
logger = logger.WithError(err)
|
||
|
logger.Warnln("trigger: cannot parse yaml")
|
||
|
return t.createBuildError(ctx, repo, base, err.Error())
|
||
|
}
|
||
|
|
||
|
err = linter.Manifest(manifest, repo.Trusted)
|
||
|
if err != nil {
|
||
|
logger = logger.WithError(err)
|
||
|
logger.Warnln("trigger: yaml linting error")
|
||
|
return t.createBuildError(ctx, repo, base, err.Error())
|
||
|
}
|
||
|
|
||
|
verified := true
|
||
|
if repo.Protected && base.Trigger == core.TriggerHook {
|
||
|
key := signer.KeyString(repo.Secret)
|
||
|
val := []byte(raw.Data)
|
||
|
verified, _ = signer.Verify(val, key)
|
||
|
}
|
||
|
|
||
|
// var paths []string
|
||
|
// paths, err := listChanges(t.client, repo, base)
|
||
|
// if err != nil {
|
||
|
// logger.Warn().Err(err).
|
||
|
// Msg("cannot fetch changeset")
|
||
|
// }
|
||
|
|
||
|
var matched []*yaml.Pipeline
|
||
|
for _, document := range manifest.Resources {
|
||
|
pipeline, ok := document.(*yaml.Pipeline)
|
||
|
if !ok {
|
||
|
continue
|
||
|
}
|
||
|
// TODO add repo
|
||
|
// TODO add instance
|
||
|
// TODO add target
|
||
|
// TODO add ref
|
||
|
if skipBranch(pipeline, base.Target) {
|
||
|
logger = logger.WithField("pipeline", pipeline.Name)
|
||
|
logger.Infoln("trigger: skipping pipeline, does not match branch")
|
||
|
continue
|
||
|
} else if skipEvent(pipeline, base.Event) {
|
||
|
logger = logger.WithField("pipeline", pipeline.Name)
|
||
|
logger.Infoln("trigger: skipping pipeline, does not match event")
|
||
|
continue
|
||
|
// } else if skipPaths(pipeline, paths) {
|
||
|
// logger.Debug().
|
||
|
// Str("branch", base.Target).
|
||
|
// Str("pipeline", pipeline.Name).
|
||
|
// Msg("skipping pipeline. does not match changed paths")
|
||
|
// continue
|
||
|
} else if skipRef(pipeline, base.Ref) {
|
||
|
logger = logger.WithField("pipeline", pipeline.Name)
|
||
|
logger.Infoln("trigger: skipping pipeline, does not match ref")
|
||
|
continue
|
||
|
} else if skipRepo(pipeline, repo.Slug) {
|
||
|
logger = logger.WithField("pipeline", pipeline.Name)
|
||
|
logger.Infoln("trigger: skipping pipeline, does not match repo")
|
||
|
continue
|
||
|
} else if skipTarget(pipeline, base.Deployment) {
|
||
|
logger = logger.WithField("pipeline", pipeline.Name)
|
||
|
logger.Infoln("trigger: skipping pipeline, does not match deploy target")
|
||
|
continue
|
||
|
} else {
|
||
|
matched = append(matched, pipeline)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if len(matched) == 0 {
|
||
|
logger.Infoln("trigger: skipping build, no matching pipelines")
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
repo, err = t.repos.Increment(ctx, repo)
|
||
|
if err != nil {
|
||
|
logger = logger.WithError(err)
|
||
|
logger.Errorln("trigger: cannot increment build sequence")
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
build := &core.Build{
|
||
|
RepoID: repo.ID,
|
||
|
Trigger: base.Trigger,
|
||
|
Number: repo.Counter,
|
||
|
Parent: base.Parent,
|
||
|
Status: core.StatusPending,
|
||
|
Event: base.Event,
|
||
|
Action: base.Action,
|
||
|
Link: base.Link,
|
||
|
// Timestamp: base.Timestamp,
|
||
|
Title: trunc(base.Title, 2000),
|
||
|
Message: trunc(base.Message, 2000),
|
||
|
Before: base.Before,
|
||
|
After: base.After,
|
||
|
Ref: base.Ref,
|
||
|
Fork: base.Fork,
|
||
|
Source: base.Source,
|
||
|
Target: base.Target,
|
||
|
Author: base.Author,
|
||
|
AuthorName: base.AuthorName,
|
||
|
AuthorEmail: base.AuthorEmail,
|
||
|
AuthorAvatar: base.AuthorAvatar,
|
||
|
Params: base.Params,
|
||
|
Deploy: base.Deployment,
|
||
|
Sender: base.Sender,
|
||
|
Created: time.Now().Unix(),
|
||
|
Updated: time.Now().Unix(),
|
||
|
}
|
||
|
|
||
|
stages := make([]*core.Stage, len(matched))
|
||
|
for i, match := range matched {
|
||
|
onSuccess := match.Trigger.Status.Match(core.StatusPassing)
|
||
|
onFailure := match.Trigger.Status.Match(core.StatusFailing)
|
||
|
if len(match.Trigger.Status.Include)+len(match.Trigger.Status.Exclude) == 0 {
|
||
|
onFailure = false
|
||
|
}
|
||
|
|
||
|
stage := &core.Stage{
|
||
|
RepoID: repo.ID,
|
||
|
Number: i + 1,
|
||
|
Name: match.Name,
|
||
|
Kind: "",
|
||
|
Type: "",
|
||
|
OS: match.Platform.OS,
|
||
|
Arch: match.Platform.Arch,
|
||
|
Variant: match.Platform.Variant,
|
||
|
Kernel: match.Platform.Version,
|
||
|
Limit: match.Concurrency.Limit,
|
||
|
Status: core.StatusWaiting,
|
||
|
DependsOn: match.DependsOn,
|
||
|
OnSuccess: onSuccess,
|
||
|
OnFailure: onFailure,
|
||
|
Labels: match.Node,
|
||
|
Created: time.Now().Unix(),
|
||
|
Updated: time.Now().Unix(),
|
||
|
}
|
||
|
|
||
|
if stage.OS == "" {
|
||
|
stage.OS = "linux"
|
||
|
}
|
||
|
if stage.Arch == "" {
|
||
|
stage.Arch = "amd64"
|
||
|
}
|
||
|
|
||
|
if stage.Name == "" {
|
||
|
stage.Name = "default"
|
||
|
}
|
||
|
if verified == false {
|
||
|
stage.Status = core.StatusBlocked
|
||
|
} else if len(stage.DependsOn) == 0 {
|
||
|
stage.Status = core.StatusPending
|
||
|
}
|
||
|
stages[i] = stage
|
||
|
}
|
||
|
|
||
|
err = t.builds.Create(ctx, build, stages)
|
||
|
if err != nil {
|
||
|
logger = logger.WithError(err)
|
||
|
logger.Errorln("trigger: cannot create build")
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
err = t.status.Send(ctx, user, &core.StatusInput{
|
||
|
Repo: repo,
|
||
|
Build: build,
|
||
|
})
|
||
|
if err != nil {
|
||
|
logger = logger.WithError(err)
|
||
|
logger.Warnln("trigger: cannot create status")
|
||
|
}
|
||
|
|
||
|
for _, stage := range stages {
|
||
|
if len(stage.DependsOn) != 0 {
|
||
|
continue
|
||
|
}
|
||
|
if stage.Status == core.StatusBlocked {
|
||
|
continue
|
||
|
}
|
||
|
err = t.sched.Schedule(ctx, stage)
|
||
|
if err != nil {
|
||
|
logger = logger.WithError(err)
|
||
|
logger.Errorln("trigger: cannot enqueue build")
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
payload := &core.WebhookData{
|
||
|
Event: core.WebhookEventBuild,
|
||
|
Action: core.WebhookActionCreated,
|
||
|
User: user,
|
||
|
Repo: repo,
|
||
|
Build: build,
|
||
|
}
|
||
|
err = t.hooks.Send(ctx, payload)
|
||
|
if err != nil {
|
||
|
logger = logger.WithError(err)
|
||
|
logger.Warnln("trigger: cannot send webhook")
|
||
|
}
|
||
|
// err = t.hooks.SendEndpoint(ctx, payload, repo.Endpoints.Webhook)
|
||
|
// if err != nil {
|
||
|
// logger.Warn().Err(err).
|
||
|
// Int64("build", build.Number).
|
||
|
// Msg("cannot send user-defined webhook")
|
||
|
// }
|
||
|
|
||
|
// // we should only synchronize the cronjob list on push
|
||
|
// // events to the default branch.
|
||
|
// if build.Event == core.EventPush &&
|
||
|
// build.Target == repo.Branch {
|
||
|
// err = t.cron.Sync(ctx, repo, manifest)
|
||
|
// if err != nil {
|
||
|
// logger.Warn().Err(err).
|
||
|
// Msg("cannot sync cronjobs")
|
||
|
// }
|
||
|
// }
|
||
|
|
||
|
return build, nil
|
||
|
}
|
||
|
|
||
|
func trunc(s string, i int) string {
|
||
|
runes := []rune(s)
|
||
|
if len(runes) > i {
|
||
|
return string(runes[:i])
|
||
|
}
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
func (t *triggerer) createBuildError(ctx context.Context, repo *core.Repository, base *core.Hook, message string) (*core.Build, error) {
|
||
|
repo, err := t.repos.Increment(ctx, repo)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
build := &core.Build{
|
||
|
RepoID: repo.ID,
|
||
|
Number: repo.Counter,
|
||
|
Parent: base.Parent,
|
||
|
Status: core.StatusError,
|
||
|
Error: message,
|
||
|
Event: base.Event,
|
||
|
Action: base.Action,
|
||
|
Link: base.Link,
|
||
|
// Timestamp: base.Timestamp,
|
||
|
Title: base.Title,
|
||
|
Message: base.Message,
|
||
|
Before: base.Before,
|
||
|
After: base.After,
|
||
|
Ref: base.Ref,
|
||
|
Fork: base.Fork,
|
||
|
Source: base.Source,
|
||
|
Target: base.Target,
|
||
|
Author: base.Author,
|
||
|
AuthorName: base.AuthorName,
|
||
|
AuthorEmail: base.AuthorEmail,
|
||
|
AuthorAvatar: base.AuthorAvatar,
|
||
|
Deploy: base.Deployment,
|
||
|
Sender: base.Sender,
|
||
|
Created: time.Now().Unix(),
|
||
|
Updated: time.Now().Unix(),
|
||
|
Finished: time.Now().Unix(),
|
||
|
}
|
||
|
|
||
|
err = t.builds.Create(ctx, build, nil)
|
||
|
return build, err
|
||
|
}
|
||
|
|
||
|
// func shouldBlock(repo *core.Repository, build *core.Build) bool {
|
||
|
// switch {
|
||
|
// case repo.Hooks.Promote == core.HookBlock && build.Event == core.EventPromote:
|
||
|
// return true
|
||
|
// case repo.Hooks.Rollback == core.HookBlock && build.Event == core.EventRollback:
|
||
|
// return true
|
||
|
// case repo.Hooks.Deploy == core.HookBlock && build.Event == core.EventRollback:
|
||
|
// return true
|
||
|
// case repo.Hooks.Pull == core.HookBlock && build.Event == core.EventPullRequest:
|
||
|
// return true
|
||
|
// case repo.Hooks.Push == core.HookBlock && build.Event == core.EventPush:
|
||
|
// return true
|
||
|
// case repo.Hooks.Tags == core.HookBlock && build.Event == core.EventTag:
|
||
|
// return true
|
||
|
// case repo.Hooks.Forks == core.HookBlock && build.Fork != repo.Slug:
|
||
|
// return true
|
||
|
// default:
|
||
|
// return false
|
||
|
// }
|
||
|
// }
|
||
|
|
||
|
// func skipHook(repo *core.Repository, build *core.Hook) bool {
|
||
|
// switch {
|
||
|
// case repo.Hooks.Promote == core.HookDisable && build.Event == core.EventPromote:
|
||
|
// return true
|
||
|
// case repo.Hooks.Rollback == core.HookDisable && build.Event == core.EventRollback:
|
||
|
// return true
|
||
|
// case repo.Hooks.Pull == core.HookDisable && build.Event == core.EventPullRequest:
|
||
|
// return true
|
||
|
// case repo.Hooks.Push == core.HookDisable && build.Event == core.EventPush:
|
||
|
// return true
|
||
|
// case repo.Hooks.Tags == core.HookDisable && build.Event == core.EventTag:
|
||
|
// return true
|
||
|
// default:
|
||
|
// return false
|
||
|
// }
|
||
|
// }
|
||
|
|
||
|
// func skipFork(repo *core.Repository, build *core.Hook) bool {
|
||
|
// return repo.Hooks.Forks == core.HookDisable && build.Fork != repo.Slug
|
||
|
// }
|