Skip to content

Commit 9cf8ae0

Browse files
authored
✨ Reconcile HCloudMachines when hcloud token secret changes (backport #2001) (#2150)
1 parent df62b40 commit 9cf8ae0

2 files changed

Lines changed: 208 additions & 0 deletions

File tree

controllers/hcloudmachine_controller.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/go-logr/logr"
2828
"github.com/google/go-cmp/cmp"
2929
"github.com/hetznercloud/hcloud-go/v2/hcloud"
30+
corev1 "k8s.io/api/core/v1"
3031
apierrors "k8s.io/apimachinery/pkg/api/errors"
3132
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3233
"k8s.io/apimachinery/pkg/util/wait"
@@ -337,6 +338,11 @@ func (r *HCloudMachineReconciler) SetupWithManager(ctx context.Context, mgr ctrl
337338
handler.EnqueueRequestsFromMapFunc(clusterToObjectFunc),
338339
builder.WithPredicates(predicates.ClusterPausedTransitionsOrInfrastructureProvisioned(mgr.GetScheme(), log)),
339340
).
341+
Watches(
342+
&corev1.Secret{},
343+
handler.EnqueueRequestsFromMapFunc(r.HetznerSecretToHCloudMachines(ctx)),
344+
builder.WithPredicates(IgnoreInsignificantSecretUpdates(log)),
345+
).
340346
Complete(r)
341347
if err != nil {
342348
return fmt.Errorf("error creating controller: %w", err)
@@ -397,6 +403,66 @@ func (r *HCloudMachineReconciler) HetznerClusterToHCloudMachines(_ context.Conte
397403
}
398404
}
399405

406+
// HetznerSecretToHCloudMachines is a handler.ToRequestsFunc to be used to enqueue requests for reconciliation
407+
// of HCloudMachines when the referenced HetznerSecret changes (e.g. after a token rotation).
408+
func (r *HCloudMachineReconciler) HetznerSecretToHCloudMachines(_ context.Context) handler.MapFunc {
409+
return func(ctx context.Context, o client.Object) []reconcile.Request {
410+
log := log.FromContext(ctx)
411+
412+
secret, ok := o.(*corev1.Secret)
413+
if !ok {
414+
log.Error(fmt.Errorf("expected a Secret but got a %T", o), "failed to get HCloudMachine for Secret")
415+
return nil
416+
}
417+
418+
log = log.WithValues("objectMapper", "hetznerSecretToHCloudMachine", "namespace", secret.Namespace, "secret", secret.Name)
419+
420+
hetznerClusterList := &infrav1.HetznerClusterList{}
421+
if err := r.List(ctx, hetznerClusterList, client.InNamespace(secret.Namespace)); err != nil {
422+
log.Error(err, "failed to list HetznerClusters, skipping mapping")
423+
return nil
424+
}
425+
426+
result := []reconcile.Request{}
427+
toRequests := r.HetznerClusterToHCloudMachines(ctx)
428+
for i := range hetznerClusterList.Items {
429+
hc := &hetznerClusterList.Items[i]
430+
if hc.Spec.HetznerSecret.Name != secret.Name {
431+
continue
432+
}
433+
result = append(result, toRequests(ctx, hc)...)
434+
}
435+
return result
436+
}
437+
}
438+
439+
// IgnoreInsignificantSecretUpdates is a predicate that only fires when the Secret's Data
440+
// actually changes, so HCloudMachines do not reconcile for ManagedFields or metadata-only
441+
// Secret updates.
442+
func IgnoreInsignificantSecretUpdates(logger logr.Logger) predicate.Funcs {
443+
return predicate.Funcs{
444+
UpdateFunc: func(e event.UpdateEvent) bool {
445+
oldSecret, ok := e.ObjectOld.(*corev1.Secret)
446+
if !ok {
447+
return false
448+
}
449+
newSecret, ok := e.ObjectNew.(*corev1.Secret)
450+
if !ok {
451+
return false
452+
}
453+
if reflect.DeepEqual(oldSecret.Data, newSecret.Data) {
454+
return false
455+
}
456+
logger.V(1).Info("Secret data changed, will enqueue HCloudMachines",
457+
"namespace", newSecret.GetNamespace(), "name", newSecret.GetName())
458+
return true
459+
},
460+
CreateFunc: func(_ event.CreateEvent) bool { return true },
461+
DeleteFunc: func(_ event.DeleteEvent) bool { return true },
462+
GenericFunc: func(_ event.GenericEvent) bool { return false },
463+
}
464+
}
465+
400466
// IgnoreInsignificantHetznerClusterUpdates is a predicate used for ignoring insignificant HetznerCluster.Status updates.
401467
func IgnoreInsignificantHetznerClusterUpdates(logger logr.Logger) predicate.Funcs {
402468
return predicate.Funcs{

controllers/hcloudmachine_controller_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@ limitations under the License.
1717
package controllers
1818

1919
import (
20+
"context"
2021
"fmt"
2122
"testing"
2223

2324
"github.com/hetznercloud/hcloud-go/v2/hcloud"
2425
. "github.com/onsi/ginkgo/v2"
2526
. "github.com/onsi/gomega"
27+
"github.com/stretchr/testify/require"
2628
corev1 "k8s.io/api/core/v1"
2729
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
"k8s.io/apimachinery/pkg/runtime"
31+
"k8s.io/apimachinery/pkg/types"
32+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
2833
"k8s.io/klog/v2"
2934
"k8s.io/utils/ptr"
3035
clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1"
@@ -33,8 +38,10 @@ import (
3338
v1beta2conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions/v1beta2"
3439
v1beta1patch "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/patch"
3540
"sigs.k8s.io/controller-runtime/pkg/client"
41+
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
3642
"sigs.k8s.io/controller-runtime/pkg/event"
3743
"sigs.k8s.io/controller-runtime/pkg/predicate"
44+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3845

3946
infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1"
4047
hcloudclient "github.com/syself/cluster-api-provider-hetzner/pkg/services/hcloud/client"
@@ -225,6 +232,141 @@ func TestIgnoreInsignificantMachineStatusUpdates(t *testing.T) {
225232
}
226233
}
227234

235+
func TestIgnoreInsignificantSecretUpdates(t *testing.T) {
236+
p := IgnoreInsignificantSecretUpdates(klog.Background())
237+
238+
makeSecret := func(data map[string][]byte, rv string) *corev1.Secret {
239+
return &corev1.Secret{
240+
ObjectMeta: metav1.ObjectMeta{
241+
Name: "hetzner",
242+
Namespace: "default",
243+
ResourceVersion: rv,
244+
},
245+
Data: data,
246+
}
247+
}
248+
249+
testCases := []struct {
250+
name string
251+
oldObj *corev1.Secret
252+
newObj *corev1.Secret
253+
expected bool
254+
}{
255+
{
256+
name: "Data changed",
257+
oldObj: makeSecret(map[string][]byte{"hcloud-token": []byte("old")}, "1"),
258+
newObj: makeSecret(map[string][]byte{"hcloud-token": []byte("new")}, "2"),
259+
expected: true,
260+
},
261+
{
262+
name: "Only ResourceVersion changed",
263+
oldObj: makeSecret(map[string][]byte{"hcloud-token": []byte("same")}, "1"),
264+
newObj: makeSecret(map[string][]byte{"hcloud-token": []byte("same")}, "2"),
265+
expected: false,
266+
},
267+
{
268+
name: "Unrelated data key added",
269+
oldObj: makeSecret(map[string][]byte{"hcloud-token": []byte("same")}, "1"),
270+
newObj: makeSecret(map[string][]byte{"hcloud-token": []byte("same"), "other": []byte("x")}, "2"),
271+
expected: true,
272+
},
273+
}
274+
275+
for _, tc := range testCases {
276+
t.Run(tc.name, func(t *testing.T) {
277+
got := p.Update(event.UpdateEvent{ObjectOld: tc.oldObj, ObjectNew: tc.newObj})
278+
require.Equal(t, tc.expected, got)
279+
})
280+
}
281+
282+
require.True(t, p.Create(event.CreateEvent{Object: makeSecret(nil, "1")}))
283+
require.True(t, p.Delete(event.DeleteEvent{Object: makeSecret(nil, "1")}))
284+
require.False(t, p.Generic(event.GenericEvent{Object: makeSecret(nil, "1")}))
285+
}
286+
287+
func TestHetznerSecretToHCloudMachines(t *testing.T) {
288+
ctx := context.Background()
289+
290+
testScheme := runtime.NewScheme()
291+
utilruntime.Must(corev1.AddToScheme(testScheme))
292+
utilruntime.Must(infrav1.AddToScheme(testScheme))
293+
utilruntime.Must(clusterv1.AddToScheme(testScheme))
294+
295+
const (
296+
ns = "default"
297+
secretName = "hetzner"
298+
clusterName = "cluster-a"
299+
)
300+
301+
newCluster := func(name string) *clusterv1.Cluster {
302+
return &clusterv1.Cluster{
303+
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns, UID: types.UID(name + "-uid")},
304+
}
305+
}
306+
newHetznerCluster := func(name, clusterOwner, secret string) *infrav1.HetznerCluster {
307+
return &infrav1.HetznerCluster{
308+
ObjectMeta: metav1.ObjectMeta{
309+
Name: name,
310+
Namespace: ns,
311+
OwnerReferences: []metav1.OwnerReference{
312+
{APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", Name: clusterOwner, UID: types.UID(clusterOwner + "-uid")},
313+
},
314+
},
315+
Spec: infrav1.HetznerClusterSpec{
316+
HetznerSecret: infrav1.HetznerSecretRef{
317+
Name: secret,
318+
Key: infrav1.HetznerSecretKeyRef{HCloudToken: "hcloud-token"},
319+
},
320+
},
321+
}
322+
}
323+
newMachine := func(name, clusterOwner, infraName string) *clusterv1.Machine {
324+
return &clusterv1.Machine{
325+
ObjectMeta: metav1.ObjectMeta{
326+
Name: name,
327+
Namespace: ns,
328+
Labels: map[string]string{clusterv1.ClusterNameLabel: clusterOwner},
329+
},
330+
Spec: clusterv1.MachineSpec{
331+
ClusterName: clusterOwner,
332+
InfrastructureRef: clusterv1.ContractVersionedObjectReference{
333+
APIGroup: infrav1.GroupVersion.Group,
334+
Kind: "HCloudMachine",
335+
Name: infraName,
336+
},
337+
},
338+
}
339+
}
340+
341+
capiClusterA := newCluster(clusterName)
342+
capiClusterB := newCluster("cluster-b")
343+
hcA := newHetznerCluster("hc-a", clusterName, secretName)
344+
hcB := newHetznerCluster("hc-b", "cluster-b", secretName)
345+
hcUnrelated := newHetznerCluster("hc-u", clusterName, "other-secret")
346+
hcmA := &infrav1.HCloudMachine{ObjectMeta: metav1.ObjectMeta{Name: "m-a", Namespace: ns}}
347+
hcmB := &infrav1.HCloudMachine{ObjectMeta: metav1.ObjectMeta{Name: "m-b", Namespace: ns}}
348+
cmA := newMachine("cm-a", clusterName, hcmA.Name)
349+
cmB := newMachine("cm-b", "cluster-b", hcmB.Name)
350+
matchingSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: ns}}
351+
otherSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "no-ref", Namespace: ns}}
352+
353+
c := fakeclient.NewClientBuilder().
354+
WithScheme(testScheme).
355+
WithObjects(capiClusterA, capiClusterB, hcA, hcB, hcUnrelated, hcmA, hcmB, cmA, cmB).
356+
Build()
357+
358+
r := &HCloudMachineReconciler{Client: c}
359+
mapper := r.HetznerSecretToHCloudMachines(ctx)
360+
361+
got := mapper(ctx, matchingSecret)
362+
require.ElementsMatch(t, []reconcile.Request{
363+
{NamespacedName: client.ObjectKey{Namespace: ns, Name: hcmA.Name}},
364+
{NamespacedName: client.ObjectKey{Namespace: ns, Name: hcmB.Name}},
365+
}, got)
366+
367+
require.Empty(t, mapper(ctx, otherSecret))
368+
}
369+
228370
var _ = Describe("HCloudMachineReconciler", func() {
229371
var (
230372
capiCluster *clusterv1.Cluster

0 commit comments

Comments
 (0)