Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Install dependencies of operators. (#1554)
When installing an operator package using the KUDO CLI, operator dependencies can be 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. Co-authored-by: Aleksey Dukhovniy <adukhovniy@mesosphere.io> Signed-off-by: Jan Schlicht <jan@d2iq.com>
- Loading branch information
Jan Schlicht
and
Aleksey Dukhovniy
committed
Jun 11, 2020
1 parent
2aac262
commit 2b4e0dd
Showing
11 changed files
with
365 additions
and
4 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
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,135 @@ | ||
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, 0, resolver); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Remove 'root' from the list of dependencies. | ||
pkgs = funk.Drop(pkgs, 1).([]packages.Resources) //nolint:errcheck | ||
|
||
return pkgs, nil | ||
} | ||
|
||
func dependencyWalk( | ||
pkgs *[]packages.Resources, | ||
g *dependencyGraph, | ||
parent packages.Resources, | ||
parentIndex int, | ||
resolver pkgresolver.Resolver) error { | ||
//nolint:errcheck | ||
childrenTasks := funk.Filter(parent.OperatorVersion.Spec.Tasks, func(task v1beta1.Task) bool { | ||
return task.Kind == engtask.KudoOperatorTaskKind | ||
}).([]v1beta1.Task) | ||
|
||
for _, childTask := range childrenTasks { | ||
childPkg, err := resolver.Resolve( | ||
childTask.Spec.KudoOperatorTaskSpec.Package, | ||
childTask.Spec.KudoOperatorTaskSpec.AppVersion, | ||
childTask.Spec.KudoOperatorTaskSpec.OperatorVersion) | ||
if err != nil { | ||
return fmt.Errorf( | ||
"failed to resolve package %s, dependency of package %s: %v", fullyQualifiedName(childTask.Spec.KudoOperatorTaskSpec), parent.OperatorVersion.FullyQualifiedName(), err) | ||
} | ||
|
||
newPackage := false | ||
childIndex := indexOf(pkgs, childPkg.Resources) | ||
if childIndex == -1 { | ||
clog.Printf("Adding new dependency %s", childPkg.Resources.OperatorVersion.FullyQualifiedName()) | ||
newPackage = true | ||
|
||
*pkgs = append(*pkgs, *childPkg.Resources) | ||
childIndex = len(*pkgs) - 1 | ||
|
||
// The number of vertices in 'g' has to match the number of packages we're tracking. | ||
g.AddVertex() | ||
} | ||
|
||
// This is a directed graph. The edge represents a dependency of the parent package on the current package. | ||
g.AddEdge(parentIndex, childIndex) | ||
|
||
if !graph.Acyclic(g) { | ||
return fmt.Errorf( | ||
"cyclic package dependency found when adding package %s -> %s", parent.OperatorVersion.FullyQualifiedName(), childPkg.Resources.OperatorVersion.FullyQualifiedName()) | ||
} | ||
|
||
// We only need to walk the dependencies if the package is new | ||
if newPackage { | ||
if err := dependencyWalk(pkgs, g, *childPkg.Resources, childIndex, resolver); err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// indexOf method searches for the pkg in pkgs that has the same OperatorVersion/AppVersion (using | ||
// EqualOperatorVersion method) and returns its index or -1 if not found. | ||
func indexOf(pkgs *[]packages.Resources, pkg *packages.Resources) int { | ||
for i, p := range *pkgs { | ||
if p.OperatorVersion.EqualOperatorVersion(pkg.OperatorVersion) { | ||
return i | ||
} | ||
} | ||
return -1 | ||
} | ||
|
||
func fullyQualifiedName(kt v1beta1.KudoOperatorTaskSpec) string { | ||
return fmt.Sprintf("%s-%s", v1beta1.OperatorVersionName(kt.Package, kt.OperatorVersion), kt.AppVersion) | ||
} |
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,196 @@ | ||
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{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: v1beta1.OperatorVersionName(name, ""), | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
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 | ||
want []string | ||
wantErr string | ||
}{ | ||
{ | ||
// A | ||
// └── A | ||
name: "trivial circular dependency", | ||
pkgs: []packages.Package{ | ||
createPackage("A", "A"), | ||
}, | ||
want: []string{}, | ||
wantErr: "cyclic package dependency found when adding package A-- -> A--", | ||
}, | ||
{ | ||
// A | ||
// └── B | ||
// └── A | ||
name: "circular dependency", | ||
pkgs: []packages.Package{ | ||
createPackage("A", "B"), | ||
createPackage("B", "A"), | ||
}, | ||
want: []string{}, | ||
wantErr: "cyclic package dependency found when adding package B-- -> A--", | ||
}, | ||
{ | ||
// A | ||
// └── B | ||
// └── C | ||
// └── B | ||
name: "nested circular dependency", | ||
pkgs: []packages.Package{ | ||
createPackage("A", "B"), | ||
createPackage("B", "C"), | ||
createPackage("C", "B"), | ||
}, | ||
want: []string{}, | ||
wantErr: "cyclic package dependency found when adding package C-- -> B--", | ||
}, | ||
{ | ||
// A | ||
// └── (B) | ||
name: "unknown dependency", | ||
pkgs: []packages.Package{ | ||
createPackage("A", "B"), | ||
}, | ||
want: []string{}, | ||
wantErr: "failed to resolve package B--, dependency of package A--: package not found", | ||
}, | ||
{ | ||
// A | ||
// └── B | ||
// └── C | ||
name: "simple dependency", | ||
pkgs: []packages.Package{ | ||
createPackage("A", "B"), | ||
createPackage("B", "C"), | ||
createPackage("C"), | ||
}, | ||
want: []string{"B", "C"}, | ||
wantErr: "", | ||
}, | ||
{ | ||
// B ----- | ||
// | \ | ||
// | | | ||
// v | | ||
// A ---> C | | ||
// | | | | ||
// | | | | ||
// \ v v | ||
// ----> D ---> E | ||
name: "complex dependency", | ||
pkgs: []packages.Package{ | ||
createPackage("A", "C", "D"), | ||
createPackage("B", "C", "E"), | ||
createPackage("C", "D"), | ||
createPackage("D", "E"), | ||
createPackage("E"), | ||
}, | ||
want: []string{"C", "D", "E"}, | ||
wantErr: "", | ||
}, | ||
{ | ||
// A | ||
// └── B | ||
// ├── C | ||
// │ ├── D | ||
// │ └── A | ||
// ├── E | ||
// └── F | ||
name: "complex circular dependency", | ||
pkgs: []packages.Package{ | ||
createPackage("A", "B"), | ||
createPackage("B", "C", "E", "F"), | ||
createPackage("C", "D", "A"), | ||
createPackage("D"), | ||
createPackage("E"), | ||
createPackage("F"), | ||
}, | ||
want: []string{}, | ||
wantErr: "cyclic package dependency found when adding package C-- -> A--", | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
tt := tt | ||
t.Run(tt.name, func(t *testing.T) { | ||
resolver := nameResolver{tt.pkgs} | ||
got, err := gatherDependencies(*tt.pkgs[0].Resources, resolver) | ||
|
||
assert.Equal(t, err == nil, tt.wantErr == "") | ||
|
||
if err != nil { | ||
assert.EqualError(t, err, tt.wantErr, tt.name) | ||
} | ||
|
||
for _, operatorName := range tt.want { | ||
operatorName := operatorName | ||
|
||
assert.NotNil(t, funk.Find(got, func(p packages.Resources) bool { | ||
return p.Operator.Name == operatorName | ||
}), tt.name) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.