Skip to content

Commit bd4bb27

Browse files
authored
Generic sync/suspend/inventory listing (#4096)
* Handle unstructured reconcilation of "ks-like" resources - Expose useInventory hook too * Convert the entire inventory from unstructured * Modernise our multi-error handling, golang can do it now * Expose `useListEvents` via npm module
1 parent ef96644 commit bd4bb27

File tree

9 files changed

+542
-166
lines changed

9 files changed

+542
-166
lines changed

core/fluxsync/adapters.go

Lines changed: 92 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
package fluxsync
22

33
import (
4-
"errors"
5-
64
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
75
imgautomationv1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
86
reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
97
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
108
"github.com/fluxcd/pkg/apis/meta"
119
sourcev1 "github.com/fluxcd/source-controller/api/v1"
1210
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13+
"k8s.io/apimachinery/pkg/runtime"
1314
"k8s.io/apimachinery/pkg/runtime/schema"
1415
"sigs.k8s.io/controller-runtime/pkg/client"
1516
)
@@ -23,7 +24,7 @@ type Reconcilable interface {
2324
GetLastHandledReconcileRequest() string
2425
AsClientObject() client.Object
2526
GroupVersionKind() schema.GroupVersionKind
26-
SetSuspended(suspend bool)
27+
SetSuspended(suspend bool) error
2728
DeepCopyClientObject() client.Object
2829
}
2930

@@ -42,30 +43,6 @@ type Automation interface {
4243
SourceRef() SourceRef
4344
}
4445

45-
func NewReconcileable(obj client.Object) Reconcilable {
46-
switch o := obj.(type) {
47-
case *kustomizev1.Kustomization:
48-
return KustomizationAdapter{Kustomization: o}
49-
case *helmv2.HelmRelease:
50-
return HelmReleaseAdapter{HelmRelease: o}
51-
case *sourcev1.GitRepository:
52-
return GitRepositoryAdapter{GitRepository: o}
53-
case *sourcev1b2.HelmRepository:
54-
return HelmRepositoryAdapter{HelmRepository: o}
55-
case *sourcev1b2.Bucket:
56-
return BucketAdapter{Bucket: o}
57-
case *sourcev1b2.HelmChart:
58-
return HelmChartAdapter{HelmChart: o}
59-
case *sourcev1b2.OCIRepository:
60-
return OCIRepositoryAdapter{OCIRepository: o}
61-
case *reflectorv1.ImageRepository:
62-
return ImageRepositoryAdapter{ImageRepository: o}
63-
case *imgautomationv1.ImageUpdateAutomation:
64-
return ImageUpdateAutomationAdapter{ImageUpdateAutomation: o}
65-
}
66-
return nil
67-
}
68-
6946
type GitRepositoryAdapter struct {
7047
*sourcev1.GitRepository
7148
}
@@ -82,8 +59,9 @@ func (obj GitRepositoryAdapter) GroupVersionKind() schema.GroupVersionKind {
8259
return sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind)
8360
}
8461

85-
func (obj GitRepositoryAdapter) SetSuspended(suspend bool) {
62+
func (obj GitRepositoryAdapter) SetSuspended(suspend bool) error {
8663
obj.Spec.Suspend = suspend
64+
return nil
8765
}
8866

8967
func (obj GitRepositoryAdapter) DeepCopyClientObject() client.Object {
@@ -106,8 +84,9 @@ func (obj BucketAdapter) GroupVersionKind() schema.GroupVersionKind {
10684
return sourcev1b2.GroupVersion.WithKind(sourcev1b2.BucketKind)
10785
}
10886

109-
func (obj BucketAdapter) SetSuspended(suspend bool) {
87+
func (obj BucketAdapter) SetSuspended(suspend bool) error {
11088
obj.Spec.Suspend = suspend
89+
return nil
11190
}
11291

11392
func (obj BucketAdapter) DeepCopyClientObject() client.Object {
@@ -130,8 +109,9 @@ func (obj HelmChartAdapter) GroupVersionKind() schema.GroupVersionKind {
130109
return sourcev1b2.GroupVersion.WithKind(sourcev1b2.HelmChartKind)
131110
}
132111

133-
func (obj HelmChartAdapter) SetSuspended(suspend bool) {
112+
func (obj HelmChartAdapter) SetSuspended(suspend bool) error {
134113
obj.Spec.Suspend = suspend
114+
return nil
135115
}
136116

137117
func (obj HelmChartAdapter) DeepCopyClientObject() client.Object {
@@ -154,8 +134,9 @@ func (obj HelmRepositoryAdapter) GroupVersionKind() schema.GroupVersionKind {
154134
return sourcev1b2.GroupVersion.WithKind(sourcev1b2.HelmRepositoryKind)
155135
}
156136

157-
func (obj HelmRepositoryAdapter) SetSuspended(suspend bool) {
137+
func (obj HelmRepositoryAdapter) SetSuspended(suspend bool) error {
158138
obj.Spec.Suspend = suspend
139+
return nil
159140
}
160141

161142
func (obj HelmRepositoryAdapter) DeepCopyClientObject() client.Object {
@@ -178,8 +159,9 @@ func (obj OCIRepositoryAdapter) GroupVersionKind() schema.GroupVersionKind {
178159
return sourcev1b2.GroupVersion.WithKind(sourcev1b2.OCIRepositoryKind)
179160
}
180161

181-
func (obj OCIRepositoryAdapter) SetSuspended(suspend bool) {
162+
func (obj OCIRepositoryAdapter) SetSuspended(suspend bool) error {
182163
obj.Spec.Suspend = suspend
164+
return nil
183165
}
184166

185167
func (obj OCIRepositoryAdapter) DeepCopyClientObject() client.Object {
@@ -213,8 +195,9 @@ func (obj HelmReleaseAdapter) GroupVersionKind() schema.GroupVersionKind {
213195
return helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)
214196
}
215197

216-
func (obj HelmReleaseAdapter) SetSuspended(suspend bool) {
198+
func (obj HelmReleaseAdapter) SetSuspended(suspend bool) error {
217199
obj.Spec.Suspend = suspend
200+
return nil
218201
}
219202

220203
func (obj HelmReleaseAdapter) DeepCopyClientObject() client.Object {
@@ -246,8 +229,9 @@ func (obj KustomizationAdapter) GroupVersionKind() schema.GroupVersionKind {
246229
return kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)
247230
}
248231

249-
func (obj KustomizationAdapter) SetSuspended(suspend bool) {
232+
func (obj KustomizationAdapter) SetSuspended(suspend bool) error {
250233
obj.Spec.Suspend = suspend
234+
return nil
251235
}
252236

253237
func (obj KustomizationAdapter) DeepCopyClientObject() client.Object {
@@ -270,8 +254,9 @@ func (obj ImageRepositoryAdapter) GroupVersionKind() schema.GroupVersionKind {
270254
return reflectorv1.GroupVersion.WithKind(reflectorv1.ImageRepositoryKind)
271255
}
272256

273-
func (obj ImageRepositoryAdapter) SetSuspended(suspend bool) {
257+
func (obj ImageRepositoryAdapter) SetSuspended(suspend bool) error {
274258
obj.Spec.Suspend = suspend
259+
return nil
275260
}
276261

277262
func (obj ImageRepositoryAdapter) DeepCopyClientObject() client.Object {
@@ -294,14 +279,61 @@ func (obj ImageUpdateAutomationAdapter) GroupVersionKind() schema.GroupVersionKi
294279
return imgautomationv1.GroupVersion.WithKind(imgautomationv1.ImageUpdateAutomationKind)
295280
}
296281

297-
func (obj ImageUpdateAutomationAdapter) SetSuspended(suspend bool) {
282+
func (obj ImageUpdateAutomationAdapter) SetSuspended(suspend bool) error {
298283
obj.Spec.Suspend = suspend
284+
return nil
299285
}
300286

301287
func (obj ImageUpdateAutomationAdapter) DeepCopyClientObject() client.Object {
302288
return obj.DeepCopy()
303289
}
304290

291+
// UnstructuredAdapter implements the Reconcilable interface for unstructured resources.
292+
// The underlying resource gvk should have the standard flux object sync/suspend fields
293+
type UnstructuredAdapter struct {
294+
*unstructured.Unstructured
295+
}
296+
297+
func (obj UnstructuredAdapter) GetLastHandledReconcileRequest() string {
298+
if val, found, _ := unstructured.NestedString(obj.Object, "status", "lastHandledReconcileAt"); found {
299+
return val
300+
}
301+
return ""
302+
}
303+
304+
func (obj UnstructuredAdapter) GetConditions() []metav1.Condition {
305+
conditionsSlice, found, err := unstructured.NestedSlice(obj.Object, "status", "conditions")
306+
if !found || err != nil {
307+
return nil
308+
}
309+
310+
var conditions []metav1.Condition
311+
for _, c := range conditionsSlice {
312+
var condition metav1.Condition
313+
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(c.(map[string]interface{}), &condition); err != nil {
314+
continue
315+
}
316+
conditions = append(conditions, condition)
317+
}
318+
319+
return conditions
320+
}
321+
322+
func (obj UnstructuredAdapter) AsClientObject() client.Object {
323+
// Important for the controller-runtime type reflection to work
324+
// We can't return just `obj` here otherwise we get a
325+
// panic: reflect: call of reflect.Value.Elem on struct Value
326+
return obj.Unstructured
327+
}
328+
329+
func (obj UnstructuredAdapter) SetSuspended(suspend bool) error {
330+
return unstructured.SetNestedField(obj.Object, suspend, "spec", "suspend")
331+
}
332+
333+
func (obj UnstructuredAdapter) DeepCopyClientObject() client.Object {
334+
return obj.DeepCopy()
335+
}
336+
305337
type sRef struct {
306338
apiVersion string
307339
name string
@@ -325,35 +357,39 @@ func (s sRef) Kind() string {
325357
return s.kind
326358
}
327359

328-
func ToReconcileable(kind string) (client.ObjectList, Reconcilable, error) {
329-
switch kind {
360+
// ToReconcileable takes a GVK and returns a "Reconcilable" for it.
361+
// The reconcilable can be passed to a controller-runtime client to fetch it
362+
// from the cluster. Once fetched we can query it for the last sync time, whether
363+
// its suspended etc, using the Reconcilable interface.
364+
//
365+
// The generic unstructured case handles "flux like" objects that we don't explicitly
366+
// know about, but which follow the same patterns for suspend/sync as a stadard flux object.
367+
// E.g. `spec.suspend` and `status.lastHandledReconcileRequest` etc.
368+
func ToReconcileable(gvk schema.GroupVersionKind) Reconcilable {
369+
switch gvk.Kind {
330370
case kustomizev1.KustomizationKind:
331-
return &kustomizev1.KustomizationList{}, NewReconcileable(&kustomizev1.Kustomization{}), nil
332-
371+
return KustomizationAdapter{Kustomization: &kustomizev1.Kustomization{}}
333372
case helmv2.HelmReleaseKind:
334-
return &helmv2.HelmReleaseList{}, NewReconcileable(&helmv2.HelmRelease{}), nil
335-
373+
return HelmReleaseAdapter{HelmRelease: &helmv2.HelmRelease{}}
374+
// TODO: remove all these and let them fall through to the Unstructured case?
336375
case sourcev1.GitRepositoryKind:
337-
return &sourcev1.GitRepositoryList{}, NewReconcileable(&sourcev1.GitRepository{}), nil
338-
376+
return GitRepositoryAdapter{GitRepository: &sourcev1.GitRepository{}}
339377
case sourcev1b2.BucketKind:
340-
return &sourcev1b2.BucketList{}, NewReconcileable(&sourcev1b2.Bucket{}), nil
341-
378+
return BucketAdapter{Bucket: &sourcev1b2.Bucket{}}
342379
case sourcev1b2.HelmRepositoryKind:
343-
return &sourcev1b2.HelmRepositoryList{}, NewReconcileable(&sourcev1b2.HelmRepository{}), nil
344-
380+
return HelmRepositoryAdapter{HelmRepository: &sourcev1b2.HelmRepository{}}
345381
case sourcev1b2.HelmChartKind:
346-
return &sourcev1b2.HelmChartList{}, NewReconcileable(&sourcev1b2.HelmChart{}), nil
347-
382+
return HelmChartAdapter{HelmChart: &sourcev1b2.HelmChart{}}
348383
case sourcev1b2.OCIRepositoryKind:
349-
return &sourcev1b2.OCIRepositoryList{}, NewReconcileable(&sourcev1b2.OCIRepository{}), nil
350-
384+
return OCIRepositoryAdapter{OCIRepository: &sourcev1b2.OCIRepository{}}
351385
case reflectorv1.ImageRepositoryKind:
352-
return &reflectorv1.ImageRepositoryList{}, NewReconcileable(&reflectorv1.ImageRepository{}), nil
353-
386+
return ImageRepositoryAdapter{ImageRepository: &reflectorv1.ImageRepository{}}
354387
case imgautomationv1.ImageUpdateAutomationKind:
355-
return &imgautomationv1.ImageUpdateAutomationList{}, NewReconcileable(&imgautomationv1.ImageUpdateAutomation{}), nil
388+
return ImageUpdateAutomationAdapter{ImageUpdateAutomation: &imgautomationv1.ImageUpdateAutomation{}}
356389
}
357390

358-
return nil, nil, errors.New("could not find source type")
391+
// Return the UnstructuredAdapter for flux-like resources
392+
obj := &unstructured.Unstructured{}
393+
obj.SetGroupVersionKind(gvk)
394+
return UnstructuredAdapter{Unstructured: obj}
359395
}

core/fluxsync/adapters_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package fluxsync
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
. "github.com/onsi/gomega"
8+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
10+
"k8s.io/apimachinery/pkg/runtime"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
12+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
13+
)
14+
15+
func TestGetLastHandledReconcileRequest(t *testing.T) {
16+
g := NewGomegaWithT(t)
17+
18+
obj := &UnstructuredAdapter{
19+
Unstructured: &unstructured.Unstructured{
20+
Object: map[string]interface{}{
21+
"status": map[string]interface{}{
22+
"lastHandledReconcileAt": "2023-10-20T10:10:10Z",
23+
},
24+
},
25+
},
26+
}
27+
28+
expected := "2023-10-20T10:10:10Z"
29+
got := obj.GetLastHandledReconcileRequest()
30+
g.Expect(got).To(Equal(expected))
31+
}
32+
33+
func TestGetConditions(t *testing.T) {
34+
g := NewGomegaWithT(t)
35+
36+
condition := v1.Condition{
37+
Type: "Ready",
38+
Status: "True",
39+
}
40+
unstructuredCondition, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(&condition)
41+
42+
obj := &UnstructuredAdapter{
43+
Unstructured: &unstructured.Unstructured{
44+
Object: map[string]interface{}{
45+
"status": map[string]interface{}{
46+
"conditions": []interface{}{unstructuredCondition},
47+
},
48+
},
49+
},
50+
}
51+
52+
conditions := obj.GetConditions()
53+
g.Expect(conditions).To(HaveLen(1))
54+
g.Expect(conditions[0].Type).To(Equal(condition.Type))
55+
g.Expect(conditions[0].Status).To(Equal(condition.Status))
56+
}
57+
58+
func TestSetSuspended(t *testing.T) {
59+
g := NewGomegaWithT(t)
60+
61+
obj := &UnstructuredAdapter{
62+
Unstructured: &unstructured.Unstructured{
63+
Object: make(map[string]interface{}),
64+
},
65+
}
66+
67+
err := obj.SetSuspended(true)
68+
g.Expect(err).NotTo(HaveOccurred())
69+
suspend, _, _ := unstructured.NestedBool(obj.Object, "spec", "suspend")
70+
g.Expect(suspend).To(BeTrue())
71+
}
72+
73+
func TestDeepCopyClientObject(t *testing.T) {
74+
g := NewGomegaWithT(t)
75+
76+
obj := &UnstructuredAdapter{
77+
Unstructured: &unstructured.Unstructured{
78+
Object: map[string]interface{}{"key": "value"},
79+
},
80+
}
81+
82+
objCopy := obj.DeepCopyClientObject().(*unstructured.Unstructured)
83+
g.Expect(objCopy.Object).To(Equal(obj.Object))
84+
g.Expect(objCopy).ToNot(BeIdenticalTo(obj))
85+
}
86+
87+
func TestAsClientObjectCompatibilityWithTestClient(t *testing.T) {
88+
g := NewGomegaWithT(t)
89+
90+
scheme := runtime.NewScheme()
91+
92+
cl := fake.NewClientBuilder().WithScheme(scheme).Build()
93+
94+
obj := &UnstructuredAdapter{
95+
Unstructured: &unstructured.Unstructured{
96+
Object: map[string]interface{}{
97+
"apiVersion": "v1",
98+
"kind": "ConfigMap",
99+
"metadata": map[string]interface{}{
100+
"name": "test-cm",
101+
"namespace": "default",
102+
},
103+
"data": map[string]interface{}{"key": "value"},
104+
},
105+
},
106+
}
107+
108+
err := cl.Create(context.TODO(), obj.AsClientObject())
109+
g.Expect(err).NotTo(HaveOccurred())
110+
111+
retrieved := &UnstructuredAdapter{
112+
Unstructured: &unstructured.Unstructured{
113+
Object: map[string]interface{}{
114+
"apiVersion": "v1",
115+
"kind": "ConfigMap",
116+
},
117+
},
118+
}
119+
err = cl.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "test-cm"}, retrieved.AsClientObject())
120+
g.Expect(err).NotTo(HaveOccurred())
121+
122+
// check the data key
123+
data, _, _ := unstructured.NestedStringMap(retrieved.Object, "data")
124+
g.Expect(data).To(Equal(map[string]string{"key": "value"}))
125+
}

0 commit comments

Comments
 (0)