Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
drone/trigger/trigger.go /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
581 lines (533 sloc)
15.9 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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 trigger | |
| import ( | |
| "context" | |
| "runtime/debug" | |
| "strings" | |
| "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/drone/drone/trigger/dag" | |
| "github.com/sirupsen/logrus" | |
| ) | |
| type triggerer struct { | |
| canceler core.Canceler | |
| config core.ConfigService | |
| convert core.ConvertService | |
| commits core.CommitService | |
| status core.StatusService | |
| builds core.BuildStore | |
| sched core.Scheduler | |
| repos core.RepositoryStore | |
| users core.UserStore | |
| validate core.ValidateService | |
| hooks core.WebhookSender | |
| } | |
| // New returns a new build triggerer. | |
| func New( | |
| canceler core.Canceler, | |
| config core.ConfigService, | |
| convert core.ConvertService, | |
| commits core.CommitService, | |
| status core.StatusService, | |
| builds core.BuildStore, | |
| sched core.Scheduler, | |
| repos core.RepositoryStore, | |
| users core.UserStore, | |
| validate core.ValidateService, | |
| hooks core.WebhookSender, | |
| ) core.Triggerer { | |
| return &triggerer{ | |
| canceler: canceler, | |
| config: config, | |
| convert: convert, | |
| commits: commits, | |
| status: status, | |
| builds: builds, | |
| sched: sched, | |
| repos: repos, | |
| users: users, | |
| validate: validate, | |
| 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 | |
| } | |
| if base.Event == core.EventPullRequest { | |
| if repo.IgnorePulls { | |
| logger.Infoln("trigger: skipping hook. project ignores pull requests") | |
| return nil, nil | |
| } | |
| if repo.IgnoreForks && !strings.EqualFold(base.Fork, repo.Slug) { | |
| logger.Infoln("trigger: skipping hook. project ignores forks") | |
| 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 .drone.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 | |
| // } | |
| // } | |
| tmpBuild := &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, | |
| Cron: base.Cron, | |
| Deploy: base.Deployment, | |
| DeployID: base.DeploymentID, | |
| Sender: base.Sender, | |
| Created: time.Now().Unix(), | |
| Updated: time.Now().Unix(), | |
| } | |
| req := &core.ConfigArgs{ | |
| User: user, | |
| Repo: repo, | |
| Build: tmpBuild, | |
| } | |
| raw, err := t.config.Find(ctx, req) | |
| if err != nil { | |
| logger = logger.WithError(err) | |
| logger.Warnln("trigger: cannot find yaml") | |
| return nil, err | |
| } | |
| raw, err = t.convert.Convert(ctx, &core.ConvertArgs{ | |
| User: user, | |
| Repo: repo, | |
| Build: tmpBuild, | |
| Config: raw, | |
| }) | |
| if err != nil { | |
| logger = logger.WithError(err) | |
| logger.Warnln("trigger: cannot convert yaml") | |
| return t.createBuildError(ctx, repo, base, err.Error()) | |
| } | |
| // 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, | |
| URL: repo.Link, | |
| Ref: base.Ref, | |
| }) | |
| if err != nil { | |
| logger = logger.WithError(err) | |
| logger.Warnln("trigger: cannot convert yaml") | |
| return t.createBuildError(ctx, repo, base, err.Error()) | |
| } | |
| 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 = t.validate.Validate(ctx, &core.ValidateArgs{ | |
| User: user, | |
| Repo: repo, | |
| Build: tmpBuild, | |
| Config: raw, | |
| }) | |
| if err != nil { | |
| logger = logger.WithError(err) | |
| logger.Warnln("trigger: yaml validation error") | |
| 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 | |
| var dag = dag.New() | |
| 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 | |
| name := pipeline.Name | |
| if name == "" { | |
| name = "default" | |
| } | |
| node := dag.Add(pipeline.Name, pipeline.DependsOn...) | |
| node.Skip = true | |
| if skipBranch(pipeline, base.Target) { | |
| logger = logger.WithField("pipeline", pipeline.Name) | |
| logger.Infoln("trigger: skipping pipeline, does not match branch") | |
| } else if skipEvent(pipeline, base.Event) { | |
| logger = logger.WithField("pipeline", pipeline.Name) | |
| logger.Infoln("trigger: skipping pipeline, does not match event") | |
| } else if skipAction(pipeline, base.Action) { | |
| logger = logger.WithField("pipeline", pipeline.Name).WithField("action", base.Action) | |
| logger.Infoln("trigger: skipping pipeline, does not match action") | |
| } else if skipRef(pipeline, base.Ref) { | |
| logger = logger.WithField("pipeline", pipeline.Name) | |
| logger.Infoln("trigger: skipping pipeline, does not match ref") | |
| } else if skipRepo(pipeline, repo.Slug) { | |
| logger = logger.WithField("pipeline", pipeline.Name) | |
| logger.Infoln("trigger: skipping pipeline, does not match repo") | |
| } else if skipTarget(pipeline, base.Deployment) { | |
| logger = logger.WithField("pipeline", pipeline.Name) | |
| logger.Infoln("trigger: skipping pipeline, does not match deploy target") | |
| } else if skipCron(pipeline, base.Cron) { | |
| logger = logger.WithField("pipeline", pipeline.Name) | |
| logger.Infoln("trigger: skipping pipeline, does not match cron job") | |
| } else { | |
| matched = append(matched, pipeline) | |
| node.Skip = false | |
| } | |
| } | |
| if dag.DetectCycles() { | |
| return t.createBuildError(ctx, repo, base, "Error: Dependency cycle detected in 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, | |
| DeployID: base.DeploymentID, | |
| Sender: base.Sender, | |
| Cron: base.Cron, | |
| 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: match.Kind, | |
| Type: match.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.Kind == "pipeline" && stage.Type == "" { | |
| stage.Type = "docker" | |
| } | |
| 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 | |
| } | |
| for _, stage := range stages { | |
| // here we re-work the dependencies for the stage to | |
| // account for the fact that some steps may be skipped | |
| // and may otherwise break the dependency chain. | |
| stage.DependsOn = dag.Dependencies(stage.Name) | |
| // if the stage is pending dependencies, but those | |
| // dependencies are skipped, the stage can be executed | |
| // immediately. | |
| if stage.Status == core.StatusWaiting && | |
| len(stage.DependsOn) == 0 { | |
| stage.Status = core.StatusPending | |
| } | |
| } | |
| 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 stage.Status != core.StatusPending { | |
| 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") | |
| } | |
| if repo.CancelPush && build.Event == core.EventPush || | |
| repo.CancelPulls && build.Event == core.EventPullRequest { | |
| go t.canceler.CancelPending(ctx, repo, build) | |
| } | |
| // 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, | |
| DeployID: base.DeploymentID, | |
| Sender: base.Sender, | |
| Created: time.Now().Unix(), | |
| Updated: time.Now().Unix(), | |
| Started: 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 | |
| // } |