Skip to content

Commit

Permalink
Adds object dependency sorting and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
seans3 committed Jun 26, 2021
1 parent 5b31557 commit bf6bc5f
Show file tree
Hide file tree
Showing 4 changed files with 916 additions and 81 deletions.
110 changes: 29 additions & 81 deletions pkg/apply/applier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,49 +69,6 @@ spec:
}
)

func Unstructured(t *testing.T, manifest string, mutators ...mutator) *unstructured.Unstructured {
u := &unstructured.Unstructured{}
err := runtime.DecodeInto(codec, []byte(manifest), u)
if !assert.NoError(t, err) {
t.FailNow()
}
for _, m := range mutators {
m.Mutate(u)
}
return u
}

type mutator interface {
Mutate(u *unstructured.Unstructured)
}

func addOwningInv(t *testing.T, inv string) mutator {
return owningInvMutator{
t: t,
inv: inv,
}
}

type owningInvMutator struct {
t *testing.T
inv string
}

func (a owningInvMutator) Mutate(u *unstructured.Unstructured) {
annos, found, err := unstructured.NestedStringMap(u.Object, "metadata", "annotations")
if !assert.NoError(a.t, err) {
a.t.FailNow()
}
if !found {
annos = make(map[string]string)
}
annos["config.k8s.io/owning-inventory"] = a.inv
err = unstructured.SetNestedStringMap(u.Object, annos, "metadata", "annotations")
if !assert.NoError(a.t, err) {
a.t.FailNow()
}
}

type resourceInfo struct {
resource *unstructured.Unstructured
exists bool
Expand Down Expand Up @@ -157,7 +114,7 @@ func TestApplier(t *testing.T) {
"initial apply without status or prune": {
namespace: "default",
resources: []*unstructured.Unstructured{
Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["deployment"]),
},
invInfo: inventoryInfo{
name: "abc-123",
Expand Down Expand Up @@ -186,8 +143,8 @@ func TestApplier(t *testing.T) {
"first apply multiple resources with status and prune": {
namespace: "default",
resources: []*unstructured.Unstructured{
Unstructured(t, resources["deployment"]),
Unstructured(t, resources["secret"]),
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["secret"]),
},
invInfo: inventoryInfo{
name: "inv-123",
Expand All @@ -202,25 +159,25 @@ func TestApplier(t *testing.T) {
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: toIdentifier(t, resources["deployment"]),
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
Resource: Unstructured(t, resources["deployment"]),
Resource: testutil.Unstructured(t, resources["deployment"]),
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: toIdentifier(t, resources["deployment"]),
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.CurrentStatus,
Resource: Unstructured(t, resources["deployment"]),
Resource: testutil.Unstructured(t, resources["deployment"]),
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: toIdentifier(t, resources["secret"]),
Identifier: testutil.ToIdentifier(t, resources["secret"]),
Status: status.CurrentStatus,
Resource: Unstructured(t, resources["secret"]),
Resource: testutil.Unstructured(t, resources["secret"]),
},
},
},
Expand Down Expand Up @@ -266,21 +223,21 @@ func TestApplier(t *testing.T) {
"apply multiple existing resources with status and prune": {
namespace: "default",
resources: []*unstructured.Unstructured{
Unstructured(t, resources["deployment"]),
Unstructured(t, resources["secret"]),
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["secret"]),
},
invInfo: inventoryInfo{
name: "inv-123",
namespace: "default",
id: "test",
list: []object.ObjMetadata{
object.UnstructuredToObjMeta(
Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["deployment"]),
),
},
},
clusterObjs: []*unstructured.Unstructured{
Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["deployment"]),
},
reconcileTimeout: time.Minute,
prune: true,
Expand All @@ -289,17 +246,17 @@ func TestApplier(t *testing.T) {
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: toIdentifier(t, resources["deployment"]),
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.CurrentStatus,
Resource: Unstructured(t, resources["deployment"]),
Resource: testutil.Unstructured(t, resources["deployment"]),
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: toIdentifier(t, resources["secret"]),
Identifier: testutil.ToIdentifier(t, resources["secret"]),
Status: status.CurrentStatus,
Resource: Unstructured(t, resources["secret"]),
Resource: testutil.Unstructured(t, resources["secret"]),
},
},
},
Expand Down Expand Up @@ -348,16 +305,16 @@ func TestApplier(t *testing.T) {
id: "test",
list: []object.ObjMetadata{
object.UnstructuredToObjMeta(
Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["deployment"]),
),
object.UnstructuredToObjMeta(
Unstructured(t, resources["secret"]),
testutil.Unstructured(t, resources["secret"]),
),
},
},
clusterObjs: []*unstructured.Unstructured{
Unstructured(t, resources["deployment"], addOwningInv(t, "test")),
Unstructured(t, resources["secret"], addOwningInv(t, "test")),
testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")),
testutil.Unstructured(t, resources["secret"], testutil.AddOwningInv(t, "test")),
},
reconcileTimeout: time.Minute,
prune: true,
Expand Down Expand Up @@ -402,15 +359,15 @@ func TestApplier(t *testing.T) {
"apply resource with existing object belonging to different inventory": {
namespace: "default",
resources: []*unstructured.Unstructured{
Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["deployment"]),
},
invInfo: inventoryInfo{
name: "abc-123",
namespace: "default",
id: "test",
},
clusterObjs: []*unstructured.Unstructured{
Unstructured(t, resources["deployment"], addOwningInv(t, "unmatched")),
testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "unmatched")),
},
reconcileTimeout: time.Minute,
prune: true,
Expand All @@ -419,14 +376,14 @@ func TestApplier(t *testing.T) {
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: toIdentifier(t, resources["deployment"]),
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: toIdentifier(t, resources["deployment"]),
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.CurrentStatus,
},
},
Expand Down Expand Up @@ -470,12 +427,12 @@ func TestApplier(t *testing.T) {
id: "test",
list: []object.ObjMetadata{
object.UnstructuredToObjMeta(
Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["deployment"]),
),
},
},
clusterObjs: []*unstructured.Unstructured{
Unstructured(t, resources["deployment"], addOwningInv(t, "unmatched")),
testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "unmatched")),
},
reconcileTimeout: 0,
prune: true,
Expand Down Expand Up @@ -513,12 +470,12 @@ func TestApplier(t *testing.T) {
id: "test",
list: []object.ObjMetadata{
object.UnstructuredToObjMeta(
Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["deployment"]),
),
},
},
clusterObjs: []*unstructured.Unstructured{
Unstructured(t, resources["deployment"], addOwningInv(t, "test")),
testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")),
},
reconcileTimeout: 0,
prune: true,
Expand Down Expand Up @@ -832,15 +789,6 @@ func newFakeRESTClient(t *testing.T, handlers []handler) *fake.RESTClient {
}
}

func toIdentifier(t *testing.T, manifest string) object.ObjMetadata {
obj := Unstructured(t, manifest)
return object.ObjMetadata{
GroupKind: obj.GetObjectKind().GroupVersionKind().GroupKind(),
Name: obj.GetName(),
Namespace: "default",
}
}

// The handler interface allows different testcases to provide
// special handling of requests. It also allows a single handler
// to keep state between a set of related requests instead of keeping
Expand Down
136 changes: 136 additions & 0 deletions pkg/object/graph/depends.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

// This package provides a object sorting functionality
// based on the explicit "depends-on" annotation, and
// implicit object dependencies like namespaces and CRD's.
package graph

import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/klog/v2"
"sigs.k8s.io/cli-utils/pkg/object"
)

// SortObjs returns a slice of the sets of objects to apply (in order).
// Each of the objects in an apply set is applied together. The order of
// the returned applied sets is a topological ordering of the sets to apply.
// Returns an single empty apply set if there are no objects to apply.
func SortObjs(objs []*unstructured.Unstructured) [][]*unstructured.Unstructured {
if len(objs) == 0 {
return [][]*unstructured.Unstructured{}
}
// Create the graph, and build a map of object metadata to the object (Unstructured).
g := New()
objToUnstructured := map[object.ObjMetadata]*unstructured.Unstructured{}
for _, obj := range objs {
id := object.UnstructuredToObjMeta(obj)
objToUnstructured[id] = obj
}
// Add object vertices and dependency edges to graph.
addExplicitEdges(g, objs)
addNamespaceEdges(g, objs)
addCRDEdges(g, objs)
// Run topological sort on the graph.
objSets := [][]*unstructured.Unstructured{}
sortedObjSets, err := g.Sort()
if err != nil {
return objSets
}
// Map the object metadata back to the sorted sets of unstructured objects.
for _, objSet := range sortedObjSets {
currentSet := []*unstructured.Unstructured{}
for _, id := range objSet {
var found bool
var obj *unstructured.Unstructured
if obj, found = objToUnstructured[id]; found {
currentSet = append(currentSet, obj)
}
}
objSets = append(objSets, currentSet)
}
return objSets
}

// ReverseSortObjs is the same as SortObjs but using reverse ordering.
func ReverseSortObjs(objs []*unstructured.Unstructured) [][]*unstructured.Unstructured {
// Sorted objects using normal ordering.
s := SortObjs(objs)
// Reverse the ordering of the object sets using swaps.
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
return s
}

// addExplicitEdges updates the graph with edges from objects
// with an explicit "depends-on" annotation.
func addExplicitEdges(g *Graph, objs []*unstructured.Unstructured) {
for _, obj := range objs {
id := object.UnstructuredToObjMeta(obj)
klog.V(3).Infof("adding vertex: %s", id)
g.AddVertex(id)
deps, err := object.DependsOnObjs(obj)
if err == nil {
for _, dep := range deps {
klog.V(3).Infof("adding edge from: %s, to: %s", id, dep)
g.AddEdge(id, dep)
}
}
}
}

// addCRDEdges adds edges to the dependency graph from custom
// resources to their definitions to ensure the CRD's exist
// before applying the custom resources created with the definition.
func addCRDEdges(g *Graph, objs []*unstructured.Unstructured) {
crds := map[string]object.ObjMetadata{}
// First create a map of all the CRD's.
for _, u := range objs {
if object.IsCRD(u) {
groupKind, found := object.GetCRDGroupKind(u)
if found {
obj := object.UnstructuredToObjMeta(u)
crds[groupKind.String()] = obj
}
}
}
// Iterate through all resources to see if we are applying any
// custom resources defined by previously recorded CRD's.
for _, u := range objs {
gvk := u.GroupVersionKind()
groupKind := gvk.GroupKind()
if to, found := crds[groupKind.String()]; found {
from := object.UnstructuredToObjMeta(u)
klog.V(3).Infof("adding edge from: custom resource %s, to CRD: %s", from, to)
g.AddEdge(from, to)
}
}
}

// addNamespaceEdges adds edges to the dependency graph from namespaced
// objects to the namespace objects. Ensures the namespaces exist
// before the resources in those namespaces are applied.
func addNamespaceEdges(g *Graph, objs []*unstructured.Unstructured) {
namespaces := map[string]object.ObjMetadata{}
// First create a map of all the namespaces objects live in.
for _, obj := range objs {
if object.IsKindNamespace(obj) {
id := object.UnstructuredToObjMeta(obj)
namespace := obj.GetName()
namespaces[namespace] = id
}
}
// Next, if the namespace of a namespaced object is being applied,
// then create an edge from the namespaced object to its namespace.
for _, obj := range objs {
if object.IsNamespaced(obj) {
objNamespace := obj.GetNamespace()
if namespace, found := namespaces[objNamespace]; found {
id := object.UnstructuredToObjMeta(obj)
klog.V(3).Infof("adding edge from: %s to namespace: %s", id, namespace)
g.AddEdge(id, namespace)
}
}
}
}
Loading

0 comments on commit bf6bc5f

Please sign in to comment.