Skip to content

Commit 68eee01

Browse files
authored
fix(kubernetes): deterministic sorting (#259)
* Implement sorting by NS and test object sorting. - the original sort code in Reconcile() didn't take into account the case where two objects of the same kind and name might reside in different namespace, allowing the sort being non-deterministic at times. - testing of the sort code is now implemented. There might be some refactor pending, since `kubernetes_test.go` also contains code testing `Reconcile()`, but this is outside the scope of this PR. Signed-off-by: Milan Plzik <[email protected]> * Implement review comments. - Refactored `mkobj` to look nicer. - Added a testcase that tests prioritizing Kind -> Namespace -> Name, in that order. Signed-off-by: Milan Plzik <[email protected]>
1 parent 178f4be commit 68eee01

File tree

2 files changed

+136
-0
lines changed

2 files changed

+136
-0
lines changed

pkg/kubernetes/reconcile.go

+5
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ func Reconcile(raw map[string]interface{}, spec v1alpha1.Spec, targets []*regexp
113113
return out[i].Kind() < out[j].Kind()
114114
}
115115

116+
// If namespaces differ, sort by the namespace.
117+
if out[i].Metadata().Namespace() != out[j].Metadata().Namespace() {
118+
return out[i].Metadata().Namespace() < out[j].Metadata().Namespace()
119+
}
120+
116121
// Otherwise, order the objects by name.
117122
return out[i].Metadata().Name() < out[j].Metadata().Name()
118123
})

pkg/kubernetes/reconcile_test.go

+131
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package kubernetes
22

33
import (
4+
"regexp"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
78
"github.com/stretchr/testify/require"
9+
10+
"github.com/grafana/tanka/pkg/kubernetes/manifest"
11+
"github.com/grafana/tanka/pkg/spec/v1alpha1"
812
)
913

1014
func TestExtract(t *testing.T) {
@@ -54,3 +58,130 @@ func TestExtract(t *testing.T) {
5458
})
5559
}
5660
}
61+
62+
func mkobj(kind string, name string, ns string) map[string]interface{} {
63+
ret := map[string]interface{}{
64+
"kind": kind,
65+
"apiVersion": "apiversion",
66+
"metadata": map[string]interface{}{
67+
"name": name,
68+
},
69+
}
70+
if ns != "" {
71+
ret["metadata"].(map[string]interface{})["namespace"] = ns
72+
}
73+
74+
return ret
75+
}
76+
77+
func TestReconcileSorting(t *testing.T) {
78+
tests := []struct {
79+
raw map[string]interface{}
80+
targets []*regexp.Regexp
81+
state manifest.List
82+
err error
83+
}{
84+
{
85+
// sorting by kinds in the `kindOrder` list
86+
raw: map[string]interface{}{
87+
"a": mkobj("Service", "service", "default"),
88+
"b": mkobj("Deployment", "deployment", "default"),
89+
"c": mkobj("CustomResourceDefinition", "crd", ""),
90+
},
91+
state: manifest.List{
92+
mkobj("CustomResourceDefinition", "crd", ""),
93+
mkobj("Service", "service", "default"),
94+
mkobj("Deployment", "deployment", "default"),
95+
},
96+
},
97+
{
98+
// alphabtical sorting by kinds outside `kindOrder` list
99+
raw: map[string]interface{}{
100+
"a": mkobj("B", "b", "default"),
101+
"b": mkobj("C", "c", "default"),
102+
"c": mkobj("A", "a", "default"),
103+
},
104+
state: manifest.List{
105+
mkobj("A", "a", "default"),
106+
mkobj("B", "b", "default"),
107+
mkobj("C", "c", "default"),
108+
},
109+
},
110+
{
111+
// sorting by the namespace if kinds match
112+
raw: map[string]interface{}{
113+
"a": mkobj("Service", "service", "default2"),
114+
"b": mkobj("Service", "service", "default"),
115+
"c": mkobj("Service", "service", "default1"),
116+
},
117+
state: manifest.List{
118+
mkobj("Service", "service", "default"),
119+
mkobj("Service", "service", "default1"),
120+
mkobj("Service", "service", "default2"),
121+
},
122+
},
123+
{
124+
// sorting by the names if both kinds and namespaces match
125+
raw: map[string]interface{}{
126+
"a": mkobj("Service", "service2", "default"),
127+
"b": mkobj("Service", "service", "default"),
128+
"c": mkobj("Service", "service1", "default"),
129+
},
130+
state: manifest.List{
131+
mkobj("Service", "service", "default"),
132+
mkobj("Service", "service1", "default"),
133+
mkobj("Service", "service2", "default"),
134+
},
135+
},
136+
{
137+
// sorting by the names if both kinds match and there are no namespaces
138+
raw: map[string]interface{}{
139+
"a": mkobj("CustomResourceDefinition", "crd2", ""),
140+
"b": mkobj("CustomResourceDefinition", "crd", ""),
141+
"c": mkobj("CustomResourceDefinition", "crd1", ""),
142+
},
143+
state: manifest.List{
144+
mkobj("CustomResourceDefinition", "crd", ""),
145+
mkobj("CustomResourceDefinition", "crd1", ""),
146+
mkobj("CustomResourceDefinition", "crd2", ""),
147+
},
148+
},
149+
{
150+
raw: map[string]interface{}{
151+
"a": mkobj("Deployment", "b", "a"),
152+
"b": mkobj("ConfigMap", "a", "a"),
153+
"c": mkobj("Issuer", "a", "a"),
154+
"d": mkobj("Service", "b", "a"),
155+
"e": mkobj("Service", "a", "a"),
156+
"f": mkobj("Deployment", "a", "a"),
157+
"g": mkobj("ConfigMap", "a", "b"),
158+
"h": mkobj("Issuer", "b", "a"),
159+
"i": mkobj("Service", "b", "b"),
160+
"j": mkobj("Deployment", "a", "b"),
161+
"k": mkobj("ConfigMap", "b", "a"),
162+
"l": mkobj("Issuer", "a", "b"),
163+
},
164+
state: manifest.List{
165+
mkobj("ConfigMap", "a", "a"),
166+
mkobj("ConfigMap", "b", "a"),
167+
mkobj("ConfigMap", "a", "b"),
168+
mkobj("Service", "a", "a"),
169+
mkobj("Service", "b", "a"),
170+
mkobj("Service", "b", "b"),
171+
mkobj("Deployment", "a", "a"),
172+
mkobj("Deployment", "b", "a"),
173+
mkobj("Deployment", "a", "b"),
174+
mkobj("Issuer", "a", "a"),
175+
mkobj("Issuer", "b", "a"),
176+
mkobj("Issuer", "a", "b"),
177+
},
178+
},
179+
}
180+
181+
for _, test := range tests {
182+
res, err := Reconcile(test.raw, v1alpha1.New().Spec, test.targets)
183+
184+
require.NoError(t, err)
185+
require.Equal(t, test.state, res)
186+
}
187+
}

0 commit comments

Comments
 (0)