Skip to content

Commit

Permalink
Make runtime dependency package to use meta API
Browse files Browse the repository at this point in the history
This commit makes the runtime dependency package use the API structs
and interface from our own meta package, and aligns the way we make
assumptions about objects being being controller-runtime compatible
options with that of the other helper packages like `controller` and
`conditions`.

Signed-off-by: Hidde Beydals <[email protected]>
  • Loading branch information
hiddeco committed Jul 7, 2021
1 parent 7c2b0b8 commit bac7da8
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 96 deletions.
19 changes: 19 additions & 0 deletions runtime/dependency/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
Copyright 2020 The Flux authors
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 dependency contains an utility for sorting a set of Kubernetes resource objects that implement the
// Dependent interface.
package dependency
63 changes: 27 additions & 36 deletions runtime/dependency/sort.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,50 +20,33 @@ import (
"fmt"

"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/internal/tarjan"
)

// Dependent provides an interface for resources that maintain
// CrossNamespaceDependencyReference list.
// Dependent interface defines methods that a Kubernetes resource object should implement in order to use the dependency
// package for ordering dependencies.
type Dependent interface {
// GetDependsOn returns the Dependent's types.NamespacedName,
// and the CrossNamespaceDependencyReference slice it depends on.
GetDependsOn() (types.NamespacedName, []CrossNamespaceDependencyReference)
client.Object
meta.ObjectWithDependencies
}

// CrossNamespaceDependencyReference holds the reference to a dependency.
type CrossNamespaceDependencyReference struct {
// Namespace holds the namespace reference of a dependency.
// +optional
Namespace string `json:"namespace,omitempty"`

// Name holds the name reference of a dependency.
// +required
Name string `json:"name"`
}

func (r CrossNamespaceDependencyReference) String() string {
if r.Namespace == "" {
return r.Name
}
return fmt.Sprintf("%s%c%s", r.Namespace, types.Separator, r.Name)
}

// CircularDependencyError contains the circular dependency chains
// that were detected while sorting the Dependent dependencies.
// CircularDependencyError contains the circular dependency chains that were detected while sorting the Dependent
// dependencies.
type CircularDependencyError [][]string

func (e CircularDependencyError) Error() string {
return fmt.Sprintf("circular dependencies: %v", [][]string(e))
}

// Sort sorts the Dependent slice based on their listed
// dependencies using Tarjan's strongly connected components algorithm.
func Sort(d []Dependent) ([]CrossNamespaceDependencyReference, error) {
// Sort sorts the Dependent slice based on their listed dependencies using Tarjan's strongly connected components
// algorithm.
func Sort(d []Dependent) ([]meta.NamespacedObjectReference, error) {
g, l := buildGraph(d)
sccs := tarjan.SCC(g)
var sorted []CrossNamespaceDependencyReference
var sorted []meta.NamespacedObjectReference
var circular CircularDependencyError
for i := 0; i < len(sccs); i++ {
s := sccs[i]
Expand All @@ -84,18 +67,22 @@ func Sort(d []Dependent) ([]CrossNamespaceDependencyReference, error) {
return sorted, nil
}

func buildGraph(d []Dependent) (tarjan.Graph, map[string]CrossNamespaceDependencyReference) {
func buildGraph(d []Dependent) (tarjan.Graph, map[string]meta.NamespacedObjectReference) {
g := make(tarjan.Graph)
l := make(map[string]CrossNamespaceDependencyReference)
l := make(map[string]meta.NamespacedObjectReference)
for i := 0; i < len(d); i++ {
name, deps := d[i].GetDependsOn()
g[name.String()] = buildEdges(deps, name.Namespace)
l[name.String()] = CrossNamespaceDependencyReference(name)
ref := meta.NamespacedObjectReference{
Namespace: d[i].GetNamespace(),
Name: d[i].GetName(),
}
deps := d[i].GetDependsOn()
g[namespacedNameObjRef(ref)] = buildEdges(deps, ref.Namespace)
l[namespacedNameObjRef(ref)] = ref
}
return g, l
}

func buildEdges(d []CrossNamespaceDependencyReference, defaultNamespace string) tarjan.Edges {
func buildEdges(d []meta.NamespacedObjectReference, defaultNamespace string) tarjan.Edges {
if len(d) == 0 {
return nil
}
Expand All @@ -104,7 +91,11 @@ func buildEdges(d []CrossNamespaceDependencyReference, defaultNamespace string)
if v.Namespace == "" {
v.Namespace = defaultNamespace
}
e[v.String()] = struct{}{}
e[namespacedNameObjRef(v)] = struct{}{}
}
return e
}

func namespacedNameObjRef(ref meta.NamespacedObjectReference) string {
return ref.Namespace + string(types.Separator) + ref.Name
}
144 changes: 84 additions & 60 deletions runtime/dependency/sort_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,38 @@ import (
"reflect"
"testing"

"k8s.io/apimachinery/pkg/types"
"github.com/fluxcd/pkg/apis/meta"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type MockDependent struct {
NamespacedName types.NamespacedName
DependsOn []CrossNamespaceDependencyReference
corev1.Node
DependsOn []meta.NamespacedObjectReference
}

func (d MockDependent) GetDependsOn() (types.NamespacedName, []CrossNamespaceDependencyReference) {
return d.NamespacedName, d.DependsOn
func (d MockDependent) GetDependsOn() []meta.NamespacedObjectReference {
return d.DependsOn
}

func TestDependencySort(t *testing.T) {
tests := []struct {
name string
d []Dependent
want []CrossNamespaceDependencyReference
want []meta.NamespacedObjectReference
wantErr bool
}{
{
"simple",
[]Dependent{
MockDependent{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "frontend",
&MockDependent{
Node: corev1.Node{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
Name: "frontend",
},
},
DependsOn: []CrossNamespaceDependencyReference{
DependsOn: []meta.NamespacedObjectReference{
{
Namespace: "linkerd",
Name: "linkerd",
Expand All @@ -58,26 +62,30 @@ func TestDependencySort(t *testing.T) {
},
},
},
MockDependent{
NamespacedName: types.NamespacedName{
Namespace: "linkerd",
Name: "linkerd",
&MockDependent{
Node: corev1.Node{
ObjectMeta: v1.ObjectMeta{
Namespace: "linkerd",
Name: "linkerd",
},
},
},
MockDependent{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "backend",
&MockDependent{
Node: corev1.Node{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
Name: "backend",
},
},
DependsOn: []CrossNamespaceDependencyReference{
DependsOn: []meta.NamespacedObjectReference{
{
Namespace: "linkerd",
Name: "linkerd",
},
},
},
},
[]CrossNamespaceDependencyReference{
[]meta.NamespacedObjectReference{
{
Namespace: "linkerd",
Name: "linkerd",
Expand All @@ -96,36 +104,42 @@ func TestDependencySort(t *testing.T) {
{
"circular dependency",
[]Dependent{
MockDependent{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "dependency",
&MockDependent{
Node: corev1.Node{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
Name: "dependency",
},
},
DependsOn: []CrossNamespaceDependencyReference{
DependsOn: []meta.NamespacedObjectReference{
{
Namespace: "default",
Name: "endless",
},
},
},
MockDependent{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "endless",
&MockDependent{
Node: corev1.Node{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
Name: "endless",
},
},
DependsOn: []CrossNamespaceDependencyReference{
DependsOn: []meta.NamespacedObjectReference{
{
Namespace: "default",
Name: "circular",
},
},
},
MockDependent{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "circular",
&MockDependent{
Node: corev1.Node{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
Name: "circular",
},
},
DependsOn: []CrossNamespaceDependencyReference{
DependsOn: []meta.NamespacedObjectReference{
{
Namespace: "default",
Name: "dependency",
Expand All @@ -139,25 +153,29 @@ func TestDependencySort(t *testing.T) {
{
"missing namespace",
[]Dependent{
MockDependent{
NamespacedName: types.NamespacedName{
Namespace: "application",
Name: "backend",
&MockDependent{
Node: corev1.Node{
ObjectMeta: v1.ObjectMeta{
Namespace: "application",
Name: "backend",
},
},
},
MockDependent{
NamespacedName: types.NamespacedName{
Namespace: "application",
Name: "frontend",
&MockDependent{
Node: corev1.Node{
ObjectMeta: v1.ObjectMeta{
Namespace: "application",
Name: "frontend",
},
},
DependsOn: []CrossNamespaceDependencyReference{
DependsOn: []meta.NamespacedObjectReference{
{
Name: "backend",
},
},
},
},
[]CrossNamespaceDependencyReference{
[]meta.NamespacedObjectReference{
{
Namespace: "application",
Name: "backend",
Expand Down Expand Up @@ -186,34 +204,40 @@ func TestDependencySort(t *testing.T) {

func TestDependencySort_DeadEnd(t *testing.T) {
d := []Dependent{
MockDependent{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "backend",
&MockDependent{
Node: corev1.Node{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
Name: "backend",
},
},
DependsOn: []CrossNamespaceDependencyReference{
DependsOn: []meta.NamespacedObjectReference{
{
Namespace: "default",
Name: "common",
},
},
},
MockDependent{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "frontend",
&MockDependent{
Node: corev1.Node{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
Name: "frontend",
},
},
DependsOn: []CrossNamespaceDependencyReference{
DependsOn: []meta.NamespacedObjectReference{
{
Namespace: "default",
Name: "infra",
},
},
},
MockDependent{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "common",
&MockDependent{
Node: corev1.Node{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
Name: "common",
},
},
},
}
Expand Down

0 comments on commit bac7da8

Please sign in to comment.