partial fix for #2654

This commit is contained in:
Brad Rydzewski 2019-04-19 17:20:40 -07:00
parent 16707c4bdb
commit fb837e4df0
4 changed files with 277 additions and 18 deletions

2
go.sum
View file

@ -126,8 +126,6 @@ github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uP
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-retryablehttp v0.0.0-20180718195005-e651d75abec6 h1:qCv4319q2q7XKn0MQbi8p37hsJ+9Xo8e6yojA73JVxk=
github.com/hashicorp/go-retryablehttp v0.0.0-20180718195005-e651d75abec6/go.mod h1:fXcdFsQoipQa7mwORhKad5jmDCeSy/RCGzWA08PO0lM=
github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/nomad v0.0.0-20190125003214-134391155854 h1:L7WhLZt2ory/kQWxqkMwOiBpIoa4BWoadN7yx8LHEtk=

116
trigger/dag/dag.go Normal file
View file

@ -0,0 +1,116 @@
// Copyright 2019 Drone IO, Inc.
// Copyright 2018 natessilva
//
// 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 dag
// Dag is a directed acyclic graph.
type Dag struct {
graph map[string]*Vertex
}
// Vertex is a vetex in the graph.
type Vertex struct {
Name string
Skip bool
graph []string
}
// New creates a new directed acyclic graph (dag) that can
// determinte if a stage has dependencies.
func New() *Dag {
return &Dag{
graph: make(map[string]*Vertex),
}
}
// Add establishes a dependency between two vertices in the graph.
func (d *Dag) Add(from string, to ...string) *Vertex {
vertex := new(Vertex)
vertex.Name = from
vertex.Skip = false
vertex.graph = to
d.graph[from] = vertex
return vertex
}
// Get returns the vertex from the graph.
func (d *Dag) Get(name string) (*Vertex, bool) {
vertex, ok := d.graph[name]
return vertex, ok
}
// Ancestors returns the acentors of the vertex.
func (d *Dag) Ancestors(name string) []*Vertex {
vertex := d.graph[name]
return d.ancestors(vertex)
}
func (d *Dag) ancestors(parent *Vertex) []*Vertex {
if parent == nil {
return nil
}
var combined []*Vertex
for _, name := range parent.graph {
vertex, found := d.graph[name]
if !found {
continue
}
if !vertex.Skip {
combined = append(combined, vertex)
}
combined = append(combined, d.ancestors(vertex)...)
}
return combined
}
// DetectCycles returns true if cycles are detected in the graph.
func (d *Dag) DetectCycles() bool {
visited := make(map[string]bool)
recStack := make(map[string]bool)
for vertex := range d.graph {
if !visited[vertex] {
if d.detectCycles(vertex, visited, recStack) {
return true
}
}
}
return false
}
func (d *Dag) detectCycles(name string, visited, recStack map[string]bool) bool {
visited[name] = true
recStack[name] = true
vertex, ok := d.graph[name]
if !ok {
return false
}
for _, v := range vertex.graph {
// only check cycles on a vertex one time
if !visited[v] {
if d.detectCycles(v, visited, recStack) {
return true
}
// if we've visited this vertex in this recursion
// stack, then we have a cycle
} else if recStack[v] {
return true
}
}
recStack[name] = false
return false
}

141
trigger/dag/dag_test.go Normal file
View file

@ -0,0 +1,141 @@
// 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.
// +build !oss
package dag
import (
"testing"
)
func TestDag(t *testing.T) {
dag := New()
dag.Add("backend")
dag.Add("frontend")
dag.Add("notify", "backend", "frontend")
if dag.DetectCycles() {
t.Errorf("cycles detected")
}
dag = New()
dag.Add("notify", "backend", "frontend")
if dag.DetectCycles() {
t.Errorf("cycles detected")
}
dag = New()
dag.Add("backend", "frontend")
dag.Add("frontend", "backend")
dag.Add("notify", "backend", "frontend")
if dag.DetectCycles() == false {
t.Errorf("Expect cycles detected")
}
dag = New()
dag.Add("backend", "backend")
dag.Add("frontend", "backend")
dag.Add("notify", "backend", "frontend")
if dag.DetectCycles() == false {
t.Errorf("Expect cycles detected")
}
dag = New()
dag.Add("backend")
dag.Add("frontend")
dag.Add("notify", "backend", "frontend", "notify")
if dag.DetectCycles() == false {
t.Errorf("Expect cycles detected")
}
}
func TestAncestors(t *testing.T) {
dag := New()
v := dag.Add("backend")
dag.Add("frontend", "backend")
dag.Add("notify", "frontend")
ancestors := dag.Ancestors("frontend")
if got, want := len(ancestors), 1; got != want {
t.Errorf("Want %d ancestors, got %d", want, got)
}
if ancestors[0] != v {
t.Errorf("Unexpected ancestor")
}
if v := dag.Ancestors("backend"); len(v) != 0 {
t.Errorf("Expect vertexes with no dependences has zero ancestors")
}
}
func TestAncestors_Skipped(t *testing.T) {
dag := New()
dag.Add("backend").Skip = true
dag.Add("frontend", "backend").Skip = true
dag.Add("notify", "frontend")
if v := dag.Ancestors("frontend"); len(v) != 0 {
t.Errorf("Expect skipped vertexes excluded")
}
if v := dag.Ancestors("notify"); len(v) != 0 {
t.Errorf("Expect skipped vertexes excluded")
}
}
func TestAncestors_NotFound(t *testing.T) {
dag := New()
dag.Add("backend")
dag.Add("frontend", "backend")
dag.Add("notify", "frontend")
if dag.DetectCycles() {
t.Errorf("cycles detected")
}
if v := dag.Ancestors("does-not-exist"); len(v) != 0 {
t.Errorf("Expect vertex not found does not panic")
}
}
func TestAncestors_Malformed(t *testing.T) {
dag := New()
dag.Add("backend")
dag.Add("frontend", "does-not-exist")
dag.Add("notify", "frontend")
if dag.DetectCycles() {
t.Errorf("cycles detected")
}
if v := dag.Ancestors("frontend"); len(v) != 0 {
t.Errorf("Expect invalid dependency does not panic")
}
}
func TestAncestors_Complex(t *testing.T) {
dag := New()
dag.Add("backend")
dag.Add("frontend")
dag.Add("publish", "backend", "frontend")
dag.Add("deploy", "publish")
last := dag.Add("notify", "deploy")
if dag.DetectCycles() {
t.Errorf("cycles detected")
}
ancestors := dag.Ancestors("notify")
if got, want := len(ancestors), 4; got != want {
t.Errorf("Want %d ancestors, got %d", want, got)
return
}
for _, ancestor := range ancestors {
if ancestor == last {
t.Errorf("Unexpected ancestor")
}
}
v, _ := dag.Get("publish")
v.Skip = true
ancestors = dag.Ancestors("notify")
if got, want := len(ancestors), 3; got != want {
t.Errorf("Want %d ancestors, got %d", want, got)
return
}
}

View file

@ -26,6 +26,7 @@ import (
"github.com/drone/drone-yaml/yaml/signer"
"github.com/drone/drone/core"
"github.com/drone/drone/trigger/dag"
"github.com/sirupsen/logrus"
)
@ -234,6 +235,7 @@ func (t *triggerer) Trigger(ctx context.Context, repo *core.Repository, base *co
// }
var matched []*yaml.Pipeline
var dag = dag.New()
for _, document := range manifest.Resources {
pipeline, ok := document.(*yaml.Pipeline)
if !ok {
@ -243,38 +245,34 @@ func (t *triggerer) Trigger(ctx context.Context, repo *core.Repository, base *co
// 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")
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 if skipCron(pipeline, base.Cron) {
logger = logger.WithField("pipeline", pipeline.Name)
logger.Infoln("trigger: skipping pipeline, does not match cron job")
continue
} else {
matched = append(matched, pipeline)
node.Skip = false
}
}
@ -365,6 +363,15 @@ func (t *triggerer) Trigger(ctx context.Context, repo *core.Repository, base *co
stages[i] = stage
}
for _, stage := range stages {
if stage.Status != core.StatusWaiting {
continue
}
if deps := dag.Ancestors(stage.Name); len(deps) == 0 {
stage.Status = core.StatusPending
}
}
err = t.builds.Create(ctx, build, stages)
if err != nil {
logger = logger.WithError(err)
@ -382,10 +389,7 @@ func (t *triggerer) Trigger(ctx context.Context, repo *core.Repository, base *co
}
for _, stage := range stages {
if len(stage.DependsOn) != 0 {
continue
}
if stage.Status == core.StatusBlocked {
if stage.Status != core.StatusPending {
continue
}
err = t.sched.Schedule(ctx, stage)