Compare commits
1 commit
main
...
feat/cozys
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c8823a835 |
2 changed files with 200 additions and 0 deletions
|
|
@ -32,12 +32,16 @@ import (
|
||||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
@ -45,6 +49,7 @@ import (
|
||||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||||
|
|
||||||
|
"github.com/cozystack/cozystack/internal/cozyvaluesreplicator"
|
||||||
"github.com/cozystack/cozystack/internal/fluxinstall"
|
"github.com/cozystack/cozystack/internal/fluxinstall"
|
||||||
"github.com/cozystack/cozystack/internal/operator"
|
"github.com/cozystack/cozystack/internal/operator"
|
||||||
// +kubebuilder:scaffold:imports
|
// +kubebuilder:scaffold:imports
|
||||||
|
|
@ -73,6 +78,9 @@ func main() {
|
||||||
var enableHTTP2 bool
|
var enableHTTP2 bool
|
||||||
var installFlux bool
|
var installFlux bool
|
||||||
var cozystackVersion string
|
var cozystackVersion string
|
||||||
|
var cozyValuesSecretName string
|
||||||
|
var cozyValuesSecretNamespace string
|
||||||
|
var cozyValuesNamespaceSelector string
|
||||||
var platformSourceURL string
|
var platformSourceURL string
|
||||||
var platformSourceName string
|
var platformSourceName string
|
||||||
var platformSourceRef string
|
var platformSourceRef string
|
||||||
|
|
@ -92,6 +100,9 @@ func main() {
|
||||||
flag.StringVar(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
|
flag.StringVar(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
|
||||||
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-packages", "Name for the generated platform source resource (default: cozystack-packages)")
|
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-packages", "Name for the generated platform source resource (default: cozystack-packages)")
|
||||||
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
|
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
|
||||||
|
flag.StringVar(&cozyValuesSecretName, "cozy-values-secret-name", "cozystack-values", "The name of the secret containing cluster-wide configuration values.")
|
||||||
|
flag.StringVar(&cozyValuesSecretNamespace, "cozy-values-secret-namespace", "cozy-system", "The namespace of the secret containing cluster-wide configuration values.")
|
||||||
|
flag.StringVar(&cozyValuesNamespaceSelector, "cozy-values-namespace-selector", "cozystack.io/system=true", "The label selector for namespaces where the cluster-wide configuration values must be replicated.")
|
||||||
|
|
||||||
opts := zap.Options{
|
opts := zap.Options{
|
||||||
Development: true,
|
Development: true,
|
||||||
|
|
@ -110,10 +121,29 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetNSSelector, err := labels.Parse(cozyValuesNamespaceSelector)
|
||||||
|
if err != nil {
|
||||||
|
setupLog.Error(err, "could not parse namespace label selector")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// Start the controller manager
|
// Start the controller manager
|
||||||
setupLog.Info("Starting controller manager")
|
setupLog.Info("Starting controller manager")
|
||||||
mgr, err := ctrl.NewManager(config, ctrl.Options{
|
mgr, err := ctrl.NewManager(config, ctrl.Options{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
|
Cache: cache.Options{
|
||||||
|
ByObject: map[client.Object]cache.ByObject{
|
||||||
|
// Cache only Secrets named <secretName> (in any namespace)
|
||||||
|
&corev1.Secret{}: {
|
||||||
|
Field: fields.OneTermEqualSelector("metadata.name", cozyValuesSecretName),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cache only Namespaces that match a label selector
|
||||||
|
&corev1.Namespace{}: {
|
||||||
|
Label: targetNSSelector,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
Metrics: metricsserver.Options{
|
Metrics: metricsserver.Options{
|
||||||
BindAddress: metricsAddr,
|
BindAddress: metricsAddr,
|
||||||
SecureServing: secureMetrics,
|
SecureServing: secureMetrics,
|
||||||
|
|
@ -169,6 +199,16 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := (&cozyvaluesreplicator.SecretReplicatorReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
SourceNamespace: cozyValuesSecretNamespace,
|
||||||
|
SecretName: cozyValuesSecretName,
|
||||||
|
TargetNamespaceSelector: targetNSSelector,
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
setupLog.Error(err, "unable to create controller", "controller", "CozyValuesReplicator")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
// Setup PackageSource reconciler
|
// Setup PackageSource reconciler
|
||||||
if err := (&operator.PackageSourceReconciler{
|
if err := (&operator.PackageSourceReconciler{
|
||||||
Client: mgr.GetClient(),
|
Client: mgr.GetClient(),
|
||||||
|
|
|
||||||
160
internal/cozyvaluesreplicator/cozyvaluesreplicator.go
Normal file
160
internal/cozyvaluesreplicator/cozyvaluesreplicator.go
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue