-
Notifications
You must be signed in to change notification settings - Fork 104
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
When installing an operator package using the KUDO CLI, operator dependencies can bebeen provided as 'KudoOperator' tasks. For each of these tasks, the respective package is resolved and its Operator and OperatorVersion installed on the cluster. Because dependent package can also have dependencies on packages, dependencies are resolved recursively. Cyclic dependencies are detected by modeling the dependencies as a directed graph and checking this graph for cycles when new dependencies are added. Signed-off-by: Jan Schlicht <jan@d2iq.com>
- Loading branch information
Jan Schlicht
committed
Jun 8, 2020
1 parent
8841203
commit c0ed987
Showing
8 changed files
with
309 additions
and
2 deletions.
There are no files selected for viewing
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
package install | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/thoas/go-funk" | ||
"github.com/yourbasic/graph" | ||
|
||
"github.com/kudobuilder/kudo/pkg/apis/kudo/v1beta1" | ||
engtask "github.com/kudobuilder/kudo/pkg/engine/task" | ||
"github.com/kudobuilder/kudo/pkg/kudoctl/clog" | ||
"github.com/kudobuilder/kudo/pkg/kudoctl/packages" | ||
pkgresolver "github.com/kudobuilder/kudo/pkg/kudoctl/packages/resolver" | ||
) | ||
|
||
// dependencyGraph is modeled after 'graph.Mutable' but allows to add vertices. | ||
type dependencyGraph struct { | ||
edges []map[int]struct{} | ||
} | ||
|
||
// AddVertex adds a new vertex to the dependency graph. | ||
func (g *dependencyGraph) AddVertex() { | ||
g.edges = append(g.edges, map[int]struct{}{}) | ||
} | ||
|
||
// AddEdge adds an edge from vertex v to w to the dependency graph. | ||
func (g *dependencyGraph) AddEdge(v, w int) { | ||
g.edges[v][w] = struct{}{} | ||
} | ||
|
||
// Order returns the number of vertices of the dependency graph. | ||
func (g *dependencyGraph) Order() int { | ||
return len(g.edges) | ||
} | ||
|
||
func (g *dependencyGraph) Visit(v int, do func(w int, c int64) bool) bool { | ||
for w := range g.edges[v] { | ||
if do(w, 1) { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} | ||
|
||
// gatherDependencies resolved all dependencies of a package. | ||
// Dependencies are resolved recursively. | ||
// Cyclic dependencies are detected and result in an error. | ||
func gatherDependencies(root packages.Resources, resolver pkgresolver.Resolver) ([]packages.Resources, error) { | ||
pkgs := []packages.Resources{ | ||
root, | ||
} | ||
|
||
// Each vertex in 'g' matches an index in 'pkgs'. | ||
g := dependencyGraph{ | ||
edges: []map[int]struct{}{{}}, | ||
} | ||
|
||
if err := dependencyWalk(&pkgs, &g, root, resolver); err != nil { | ||
return nil, err | ||
} | ||
|
||
return pkgs, nil | ||
} | ||
|
||
func dependencyWalk( | ||
pkgs *[]packages.Resources, | ||
g *dependencyGraph, | ||
parent packages.Resources, | ||
resolver pkgresolver.Resolver) error { | ||
//nolint:errcheck // False positive, 'funk.Filter' doesn't return an error. | ||
operatorTasks := funk.Filter(parent.OperatorVersion.Spec.Tasks, func(task v1beta1.Task) bool { | ||
return task.Kind == engtask.KudoOperatorTaskKind | ||
}).([]v1beta1.Task) | ||
|
||
versionOf := func(pkg packages.Resources) func(packages.Resources) bool { | ||
return func(r packages.Resources) bool { | ||
return r.Operator.Name == pkg.Operator.Name && | ||
r.OperatorVersion.Spec.AppVersion == pkg.OperatorVersion.Spec.AppVersion && | ||
r.OperatorVersion.Spec.Version == pkg.OperatorVersion.Spec.Version | ||
} | ||
} | ||
|
||
for _, task := range operatorTasks { | ||
pkg, err := resolver.Resolve( | ||
task.Spec.Package, | ||
task.Spec.AppVersion, | ||
task.Spec.OperatorVersion) | ||
if err != nil { | ||
return fmt.Errorf( | ||
"failed to resolve package %s-%s-%s, dependency of package %s-%s-%s: %v", | ||
task.Spec.Package, | ||
task.Spec.AppVersion, | ||
task.Spec.OperatorVersion, | ||
parent.Operator.Name, | ||
parent.OperatorVersion.Spec.AppVersion, | ||
parent.OperatorVersion.Spec.Version, | ||
err) | ||
} | ||
|
||
parentIndex := funk.IndexOf(*pkgs, funk.Find(*pkgs, versionOf(parent))) | ||
if parentIndex == -1 { | ||
panic("failed to find parent index in dependency graph") | ||
} | ||
|
||
if funk.Find(*pkgs, versionOf(*pkg.Resources)) == nil { | ||
clog.Printf( | ||
"Adding new dependency %s-%s-%s", | ||
pkg.Resources.Operator.Name, | ||
pkg.Resources.OperatorVersion.Spec.AppVersion, | ||
pkg.Resources.OperatorVersion.Spec.Version) | ||
|
||
*pkgs = append(*pkgs, *pkg.Resources) | ||
|
||
// The number of vertices in 'g' has to match the number of packages | ||
// we're tracking. | ||
g.AddVertex() | ||
} | ||
|
||
pkgIndex := funk.IndexOf(*pkgs, funk.Find(*pkgs, versionOf(*pkg.Resources))) | ||
if pkgIndex == -1 { | ||
panic("failed to find package index in dependency graph") | ||
} | ||
|
||
// This is a directed graph. The edge represents a dependency of | ||
// the parent package on the current package. | ||
g.AddEdge(parentIndex, pkgIndex) | ||
|
||
if !graph.Acyclic(g) { | ||
return fmt.Errorf( | ||
"cyclic package dependency found when adding package %s-%s-%s", | ||
pkg.Resources.Operator.Name, | ||
pkg.Resources.OperatorVersion.Spec.AppVersion, | ||
pkg.Resources.OperatorVersion.Spec.Version) | ||
} | ||
|
||
if err := dependencyWalk(pkgs, g, *pkg.Resources, resolver); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
package install | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/thoas/go-funk" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
|
||
"github.com/kudobuilder/kudo/pkg/apis/kudo/v1beta1" | ||
engtask "github.com/kudobuilder/kudo/pkg/engine/task" | ||
"github.com/kudobuilder/kudo/pkg/kudoctl/packages" | ||
) | ||
|
||
type nameResolver struct { | ||
Pkgs []packages.Package | ||
} | ||
|
||
func (resolver nameResolver) Resolve( | ||
name string, | ||
appVersion string, | ||
operatorVersion string) (*packages.Package, error) { | ||
for _, pkg := range resolver.Pkgs { | ||
if pkg.Resources.Operator.Name == name { | ||
return &pkg, nil | ||
} | ||
} | ||
|
||
return nil, fmt.Errorf("package not found") | ||
} | ||
|
||
func createPackage(name string, dependencies ...string) packages.Package { | ||
p := packages.Package{ | ||
Resources: &packages.Resources{ | ||
Operator: &v1beta1.Operator{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: name, | ||
}, | ||
}, | ||
OperatorVersion: &v1beta1.OperatorVersion{}, | ||
}, | ||
} | ||
|
||
for _, dependency := range dependencies { | ||
p.Resources.OperatorVersion.Spec.Tasks = append( | ||
p.Resources.OperatorVersion.Spec.Tasks, | ||
v1beta1.Task{ | ||
Name: "dependency", | ||
Kind: engtask.KudoOperatorTaskKind, | ||
Spec: v1beta1.TaskSpec{ | ||
KudoOperatorTaskSpec: v1beta1.KudoOperatorTaskSpec{ | ||
Package: dependency, | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
return p | ||
} | ||
|
||
func TestGatherDependencies(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
pkgs []packages.Package | ||
expected []string | ||
expectedErr string | ||
}{ | ||
{ | ||
"trivial circular dependency", | ||
[]packages.Package{ | ||
createPackage("A", "A"), | ||
}, | ||
[]string{}, | ||
"cyclic package dependency found when adding package A--", | ||
}, | ||
{ | ||
"circular dependency", | ||
[]packages.Package{ | ||
createPackage("A", "B"), | ||
createPackage("B", "A"), | ||
}, | ||
[]string{}, | ||
"cyclic package dependency found when adding package A--", | ||
}, | ||
{ | ||
"unknown dependency", | ||
[]packages.Package{ | ||
createPackage("A", "B"), | ||
}, | ||
[]string{}, | ||
"failed to resolve package B--, dependency of package A--: package not found", | ||
}, | ||
{ | ||
"simple dependency", | ||
[]packages.Package{ | ||
createPackage("A", "B"), | ||
createPackage("B", "C"), | ||
createPackage("C"), | ||
}, | ||
[]string{"A", "B", "C"}, | ||
"", | ||
}, | ||
{ | ||
"complex dependency", | ||
[]packages.Package{ | ||
createPackage("A", "C", "D"), | ||
createPackage("B", "C", "E"), | ||
createPackage("C", "D"), | ||
createPackage("D", "E"), | ||
createPackage("E"), | ||
}, | ||
[]string{"A", "C", "D", "E"}, | ||
"", | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
test := test | ||
|
||
resolver := nameResolver{test.pkgs} | ||
|
||
actual, err := gatherDependencies(*test.pkgs[0].Resources, resolver) | ||
if err != nil { | ||
if test.expectedErr == "" { | ||
t.Errorf("%s: expected no error but got %v", test.name, err) | ||
} | ||
|
||
assert.EqualError(t, err, test.expectedErr, test.name) | ||
} else { | ||
if test.expectedErr != "" { | ||
t.Errorf("%s: expected an error but got none", test.name) | ||
} | ||
|
||
for _, operatorName := range test.expected { | ||
operatorName := operatorName | ||
|
||
assert.NotNil(t, funk.Find(actual, func(p packages.Resources) bool { | ||
return p.Operator.Name == operatorName | ||
}), test.name) | ||
} | ||
} | ||
} | ||
} |
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
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
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