mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-05 11:37:31 +09:00
161 lines
5.8 KiB
Go
161 lines
5.8 KiB
Go
package cozyvaluesreplicator
|
||
|
||
import (
|
||
"context"
|
||
|
||
corev1 "k8s.io/api/core/v1"
|
||
"k8s.io/apimachinery/pkg/labels"
|
||
"k8s.io/apimachinery/pkg/runtime"
|
||
"k8s.io/apimachinery/pkg/types"
|
||
ctrl "sigs.k8s.io/controller-runtime"
|
||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||
)
|
||
|
||
// Reconciler fields this setup relies on.
|
||
type SecretReplicatorReconciler struct {
|
||
client.Client
|
||
Scheme *runtime.Scheme
|
||
|
||
// Source of truth:
|
||
SourceNamespace string
|
||
SecretName string
|
||
|
||
// Namespaces to replicate into:
|
||
// (e.g. labels.SelectorFromSet(labels.Set{"tenant":"true"}), or metav1.LabelSelectorAsSelector(...))
|
||
TargetNamespaceSelector labels.Selector
|
||
}
|
||
|
||
func (r *SecretReplicatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||
// 1) Primary watch for requirement (b):
|
||
// Reconcile any Secret named r.SecretName in any namespace (includes source too).
|
||
// This keeps Secrets in cache and causes “copy changed -> reconcile it” to happen.
|
||
secretNameOnly := predicate.NewPredicateFuncs(func(obj client.Object) bool {
|
||
return obj.GetName() == r.SecretName
|
||
})
|
||
|
||
// 2) Secondary watch for requirement (c):
|
||
// When the *source* Secret changes, fan-out reconcile requests to every matching namespace.
|
||
onlySourceSecret := predicate.Funcs{
|
||
CreateFunc: func(e event.CreateEvent) bool { return isSourceSecret(e.Object, r) },
|
||
UpdateFunc: func(e event.UpdateEvent) bool { return isSourceSecret(e.ObjectNew, r) },
|
||
DeleteFunc: func(e event.DeleteEvent) bool { return isSourceSecret(e.Object, r) },
|
||
GenericFunc: func(e event.GenericEvent) bool {
|
||
return isSourceSecret(e.Object, r)
|
||
},
|
||
}
|
||
|
||
// Fan-out mapper for source Secret events -> one request per matching target namespace.
|
||
fanOutOnSourceSecret := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, _ client.Object) []reconcile.Request {
|
||
// List namespaces *from the cache* (because we also watch Namespaces below).
|
||
var nsList corev1.NamespaceList
|
||
if err := r.List(ctx, &nsList); err != nil {
|
||
// If list fails, best-effort: return nothing; reconcile will be retried by next event.
|
||
return nil
|
||
}
|
||
|
||
reqs := make([]reconcile.Request, 0, len(nsList.Items))
|
||
for i := range nsList.Items {
|
||
ns := &nsList.Items[i]
|
||
if ns.Name == r.SourceNamespace {
|
||
continue
|
||
}
|
||
if r.TargetNamespaceSelector != nil && !r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)) {
|
||
continue
|
||
}
|
||
reqs = append(reqs, reconcile.Request{
|
||
NamespacedName: types.NamespacedName{
|
||
Namespace: ns.Name,
|
||
Name: r.SecretName,
|
||
},
|
||
})
|
||
}
|
||
return reqs
|
||
})
|
||
|
||
// 3) Namespace watch for requirement (a):
|
||
// When a namespace is created/updated to match selector, enqueue reconcile for the Secret copy in that namespace.
|
||
enqueueOnNamespaceMatch := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||
ns, ok := obj.(*corev1.Namespace)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
if ns.Name == r.SourceNamespace {
|
||
return nil
|
||
}
|
||
if r.TargetNamespaceSelector != nil && !r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)) {
|
||
return nil
|
||
}
|
||
return []reconcile.Request{{
|
||
NamespacedName: types.NamespacedName{
|
||
Namespace: ns.Name,
|
||
Name: r.SecretName,
|
||
},
|
||
}}
|
||
})
|
||
|
||
// Only trigger from namespace events where the label match may be (or become) true.
|
||
// (You can keep this simple; it’s fine if it fires on any update—your Reconcile should be idempotent.)
|
||
namespaceMayMatter := predicate.Funcs{
|
||
CreateFunc: func(e event.CreateEvent) bool {
|
||
ns, ok := e.Object.(*corev1.Namespace)
|
||
return ok && (r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)))
|
||
},
|
||
UpdateFunc: func(e event.UpdateEvent) bool {
|
||
oldNS, okOld := e.ObjectOld.(*corev1.Namespace)
|
||
newNS, okNew := e.ObjectNew.(*corev1.Namespace)
|
||
if !okOld || !okNew {
|
||
return false
|
||
}
|
||
// Fire if it matches now OR matched before (covers transitions both ways; reconcile can decide what to do).
|
||
oldMatch := r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(oldNS.Labels))
|
||
newMatch := r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(newNS.Labels))
|
||
return oldMatch || newMatch
|
||
},
|
||
DeleteFunc: func(event.DeleteEvent) bool { return false }, // nothing to do on namespace delete
|
||
GenericFunc: func(event.GenericEvent) bool { return false },
|
||
}
|
||
|
||
return ctrl.NewControllerManagedBy(mgr).
|
||
// (b) Watch all Secrets with the chosen name; this also ensures Secret objects are cached.
|
||
For(&corev1.Secret{}, builder.WithPredicates(secretNameOnly)).
|
||
|
||
// (c) Add a second watch on Secret, but only for the source secret, and fan-out to all namespaces.
|
||
Watches(
|
||
&corev1.Secret{},
|
||
fanOutOnSourceSecret,
|
||
builder.WithPredicates(onlySourceSecret),
|
||
).
|
||
|
||
// (a) Watch Namespaces so they’re cached and so “namespace appears / starts matching” enqueues reconcile.
|
||
Watches(
|
||
&corev1.Namespace{},
|
||
enqueueOnNamespaceMatch,
|
||
builder.WithPredicates(namespaceMayMatter),
|
||
).
|
||
Complete(r)
|
||
}
|
||
|
||
func isSourceSecret(obj client.Object, r *SecretReplicatorReconciler) bool {
|
||
if obj == nil {
|
||
return false
|
||
}
|
||
return obj.GetNamespace() == r.SourceNamespace && obj.GetName() == r.SecretName
|
||
}
|
||
|
||
func (r *SecretReplicatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||
if req.Name != r.SecretName || req.Namespace == r.SourceNamespace {
|
||
return ctrl.Result{}, nil
|
||
}
|
||
originalSecret := &corev1.Secret{}
|
||
r.Get(ctx, types.NamespacedName{Namespace: r.SourceNamespace, Name: r.SecretName}, originalSecret)
|
||
replicatedSecret := originalSecret.DeepCopy()
|
||
replicatedSecret.Namespace = req.Namespace
|
||
r.Update(ctx, replicatedSecret)
|
||
return ctrl.Result{}, nil
|
||
}
|