Skip to content

Commit 1969089

Browse files
committed
Add graph
Signed-off-by: Nelo-T. Wallus <[email protected]> Signed-off-by: Nelo-T. Wallus <[email protected]>
1 parent b4ce031 commit 1969089

File tree

2 files changed

+241
-0
lines changed

2 files changed

+241
-0
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
Copyright 2025 The KCP Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package garbagecollector
18+
19+
import (
20+
"slices"
21+
22+
"k8s.io/utils/ptr"
23+
24+
"github.com/kcp-dev/logicalcluster/v3"
25+
26+
"github.com/kcp-dev/kcp/pkg/reconciler/garbagecollector/syncmap"
27+
)
28+
29+
type Graph struct {
30+
// global is a map of objects to its owned objects across all clusters.
31+
//
32+
// The value is a pointer to a slice because the value must be
33+
// comparable for the syncmap to be able to do atomic operations on
34+
// it. That also means that when modifying the slice, a new slice
35+
// must be created.
36+
global *syncmap.SyncMap[ID, *[]ObjectReference]
37+
}
38+
39+
func NewGraph() *Graph {
40+
g := &Graph{}
41+
g.global = syncmap.NewSyncMap[ID, *[]ObjectReference]()
42+
return g
43+
}
44+
45+
// ClusterOwned returns all object references owned by objects in the given cluster.
46+
func (g *Graph) ClusterOwned(clusterName logicalcluster.Name) []ObjectReference {
47+
var allDependents []ObjectReference
48+
g.global.Range(func(id ID, deps *[]ObjectReference) bool {
49+
if id.ClusterName == clusterName {
50+
allDependents = append(allDependents, *deps...)
51+
}
52+
return true
53+
})
54+
return allDependents
55+
}
56+
57+
// RemoveCluster removes the given cluster from the graph.
58+
// It returns true if the cluster was removed or not present.
59+
// If returned false the cluster had objects with owned objects, and the owned objects are returned.
60+
func (g *Graph) RemoveCluster(clusterName logicalcluster.Name) (bool, []ObjectReference) {
61+
owned := g.ClusterOwned(clusterName)
62+
if len(owned) > 0 {
63+
return false, owned
64+
}
65+
// TODO this function might not be necessary.
66+
return true, nil
67+
}
68+
69+
// Add adds the given object and updates its owners in the graph.
70+
func (g *Graph) Add(obj ObjectReference, oldOwners, newOwners []ObjectReference) {
71+
// Store the object.
72+
g.global.StoreIfAbsent(obj.ID(), ptr.To([]ObjectReference{}))
73+
74+
// TODO diff oldOwners and newOwners to avoid unnecessary
75+
// modifications.
76+
77+
for _, owner := range oldOwners {
78+
g.global.Modify(owner.ID(), func(deps *[]ObjectReference, exists bool) *[]ObjectReference {
79+
if !exists || deps == nil {
80+
deps = ptr.To([]ObjectReference{})
81+
}
82+
newDeps := slices.DeleteFunc(*deps, func(dep ObjectReference) bool {
83+
return dep.Equals(obj)
84+
})
85+
return &newDeps
86+
})
87+
}
88+
89+
for _, owner := range newOwners {
90+
g.global.Modify(owner.ID(), func(deps *[]ObjectReference, exists bool) *[]ObjectReference {
91+
return ptr.To(
92+
append(
93+
ptr.Deref(deps, []ObjectReference{}),
94+
obj,
95+
),
96+
)
97+
})
98+
}
99+
}
100+
101+
func (g *Graph) Owned(or ObjectReference) []ObjectReference {
102+
ownedObjs, _ := g.global.Load(or.ID())
103+
if ownedObjs == nil {
104+
return []ObjectReference{}
105+
}
106+
return *ownedObjs
107+
}
108+
109+
// Remove removes the given object from the graph.
110+
// It returns true if the object was removed or is not present.
111+
// If the object owns objects it returns false and the owned objects.
112+
func (g *Graph) Remove(or ObjectReference) (bool, []ObjectReference) {
113+
ownedObjs, exists := g.global.Load(or.ID())
114+
// TODO(ntnn): This could be an error. Depends on if "empty" objects
115+
// (so nodes not owning anything) are expected to be in the graph or
116+
// not.
117+
if !exists {
118+
return true, nil
119+
}
120+
if len(*ownedObjs) > 0 {
121+
return false, *ownedObjs
122+
}
123+
g.global.Delete(or.ID())
124+
g.global.Range(func(ownerID ID, ownedObjs *[]ObjectReference) bool {
125+
g.global.Modify(ownerID, func(ownedObjs *[]ObjectReference, _ bool) *[]ObjectReference {
126+
newOwnedObjs := slices.DeleteFunc(*ownedObjs, func(ownedObj ObjectReference) bool {
127+
return ownedObj.ID() == or.ID()
128+
})
129+
if slices.Equal(*ownedObjs, newOwnedObjs) {
130+
return ownedObjs
131+
}
132+
return &newOwnedObjs
133+
})
134+
return true
135+
})
136+
return true, nil
137+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
Copyright 2025 The KCP Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package garbagecollector
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
26+
"github.com/kcp-dev/logicalcluster/v3"
27+
)
28+
29+
var (
30+
testNodeA = ObjectReference{
31+
OwnerReference: metav1.OwnerReference{
32+
APIVersion: "apps/v1",
33+
Kind: "Deployment",
34+
Name: "test-deployment",
35+
UID: "uid-deployment",
36+
},
37+
Namespace: "default",
38+
ClusterName: logicalcluster.Name("cluster-a"),
39+
}
40+
testNodeB = ObjectReference{
41+
OwnerReference: metav1.OwnerReference{
42+
APIVersion: "v1",
43+
Kind: "Pod",
44+
Name: "test-pod",
45+
UID: "uid-pod",
46+
},
47+
Namespace: "default",
48+
ClusterName: logicalcluster.Name("cluster-a"),
49+
}
50+
testNodeC = ObjectReference{
51+
OwnerReference: metav1.OwnerReference{
52+
APIVersion: "v1",
53+
Kind: "Pod",
54+
Name: "test-pod2",
55+
UID: "uid-pod2",
56+
},
57+
Namespace: "default",
58+
ClusterName: logicalcluster.Name("cluster-a"),
59+
}
60+
)
61+
62+
func TestGraph_Nodes(t *testing.T) {
63+
t.Parallel()
64+
65+
graph := NewGraph()
66+
67+
t.Log("Adding Deployment node to graph")
68+
graph.Add(testNodeA, nil, nil)
69+
70+
t.Log("Add Pod nodes with Deployment as owner")
71+
graph.Add(testNodeB, nil, []ObjectReference{testNodeA})
72+
graph.Add(testNodeC, nil, []ObjectReference{testNodeA})
73+
74+
t.Log("Verify that Deployment owns the two Pods")
75+
owned := graph.Owned(testNodeA)
76+
assert.Equal(t, 2, len(owned), "expected Deployment to own 2 Pods")
77+
assert.Contains(t, owned, testNodeB, "expected Deployment to own testNodeB")
78+
assert.Contains(t, owned, testNodeC, "expected Deployment to own testNodeC")
79+
80+
t.Log("Remove one Pod and verify ownership")
81+
removed, owned := graph.Remove(testNodeB)
82+
assert.True(t, removed, "expected Pod removal to succeed")
83+
assert.Equal(t, 0, len(owned), "expected Pod to own 0 objects upon removal")
84+
85+
owned = graph.Owned(testNodeA)
86+
assert.Equal(t, 1, len(owned), "expected Deployment to own 1 Pod after removal")
87+
assert.Contains(t, owned, testNodeC, "expected Deployment to still own testNodeC")
88+
89+
t.Log("Try removing Deployment with owned Pod")
90+
success, owned := graph.Remove(testNodeA)
91+
assert.False(t, success, "expected Deployment removal to fail due to owned Pods")
92+
assert.Equal(t, 1, len(owned), "expected Deployment to own 1 Pod during removal attempt")
93+
94+
t.Log("Remove remaining Pod and verify Deployment ownership")
95+
graph.Remove(testNodeC)
96+
97+
owned = graph.Owned(testNodeA)
98+
assert.Equal(t, 0, len(owned), "expected Deployment to own 0 Pods after all removals")
99+
100+
t.Log("Now remove Deployment successfully")
101+
success, owned = graph.Remove(testNodeA)
102+
assert.True(t, success, "expected Deployment removal to succeed with no owned Pods")
103+
assert.Equal(t, 0, len(owned), "expected no owned Pods during Deployment removal")
104+
}

0 commit comments

Comments
 (0)