From c902d0578cef25685c2a0846dd68018b6b81ecdf Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 6 Apr 2026 18:45:02 -0600 Subject: [PATCH 001/153] first draft to create targets --- api/v1alpha1/targetsource_types.go | 7 +++ .../operator.gnmic.dev_targetsources.yaml | 11 +++++ .../controller/targetsource_controller.go | 41 +++++++++++++--- internal/discovery/client.go | 49 +++++++++++++++++++ internal/discovery/mapper.go | 21 ++++++++ 5 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 internal/discovery/client.go create mode 100644 internal/discovery/mapper.go diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 99da5d5..93ec890 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -27,10 +27,17 @@ type TargetSourceSpec struct { ConfigMap string `json:"configMap,omitempty"` PodSelector metav1.LabelSelector `json:"podSelector,omitempty"` ServiceSelector metav1.LabelSelector `json:"serviceSelector,omitempty"` + Manual []ManualTarget `json:"manual,omitempty"` // Labels map[string]string `json:"labels,omitempty"` } +type ManualTarget struct { + Name string `json:"name,omitempty"` + Address string `json:"address,omitempty"` + TargetProfile string `json:"targetProfile,omitempty"` +} + type HTTPConfig struct { URL string `json:"url,omitempty"` } diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 789ff3f..a4daeb8 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -55,6 +55,17 @@ spec: additionalProperties: type: string type: object + manual: + items: + properties: + address: + type: string + name: + type: string + targetProfile: + type: string + type: object + type: array podSelector: description: |- A label selector is a label query over a set of resources. The result of matchLabels and diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 032b103..60bb8af 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -22,9 +22,11 @@ import ( "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" + "github.com/gnmic/operator/internal/discovery" ) // TargetSourceReconciler reconciles a TargetSource object @@ -50,12 +52,39 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request logger.Info("reconciling TargetSource", "name", targetSource.Name) - // TODO: Implement target discovery logic based on spec: - // - HTTP: fetch targets from HTTP endpoint - // - Consul: discover from Consul - // - ConfigMap: read from ConfigMap - // - PodSelector: select Kubernetes pods - // - ServiceSelector: select Kubernetes services + // VD: Approach for the reconciliation loop: + // 1. Fetch objects from TargetSource + // 2. Build desired state + // 3. Get actual state (only targets owned by TargetSource) + // 4. Compute diff + // 5. Apply changes (create, update, delete) + + discoveredTargets, err := discovery.FetchNewTargets(ctx, targetSource) + if err != nil { + logger.Error(err, "error getting discovered targets") + return ctrl.Result{}, err + } + + existingTargets, err := discovery.GetExistingTargets(ctx, r.Client, targetSource) + if err != nil { + logger.Error(err, "error fetching existing targets") + return ctrl.Result{}, err + } + + newTargets, err := discovery.GetNewTargets(existingTargets, discoveredTargets) + + for _, t := range newTargets { + err = controllerutil.SetControllerReference(&targetSource, &t, r.Scheme) + if err != nil { + return ctrl.Result{}, err + } + + err = r.Client.Create(ctx, &t) + if err != nil { + logger.Error(err, "error when creating target") + return ctrl.Result{}, err + } + } return ctrl.Result{}, nil } diff --git a/internal/discovery/client.go b/internal/discovery/client.go new file mode 100644 index 0000000..61a94dd --- /dev/null +++ b/internal/discovery/client.go @@ -0,0 +1,49 @@ +package discovery + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" +) + +func FetchNewTargets(ctx context.Context, ts gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { + var targets []gnmicv1alpha1.Target + + for _, e := range ts.Spec.Manual { + target := &gnmicv1alpha1.Target{ + ObjectMeta: metav1.ObjectMeta{ + Name: e.Name, + Namespace: ts.Namespace, + Labels: map[string]string{ + "gnmic.io/source": ts.Name, + }, + }, + Spec: gnmicv1alpha1.TargetSpec{ + Address: e.Address, + Profile: e.TargetProfile, + }, + } + targets = append(targets, *target) + } + + return targets, nil +} + +func GetExistingTargets(ctx context.Context, c client.Client, ts gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { + var targetList gnmicv1alpha1.TargetList + + err := c.List(ctx, &targetList, + client.InNamespace(ts.Namespace), + client.MatchingLabels{ + "gnmic.io/source": ts.Name, + }, + ) + if err != nil { + return nil, err + } + + return targetList.Items, nil +} diff --git a/internal/discovery/mapper.go b/internal/discovery/mapper.go new file mode 100644 index 0000000..744a98d --- /dev/null +++ b/internal/discovery/mapper.go @@ -0,0 +1,21 @@ +package discovery + +import ( + "slices" + + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" +) + +func GetNewTargets(existing, discovered []gnmicv1alpha1.Target) ([]gnmicv1alpha1.Target, error) { + var new []gnmicv1alpha1.Target + + for _, t := range discovered { + if !slices.ContainsFunc(existing, func(e gnmicv1alpha1.Target) bool { + return e.ObjectMeta.Name == t.ObjectMeta.Name && e.ObjectMeta.Namespace == t.ObjectMeta.Namespace + }) { + new = append(new, t) + } + } + + return new, nil +} From 95be5ab5262f30fafd94cef7d298dccddb5603d8 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 6 Apr 2026 18:52:18 -0600 Subject: [PATCH 002/153] added deletion of targets --- internal/controller/targetsource_controller.go | 10 ++++++++++ internal/discovery/mapper.go | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 60bb8af..63ee455 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -86,6 +86,16 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request } } + deletedTargets, err := discovery.GetDeletedTargets(existingTargets, discoveredTargets) + + for _, t := range deletedTargets { + err = r.Client.Delete(ctx, &t) + if err != nil { + logger.Error(err, "error deleting the object") + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil } diff --git a/internal/discovery/mapper.go b/internal/discovery/mapper.go index 744a98d..6fa7f9f 100644 --- a/internal/discovery/mapper.go +++ b/internal/discovery/mapper.go @@ -19,3 +19,17 @@ func GetNewTargets(existing, discovered []gnmicv1alpha1.Target) ([]gnmicv1alpha1 return new, nil } + +func GetDeletedTargets(existing, discovered []gnmicv1alpha1.Target) ([]gnmicv1alpha1.Target, error) { + var deleted []gnmicv1alpha1.Target + + for _, e := range existing { + if !slices.ContainsFunc(discovered, func(d gnmicv1alpha1.Target) bool { + return d.ObjectMeta.Name == e.ObjectMeta.Name && d.ObjectMeta.Namespace == e.ObjectMeta.Namespace + }) { + deleted = append(deleted, e) + } + } + + return deleted, nil +} From 4e3104e80689c40d53c71b8ede6ae0d9fa460a08 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 6 Apr 2026 20:07:57 -0600 Subject: [PATCH 003/153] renamed client functions --- internal/controller/targetsource_controller.go | 4 ++-- internal/discovery/client.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 63ee455..aaf758c 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -59,13 +59,13 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request // 4. Compute diff // 5. Apply changes (create, update, delete) - discoveredTargets, err := discovery.FetchNewTargets(ctx, targetSource) + discoveredTargets, err := discovery.FetchDiscoveryTargets(ctx, targetSource) if err != nil { logger.Error(err, "error getting discovered targets") return ctrl.Result{}, err } - existingTargets, err := discovery.GetExistingTargets(ctx, r.Client, targetSource) + existingTargets, err := discovery.FetchExistingTargets(ctx, r.Client, targetSource) if err != nil { logger.Error(err, "error fetching existing targets") return ctrl.Result{}, err diff --git a/internal/discovery/client.go b/internal/discovery/client.go index 61a94dd..fccb571 100644 --- a/internal/discovery/client.go +++ b/internal/discovery/client.go @@ -9,7 +9,7 @@ import ( gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" ) -func FetchNewTargets(ctx context.Context, ts gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { +func FetchDiscoveryTargets(ctx context.Context, ts gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { var targets []gnmicv1alpha1.Target for _, e := range ts.Spec.Manual { @@ -32,7 +32,7 @@ func FetchNewTargets(ctx context.Context, ts gnmicv1alpha1.TargetSource) ([]gnmi return targets, nil } -func GetExistingTargets(ctx context.Context, c client.Client, ts gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { +func FetchExistingTargets(ctx context.Context, c client.Client, ts gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { var targetList gnmicv1alpha1.TargetList err := c.List(ctx, &targetList, From 32eb936c9ac57918e531a7b8a1f187464dfc26e9 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 6 Apr 2026 20:31:34 -0600 Subject: [PATCH 004/153] added update functionality --- .../controller/targetsource_controller.go | 25 +++++++++++++++++++ internal/discovery/mapper.go | 23 +++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index aaf758c..907d9b0 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -20,6 +20,7 @@ import ( "context" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -86,6 +87,30 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request } } + updatedTargets, err := discovery.GetUpdatedTargets(existingTargets, discoveredTargets) + + for _, t := range updatedTargets { + existing := &gnmicv1alpha1.Target{} + + err := r.Get(ctx, types.NamespacedName{ + Name: t.ObjectMeta.Name, + Namespace: t.ObjectMeta.Namespace, + }, existing) + + if err != nil { + logger.Error(err, "error fetching existing object") + return ctrl.Result{}, err + } + + existing.Spec = t.Spec + + err = r.Update(ctx, existing) + if err != nil { + logger.Error(err, "error updating object") + return ctrl.Result{}, err + } + } + deletedTargets, err := discovery.GetDeletedTargets(existingTargets, discoveredTargets) for _, t := range deletedTargets { diff --git a/internal/discovery/mapper.go b/internal/discovery/mapper.go index 6fa7f9f..a4ba5e6 100644 --- a/internal/discovery/mapper.go +++ b/internal/discovery/mapper.go @@ -20,6 +20,29 @@ func GetNewTargets(existing, discovered []gnmicv1alpha1.Target) ([]gnmicv1alpha1 return new, nil } +func GetUpdatedTargets(existing, discovered []gnmicv1alpha1.Target) ([]gnmicv1alpha1.Target, error) { + var updated []gnmicv1alpha1.Target + + existingMap := make(map[string]gnmicv1alpha1.Target) + + for _, e := range existing { + key := e.Namespace + "/" + e.Name + existingMap[key] = e + } + + for _, t := range discovered { + key := t.Namespace + "/" + t.Name + + if e, found := existingMap[key]; found { + if e.Spec.Address != t.Spec.Address || e.Spec.Profile != t.Spec.Address { + updated = append(updated, t) + } + } + } + + return updated, nil +} + func GetDeletedTargets(existing, discovered []gnmicv1alpha1.Target) ([]gnmicv1alpha1.Target, error) { var deleted []gnmicv1alpha1.Target From 3c4141f479ed76716be27585826ae716961c73dc Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 6 Apr 2026 20:54:15 -0600 Subject: [PATCH 005/153] refactored crud options into separate function with struct --- .../controller/targetsource_controller.go | 12 ++---- internal/discovery/mapper.go | 38 +++++++------------ 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 907d9b0..a76836d 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -72,9 +72,9 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, err } - newTargets, err := discovery.GetNewTargets(existingTargets, discoveredTargets) + diff := discovery.BuildDiff(existingTargets, discoveredTargets) - for _, t := range newTargets { + for _, t := range diff.ToCreate { err = controllerutil.SetControllerReference(&targetSource, &t, r.Scheme) if err != nil { return ctrl.Result{}, err @@ -87,9 +87,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request } } - updatedTargets, err := discovery.GetUpdatedTargets(existingTargets, discoveredTargets) - - for _, t := range updatedTargets { + for _, t := range diff.ToUpdate { existing := &gnmicv1alpha1.Target{} err := r.Get(ctx, types.NamespacedName{ @@ -111,9 +109,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request } } - deletedTargets, err := discovery.GetDeletedTargets(existingTargets, discoveredTargets) - - for _, t := range deletedTargets { + for _, t := range diff.ToDelete { err = r.Client.Delete(ctx, &t) if err != nil { logger.Error(err, "error deleting the object") diff --git a/internal/discovery/mapper.go b/internal/discovery/mapper.go index a4ba5e6..f8d6468 100644 --- a/internal/discovery/mapper.go +++ b/internal/discovery/mapper.go @@ -4,27 +4,19 @@ import ( "slices" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" + "k8s.io/apimachinery/pkg/api/equality" ) -func GetNewTargets(existing, discovered []gnmicv1alpha1.Target) ([]gnmicv1alpha1.Target, error) { - var new []gnmicv1alpha1.Target - - for _, t := range discovered { - if !slices.ContainsFunc(existing, func(e gnmicv1alpha1.Target) bool { - return e.ObjectMeta.Name == t.ObjectMeta.Name && e.ObjectMeta.Namespace == t.ObjectMeta.Namespace - }) { - new = append(new, t) - } - } - - return new, nil +type Diff struct { + ToCreate []gnmicv1alpha1.Target + ToUpdate []gnmicv1alpha1.Target + ToDelete []gnmicv1alpha1.Target } -func GetUpdatedTargets(existing, discovered []gnmicv1alpha1.Target) ([]gnmicv1alpha1.Target, error) { - var updated []gnmicv1alpha1.Target +func BuildDiff(existing, discovered []gnmicv1alpha1.Target) Diff { + var diff Diff existingMap := make(map[string]gnmicv1alpha1.Target) - for _, e := range existing { key := e.Namespace + "/" + e.Name existingMap[key] = e @@ -34,25 +26,21 @@ func GetUpdatedTargets(existing, discovered []gnmicv1alpha1.Target) ([]gnmicv1al key := t.Namespace + "/" + t.Name if e, found := existingMap[key]; found { - if e.Spec.Address != t.Spec.Address || e.Spec.Profile != t.Spec.Address { - updated = append(updated, t) + if !equality.Semantic.DeepEqual(e.Spec, t.Spec) { + diff.ToUpdate = append(diff.ToUpdate, t) } + } else { + diff.ToCreate = append(diff.ToCreate, t) } } - return updated, nil -} - -func GetDeletedTargets(existing, discovered []gnmicv1alpha1.Target) ([]gnmicv1alpha1.Target, error) { - var deleted []gnmicv1alpha1.Target - for _, e := range existing { if !slices.ContainsFunc(discovered, func(d gnmicv1alpha1.Target) bool { return d.ObjectMeta.Name == e.ObjectMeta.Name && d.ObjectMeta.Namespace == e.ObjectMeta.Namespace }) { - deleted = append(deleted, e) + diff.ToDelete = append(diff.ToDelete, e) } } - return deleted, nil + return diff } From 590a494d3b02cab3f6136b2635672fac31e2e239 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 6 Apr 2026 20:57:36 -0600 Subject: [PATCH 006/153] refactored delete to use map lookup --- internal/discovery/mapper.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/internal/discovery/mapper.go b/internal/discovery/mapper.go index f8d6468..6d1c271 100644 --- a/internal/discovery/mapper.go +++ b/internal/discovery/mapper.go @@ -1,8 +1,6 @@ package discovery import ( - "slices" - gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "k8s.io/apimachinery/pkg/api/equality" ) @@ -22,6 +20,12 @@ func BuildDiff(existing, discovered []gnmicv1alpha1.Target) Diff { existingMap[key] = e } + discoveredMap := make(map[string]gnmicv1alpha1.Target) + for _, e := range discovered { + key := e.Namespace + "/" + e.Name + discoveredMap[key] = e + } + for _, t := range discovered { key := t.Namespace + "/" + t.Name @@ -35,9 +39,9 @@ func BuildDiff(existing, discovered []gnmicv1alpha1.Target) Diff { } for _, e := range existing { - if !slices.ContainsFunc(discovered, func(d gnmicv1alpha1.Target) bool { - return d.ObjectMeta.Name == e.ObjectMeta.Name && d.ObjectMeta.Namespace == e.ObjectMeta.Namespace - }) { + key := e.Namespace + "/" + e.Name + + if e, found := discoveredMap[key]; !found { diff.ToDelete = append(diff.ToDelete, e) } } From 26669868c647484ee389232a6babf8adc315d2f8 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 6 Apr 2026 21:01:09 -0600 Subject: [PATCH 007/153] added comments --- internal/controller/targetsource_controller.go | 11 +++++++---- internal/discovery/mapper.go | 6 +++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index a76836d..cfc0502 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -55,25 +55,28 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request // VD: Approach for the reconciliation loop: // 1. Fetch objects from TargetSource - // 2. Build desired state - // 3. Get actual state (only targets owned by TargetSource) - // 4. Compute diff - // 5. Apply changes (create, update, delete) + // 2. Get actual state (only targets owned by TargetSource) + // 3. Compute diff + // 4. Apply changes (create, update, delete) + // Step 1: Get desired state from discovery source discoveredTargets, err := discovery.FetchDiscoveryTargets(ctx, targetSource) if err != nil { logger.Error(err, "error getting discovered targets") return ctrl.Result{}, err } + // Step 2: Get current state from Kubernetes cluster (lookup by label of TargetSource) existingTargets, err := discovery.FetchExistingTargets(ctx, r.Client, targetSource) if err != nil { logger.Error(err, "error fetching existing targets") return ctrl.Result{}, err } + // Step 3: Compute diff diff := discovery.BuildDiff(existingTargets, discoveredTargets) + // Step 4: Iterate over each list and do create, update, delete respectively for _, t := range diff.ToCreate { err = controllerutil.SetControllerReference(&targetSource, &t, r.Scheme) if err != nil { diff --git a/internal/discovery/mapper.go b/internal/discovery/mapper.go index 6d1c271..1ec2931 100644 --- a/internal/discovery/mapper.go +++ b/internal/discovery/mapper.go @@ -26,18 +26,22 @@ func BuildDiff(existing, discovered []gnmicv1alpha1.Target) Diff { discoveredMap[key] = e } + // Loop for targets to create + update for _, t := range discovered { key := t.Namespace + "/" + t.Name + // Check if target already exists if e, found := existingMap[key]; found { + // Check if the spec of the target changed if !equality.Semantic.DeepEqual(e.Spec, t.Spec) { diff.ToUpdate = append(diff.ToUpdate, t) } - } else { + } else { // Target is new diff.ToCreate = append(diff.ToCreate, t) } } + // Loop for targets to delete for _, e := range existing { key := e.Namespace + "/" + e.Name From b9cd85ef6ca79c4672cb4ff4d337ff11c25bb211 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 6 Apr 2026 21:23:03 -0600 Subject: [PATCH 008/153] fixed bug with deleted object name being empty --- internal/discovery/mapper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/discovery/mapper.go b/internal/discovery/mapper.go index 1ec2931..dc686a4 100644 --- a/internal/discovery/mapper.go +++ b/internal/discovery/mapper.go @@ -45,7 +45,7 @@ func BuildDiff(existing, discovered []gnmicv1alpha1.Target) Diff { for _, e := range existing { key := e.Namespace + "/" + e.Name - if e, found := discoveredMap[key]; !found { + if _, found := discoveredMap[key]; !found { diff.ToDelete = append(diff.ToDelete, e) } } From 109a85e504a7c02b0ee74dbc8bc981843a45a2ed Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 6 Apr 2026 21:23:14 -0600 Subject: [PATCH 009/153] added info logs --- internal/controller/targetsource_controller.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index cfc0502..d863e48 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -18,6 +18,7 @@ package controller import ( "context" + "fmt" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -80,14 +81,16 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request for _, t := range diff.ToCreate { err = controllerutil.SetControllerReference(&targetSource, &t, r.Scheme) if err != nil { + logger.Error(err, "error setting the owner reference") return ctrl.Result{}, err } err = r.Client.Create(ctx, &t) if err != nil { - logger.Error(err, "error when creating target") + logger.Error(err, "error creating target object") return ctrl.Result{}, err } + logger.Info(fmt.Sprintf("created new target object %s/%s", t.ObjectMeta.Namespace, t.ObjectMeta.Name)) } for _, t := range diff.ToUpdate { @@ -99,7 +102,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request }, existing) if err != nil { - logger.Error(err, "error fetching existing object") + logger.Error(err, "error fetching existing target object") return ctrl.Result{}, err } @@ -110,14 +113,17 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request logger.Error(err, "error updating object") return ctrl.Result{}, err } + logger.Info(fmt.Sprintf("updated existing target object %s/%s", t.ObjectMeta.Namespace, t.ObjectMeta.Name)) } for _, t := range diff.ToDelete { err = r.Client.Delete(ctx, &t) + logger.Info(fmt.Sprintf("resource name to be deleted: %s/%s", t.ObjectMeta.Namespace, t.ObjectMeta.Name)) if err != nil { logger.Error(err, "error deleting the object") return ctrl.Result{}, err } + logger.Info(fmt.Sprintf("deleted target object %s/%s", t.ObjectMeta.Namespace, t.ObjectMeta.Name)) } return ctrl.Result{}, nil From 402c70a06b85c6d891bc26017272cac6a6fcb66a Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 13 Apr 2026 14:46:40 -0600 Subject: [PATCH 010/153] changed NewLoader function call --- internal/controller/targetsource/loaders.go | 9 ++++++--- internal/controller/targetsource_controller.go | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/controller/targetsource/loaders.go b/internal/controller/targetsource/loaders.go index 309bf1a..c7e967e 100644 --- a/internal/controller/targetsource/loaders.go +++ b/internal/controller/targetsource/loaders.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "sync" + + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" ) // Loader defines a pluggable TargetSource loader interface @@ -39,13 +41,14 @@ func Register(name string, factory func() Loader) { } // NewLoader creates a loader by name -func NewLoader(name string) (Loader, error) { +func NewLoader(name string, namespace string, spec gnmicv1alpha1.TargetSourceSpec) (Loader, error) { registryMu.RLock() defer registryMu.RUnlock() - factory, ok := registry[name] + loaderName := namespace + "/" + name + factory, ok := registry[loaderName] if !ok { - return nil, fmt.Errorf("unknown targetsource loader: %q", name) + return nil, fmt.Errorf("unknown targetsource loader: %q", loaderName) } return factory(), nil } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 58afd52..16d5d64 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -72,13 +72,13 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request _, exists := r.running[req.NamespacedName] r.mu.Unlock() - // If an targetsource loader exists, return immediately without starting + // If a targetsource loader exists, return immediately without starting // any new loader or target manager if exists { return ctrl.Result{}, nil } - loader, err := targetsource.NewLoader(targetSource.Spec.Type) // TODO: pass configuration to loader based on spec + loader, err := targetsource.NewLoader(targetSource.ObjectMeta.Name, targetSource.ObjectMeta.Namespace, targetSource.Spec) // TODO: pass configuration to loader based on spec if err != nil { return ctrl.Result{}, err } From 7f8328502c49073b181770fd3f3a984bb3674bb6 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 13 Apr 2026 15:14:06 -0600 Subject: [PATCH 011/153] added discovery message to types --- internal/controller/targetsource/types.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/controller/targetsource/types.go b/internal/controller/targetsource/types.go index 51f7468..9f1c150 100644 --- a/internal/controller/targetsource/types.go +++ b/internal/controller/targetsource/types.go @@ -10,6 +10,19 @@ type DiscoveredTarget struct { Labels map[string]string } +const ( + DELETE DiscoveryEvent = 0 + CREATE DiscoveryEvent = 1 + UPDATE DiscoveryEvent = 2 +) + +type DiscoveryEvent int + +type DiscoveryMessage struct { + Target DiscoveredTarget + Event DiscoveryEvent +} + // TargetManager consumes discovered targets and applies them to Kubernetes. type TargetManager struct { client client.Client From 7d03411e7b7eb96ebc9be336acf2e75ca408e0aa Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 13 Apr 2026 15:16:45 -0600 Subject: [PATCH 012/153] changed target source channel type --- internal/controller/targetsource/loaders.go | 2 +- internal/controller/targetsource/target_manager.go | 2 +- internal/controller/targetsource/types.go | 2 +- internal/controller/targetsource_controller.go | 7 ++++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/controller/targetsource/loaders.go b/internal/controller/targetsource/loaders.go index c7e967e..62344ca 100644 --- a/internal/controller/targetsource/loaders.go +++ b/internal/controller/targetsource/loaders.go @@ -19,7 +19,7 @@ type Loader interface { Start( ctx context.Context, targetsourceName string, - out chan<- []DiscoveredTarget, + out chan<- []DiscoveryMessage, ) error } diff --git a/internal/controller/targetsource/target_manager.go b/internal/controller/targetsource/target_manager.go index e6b20b9..be34fa2 100644 --- a/internal/controller/targetsource/target_manager.go +++ b/internal/controller/targetsource/target_manager.go @@ -8,7 +8,7 @@ import ( ) // NewTargetManager wires a TargetManager instance. -func NewTargetManager(c client.Client, sourceName string, in <-chan []DiscoveredTarget) *TargetManager { +func NewTargetManager(c client.Client, sourceName string, in <-chan []DiscoveryMessage) *TargetManager { return &TargetManager{ client: c, targetsource: sourceName, diff --git a/internal/controller/targetsource/types.go b/internal/controller/targetsource/types.go index 9f1c150..43928f2 100644 --- a/internal/controller/targetsource/types.go +++ b/internal/controller/targetsource/types.go @@ -27,5 +27,5 @@ type DiscoveryMessage struct { type TargetManager struct { client client.Client targetsource string - in <-chan []DiscoveredTarget + in <-chan []DiscoveryMessage } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 16d5d64..b4218b6 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -84,16 +84,17 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request } runtimeCtx, cancel := context.WithCancel(context.Background()) - target_channel := make(chan []targetsource.DiscoveredTarget) + + targetChannel := make(chan []targetsource.DiscoveryMessage, 10) // start loader - go loader.Start(runtimeCtx, targetSource.Name, target_channel) + go loader.Start(runtimeCtx, targetSource.Name, targetChannel) // start target manager manager := targetsource.NewTargetManager( r.Client, targetSource.Name, - target_channel, + targetChannel, ) go manager.Run(runtimeCtx) From 4fb037319f88de0f957d3d52f54b411537532501 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 13 Apr 2026 15:20:15 -0600 Subject: [PATCH 013/153] fixed http_pull implementation based on new types --- .../targetsource/loaders/http_pull/loader.go | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/internal/controller/targetsource/loaders/http_pull/loader.go b/internal/controller/targetsource/loaders/http_pull/loader.go index 58efba4..dee5e13 100644 --- a/internal/controller/targetsource/loaders/http_pull/loader.go +++ b/internal/controller/targetsource/loaders/http_pull/loader.go @@ -23,7 +23,7 @@ func (l *Loader) Name() string { func (l *Loader) Start( ctx context.Context, targetsourceName string, - out chan<- []targetsource.DiscoveredTarget, + out chan<- []targetsource.DiscoveryMessage, ) error { logger := log.FromContext(ctx).WithValues("loader", l.Name()) @@ -41,16 +41,22 @@ func (l *Loader) Start( case <-ticker.C: // Example snapshot (placeholder) - targets := []targetsource.DiscoveredTarget{ + targets := []targetsource.DiscoveryMessage{ { - Name: "ceos1", - Address: "clab-3-nodes-ceos1:6030", - Labels: map[string]string{"TargetSource": targetsourceName}, + Target: targetsource.DiscoveredTarget{ + Name: "ceos1", + Address: "clab-3-nodes-ceos1:6030", + Labels: map[string]string{"TargetSource": targetsourceName}, + }, + Event: 1, }, { - Name: "leaf1", - Address: "clab-3-nodes-leaf1:57400", - Labels: map[string]string{"TargetSource": targetsourceName}, + Target: targetsource.DiscoveredTarget{ + Name: "leaf1", + Address: "clab-3-nodes-leaf1:57400", + Labels: map[string]string{"TargetSource": targetsourceName}, + }, + Event: 1, }, } From 2294f1e71e3efc60671879902f8eb11d087b084f Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 13 Apr 2026 16:06:52 -0600 Subject: [PATCH 014/153] implemented first draft of target creation using loaders --- internal/controller/targetsource/loaders.go | 2 +- .../targetsource/loaders/http_pull/loader.go | 4 +- .../controller/targetsource/target_manager.go | 44 ++++++++++++++++--- internal/controller/targetsource/types.go | 10 ++++- .../controller/targetsource_controller.go | 3 +- 5 files changed, 52 insertions(+), 11 deletions(-) diff --git a/internal/controller/targetsource/loaders.go b/internal/controller/targetsource/loaders.go index 62344ca..b591acd 100644 --- a/internal/controller/targetsource/loaders.go +++ b/internal/controller/targetsource/loaders.go @@ -46,7 +46,7 @@ func NewLoader(name string, namespace string, spec gnmicv1alpha1.TargetSourceSpe defer registryMu.RUnlock() loaderName := namespace + "/" + name - factory, ok := registry[loaderName] + factory, ok := registry[spec.Type] if !ok { return nil, fmt.Errorf("unknown targetsource loader: %q", loaderName) } diff --git a/internal/controller/targetsource/loaders/http_pull/loader.go b/internal/controller/targetsource/loaders/http_pull/loader.go index dee5e13..4157107 100644 --- a/internal/controller/targetsource/loaders/http_pull/loader.go +++ b/internal/controller/targetsource/loaders/http_pull/loader.go @@ -48,7 +48,7 @@ func (l *Loader) Start( Address: "clab-3-nodes-ceos1:6030", Labels: map[string]string{"TargetSource": targetsourceName}, }, - Event: 1, + Event: targetsource.CREATE, }, { Target: targetsource.DiscoveredTarget{ @@ -56,7 +56,7 @@ func (l *Loader) Start( Address: "clab-3-nodes-leaf1:57400", Labels: map[string]string{"TargetSource": targetsourceName}, }, - Event: 1, + Event: targetsource.CREATE, }, } diff --git a/internal/controller/targetsource/target_manager.go b/internal/controller/targetsource/target_manager.go index be34fa2..b69a9c9 100644 --- a/internal/controller/targetsource/target_manager.go +++ b/internal/controller/targetsource/target_manager.go @@ -2,16 +2,22 @@ package targetsource import ( "context" + "fmt" + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" ) // NewTargetManager wires a TargetManager instance. -func NewTargetManager(c client.Client, sourceName string, in <-chan []DiscoveryMessage) *TargetManager { +func NewTargetManager(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []DiscoveryMessage) *TargetManager { return &TargetManager{ client: c, - targetsource: sourceName, + scheme: s, + targetSource: ts, in: in, } } @@ -20,7 +26,7 @@ func NewTargetManager(c client.Client, sourceName string, in <-chan []DiscoveryM // and reconciles Target CRs accordingly func (m *TargetManager) Run(ctx context.Context) error { logger := log.FromContext(ctx). - WithValues("targetSource", m.targetsource) + WithValues("targetSource", m.targetSource) logger.Info("target manager started") @@ -30,12 +36,40 @@ func (m *TargetManager) Run(ctx context.Context) error { logger.Info("target manager stopped") return nil - case targets := <-m.in: + case messages := <-m.in: logger.Info( "received discovered targets", - "count", len(targets), + "count", len(messages), ) + for _, msg := range messages { + if msg.Event == CREATE { + target := &gnmicv1alpha1.Target{ + ObjectMeta: metav1.ObjectMeta{ + Name: msg.Target.Name, + Namespace: m.targetSource.ObjectMeta.Namespace, + Labels: map[string]string{ + "gnmic.io/source": m.targetSource.ObjectMeta.Name, + }, + }, + Spec: gnmicv1alpha1.TargetSpec{ + Address: msg.Target.Address, + Profile: "default", + }, + } + err := controllerutil.SetControllerReference(m.targetSource, target, m.scheme) + if err != nil { + logger.Error(err, "error setting the owner reference") + } + + err = m.client.Create(ctx, target) + if err != nil { + logger.Error(err, "error creating target object") + } + logger.Info(fmt.Sprintf("created new target object %s/%s", target.ObjectMeta.Namespace, target.ObjectMeta.Name)) + } + } + // List existing Target CRs owned by this TargetSource // var existing gnmicv1alpha1.TargetList // if err := m.client.List( diff --git a/internal/controller/targetsource/types.go b/internal/controller/targetsource/types.go index 43928f2..6c2b283 100644 --- a/internal/controller/targetsource/types.go +++ b/internal/controller/targetsource/types.go @@ -1,6 +1,11 @@ package targetsource -import "sigs.k8s.io/controller-runtime/pkg/client" +import ( + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" +) // DiscoveredTarget represents a target discovered from an external source // before it is materialized as a Kubernetes Target CR @@ -26,6 +31,7 @@ type DiscoveryMessage struct { // TargetManager consumes discovered targets and applies them to Kubernetes. type TargetManager struct { client client.Client - targetsource string + scheme *runtime.Scheme + targetSource *gnmicv1alpha1.TargetSource in <-chan []DiscoveryMessage } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index b4218b6..a7b887c 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -93,7 +93,8 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request // start target manager manager := targetsource.NewTargetManager( r.Client, - targetSource.Name, + r.Scheme, + &targetSource, targetChannel, ) go manager.Run(runtimeCtx) From 912e05a9945048de6518ec3684d11b0d4e05dd28 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Tue, 14 Apr 2026 15:12:58 -0600 Subject: [PATCH 015/153] removed manual targetsource spec --- api/v1alpha1/targetsource_types.go | 7 ------- internal/controller/targetsource/client.go | 24 ---------------------- 2 files changed, 31 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 30aa6bd..037b581 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -28,18 +28,11 @@ type TargetSourceSpec struct { ConfigMap string `json:"configMap,omitempty"` PodSelector metav1.LabelSelector `json:"podSelector,omitempty"` ServiceSelector metav1.LabelSelector `json:"serviceSelector,omitempty"` - Manual []ManualTarget `json:"manual,omitempty"` // Type string `json:"type,omitempty"` Labels map[string]string `json:"labels,omitempty"` } -type ManualTarget struct { - Name string `json:"name,omitempty"` - Address string `json:"address,omitempty"` - TargetProfile string `json:"targetProfile,omitempty"` -} - type HTTPConfig struct { URL string `json:"url,omitempty"` } diff --git a/internal/controller/targetsource/client.go b/internal/controller/targetsource/client.go index b190adc..c918ba1 100644 --- a/internal/controller/targetsource/client.go +++ b/internal/controller/targetsource/client.go @@ -3,35 +3,11 @@ package targetsource import ( "context" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" ) -func FetchDiscoveryTargets(ctx context.Context, ts gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { - var targets []gnmicv1alpha1.Target - - for _, e := range ts.Spec.Manual { - target := &gnmicv1alpha1.Target{ - ObjectMeta: metav1.ObjectMeta{ - Name: e.Name, - Namespace: ts.Namespace, - Labels: map[string]string{ - "gnmic.io/source": ts.Name, - }, - }, - Spec: gnmicv1alpha1.TargetSpec{ - Address: e.Address, - Profile: e.TargetProfile, - }, - } - targets = append(targets, *target) - } - - return targets, nil -} - func FetchExistingTargets(ctx context.Context, c client.Client, ts gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { var targetList gnmicv1alpha1.TargetList From bf28aad5fd34de0043e52c144aa51d719ec7be5b Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Tue, 14 Apr 2026 15:36:02 -0600 Subject: [PATCH 016/153] cleaned up reconciliation loop and refactored into target manager --- .../operator.gnmic.dev_targetsources.yaml | 11 --- .../controller/targetsource/target_manager.go | 68 +++++++++++++------ .../controller/targetsource_controller.go | 67 +----------------- 3 files changed, 49 insertions(+), 97 deletions(-) diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index c5604d8..3070c0c 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -60,17 +60,6 @@ spec: additionalProperties: type: string type: object - manual: - items: - properties: - address: - type: string - name: - type: string - targetProfile: - type: string - type: object - type: array podSelector: description: |- A label selector is a label query over a set of resources. The result of matchLabels and diff --git a/internal/controller/targetsource/target_manager.go b/internal/controller/targetsource/target_manager.go index b69a9c9..ebb3bf5 100644 --- a/internal/controller/targetsource/target_manager.go +++ b/internal/controller/targetsource/target_manager.go @@ -4,12 +4,14 @@ import ( "context" "fmt" - gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" + + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" ) // NewTargetManager wires a TargetManager instance. @@ -22,7 +24,7 @@ func NewTargetManager(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.Targ } } -// Run is a long‑running loop that receives target snapshots +// Run is a long‑running loop that receives target event messages // and reconciles Target CRs accordingly func (m *TargetManager) Run(ctx context.Context) error { logger := log.FromContext(ctx). @@ -43,7 +45,24 @@ func (m *TargetManager) Run(ctx context.Context) error { ) for _, msg := range messages { - if msg.Event == CREATE { + switch msg.Event { + case DELETE: + existing := &gnmicv1alpha1.Target{} + err := m.client.Get(ctx, types.NamespacedName{ + Name: msg.Target.Name, + Namespace: m.targetSource.Namespace, + }, existing) + if err != nil { + logger.Error(err, "error fetching existing target object") + } + + err = m.client.Delete(ctx, existing) + if err != nil { + logger.Error(err, "error deleting the object") + } + logger.Info(fmt.Sprintf("deleted target object %s/%s", m.targetSource.Namespace, msg.Target.Name)) + + case CREATE: target := &gnmicv1alpha1.Target{ ObjectMeta: metav1.ObjectMeta{ Name: msg.Target.Name, @@ -67,25 +86,34 @@ func (m *TargetManager) Run(ctx context.Context) error { logger.Error(err, "error creating target object") } logger.Info(fmt.Sprintf("created new target object %s/%s", target.ObjectMeta.Namespace, target.ObjectMeta.Name)) + + case UPDATE: + existing := &gnmicv1alpha1.Target{} + newSpec := gnmicv1alpha1.TargetSpec{ + Address: msg.Target.Address, + Profile: "default", + } + + err := m.client.Get(ctx, types.NamespacedName{ + Name: msg.Target.Name, + Namespace: m.targetSource.Namespace, + }, existing) + if err != nil { + logger.Error(err, "error fetching existing target object") + } + + existing.Spec = newSpec + + err = m.client.Update(ctx, existing) + if err != nil { + logger.Error(err, "error updating object") + } + logger.Info(fmt.Sprintf("updated existing target object %s/%s", existing.ObjectMeta.Namespace, existing.ObjectMeta.Name)) + + default: + logger.Error(nil, "unknown discovery event received") } } - - // List existing Target CRs owned by this TargetSource - // var existing gnmicv1alpha1.TargetList - // if err := m.client.List( - // ctx, - // &existing, - // client.MatchingLabels{ - // "gnmic.dev/targetsource": m.targetsource, - // }, - // ); err != nil { - // return err - // } - - // TODO: Target Lifecycle Management - // 1. Compare and determine which Targets to create/update/delete - // 2. Create/update/delete Target CRs accordingly - // 3. Update TargetSource status with sync results } } } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index a7b887c..79bc38b 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -74,6 +74,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request // If a targetsource loader exists, return immediately without starting // any new loader or target manager + // TODO: check for spec changes and handle running process accordingly if exists { return ctrl.Result{}, nil } @@ -105,72 +106,6 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request logger.Info("TargetSource pipeline started", "name", targetSource.Name) - // // Step 1: Get desired state from discovery source - // discoveredTargets, err := targetsource.FetchDiscoveryTargets(ctx, targetSource) - // if err != nil { - // logger.Error(err, "error getting discovered targets") - // return ctrl.Result{}, err - // } - - // // Step 2: Get current state from Kubernetes cluster (lookup by label of TargetSource) - // existingTargets, err := targetsource.FetchExistingTargets(ctx, r.Client, targetSource) - // if err != nil { - // logger.Error(err, "error fetching existing targets") - // return ctrl.Result{}, err - // } - - // // Step 3: Compute diff - // diff := targetsource.BuildDiff(existingTargets, discoveredTargets) - - // // Step 4: Iterate over each list and do create, update, delete respectively - // for _, t := range diff.ToCreate { - // err = controllerutil.SetControllerReference(&targetSource, &t, r.Scheme) - // if err != nil { - // logger.Error(err, "error setting the owner reference") - // return ctrl.Result{}, err - // } - - // err = r.Client.Create(ctx, &t) - // if err != nil { - // logger.Error(err, "error creating target object") - // return ctrl.Result{}, err - // } - // logger.Info(fmt.Sprintf("created new target object %s/%s", t.ObjectMeta.Namespace, t.ObjectMeta.Name)) - // } - - // for _, t := range diff.ToUpdate { - // existing := &gnmicv1alpha1.Target{} - - // err := r.Get(ctx, types.NamespacedName{ - // Name: t.ObjectMeta.Name, - // Namespace: t.ObjectMeta.Namespace, - // }, existing) - - // if err != nil { - // logger.Error(err, "error fetching existing target object") - // return ctrl.Result{}, err - // } - - // existing.Spec = t.Spec - - // err = r.Update(ctx, existing) - // if err != nil { - // logger.Error(err, "error updating object") - // return ctrl.Result{}, err - // } - // logger.Info(fmt.Sprintf("updated existing target object %s/%s", t.ObjectMeta.Namespace, t.ObjectMeta.Name)) - // } - - // for _, t := range diff.ToDelete { - // err = r.Client.Delete(ctx, &t) - // logger.Info(fmt.Sprintf("resource name to be deleted: %s/%s", t.ObjectMeta.Namespace, t.ObjectMeta.Name)) - // if err != nil { - // logger.Error(err, "error deleting the object") - // return ctrl.Result{}, err - // } - // logger.Info(fmt.Sprintf("deleted target object %s/%s", t.ObjectMeta.Namespace, t.ObjectMeta.Name)) - // } - return ctrl.Result{}, nil } From 022dbaef5a3f604e0e52e0c82798febb26a525e6 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 15 Apr 2026 09:28:49 -0600 Subject: [PATCH 017/153] restructured project to introduce new architecture --- .../operator.gnmic.dev_targetsources.yaml | 2 + .../controller/targetsource/core/loaders.go | 20 +++++++ .../targetsource/core/loaders_test.go | 1 + .../targetsource/{ => core}/target_manager.go | 2 +- .../targetsource/{ => core}/types.go | 2 +- internal/controller/targetsource/factory.go | 24 +++++++++ internal/controller/targetsource/loaders.go | 54 ------------------- .../targetsource/loaders/http_pull/loader.go | 20 +++---- .../controller/targetsource/loaders_test.go | 1 - .../controller/targetsource_controller.go | 5 +- 10 files changed, 60 insertions(+), 71 deletions(-) create mode 100644 internal/controller/targetsource/core/loaders.go create mode 100644 internal/controller/targetsource/core/loaders_test.go rename internal/controller/targetsource/{ => core}/target_manager.go (99%) rename internal/controller/targetsource/{ => core}/types.go (97%) create mode 100644 internal/controller/targetsource/factory.go delete mode 100644 internal/controller/targetsource/loaders.go delete mode 100644 internal/controller/targetsource/loaders_test.go diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 1212ff9..0129a88 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -60,6 +60,8 @@ spec: type: string type: object type: object + type: + type: string required: - profile - provider diff --git a/internal/controller/targetsource/core/loaders.go b/internal/controller/targetsource/core/loaders.go new file mode 100644 index 0000000..7007349 --- /dev/null +++ b/internal/controller/targetsource/core/loaders.go @@ -0,0 +1,20 @@ +package core + +import ( + "context" +) + +// Loader defines a pluggable TargetSource loader interface +// Loaders observe external Sources of Truth and emit target snapshots through a channel +type Loader interface { + // Name returns the unique loader identifier e.g. "http_pull" + Name() string + + // Start begins discovery and pushes target snapshots into the out channel + // The loader must stop cleanly when ctx is cancelled + Start( + ctx context.Context, + targetsourceName string, + out chan<- []DiscoveryMessage, + ) error +} diff --git a/internal/controller/targetsource/core/loaders_test.go b/internal/controller/targetsource/core/loaders_test.go new file mode 100644 index 0000000..9a8bc95 --- /dev/null +++ b/internal/controller/targetsource/core/loaders_test.go @@ -0,0 +1 @@ +package core diff --git a/internal/controller/targetsource/target_manager.go b/internal/controller/targetsource/core/target_manager.go similarity index 99% rename from internal/controller/targetsource/target_manager.go rename to internal/controller/targetsource/core/target_manager.go index ebb3bf5..e41a9ad 100644 --- a/internal/controller/targetsource/target_manager.go +++ b/internal/controller/targetsource/core/target_manager.go @@ -1,4 +1,4 @@ -package targetsource +package core import ( "context" diff --git a/internal/controller/targetsource/types.go b/internal/controller/targetsource/core/types.go similarity index 97% rename from internal/controller/targetsource/types.go rename to internal/controller/targetsource/core/types.go index 6c2b283..2b39606 100644 --- a/internal/controller/targetsource/types.go +++ b/internal/controller/targetsource/core/types.go @@ -1,4 +1,4 @@ -package targetsource +package core import ( "k8s.io/apimachinery/pkg/runtime" diff --git a/internal/controller/targetsource/factory.go b/internal/controller/targetsource/factory.go new file mode 100644 index 0000000..1421390 --- /dev/null +++ b/internal/controller/targetsource/factory.go @@ -0,0 +1,24 @@ +package targetsource + +import ( + "fmt" + + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" + "github.com/gnmic/operator/internal/controller/targetsource/core" + "github.com/gnmic/operator/internal/controller/targetsource/loaders/http_pull" +) + +// NewLoader creates a loader by name +func NewLoader(name string, namespace string, spec gnmicv1alpha1.TargetSourceSpec) (core.Loader, error) { + loaderName := namespace + "/" + name + + switch { + case spec.Provider.HTTP != nil: + return http_pull.New(), nil + case spec.Provider.Consul != nil: + return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", loaderName) + default: + return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", loaderName) + } + +} diff --git a/internal/controller/targetsource/loaders.go b/internal/controller/targetsource/loaders.go deleted file mode 100644 index b591acd..0000000 --- a/internal/controller/targetsource/loaders.go +++ /dev/null @@ -1,54 +0,0 @@ -package targetsource - -import ( - "context" - "fmt" - "sync" - - gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" -) - -// Loader defines a pluggable TargetSource loader interface -// Loaders observe external Sources of Truth and emit target snapshots through a channel -type Loader interface { - // Name returns the unique loader identifier e.g. "http_pull" - Name() string - - // Start begins discovery and pushes target snapshots into the out channel - // The loader must stop cleanly when ctx is cancelled - Start( - ctx context.Context, - targetsourceName string, - out chan<- []DiscoveryMessage, - ) error -} - -var ( - registryMu sync.RWMutex - registry = make(map[string]func() Loader) -) - -// Register registers a loader implementation -// It panics on duplicate registrations to fail fast during startup rather than at runtime -func Register(name string, factory func() Loader) { - registryMu.Lock() - defer registryMu.Unlock() - - if _, exists := registry[name]; exists { - panic(fmt.Sprintf("targetsource loader %q already registered", name)) - } - registry[name] = factory -} - -// NewLoader creates a loader by name -func NewLoader(name string, namespace string, spec gnmicv1alpha1.TargetSourceSpec) (Loader, error) { - registryMu.RLock() - defer registryMu.RUnlock() - - loaderName := namespace + "/" + name - factory, ok := registry[spec.Type] - if !ok { - return nil, fmt.Errorf("unknown targetsource loader: %q", loaderName) - } - return factory(), nil -} diff --git a/internal/controller/targetsource/loaders/http_pull/loader.go b/internal/controller/targetsource/loaders/http_pull/loader.go index 4157107..269db18 100644 --- a/internal/controller/targetsource/loaders/http_pull/loader.go +++ b/internal/controller/targetsource/loaders/http_pull/loader.go @@ -6,13 +6,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/gnmic/operator/internal/controller/targetsource" + "github.com/gnmic/operator/internal/controller/targetsource/core" ) type Loader struct{} // New instantiates the http_pull loader -func New() targetsource.Loader { +func New() core.Loader { return &Loader{} } @@ -23,7 +23,7 @@ func (l *Loader) Name() string { func (l *Loader) Start( ctx context.Context, targetsourceName string, - out chan<- []targetsource.DiscoveryMessage, + out chan<- []core.DiscoveryMessage, ) error { logger := log.FromContext(ctx).WithValues("loader", l.Name()) @@ -41,22 +41,22 @@ func (l *Loader) Start( case <-ticker.C: // Example snapshot (placeholder) - targets := []targetsource.DiscoveryMessage{ + targets := []core.DiscoveryMessage{ { - Target: targetsource.DiscoveredTarget{ + Target: core.DiscoveredTarget{ Name: "ceos1", Address: "clab-3-nodes-ceos1:6030", Labels: map[string]string{"TargetSource": targetsourceName}, }, - Event: targetsource.CREATE, + Event: core.CREATE, }, { - Target: targetsource.DiscoveredTarget{ + Target: core.DiscoveredTarget{ Name: "leaf1", Address: "clab-3-nodes-leaf1:57400", Labels: map[string]string{"TargetSource": targetsourceName}, }, - Event: targetsource.CREATE, + Event: core.CREATE, }, } @@ -74,7 +74,3 @@ func (l *Loader) Start( } } } - -func init() { - targetsource.Register("http_pull", New) -} diff --git a/internal/controller/targetsource/loaders_test.go b/internal/controller/targetsource/loaders_test.go deleted file mode 100644 index 603b690..0000000 --- a/internal/controller/targetsource/loaders_test.go +++ /dev/null @@ -1 +0,0 @@ -package targetsource diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 79bc38b..7980a9b 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -27,6 +27,7 @@ import ( gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/targetsource" + "github.com/gnmic/operator/internal/controller/targetsource/core" _ "github.com/gnmic/operator/internal/controller/targetsource/loaders/all" ) @@ -86,13 +87,13 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request runtimeCtx, cancel := context.WithCancel(context.Background()) - targetChannel := make(chan []targetsource.DiscoveryMessage, 10) + targetChannel := make(chan []core.DiscoveryMessage, 10) // start loader go loader.Start(runtimeCtx, targetSource.Name, targetChannel) // start target manager - manager := targetsource.NewTargetManager( + manager := core.NewTargetManager( r.Client, r.Scheme, &targetSource, From 4dc2eb31da6bc8ab0a8fab7faae2109ca8e596d4 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 15 Apr 2026 10:52:36 -0600 Subject: [PATCH 018/153] renamed targetsource package to discovery --- .../{targetsource => discovery}/client.go | 4 +++- .../core/loader_interface.go} | 0 .../{targetsource => discovery}/core/types.go | 15 --------------- .../factory.go => discovery/loader.go} | 6 +++--- .../loaders/all/all.go | 2 +- .../loaders/http_pull/loader.go | 2 +- .../loaders/http_pull/loader_test.go | 0 .../loaders/http_push/loader.go | 0 .../loaders/http_push/loader_test.go | 0 .../{targetsource => discovery}/mapper.go | 2 +- internal/controller/discovery/mapper_test.go | 1 + .../core => discovery}/target_manager.go | 19 ++++++++++++++----- .../targetsource/core/loaders_test.go | 1 - .../controller/targetsource/mapper_test.go | 1 - .../controller/targetsource_controller.go | 10 +++++----- 15 files changed, 29 insertions(+), 34 deletions(-) rename internal/controller/{targetsource => discovery}/client.go (79%) rename internal/controller/{targetsource/core/loaders.go => discovery/core/loader_interface.go} (100%) rename internal/controller/{targetsource => discovery}/core/types.go (52%) rename internal/controller/{targetsource/factory.go => discovery/loader.go} (77%) rename internal/controller/{targetsource => discovery}/loaders/all/all.go (57%) rename internal/controller/{targetsource => discovery}/loaders/http_pull/loader.go (95%) rename internal/controller/{targetsource => discovery}/loaders/http_pull/loader_test.go (100%) rename internal/controller/{targetsource => discovery}/loaders/http_push/loader.go (100%) rename internal/controller/{targetsource => discovery}/loaders/http_push/loader_test.go (100%) rename internal/controller/{targetsource => discovery}/mapper.go (98%) create mode 100644 internal/controller/discovery/mapper_test.go rename internal/controller/{targetsource/core => discovery}/target_manager.go (87%) delete mode 100644 internal/controller/targetsource/core/loaders_test.go delete mode 100644 internal/controller/targetsource/mapper_test.go diff --git a/internal/controller/targetsource/client.go b/internal/controller/discovery/client.go similarity index 79% rename from internal/controller/targetsource/client.go rename to internal/controller/discovery/client.go index c918ba1..3bc7ef7 100644 --- a/internal/controller/targetsource/client.go +++ b/internal/controller/discovery/client.go @@ -1,4 +1,6 @@ -package targetsource +package discovery + +// File may become obsolete, depends on how the logic to compare desired vs. existing state will get implemented import ( "context" diff --git a/internal/controller/targetsource/core/loaders.go b/internal/controller/discovery/core/loader_interface.go similarity index 100% rename from internal/controller/targetsource/core/loaders.go rename to internal/controller/discovery/core/loader_interface.go diff --git a/internal/controller/targetsource/core/types.go b/internal/controller/discovery/core/types.go similarity index 52% rename from internal/controller/targetsource/core/types.go rename to internal/controller/discovery/core/types.go index 2b39606..406a22b 100644 --- a/internal/controller/targetsource/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -1,12 +1,5 @@ package core -import ( - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" -) - // DiscoveredTarget represents a target discovered from an external source // before it is materialized as a Kubernetes Target CR type DiscoveredTarget struct { @@ -27,11 +20,3 @@ type DiscoveryMessage struct { Target DiscoveredTarget Event DiscoveryEvent } - -// TargetManager consumes discovered targets and applies them to Kubernetes. -type TargetManager struct { - client client.Client - scheme *runtime.Scheme - targetSource *gnmicv1alpha1.TargetSource - in <-chan []DiscoveryMessage -} diff --git a/internal/controller/targetsource/factory.go b/internal/controller/discovery/loader.go similarity index 77% rename from internal/controller/targetsource/factory.go rename to internal/controller/discovery/loader.go index 1421390..ad1e83f 100644 --- a/internal/controller/targetsource/factory.go +++ b/internal/controller/discovery/loader.go @@ -1,11 +1,11 @@ -package targetsource +package discovery import ( "fmt" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" - "github.com/gnmic/operator/internal/controller/targetsource/core" - "github.com/gnmic/operator/internal/controller/targetsource/loaders/http_pull" + "github.com/gnmic/operator/internal/controller/discovery/core" + "github.com/gnmic/operator/internal/controller/discovery/loaders/http_pull" ) // NewLoader creates a loader by name diff --git a/internal/controller/targetsource/loaders/all/all.go b/internal/controller/discovery/loaders/all/all.go similarity index 57% rename from internal/controller/targetsource/loaders/all/all.go rename to internal/controller/discovery/loaders/all/all.go index 629c5d9..c53b98a 100644 --- a/internal/controller/targetsource/loaders/all/all.go +++ b/internal/controller/discovery/loaders/all/all.go @@ -1,6 +1,6 @@ package all import ( - _ "github.com/gnmic/operator/internal/controller/targetsource/loaders/http_pull" + _ "github.com/gnmic/operator/internal/controller/discovery/loaders/http_pull" // _ "github.com/gnmic/operator/internal/controller/targetsource/loaders/http_push" ) diff --git a/internal/controller/targetsource/loaders/http_pull/loader.go b/internal/controller/discovery/loaders/http_pull/loader.go similarity index 95% rename from internal/controller/targetsource/loaders/http_pull/loader.go rename to internal/controller/discovery/loaders/http_pull/loader.go index 269db18..e987d78 100644 --- a/internal/controller/targetsource/loaders/http_pull/loader.go +++ b/internal/controller/discovery/loaders/http_pull/loader.go @@ -6,7 +6,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/gnmic/operator/internal/controller/targetsource/core" + "github.com/gnmic/operator/internal/controller/discovery/core" ) type Loader struct{} diff --git a/internal/controller/targetsource/loaders/http_pull/loader_test.go b/internal/controller/discovery/loaders/http_pull/loader_test.go similarity index 100% rename from internal/controller/targetsource/loaders/http_pull/loader_test.go rename to internal/controller/discovery/loaders/http_pull/loader_test.go diff --git a/internal/controller/targetsource/loaders/http_push/loader.go b/internal/controller/discovery/loaders/http_push/loader.go similarity index 100% rename from internal/controller/targetsource/loaders/http_push/loader.go rename to internal/controller/discovery/loaders/http_push/loader.go diff --git a/internal/controller/targetsource/loaders/http_push/loader_test.go b/internal/controller/discovery/loaders/http_push/loader_test.go similarity index 100% rename from internal/controller/targetsource/loaders/http_push/loader_test.go rename to internal/controller/discovery/loaders/http_push/loader_test.go diff --git a/internal/controller/targetsource/mapper.go b/internal/controller/discovery/mapper.go similarity index 98% rename from internal/controller/targetsource/mapper.go rename to internal/controller/discovery/mapper.go index dbdfbcf..6f8dbf0 100644 --- a/internal/controller/targetsource/mapper.go +++ b/internal/controller/discovery/mapper.go @@ -1,4 +1,4 @@ -package targetsource +package discovery // This file makes diff between existing and new targets // file decides which targets to create/update/delete diff --git a/internal/controller/discovery/mapper_test.go b/internal/controller/discovery/mapper_test.go new file mode 100644 index 0000000..5844159 --- /dev/null +++ b/internal/controller/discovery/mapper_test.go @@ -0,0 +1 @@ +package discovery diff --git a/internal/controller/targetsource/core/target_manager.go b/internal/controller/discovery/target_manager.go similarity index 87% rename from internal/controller/targetsource/core/target_manager.go rename to internal/controller/discovery/target_manager.go index e41a9ad..c13b1ef 100644 --- a/internal/controller/targetsource/core/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -1,4 +1,4 @@ -package core +package discovery import ( "context" @@ -12,10 +12,19 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" + "github.com/gnmic/operator/internal/controller/discovery/core" ) +// TargetManager consumes discovered targets and applies them to Kubernetes. +type TargetManager struct { + client client.Client + scheme *runtime.Scheme + targetSource *gnmicv1alpha1.TargetSource + in <-chan []core.DiscoveryMessage +} + // NewTargetManager wires a TargetManager instance. -func NewTargetManager(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []DiscoveryMessage) *TargetManager { +func NewTargetManager(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []core.DiscoveryMessage) *TargetManager { return &TargetManager{ client: c, scheme: s, @@ -46,7 +55,7 @@ func (m *TargetManager) Run(ctx context.Context) error { for _, msg := range messages { switch msg.Event { - case DELETE: + case core.DELETE: existing := &gnmicv1alpha1.Target{} err := m.client.Get(ctx, types.NamespacedName{ Name: msg.Target.Name, @@ -62,7 +71,7 @@ func (m *TargetManager) Run(ctx context.Context) error { } logger.Info(fmt.Sprintf("deleted target object %s/%s", m.targetSource.Namespace, msg.Target.Name)) - case CREATE: + case core.CREATE: target := &gnmicv1alpha1.Target{ ObjectMeta: metav1.ObjectMeta{ Name: msg.Target.Name, @@ -87,7 +96,7 @@ func (m *TargetManager) Run(ctx context.Context) error { } logger.Info(fmt.Sprintf("created new target object %s/%s", target.ObjectMeta.Namespace, target.ObjectMeta.Name)) - case UPDATE: + case core.UPDATE: existing := &gnmicv1alpha1.Target{} newSpec := gnmicv1alpha1.TargetSpec{ Address: msg.Target.Address, diff --git a/internal/controller/targetsource/core/loaders_test.go b/internal/controller/targetsource/core/loaders_test.go deleted file mode 100644 index 9a8bc95..0000000 --- a/internal/controller/targetsource/core/loaders_test.go +++ /dev/null @@ -1 +0,0 @@ -package core diff --git a/internal/controller/targetsource/mapper_test.go b/internal/controller/targetsource/mapper_test.go deleted file mode 100644 index 603b690..0000000 --- a/internal/controller/targetsource/mapper_test.go +++ /dev/null @@ -1 +0,0 @@ -package targetsource diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 7980a9b..64c9909 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -26,9 +26,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" - "github.com/gnmic/operator/internal/controller/targetsource" - "github.com/gnmic/operator/internal/controller/targetsource/core" - _ "github.com/gnmic/operator/internal/controller/targetsource/loaders/all" + "github.com/gnmic/operator/internal/controller/discovery" + "github.com/gnmic/operator/internal/controller/discovery/core" + _ "github.com/gnmic/operator/internal/controller/discovery/loaders/all" ) type runningSource struct { @@ -80,7 +80,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, nil } - loader, err := targetsource.NewLoader(targetSource.ObjectMeta.Name, targetSource.ObjectMeta.Namespace, targetSource.Spec) // TODO: pass configuration to loader based on spec + loader, err := discovery.NewLoader(targetSource.ObjectMeta.Name, targetSource.ObjectMeta.Namespace, targetSource.Spec) // TODO: pass configuration to loader based on spec if err != nil { return ctrl.Result{}, err } @@ -93,7 +93,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request go loader.Start(runtimeCtx, targetSource.Name, targetChannel) // start target manager - manager := core.NewTargetManager( + manager := discovery.NewTargetManager( r.Client, r.Scheme, &targetSource, From ad172c929ed5a6052f27d7b4be21820f51255aec Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 15 Apr 2026 11:44:38 -0600 Subject: [PATCH 019/153] removed unnecessary files and updated gitignore --- .gitignore | 1 + lab/dev/3-nodes.clab.yaml.annotations.json | 35 ---------------------- lab/dev/netbox/readme.txt | 3 -- lab/dev/temp | 0 4 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 lab/dev/3-nodes.clab.yaml.annotations.json delete mode 100644 lab/dev/netbox/readme.txt delete mode 100644 lab/dev/temp diff --git a/.gitignore b/.gitignore index c04366f..29d31af 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ Dockerfile.cross *.swo *~ private/ +lab/dev/**/*.annotations.json lab/dev/clab-* lab/dev/netbox/secrets design/ diff --git a/lab/dev/3-nodes.clab.yaml.annotations.json b/lab/dev/3-nodes.clab.yaml.annotations.json deleted file mode 100644 index 9188d4c..0000000 --- a/lab/dev/3-nodes.clab.yaml.annotations.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "freeTextAnnotations": [], - "freeShapeAnnotations": [], - "groupStyleAnnotations": [], - "networkNodeAnnotations": [], - "nodeAnnotations": [ - { - "id": "spine1", - "interfacePattern": "e1-{n}", - "position": { - "x": 360, - "y": 380 - } - }, - { - "id": "leaf1", - "interfacePattern": "e1-{n}", - "position": { - "x": 480, - "y": 260 - } - }, - { - "id": "leaf2", - "interfacePattern": "e1-{n}", - "position": { - "x": 520, - "y": 400 - } - } - ], - "edgeAnnotations": [], - "aliasEndpointAnnotations": [], - "viewerSettings": {} -} \ No newline at end of file diff --git a/lab/dev/netbox/readme.txt b/lab/dev/netbox/readme.txt deleted file mode 100644 index c0edbe1..0000000 --- a/lab/dev/netbox/readme.txt +++ /dev/null @@ -1,3 +0,0 @@ -# All files within operator/lab/dev/netbox are -# only for development and testing purposes -# is generally vibe coded and will be removed after development \ No newline at end of file diff --git a/lab/dev/temp b/lab/dev/temp deleted file mode 100644 index e69de29..0000000 From 181ea95a26c2f88be3b8b01f0b4e6de0c176d39d Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 16 Apr 2026 12:29:50 +0000 Subject: [PATCH 020/153] extend TargetSource CRD by http token --- api/v1alpha1/targetsource_types.go | 3 ++- config/crd/bases/operator.gnmic.dev_targetsources.yaml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 3cf029b..057bbb2 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -39,7 +39,8 @@ type ProviderSpec struct { } type HTTPConfig struct { - URL string `json:"url,omitempty"` + URL string `json:"url,omitempty"` + Token string `json:"token,omitempty"` } type ConsulConfig struct { diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 0129a88..7aa6084 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -56,6 +56,8 @@ spec: type: object http: properties: + token: + type: string url: type: string type: object From 2fddddf3e33ba4112550c59b59583729e548bc0d Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 16 Apr 2026 12:30:08 +0000 Subject: [PATCH 021/153] add pull logic as poc --- .../discovery/loaders/http_pull/loader.go | 74 +++++++++++++------ 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/internal/controller/discovery/loaders/http_pull/loader.go b/internal/controller/discovery/loaders/http_pull/loader.go index 22868b2..7162119 100644 --- a/internal/controller/discovery/loaders/http_pull/loader.go +++ b/internal/controller/discovery/loaders/http_pull/loader.go @@ -2,6 +2,9 @@ package http_pull import ( "context" + "encoding/json" + "fmt" + "net/http" "time" "sigs.k8s.io/controller-runtime/pkg/log" @@ -31,6 +34,10 @@ func (l *Loader) Start( logger.Info("HTTP pull loader started") + client := &http.Client{ + Timeout: 30 * time.Second, + } + // Only for debugging: emit a static snapshot every 30 seconds ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() @@ -42,32 +49,26 @@ func (l *Loader) Start( return nil case <-ticker.C: - // Example snapshot (placeholder) - targets := []core.DiscoveryMessage{ - { - Target: core.DiscoveredTarget{ - Name: "ceos1", - Address: "clab-3-nodes-ceos1:6030", - Labels: map[string]string{"TargetSource": targetsourceName}, - }, - Event: core.CREATE, - }, - { - Target: core.DiscoveredTarget{ - Name: "leaf1", - Address: "clab-3-nodes-leaf1:57400", - Labels: map[string]string{"TargetSource": targetsourceName}, - }, - Event: core.CREATE, - }, + targets, err := l.fetchTargetsFromHTTPEndpoint(ctx, client, spec.Provider.HTTP.URL, spec.Provider.HTTP.Token) + if err != nil { + logger.Error(err, "failed to fetch targets from HTTP endpoint") + continue + } + + var messages []core.DiscoveryMessage + for _, target := range targets { + messages = append(messages, core.DiscoveryMessage{ + Target: target, + Event: core.CREATE, + }) } // Non-blocking context-aware send select { - case out <- targets: - logger.V(1).Info( + case out <- messages: + logger.Info( "emitted target snapshot", - "count", len(targets), + "count", len(messages), ) case <-ctx.Done(): logger.Info("context cancelled while emitting targets") @@ -76,3 +77,34 @@ func (l *Loader) Start( } } } + +func (l *Loader) fetchTargetsFromHTTPEndpoint(ctx context.Context, client *http.Client, url string, token string) ([]core.DiscoveredTarget, error) { + req, err := http.NewRequestWithContext( + ctx, + http.MethodGet, + url, + nil, + ) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Token +"+token) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var targets []core.DiscoveredTarget + if err := json.NewDecoder(resp.Body).Decode(&targets); err != nil { + return nil, err + } + + return targets, nil +} From 64a83cd28ab91846126ae612f09f0292cd2fd62d Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 16 Apr 2026 13:47:48 +0000 Subject: [PATCH 022/153] fix request header typo --- internal/controller/discovery/loaders/http_pull/loader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/discovery/loaders/http_pull/loader.go b/internal/controller/discovery/loaders/http_pull/loader.go index 7162119..7bd0bd1 100644 --- a/internal/controller/discovery/loaders/http_pull/loader.go +++ b/internal/controller/discovery/loaders/http_pull/loader.go @@ -89,7 +89,7 @@ func (l *Loader) fetchTargetsFromHTTPEndpoint(ctx context.Context, client *http. return nil, err } req.Header.Set("Accept", "application/json") - req.Header.Set("Authorization", "Token +"+token) + req.Header.Set("Authorization", "Token "+token) resp, err := client.Do(req) if err != nil { From 98823e83dc124853258357e34c4e1571dafe66a5 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 16 Apr 2026 14:25:18 +0000 Subject: [PATCH 023/153] refactor pull implementation --- .../discovery/loaders/http_pull/loader.go | 107 +++++++++++------- 1 file changed, 66 insertions(+), 41 deletions(-) diff --git a/internal/controller/discovery/loaders/http_pull/loader.go b/internal/controller/discovery/loaders/http_pull/loader.go index 7bd0bd1..fb081f8 100644 --- a/internal/controller/discovery/loaders/http_pull/loader.go +++ b/internal/controller/discovery/loaders/http_pull/loader.go @@ -3,6 +3,7 @@ package http_pull import ( "context" "encoding/json" + "errors" "fmt" "net/http" "time" @@ -13,9 +14,14 @@ import ( "github.com/gnmic/operator/internal/controller/discovery/core" ) +const ( + defaultPollInterval = 30 * time.Second +) + +// Loader implements the HTTP pull discovery mechanism type Loader struct{} -// New instantiates the http_pull loader +// New returns a new http_pull loader instance func New() core.Loader { return &Loader{} } @@ -30,18 +36,59 @@ func (l *Loader) Start( spec gnmicv1alpha1.TargetSourceSpec, out chan<- []core.DiscoveryMessage, ) error { - logger := log.FromContext(ctx).WithValues("loader", l.Name()) + logger := log.FromContext(ctx).WithValues( + "loader", l.Name(), + "targetSource", targetsourceName, + ) - logger.Info("HTTP pull loader started") + // Input Validation of spec + if spec.Provider == nil || spec.Provider.HTTP == nil { + return errors.New("http_pull loader requires spec.provider.http to be set") + } client := &http.Client{ Timeout: 30 * time.Second, } - // Only for debugging: emit a static snapshot every 30 seconds - ticker := time.NewTicker(30 * time.Second) + interval := defaultPollInterval + ticker := time.NewTicker(interval) defer ticker.Stop() + logger.Info("HTTP pull loader started", "interval", interval.String()) + + // helper function to fetch targets and emit discovery messages + fetchAndEmit := func() { + targets, err := l.fetchTargetsFromHTTPEndpoint( + ctx, + client, + spec.Provider.HTTP.URL, + spec.Provider.HTTP.Token, + ) + if err != nil { + logger.Error(err, "failed to fetch targets from HTTP endpoint") + return + } + + messages := make([]core.DiscoveryMessage, 0, len(targets)) + for _, target := range targets { + messages = append(messages, core.DiscoveryMessage{ + Target: target, + Event: core.CREATE, + }) + } + + select { + case out <- messages: + logger.Info("emitted target snapshot", "count", len(messages)) + case <-ctx.Done(): + logger.Info("context cancelled while emitting targets") + } + } + + // Immediate fetch on startup + fetchAndEmit() + + // Periodic fetch for { select { case <-ctx.Done(): @@ -49,61 +96,39 @@ func (l *Loader) Start( return nil case <-ticker.C: - targets, err := l.fetchTargetsFromHTTPEndpoint(ctx, client, spec.Provider.HTTP.URL, spec.Provider.HTTP.Token) - if err != nil { - logger.Error(err, "failed to fetch targets from HTTP endpoint") - continue - } - - var messages []core.DiscoveryMessage - for _, target := range targets { - messages = append(messages, core.DiscoveryMessage{ - Target: target, - Event: core.CREATE, - }) - } - - // Non-blocking context-aware send - select { - case out <- messages: - logger.Info( - "emitted target snapshot", - "count", len(messages), - ) - case <-ctx.Done(): - logger.Info("context cancelled while emitting targets") - return nil - } + fetchAndEmit() } } } -func (l *Loader) fetchTargetsFromHTTPEndpoint(ctx context.Context, client *http.Client, url string, token string) ([]core.DiscoveredTarget, error) { - req, err := http.NewRequestWithContext( - ctx, - http.MethodGet, - url, - nil, - ) +func (l *Loader) fetchTargetsFromHTTPEndpoint( + ctx context.Context, + client *http.Client, + url string, + token string, +) ([]core.DiscoveredTarget, error) { + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - return nil, err + return nil, fmt.Errorf("creating HTTP request failed: %w", err) } + req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Token "+token) resp, err := client.Do(req) if err != nil { - return nil, err + return nil, fmt.Errorf("HTTP request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + return nil, fmt.Errorf("unexpected HTTP status code: %d", resp.StatusCode) } var targets []core.DiscoveredTarget if err := json.NewDecoder(resp.Body).Decode(&targets); err != nil { - return nil, err + return nil, fmt.Errorf("failed to decode HTTP response: %w", err) } return targets, nil From e76c6f35c7d9bf06ae79a7677e09e78cc5bedebf Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 17 Apr 2026 16:10:07 +0000 Subject: [PATCH 024/153] restructure discovery structs --- .../discovery/core/loader_interface.go | 2 +- .../discovery/core/message_interface.go | 8 ++++++++ internal/controller/discovery/core/types.go | 19 +++++++++++++------ 3 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 internal/controller/discovery/core/message_interface.go diff --git a/internal/controller/discovery/core/loader_interface.go b/internal/controller/discovery/core/loader_interface.go index 2b87a0a..f8e343b 100644 --- a/internal/controller/discovery/core/loader_interface.go +++ b/internal/controller/discovery/core/loader_interface.go @@ -12,7 +12,7 @@ type Loader interface { // Name returns the unique loader identifier e.g. "http_pull" Name() string - // Start begins discovery and pushes target snapshots into the out channel + // Start begins discovery and pushes target snapshots or events into the out channel // The loader must stop cleanly when ctx is cancelled Start( ctx context.Context, diff --git a/internal/controller/discovery/core/message_interface.go b/internal/controller/discovery/core/message_interface.go new file mode 100644 index 0000000..0836bc6 --- /dev/null +++ b/internal/controller/discovery/core/message_interface.go @@ -0,0 +1,8 @@ +package core + +type DiscoveryMessage interface { + isDiscoveryMessage() +} + +func (DiscoveryEvent) isDiscoveryMessage() {} +func (DiscoverySnapshot) isDiscoveryMessage() {} diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 406a22b..f56eaa2 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -9,14 +9,21 @@ type DiscoveredTarget struct { } const ( - DELETE DiscoveryEvent = 0 - CREATE DiscoveryEvent = 1 - UPDATE DiscoveryEvent = 2 + DELETE EventAction = 0 + CREATE EventAction = 1 + UPDATE EventAction = 2 ) -type DiscoveryEvent int +type EventAction int -type DiscoveryMessage struct { +type DiscoveryEvent struct { Target DiscoveredTarget - Event DiscoveryEvent + Event EventAction +} + +type DiscoverySnapshot struct { + Target []DiscoveredTarget + Event EventAction + SnapshotID string + IsLastChunk bool } From 3c18fb54fbb78db867ebba48ef3ff7e0b58e5e0a Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 17 Apr 2026 16:11:42 +0000 Subject: [PATCH 025/153] offload sending logic from loader implementation --- internal/controller/discovery/core/sender.go | 69 +++++++++++++++++++ .../discovery/loaders/http_pull/loader.go | 45 ++++++------ 2 files changed, 90 insertions(+), 24 deletions(-) create mode 100644 internal/controller/discovery/core/sender.go diff --git a/internal/controller/discovery/core/sender.go b/internal/controller/discovery/core/sender.go new file mode 100644 index 0000000..84de206 --- /dev/null +++ b/internal/controller/discovery/core/sender.go @@ -0,0 +1,69 @@ +package core + +import ( + "context" +) + +// sendMessages sends discovery messages over a channel in a context-aware manner +func sendMessages(ctx context.Context, out chan<- []DiscoveryMessage, messages []DiscoveryMessage) error { + select { + case <-ctx.Done(): + return ctx.Err() + case out <- messages: + } + return nil +} + +// createDiscoverySnapshots takes a list of discovered targets and returns chunked DiscoverySnapshots +func createDiscoverySnapshots(targets []DiscoveredTarget, snapshotID string, chunkSize int) []DiscoverySnapshot { + if chunkSize <= 0 { + chunkSize = 1 + } + + var snapshots []DiscoverySnapshot + totalTargets := len(targets) + + for i := 0; i < totalTargets; i += chunkSize { + end := i + chunkSize + if end > totalTargets { + end = totalTargets + } + + chunk := targets[i:end] + snapshots = append(snapshots, DiscoverySnapshot{ + Target: chunk, + SnapshotID: snapshotID, + IsLastChunk: (end == totalTargets), + }) + } + + return snapshots +} + +// SendSnapshot sends discovered targets as a snapshot over a channel in chunks +func SendSnapshot(ctx context.Context, out chan<- []DiscoveryMessage, targets []DiscoveredTarget, snapshotID string, chunkSize int) error { + snapshots := createDiscoverySnapshots(targets, snapshotID, chunkSize) + + for _, snapshot := range snapshots { + // Convert DiscoverySnapshot to DiscoveryMessage interface + messages := make([]DiscoveryMessage, 1) + messages[0] = snapshot + + if err := sendMessages(ctx, out, messages); err != nil { + return err + } + } + + return nil +} + +// SendEvents sends discovery messages over channel in a context-aware manner +func SendEvents(ctx context.Context, out chan<- []DiscoveryMessage, events []DiscoveryEvent) error { + // Convert DiscoveryEvent slice to DiscoveryMessage slice + messages := make([]DiscoveryMessage, len(events)) + for i, msg := range events { + messages[i] = msg + } + + return sendMessages(ctx, out, messages) +} diff --git a/internal/controller/discovery/loaders/http_pull/loader.go b/internal/controller/discovery/loaders/http_pull/loader.go index 22868b2..94660d0 100644 --- a/internal/controller/discovery/loaders/http_pull/loader.go +++ b/internal/controller/discovery/loaders/http_pull/loader.go @@ -2,12 +2,18 @@ package http_pull import ( "context" + "fmt" "time" "sigs.k8s.io/controller-runtime/pkg/log" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery/core" + "github.com/google/uuid" +) + +const ( + chunkSize = 100 ) type Loader struct{} @@ -27,7 +33,11 @@ func (l *Loader) Start( spec gnmicv1alpha1.TargetSourceSpec, out chan<- []core.DiscoveryMessage, ) error { - logger := log.FromContext(ctx).WithValues("loader", l.Name()) + logger := log.FromContext(ctx).WithValues( + "component", "loader", + "name", l.Name(), + "targetsource", targetsourceName, + ) logger.Info("HTTP pull loader started") @@ -43,35 +53,22 @@ func (l *Loader) Start( case <-ticker.C: // Example snapshot (placeholder) - targets := []core.DiscoveryMessage{ + snapshotID := fmt.Sprintf("snapshot-%s-%s", targetsourceName, uuid.NewString()) + targets := []core.DiscoveredTarget{ { - Target: core.DiscoveredTarget{ - Name: "ceos1", - Address: "clab-3-nodes-ceos1:6030", - Labels: map[string]string{"TargetSource": targetsourceName}, - }, - Event: core.CREATE, + Name: "ceos1", + Address: "clab-3-nodes-ceos1:6030", + Labels: map[string]string{"TargetSource": targetsourceName}, }, { - Target: core.DiscoveredTarget{ - Name: "leaf1", - Address: "clab-3-nodes-leaf1:57400", - Labels: map[string]string{"TargetSource": targetsourceName}, - }, - Event: core.CREATE, + Name: "leaf1", + Address: "clab-3-nodes-leaf1:57400", + Labels: map[string]string{"TargetSource": targetsourceName}, }, } - // Non-blocking context-aware send - select { - case out <- targets: - logger.V(1).Info( - "emitted target snapshot", - "count", len(targets), - ) - case <-ctx.Done(): - logger.Info("context cancelled while emitting targets") - return nil + if err := core.SendSnapshot(ctx, out, targets, snapshotID, chunkSize); err != nil { + return err } } } From 86ab0f35818be90b429177c013a78b7c3fed083f Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 17 Apr 2026 16:16:08 +0000 Subject: [PATCH 026/153] implement type assertion based on received message --- .../controller/discovery/target_manager.go | 75 +++++++++++++------ 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_manager.go index 245942d..f44e33c 100644 --- a/internal/controller/discovery/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -9,23 +9,26 @@ import ( gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery/core" + "github.com/go-logr/logr" ) -// TargetManager consumes discovered targets and applies them to Kubernetes. +// TargetManager consumes discovered targets and applies them to Kubernetes type TargetManager struct { client client.Client scheme *runtime.Scheme targetSource *gnmicv1alpha1.TargetSource in <-chan []core.DiscoveryMessage + collected map[string][]core.DiscoveredTarget } -// NewTargetManager wires a TargetManager instance. +// NewTargetManager wires a TargetManager instance func NewTargetManager(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []core.DiscoveryMessage) *TargetManager { return &TargetManager{ client: c, scheme: s, targetSource: ts, in: in, + collected: make(map[string][]core.DiscoveredTarget), } } @@ -43,28 +46,54 @@ func (m *TargetManager) Run(ctx context.Context) error { logger.Info("target manager stopped") return nil - case targets := <-m.in: - logger.Info( - "received discovered targets", - "count", len(targets), - ) + case messages := <-m.in: + for _, message := range messages { + // Type assert to determine if this is a snapshot or event + switch msg := message.(type) { + case core.DiscoverySnapshot: + // Collect snapshot chunks + logger.Info( + "received snapshot chunk", + "snapshotID", msg.SnapshotID, + "targetCount", len(msg.Target), + ) + m.collected[msg.SnapshotID] = append(m.collected[msg.SnapshotID], msg.Target...) + if msg.IsLastChunk { + m.processSnapshot(msg.SnapshotID, logger) + } - // List existing Target CRs owned by this TargetSource - // var existing gnmicv1alpha1.TargetList - // if err := m.client.List( - // ctx, - // &existing, - // client.MatchingLabels{ - // "gnmic.dev/targetsource": m.targetsource, - // }, - // ); err != nil { - // return err - // } - - // TODO: Target Lifecycle Management - // 1. Compare and determine which Targets to create/update/delete - // 2. Create/update/delete Target CRs accordingly - // 3. Update TargetSource status with sync results + case core.DiscoveryEvent: + // Process individual event-driven update + logger.Info( + "received discovery event", + "target", msg.Target.Name, + ) + switch msg.Event { + case core.CREATE: + logger.Info("Would create target", "name", msg.Target.Name, "address", msg.Target.Address, "labels", msg.Target.Labels) + case core.UPDATE: + logger.Info("Would update target", "name", msg.Target.Name, "address", msg.Target.Address, "labels", msg.Target.Labels) + case core.DELETE: + logger.Info("Would delete target", "name", msg.Target.Name) + } + } + } } } } + +// processSnapshot takes a complete snapshot of discovered targets and reconciles Target CRs accordingly +func (m *TargetManager) processSnapshot(snapshotID string, logger logr.Logger) { + targets := m.collected[snapshotID] + delete(m.collected, snapshotID) + + logger.Info("Processing full snapshot", "snapshotID", snapshotID, "totalTargets", len(targets)) + + if m.targetSource.Spec.Provider.HTTP != nil { + logger.Info("Would delete all existing targets for targetsource", "targetsource", m.targetSource.Name) + } + + for _, target := range targets { + logger.Info("Would create target", "name", target.Name, "address", target.Address, "labels", target.Labels) + } +} From 8b36d7dd34e1200a50cc1c9c1176e9cbfbf97371 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 17 Apr 2026 17:28:51 +0000 Subject: [PATCH 027/153] add http_push skeleton --- .../discovery/loaders/http_push/loader.go | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/internal/controller/discovery/loaders/http_push/loader.go b/internal/controller/discovery/loaders/http_push/loader.go index 95dc1e9..2e4ae0e 100644 --- a/internal/controller/discovery/loaders/http_push/loader.go +++ b/internal/controller/discovery/loaders/http_push/loader.go @@ -1,4 +1,53 @@ package http_push +import ( + "context" + "errors" + + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" + "github.com/gnmic/operator/internal/controller/discovery/core" + "sigs.k8s.io/controller-runtime/pkg/log" +) + // this file implements the logic receive target updates via HTTP push // REST API defined internal/apiserver + +// Loader implements the HTTP pull discovery mechanism +type Loader struct{} + +// New returns a new http_pull loader instance +func New() core.Loader { + return &Loader{} +} + +func (l *Loader) Name() string { + return "http_push" +} + +func (l *Loader) Start( + ctx context.Context, + targetsourceName string, + spec gnmicv1alpha1.TargetSourceSpec, + out chan<- []core.DiscoveryMessage, +) error { + logger := log.FromContext(ctx).WithValues( + "component", "loader", + "name", l.Name(), + "targetsource", targetsourceName, + ) + logger.Info("HTTP push loader started") + + // Input Validation of spec + if spec.Provider == nil || spec.Provider.HTTP == nil { + return errors.New("http_push loader requires spec.provider.http to be set") + } + + // Receive target updates via HTTP push + var targetEvents []core.DiscoveryEvent + + if err := core.SendEvents(ctx, out, targetEvents); err != nil { + logger.Error(err, "failed to send events") + return nil + } + return nil +} From efbf727aed95de42e0a582333e90262689a2a3e5 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 17 Apr 2026 17:30:22 +0000 Subject: [PATCH 028/153] add http_push skeleton --- .../discovery/loaders/http_push/loader.go | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/internal/controller/discovery/loaders/http_push/loader.go b/internal/controller/discovery/loaders/http_push/loader.go index 95dc1e9..2e4ae0e 100644 --- a/internal/controller/discovery/loaders/http_push/loader.go +++ b/internal/controller/discovery/loaders/http_push/loader.go @@ -1,4 +1,53 @@ package http_push +import ( + "context" + "errors" + + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" + "github.com/gnmic/operator/internal/controller/discovery/core" + "sigs.k8s.io/controller-runtime/pkg/log" +) + // this file implements the logic receive target updates via HTTP push // REST API defined internal/apiserver + +// Loader implements the HTTP pull discovery mechanism +type Loader struct{} + +// New returns a new http_pull loader instance +func New() core.Loader { + return &Loader{} +} + +func (l *Loader) Name() string { + return "http_push" +} + +func (l *Loader) Start( + ctx context.Context, + targetsourceName string, + spec gnmicv1alpha1.TargetSourceSpec, + out chan<- []core.DiscoveryMessage, +) error { + logger := log.FromContext(ctx).WithValues( + "component", "loader", + "name", l.Name(), + "targetsource", targetsourceName, + ) + logger.Info("HTTP push loader started") + + // Input Validation of spec + if spec.Provider == nil || spec.Provider.HTTP == nil { + return errors.New("http_push loader requires spec.provider.http to be set") + } + + // Receive target updates via HTTP push + var targetEvents []core.DiscoveryEvent + + if err := core.SendEvents(ctx, out, targetEvents); err != nil { + logger.Error(err, "failed to send events") + return nil + } + return nil +} From 60a5eb3a34a741077ec465b20266ecc58eecc59b Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 17 Apr 2026 17:43:15 +0000 Subject: [PATCH 029/153] refactor targetsource_controller.go --- .../controller/targetsource_controller.go | 130 +++++++++++------- lab/dev/resources/targetsources/ctest1.yaml | 3 +- 2 files changed, 83 insertions(+), 50 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 8cd6f68..9fb587f 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -55,92 +55,124 @@ type TargetSourceReconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx) + logger := log.FromContext(ctx).WithValues( + "Name", req.NamespacedName, + ) + targetSource, err := r.getTargetSource(ctx, req.NamespacedName) + if err != nil { + return ctrl.Result{}, err + } + + // Handle deletion with finalizer + if !targetSource.DeletionTimestamp.IsZero() { + return r.handleTargetSourceDeletion(ctx, req.NamespacedName, targetSource) + } + + // Ensure finalizer is set + if err := r.ensureFinalizer(ctx, targetSource); err != nil { + return ctrl.Result{}, err + } + + // Check if pipeline is already running + if r.isPipelineRunning(req.NamespacedName) { + return ctrl.Result{}, nil + } + + // Start discovery pipeline + if err := r.startDiscoveryPipeline(req.NamespacedName, targetSource); err != nil { + return ctrl.Result{}, err + } + + logger.Info("TargetSource pipeline started") + return ctrl.Result{}, nil +} + +// getTargetSource retrieves a TargetSource by name, handling cleanup if not found +func (r *TargetSourceReconciler) getTargetSource(ctx context.Context, key client.ObjectKey) (*gnmicv1alpha1.TargetSource, error) { var targetSource gnmicv1alpha1.TargetSource - if err := r.Get(ctx, req.NamespacedName, &targetSource); err != nil { + if err := r.Get(ctx, key, &targetSource); err != nil { // If the TargetSource no longer exists, ensure runtime cleanup if client.IgnoreNotFound(err) == nil { - r.stopDiscovery(req.NamespacedName) + r.stopDiscovery(key) } - return ctrl.Result{}, client.IgnoreNotFound(err) + return nil, client.IgnoreNotFound(err) } + return &targetSource, nil +} - logger.Info("reconciling TargetSource", "name", targetSource.Name) - - // Handle deletion with finalizer - if !targetSource.DeletionTimestamp.IsZero() { - logger.Info("TargetSource is being deleted, stopping pipeline", "name", targetSource.Name) +// handleTargetSourceDeletion stops the discovery pipeline and removes the finalizer +func (r *TargetSourceReconciler) handleTargetSourceDeletion(ctx context.Context, key client.ObjectKey, targetSource *gnmicv1alpha1.TargetSource) (ctrl.Result, error) { + logger := log.FromContext(ctx) + logger.Info("TargetSource is being deleted, stopping pipeline", "name", targetSource.Name) - r.stopDiscovery(req.NamespacedName) + r.stopDiscovery(key) - // Remove finalizer if exists - if controllerutil.ContainsFinalizer(&targetSource, targetSourceFinalizer) { - controllerutil.RemoveFinalizer(&targetSource, targetSourceFinalizer) - if err := r.Update(ctx, &targetSource); err != nil { - return ctrl.Result{}, err - } + // Remove finalizer if exists + if controllerutil.ContainsFinalizer(targetSource, targetSourceFinalizer) { + controllerutil.RemoveFinalizer(targetSource, targetSourceFinalizer) + if err := r.Update(ctx, targetSource); err != nil { + return ctrl.Result{}, err } + } - return ctrl.Result{}, nil + return ctrl.Result{}, nil +} + +// ensureFinalizer adds the finalizer if not present and updates the TargetSource +func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSource *gnmicv1alpha1.TargetSource) error { + if controllerutil.ContainsFinalizer(targetSource, targetSourceFinalizer) { + return nil } - // Ensure finalizer is set - if !controllerutil.ContainsFinalizer(&targetSource, targetSourceFinalizer) { - controllerutil.AddFinalizer(&targetSource, targetSourceFinalizer) - if err := r.Update(ctx, &targetSource); err != nil { - return ctrl.Result{}, err - } - // Requeue to continue with a clean state - return ctrl.Result{}, nil + controllerutil.AddFinalizer(targetSource, targetSourceFinalizer) + if err := r.Update(ctx, targetSource); err != nil { + return err } - // TODO: - // 1. Check if a pipeline is already running for this TargetSource - // 2. If not, create and start a new pipeline: - // a. Create a Loader based on TargetSource spec - // b. Start the Loader in a new goroutine, passing a channel for discovered targets - // c. Start a TargetManager in another goroutine to consume discovered targets and manage Target CRs - // 3. If yes, check if the spec has changed and restart the pipeline if needed + return nil +} +// isPipelineRunning checks if a discovery pipeline is already running for the given key +func (r *TargetSourceReconciler) isPipelineRunning(key client.ObjectKey) bool { r.mu.Lock() - _, exists := r.running[req.NamespacedName] - r.mu.Unlock() + defer r.mu.Unlock() - // If a targetsource loader exists, return immediately without starting - // any new loader or target manager - if exists { - return ctrl.Result{}, nil - } + _, exists := r.running[key] + return exists +} - loader, err := discovery.NewLoader(targetSource.ObjectMeta.Name, targetSource.ObjectMeta.Namespace, targetSource.Spec) +// startDiscoveryPipeline creates and starts the loader and target manager +func (r *TargetSourceReconciler) startDiscoveryPipeline(key client.ObjectKey, targetSource *gnmicv1alpha1.TargetSource) error { + loader, err := discovery.NewLoader( + targetSource.ObjectMeta.Name, + targetSource.ObjectMeta.Namespace, + targetSource.Spec, + ) if err != nil { - return ctrl.Result{}, err + return err } runtimeCtx, cancel := context.WithCancel(context.Background()) - targetChannel := make(chan []core.DiscoveryMessage, 10) - // start loader + // Start loader go loader.Start(runtimeCtx, targetSource.Name, targetSource.Spec, targetChannel) - // start target manager + // Start target manager manager := discovery.NewTargetManager( r.Client, r.Scheme, - &targetSource, + targetSource, targetChannel, ) go manager.Run(runtimeCtx) r.mu.Lock() - r.running[req.NamespacedName] = runningSource{cancel: cancel} + r.running[key] = runningSource{cancel: cancel} r.mu.Unlock() - logger.Info("TargetSource pipeline started", "name", targetSource.Name) - - return ctrl.Result{}, nil + return nil } // stopDiscovery stops and removes a running discovery pipeline diff --git a/lab/dev/resources/targetsources/ctest1.yaml b/lab/dev/resources/targetsources/ctest1.yaml index e0aea43..bdb1bf8 100644 --- a/lab/dev/resources/targetsources/ctest1.yaml +++ b/lab/dev/resources/targetsources/ctest1.yaml @@ -5,7 +5,8 @@ metadata: spec: provider: http: - url: http://inventory-service:8080/targets + url: http://srbsci-121:8081/api/dcim/devices/?export=test + token: nbt_PtTwUBOtEvm7.64263351281a7a34227c81e6c083c7b1ff71447348c92f5821cc2088462320f1 labels: source: inventory type: http From 1bc5d2be5e429076d9bb95578cf56eb2a42fda14 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 17 Apr 2026 17:49:59 +0000 Subject: [PATCH 030/153] remove targetsource ressource to not impact main --- lab/dev/resources/targetsources/ctest1.yaml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 lab/dev/resources/targetsources/ctest1.yaml diff --git a/lab/dev/resources/targetsources/ctest1.yaml b/lab/dev/resources/targetsources/ctest1.yaml deleted file mode 100644 index bdb1bf8..0000000 --- a/lab/dev/resources/targetsources/ctest1.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: operator.gnmic.dev/v1alpha1 -kind: TargetSource -metadata: - name: http-discovery -spec: - provider: - http: - url: http://srbsci-121:8081/api/dcim/devices/?export=test - token: nbt_PtTwUBOtEvm7.64263351281a7a34227c81e6c083c7b1ff71447348c92f5821cc2088462320f1 - labels: - source: inventory - type: http - profile: eos \ No newline at end of file From 14e7765ae44c19dad961dc367a7a2da4ff818190 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 17 Apr 2026 18:09:17 +0000 Subject: [PATCH 031/153] add batching to DiscoveryEvent's --- internal/controller/discovery/core/sender.go | 29 +++++++++++++++---- .../discovery/loaders/http_push/loader.go | 3 +- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/internal/controller/discovery/core/sender.go b/internal/controller/discovery/core/sender.go index 84de206..cc8e3c1 100644 --- a/internal/controller/discovery/core/sender.go +++ b/internal/controller/discovery/core/sender.go @@ -58,12 +58,29 @@ func SendSnapshot(ctx context.Context, out chan<- []DiscoveryMessage, targets [] } // SendEvents sends discovery messages over channel in a context-aware manner -func SendEvents(ctx context.Context, out chan<- []DiscoveryMessage, events []DiscoveryEvent) error { - // Convert DiscoveryEvent slice to DiscoveryMessage slice - messages := make([]DiscoveryMessage, len(events)) - for i, msg := range events { - messages[i] = msg +func SendEvents(ctx context.Context, out chan<- []DiscoveryMessage, events []DiscoveryEvent, chunkSize int) error { + if chunkSize <= 0 { + chunkSize = 1 } - return sendMessages(ctx, out, messages) + totalEvents := len(events) + for i := 0; i < totalEvents; i += chunkSize { + end := i + chunkSize + if end > totalEvents { + end = totalEvents + } + + chunk := events[i:end] + // Convert DiscoveryEvent chunk to DiscoveryMessage slice + messages := make([]DiscoveryMessage, len(chunk)) + for j, event := range chunk { + messages[j] = event + } + + if err := sendMessages(ctx, out, messages); err != nil { + return err + } + } + + return nil } diff --git a/internal/controller/discovery/loaders/http_push/loader.go b/internal/controller/discovery/loaders/http_push/loader.go index 2e4ae0e..572df1d 100644 --- a/internal/controller/discovery/loaders/http_push/loader.go +++ b/internal/controller/discovery/loaders/http_push/loader.go @@ -44,8 +44,9 @@ func (l *Loader) Start( // Receive target updates via HTTP push var targetEvents []core.DiscoveryEvent + const chunkSize = 100 - if err := core.SendEvents(ctx, out, targetEvents); err != nil { + if err := core.SendEvents(ctx, out, targetEvents, chunkSize); err != nil { logger.Error(err, "failed to send events") return nil } From b4337ead8b4eb7f3bb3b764f2141707f69698483 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 17 Apr 2026 18:26:33 +0000 Subject: [PATCH 032/153] refactored sender.go --- internal/controller/discovery/core/sender.go | 65 ++++++++++--------- internal/controller/discovery/core/types.go | 2 +- .../controller/discovery/target_manager.go | 4 +- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/internal/controller/discovery/core/sender.go b/internal/controller/discovery/core/sender.go index cc8e3c1..3e6b4aa 100644 --- a/internal/controller/discovery/core/sender.go +++ b/internal/controller/discovery/core/sender.go @@ -14,6 +14,24 @@ func sendMessages(ctx context.Context, out chan<- []DiscoveryMessage, messages [ return nil } +// forEachChunk iterates over ranges [start,end) for a total count using the provided chunkSize +func forEachChunk(total, chunkSize int, fn func(start, end int) error) error { + if chunkSize <= 0 { + chunkSize = 1 + } + + for i := 0; i < total; i += chunkSize { + end := i + chunkSize + if end > total { + end = total + } + if err := fn(i, end); err != nil { + return err + } + } + return nil +} + // createDiscoverySnapshots takes a list of discovered targets and returns chunked DiscoverySnapshots func createDiscoverySnapshots(targets []DiscoveredTarget, snapshotID string, chunkSize int) []DiscoverySnapshot { if chunkSize <= 0 { @@ -23,19 +41,15 @@ func createDiscoverySnapshots(targets []DiscoveredTarget, snapshotID string, chu var snapshots []DiscoverySnapshot totalTargets := len(targets) - for i := 0; i < totalTargets; i += chunkSize { - end := i + chunkSize - if end > totalTargets { - end = totalTargets - } - + _ = forEachChunk(totalTargets, chunkSize, func(i, end int) error { chunk := targets[i:end] snapshots = append(snapshots, DiscoverySnapshot{ - Target: chunk, + Targets: chunk, SnapshotID: snapshotID, IsLastChunk: (end == totalTargets), }) - } + return nil + }) return snapshots } @@ -45,7 +59,7 @@ func SendSnapshot(ctx context.Context, out chan<- []DiscoveryMessage, targets [] snapshots := createDiscoverySnapshots(targets, snapshotID, chunkSize) for _, snapshot := range snapshots { - // Convert DiscoverySnapshot to DiscoveryMessage interface + // Convert DiscoverySnapshot to DiscoveryMessage messages := make([]DiscoveryMessage, 1) messages[0] = snapshot @@ -57,30 +71,23 @@ func SendSnapshot(ctx context.Context, out chan<- []DiscoveryMessage, targets [] return nil } +func eventsToMessages(events []DiscoveryEvent) []DiscoveryMessage { + message := make([]DiscoveryMessage, len(events)) + for i, event := range events { + message[i] = event + } + return message +} + // SendEvents sends discovery messages over channel in a context-aware manner func SendEvents(ctx context.Context, out chan<- []DiscoveryMessage, events []DiscoveryEvent, chunkSize int) error { if chunkSize <= 0 { chunkSize = 1 } + messages := eventsToMessages(events) + total := len(messages) - totalEvents := len(events) - for i := 0; i < totalEvents; i += chunkSize { - end := i + chunkSize - if end > totalEvents { - end = totalEvents - } - - chunk := events[i:end] - // Convert DiscoveryEvent chunk to DiscoveryMessage slice - messages := make([]DiscoveryMessage, len(chunk)) - for j, event := range chunk { - messages[j] = event - } - - if err := sendMessages(ctx, out, messages); err != nil { - return err - } - } - - return nil + return forEachChunk(total, chunkSize, func(i, end int) error { + return sendMessages(ctx, out, messages[i:end]) + }) } diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index f56eaa2..cac249d 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -22,7 +22,7 @@ type DiscoveryEvent struct { } type DiscoverySnapshot struct { - Target []DiscoveredTarget + Targets []DiscoveredTarget Event EventAction SnapshotID string IsLastChunk bool diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_manager.go index f44e33c..153723c 100644 --- a/internal/controller/discovery/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -55,9 +55,9 @@ func (m *TargetManager) Run(ctx context.Context) error { logger.Info( "received snapshot chunk", "snapshotID", msg.SnapshotID, - "targetCount", len(msg.Target), + "targetCount", len(msg.Targets), ) - m.collected[msg.SnapshotID] = append(m.collected[msg.SnapshotID], msg.Target...) + m.collected[msg.SnapshotID] = append(m.collected[msg.SnapshotID], msg.Targets...) if msg.IsLastChunk { m.processSnapshot(msg.SnapshotID, logger) } From 30f3ecb6f291c55d7cdd2b73e9257189acacd106 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 17 Apr 2026 19:20:56 +0000 Subject: [PATCH 033/153] load buffer and chunk size from env variable --- cmd/main.go | 10 ++++++++-- internal/controller/discovery/core/sender.go | 11 ----------- internal/controller/discovery/core/types.go | 4 ++++ internal/controller/discovery/loader.go | 4 ++-- .../discovery/loaders/http_pull/loader.go | 16 +++++++--------- .../discovery/loaders/http_push/loader.go | 13 +++++++------ internal/controller/targetsource_controller.go | 10 +++++++++- 7 files changed, 37 insertions(+), 31 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 4c37a0d..eacdee5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -64,6 +64,8 @@ func main() { var probeAddr string var devMode bool var apiAddr string + var discoveryChunkSize int + var discoveryBufferSize int flag.StringVar(&apiAddr, "api-bind-address", "", "The address the operator API endpoint binds to. Disabled if empty.") flag.BoolVar(&devMode, "dev-mode", false, "Enable development mode.") flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") @@ -71,6 +73,8 @@ func main() { flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") + flag.IntVar(&discoveryChunkSize, "discovery-chunk-size", 100, "Maximum number of targets/events sent in a single discovery message.") + flag.IntVar(&discoveryBufferSize, "discovery-buffer-size", 10, "Amount of discovery messages that can be queued in the channel buffer.") opts := zap.Options{ Development: devMode, } @@ -117,8 +121,10 @@ func main() { os.Exit(1) } if err := (&controller.TargetSourceReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + BufferSize: discoveryBufferSize, + ChunkSize: discoveryChunkSize, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "TargetSource") os.Exit(1) diff --git a/internal/controller/discovery/core/sender.go b/internal/controller/discovery/core/sender.go index 3e6b4aa..843f30e 100644 --- a/internal/controller/discovery/core/sender.go +++ b/internal/controller/discovery/core/sender.go @@ -16,10 +16,6 @@ func sendMessages(ctx context.Context, out chan<- []DiscoveryMessage, messages [ // forEachChunk iterates over ranges [start,end) for a total count using the provided chunkSize func forEachChunk(total, chunkSize int, fn func(start, end int) error) error { - if chunkSize <= 0 { - chunkSize = 1 - } - for i := 0; i < total; i += chunkSize { end := i + chunkSize if end > total { @@ -34,10 +30,6 @@ func forEachChunk(total, chunkSize int, fn func(start, end int) error) error { // createDiscoverySnapshots takes a list of discovered targets and returns chunked DiscoverySnapshots func createDiscoverySnapshots(targets []DiscoveredTarget, snapshotID string, chunkSize int) []DiscoverySnapshot { - if chunkSize <= 0 { - chunkSize = 1 - } - var snapshots []DiscoverySnapshot totalTargets := len(targets) @@ -81,9 +73,6 @@ func eventsToMessages(events []DiscoveryEvent) []DiscoveryMessage { // SendEvents sends discovery messages over channel in a context-aware manner func SendEvents(ctx context.Context, out chan<- []DiscoveryMessage, events []DiscoveryEvent, chunkSize int) error { - if chunkSize <= 0 { - chunkSize = 1 - } messages := eventsToMessages(events) total := len(messages) diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index cac249d..69a407e 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -1,5 +1,9 @@ package core +type LoaderConfig struct { + ChunkSize int +} + // DiscoveredTarget represents a target discovered from an external source // before it is materialized as a Kubernetes Target CR type DiscoveredTarget struct { diff --git a/internal/controller/discovery/loader.go b/internal/controller/discovery/loader.go index ad1e83f..e0834c0 100644 --- a/internal/controller/discovery/loader.go +++ b/internal/controller/discovery/loader.go @@ -9,12 +9,12 @@ import ( ) // NewLoader creates a loader by name -func NewLoader(name string, namespace string, spec gnmicv1alpha1.TargetSourceSpec) (core.Loader, error) { +func NewLoader(name string, namespace string, spec gnmicv1alpha1.TargetSourceSpec, cfg core.LoaderConfig) (core.Loader, error) { loaderName := namespace + "/" + name switch { case spec.Provider.HTTP != nil: - return http_pull.New(), nil + return http_pull.New(cfg), nil case spec.Provider.Consul != nil: return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", loaderName) default: diff --git a/internal/controller/discovery/loaders/http_pull/loader.go b/internal/controller/discovery/loaders/http_pull/loader.go index 94660d0..8213c8a 100644 --- a/internal/controller/discovery/loaders/http_pull/loader.go +++ b/internal/controller/discovery/loaders/http_pull/loader.go @@ -12,15 +12,13 @@ import ( "github.com/google/uuid" ) -const ( - chunkSize = 100 -) - -type Loader struct{} +type Loader struct { + cfg core.LoaderConfig +} -// New instantiates the http_pull loader -func New() core.Loader { - return &Loader{} +// New instantiates the http_pull loader with the provided config +func New(cfg core.LoaderConfig) core.Loader { + return &Loader{cfg: cfg} } func (l *Loader) Name() string { @@ -67,7 +65,7 @@ func (l *Loader) Start( }, } - if err := core.SendSnapshot(ctx, out, targets, snapshotID, chunkSize); err != nil { + if err := core.SendSnapshot(ctx, out, targets, snapshotID, l.cfg.ChunkSize); err != nil { return err } } diff --git a/internal/controller/discovery/loaders/http_push/loader.go b/internal/controller/discovery/loaders/http_push/loader.go index 572df1d..025176f 100644 --- a/internal/controller/discovery/loaders/http_push/loader.go +++ b/internal/controller/discovery/loaders/http_push/loader.go @@ -13,11 +13,13 @@ import ( // REST API defined internal/apiserver // Loader implements the HTTP pull discovery mechanism -type Loader struct{} +type Loader struct{ + cfg core.LoaderConfig +} -// New returns a new http_pull loader instance -func New() core.Loader { - return &Loader{} +// New returns a new http_push loader instance configured with cfg +func New(cfg core.LoaderConfig) core.Loader { + return &Loader{cfg: cfg} } func (l *Loader) Name() string { @@ -44,9 +46,8 @@ func (l *Loader) Start( // Receive target updates via HTTP push var targetEvents []core.DiscoveryEvent - const chunkSize = 100 - if err := core.SendEvents(ctx, out, targetEvents, chunkSize); err != nil { + if err := core.SendEvents(ctx, out, targetEvents, l.cfg.ChunkSize); err != nil { logger.Error(err, "failed to send events") return nil } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 9fb587f..fce6742 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -45,6 +45,9 @@ type TargetSourceReconciler struct { mu sync.Mutex running map[client.ObjectKey]runningSource + + BufferSize int + ChunkSize int } // +kubebuilder:rbac:groups=operator.gnmic.dev,resources=targetsources,verbs=get;list;watch;create;update;patch;delete @@ -144,17 +147,22 @@ func (r *TargetSourceReconciler) isPipelineRunning(key client.ObjectKey) bool { // startDiscoveryPipeline creates and starts the loader and target manager func (r *TargetSourceReconciler) startDiscoveryPipeline(key client.ObjectKey, targetSource *gnmicv1alpha1.TargetSource) error { + cfg := core.LoaderConfig{ + ChunkSize: r.ChunkSize, + } + loader, err := discovery.NewLoader( targetSource.ObjectMeta.Name, targetSource.ObjectMeta.Namespace, targetSource.Spec, + cfg, ) if err != nil { return err } runtimeCtx, cancel := context.WithCancel(context.Background()) - targetChannel := make(chan []core.DiscoveryMessage, 10) + targetChannel := make(chan []core.DiscoveryMessage, r.BufferSize) // Start loader go loader.Start(runtimeCtx, targetSource.Name, targetSource.Spec, targetChannel) From 6684aa9c33e9a21108218e4765dfb8be9bd6c929 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Fri, 17 Apr 2026 17:18:47 -0600 Subject: [PATCH 034/153] ran go mod tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index f236ded..827da2a 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.5 require ( github.com/cert-manager/cert-manager v1.19.3 github.com/go-logr/logr v1.4.3 + github.com/google/uuid v1.6.0 github.com/onsi/ginkgo/v2 v2.27.3 github.com/onsi/gomega v1.38.3 github.com/openconfig/gnmic/pkg/api v0.1.10 @@ -47,7 +48,6 @@ require ( github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f // indirect - github.com/google/uuid v1.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect From c952cbf267a36aa4dc1acebc89ffafed34890700 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Fri, 17 Apr 2026 18:39:33 -0600 Subject: [PATCH 035/153] changed events to delete/apply and implemented draft with snapshots --- internal/controller/discovery/core/types.go | 4 +- .../controller/discovery/target_manager.go | 134 +++++++++--------- 2 files changed, 65 insertions(+), 73 deletions(-) diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index cac249d..8368d06 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -10,8 +10,7 @@ type DiscoveredTarget struct { const ( DELETE EventAction = 0 - CREATE EventAction = 1 - UPDATE EventAction = 2 + APPLY EventAction = 1 ) type EventAction int @@ -23,7 +22,6 @@ type DiscoveryEvent struct { type DiscoverySnapshot struct { Targets []DiscoveredTarget - Event EventAction SnapshotID string IsLastChunk bool } diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_manager.go index 0d9bd94..e34a272 100644 --- a/internal/controller/discovery/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -3,7 +3,9 @@ package discovery import ( "context" "fmt" + "maps" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -73,77 +75,20 @@ func (m *TargetManager) Run(ctx context.Context) error { case core.DiscoveryEvent: // Process individual event-driven update - logger.Info( - "received discovery event", - "target", msg.Target.Name, - ) - switch msg.Event { - case core.CREATE: - logger.Info("Would create target", "name", msg.Target.Name, "address", msg.Target.Address, "labels", msg.Target.Labels) - target := &gnmicv1alpha1.Target{ - ObjectMeta: metav1.ObjectMeta{ - Name: msg.Target.Name, - Namespace: m.targetSource.ObjectMeta.Namespace, - Labels: map[string]string{ - "gnmic.io/source": m.targetSource.ObjectMeta.Name, - }, - }, - Spec: gnmicv1alpha1.TargetSpec{ - Address: msg.Target.Address, - Profile: "default", - }, - } - err := controllerutil.SetControllerReference(m.targetSource, target, m.scheme) - if err != nil { - logger.Error(err, "error setting the owner reference") - } - - err = m.client.Create(ctx, target) - if err != nil { - logger.Error(err, "error creating target object") - } - logger.Info(fmt.Sprintf("created new target object %s/%s", target.ObjectMeta.Namespace, target.ObjectMeta.Name)) - - case core.UPDATE: - logger.Info("Would update target", "name", msg.Target.Name, "address", msg.Target.Address, "labels", msg.Target.Labels) - existing := &gnmicv1alpha1.Target{} - newSpec := gnmicv1alpha1.TargetSpec{ - Address: msg.Target.Address, - Profile: "default", - } - - err := m.client.Get(ctx, types.NamespacedName{ - Name: msg.Target.Name, - Namespace: m.targetSource.Namespace, - }, existing) - if err != nil { - logger.Error(err, "error fetching existing target object") - } - - existing.Spec = newSpec - - err = m.client.Update(ctx, existing) - if err != nil { - logger.Error(err, "error updating object") - } - logger.Info(fmt.Sprintf("updated existing target object %s/%s", existing.ObjectMeta.Namespace, existing.ObjectMeta.Name)) + logger.Info(fmt.Sprintf("received discovery event for target %s", msg.Target.Name)) + switch msg.Event { case core.DELETE: - logger.Info("Would delete target", "name", msg.Target.Name) - existing := &gnmicv1alpha1.Target{} - err := m.client.Get(ctx, types.NamespacedName{ - Name: msg.Target.Name, - Namespace: m.targetSource.Namespace, - }, existing) + err := m.deleteTarget(ctx, msg.Target.Name) if err != nil { - logger.Error(err, "error fetching existing target object") + logger.Error(err, fmt.Sprintf("error deleting target object %s/%s", m.targetSource.ObjectMeta.Namespace, msg.Target.Name)) } - - err = m.client.Delete(ctx, existing) + logger.Info(fmt.Sprintf("deleted target object %s/%s", m.targetSource.ObjectMeta.Namespace, msg.Target.Name)) + case core.APPLY: + err := m.applyTarget(ctx, logger, msg.Target.Name, msg.Target.Address) if err != nil { - logger.Error(err, "error deleting the object") + logger.Error(err, fmt.Sprintf("error applying target object %s/%s", m.targetSource.ObjectMeta.Namespace, msg.Target.Name)) } - logger.Info(fmt.Sprintf("deleted target object %s/%s", m.targetSource.Namespace, msg.Target.Name)) } } } @@ -156,13 +101,62 @@ func (m *TargetManager) processSnapshot(snapshotID string, logger logr.Logger) { targets := m.collected[snapshotID] delete(m.collected, snapshotID) - logger.Info("Processing full snapshot", "snapshotID", snapshotID, "totalTargets", len(targets)) + logger.Info(fmt.Sprintf("Processing full snapshot ID: %s, targets: %d", snapshotID, len(targets))) - if m.targetSource.Spec.Provider.HTTP != nil { - logger.Info("Would delete all existing targets for targetsource", "targetsource", m.targetSource.Name) + for _, target := range targets { + err := m.applyTarget(context.Background(), logger, target.Name, target.Address) + if err != nil { + logger.Error(err, fmt.Sprintf("error applying target object %s/%s", m.targetSource.ObjectMeta.Namespace, target.Name)) + } } +} - for _, target := range targets { - logger.Info("Would create target", "name", target.Name, "address", target.Address, "labels", target.Labels) +func (m *TargetManager) applyTarget(ctx context.Context, logger logr.Logger, name string, address string) error { + target := &gnmicv1alpha1.Target{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: m.targetSource.Namespace, + }, } + + _, err := controllerutil.CreateOrUpdate(ctx, m.client, target, func() error { + labels := map[string]string{ + "gnmic.io/source": m.targetSource.Name, + } + + maps.Copy(labels, m.targetSource.Spec.TargetLabels) + + target.Labels = labels + + target.Spec = gnmicv1alpha1.TargetSpec{ + Address: address, + Profile: m.targetSource.Spec.TargetProfile, + } + + return controllerutil.SetControllerReference(m.targetSource, target, m.scheme) + }) + + logger.Info(fmt.Sprintf("applied target object %s/%s", m.targetSource.ObjectMeta.Namespace, name)) + + return err +} + +func (m *TargetManager) deleteTarget(ctx context.Context, name string) error { + existing := &gnmicv1alpha1.Target{} + err := m.client.Get(ctx, types.NamespacedName{ + Name: name, + Namespace: m.targetSource.Namespace, + }, existing) + if apierrors.IsNotFound(err) { + return nil + } else if err != nil { + return err + } + + err = m.client.Delete(ctx, existing) + if apierrors.IsNotFound(err) { + return nil + } + + return err } From d4a9053433983bc43db3b2267c12fe2401902692 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 20 Apr 2026 11:22:05 -0600 Subject: [PATCH 036/153] first implementation for full snapshot processing --- internal/controller/discovery/mapper.go | 47 +++++-------------- .../controller/discovery/target_manager.go | 20 ++++++-- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/internal/controller/discovery/mapper.go b/internal/controller/discovery/mapper.go index 6f8dbf0..07c3c01 100644 --- a/internal/controller/discovery/mapper.go +++ b/internal/controller/discovery/mapper.go @@ -5,51 +5,30 @@ package discovery import ( gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" - "k8s.io/apimachinery/pkg/api/equality" + "github.com/gnmic/operator/internal/controller/discovery/core" ) type Diff struct { - ToCreate []gnmicv1alpha1.Target - ToUpdate []gnmicv1alpha1.Target - ToDelete []gnmicv1alpha1.Target + ToApply []core.DiscoveredTarget + ToDelete []core.DiscoveredTarget } -func BuildDiff(existing, discovered []gnmicv1alpha1.Target) Diff { +func BuildDiff(existing []gnmicv1alpha1.Target, discovered []core.DiscoveredTarget) Diff { var diff Diff - existingMap := make(map[string]gnmicv1alpha1.Target) - for _, e := range existing { - key := e.Namespace + "/" + e.Name - existingMap[key] = e - } - - discoveredMap := make(map[string]gnmicv1alpha1.Target) + discoveredMap := make(map[string]core.DiscoveredTarget) for _, e := range discovered { - key := e.Namespace + "/" + e.Name - discoveredMap[key] = e - } - - // Loop for targets to create + update - for _, t := range discovered { - key := t.Namespace + "/" + t.Name - - // Check if target already exists - if e, found := existingMap[key]; found { - // Check if the spec of the target changed - if !equality.Semantic.DeepEqual(e.Spec, t.Spec) { - diff.ToUpdate = append(diff.ToUpdate, t) - } - } else { // Target is new - diff.ToCreate = append(diff.ToCreate, t) - } + discoveredMap[e.Name] = e } - // Loop for targets to delete + // Loop for targets to delete, else they get applied for _, e := range existing { - key := e.Namespace + "/" + e.Name - - if _, found := discoveredMap[key]; !found { - diff.ToDelete = append(diff.ToDelete, e) + if t, found := discoveredMap[e.ObjectMeta.Name]; !found { + diff.ToDelete = append(diff.ToDelete, core.DiscoveredTarget{ + Name: e.ObjectMeta.Name, + }) + } else { + diff.ToApply = append(diff.ToApply, t) } } diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_manager.go index e34a272..61ed16d 100644 --- a/internal/controller/discovery/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -103,10 +103,24 @@ func (m *TargetManager) processSnapshot(snapshotID string, logger logr.Logger) { logger.Info(fmt.Sprintf("Processing full snapshot ID: %s, targets: %d", snapshotID, len(targets))) - for _, target := range targets { - err := m.applyTarget(context.Background(), logger, target.Name, target.Address) + existing, err := FetchExistingTargets(context.Background(), m.client, *m.targetSource) + if err != nil { + logger.Error(err, "error fetching existing targets") + } + + diff := BuildDiff(existing, targets) + + for _, t := range diff.ToDelete { + err := m.deleteTarget(context.Background(), t.Name) + if err != nil { + logger.Error(err, fmt.Sprintf("error deleting target object %s/%s", m.targetSource.ObjectMeta.Namespace, t.Name)) + } + } + + for _, t := range diff.ToApply { + err := m.applyTarget(context.Background(), logger, t.Name, t.Address) if err != nil { - logger.Error(err, fmt.Sprintf("error applying target object %s/%s", m.targetSource.ObjectMeta.Namespace, target.Name)) + logger.Error(err, fmt.Sprintf("error applying target object %s/%s", m.targetSource.ObjectMeta.Namespace, t.Name)) } } } From 5af4f5e998f805e6ac7f6412063adae94be5243e Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 20 Apr 2026 12:07:54 -0600 Subject: [PATCH 037/153] fixed mapper function to work for empty existing targets --- .../discovery/loaders/http_pull/loader.go | 57 +++++++++++++------ internal/controller/discovery/mapper.go | 22 +++++-- .../controller/discovery/target_manager.go | 6 ++ 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/internal/controller/discovery/loaders/http_pull/loader.go b/internal/controller/discovery/loaders/http_pull/loader.go index 94660d0..d9aacdc 100644 --- a/internal/controller/discovery/loaders/http_pull/loader.go +++ b/internal/controller/discovery/loaders/http_pull/loader.go @@ -45,6 +45,8 @@ func (l *Loader) Start( ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() + i := 1 + for { select { case <-ctx.Done(): @@ -52,24 +54,47 @@ func (l *Loader) Start( return nil case <-ticker.C: - // Example snapshot (placeholder) - snapshotID := fmt.Sprintf("snapshot-%s-%s", targetsourceName, uuid.NewString()) - targets := []core.DiscoveredTarget{ - { - Name: "ceos1", - Address: "clab-3-nodes-ceos1:6030", - Labels: map[string]string{"TargetSource": targetsourceName}, - }, - { - Name: "leaf1", - Address: "clab-3-nodes-leaf1:57400", - Labels: map[string]string{"TargetSource": targetsourceName}, - }, - } + if i == 1 { + // Example snapshot (placeholder) + snapshotID := fmt.Sprintf("snapshot-%s-%s", targetsourceName, uuid.NewString()) + targets := []core.DiscoveredTarget{ + { + Name: "ceos1", + Address: "clab-3-nodes-ceos1:6030", + Labels: map[string]string{"TargetSource": targetsourceName}, + }, + { + Name: "leaf1", + Address: "clab-3-nodes-leaf1:57400", + Labels: map[string]string{"TargetSource": targetsourceName}, + }, + } - if err := core.SendSnapshot(ctx, out, targets, snapshotID, chunkSize); err != nil { - return err + if err := core.SendSnapshot(ctx, out, targets, snapshotID, chunkSize); err != nil { + return err + } + } else if i == 2 { + // Example snapshot (placeholder) + snapshotID := fmt.Sprintf("snapshot-%s-%s", targetsourceName, uuid.NewString()) + targets := []core.DiscoveredTarget{ + { + Name: "ceos1", + Address: "clab-3-nodes-ceos1:6030", + Labels: map[string]string{"TargetSource": targetsourceName}, + }, + { + Name: "leaf2", + Address: "clab-3-nodes-leaf2:57400", + Labels: map[string]string{"TargetSource": targetsourceName}, + }, + } + + if err := core.SendSnapshot(ctx, out, targets, snapshotID, chunkSize); err != nil { + return err + } } + + i++ } } } diff --git a/internal/controller/discovery/mapper.go b/internal/controller/discovery/mapper.go index 07c3c01..943ca25 100644 --- a/internal/controller/discovery/mapper.go +++ b/internal/controller/discovery/mapper.go @@ -16,19 +16,29 @@ type Diff struct { func BuildDiff(existing []gnmicv1alpha1.Target, discovered []core.DiscoveredTarget) Diff { var diff Diff + existingMap := make(map[string]gnmicv1alpha1.Target) + for _, e := range existing { + existingMap[e.ObjectMeta.Name] = e + } + discoveredMap := make(map[string]core.DiscoveredTarget) - for _, e := range discovered { - discoveredMap[e.Name] = e + for _, d := range discovered { + discoveredMap[d.Name] = d } - // Loop for targets to delete, else they get applied - for _, e := range existing { - if t, found := discoveredMap[e.ObjectMeta.Name]; !found { + for name, e := range existingMap { + if d, found := discoveredMap[name]; !found { diff.ToDelete = append(diff.ToDelete, core.DiscoveredTarget{ Name: e.ObjectMeta.Name, }) } else { - diff.ToApply = append(diff.ToApply, t) + diff.ToApply = append(diff.ToApply, d) + } + } + + for name, d := range discoveredMap { + if _, found := existingMap[name]; !found { + diff.ToApply = append(diff.ToApply, d) } } diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_manager.go index 61ed16d..b585aa7 100644 --- a/internal/controller/discovery/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -108,8 +108,12 @@ func (m *TargetManager) processSnapshot(snapshotID string, logger logr.Logger) { logger.Error(err, "error fetching existing targets") } + logger.Info("fetched targets") + diff := BuildDiff(existing, targets) + logger.Info(fmt.Sprintf("apply targets: %d, delete targets: %d", len(diff.ToApply), len(diff.ToDelete))) + for _, t := range diff.ToDelete { err := m.deleteTarget(context.Background(), t.Name) if err != nil { @@ -123,6 +127,8 @@ func (m *TargetManager) processSnapshot(snapshotID string, logger logr.Logger) { logger.Error(err, fmt.Sprintf("error applying target object %s/%s", m.targetSource.ObjectMeta.Namespace, t.Name)) } } + + logger.Info("end of snapshot processing") } func (m *TargetManager) applyTarget(ctx context.Context, logger logr.Logger, name string, address string) error { From d5ea4da50e3777ef7040ff5473a884177fb800fb Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 20 Apr 2026 12:17:15 -0600 Subject: [PATCH 038/153] introduce observedGeneration for pipeline restart --- api/v1alpha1/targetsource_types.go | 7 ++++--- api/v1alpha1/zz_generated.deepcopy.go | 2 +- config/crd/bases/operator.gnmic.dev_targetsources.yaml | 4 ++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index feea000..eb0be17 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -49,9 +49,10 @@ type ConsulConfig struct { // TargetSourceStatus defines the observed state of TargetSource type TargetSourceStatus struct { - Status string `json:"status"` - TargetsCount int32 `json:"targetsCount"` - LastSync metav1.Time `json:"lastSync"` + Status string `json:"status"` + ObservedGeneration int64 `json:"observedGeneration"` + TargetsCount int32 `json:"targetsCount"` + LastSync metav1.Time `json:"lastSync"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3848412..61e81fd 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ limitations under the License. package v1alpha1 import ( - v1 "k8s.io/api/core/v1" + "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index f373822..4c8c866 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -77,6 +77,9 @@ spec: lastSync: format: date-time type: string + observedGeneration: + format: int64 + type: integer status: type: string targetsCount: @@ -84,6 +87,7 @@ spec: type: integer required: - lastSync + - observedGeneration - status - targetsCount type: object From fe086e2204df82fd587a8edafdf374e209f01298 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 20 Apr 2026 12:33:42 -0600 Subject: [PATCH 039/153] tests with targetsource status field to restart pipeline --- api/v1alpha1/targetsource_types.go | 6 +++--- .../operator.gnmic.dev_targetsources.yaml | 3 --- .../discovery/loaders/http_pull/loader.go | 19 +++++++++++++++++-- .../controller/targetsource_controller.go | 12 +++++++++++- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index eb0be17..255732c 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -49,10 +49,10 @@ type ConsulConfig struct { // TargetSourceStatus defines the observed state of TargetSource type TargetSourceStatus struct { - Status string `json:"status"` + Status string `json:"status,omitempty"` ObservedGeneration int64 `json:"observedGeneration"` - TargetsCount int32 `json:"targetsCount"` - LastSync metav1.Time `json:"lastSync"` + TargetsCount int32 `json:"targetsCount,omitempty"` + LastSync metav1.Time `json:"lastSync,omitempty"` } //+kubebuilder:object:root=true diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 4c8c866..68b669c 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -86,10 +86,7 @@ spec: format: int32 type: integer required: - - lastSync - observedGeneration - - status - - targetsCount type: object type: object served: true diff --git a/internal/controller/discovery/loaders/http_pull/loader.go b/internal/controller/discovery/loaders/http_pull/loader.go index d9aacdc..2d8a9e9 100644 --- a/internal/controller/discovery/loaders/http_pull/loader.go +++ b/internal/controller/discovery/loaders/http_pull/loader.go @@ -54,7 +54,8 @@ func (l *Loader) Start( return nil case <-ticker.C: - if i == 1 { + switch i { + case 1: // Example snapshot (placeholder) snapshotID := fmt.Sprintf("snapshot-%s-%s", targetsourceName, uuid.NewString()) targets := []core.DiscoveredTarget{ @@ -73,7 +74,7 @@ func (l *Loader) Start( if err := core.SendSnapshot(ctx, out, targets, snapshotID, chunkSize); err != nil { return err } - } else if i == 2 { + case 2: // Example snapshot (placeholder) snapshotID := fmt.Sprintf("snapshot-%s-%s", targetsourceName, uuid.NewString()) targets := []core.DiscoveredTarget{ @@ -89,6 +90,20 @@ func (l *Loader) Start( }, } + if err := core.SendSnapshot(ctx, out, targets, snapshotID, chunkSize); err != nil { + return err + } + + default: + snapshotID := fmt.Sprintf("snapshot-%s-%s", targetsourceName, uuid.NewString()) + targets := []core.DiscoveredTarget{ + { + Name: "ceos1", + Address: "clab-3-nodes-ceos2:6030", + Labels: map[string]string{"TargetSource": targetsourceName}, + }, + } + if err := core.SendSnapshot(ctx, out, targets, snapshotID, chunkSize); err != nil { return err } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 9fb587f..993fa61 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -76,7 +76,11 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request // Check if pipeline is already running if r.isPipelineRunning(req.NamespacedName) { - return ctrl.Result{}, nil + if targetSource.Generation != targetSource.Status.ObservedGeneration { + r.stopDiscovery(req.NamespacedName) + } else { + return ctrl.Result{}, nil + } } // Start discovery pipeline @@ -84,6 +88,12 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, err } + // Update TargetSource ObservedGeneration Status field + targetSource.Status.ObservedGeneration = targetSource.Generation + if err := r.Status().Update(ctx, targetSource); err != nil { + return ctrl.Result{}, err + } + logger.Info("TargetSource pipeline started") return ctrl.Result{}, nil } From bd6056815134f7ac7d1e0a1581e1a08e8da6c061 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 20 Apr 2026 12:59:58 -0600 Subject: [PATCH 040/153] added status update to targetmanager --- internal/controller/discovery/target_manager.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_manager.go index b585aa7..0b630e7 100644 --- a/internal/controller/discovery/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -91,6 +91,18 @@ func (m *TargetManager) Run(ctx context.Context) error { } } } + + existing, err := FetchExistingTargets(ctx, m.client, *m.targetSource) + if err != nil { + logger.Error(err, "error fetching existing targets") + } + + m.targetSource.Status.TargetsCount = int32(len(existing)) + m.targetSource.Status.LastSync = metav1.Now() + + if err := m.client.Status().Update(ctx, m.targetSource); err != nil { + logger.Error(err, "error updating targetSource status") + } } } } From 1e81e1102a43e8d4e43ad80e2c6cb0c725ab184f Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 20 Apr 2026 17:47:30 -0600 Subject: [PATCH 041/153] separated status updates & cleaned up functions --- .../controller/discovery/target_manager.go | 98 ++++++++++++------- .../controller/targetsource_controller.go | 40 +++++++- 2 files changed, 103 insertions(+), 35 deletions(-) diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_manager.go index 0b630e7..9a7d2fd 100644 --- a/internal/controller/discovery/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -2,7 +2,6 @@ package discovery import ( "context" - "fmt" "maps" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -42,7 +41,10 @@ func NewTargetManager(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.Targ // and reconciles Target CRs accordingly func (m *TargetManager) Run(ctx context.Context) error { logger := log.FromContext(ctx). - WithValues("targetSource", m.targetSource) + WithValues( + "targetSource", m.targetSource.ObjectMeta.Name, + "namespace", m.targetSource.ObjectMeta.Namespace, + ) logger.Info("target manager started") @@ -70,80 +72,112 @@ func (m *TargetManager) Run(ctx context.Context) error { ) m.collected[msg.SnapshotID] = append(m.collected[msg.SnapshotID], msg.Targets...) if msg.IsLastChunk { - m.processSnapshot(msg.SnapshotID, logger) + m.processSnapshot(ctx, msg.SnapshotID, logger) } case core.DiscoveryEvent: // Process individual event-driven update - logger.Info(fmt.Sprintf("received discovery event for target %s", msg.Target.Name)) + logger.Info("received discovery event", + "name", msg.Target.Name, + ) switch msg.Event { case core.DELETE: err := m.deleteTarget(ctx, msg.Target.Name) if err != nil { - logger.Error(err, fmt.Sprintf("error deleting target object %s/%s", m.targetSource.ObjectMeta.Namespace, msg.Target.Name)) + logger.Error(err, "error deleting target object", + "namespace", m.targetSource.ObjectMeta.Namespace, + "name", msg.Target.Name, + ) + } else { + logger.Info("deleted target object", + "namespace", m.targetSource.ObjectMeta.Namespace, + "name", msg.Target.Name, + ) } - logger.Info(fmt.Sprintf("deleted target object %s/%s", m.targetSource.ObjectMeta.Namespace, msg.Target.Name)) + case core.APPLY: - err := m.applyTarget(ctx, logger, msg.Target.Name, msg.Target.Address) + err := m.applyTarget(ctx, msg.Target.Name, msg.Target.Address) if err != nil { - logger.Error(err, fmt.Sprintf("error applying target object %s/%s", m.targetSource.ObjectMeta.Namespace, msg.Target.Name)) + logger.Error(err, "error applying target object", + "namespace", m.targetSource.ObjectMeta.Namespace, + "name", msg.Target.Name, + ) + + } else { + logger.Info("applied target object", + "namespace", m.targetSource.ObjectMeta.Namespace, + "name", msg.Target.Name, + ) } } } - - existing, err := FetchExistingTargets(ctx, m.client, *m.targetSource) - if err != nil { - logger.Error(err, "error fetching existing targets") - } - - m.targetSource.Status.TargetsCount = int32(len(existing)) - m.targetSource.Status.LastSync = metav1.Now() - - if err := m.client.Status().Update(ctx, m.targetSource); err != nil { - logger.Error(err, "error updating targetSource status") - } } } } } // processSnapshot takes a complete snapshot of discovered targets and reconciles Target CRs accordingly -func (m *TargetManager) processSnapshot(snapshotID string, logger logr.Logger) { +func (m *TargetManager) processSnapshot(ctx context.Context, snapshotID string, logger logr.Logger) { targets := m.collected[snapshotID] delete(m.collected, snapshotID) - logger.Info(fmt.Sprintf("Processing full snapshot ID: %s, targets: %d", snapshotID, len(targets))) + logger.Info("processing full snapshot", + "id", snapshotID, + "numOfTargets", len(targets), + ) - existing, err := FetchExistingTargets(context.Background(), m.client, *m.targetSource) + existing, err := FetchExistingTargets(ctx, m.client, *m.targetSource) if err != nil { logger.Error(err, "error fetching existing targets") + } else { + logger.Info("fetched existing targets", + "numOfTargets", len(existing), + ) } - logger.Info("fetched targets") - diff := BuildDiff(existing, targets) - logger.Info(fmt.Sprintf("apply targets: %d, delete targets: %d", len(diff.ToApply), len(diff.ToDelete))) + logger.Info("built diff", + "numOfTargetsToApply", len(diff.ToApply), + "numOfTargetsToDelete", len(diff.ToDelete), + ) for _, t := range diff.ToDelete { - err := m.deleteTarget(context.Background(), t.Name) + err := m.deleteTarget(ctx, t.Name) if err != nil { - logger.Error(err, fmt.Sprintf("error deleting target object %s/%s", m.targetSource.ObjectMeta.Namespace, t.Name)) + logger.Error(err, "error deleting target object", + "namespace", m.targetSource.ObjectMeta.Namespace, + "name", t.Name, + ) + } else { + logger.Info("deleted target object", + "namespace", m.targetSource.ObjectMeta.Namespace, + "name", t.Name, + ) } } for _, t := range diff.ToApply { - err := m.applyTarget(context.Background(), logger, t.Name, t.Address) + err := m.applyTarget(ctx, t.Name, t.Address) if err != nil { - logger.Error(err, fmt.Sprintf("error applying target object %s/%s", m.targetSource.ObjectMeta.Namespace, t.Name)) + logger.Error(err, "error applying target object", + "namespace", m.targetSource.ObjectMeta.Namespace, + "name", t.Name, + ) + } else { + logger.Info("applied target object", + "namespace", m.targetSource.ObjectMeta.Namespace, + "name", t.Name, + ) } + } logger.Info("end of snapshot processing") } -func (m *TargetManager) applyTarget(ctx context.Context, logger logr.Logger, name string, address string) error { +func (m *TargetManager) applyTarget(ctx context.Context, name string, address string) error { target := &gnmicv1alpha1.Target{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -168,8 +202,6 @@ func (m *TargetManager) applyTarget(ctx context.Context, logger logr.Logger, nam return controllerutil.SetControllerReference(m.targetSource, target, m.scheme) }) - logger.Info(fmt.Sprintf("applied target object %s/%s", m.targetSource.ObjectMeta.Namespace, name)) - return err } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 993fa61..da80030 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -20,7 +20,9 @@ import ( "context" "sync" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -79,7 +81,8 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request if targetSource.Generation != targetSource.Status.ObservedGeneration { r.stopDiscovery(req.NamespacedName) } else { - return ctrl.Result{}, nil + err := r.updateStatus(ctx, targetSource) + return ctrl.Result{}, err } } @@ -88,12 +91,15 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, err } - // Update TargetSource ObservedGeneration Status field targetSource.Status.ObservedGeneration = targetSource.Generation if err := r.Status().Update(ctx, targetSource); err != nil { return ctrl.Result{}, err } + if err := r.updateStatus(ctx, targetSource); err != nil { + return ctrl.Result{}, err + } + logger.Info("TargetSource pipeline started") return ctrl.Result{}, nil } @@ -197,12 +203,42 @@ func (r *TargetSourceReconciler) stopDiscovery(key client.ObjectKey) { } } +func (r *TargetSourceReconciler) updateStatus(ctx context.Context, ts *gnmicv1alpha1.TargetSource) error { + // Update TargetSource Status field + var targetList gnmicv1alpha1.TargetList + + err := r.Client.List(ctx, &targetList, + client.InNamespace(ts.Namespace), + client.MatchingLabels{ + "gnmic.io/source": ts.Name, + }, + ) + if err != nil { + return err + } + + err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + latest := &gnmicv1alpha1.TargetSource{} + if err := r.Get(ctx, client.ObjectKeyFromObject(ts), latest); err != nil { + return err + } + + latest.Status.TargetsCount = int32(len(targetList.Items)) + latest.Status.LastSync = metav1.Now() + + return r.Status().Update(ctx, latest) + }) + + return err +} + // SetupWithManager sets up the controller with the Manager. func (r *TargetSourceReconciler) SetupWithManager(mgr ctrl.Manager) error { r.running = make(map[client.ObjectKey]runningSource) return ctrl.NewControllerManagedBy(mgr). For(&gnmicv1alpha1.TargetSource{}). + Owns(&gnmicv1alpha1.Target{}). Named("targetsource"). Complete(r) } From d3e708cd1bfedd2797e2e48723fad6b232e7af96 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 20 Apr 2026 18:28:57 -0600 Subject: [PATCH 042/153] restructured target manager logic to handle events --- internal/controller/discovery/core/types.go | 11 +++ internal/controller/discovery/mapper.go | 26 ++++--- .../controller/discovery/target_manager.go | 77 +++++-------------- 3 files changed, 46 insertions(+), 68 deletions(-) diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 8368d06..d8ce3d1 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -20,6 +20,17 @@ type DiscoveryEvent struct { Event EventAction } +func (e EventAction) ToString() string { + switch e { + case DELETE: + return "DELETE" + case APPLY: + return "APPLY" + default: + return "UNKNOWN" + } +} + type DiscoverySnapshot struct { Targets []DiscoveredTarget SnapshotID string diff --git a/internal/controller/discovery/mapper.go b/internal/controller/discovery/mapper.go index 943ca25..a49b7b4 100644 --- a/internal/controller/discovery/mapper.go +++ b/internal/controller/discovery/mapper.go @@ -13,8 +13,8 @@ type Diff struct { ToDelete []core.DiscoveredTarget } -func BuildDiff(existing []gnmicv1alpha1.Target, discovered []core.DiscoveredTarget) Diff { - var diff Diff +func BuildDiff(existing []gnmicv1alpha1.Target, discovered []core.DiscoveredTarget) []core.DiscoveryEvent { + var events []core.DiscoveryEvent existingMap := make(map[string]gnmicv1alpha1.Target) for _, e := range existing { @@ -27,20 +27,22 @@ func BuildDiff(existing []gnmicv1alpha1.Target, discovered []core.DiscoveredTarg } for name, e := range existingMap { - if d, found := discoveredMap[name]; !found { - diff.ToDelete = append(diff.ToDelete, core.DiscoveredTarget{ - Name: e.ObjectMeta.Name, + if _, found := discoveredMap[name]; !found { + events = append(events, core.DiscoveryEvent{ + Target: core.DiscoveredTarget{ + Name: e.Name, + }, + Event: core.DELETE, }) - } else { - diff.ToApply = append(diff.ToApply, d) } } - for name, d := range discoveredMap { - if _, found := existingMap[name]; !found { - diff.ToApply = append(diff.ToApply, d) - } + for _, d := range discoveredMap { + events = append(events, core.DiscoveryEvent{ + Target: d, + Event: core.APPLY, + }) } - return diff + return events } diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_manager.go index 9a7d2fd..0b50773 100644 --- a/internal/controller/discovery/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -79,38 +79,10 @@ func (m *TargetManager) Run(ctx context.Context) error { // Process individual event-driven update logger.Info("received discovery event", "name", msg.Target.Name, + "eventAction", msg.Event.ToString(), ) - switch msg.Event { - case core.DELETE: - err := m.deleteTarget(ctx, msg.Target.Name) - if err != nil { - logger.Error(err, "error deleting target object", - "namespace", m.targetSource.ObjectMeta.Namespace, - "name", msg.Target.Name, - ) - } else { - logger.Info("deleted target object", - "namespace", m.targetSource.ObjectMeta.Namespace, - "name", msg.Target.Name, - ) - } - - case core.APPLY: - err := m.applyTarget(ctx, msg.Target.Name, msg.Target.Address) - if err != nil { - logger.Error(err, "error applying target object", - "namespace", m.targetSource.ObjectMeta.Namespace, - "name", msg.Target.Name, - ) - - } else { - logger.Info("applied target object", - "namespace", m.targetSource.ObjectMeta.Namespace, - "name", msg.Target.Name, - ) - } - } + m.processEvent(ctx, msg, logger) } } } @@ -136,45 +108,38 @@ func (m *TargetManager) processSnapshot(ctx context.Context, snapshotID string, ) } - diff := BuildDiff(existing, targets) + events := BuildDiff(existing, targets) - logger.Info("built diff", - "numOfTargetsToApply", len(diff.ToApply), - "numOfTargetsToDelete", len(diff.ToDelete), - ) + for _, e := range events { + m.processEvent(ctx, e, logger) + } + + logger.Info("end of snapshot processing") +} - for _, t := range diff.ToDelete { - err := m.deleteTarget(ctx, t.Name) - if err != nil { - logger.Error(err, "error deleting target object", - "namespace", m.targetSource.ObjectMeta.Namespace, - "name", t.Name, +func (m *TargetManager) processEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) { + switch event.Event { + case core.DELETE: + if err := m.deleteTarget(ctx, event.Target.Name); err != nil { + logger.Error(err, "error deleting target", + "targetName", event.Target.Name, ) } else { logger.Info("deleted target object", - "namespace", m.targetSource.ObjectMeta.Namespace, - "name", t.Name, + "name", event.Target.Name, ) } - } - - for _, t := range diff.ToApply { - err := m.applyTarget(ctx, t.Name, t.Address) - if err != nil { - logger.Error(err, "error applying target object", - "namespace", m.targetSource.ObjectMeta.Namespace, - "name", t.Name, + case core.APPLY: + if err := m.deleteTarget(ctx, event.Target.Name); err != nil { + logger.Error(err, "error applying target", + "targetName", event.Target.Name, ) } else { logger.Info("applied target object", - "namespace", m.targetSource.ObjectMeta.Namespace, - "name", t.Name, + "name", event.Target.Name, ) } - } - - logger.Info("end of snapshot processing") } func (m *TargetManager) applyTarget(ctx context.Context, name string, address string) error { From 9a78cf4028027cbb0a7220959c01ad6882af99ef Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 20 Apr 2026 18:31:48 -0600 Subject: [PATCH 043/153] fixed small issues --- internal/controller/discovery/mapper.go | 7 +------ internal/controller/discovery/target_manager.go | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/internal/controller/discovery/mapper.go b/internal/controller/discovery/mapper.go index a49b7b4..3c4414d 100644 --- a/internal/controller/discovery/mapper.go +++ b/internal/controller/discovery/mapper.go @@ -8,12 +8,7 @@ import ( "github.com/gnmic/operator/internal/controller/discovery/core" ) -type Diff struct { - ToApply []core.DiscoveredTarget - ToDelete []core.DiscoveredTarget -} - -func BuildDiff(existing []gnmicv1alpha1.Target, discovered []core.DiscoveredTarget) []core.DiscoveryEvent { +func GenerateEvents(existing []gnmicv1alpha1.Target, discovered []core.DiscoveredTarget) []core.DiscoveryEvent { var events []core.DiscoveryEvent existingMap := make(map[string]gnmicv1alpha1.Target) diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_manager.go index 0b50773..18d6d72 100644 --- a/internal/controller/discovery/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -108,7 +108,7 @@ func (m *TargetManager) processSnapshot(ctx context.Context, snapshotID string, ) } - events := BuildDiff(existing, targets) + events := GenerateEvents(existing, targets) for _, e := range events { m.processEvent(ctx, e, logger) @@ -130,7 +130,7 @@ func (m *TargetManager) processEvent(ctx context.Context, event core.DiscoveryEv ) } case core.APPLY: - if err := m.deleteTarget(ctx, event.Target.Name); err != nil { + if err := m.applyTarget(ctx, event.Target.Name, event.Target.Address); err != nil { logger.Error(err, "error applying target", "targetName", event.Target.Name, ) From 586001e963125cde484ddead4e16ef11c4939c7b Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Wed, 22 Apr 2026 12:58:44 +0000 Subject: [PATCH 044/153] rename file to helpers --- internal/controller/discovery/core/{sender.go => helpers.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/controller/discovery/core/{sender.go => helpers.go} (100%) diff --git a/internal/controller/discovery/core/sender.go b/internal/controller/discovery/core/helpers.go similarity index 100% rename from internal/controller/discovery/core/sender.go rename to internal/controller/discovery/core/helpers.go From 7430815bb78b417702c6df5b8e85377e63193a4b Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Wed, 22 Apr 2026 13:03:03 +0000 Subject: [PATCH 045/153] rebuild and reformat --- api/v1alpha1/zz_generated.deepcopy.go | 2 +- internal/controller/discovery/loaders/push/loader.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3848412..61e81fd 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ limitations under the License. package v1alpha1 import ( - v1 "k8s.io/api/core/v1" + "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/internal/controller/discovery/loaders/push/loader.go b/internal/controller/discovery/loaders/push/loader.go index 5b00081..ec70830 100644 --- a/internal/controller/discovery/loaders/push/loader.go +++ b/internal/controller/discovery/loaders/push/loader.go @@ -13,7 +13,7 @@ import ( // REST API defined internal/apiserver // Loader implements the HTTP pull discovery mechanism -type Loader struct{ +type Loader struct { cfg core.LoaderConfig } From 4fb6755f591f9d37792e43f17b3a1fd048ca1382 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 22 Apr 2026 08:28:22 -0600 Subject: [PATCH 046/153] removed unnecessary map --- internal/controller/discovery/mapper.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/internal/controller/discovery/mapper.go b/internal/controller/discovery/mapper.go index 3c4414d..bee897a 100644 --- a/internal/controller/discovery/mapper.go +++ b/internal/controller/discovery/mapper.go @@ -11,18 +11,13 @@ import ( func GenerateEvents(existing []gnmicv1alpha1.Target, discovered []core.DiscoveredTarget) []core.DiscoveryEvent { var events []core.DiscoveryEvent - existingMap := make(map[string]gnmicv1alpha1.Target) - for _, e := range existing { - existingMap[e.ObjectMeta.Name] = e - } - discoveredMap := make(map[string]core.DiscoveredTarget) for _, d := range discovered { discoveredMap[d.Name] = d } - for name, e := range existingMap { - if _, found := discoveredMap[name]; !found { + for _, e := range existing { + if _, found := discoveredMap[e.Name]; !found { events = append(events, core.DiscoveryEvent{ Target: core.DiscoveredTarget{ Name: e.Name, @@ -32,7 +27,7 @@ func GenerateEvents(existing []gnmicv1alpha1.Target, discovered []core.Discovere } } - for _, d := range discoveredMap { + for _, d := range discovered { events = append(events, core.DiscoveryEvent{ Target: d, Event: core.APPLY, From aaf9f2ba6545ac41ea5fb7387b937babee0214fa Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 22 Apr 2026 09:59:25 -0600 Subject: [PATCH 047/153] added prefix to targets --- .../controller/discovery/target_manager.go | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_manager.go index 18d6d72..c9ac079 100644 --- a/internal/controller/discovery/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -70,6 +70,11 @@ func (m *TargetManager) Run(ctx context.Context) error { "snapshotID", msg.SnapshotID, "targetCount", len(msg.Targets), ) + + for i := range msg.Targets { + msg.Targets[i] = m.normalizeTarget(msg.Targets[i]) + } + m.collected[msg.SnapshotID] = append(m.collected[msg.SnapshotID], msg.Targets...) if msg.IsLastChunk { m.processSnapshot(ctx, msg.SnapshotID, logger) @@ -82,6 +87,7 @@ func (m *TargetManager) Run(ctx context.Context) error { "eventAction", msg.Event.ToString(), ) + msg.Target = m.normalizeTarget(msg.Target) m.processEvent(ctx, msg, logger) } } @@ -110,6 +116,23 @@ func (m *TargetManager) processSnapshot(ctx context.Context, snapshotID string, events := GenerateEvents(existing, targets) + nApply := 0 + nDelete := 0 + + for _, e := range events { + switch e.Event { + case core.APPLY: + nApply++ + case core.DELETE: + nDelete++ + } + } + + logger.Info("generated events", + "numOfApply", nApply, + "numOfDelete", nDelete, + ) + for _, e := range events { m.processEvent(ctx, e, logger) } @@ -189,3 +212,8 @@ func (m *TargetManager) deleteTarget(ctx context.Context, name string) error { return err } + +func (m *TargetManager) normalizeTarget(t core.DiscoveredTarget) core.DiscoveredTarget { + t.Name = m.targetSource.Name + "-" + t.Name + return t +} From 255a1f3facb9f3c6b4e4ae17b4ad1afae0bcd0bd Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 23 Apr 2026 07:15:38 +0000 Subject: [PATCH 048/153] consolidate pull and push to http --- internal/controller/discovery/loader.go | 4 +- .../controller/discovery/loaders/all/all.go | 3 +- .../loaders/{pull => http}/loader.go | 10 ++-- .../discovery/loaders/http/loader_test.go | 1 + .../discovery/loaders/pull/loader_test.go | 1 - .../discovery/loaders/push/loader.go | 55 ------------------- .../discovery/loaders/push/loader_test.go | 1 - 7 files changed, 9 insertions(+), 66 deletions(-) rename internal/controller/discovery/loaders/{pull => http}/loader.go (89%) create mode 100644 internal/controller/discovery/loaders/http/loader_test.go delete mode 100644 internal/controller/discovery/loaders/pull/loader_test.go delete mode 100644 internal/controller/discovery/loaders/push/loader.go delete mode 100644 internal/controller/discovery/loaders/push/loader_test.go diff --git a/internal/controller/discovery/loader.go b/internal/controller/discovery/loader.go index 64dc8d3..42ce8da 100644 --- a/internal/controller/discovery/loader.go +++ b/internal/controller/discovery/loader.go @@ -5,7 +5,7 @@ import ( gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery/core" - pull "github.com/gnmic/operator/internal/controller/discovery/loaders/pull" + http "github.com/gnmic/operator/internal/controller/discovery/loaders/http" ) // NewLoader creates a loader by name @@ -14,7 +14,7 @@ func NewLoader(name string, namespace string, spec gnmicv1alpha1.TargetSourceSpe switch { case spec.Provider.HTTP != nil: - return pull.New(cfg), nil + return http.New(cfg), nil case spec.Provider.Consul != nil: return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", loaderName) default: diff --git a/internal/controller/discovery/loaders/all/all.go b/internal/controller/discovery/loaders/all/all.go index d05604b..3590cda 100644 --- a/internal/controller/discovery/loaders/all/all.go +++ b/internal/controller/discovery/loaders/all/all.go @@ -1,6 +1,5 @@ package all import ( - _ "github.com/gnmic/operator/internal/controller/discovery/loaders/pull" - // _ "github.com/gnmic/operator/internal/controller/targetsource/loaders/push" + _ "github.com/gnmic/operator/internal/controller/discovery/loaders/http" ) diff --git a/internal/controller/discovery/loaders/pull/loader.go b/internal/controller/discovery/loaders/http/loader.go similarity index 89% rename from internal/controller/discovery/loaders/pull/loader.go rename to internal/controller/discovery/loaders/http/loader.go index 729233d..f014a2f 100644 --- a/internal/controller/discovery/loaders/pull/loader.go +++ b/internal/controller/discovery/loaders/http/loader.go @@ -1,4 +1,4 @@ -package pull +package http import ( "context" @@ -16,13 +16,13 @@ type Loader struct { cfg core.LoaderConfig } -// New instantiates the pull loader with the provided config +// New instantiates the http loader with the provided config func New(cfg core.LoaderConfig) core.Loader { return &Loader{cfg: cfg} } func (l *Loader) Name() string { - return "pull" + return "http" } func (l *Loader) Start( @@ -37,7 +37,7 @@ func (l *Loader) Start( "targetsource", targetsourceName, ) - logger.Info("HTTP pull loader started") + logger.Info("HTTP loader started") // Only for debugging: emit a static snapshot every 30 seconds ticker := time.NewTicker(30 * time.Second) @@ -46,7 +46,7 @@ func (l *Loader) Start( for { select { case <-ctx.Done(): - logger.Info("HTTP pull loader stopped") + logger.Info("HTTP loader stopped") return nil case <-ticker.C: diff --git a/internal/controller/discovery/loaders/http/loader_test.go b/internal/controller/discovery/loaders/http/loader_test.go new file mode 100644 index 0000000..d02cfda --- /dev/null +++ b/internal/controller/discovery/loaders/http/loader_test.go @@ -0,0 +1 @@ +package http diff --git a/internal/controller/discovery/loaders/pull/loader_test.go b/internal/controller/discovery/loaders/pull/loader_test.go deleted file mode 100644 index 0493bec..0000000 --- a/internal/controller/discovery/loaders/pull/loader_test.go +++ /dev/null @@ -1 +0,0 @@ -package pull diff --git a/internal/controller/discovery/loaders/push/loader.go b/internal/controller/discovery/loaders/push/loader.go deleted file mode 100644 index ec70830..0000000 --- a/internal/controller/discovery/loaders/push/loader.go +++ /dev/null @@ -1,55 +0,0 @@ -package push - -import ( - "context" - "errors" - - gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" - "github.com/gnmic/operator/internal/controller/discovery/core" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -// this file implements the logic receive target updates via HTTP push -// REST API defined internal/apiserver - -// Loader implements the HTTP pull discovery mechanism -type Loader struct { - cfg core.LoaderConfig -} - -// New returns a new http_push loader instance configured with cfg -func New(cfg core.LoaderConfig) core.Loader { - return &Loader{cfg: cfg} -} - -func (l *Loader) Name() string { - return "http_push" -} - -func (l *Loader) Start( - ctx context.Context, - targetsourceName string, - spec gnmicv1alpha1.TargetSourceSpec, - out chan<- []core.DiscoveryMessage, -) error { - logger := log.FromContext(ctx).WithValues( - "component", "loader", - "name", l.Name(), - "targetsource", targetsourceName, - ) - logger.Info("HTTP push loader started") - - // Input Validation of spec - if spec.Provider == nil || spec.Provider.HTTP == nil { - return errors.New("http_push loader requires spec.provider.http to be set") - } - - // Receive target updates via HTTP push - var targetEvents []core.DiscoveryEvent - - if err := core.SendEvents(ctx, out, targetEvents, l.cfg.ChunkSize); err != nil { - logger.Error(err, "failed to send events") - return nil - } - return nil -} diff --git a/internal/controller/discovery/loaders/push/loader_test.go b/internal/controller/discovery/loaders/push/loader_test.go deleted file mode 100644 index 63fdf61..0000000 --- a/internal/controller/discovery/loaders/push/loader_test.go +++ /dev/null @@ -1 +0,0 @@ -package push From bd2b45f63366eaaba0170c37e1783e018049eaca Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 23 Apr 2026 12:34:50 +0000 Subject: [PATCH 049/153] rename target manager to target applier --- .../controller/discovery/{target_manager.go => target_applier.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/controller/discovery/{target_manager.go => target_applier.go} (100%) diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_applier.go similarity index 100% rename from internal/controller/discovery/target_manager.go rename to internal/controller/discovery/target_applier.go From 5a561a768f1a2d17e1ed09a40b82884bc512527f Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 23 Apr 2026 13:07:25 +0000 Subject: [PATCH 050/153] implement a generic registry --- .../controller/discovery/registry/registry.go | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 internal/controller/discovery/registry/registry.go diff --git a/internal/controller/discovery/registry/registry.go b/internal/controller/discovery/registry/registry.go new file mode 100644 index 0000000..7da0757 --- /dev/null +++ b/internal/controller/discovery/registry/registry.go @@ -0,0 +1,61 @@ +package registry + +import ( + "fmt" + "sync" +) + +/* USAGE + +// create registry once in main.go +discoveryReg := discovery.NewRegistry[[]core.DiscoveryMessage]() + +// inside targetsource controller, when starting discovery pipeline: +key := fmt.Sprintf("%s/%s", spec.Namespace, targetsourceName) +if err := discoveryReg.Register(key, out); err != nil { + logger.Error(err, "could not register loader") + return err +} +defer discoveryReg.Unregister(key) + +// CHECK REGISTRY +ch, ok := discoveryReg.Get(ns + "/" + ts) +if !ok { + http.Error(w, "no loader for targetsource", http.StatusNotFound) + return +} +// then deliver payload to ch +*/ + +// Registry is a thread-safe map: key -> channel of T. +type Registry[T any] struct { + mu sync.RWMutex + m map[string]chan<- T +} + +func NewRegistry[T any]() *Registry[T] { + return &Registry[T]{m: make(map[string]chan<- T)} +} + +func (r *Registry[T]) Register(key string, ch chan<- T) error { + r.mu.Lock() + defer r.mu.Unlock() + if _, exists := r.m[key]; exists { + return fmt.Errorf("already registered: %s", key) + } + r.m[key] = ch + return nil +} + +func (r *Registry[T]) Unregister(key string) { + r.mu.Lock() + delete(r.m, key) + r.mu.Unlock() +} + +func (r *Registry[T]) Get(key string) (chan<- T, bool) { + r.mu.RLock() + ch, ok := r.m[key] + r.mu.RUnlock() + return ch, ok +} From f5481b8f9c7627d9c499c9156afdb3c7c2346146 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 23 Apr 2026 13:08:01 +0000 Subject: [PATCH 051/153] add a discoveryTegistry to share targetchannel between apiserver and target manager --- cmd/main.go | 14 +++++++++---- internal/apiserver/apiserver.go | 4 ++++ .../controller/targetsource_controller.go | 20 ++++++++++++++++--- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index eacdee5..5cf8169 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -40,6 +40,8 @@ import ( operatorv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/apiserver" "github.com/gnmic/operator/internal/controller" + "github.com/gnmic/operator/internal/controller/discovery/core" + "github.com/gnmic/operator/internal/controller/discovery/registry" webhookv1alpha1 "github.com/gnmic/operator/internal/webhook/v1alpha1" //+kubebuilder:scaffold:imports ) @@ -83,6 +85,8 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + discoveryRegistry := registry.NewRegistry[[]core.DiscoveryMessage]() + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{BindAddress: metricsAddr}, @@ -121,10 +125,11 @@ func main() { os.Exit(1) } if err := (&controller.TargetSourceReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - BufferSize: discoveryBufferSize, - ChunkSize: discoveryChunkSize, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + BufferSize: discoveryBufferSize, + ChunkSize: discoveryChunkSize, + DiscoveryRegistry: discoveryRegistry, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "TargetSource") os.Exit(1) @@ -226,6 +231,7 @@ func main() { if apiAddr != "" { apiServer := apiserver.New(apiAddr, clusterReconciler) + apiServer.DiscoveryRegistry = discoveryRegistry err = mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { errCh := make(chan error) go func() { diff --git a/internal/apiserver/apiserver.go b/internal/apiserver/apiserver.go index f31abaa..b84eb9a 100644 --- a/internal/apiserver/apiserver.go +++ b/internal/apiserver/apiserver.go @@ -5,11 +5,15 @@ import ( "net/http" "github.com/gnmic/operator/internal/controller" + "github.com/gnmic/operator/internal/controller/discovery/core" + "github.com/gnmic/operator/internal/controller/discovery/registry" ) type APIServer struct { Server *http.Server clusterReconciler *controller.ClusterReconciler + + DiscoveryRegistry *registry.Registry[[]core.DiscoveryMessage] } func New(addr string, clusterReconciler *controller.ClusterReconciler) *APIServer { diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index fce6742..c714acc 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -30,6 +30,7 @@ import ( "github.com/gnmic/operator/internal/controller/discovery" "github.com/gnmic/operator/internal/controller/discovery/core" _ "github.com/gnmic/operator/internal/controller/discovery/loaders/all" + "github.com/gnmic/operator/internal/controller/discovery/registry" ) const targetSourceFinalizer = "operator.gnmic.dev/targetsource-finalizer" @@ -48,6 +49,8 @@ type TargetSourceReconciler struct { BufferSize int ChunkSize int + + DiscoveryRegistry *registry.Registry[[]core.DiscoveryMessage] } // +kubebuilder:rbac:groups=operator.gnmic.dev,resources=targetsources,verbs=get;list;watch;create;update;patch;delete @@ -164,6 +167,12 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key client.ObjectKey, ta runtimeCtx, cancel := context.WithCancel(context.Background()) targetChannel := make(chan []core.DiscoveryMessage, r.BufferSize) + registryKey := key.Namespace + "/" + key.Name + if err := r.DiscoveryRegistry.Register(registryKey, targetChannel); err != nil { + cancel() + return err + } + // Start loader go loader.Start(runtimeCtx, targetSource.Name, targetSource.Spec, targetChannel) @@ -187,12 +196,17 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key client.ObjectKey, ta // for the given TargetSource key func (r *TargetSourceReconciler) stopDiscovery(key client.ObjectKey) { r.mu.Lock() - defer r.mu.Unlock() - - if running, ok := r.running[key]; ok { + running, ok := r.running[key] + if ok { running.cancel() delete(r.running, key) } + r.mu.Unlock() + + if ok { + registryKey := key.Namespace + "/" + key.Name + r.DiscoveryRegistry.Unregister(registryKey) + } } // SetupWithManager sets up the controller with the Manager. From 22683f4e4b0ee7853f45c8fce20c7d1646317162 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 23 Apr 2026 16:37:08 +0000 Subject: [PATCH 052/153] remove unused event action from DiscoverySnapshot --- internal/controller/discovery/core/types.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 69a407e..61209fd 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -27,7 +27,6 @@ type DiscoveryEvent struct { type DiscoverySnapshot struct { Targets []DiscoveredTarget - Event EventAction SnapshotID string IsLastChunk bool } From 922bbc6a6be0900f27e9aed9c09d6bce1c19caf6 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 24 Apr 2026 07:32:40 +0000 Subject: [PATCH 053/153] rename target manager to target applier --- .../controller/discovery/target_applier.go | 18 +++++++++--------- internal/controller/targetsource_controller.go | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/controller/discovery/target_applier.go b/internal/controller/discovery/target_applier.go index 153723c..3babebf 100644 --- a/internal/controller/discovery/target_applier.go +++ b/internal/controller/discovery/target_applier.go @@ -12,8 +12,8 @@ import ( "github.com/go-logr/logr" ) -// TargetManager consumes discovered targets and applies them to Kubernetes -type TargetManager struct { +// TargetApplier consumes discovered targets and applies them to Kubernetes +type TargetApplier struct { client client.Client scheme *runtime.Scheme targetSource *gnmicv1alpha1.TargetSource @@ -21,9 +21,9 @@ type TargetManager struct { collected map[string][]core.DiscoveredTarget } -// NewTargetManager wires a TargetManager instance -func NewTargetManager(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []core.DiscoveryMessage) *TargetManager { - return &TargetManager{ +// NewTargetApplier wires a TargetApplier instance +func NewTargetApplier(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []core.DiscoveryMessage) *TargetApplier { + return &TargetApplier{ client: c, scheme: s, targetSource: ts, @@ -34,16 +34,16 @@ func NewTargetManager(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.Targ // Run is a long‑running loop that receives target snapshots // and reconciles Target CRs accordingly -func (m *TargetManager) Run(ctx context.Context) error { +func (m *TargetApplier) Run(ctx context.Context) error { logger := log.FromContext(ctx). WithValues("targetSource", m.targetSource) - logger.Info("target manager started") + logger.Info("target applier started") for { select { case <-ctx.Done(): - logger.Info("target manager stopped") + logger.Info("target applier stopped") return nil case messages := <-m.in: @@ -83,7 +83,7 @@ func (m *TargetManager) Run(ctx context.Context) error { } // processSnapshot takes a complete snapshot of discovered targets and reconciles Target CRs accordingly -func (m *TargetManager) processSnapshot(snapshotID string, logger logr.Logger) { +func (m *TargetApplier) processSnapshot(snapshotID string, logger logr.Logger) { targets := m.collected[snapshotID] delete(m.collected, snapshotID) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index c714acc..78d64d0 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -176,8 +176,8 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key client.ObjectKey, ta // Start loader go loader.Start(runtimeCtx, targetSource.Name, targetSource.Spec, targetChannel) - // Start target manager - manager := discovery.NewTargetManager( + // Start target applier + manager := discovery.NewTargetApplier( r.Client, r.Scheme, targetSource, From 733927fa680c2896c83ee2863f7d2c2b24575448 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 24 Apr 2026 07:51:37 +0000 Subject: [PATCH 054/153] implement key for registry as a comparable --- cmd/main.go | 3 +- internal/apiserver/apiserver.go | 3 +- .../controller/discovery/registry/registry.go | 39 +++++-------------- .../controller/targetsource_controller.go | 13 +++---- 4 files changed, 19 insertions(+), 39 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 5cf8169..e4bad31 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,6 +28,7 @@ import ( certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" @@ -85,7 +86,7 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - discoveryRegistry := registry.NewRegistry[[]core.DiscoveryMessage]() + discoveryRegistry := registry.NewRegistry[types.NamespacedName, []core.DiscoveryMessage]() mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, diff --git a/internal/apiserver/apiserver.go b/internal/apiserver/apiserver.go index b84eb9a..17e5c82 100644 --- a/internal/apiserver/apiserver.go +++ b/internal/apiserver/apiserver.go @@ -7,13 +7,14 @@ import ( "github.com/gnmic/operator/internal/controller" "github.com/gnmic/operator/internal/controller/discovery/core" "github.com/gnmic/operator/internal/controller/discovery/registry" + "k8s.io/apimachinery/pkg/types" ) type APIServer struct { Server *http.Server clusterReconciler *controller.ClusterReconciler - DiscoveryRegistry *registry.Registry[[]core.DiscoveryMessage] + DiscoveryRegistry *registry.Registry[types.NamespacedName, []core.DiscoveryMessage] } func New(addr string, clusterReconciler *controller.ClusterReconciler) *APIServer { diff --git a/internal/controller/discovery/registry/registry.go b/internal/controller/discovery/registry/registry.go index 7da0757..1892d28e 100644 --- a/internal/controller/discovery/registry/registry.go +++ b/internal/controller/discovery/registry/registry.go @@ -5,39 +5,18 @@ import ( "sync" ) -/* USAGE - -// create registry once in main.go -discoveryReg := discovery.NewRegistry[[]core.DiscoveryMessage]() - -// inside targetsource controller, when starting discovery pipeline: -key := fmt.Sprintf("%s/%s", spec.Namespace, targetsourceName) -if err := discoveryReg.Register(key, out); err != nil { - logger.Error(err, "could not register loader") - return err -} -defer discoveryReg.Unregister(key) - -// CHECK REGISTRY -ch, ok := discoveryReg.Get(ns + "/" + ts) -if !ok { - http.Error(w, "no loader for targetsource", http.StatusNotFound) - return -} -// then deliver payload to ch -*/ - -// Registry is a thread-safe map: key -> channel of T. -type Registry[T any] struct { +// Registry is a thread-safe key -> channel registry +// K must be comparable so it can be used as a map key +type Registry[K comparable, V any] struct { mu sync.RWMutex - m map[string]chan<- T + m map[K]chan<- V } -func NewRegistry[T any]() *Registry[T] { - return &Registry[T]{m: make(map[string]chan<- T)} +func NewRegistry[K comparable, V any]() *Registry[K, V] { + return &Registry[K, V]{m: make(map[K]chan<- V)} } -func (r *Registry[T]) Register(key string, ch chan<- T) error { +func (r *Registry[K, V]) Register(key K, ch chan<- V) error { r.mu.Lock() defer r.mu.Unlock() if _, exists := r.m[key]; exists { @@ -47,13 +26,13 @@ func (r *Registry[T]) Register(key string, ch chan<- T) error { return nil } -func (r *Registry[T]) Unregister(key string) { +func (r *Registry[K, V]) Unregister(key K) { r.mu.Lock() delete(r.m, key) r.mu.Unlock() } -func (r *Registry[T]) Get(key string) (chan<- T, bool) { +func (r *Registry[K, V]) Get(key K) (chan<- V, bool) { r.mu.RLock() ch, ok := r.m[key] r.mu.RUnlock() diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 78d64d0..d97b3a6 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -21,6 +21,7 @@ import ( "sync" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -50,7 +51,7 @@ type TargetSourceReconciler struct { BufferSize int ChunkSize int - DiscoveryRegistry *registry.Registry[[]core.DiscoveryMessage] + DiscoveryRegistry *registry.Registry[types.NamespacedName, []core.DiscoveryMessage] } // +kubebuilder:rbac:groups=operator.gnmic.dev,resources=targetsources,verbs=get;list;watch;create;update;patch;delete @@ -149,7 +150,7 @@ func (r *TargetSourceReconciler) isPipelineRunning(key client.ObjectKey) bool { } // startDiscoveryPipeline creates and starts the loader and target manager -func (r *TargetSourceReconciler) startDiscoveryPipeline(key client.ObjectKey, targetSource *gnmicv1alpha1.TargetSource) error { +func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource) error { cfg := core.LoaderConfig{ ChunkSize: r.ChunkSize, } @@ -167,8 +168,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key client.ObjectKey, ta runtimeCtx, cancel := context.WithCancel(context.Background()) targetChannel := make(chan []core.DiscoveryMessage, r.BufferSize) - registryKey := key.Namespace + "/" + key.Name - if err := r.DiscoveryRegistry.Register(registryKey, targetChannel); err != nil { + if err := r.DiscoveryRegistry.Register(key, targetChannel); err != nil { cancel() return err } @@ -194,7 +194,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key client.ObjectKey, ta // stopDiscovery stops and removes a running discovery pipeline // for the given TargetSource key -func (r *TargetSourceReconciler) stopDiscovery(key client.ObjectKey) { +func (r *TargetSourceReconciler) stopDiscovery(key types.NamespacedName) { r.mu.Lock() running, ok := r.running[key] if ok { @@ -204,8 +204,7 @@ func (r *TargetSourceReconciler) stopDiscovery(key client.ObjectKey) { r.mu.Unlock() if ok { - registryKey := key.Namespace + "/" + key.Name - r.DiscoveryRegistry.Unregister(registryKey) + r.DiscoveryRegistry.Unregister(key) } } From 9d305601d18ae0f9f4d9f0168ec799b15e8b4a2a Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 24 Apr 2026 08:10:41 +0000 Subject: [PATCH 055/153] fix error message and add a word of caution for key comparables --- internal/controller/discovery/registry/registry.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/controller/discovery/registry/registry.go b/internal/controller/discovery/registry/registry.go index 1892d28e..093bd2c 100644 --- a/internal/controller/discovery/registry/registry.go +++ b/internal/controller/discovery/registry/registry.go @@ -7,6 +7,7 @@ import ( // Registry is a thread-safe key -> channel registry // K must be comparable so it can be used as a map key +// DO NOT USE a pointer type as K type Registry[K comparable, V any] struct { mu sync.RWMutex m map[K]chan<- V @@ -20,7 +21,7 @@ func (r *Registry[K, V]) Register(key K, ch chan<- V) error { r.mu.Lock() defer r.mu.Unlock() if _, exists := r.m[key]; exists { - return fmt.Errorf("already registered: %s", key) + return fmt.Errorf("already registered: %v", key) } r.m[key] = ch return nil From dafa82bb1fd1fbbb5369d14ff82594be38b19ddb Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 24 Apr 2026 08:11:17 +0000 Subject: [PATCH 056/153] consistently use namespaced name as refference to the targetsource --- .../discovery/core/loader_interface.go | 3 ++- internal/controller/discovery/loader.go | 8 ++++---- .../controller/discovery/loaders/http/loader.go | 11 ++++++----- internal/controller/targetsource_controller.go | 17 ++++++++--------- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/internal/controller/discovery/core/loader_interface.go b/internal/controller/discovery/core/loader_interface.go index 17cd5f4..8964be8 100644 --- a/internal/controller/discovery/core/loader_interface.go +++ b/internal/controller/discovery/core/loader_interface.go @@ -4,6 +4,7 @@ import ( "context" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" + "k8s.io/apimachinery/pkg/types" ) // Loader defines a pluggable TargetSource loader interface @@ -16,7 +17,7 @@ type Loader interface { // The loader must stop cleanly when ctx is cancelled Start( ctx context.Context, - targetsourceName string, + targetsourceName types.NamespacedName, spec gnmicv1alpha1.TargetSourceSpec, out chan<- []DiscoveryMessage, ) error diff --git a/internal/controller/discovery/loader.go b/internal/controller/discovery/loader.go index 42ce8da..0d8ddd3 100644 --- a/internal/controller/discovery/loader.go +++ b/internal/controller/discovery/loader.go @@ -6,19 +6,19 @@ import ( gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery/core" http "github.com/gnmic/operator/internal/controller/discovery/loaders/http" + "k8s.io/apimachinery/pkg/types" ) // NewLoader creates a loader by name -func NewLoader(name string, namespace string, spec gnmicv1alpha1.TargetSourceSpec, cfg core.LoaderConfig) (core.Loader, error) { - loaderName := namespace + "/" + name +func NewLoader(name types.NamespacedName, spec gnmicv1alpha1.TargetSourceSpec, cfg core.LoaderConfig) (core.Loader, error) { switch { case spec.Provider.HTTP != nil: return http.New(cfg), nil case spec.Provider.Consul != nil: - return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", loaderName) + return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", name) default: - return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", loaderName) + return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", name) } } diff --git a/internal/controller/discovery/loaders/http/loader.go b/internal/controller/discovery/loaders/http/loader.go index f014a2f..09bb7d6 100644 --- a/internal/controller/discovery/loaders/http/loader.go +++ b/internal/controller/discovery/loaders/http/loader.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/log" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" @@ -27,14 +28,14 @@ func (l *Loader) Name() string { func (l *Loader) Start( ctx context.Context, - targetsourceName string, + targetsourceNN types.NamespacedName, spec gnmicv1alpha1.TargetSourceSpec, out chan<- []core.DiscoveryMessage, ) error { logger := log.FromContext(ctx).WithValues( "component", "loader", "name", l.Name(), - "targetsource", targetsourceName, + "targetsource", targetsourceNN, ) logger.Info("HTTP loader started") @@ -51,17 +52,17 @@ func (l *Loader) Start( case <-ticker.C: // Example snapshot (placeholder) - snapshotID := fmt.Sprintf("snapshot-%s-%s", targetsourceName, uuid.NewString()) + snapshotID := fmt.Sprintf("snapshot-%s-%s", targetsourceNN, uuid.NewString()) targets := []core.DiscoveredTarget{ { Name: "ceos1", Address: "clab-3-nodes-ceos1:6030", - Labels: map[string]string{"TargetSource": targetsourceName}, + Labels: map[string]string{"TargetSource": targetsourceNN.String()}, }, { Name: "leaf1", Address: "clab-3-nodes-leaf1:57400", - Labels: map[string]string{"TargetSource": targetsourceName}, + Labels: map[string]string{"TargetSource": targetsourceNN.String()}, }, } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index d97b3a6..62b057d 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -46,7 +46,7 @@ type TargetSourceReconciler struct { Scheme *runtime.Scheme mu sync.Mutex - running map[client.ObjectKey]runningSource + running map[types.NamespacedName]runningSource BufferSize int ChunkSize int @@ -96,7 +96,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request } // getTargetSource retrieves a TargetSource by name, handling cleanup if not found -func (r *TargetSourceReconciler) getTargetSource(ctx context.Context, key client.ObjectKey) (*gnmicv1alpha1.TargetSource, error) { +func (r *TargetSourceReconciler) getTargetSource(ctx context.Context, key types.NamespacedName) (*gnmicv1alpha1.TargetSource, error) { var targetSource gnmicv1alpha1.TargetSource if err := r.Get(ctx, key, &targetSource); err != nil { // If the TargetSource no longer exists, ensure runtime cleanup @@ -109,9 +109,9 @@ func (r *TargetSourceReconciler) getTargetSource(ctx context.Context, key client } // handleTargetSourceDeletion stops the discovery pipeline and removes the finalizer -func (r *TargetSourceReconciler) handleTargetSourceDeletion(ctx context.Context, key client.ObjectKey, targetSource *gnmicv1alpha1.TargetSource) (ctrl.Result, error) { +func (r *TargetSourceReconciler) handleTargetSourceDeletion(ctx context.Context, key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource) (ctrl.Result, error) { logger := log.FromContext(ctx) - logger.Info("TargetSource is being deleted, stopping pipeline", "name", targetSource.Name) + logger.Info("TargetSource is being deleted, stopping pipeline", "name", key) r.stopDiscovery(key) @@ -141,7 +141,7 @@ func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSour } // isPipelineRunning checks if a discovery pipeline is already running for the given key -func (r *TargetSourceReconciler) isPipelineRunning(key client.ObjectKey) bool { +func (r *TargetSourceReconciler) isPipelineRunning(key types.NamespacedName) bool { r.mu.Lock() defer r.mu.Unlock() @@ -156,8 +156,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName } loader, err := discovery.NewLoader( - targetSource.ObjectMeta.Name, - targetSource.ObjectMeta.Namespace, + key, targetSource.Spec, cfg, ) @@ -174,7 +173,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName } // Start loader - go loader.Start(runtimeCtx, targetSource.Name, targetSource.Spec, targetChannel) + go loader.Start(runtimeCtx, key, targetSource.Spec, targetChannel) // Start target applier manager := discovery.NewTargetApplier( @@ -210,7 +209,7 @@ func (r *TargetSourceReconciler) stopDiscovery(key types.NamespacedName) { // SetupWithManager sets up the controller with the Manager. func (r *TargetSourceReconciler) SetupWithManager(mgr ctrl.Manager) error { - r.running = make(map[client.ObjectKey]runningSource) + r.running = make(map[types.NamespacedName]runningSource) return ctrl.NewControllerManagedBy(mgr). For(&gnmicv1alpha1.TargetSource{}). From 2973c03a665beeb3b53ef7ff71d55921c21053e1 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 24 Apr 2026 09:05:21 +0000 Subject: [PATCH 057/153] improve context cancling and error handling --- .../controller/discovery/target_applier.go | 102 ++++++++++++------ 1 file changed, 68 insertions(+), 34 deletions(-) diff --git a/internal/controller/discovery/target_applier.go b/internal/controller/discovery/target_applier.go index 3babebf..7fed5c9 100644 --- a/internal/controller/discovery/target_applier.go +++ b/internal/controller/discovery/target_applier.go @@ -37,49 +37,83 @@ func NewTargetApplier(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.Targ func (m *TargetApplier) Run(ctx context.Context) error { logger := log.FromContext(ctx). WithValues("targetSource", m.targetSource) - logger.Info("target applier started") - for { + queue := make([]core.DiscoveryMessage, 0, 265) + + for ctx.Err() == nil { select { + case batch, ok := <-m.in: + if !ok { + // Channel closed, pipeline is shutting down + logger.Info("input channel closed, stopping target applier") + return nil + } + queue = append(queue, batch...) + case <-ctx.Done(): - logger.Info("target applier stopped") + logger.Info("context canceled, stopping target applier") return nil + } - case messages := <-m.in: - for _, message := range messages { - // Type assert to determine if this is a snapshot or event - switch msg := message.(type) { - case core.DiscoverySnapshot: - // Collect snapshot chunks - logger.Info( - "received snapshot chunk", - "snapshotID", msg.SnapshotID, - "targetCount", len(msg.Targets), - ) - m.collected[msg.SnapshotID] = append(m.collected[msg.SnapshotID], msg.Targets...) - if msg.IsLastChunk { - m.processSnapshot(msg.SnapshotID, logger) - } - - case core.DiscoveryEvent: - // Process individual event-driven update - logger.Info( - "received discovery event", - "target", msg.Target.Name, - ) - switch msg.Event { - case core.CREATE: - logger.Info("Would create target", "name", msg.Target.Name, "address", msg.Target.Address, "labels", msg.Target.Labels) - case core.UPDATE: - logger.Info("Would update target", "name", msg.Target.Name, "address", msg.Target.Address, "labels", msg.Target.Labels) - case core.DELETE: - logger.Info("Would delete target", "name", msg.Target.Name) - } - } + for len(queue) > 0 { + if ctx.Err() != nil { + break } + + msg := queue[0] + queue = queue[1:] + + if err := m.handleMessage(ctx, msg, logger); err != nil { + // Returning error lets the supervisor (controller) + // tear down and restart the pipeline via reconciliation + // Q: when to return an error vs just log and continue? + return err + } + } } + + logger.Info("target applier stopped") + return nil +} + +func (m *TargetApplier) handleMessage(ctx context.Context, message core.DiscoveryMessage, logger logr.Logger) error { + if err := ctx.Err(); err != nil { + return err + } + + // Type assert to determine if this is a snapshot or event + switch msg := message.(type) { + case core.DiscoverySnapshot: + // Collect snapshot chunks + logger.Info( + "received snapshot chunk", + "snapshotID", msg.SnapshotID, + "targetCount", len(msg.Targets), + ) + m.collected[msg.SnapshotID] = append(m.collected[msg.SnapshotID], msg.Targets...) + if msg.IsLastChunk { + m.processSnapshot(msg.SnapshotID, logger) + } + + case core.DiscoveryEvent: + // Process individual event-driven update + logger.Info( + "received discovery event", + "target", msg.Target.Name, + ) + switch msg.Event { + case core.CREATE: + logger.Info("Would create target", "name", msg.Target.Name, "address", msg.Target.Address, "labels", msg.Target.Labels) + case core.UPDATE: + logger.Info("Would update target", "name", msg.Target.Name, "address", msg.Target.Address, "labels", msg.Target.Labels) + case core.DELETE: + logger.Info("Would delete target", "name", msg.Target.Name) + } + } + + return nil } // processSnapshot takes a complete snapshot of discovered targets and reconciles Target CRs accordingly From c95bdaf389038386a0b0b98759c98d4c10cb3f31 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 24 Apr 2026 10:01:31 +0000 Subject: [PATCH 058/153] add supervised goroutines --- .../controller/targetsource_controller.go | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 62b057d..9fad373 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -18,6 +18,7 @@ package controller import ( "context" + "fmt" "sync" "k8s.io/apimachinery/pkg/runtime" @@ -172,17 +173,45 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName return err } + // goroutines use done channel to report termination (nil or error) back to supervisor + // Buffer size = supervised goroutines = 2 (loader + applier) + done := make(chan error, 2) + // Start loader - go loader.Start(runtimeCtx, key, targetSource.Spec, targetChannel) + go runWithRecovery( + runtimeCtx, + "loader", + func(ctx context.Context) error { + return loader.Start(ctx, key, targetSource.Spec, targetChannel) + }, + done, + ) // Start target applier - manager := discovery.NewTargetApplier( + applier := discovery.NewTargetApplier( r.Client, r.Scheme, targetSource, targetChannel, ) - go manager.Run(runtimeCtx) + go runWithRecovery( + runtimeCtx, + "target-applier", + applier.Run, + done, + ) + + // Supervision goroutine to handle pipeline termination + go func() { + err := <-done + logger := log.FromContext(context.Background()).WithValues("targetSource", key) + if err != nil { + logger.Error(err, "Discovery pipeline terminated with error") + } + + // Ensure cleanup on termination + r.stopDiscovery(key) + }() r.mu.Lock() r.running[key] = runningSource{cancel: cancel} @@ -207,6 +236,25 @@ func (r *TargetSourceReconciler) stopDiscovery(key types.NamespacedName) { } } +// runWithRecovery executes a worker function under panic protection +// and reports termination (nil or error) through done. +func runWithRecovery( + ctx context.Context, + name string, + run func(context.Context) error, + done chan<- error, +) { + defer func() { + if r := recover(); r != nil { + done <- fmt.Errorf("panic in %s: %v", name, r) + } + }() + + // Normal exit path + err := run(ctx) + done <- err +} + // SetupWithManager sets up the controller with the Manager. func (r *TargetSourceReconciler) SetupWithManager(mgr ctrl.Manager) error { r.running = make(map[types.NamespacedName]runningSource) From 0aa883d98c940ebf374c6b9492522e63a601ac6d Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 24 Apr 2026 12:52:08 +0000 Subject: [PATCH 059/153] refactor target applier --- internal/controller/discovery/target_applier.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/controller/discovery/target_applier.go b/internal/controller/discovery/target_applier.go index 7fed5c9..c60f2b8 100644 --- a/internal/controller/discovery/target_applier.go +++ b/internal/controller/discovery/target_applier.go @@ -36,10 +36,14 @@ func NewTargetApplier(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.Targ // and reconciles Target CRs accordingly func (m *TargetApplier) Run(ctx context.Context) error { logger := log.FromContext(ctx). - WithValues("targetSource", m.targetSource) + WithValues( + "name", m.targetSource.Name, + "namespace", m.targetSource.Namespace, + ) + logger.Info("target applier started") - queue := make([]core.DiscoveryMessage, 0, 265) + queue := []core.DiscoveryMessage{} for ctx.Err() == nil { select { @@ -58,7 +62,7 @@ func (m *TargetApplier) Run(ctx context.Context) error { for len(queue) > 0 { if ctx.Err() != nil { - break + return ctx.Err() } msg := queue[0] From 27b2b1f711a4f60edd2609d8e2822adbfaf07991 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 24 Apr 2026 14:15:34 +0000 Subject: [PATCH 060/153] add supervisor for the discovery pipelines --- internal/controller/discovery/supervisor.go | 123 ++++++++++++++++++ .../controller/targetsource_controller.go | 117 +++++++---------- 2 files changed, 171 insertions(+), 69 deletions(-) create mode 100644 internal/controller/discovery/supervisor.go diff --git a/internal/controller/discovery/supervisor.go b/internal/controller/discovery/supervisor.go new file mode 100644 index 0000000..ff19604 --- /dev/null +++ b/internal/controller/discovery/supervisor.go @@ -0,0 +1,123 @@ +package discovery + +import ( + "context" + "sync" + "time" + + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type ComponentExit struct { + Name string + Err error +} + +type RestartPolicy struct { + MaxRestarts int + Backoff time.Duration +} + +type Supervisor struct { + ctx context.Context + cancel context.CancelFunc + policy RestartPolicy + failures int + exits chan ComponentExit + wg sync.WaitGroup + stopped bool + stopMu sync.Mutex +} + +func NewSupervisor(parentCtx context.Context, policy RestartPolicy) *Supervisor { + ctx, cancel := context.WithCancel(parentCtx) + return &Supervisor{ + ctx: ctx, + cancel: cancel, + policy: policy, + exits: make(chan ComponentExit, 4), + failures: 0, + } +} + +func (s *Supervisor) Context() context.Context { + return s.ctx +} + +func (s *Supervisor) Stop() { + s.stopMu.Lock() + defer s.stopMu.Unlock() + + if s.stopped { + return + } + + s.stopped = true + s.cancel() +} + +func (s *Supervisor) Run( + start func(ctx context.Context, exits chan<- ComponentExit), +) error { + logger := log.FromContext(s.ctx).WithName("discovery-supervisor") + + for { + if s.failures > 0 { + logger.Info("Restarting pipeline", + "attempt", s.failures, + "maxAttempts", s.policy.MaxRestarts, + ) + + runtimeCtx, cancel := context.WithCancel(s.ctx) + s.wg = sync.WaitGroup{} + start(runtimeCtx, s.exits) + exit := <-s.exits // first failure wins + + logger.Error(exit.Err, + "Pipeline component crashed", + "component", exit.Name, + ) + + cancel() + s.wg.Wait() + + s.failures++ + if s.failures >= s.policy.MaxRestarts { + logger.Error(exit.Err, + "Pipeline exceeded maximum restart attempts; waiting for next reconciliation to restart", + "restarts", s.failures, + ) + s.Stop() + return exit.Err + } + + select { + case <-time.After(s.policy.Backoff): + // continue to restart + case <-s.ctx.Done(): + // Supervisor context canceled during backoff + return s.ctx.Err() + } + } + } +} + +func (s *Supervisor) Go(name string, fn func(ctx context.Context) error) { + s.wg.Add(1) + + go func() { + defer s.wg.Done() + + err := fn(s.ctx) + if err == nil { + err = context.Canceled // treat normal exit as cancellation + } + + select { + case s.exits <- ComponentExit{Name: name, Err: err}: + // exit reported successfully + case <-s.ctx.Done(): + // Supervisor context canceled before reporting exit + } + }() +} diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 9fad373..5d83db9 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -18,8 +18,8 @@ package controller import ( "context" - "fmt" "sync" + "time" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -33,9 +33,14 @@ import ( "github.com/gnmic/operator/internal/controller/discovery/core" _ "github.com/gnmic/operator/internal/controller/discovery/loaders/all" "github.com/gnmic/operator/internal/controller/discovery/registry" + "github.com/go-logr/logr" ) -const targetSourceFinalizer = "operator.gnmic.dev/targetsource-finalizer" +const ( + targetSourceFinalizer = "operator.gnmic.dev/targetsource-finalizer" + pipelineMaxRestarts = 5 + pipelineBackoff = 3 * time.Second +) type runningSource struct { cancel context.CancelFunc @@ -63,8 +68,8 @@ type TargetSourceReconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx).WithValues( - "Name", req.NamespacedName, + logger := log.FromContext(ctx).WithName("targetsource controller").WithValues( + "targetsource", req.NamespacedName, ) targetSource, err := r.getTargetSource(ctx, req.NamespacedName) @@ -88,7 +93,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request } // Start discovery pipeline - if err := r.startDiscoveryPipeline(req.NamespacedName, targetSource); err != nil { + if err := r.startDiscoveryPipeline(req.NamespacedName, targetSource, logger); err != nil { return ctrl.Result{}, err } @@ -151,70 +156,63 @@ func (r *TargetSourceReconciler) isPipelineRunning(key types.NamespacedName) boo } // startDiscoveryPipeline creates and starts the loader and target manager -func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource) error { - cfg := core.LoaderConfig{ - ChunkSize: r.ChunkSize, - } - - loader, err := discovery.NewLoader( - key, - targetSource.Spec, - cfg, +func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource, logger logr.Logger) error { + supervisor := discovery.NewSupervisor( + context.Background(), + discovery.RestartPolicy{ + MaxRestarts: pipelineMaxRestarts, + Backoff: pipelineBackoff, + }, ) - if err != nil { - return err - } - runtimeCtx, cancel := context.WithCancel(context.Background()) targetChannel := make(chan []core.DiscoveryMessage, r.BufferSize) - if err := r.DiscoveryRegistry.Register(key, targetChannel); err != nil { - cancel() return err } - // goroutines use done channel to report termination (nil or error) back to supervisor - // Buffer size = supervised goroutines = 2 (loader + applier) - done := make(chan error, 2) + start := func(ctx context.Context, exits chan<- discovery.ComponentExit) { + // Create loader instance + loader, err := discovery.NewLoader( + key, + targetSource.Spec, + core.LoaderConfig{ + ChunkSize: r.ChunkSize, + }, + ) + if err != nil { + return + } + + // Create target applier instance + applier := discovery.NewTargetApplier( + r.Client, + r.Scheme, + targetSource, + targetChannel, + ) - // Start loader - go runWithRecovery( - runtimeCtx, - "loader", - func(ctx context.Context) error { + // Start loader + supervisor.Go("loader", func(ctx context.Context) error { return loader.Start(ctx, key, targetSource.Spec, targetChannel) - }, - done, - ) + }) + // Start target applier + supervisor.Go("target-applier", applier.Run) - // Start target applier - applier := discovery.NewTargetApplier( - r.Client, - r.Scheme, - targetSource, - targetChannel, - ) - go runWithRecovery( - runtimeCtx, - "target-applier", - applier.Run, - done, - ) + } - // Supervision goroutine to handle pipeline termination go func() { - err := <-done - logger := log.FromContext(context.Background()).WithValues("targetSource", key) + err := supervisor.Run(start) if err != nil { - logger.Error(err, "Discovery pipeline terminated with error") + logger.Error(err, "Discovery pipeline stopped permanently") } - // Ensure cleanup on termination + close(targetChannel) + r.DiscoveryRegistry.Unregister(key) r.stopDiscovery(key) }() r.mu.Lock() - r.running[key] = runningSource{cancel: cancel} + r.running[key] = runningSource{cancel: supervisor.Stop} r.mu.Unlock() return nil @@ -236,25 +234,6 @@ func (r *TargetSourceReconciler) stopDiscovery(key types.NamespacedName) { } } -// runWithRecovery executes a worker function under panic protection -// and reports termination (nil or error) through done. -func runWithRecovery( - ctx context.Context, - name string, - run func(context.Context) error, - done chan<- error, -) { - defer func() { - if r := recover(); r != nil { - done <- fmt.Errorf("panic in %s: %v", name, r) - } - }() - - // Normal exit path - err := run(ctx) - done <- err -} - // SetupWithManager sets up the controller with the Manager. func (r *TargetSourceReconciler) SetupWithManager(mgr ctrl.Manager) error { r.running = make(map[types.NamespacedName]runningSource) From 22fe2d894e2109c817a11b3153f298ba0fb8eb06 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 24 Apr 2026 14:55:21 +0000 Subject: [PATCH 061/153] improve readability --- internal/controller/targetsource_controller.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 5d83db9..db60520 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -38,8 +38,9 @@ import ( const ( targetSourceFinalizer = "operator.gnmic.dev/targetsource-finalizer" - pipelineMaxRestarts = 5 - pipelineBackoff = 3 * time.Second + + pipelineMaxRestarts = 5 + pipelineBackoff = 3 * time.Second ) type runningSource struct { From 58538c76c0583e031b56031f72e639450c918910 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 24 Apr 2026 15:02:48 +0000 Subject: [PATCH 062/153] remove side-effects from getter getTargetSource --- internal/controller/targetsource_controller.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index db60520..33342b4 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -75,6 +75,12 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request targetSource, err := r.getTargetSource(ctx, req.NamespacedName) if err != nil { + // If the TargetSource no longer exists, ensure runtime cleanup + if client.IgnoreNotFound(err) == nil { + logger.Info("TargetSource not found, ensuring cleanup") + r.stopDiscovery(req.NamespacedName) + return ctrl.Result{}, nil + } return ctrl.Result{}, err } @@ -106,11 +112,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request func (r *TargetSourceReconciler) getTargetSource(ctx context.Context, key types.NamespacedName) (*gnmicv1alpha1.TargetSource, error) { var targetSource gnmicv1alpha1.TargetSource if err := r.Get(ctx, key, &targetSource); err != nil { - // If the TargetSource no longer exists, ensure runtime cleanup - if client.IgnoreNotFound(err) == nil { - r.stopDiscovery(key) - } - return nil, client.IgnoreNotFound(err) + return nil, err } return &targetSource, nil } From 4f0457ec86f4ed5df64a4216aadc8e3fc3551391 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 24 Apr 2026 17:38:41 +0000 Subject: [PATCH 063/153] redesign supervisor --- internal/controller/discovery/supervisor.go | 145 ++++++++---------- .../controller/targetsource_controller.go | 85 +++++----- 2 files changed, 106 insertions(+), 124 deletions(-) diff --git a/internal/controller/discovery/supervisor.go b/internal/controller/discovery/supervisor.go index ff19604..c716965 100644 --- a/internal/controller/discovery/supervisor.go +++ b/internal/controller/discovery/supervisor.go @@ -8,116 +8,93 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) -type ComponentExit struct { - Name string - Err error -} - type RestartPolicy struct { MaxRestarts int Backoff time.Duration } +type Component struct { + Name string + Run func(ctx context.Context) error + Policy RestartPolicy +} + type Supervisor struct { - ctx context.Context - cancel context.CancelFunc - policy RestartPolicy - failures int - exits chan ComponentExit - wg sync.WaitGroup - stopped bool - stopMu sync.Mutex + ctx context.Context + cancel context.CancelFunc + + stopped bool + mu sync.Mutex + + components []Component } -func NewSupervisor(parentCtx context.Context, policy RestartPolicy) *Supervisor { - ctx, cancel := context.WithCancel(parentCtx) +func NewSupervisor(parent context.Context) *Supervisor { + ctx, cancel := context.WithCancel(parent) return &Supervisor{ - ctx: ctx, - cancel: cancel, - policy: policy, - exits: make(chan ComponentExit, 4), - failures: 0, + ctx: ctx, + cancel: cancel, } } -func (s *Supervisor) Context() context.Context { - return s.ctx +func (s *Supervisor) AddComponent(c Component) { + s.components = append(s.components, c) } -func (s *Supervisor) Stop() { - s.stopMu.Lock() - defer s.stopMu.Unlock() +func (s *Supervisor) runComponent(c Component) { + logger := log.FromContext(s.ctx).WithValues( + "component", c.Name, + ) - if s.stopped { - return - } - - s.stopped = true - s.cancel() -} - -func (s *Supervisor) Run( - start func(ctx context.Context, exits chan<- ComponentExit), -) error { - logger := log.FromContext(s.ctx).WithName("discovery-supervisor") + failures := 0 for { - if s.failures > 0 { - logger.Info("Restarting pipeline", - "attempt", s.failures, - "maxAttempts", s.policy.MaxRestarts, - ) + err := c.Run(s.ctx) + if s.ctx.Err() != nil { + return + } - runtimeCtx, cancel := context.WithCancel(s.ctx) - s.wg = sync.WaitGroup{} - start(runtimeCtx, s.exits) - exit := <-s.exits // first failure wins + failures++ + logger.Error(err, + "Component failed", + "attempt", failures, + ) - logger.Error(exit.Err, - "Pipeline component crashed", - "component", exit.Name, + if failures >= c.Policy.MaxRestarts { + logger.Error(err, + "Component exceeded restart limit; stopping discovery pipeline", + "restarts", failures, ) + s.Stop() + return + } - cancel() - s.wg.Wait() - - s.failures++ - if s.failures >= s.policy.MaxRestarts { - logger.Error(exit.Err, - "Pipeline exceeded maximum restart attempts; waiting for next reconciliation to restart", - "restarts", s.failures, - ) - s.Stop() - return exit.Err - } - - select { - case <-time.After(s.policy.Backoff): - // continue to restart - case <-s.ctx.Done(): - // Supervisor context canceled during backoff - return s.ctx.Err() - } + select { + case <-time.After(c.Policy.Backoff): + case <-s.ctx.Done(): + return } } } -func (s *Supervisor) Go(name string, fn func(ctx context.Context) error) { - s.wg.Add(1) +func (s *Supervisor) Run() { + for _, c := range s.components { + component := c + go s.runComponent(component) + } +} - go func() { - defer s.wg.Done() +func (s *Supervisor) Stop() { + s.mu.Lock() + defer s.mu.Unlock() - err := fn(s.ctx) - if err == nil { - err = context.Canceled // treat normal exit as cancellation - } + if s.stopped { + return + } + s.stopped = true + s.cancel() +} - select { - case s.exits <- ComponentExit{Name: name, Err: err}: - // exit reported successfully - case <-s.ctx.Done(): - // Supervisor context canceled before reporting exit - } - }() +func (s *Supervisor) Done() <-chan struct{} { + return s.ctx.Done() } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 33342b4..68f47eb 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -160,54 +160,57 @@ func (r *TargetSourceReconciler) isPipelineRunning(key types.NamespacedName) boo // startDiscoveryPipeline creates and starts the loader and target manager func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource, logger logr.Logger) error { - supervisor := discovery.NewSupervisor( - context.Background(), - discovery.RestartPolicy{ - MaxRestarts: pipelineMaxRestarts, - Backoff: pipelineBackoff, - }, - ) + supervisor := discovery.NewSupervisor(context.Background()) targetChannel := make(chan []core.DiscoveryMessage, r.BufferSize) if err := r.DiscoveryRegistry.Register(key, targetChannel); err != nil { return err } - start := func(ctx context.Context, exits chan<- discovery.ComponentExit) { - // Create loader instance - loader, err := discovery.NewLoader( - key, - targetSource.Spec, - core.LoaderConfig{ - ChunkSize: r.ChunkSize, - }, - ) - if err != nil { - return - } + // Create loader instance + loader, err := discovery.NewLoader( + key, + targetSource.Spec, + core.LoaderConfig{ChunkSize: r.ChunkSize}, + ) + if err != nil { + return err + } - // Create target applier instance - applier := discovery.NewTargetApplier( - r.Client, - r.Scheme, - targetSource, - targetChannel, - ) + // Create target applier instance + applier := discovery.NewTargetApplier( + r.Client, + r.Scheme, + targetSource, + targetChannel, + ) - // Start loader - supervisor.Go("loader", func(ctx context.Context) error { + supervisor.AddComponent(discovery.Component{ + Name: "loader", + Run: func(ctx context.Context) error { return loader.Start(ctx, key, targetSource.Spec, targetChannel) - }) - // Start target applier - supervisor.Go("target-applier", applier.Run) + }, + Policy: discovery.RestartPolicy{ + MaxRestarts: pipelineMaxRestarts, + Backoff: pipelineBackoff, + }, + }) - } + supervisor.AddComponent(discovery.Component{ + Name: "target-applier", + Run: applier.Run, + Policy: discovery.RestartPolicy{ + MaxRestarts: pipelineMaxRestarts, + Backoff: pipelineBackoff, + }, + }) + + supervisor.Run() go func() { - err := supervisor.Run(start) - if err != nil { - logger.Error(err, "Discovery pipeline stopped permanently") - } + <-supervisor.Done() + + logger.Info("Pipeline stopped; performing final cleanup") close(targetChannel) r.DiscoveryRegistry.Unregister(key) @@ -215,25 +218,27 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName }() r.mu.Lock() - r.running[key] = runningSource{cancel: supervisor.Stop} + r.running[key] = runningSource{ + cancel: func() { + supervisor.Stop() + }, + } r.mu.Unlock() return nil } // stopDiscovery stops and removes a running discovery pipeline -// for the given TargetSource key func (r *TargetSourceReconciler) stopDiscovery(key types.NamespacedName) { r.mu.Lock() running, ok := r.running[key] if ok { - running.cancel() delete(r.running, key) } r.mu.Unlock() if ok { - r.DiscoveryRegistry.Unregister(key) + running.cancel() } } From 2b728c4ff0287a1a9dd7137eee1b1c54f9d72dca Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Fri, 24 Apr 2026 15:15:56 -0600 Subject: [PATCH 064/153] added const file for common labels --- internal/controller/discovery/core/const.go | 6 ++++++ internal/controller/discovery/target_manager.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 internal/controller/discovery/core/const.go diff --git a/internal/controller/discovery/core/const.go b/internal/controller/discovery/core/const.go new file mode 100644 index 0000000..82a5962 --- /dev/null +++ b/internal/controller/discovery/core/const.go @@ -0,0 +1,6 @@ +package core + +const ( + // Labels + LabelTargetSourceName = "operator.gnmic.dev/targetsource" +) diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_manager.go index c9ac079..a9cd463 100644 --- a/internal/controller/discovery/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -175,7 +175,7 @@ func (m *TargetManager) applyTarget(ctx context.Context, name string, address st _, err := controllerutil.CreateOrUpdate(ctx, m.client, target, func() error { labels := map[string]string{ - "gnmic.io/source": m.targetSource.Name, + core.LabelTargetSourceName: m.targetSource.Name, } maps.Copy(labels, m.targetSource.Spec.TargetLabels) From 5abbd63ed664e58047785b445ae2b64c54ea7a9f Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Fri, 24 Apr 2026 15:17:09 -0600 Subject: [PATCH 065/153] simplified name and namespace calls for objects --- internal/controller/discovery/target_manager.go | 4 ++-- internal/controller/targetsource_controller.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_manager.go index a9cd463..be6b1fe 100644 --- a/internal/controller/discovery/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -42,8 +42,8 @@ func NewTargetManager(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.Targ func (m *TargetManager) Run(ctx context.Context) error { logger := log.FromContext(ctx). WithValues( - "targetSource", m.targetSource.ObjectMeta.Name, - "namespace", m.targetSource.ObjectMeta.Namespace, + "targetSource", m.targetSource.Name, + "namespace", m.targetSource.Namespace, ) logger.Info("target manager started") diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index da80030..ad5fdee 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -161,8 +161,8 @@ func (r *TargetSourceReconciler) isPipelineRunning(key client.ObjectKey) bool { // startDiscoveryPipeline creates and starts the loader and target manager func (r *TargetSourceReconciler) startDiscoveryPipeline(key client.ObjectKey, targetSource *gnmicv1alpha1.TargetSource) error { loader, err := discovery.NewLoader( - targetSource.ObjectMeta.Name, - targetSource.ObjectMeta.Namespace, + targetSource.Name, + targetSource.Namespace, targetSource.Spec, ) if err != nil { From 3d7ff3851710baed90bd66e00b266a919cf6f328 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Fri, 24 Apr 2026 16:32:57 -0600 Subject: [PATCH 066/153] changed label handling and target object creation --- internal/controller/discovery/core/const.go | 9 ++++- .../discovery/loaders/http_pull/loader.go | 10 ++--- internal/controller/discovery/mapper.go | 39 ++++++++++++++++++- .../controller/discovery/target_manager.go | 34 +++++++--------- 4 files changed, 64 insertions(+), 28 deletions(-) diff --git a/internal/controller/discovery/core/const.go b/internal/controller/discovery/core/const.go index 82a5962..9548d94 100644 --- a/internal/controller/discovery/core/const.go +++ b/internal/controller/discovery/core/const.go @@ -1,6 +1,13 @@ package core const ( - // Labels + // Kubernetes Side Labels LabelTargetSourceName = "operator.gnmic.dev/targetsource" ) + +const ( + // Prefix and Labels for external systems + ExternalLabelPrefix = "gnmic_operator_" + + ExternalLabelTargetProfile = ExternalLabelPrefix + "target_profile" +) diff --git a/internal/controller/discovery/loaders/http_pull/loader.go b/internal/controller/discovery/loaders/http_pull/loader.go index 2d8a9e9..e4325d0 100644 --- a/internal/controller/discovery/loaders/http_pull/loader.go +++ b/internal/controller/discovery/loaders/http_pull/loader.go @@ -62,12 +62,12 @@ func (l *Loader) Start( { Name: "ceos1", Address: "clab-3-nodes-ceos1:6030", - Labels: map[string]string{"TargetSource": targetsourceName}, + Labels: map[string]string{"gnmic_operator_target_profile": "default1"}, }, { Name: "leaf1", Address: "clab-3-nodes-leaf1:57400", - Labels: map[string]string{"TargetSource": targetsourceName}, + Labels: map[string]string{"gnmic_operator_target_profile": "default1"}, }, } @@ -81,12 +81,12 @@ func (l *Loader) Start( { Name: "ceos1", Address: "clab-3-nodes-ceos1:6030", - Labels: map[string]string{"TargetSource": targetsourceName}, + Labels: map[string]string{"gnmic_operator_target_profile": "default1"}, }, { Name: "leaf2", Address: "clab-3-nodes-leaf2:57400", - Labels: map[string]string{"TargetSource": targetsourceName}, + Labels: map[string]string{"gnmic_operator_target_profile": "default1"}, }, } @@ -100,7 +100,7 @@ func (l *Loader) Start( { Name: "ceos1", Address: "clab-3-nodes-ceos2:6030", - Labels: map[string]string{"TargetSource": targetsourceName}, + Labels: map[string]string{"gnmic_operator_target_profile": "default2"}, }, } diff --git a/internal/controller/discovery/mapper.go b/internal/controller/discovery/mapper.go index bee897a..765e5aa 100644 --- a/internal/controller/discovery/mapper.go +++ b/internal/controller/discovery/mapper.go @@ -4,11 +4,48 @@ package discovery // file decides which targets to create/update/delete import ( + "maps" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery/core" ) -func GenerateEvents(existing []gnmicv1alpha1.Target, discovered []core.DiscoveredTarget) []core.DiscoveryEvent { +func generateTargetResource(d core.DiscoveredTarget, ts *gnmicv1alpha1.TargetSource) *gnmicv1alpha1.Target { + t := &gnmicv1alpha1.Target{ + ObjectMeta: metav1.ObjectMeta{ + Name: d.Name, + Namespace: ts.Namespace, + Labels: make(map[string]string), + }, + } + + t.Spec.Address = d.Address + t.Spec.Profile = ts.Spec.TargetProfile + + maps.Copy(t.Labels, ts.Spec.TargetLabels) + + for k, v := range d.Labels { + if strings.HasPrefix(k, core.ExternalLabelPrefix) { + switch k { + case core.ExternalLabelTargetProfile: + t.Spec.Profile = v + default: + // handle unknown label + } + } else { + t.Labels[k] = v + } + } + + t.Labels[core.LabelTargetSourceName] = ts.Name + + return t +} + +func generateEvents(existing []gnmicv1alpha1.Target, discovered []core.DiscoveredTarget) []core.DiscoveryEvent { var events []core.DiscoveryEvent discoveredMap := make(map[string]core.DiscoveredTarget) diff --git a/internal/controller/discovery/target_manager.go b/internal/controller/discovery/target_manager.go index be6b1fe..22d8012 100644 --- a/internal/controller/discovery/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -2,7 +2,6 @@ package discovery import ( "context" - "maps" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -114,7 +113,7 @@ func (m *TargetManager) processSnapshot(ctx context.Context, snapshotID string, ) } - events := GenerateEvents(existing, targets) + events := generateEvents(existing, targets) nApply := 0 nDelete := 0 @@ -153,7 +152,9 @@ func (m *TargetManager) processEvent(ctx context.Context, event core.DiscoveryEv ) } case core.APPLY: - if err := m.applyTarget(ctx, event.Target.Name, event.Target.Address); err != nil { + target := generateTargetResource(event.Target, m.targetSource) + + if err := m.applyTarget(ctx, target); err != nil { logger.Error(err, "error applying target", "targetName", event.Target.Name, ) @@ -165,29 +166,19 @@ func (m *TargetManager) processEvent(ctx context.Context, event core.DiscoveryEv } } -func (m *TargetManager) applyTarget(ctx context.Context, name string, address string) error { - target := &gnmicv1alpha1.Target{ +func (m *TargetManager) applyTarget(ctx context.Context, desired *gnmicv1alpha1.Target) error { + existing := &gnmicv1alpha1.Target{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: m.targetSource.Namespace, + Name: desired.Name, + Namespace: desired.Namespace, }, } - _, err := controllerutil.CreateOrUpdate(ctx, m.client, target, func() error { - labels := map[string]string{ - core.LabelTargetSourceName: m.targetSource.Name, - } - - maps.Copy(labels, m.targetSource.Spec.TargetLabels) - - target.Labels = labels + _, err := controllerutil.CreateOrUpdate(ctx, m.client, existing, func() error { + existing.Spec = desired.Spec + existing.Labels = desired.Labels - target.Spec = gnmicv1alpha1.TargetSpec{ - Address: address, - Profile: m.targetSource.Spec.TargetProfile, - } - - return controllerutil.SetControllerReference(m.targetSource, target, m.scheme) + return controllerutil.SetControllerReference(m.targetSource, existing, m.scheme) }) return err @@ -195,6 +186,7 @@ func (m *TargetManager) applyTarget(ctx context.Context, name string, address st func (m *TargetManager) deleteTarget(ctx context.Context, name string) error { existing := &gnmicv1alpha1.Target{} + err := m.client.Get(ctx, types.NamespacedName{ Name: name, Namespace: m.targetSource.Namespace, From 4d0a93740db7f3fc09e793175e20478710280e72 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Fri, 24 Apr 2026 16:43:34 -0600 Subject: [PATCH 067/153] fixed label filtering for existing targets --- internal/controller/discovery/client.go | 3 ++- internal/controller/discovery/loaders/http_pull/loader.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/controller/discovery/client.go b/internal/controller/discovery/client.go index 3bc7ef7..d23c043 100644 --- a/internal/controller/discovery/client.go +++ b/internal/controller/discovery/client.go @@ -8,6 +8,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" + "github.com/gnmic/operator/internal/controller/discovery/core" ) func FetchExistingTargets(ctx context.Context, c client.Client, ts gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { @@ -16,7 +17,7 @@ func FetchExistingTargets(ctx context.Context, c client.Client, ts gnmicv1alpha1 err := c.List(ctx, &targetList, client.InNamespace(ts.Namespace), client.MatchingLabels{ - "gnmic.io/source": ts.Name, + core.LabelTargetSourceName: ts.Name, }, ) if err != nil { diff --git a/internal/controller/discovery/loaders/http_pull/loader.go b/internal/controller/discovery/loaders/http_pull/loader.go index e4325d0..24ae094 100644 --- a/internal/controller/discovery/loaders/http_pull/loader.go +++ b/internal/controller/discovery/loaders/http_pull/loader.go @@ -62,7 +62,7 @@ func (l *Loader) Start( { Name: "ceos1", Address: "clab-3-nodes-ceos1:6030", - Labels: map[string]string{"gnmic_operator_target_profile": "default1"}, + Labels: map[string]string{}, }, { Name: "leaf1", From 60491be6b980c081f46955c59a8dc995db26c2e0 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Sat, 25 Apr 2026 09:06:20 +0000 Subject: [PATCH 068/153] add dependency handling of discovery pipeline components --- api/v1alpha1/targetsource_types.go | 7 ++ api/v1alpha1/zz_generated.deepcopy.go | 21 ++++ .../operator.gnmic.dev_targetsources.yaml | 5 + internal/controller/discovery/supervisor.go | 112 +++++++++--------- .../controller/targetsource_controller.go | 67 +++++++---- 5 files changed, 133 insertions(+), 79 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index feea000..a936e66 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -24,6 +24,8 @@ import ( // +kubebuilder:validation:Required type TargetSourceSpec struct { Provider *ProviderSpec `json:"provider"` + // +kubebuilder:validation:Optional + Webhook WebhookSpec `json:"webhook,omitempty"` // TargetLabels map[string]string `json:"targetLabels,omitempty"` @@ -37,6 +39,11 @@ type ProviderSpec struct { Consul *ConsulConfig `json:"consul,omitempty"` } +type WebhookSpec struct { + // +kubebuilder:validation:Optional + Enabled *bool `json:"enabled,omitempty"` +} + type HTTPConfig struct { // +kubebuilder:validation:MinLength=1 URL string `json:"url"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 61e81fd..608d47e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1292,6 +1292,7 @@ func (in *TargetSourceSpec) DeepCopyInto(out *TargetSourceSpec) { *out = new(ProviderSpec) (*in).DeepCopyInto(*out) } + in.Webhook.DeepCopyInto(&out.Webhook) if in.TargetLabels != nil { in, out := &in.TargetLabels, &out.TargetLabels *out = make(map[string]string, len(*in)) @@ -1477,3 +1478,23 @@ func (in *TunnelTargetPolicyStatus) DeepCopy() *TunnelTargetPolicyStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookSpec) DeepCopyInto(out *WebhookSpec) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookSpec. +func (in *WebhookSpec) DeepCopy() *WebhookSpec { + if in == nil { + return nil + } + out := new(WebhookSpec) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index f373822..b385d8e 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -67,6 +67,11 @@ spec: targetProfile: minLength: 1 type: string + webhook: + properties: + enabled: + type: boolean + type: object required: - provider - targetProfile diff --git a/internal/controller/discovery/supervisor.go b/internal/controller/discovery/supervisor.go index c716965..128305a 100644 --- a/internal/controller/discovery/supervisor.go +++ b/internal/controller/discovery/supervisor.go @@ -14,19 +14,20 @@ type RestartPolicy struct { } type Component struct { - Name string - Run func(ctx context.Context) error - Policy RestartPolicy + Name string + Run func(ctx context.Context) error + Policy RestartPolicy + DegradeOnFailure bool } type Supervisor struct { ctx context.Context cancel context.CancelFunc - stopped bool - mu sync.Mutex + wg sync.WaitGroup - components []Component + mu sync.Mutex + stopped bool } func NewSupervisor(parent context.Context) *Supervisor { @@ -37,53 +38,6 @@ func NewSupervisor(parent context.Context) *Supervisor { } } -func (s *Supervisor) AddComponent(c Component) { - s.components = append(s.components, c) -} - -func (s *Supervisor) runComponent(c Component) { - logger := log.FromContext(s.ctx).WithValues( - "component", c.Name, - ) - - failures := 0 - - for { - err := c.Run(s.ctx) - if s.ctx.Err() != nil { - return - } - - failures++ - logger.Error(err, - "Component failed", - "attempt", failures, - ) - - if failures >= c.Policy.MaxRestarts { - logger.Error(err, - "Component exceeded restart limit; stopping discovery pipeline", - "restarts", failures, - ) - s.Stop() - return - } - - select { - case <-time.After(c.Policy.Backoff): - case <-s.ctx.Done(): - return - } - } -} - -func (s *Supervisor) Run() { - for _, c := range s.components { - component := c - go s.runComponent(component) - } -} - func (s *Supervisor) Stop() { s.mu.Lock() defer s.mu.Unlock() @@ -98,3 +52,55 @@ func (s *Supervisor) Stop() { func (s *Supervisor) Done() <-chan struct{} { return s.ctx.Done() } + +func (s *Supervisor) Wait() { + s.wg.Wait() +} + +func (s *Supervisor) RunComponent(component Component) { + s.wg.Add(1) + + go func() { + defer s.wg.Done() + + logger := log.FromContext(s.ctx).WithValues("component", component.Name) + failures := 0 + + for { + logger.Info("starting component") + err := component.Run(s.ctx) + + if s.ctx.Err() != nil { + logger.Info("component stopped due to pipeline shutdown") + return + } + + failures++ + logger.Error(err, + "component failed to run", + "attempt", failures, + "max", component.Policy.MaxRestarts, + ) + + if failures >= component.Policy.MaxRestarts { + if component.DegradeOnFailure { + logger.Error(err, + "component permanently failed; shutting down pipeline", + ) + s.Stop() + } else { + logger.Info( + "optional component permanently failed; continuing without it", + ) + } + return + } + + select { + case <-time.After(component.Policy.Backoff): + case <-s.ctx.Done(): + return + } + } + }() +} diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 68f47eb..a687e80 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -167,16 +167,6 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName return err } - // Create loader instance - loader, err := discovery.NewLoader( - key, - targetSource.Spec, - core.LoaderConfig{ChunkSize: r.ChunkSize}, - ) - if err != nil { - return err - } - // Create target applier instance applier := discovery.NewTargetApplier( r.Client, @@ -184,34 +174,59 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName targetSource, targetChannel, ) - - supervisor.AddComponent(discovery.Component{ - Name: "loader", - Run: func(ctx context.Context) error { - return loader.Start(ctx, key, targetSource.Spec, targetChannel) - }, - Policy: discovery.RestartPolicy{ - MaxRestarts: pipelineMaxRestarts, - Backoff: pipelineBackoff, - }, - }) - - supervisor.AddComponent(discovery.Component{ + // Start target applier + applierReady := make(chan struct{}) + supervisor.RunComponent(discovery.Component{ Name: "target-applier", - Run: applier.Run, Policy: discovery.RestartPolicy{ MaxRestarts: pipelineMaxRestarts, Backoff: pipelineBackoff, }, + DegradeOnFailure: true, + Run: func(ctx context.Context) error { + close(applierReady) + return applier.Run(ctx) + }, }) + // Wait for applier to be ready before starting loader + select { + case <-applierReady: + case <-supervisor.Done(): + return nil + } - supervisor.Run() + // Create loader instance + loaderConfigured := targetSource.Spec.Provider != nil + webhookConfigured := targetSource.Spec.Webhook.Enabled != nil + if loaderConfigured { + loader, err := discovery.NewLoader( + key, + targetSource.Spec, + core.LoaderConfig{ChunkSize: r.ChunkSize}, + ) + if err != nil { + supervisor.Stop() + return err + } + + supervisor.RunComponent(discovery.Component{ + Name: "loader", + Policy: discovery.RestartPolicy{ + MaxRestarts: pipelineMaxRestarts, + Backoff: pipelineBackoff, + }, + DegradeOnFailure: !webhookConfigured, + Run: func(ctx context.Context) error { + return loader.Start(ctx, key, targetSource.Spec, targetChannel) + }, + }) + } go func() { <-supervisor.Done() + supervisor.Wait() // Wait for components to exit logger.Info("Pipeline stopped; performing final cleanup") - close(targetChannel) r.DiscoveryRegistry.Unregister(key) r.stopDiscovery(key) From b8a6d272d479a97f05b5adeb6f9081520a236f8e Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Sat, 25 Apr 2026 09:31:29 +0000 Subject: [PATCH 069/153] refactor code --- internal/controller/discovery/supervisor.go | 54 +++++----- .../controller/targetsource_controller.go | 99 ++++++++++--------- 2 files changed, 87 insertions(+), 66 deletions(-) diff --git a/internal/controller/discovery/supervisor.go b/internal/controller/discovery/supervisor.go index 128305a..710381e 100644 --- a/internal/controller/discovery/supervisor.go +++ b/internal/controller/discovery/supervisor.go @@ -8,18 +8,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) -type RestartPolicy struct { - MaxRestarts int - Backoff time.Duration -} - -type Component struct { - Name string - Run func(ctx context.Context) error - Policy RestartPolicy - DegradeOnFailure bool -} - +// Supervisor coordinates the runtime lifecycle of pipeline components +// +// Guarantees: +// - Each component is restarted independently +// - Permanent failure escalates according to policy +// - Stop() cancels all components +// - Wait() blocks until all goroutines exit type Supervisor struct { ctx context.Context cancel context.CancelFunc @@ -30,14 +25,30 @@ type Supervisor struct { stopped bool } -func NewSupervisor(parent context.Context) *Supervisor { - ctx, cancel := context.WithCancel(parent) +// RestartPolicy defines the restart behavior for a component +type RestartPolicy struct { + MaxRestarts int + Backoff time.Duration +} + +type ComponentSpec struct { + Name string + Run func(ctx context.Context) error + Policy RestartPolicy + // EscalatesOnFailure indicates whether a permanent failure of this component should shut down the entire pipeline + EscalatesOnFailure bool +} + +// NewSupervisor creates a new Supervisor with a cancellable context +func NewSupervisor(parentCtx context.Context) *Supervisor { + ctx, cancel := context.WithCancel(parentCtx) return &Supervisor{ ctx: ctx, cancel: cancel, } } +// Stop signals all supervised components to stop by canceling the context func (s *Supervisor) Stop() { s.mu.Lock() defer s.mu.Unlock() @@ -49,15 +60,14 @@ func (s *Supervisor) Stop() { s.cancel() } -func (s *Supervisor) Done() <-chan struct{} { - return s.ctx.Done() -} +// Done returns a channel that is closed when the pipeline is stopped +func (s *Supervisor) Done() <-chan struct{} { return s.ctx.Done() } -func (s *Supervisor) Wait() { - s.wg.Wait() -} +// Wait blocks until all supervised components have exited +func (s *Supervisor) Wait() { s.wg.Wait() } -func (s *Supervisor) RunComponent(component Component) { +// StartSupervisedComponent starts and supervises a component +func (s *Supervisor) StartSupervisedComponent(component ComponentSpec) { s.wg.Add(1) go func() { @@ -83,7 +93,7 @@ func (s *Supervisor) RunComponent(component Component) { ) if failures >= component.Policy.MaxRestarts { - if component.DegradeOnFailure { + if component.EscalatesOnFailure { logger.Error(err, "component permanently failed; shutting down pipeline", ) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index a687e80..f04eced 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -43,17 +43,26 @@ const ( pipelineBackoff = 3 * time.Second ) -type runningSource struct { +// pipelineHandle represents a controller-owned handle to a running pipeline +// The controller never manipulates internals; it only invokes cancel() +type pipelineHandle struct { cancel context.CancelFunc } // TargetSourceReconciler reconciles a TargetSource object +// +// Responsibilities: +// - Ensure at most one pipeline per TargetSource +// - Start pipelines on reconcile +// - Stop pipelines on deletion or NotFound +// - Delegate runtime failure handling to the Supervisor type TargetSourceReconciler struct { client.Client Scheme *runtime.Scheme - mu sync.Mutex - running map[types.NamespacedName]runningSource + mu sync.Mutex + // runningPipelines tracks currently active pipelines by NamespacedName + runningPipelines map[types.NamespacedName]pipelineHandle BufferSize int ChunkSize int @@ -69,47 +78,43 @@ type TargetSourceReconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx).WithName("targetsource controller").WithValues( - "targetsource", req.NamespacedName, - ) + logger := log.FromContext(ctx). + WithName("targetsource controller"). + WithValues("targetsource", req.NamespacedName) - targetSource, err := r.getTargetSource(ctx, req.NamespacedName) + targetSource, err := r.fetchTargetSource(ctx, req.NamespacedName) if err != nil { // If the TargetSource no longer exists, ensure runtime cleanup if client.IgnoreNotFound(err) == nil { - logger.Info("TargetSource not found, ensuring cleanup") - r.stopDiscovery(req.NamespacedName) + logger.Info("TargetSource not found; stopping discovery pipeline") + r.stopDiscoveryPipeline(req.NamespacedName) return ctrl.Result{}, nil } return ctrl.Result{}, err } - // Handle deletion with finalizer if !targetSource.DeletionTimestamp.IsZero() { - return r.handleTargetSourceDeletion(ctx, req.NamespacedName, targetSource) + return r.reconcileDeletion(ctx, req.NamespacedName, targetSource) } - // Ensure finalizer is set if err := r.ensureFinalizer(ctx, targetSource); err != nil { return ctrl.Result{}, err } - // Check if pipeline is already running - if r.isPipelineRunning(req.NamespacedName) { + if r.hasPipelineRunning(req.NamespacedName) { return ctrl.Result{}, nil } - // Start discovery pipeline if err := r.startDiscoveryPipeline(req.NamespacedName, targetSource, logger); err != nil { return ctrl.Result{}, err } - logger.Info("TargetSource pipeline started") + logger.Info("Discover pipeline started") return ctrl.Result{}, nil } -// getTargetSource retrieves a TargetSource by name, handling cleanup if not found -func (r *TargetSourceReconciler) getTargetSource(ctx context.Context, key types.NamespacedName) (*gnmicv1alpha1.TargetSource, error) { +// fetchTargetSource retrieves a TargetSource by name, handling cleanup if not found +func (r *TargetSourceReconciler) fetchTargetSource(ctx context.Context, key types.NamespacedName) (*gnmicv1alpha1.TargetSource, error) { var targetSource gnmicv1alpha1.TargetSource if err := r.Get(ctx, key, &targetSource); err != nil { return nil, err @@ -117,12 +122,20 @@ func (r *TargetSourceReconciler) getTargetSource(ctx context.Context, key types. return &targetSource, nil } -// handleTargetSourceDeletion stops the discovery pipeline and removes the finalizer -func (r *TargetSourceReconciler) handleTargetSourceDeletion(ctx context.Context, key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource) (ctrl.Result, error) { +// hasPipelineRunning checks if a discovery pipeline is already running for the given key +func (r *TargetSourceReconciler) hasPipelineRunning(key types.NamespacedName) bool { + r.mu.Lock() + defer r.mu.Unlock() + _, exists := r.runningPipelines[key] + return exists +} + +// reconcileDeletion stops the discovery pipeline and removes the finalizer +func (r *TargetSourceReconciler) reconcileDeletion(ctx context.Context, key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource) (ctrl.Result, error) { logger := log.FromContext(ctx) logger.Info("TargetSource is being deleted, stopping pipeline", "name", key) - r.stopDiscovery(key) + r.stopDiscoveryPipeline(key) // Remove finalizer if exists if controllerutil.ContainsFinalizer(targetSource, targetSourceFinalizer) { @@ -149,16 +162,13 @@ func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSour return nil } -// isPipelineRunning checks if a discovery pipeline is already running for the given key -func (r *TargetSourceReconciler) isPipelineRunning(key types.NamespacedName) bool { - r.mu.Lock() - defer r.mu.Unlock() - - _, exists := r.running[key] - return exists -} - -// startDiscoveryPipeline creates and starts the loader and target manager +// startDiscoveryPipeline creates and starts a discover pipeline for a TargetSource +// +// Pipeline semantics: +// 1. target-applier is mandatory and must start first +// 2. loader is optional and conditional on spec +// 3. Permanent failure of required components shuts down the pipeline +// 4. Shutdown ordering: cancel ctx -> wait for goroutines to exit -> close channel -> unregister func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource, logger logr.Logger) error { supervisor := discovery.NewSupervisor(context.Background()) @@ -176,15 +186,15 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName ) // Start target applier applierReady := make(chan struct{}) - supervisor.RunComponent(discovery.Component{ + supervisor.StartSupervisedComponent(discovery.ComponentSpec{ Name: "target-applier", Policy: discovery.RestartPolicy{ MaxRestarts: pipelineMaxRestarts, Backoff: pipelineBackoff, }, - DegradeOnFailure: true, + EscalatesOnFailure: true, Run: func(ctx context.Context) error { - close(applierReady) + close(applierReady) // Signals that applier started successfully return applier.Run(ctx) }, }) @@ -209,31 +219,32 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName return err } - supervisor.RunComponent(discovery.Component{ + supervisor.StartSupervisedComponent(discovery.ComponentSpec{ Name: "loader", Policy: discovery.RestartPolicy{ MaxRestarts: pipelineMaxRestarts, Backoff: pipelineBackoff, }, - DegradeOnFailure: !webhookConfigured, + EscalatesOnFailure: !webhookConfigured, Run: func(ctx context.Context) error { return loader.Start(ctx, key, targetSource.Spec, targetChannel) }, }) } + // Monitor supervisor in a separate goroutine to handle shutdown and cleanup go func() { <-supervisor.Done() supervisor.Wait() // Wait for components to exit - logger.Info("Pipeline stopped; performing final cleanup") + logger.Info("Pipeline stopped; cleaning up") close(targetChannel) r.DiscoveryRegistry.Unregister(key) - r.stopDiscovery(key) + r.stopDiscoveryPipeline(key) }() r.mu.Lock() - r.running[key] = runningSource{ + r.runningPipelines[key] = pipelineHandle{ cancel: func() { supervisor.Stop() }, @@ -243,12 +254,12 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName return nil } -// stopDiscovery stops and removes a running discovery pipeline -func (r *TargetSourceReconciler) stopDiscovery(key types.NamespacedName) { +// stopDiscoveryPipeline stops and removes a running discovery pipeline +func (r *TargetSourceReconciler) stopDiscoveryPipeline(key types.NamespacedName) { r.mu.Lock() - running, ok := r.running[key] + running, ok := r.runningPipelines[key] if ok { - delete(r.running, key) + delete(r.runningPipelines, key) } r.mu.Unlock() @@ -259,7 +270,7 @@ func (r *TargetSourceReconciler) stopDiscovery(key types.NamespacedName) { // SetupWithManager sets up the controller with the Manager. func (r *TargetSourceReconciler) SetupWithManager(mgr ctrl.Manager) error { - r.running = make(map[types.NamespacedName]runningSource) + r.runningPipelines = make(map[types.NamespacedName]pipelineHandle) return ctrl.NewControllerManagedBy(mgr). For(&gnmicv1alpha1.TargetSource{}). From eedfedf930d6f78ef9ca430115bb121ee9db129c Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Sat, 25 Apr 2026 12:55:52 +0000 Subject: [PATCH 070/153] improve context handling of and target applier semantics --- internal/controller/discovery/core/helpers.go | 14 +- internal/controller/discovery/core/types.go | 5 +- .../controller/discovery/target_applier.go | 209 ++++++++++++++---- 3 files changed, 184 insertions(+), 44 deletions(-) diff --git a/internal/controller/discovery/core/helpers.go b/internal/controller/discovery/core/helpers.go index 843f30e..f24b50c 100644 --- a/internal/controller/discovery/core/helpers.go +++ b/internal/controller/discovery/core/helpers.go @@ -2,6 +2,7 @@ package core import ( "context" + "fmt" ) // sendMessages sends discovery messages over a channel in a context-aware manner @@ -32,13 +33,15 @@ func forEachChunk(total, chunkSize int, fn func(start, end int) error) error { func createDiscoverySnapshots(targets []DiscoveredTarget, snapshotID string, chunkSize int) []DiscoverySnapshot { var snapshots []DiscoverySnapshot totalTargets := len(targets) + totalChunks := (totalTargets + chunkSize - 1) / chunkSize _ = forEachChunk(totalTargets, chunkSize, func(i, end int) error { chunk := targets[i:end] snapshots = append(snapshots, DiscoverySnapshot{ Targets: chunk, SnapshotID: snapshotID, - IsLastChunk: (end == totalTargets), + ChunkIndex: i / chunkSize, + TotalChunks: totalChunks, }) return nil }) @@ -48,8 +51,11 @@ func createDiscoverySnapshots(targets []DiscoveredTarget, snapshotID string, chu // SendSnapshot sends discovered targets as a snapshot over a channel in chunks func SendSnapshot(ctx context.Context, out chan<- []DiscoveryMessage, targets []DiscoveredTarget, snapshotID string, chunkSize int) error { - snapshots := createDiscoverySnapshots(targets, snapshotID, chunkSize) + if len(targets) == 0 { + return fmt.Errorf("no targets in Snapshot") + } + snapshots := createDiscoverySnapshots(targets, snapshotID, chunkSize) for _, snapshot := range snapshots { // Convert DiscoverySnapshot to DiscoveryMessage messages := make([]DiscoveryMessage, 1) @@ -73,6 +79,10 @@ func eventsToMessages(events []DiscoveryEvent) []DiscoveryMessage { // SendEvents sends discovery messages over channel in a context-aware manner func SendEvents(ctx context.Context, out chan<- []DiscoveryMessage, events []DiscoveryEvent, chunkSize int) error { + if len(events) == 0 { + return fmt.Errorf("no events to process") + } + messages := eventsToMessages(events) total := len(messages) diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 61209fd..3f6957a 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -26,7 +26,8 @@ type DiscoveryEvent struct { } type DiscoverySnapshot struct { - Targets []DiscoveredTarget SnapshotID string - IsLastChunk bool + ChunkIndex int + TotalChunks int + Targets []DiscoveredTarget } diff --git a/internal/controller/discovery/target_applier.go b/internal/controller/discovery/target_applier.go index c60f2b8..ee127c5 100644 --- a/internal/controller/discovery/target_applier.go +++ b/internal/controller/discovery/target_applier.go @@ -2,6 +2,7 @@ package discovery import ( "context" + "fmt" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -12,13 +13,23 @@ import ( "github.com/go-logr/logr" ) +type snapshotBuffer struct { + snapshotID string + totalChunks int + received map[int][]core.DiscoveredTarget + complete bool +} + // TargetApplier consumes discovered targets and applies them to Kubernetes type TargetApplier struct { - client client.Client - scheme *runtime.Scheme - targetSource *gnmicv1alpha1.TargetSource - in <-chan []core.DiscoveryMessage - collected map[string][]core.DiscoveredTarget + client client.Client + scheme *runtime.Scheme + targetSource *gnmicv1alpha1.TargetSource + in <-chan []core.DiscoveryMessage + queue []core.DiscoveryMessage + activeSnapshot *snapshotBuffer + // Events are deferred while snapshot is in progress + defferedEvents []core.DiscoveryEvent } // NewTargetApplier wires a TargetApplier instance @@ -28,47 +39,43 @@ func NewTargetApplier(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.Targ scheme: s, targetSource: ts, in: in, - collected: make(map[string][]core.DiscoveredTarget), } } // Run is a long‑running loop that receives target snapshots // and reconciles Target CRs accordingly -func (m *TargetApplier) Run(ctx context.Context) error { +func (a *TargetApplier) Run(ctx context.Context) error { logger := log.FromContext(ctx). WithValues( - "name", m.targetSource.Name, - "namespace", m.targetSource.Namespace, + "name", a.targetSource.Name, + "namespace", a.targetSource.Namespace, ) - logger.Info("target applier started") - queue := []core.DiscoveryMessage{} - for ctx.Err() == nil { select { - case batch, ok := <-m.in: + case batch, ok := <-a.in: if !ok { // Channel closed, pipeline is shutting down logger.Info("input channel closed, stopping target applier") return nil } - queue = append(queue, batch...) + a.queue = append(a.queue, batch...) case <-ctx.Done(): logger.Info("context canceled, stopping target applier") return nil } - for len(queue) > 0 { + for len(a.queue) > 0 { if ctx.Err() != nil { - return ctx.Err() + return nil // why return nil? } - msg := queue[0] - queue = queue[1:] + msg := a.queue[0] + a.queue = a.queue[1:] - if err := m.handleMessage(ctx, msg, logger); err != nil { + if err := a.processMessage(ctx, msg, logger); err != nil { // Returning error lets the supervisor (controller) // tear down and restart the pipeline via reconciliation // Q: when to return an error vs just log and continue? @@ -82,7 +89,7 @@ func (m *TargetApplier) Run(ctx context.Context) error { return nil } -func (m *TargetApplier) handleMessage(ctx context.Context, message core.DiscoveryMessage, logger logr.Logger) error { +func (a *TargetApplier) processMessage(ctx context.Context, message core.DiscoveryMessage, logger logr.Logger) error { if err := ctx.Err(); err != nil { return err } @@ -94,12 +101,10 @@ func (m *TargetApplier) handleMessage(ctx context.Context, message core.Discover logger.Info( "received snapshot chunk", "snapshotID", msg.SnapshotID, + "index", msg.ChunkIndex, "targetCount", len(msg.Targets), ) - m.collected[msg.SnapshotID] = append(m.collected[msg.SnapshotID], msg.Targets...) - if msg.IsLastChunk { - m.processSnapshot(msg.SnapshotID, logger) - } + return a.processSnapshot(ctx, msg, logger) case core.DiscoveryEvent: // Process individual event-driven update @@ -107,31 +112,155 @@ func (m *TargetApplier) handleMessage(ctx context.Context, message core.Discover "received discovery event", "target", msg.Target.Name, ) - switch msg.Event { - case core.CREATE: - logger.Info("Would create target", "name", msg.Target.Name, "address", msg.Target.Address, "labels", msg.Target.Labels) - case core.UPDATE: - logger.Info("Would update target", "name", msg.Target.Name, "address", msg.Target.Address, "labels", msg.Target.Labels) - case core.DELETE: - logger.Info("Would delete target", "name", msg.Target.Name) + return a.processEvent(ctx, msg, logger) + + default: + return fmt.Errorf("unknonw discovery message type %T", msg) + } +} + +// processSnapshot takes a complete snapshot of discovered targets and reconciles Target CRs accordingly +func (a *TargetApplier) processSnapshot(ctx context.Context, chunk core.DiscoverySnapshot, logger logr.Logger) error { + if a.activeSnapshot == nil { + a.startNewSnapshot(chunk, logger) + return nil + } + + snapshot := a.activeSnapshot + // Check if a new snapshot arrived + if snapshot.snapshotID != chunk.SnapshotID { + // If current snapshot is complete apply it first + if snapshot.complete { + if err := a.applySnapshot(ctx, snapshot, logger); err != nil { + return err + } + } else { + // If a new snapshot is started before the old one completed + // the old one can be discarded + logger.Info( + "discarding incomplete snapshot", + "snapshotID", snapshot.snapshotID, + ) } + + // Start collecting the new snapshot + a.startNewSnapshot(chunk, logger) + return nil + } + + return a.collectSnapshot(chunk, logger) +} + +func (a *TargetApplier) startNewSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) { + a.activeSnapshot = &snapshotBuffer{ + snapshotID: chunk.SnapshotID, + totalChunks: chunk.TotalChunks, + received: make(map[int][]core.DiscoveredTarget), + complete: false, + } + // Delete buffered events that will be current with new snapshot + a.defferedEvents = nil + + a.collectSnapshot(chunk, logger) +} + +func (a *TargetApplier) collectSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) error { + snapshot := a.activeSnapshot + + if chunk.TotalChunks != snapshot.totalChunks { + logger.Error(nil, "snapshot totalChunks mismatch", "snapshotID", snapshot.snapshotID) + } + if chunk.ChunkIndex < 0 || chunk.ChunkIndex >= snapshot.totalChunks { + logger.Error(nil, "snapshot chunk index out of range", "index", chunk.ChunkIndex) + a.activeSnapshot = nil + return nil + } + if _, exists := snapshot.received[chunk.ChunkIndex]; exists { + logger.Error(nil, "duplicate snapshot chunk", "index", chunk.ChunkIndex) + a.activeSnapshot = nil + return nil + } + + snapshot.received[chunk.ChunkIndex] = chunk.Targets + + if len(snapshot.received) == snapshot.totalChunks { + snapshot.complete = true } return nil } -// processSnapshot takes a complete snapshot of discovered targets and reconciles Target CRs accordingly -func (m *TargetApplier) processSnapshot(snapshotID string, logger logr.Logger) { - targets := m.collected[snapshotID] - delete(m.collected, snapshotID) +func (a *TargetApplier) applySnapshot(ctx context.Context, snapshot *snapshotBuffer, logger logr.Logger) error { + select { + case <-ctx.Done(): + a.activeSnapshot = nil + return nil + default: + } - logger.Info("Processing full snapshot", "snapshotID", snapshotID, "totalTargets", len(targets)) + var allTargets []core.DiscoveredTarget + for i := 0; i < snapshot.totalChunks; i++ { + select { + case <-ctx.Done(): + a.activeSnapshot = nil + return nil + default: + } - if m.targetSource.Spec.Provider.HTTP != nil { - logger.Info("Would delete all existing targets for targetsource", "targetsource", m.targetSource.Name) + chunk, ok := snapshot.received[i] + if !ok { + logger.Error(nil, "missing snapshot chunk", "index", i) + a.activeSnapshot = nil + return nil + } + allTargets = append(allTargets, chunk...) } - for _, target := range targets { - logger.Info("Would create target", "name", target.Name, "address", target.Address, "labels", target.Labels) + logger.Info( + "applying snapshot", + "snapshotID", snapshot.snapshotID, + "targetCount", len(allTargets), + ) + + // apply all targets + // a.applyTargets + + // Replay deffered events + for _, event := range a.defferedEvents { + select { + case <-ctx.Done(): + return nil + default: + } + if err := a.applyEvent(ctx, event, logger); err != nil { + return err + } } + + a.activeSnapshot = nil + a.defferedEvents = nil + return nil +} + +func (a *TargetApplier) processEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { + // If snapshot collecting is active defer events + if a.activeSnapshot != nil { + a.defferedEvents = append(a.defferedEvents, event) + return nil + } + + // Apply events + return a.applyEvent(ctx, event, logger) +} + +func (a *TargetApplier) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { + switch event.Event { + case core.CREATE: + logger.Info("Would create target", "name", event.Target.Name, "address", event.Target.Address, "labels", event.Target.Labels) + case core.UPDATE: + logger.Info("Would update target", "name", event.Target.Name, "address", event.Target.Address, "labels", event.Target.Labels) + case core.DELETE: + logger.Info("Would delete target", "name", event.Target.Name) + } + return nil } From a66accbbcac43a0cdbefa4f59231ca57fca1635f Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Sun, 26 Apr 2026 18:23:19 -0600 Subject: [PATCH 071/153] moved finalizer label into const file --- internal/controller/const.go | 2 ++ internal/controller/targetsource_controller.go | 10 ++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/controller/const.go b/internal/controller/const.go index b5196b8..5ef2e8f 100644 --- a/internal/controller/const.go +++ b/internal/controller/const.go @@ -21,6 +21,8 @@ const ( LabelCertType = "operator.gnmic.dev/cert-type" LabelValueCertTypeClient = "client" LabelValueCertTypeTunnel = "tunnel" + + LabelTargetSourceFinalizer = "operator.gnmic.dev/targetsource-finalizer" ) const ( diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index f04eced..232c624 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -37,8 +37,6 @@ import ( ) const ( - targetSourceFinalizer = "operator.gnmic.dev/targetsource-finalizer" - pipelineMaxRestarts = 5 pipelineBackoff = 3 * time.Second ) @@ -138,8 +136,8 @@ func (r *TargetSourceReconciler) reconcileDeletion(ctx context.Context, key type r.stopDiscoveryPipeline(key) // Remove finalizer if exists - if controllerutil.ContainsFinalizer(targetSource, targetSourceFinalizer) { - controllerutil.RemoveFinalizer(targetSource, targetSourceFinalizer) + if controllerutil.ContainsFinalizer(targetSource, LabelTargetSourceFinalizer) { + controllerutil.RemoveFinalizer(targetSource, LabelTargetSourceFinalizer) if err := r.Update(ctx, targetSource); err != nil { return ctrl.Result{}, err } @@ -150,11 +148,11 @@ func (r *TargetSourceReconciler) reconcileDeletion(ctx context.Context, key type // ensureFinalizer adds the finalizer if not present and updates the TargetSource func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSource *gnmicv1alpha1.TargetSource) error { - if controllerutil.ContainsFinalizer(targetSource, targetSourceFinalizer) { + if controllerutil.ContainsFinalizer(targetSource, LabelTargetSourceFinalizer) { return nil } - controllerutil.AddFinalizer(targetSource, targetSourceFinalizer) + controllerutil.AddFinalizer(targetSource, LabelTargetSourceFinalizer) if err := r.Update(ctx, targetSource); err != nil { return err } From 3b2d9258a06116738be182e567ee6f275c9ad0e4 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Sun, 26 Apr 2026 18:29:05 -0600 Subject: [PATCH 072/153] fixed typo --- internal/controller/discovery/target_applier.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/controller/discovery/target_applier.go b/internal/controller/discovery/target_applier.go index ee127c5..3c714bd 100644 --- a/internal/controller/discovery/target_applier.go +++ b/internal/controller/discovery/target_applier.go @@ -29,7 +29,7 @@ type TargetApplier struct { queue []core.DiscoveryMessage activeSnapshot *snapshotBuffer // Events are deferred while snapshot is in progress - defferedEvents []core.DiscoveryEvent + deferredEvents []core.DiscoveryEvent } // NewTargetApplier wires a TargetApplier instance @@ -159,7 +159,7 @@ func (a *TargetApplier) startNewSnapshot(chunk core.DiscoverySnapshot, logger lo complete: false, } // Delete buffered events that will be current with new snapshot - a.defferedEvents = nil + a.deferredEvents = nil a.collectSnapshot(chunk, logger) } @@ -225,8 +225,8 @@ func (a *TargetApplier) applySnapshot(ctx context.Context, snapshot *snapshotBuf // apply all targets // a.applyTargets - // Replay deffered events - for _, event := range a.defferedEvents { + // Replay deferred events + for _, event := range a.deferredEvents { select { case <-ctx.Done(): return nil @@ -238,14 +238,14 @@ func (a *TargetApplier) applySnapshot(ctx context.Context, snapshot *snapshotBuf } a.activeSnapshot = nil - a.defferedEvents = nil + a.deferredEvents = nil return nil } func (a *TargetApplier) processEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { // If snapshot collecting is active defer events if a.activeSnapshot != nil { - a.defferedEvents = append(a.defferedEvents, event) + a.deferredEvents = append(a.deferredEvents, event) return nil } From 3ba86cb63c45a7f042a2051faca5f8ddfdc5b2ad Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Sun, 26 Apr 2026 19:21:01 -0600 Subject: [PATCH 073/153] restructured loaders package --- .../controller/discovery/loaders/http/{loader.go => http.go} | 0 .../discovery/loaders/http/{loader_test.go => http_test.go} | 0 .../controller/discovery/{loader.go => loaders/loaders.go} | 2 +- internal/controller/targetsource_controller.go | 3 ++- 4 files changed, 3 insertions(+), 2 deletions(-) rename internal/controller/discovery/loaders/http/{loader.go => http.go} (100%) rename internal/controller/discovery/loaders/http/{loader_test.go => http_test.go} (100%) rename internal/controller/discovery/{loader.go => loaders/loaders.go} (97%) diff --git a/internal/controller/discovery/loaders/http/loader.go b/internal/controller/discovery/loaders/http/http.go similarity index 100% rename from internal/controller/discovery/loaders/http/loader.go rename to internal/controller/discovery/loaders/http/http.go diff --git a/internal/controller/discovery/loaders/http/loader_test.go b/internal/controller/discovery/loaders/http/http_test.go similarity index 100% rename from internal/controller/discovery/loaders/http/loader_test.go rename to internal/controller/discovery/loaders/http/http_test.go diff --git a/internal/controller/discovery/loader.go b/internal/controller/discovery/loaders/loaders.go similarity index 97% rename from internal/controller/discovery/loader.go rename to internal/controller/discovery/loaders/loaders.go index 0d8ddd3..45bf9c1 100644 --- a/internal/controller/discovery/loader.go +++ b/internal/controller/discovery/loaders/loaders.go @@ -1,4 +1,4 @@ -package discovery +package loaders import ( "fmt" diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 232c624..77a3a35 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -31,6 +31,7 @@ import ( gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery" "github.com/gnmic/operator/internal/controller/discovery/core" + "github.com/gnmic/operator/internal/controller/discovery/loaders" _ "github.com/gnmic/operator/internal/controller/discovery/loaders/all" "github.com/gnmic/operator/internal/controller/discovery/registry" "github.com/go-logr/logr" @@ -207,7 +208,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName loaderConfigured := targetSource.Spec.Provider != nil webhookConfigured := targetSource.Spec.Webhook.Enabled != nil if loaderConfigured { - loader, err := discovery.NewLoader( + loader, err := loaders.NewLoader( key, targetSource.Spec, core.LoaderConfig{ChunkSize: r.ChunkSize}, From d0ac86be2e389e91ef833bf5c278324af2df59bb Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Sun, 26 Apr 2026 19:21:13 -0600 Subject: [PATCH 074/153] restructured target handler --- internal/controller/discovery/client.go | 27 ---- .../{target_applier.go => target_handler.go} | 121 ++++++++++-------- .../controller/targetsource_controller.go | 20 +-- 3 files changed, 80 insertions(+), 88 deletions(-) delete mode 100644 internal/controller/discovery/client.go rename internal/controller/discovery/{target_applier.go => target_handler.go} (66%) diff --git a/internal/controller/discovery/client.go b/internal/controller/discovery/client.go deleted file mode 100644 index 3bc7ef7..0000000 --- a/internal/controller/discovery/client.go +++ /dev/null @@ -1,27 +0,0 @@ -package discovery - -// File may become obsolete, depends on how the logic to compare desired vs. existing state will get implemented - -import ( - "context" - - "sigs.k8s.io/controller-runtime/pkg/client" - - gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" -) - -func FetchExistingTargets(ctx context.Context, c client.Client, ts gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { - var targetList gnmicv1alpha1.TargetList - - err := c.List(ctx, &targetList, - client.InNamespace(ts.Namespace), - client.MatchingLabels{ - "gnmic.io/source": ts.Name, - }, - ) - if err != nil { - return nil, err - } - - return targetList.Items, nil -} diff --git a/internal/controller/discovery/target_applier.go b/internal/controller/discovery/target_handler.go similarity index 66% rename from internal/controller/discovery/target_applier.go rename to internal/controller/discovery/target_handler.go index 3c714bd..e8c0308 100644 --- a/internal/controller/discovery/target_applier.go +++ b/internal/controller/discovery/target_handler.go @@ -20,8 +20,9 @@ type snapshotBuffer struct { complete bool } -// TargetApplier consumes discovered targets and applies them to Kubernetes -type TargetApplier struct { +// TargetHandler consumes discovered targets and applies them to Kubernetes +type TargetHandler struct { + ctx context.Context client client.Client scheme *runtime.Scheme targetSource *gnmicv1alpha1.TargetSource @@ -32,9 +33,9 @@ type TargetApplier struct { deferredEvents []core.DiscoveryEvent } -// NewTargetApplier wires a TargetApplier instance -func NewTargetApplier(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []core.DiscoveryMessage) *TargetApplier { - return &TargetApplier{ +// NewTargetHandler wires a TargetHandler instance +func NewTargetHandler(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []core.DiscoveryMessage) *TargetHandler { + return &TargetHandler{ client: c, scheme: s, targetSource: ts, @@ -44,38 +45,40 @@ func NewTargetApplier(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.Targ // Run is a long‑running loop that receives target snapshots // and reconciles Target CRs accordingly -func (a *TargetApplier) Run(ctx context.Context) error { - logger := log.FromContext(ctx). +func (c *TargetHandler) Run(ctx context.Context) error { + c.ctx = ctx + + logger := log.FromContext(c.ctx). WithValues( - "name", a.targetSource.Name, - "namespace", a.targetSource.Namespace, + "name", c.targetSource.Name, + "namespace", c.targetSource.Namespace, ) - logger.Info("target applier started") + logger.Info("target handler started") - for ctx.Err() == nil { + for c.ctx.Err() == nil { select { - case batch, ok := <-a.in: + case batch, ok := <-c.in: if !ok { // Channel closed, pipeline is shutting down - logger.Info("input channel closed, stopping target applier") + logger.Info("input channel closed, stopping target handler") return nil } - a.queue = append(a.queue, batch...) + c.queue = append(c.queue, batch...) case <-ctx.Done(): - logger.Info("context canceled, stopping target applier") + logger.Info("context canceled, stopping target handler") return nil } - for len(a.queue) > 0 { + for len(c.queue) > 0 { if ctx.Err() != nil { return nil // why return nil? } - msg := a.queue[0] - a.queue = a.queue[1:] + msg := c.queue[0] + c.queue = c.queue[1:] - if err := a.processMessage(ctx, msg, logger); err != nil { + if err := c.processMessage(c.ctx, msg, logger); err != nil { // Returning error lets the supervisor (controller) // tear down and restart the pipeline via reconciliation // Q: when to return an error vs just log and continue? @@ -85,11 +88,11 @@ func (a *TargetApplier) Run(ctx context.Context) error { } } - logger.Info("target applier stopped") + logger.Info("target handler stopped") return nil } -func (a *TargetApplier) processMessage(ctx context.Context, message core.DiscoveryMessage, logger logr.Logger) error { +func (c *TargetHandler) processMessage(ctx context.Context, message core.DiscoveryMessage, logger logr.Logger) error { if err := ctx.Err(); err != nil { return err } @@ -104,7 +107,7 @@ func (a *TargetApplier) processMessage(ctx context.Context, message core.Discove "index", msg.ChunkIndex, "targetCount", len(msg.Targets), ) - return a.processSnapshot(ctx, msg, logger) + return c.processSnapshot(ctx, msg, logger) case core.DiscoveryEvent: // Process individual event-driven update @@ -112,7 +115,7 @@ func (a *TargetApplier) processMessage(ctx context.Context, message core.Discove "received discovery event", "target", msg.Target.Name, ) - return a.processEvent(ctx, msg, logger) + return c.processEvent(ctx, msg, logger) default: return fmt.Errorf("unknonw discovery message type %T", msg) @@ -120,18 +123,18 @@ func (a *TargetApplier) processMessage(ctx context.Context, message core.Discove } // processSnapshot takes a complete snapshot of discovered targets and reconciles Target CRs accordingly -func (a *TargetApplier) processSnapshot(ctx context.Context, chunk core.DiscoverySnapshot, logger logr.Logger) error { - if a.activeSnapshot == nil { - a.startNewSnapshot(chunk, logger) +func (c *TargetHandler) processSnapshot(ctx context.Context, chunk core.DiscoverySnapshot, logger logr.Logger) error { + if c.activeSnapshot == nil { + c.startNewSnapshot(chunk, logger) return nil } - snapshot := a.activeSnapshot + snapshot := c.activeSnapshot // Check if a new snapshot arrived if snapshot.snapshotID != chunk.SnapshotID { // If current snapshot is complete apply it first if snapshot.complete { - if err := a.applySnapshot(ctx, snapshot, logger); err != nil { + if err := c.applySnapshot(ctx, snapshot, logger); err != nil { return err } } else { @@ -144,40 +147,40 @@ func (a *TargetApplier) processSnapshot(ctx context.Context, chunk core.Discover } // Start collecting the new snapshot - a.startNewSnapshot(chunk, logger) + c.startNewSnapshot(chunk, logger) return nil } - return a.collectSnapshot(chunk, logger) + return c.collectSnapshot(chunk, logger) } -func (a *TargetApplier) startNewSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) { - a.activeSnapshot = &snapshotBuffer{ +func (c *TargetHandler) startNewSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) { + c.activeSnapshot = &snapshotBuffer{ snapshotID: chunk.SnapshotID, totalChunks: chunk.TotalChunks, received: make(map[int][]core.DiscoveredTarget), complete: false, } // Delete buffered events that will be current with new snapshot - a.deferredEvents = nil + c.deferredEvents = nil - a.collectSnapshot(chunk, logger) + c.collectSnapshot(chunk, logger) } -func (a *TargetApplier) collectSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) error { - snapshot := a.activeSnapshot +func (c *TargetHandler) collectSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) error { + snapshot := c.activeSnapshot if chunk.TotalChunks != snapshot.totalChunks { logger.Error(nil, "snapshot totalChunks mismatch", "snapshotID", snapshot.snapshotID) } if chunk.ChunkIndex < 0 || chunk.ChunkIndex >= snapshot.totalChunks { logger.Error(nil, "snapshot chunk index out of range", "index", chunk.ChunkIndex) - a.activeSnapshot = nil + c.activeSnapshot = nil return nil } if _, exists := snapshot.received[chunk.ChunkIndex]; exists { logger.Error(nil, "duplicate snapshot chunk", "index", chunk.ChunkIndex) - a.activeSnapshot = nil + c.activeSnapshot = nil return nil } @@ -190,10 +193,10 @@ func (a *TargetApplier) collectSnapshot(chunk core.DiscoverySnapshot, logger log return nil } -func (a *TargetApplier) applySnapshot(ctx context.Context, snapshot *snapshotBuffer, logger logr.Logger) error { +func (c *TargetHandler) applySnapshot(ctx context.Context, snapshot *snapshotBuffer, logger logr.Logger) error { select { case <-ctx.Done(): - a.activeSnapshot = nil + c.activeSnapshot = nil return nil default: } @@ -202,7 +205,7 @@ func (a *TargetApplier) applySnapshot(ctx context.Context, snapshot *snapshotBuf for i := 0; i < snapshot.totalChunks; i++ { select { case <-ctx.Done(): - a.activeSnapshot = nil + c.activeSnapshot = nil return nil default: } @@ -210,7 +213,7 @@ func (a *TargetApplier) applySnapshot(ctx context.Context, snapshot *snapshotBuf chunk, ok := snapshot.received[i] if !ok { logger.Error(nil, "missing snapshot chunk", "index", i) - a.activeSnapshot = nil + c.activeSnapshot = nil return nil } allTargets = append(allTargets, chunk...) @@ -226,34 +229,34 @@ func (a *TargetApplier) applySnapshot(ctx context.Context, snapshot *snapshotBuf // a.applyTargets // Replay deferred events - for _, event := range a.deferredEvents { + for _, event := range c.deferredEvents { select { case <-ctx.Done(): return nil default: } - if err := a.applyEvent(ctx, event, logger); err != nil { + if err := c.applyEvent(ctx, event, logger); err != nil { return err } } - a.activeSnapshot = nil - a.deferredEvents = nil + c.activeSnapshot = nil + c.deferredEvents = nil return nil } -func (a *TargetApplier) processEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { +func (c *TargetHandler) processEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { // If snapshot collecting is active defer events - if a.activeSnapshot != nil { - a.deferredEvents = append(a.deferredEvents, event) + if c.activeSnapshot != nil { + c.deferredEvents = append(c.deferredEvents, event) return nil } // Apply events - return a.applyEvent(ctx, event, logger) + return c.applyEvent(ctx, event, logger) } -func (a *TargetApplier) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { +func (c *TargetHandler) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { switch event.Event { case core.CREATE: logger.Info("Would create target", "name", event.Target.Name, "address", event.Target.Address, "labels", event.Target.Labels) @@ -264,3 +267,19 @@ func (a *TargetApplier) applyEvent(ctx context.Context, event core.DiscoveryEven } return nil } + +func (c *TargetHandler) fetchExistingTargets() ([]gnmicv1alpha1.Target, error) { + var targetList gnmicv1alpha1.TargetList + + err := c.client.List(c.ctx, &targetList, + client.InNamespace(c.targetSource.Namespace), + client.MatchingLabels{ + "gnmic.io/source": c.targetSource.Name, + }, + ) + if err != nil { + return nil, err + } + + return targetList.Items, nil +} diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 77a3a35..4d5f400 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -164,7 +164,7 @@ func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSour // startDiscoveryPipeline creates and starts a discover pipeline for a TargetSource // // Pipeline semantics: -// 1. target-applier is mandatory and must start first +// 1. target-handler is mandatory and must start first // 2. loader is optional and conditional on spec // 3. Permanent failure of required components shuts down the pipeline // 4. Shutdown ordering: cancel ctx -> wait for goroutines to exit -> close channel -> unregister @@ -176,30 +176,30 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName return err } - // Create target applier instance - applier := discovery.NewTargetApplier( + // Create target targetHandler instance + targetHandler := discovery.NewTargetHandler( r.Client, r.Scheme, targetSource, targetChannel, ) - // Start target applier - applierReady := make(chan struct{}) + // Start target handler + handlerReady := make(chan struct{}) supervisor.StartSupervisedComponent(discovery.ComponentSpec{ - Name: "target-applier", + Name: "target-handler", Policy: discovery.RestartPolicy{ MaxRestarts: pipelineMaxRestarts, Backoff: pipelineBackoff, }, EscalatesOnFailure: true, Run: func(ctx context.Context) error { - close(applierReady) // Signals that applier started successfully - return applier.Run(ctx) + close(handlerReady) // Signals that handler started successfully + return targetHandler.Run(ctx) }, }) - // Wait for applier to be ready before starting loader + // Wait for handler to be ready before starting loader select { - case <-applierReady: + case <-handlerReady: case <-supervisor.Done(): return nil } From 240a2bc382c5133829d327bde1cebfb4fd1530e9 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Sun, 26 Apr 2026 19:59:29 -0600 Subject: [PATCH 075/153] ran go mod tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index f236ded..827da2a 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.5 require ( github.com/cert-manager/cert-manager v1.19.3 github.com/go-logr/logr v1.4.3 + github.com/google/uuid v1.6.0 github.com/onsi/ginkgo/v2 v2.27.3 github.com/onsi/gomega v1.38.3 github.com/openconfig/gnmic/pkg/api v0.1.10 @@ -47,7 +48,6 @@ require ( github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f // indirect - github.com/google/uuid v1.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect From 7ef1281a7b37bd8b9a845501f7011c615710429b Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 27 Apr 2026 10:10:08 -0600 Subject: [PATCH 076/153] renamed target applier to message processor & created client.go for generic functions --- internal/controller/discovery/client.go | 25 ++++ ...target_handler.go => message_processor.go} | 112 ++++++++---------- .../controller/targetsource_controller.go | 2 +- 3 files changed, 74 insertions(+), 65 deletions(-) create mode 100644 internal/controller/discovery/client.go rename internal/controller/discovery/{target_handler.go => message_processor.go} (63%) diff --git a/internal/controller/discovery/client.go b/internal/controller/discovery/client.go new file mode 100644 index 0000000..72147b7 --- /dev/null +++ b/internal/controller/discovery/client.go @@ -0,0 +1,25 @@ +package discovery + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" + + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" +) + +func fetchExistingTargets(ctx context.Context, c client.Client, ts *gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { + var targetList gnmicv1alpha1.TargetList + + err := c.List(ctx, &targetList, + client.InNamespace(ts.Namespace), + client.MatchingLabels{ + "gnmic.io/source": ts.Name, + }, + ) + if err != nil { + return nil, err + } + + return targetList.Items, nil +} diff --git a/internal/controller/discovery/target_handler.go b/internal/controller/discovery/message_processor.go similarity index 63% rename from internal/controller/discovery/target_handler.go rename to internal/controller/discovery/message_processor.go index e8c0308..65c8b44 100644 --- a/internal/controller/discovery/target_handler.go +++ b/internal/controller/discovery/message_processor.go @@ -20,8 +20,8 @@ type snapshotBuffer struct { complete bool } -// TargetHandler consumes discovered targets and applies them to Kubernetes -type TargetHandler struct { +// MessageProcessor consumes discovered targets and applies them to Kubernetes +type MessageProcessor struct { ctx context.Context client client.Client scheme *runtime.Scheme @@ -33,9 +33,9 @@ type TargetHandler struct { deferredEvents []core.DiscoveryEvent } -// NewTargetHandler wires a TargetHandler instance -func NewTargetHandler(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []core.DiscoveryMessage) *TargetHandler { - return &TargetHandler{ +// NewMessageProcessor wires a MessageProcessor instance +func NewMessageProcessor(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []core.DiscoveryMessage) *MessageProcessor { + return &MessageProcessor{ client: c, scheme: s, targetSource: ts, @@ -45,40 +45,40 @@ func NewTargetHandler(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.Targ // Run is a long‑running loop that receives target snapshots // and reconciles Target CRs accordingly -func (c *TargetHandler) Run(ctx context.Context) error { - c.ctx = ctx +func (m *MessageProcessor) Run(ctx context.Context) error { + m.ctx = ctx - logger := log.FromContext(c.ctx). + logger := log.FromContext(m.ctx). WithValues( - "name", c.targetSource.Name, - "namespace", c.targetSource.Namespace, + "name", m.targetSource.Name, + "namespace", m.targetSource.Namespace, ) logger.Info("target handler started") - for c.ctx.Err() == nil { + for m.ctx.Err() == nil { select { - case batch, ok := <-c.in: + case batch, ok := <-m.in: if !ok { // Channel closed, pipeline is shutting down logger.Info("input channel closed, stopping target handler") return nil } - c.queue = append(c.queue, batch...) + m.queue = append(m.queue, batch...) case <-ctx.Done(): logger.Info("context canceled, stopping target handler") return nil } - for len(c.queue) > 0 { + for len(m.queue) > 0 { if ctx.Err() != nil { return nil // why return nil? } - msg := c.queue[0] - c.queue = c.queue[1:] + msg := m.queue[0] + m.queue = m.queue[1:] - if err := c.processMessage(c.ctx, msg, logger); err != nil { + if err := m.processMessage(m.ctx, msg, logger); err != nil { // Returning error lets the supervisor (controller) // tear down and restart the pipeline via reconciliation // Q: when to return an error vs just log and continue? @@ -92,7 +92,7 @@ func (c *TargetHandler) Run(ctx context.Context) error { return nil } -func (c *TargetHandler) processMessage(ctx context.Context, message core.DiscoveryMessage, logger logr.Logger) error { +func (m *MessageProcessor) processMessage(ctx context.Context, message core.DiscoveryMessage, logger logr.Logger) error { if err := ctx.Err(); err != nil { return err } @@ -107,7 +107,7 @@ func (c *TargetHandler) processMessage(ctx context.Context, message core.Discove "index", msg.ChunkIndex, "targetCount", len(msg.Targets), ) - return c.processSnapshot(ctx, msg, logger) + return m.processSnapshot(ctx, msg, logger) case core.DiscoveryEvent: // Process individual event-driven update @@ -115,7 +115,7 @@ func (c *TargetHandler) processMessage(ctx context.Context, message core.Discove "received discovery event", "target", msg.Target.Name, ) - return c.processEvent(ctx, msg, logger) + return m.processEvent(ctx, msg, logger) default: return fmt.Errorf("unknonw discovery message type %T", msg) @@ -123,18 +123,18 @@ func (c *TargetHandler) processMessage(ctx context.Context, message core.Discove } // processSnapshot takes a complete snapshot of discovered targets and reconciles Target CRs accordingly -func (c *TargetHandler) processSnapshot(ctx context.Context, chunk core.DiscoverySnapshot, logger logr.Logger) error { - if c.activeSnapshot == nil { - c.startNewSnapshot(chunk, logger) +func (m *MessageProcessor) processSnapshot(ctx context.Context, chunk core.DiscoverySnapshot, logger logr.Logger) error { + if m.activeSnapshot == nil { + m.startNewSnapshot(chunk, logger) return nil } - snapshot := c.activeSnapshot + snapshot := m.activeSnapshot // Check if a new snapshot arrived if snapshot.snapshotID != chunk.SnapshotID { // If current snapshot is complete apply it first if snapshot.complete { - if err := c.applySnapshot(ctx, snapshot, logger); err != nil { + if err := m.applySnapshot(ctx, snapshot, logger); err != nil { return err } } else { @@ -147,40 +147,40 @@ func (c *TargetHandler) processSnapshot(ctx context.Context, chunk core.Discover } // Start collecting the new snapshot - c.startNewSnapshot(chunk, logger) + m.startNewSnapshot(chunk, logger) return nil } - return c.collectSnapshot(chunk, logger) + return m.collectSnapshot(chunk, logger) } -func (c *TargetHandler) startNewSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) { - c.activeSnapshot = &snapshotBuffer{ +func (m *MessageProcessor) startNewSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) { + m.activeSnapshot = &snapshotBuffer{ snapshotID: chunk.SnapshotID, totalChunks: chunk.TotalChunks, received: make(map[int][]core.DiscoveredTarget), complete: false, } // Delete buffered events that will be current with new snapshot - c.deferredEvents = nil + m.deferredEvents = nil - c.collectSnapshot(chunk, logger) + m.collectSnapshot(chunk, logger) } -func (c *TargetHandler) collectSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) error { - snapshot := c.activeSnapshot +func (m *MessageProcessor) collectSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) error { + snapshot := m.activeSnapshot if chunk.TotalChunks != snapshot.totalChunks { logger.Error(nil, "snapshot totalChunks mismatch", "snapshotID", snapshot.snapshotID) } if chunk.ChunkIndex < 0 || chunk.ChunkIndex >= snapshot.totalChunks { logger.Error(nil, "snapshot chunk index out of range", "index", chunk.ChunkIndex) - c.activeSnapshot = nil + m.activeSnapshot = nil return nil } if _, exists := snapshot.received[chunk.ChunkIndex]; exists { logger.Error(nil, "duplicate snapshot chunk", "index", chunk.ChunkIndex) - c.activeSnapshot = nil + m.activeSnapshot = nil return nil } @@ -193,10 +193,10 @@ func (c *TargetHandler) collectSnapshot(chunk core.DiscoverySnapshot, logger log return nil } -func (c *TargetHandler) applySnapshot(ctx context.Context, snapshot *snapshotBuffer, logger logr.Logger) error { +func (m *MessageProcessor) applySnapshot(ctx context.Context, snapshot *snapshotBuffer, logger logr.Logger) error { select { case <-ctx.Done(): - c.activeSnapshot = nil + m.activeSnapshot = nil return nil default: } @@ -205,7 +205,7 @@ func (c *TargetHandler) applySnapshot(ctx context.Context, snapshot *snapshotBuf for i := 0; i < snapshot.totalChunks; i++ { select { case <-ctx.Done(): - c.activeSnapshot = nil + m.activeSnapshot = nil return nil default: } @@ -213,7 +213,7 @@ func (c *TargetHandler) applySnapshot(ctx context.Context, snapshot *snapshotBuf chunk, ok := snapshot.received[i] if !ok { logger.Error(nil, "missing snapshot chunk", "index", i) - c.activeSnapshot = nil + m.activeSnapshot = nil return nil } allTargets = append(allTargets, chunk...) @@ -229,34 +229,34 @@ func (c *TargetHandler) applySnapshot(ctx context.Context, snapshot *snapshotBuf // a.applyTargets // Replay deferred events - for _, event := range c.deferredEvents { + for _, event := range m.deferredEvents { select { case <-ctx.Done(): return nil default: } - if err := c.applyEvent(ctx, event, logger); err != nil { + if err := m.applyEvent(ctx, event, logger); err != nil { return err } } - c.activeSnapshot = nil - c.deferredEvents = nil + m.activeSnapshot = nil + m.deferredEvents = nil return nil } -func (c *TargetHandler) processEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { +func (m *MessageProcessor) processEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { // If snapshot collecting is active defer events - if c.activeSnapshot != nil { - c.deferredEvents = append(c.deferredEvents, event) + if m.activeSnapshot != nil { + m.deferredEvents = append(m.deferredEvents, event) return nil } // Apply events - return c.applyEvent(ctx, event, logger) + return m.applyEvent(ctx, event, logger) } -func (c *TargetHandler) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { +func (m *MessageProcessor) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { switch event.Event { case core.CREATE: logger.Info("Would create target", "name", event.Target.Name, "address", event.Target.Address, "labels", event.Target.Labels) @@ -267,19 +267,3 @@ func (c *TargetHandler) applyEvent(ctx context.Context, event core.DiscoveryEven } return nil } - -func (c *TargetHandler) fetchExistingTargets() ([]gnmicv1alpha1.Target, error) { - var targetList gnmicv1alpha1.TargetList - - err := c.client.List(c.ctx, &targetList, - client.InNamespace(c.targetSource.Namespace), - client.MatchingLabels{ - "gnmic.io/source": c.targetSource.Name, - }, - ) - if err != nil { - return nil, err - } - - return targetList.Items, nil -} diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 4d5f400..8070a3a 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -177,7 +177,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName } // Create target targetHandler instance - targetHandler := discovery.NewTargetHandler( + targetHandler := discovery.NewMessageProcessor( r.Client, r.Scheme, targetSource, From 7bcbcc023ff39e36a565e9235f503a98375f3327 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Fri, 24 Apr 2026 15:15:56 -0600 Subject: [PATCH 077/153] added const file for common labels --- internal/controller/discovery/client.go | 3 ++- internal/controller/discovery/core/const.go | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 internal/controller/discovery/core/const.go diff --git a/internal/controller/discovery/client.go b/internal/controller/discovery/client.go index 3bc7ef7..d23c043 100644 --- a/internal/controller/discovery/client.go +++ b/internal/controller/discovery/client.go @@ -8,6 +8,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" + "github.com/gnmic/operator/internal/controller/discovery/core" ) func FetchExistingTargets(ctx context.Context, c client.Client, ts gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { @@ -16,7 +17,7 @@ func FetchExistingTargets(ctx context.Context, c client.Client, ts gnmicv1alpha1 err := c.List(ctx, &targetList, client.InNamespace(ts.Namespace), client.MatchingLabels{ - "gnmic.io/source": ts.Name, + core.LabelTargetSourceName: ts.Name, }, ) if err != nil { diff --git a/internal/controller/discovery/core/const.go b/internal/controller/discovery/core/const.go new file mode 100644 index 0000000..82a5962 --- /dev/null +++ b/internal/controller/discovery/core/const.go @@ -0,0 +1,6 @@ +package core + +const ( + // Labels + LabelTargetSourceName = "operator.gnmic.dev/targetsource" +) From d10fc9ac868d50be64c123cbc619b2f4eb189682 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 27 Apr 2026 11:26:10 -0600 Subject: [PATCH 078/153] removed all package --- internal/controller/discovery/loaders/all/all.go | 5 ----- internal/controller/targetsource_controller.go | 1 - 2 files changed, 6 deletions(-) delete mode 100644 internal/controller/discovery/loaders/all/all.go diff --git a/internal/controller/discovery/loaders/all/all.go b/internal/controller/discovery/loaders/all/all.go deleted file mode 100644 index 3590cda..0000000 --- a/internal/controller/discovery/loaders/all/all.go +++ /dev/null @@ -1,5 +0,0 @@ -package all - -import ( - _ "github.com/gnmic/operator/internal/controller/discovery/loaders/http" -) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 8070a3a..49f9683 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -32,7 +32,6 @@ import ( "github.com/gnmic/operator/internal/controller/discovery" "github.com/gnmic/operator/internal/controller/discovery/core" "github.com/gnmic/operator/internal/controller/discovery/loaders" - _ "github.com/gnmic/operator/internal/controller/discovery/loaders/all" "github.com/gnmic/operator/internal/controller/discovery/registry" "github.com/go-logr/logr" ) From 108bd2dc3f58b2193535c8eadf6c30ee1d6d0dad Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 27 Apr 2026 11:34:11 -0600 Subject: [PATCH 079/153] changed error lookup to apierrors --- internal/controller/targetsource_controller.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 232c624..2f198a6 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -21,6 +21,7 @@ import ( "sync" "time" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -81,13 +82,12 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request WithValues("targetsource", req.NamespacedName) targetSource, err := r.fetchTargetSource(ctx, req.NamespacedName) - if err != nil { - // If the TargetSource no longer exists, ensure runtime cleanup - if client.IgnoreNotFound(err) == nil { - logger.Info("TargetSource not found; stopping discovery pipeline") - r.stopDiscoveryPipeline(req.NamespacedName) - return ctrl.Result{}, nil - } + // If the TargetSource no longer exists, ensure runtime cleanup + if apierrors.IsNotFound(err) { + logger.Info("TargetSource not found; stopping discovery pipeline") + r.stopDiscoveryPipeline(req.NamespacedName) + return ctrl.Result{}, nil + } else if err != nil { return ctrl.Result{}, err } From b7dd0367e99a0c5435db00092c83e1bc01ab439b Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Wed, 29 Apr 2026 08:53:30 +0000 Subject: [PATCH 080/153] remove unused fiels --- internal/controller/discovery/mapper.go | 4 ---- internal/controller/discovery/mapper_test.go | 1 - 2 files changed, 5 deletions(-) delete mode 100644 internal/controller/discovery/mapper.go delete mode 100644 internal/controller/discovery/mapper_test.go diff --git a/internal/controller/discovery/mapper.go b/internal/controller/discovery/mapper.go deleted file mode 100644 index 18470b2..0000000 --- a/internal/controller/discovery/mapper.go +++ /dev/null @@ -1,4 +0,0 @@ -package discovery - -// This file makes diff between existing and new targets -// file decides which targets to create/update/delete diff --git a/internal/controller/discovery/mapper_test.go b/internal/controller/discovery/mapper_test.go deleted file mode 100644 index 5844159..0000000 --- a/internal/controller/discovery/mapper_test.go +++ /dev/null @@ -1 +0,0 @@ -package discovery From d3a9b5ca3021c9f0485698c1d1c54bbd3562bb9b Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Wed, 29 Apr 2026 11:56:56 +0000 Subject: [PATCH 081/153] rename files and restructure packages --- .../core/{loader_interface.go => loader.go} | 0 .../core/{message_interface.go => message.go} | 0 .../discovery/core/{helpers.go => send.go} | 0 internal/controller/discovery/core/types.go | 4 ++-- internal/controller/discovery/discovery.go | 17 +++++++++++++++++ .../loaders/{loaders.go => factory.go} | 0 .../loaders/http/{http.go => loader.go} | 0 .../http/{http_test.go => loader_test.go} | 0 .../discovery/{ => pipeline}/supervisor.go | 5 +++-- .../discovery/{ => reconciler}/client.go | 13 ++++++++++--- .../{ => reconciler}/message_processor.go | 2 +- internal/controller/targetsource_controller.go | 15 ++++++++------- 12 files changed, 41 insertions(+), 15 deletions(-) rename internal/controller/discovery/core/{loader_interface.go => loader.go} (100%) rename internal/controller/discovery/core/{message_interface.go => message.go} (100%) rename internal/controller/discovery/core/{helpers.go => send.go} (100%) create mode 100644 internal/controller/discovery/discovery.go rename internal/controller/discovery/loaders/{loaders.go => factory.go} (100%) rename internal/controller/discovery/loaders/http/{http.go => loader.go} (100%) rename internal/controller/discovery/loaders/http/{http_test.go => loader_test.go} (100%) rename internal/controller/discovery/{ => pipeline}/supervisor.go (95%) rename internal/controller/discovery/{ => reconciler}/client.go (68%) rename internal/controller/discovery/{ => reconciler}/message_processor.go (99%) diff --git a/internal/controller/discovery/core/loader_interface.go b/internal/controller/discovery/core/loader.go similarity index 100% rename from internal/controller/discovery/core/loader_interface.go rename to internal/controller/discovery/core/loader.go diff --git a/internal/controller/discovery/core/message_interface.go b/internal/controller/discovery/core/message.go similarity index 100% rename from internal/controller/discovery/core/message_interface.go rename to internal/controller/discovery/core/message.go diff --git a/internal/controller/discovery/core/helpers.go b/internal/controller/discovery/core/send.go similarity index 100% rename from internal/controller/discovery/core/helpers.go rename to internal/controller/discovery/core/send.go diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 3f6957a..28ec503 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -12,14 +12,14 @@ type DiscoveredTarget struct { Labels map[string]string } +type EventAction int + const ( DELETE EventAction = 0 CREATE EventAction = 1 UPDATE EventAction = 2 ) -type EventAction int - type DiscoveryEvent struct { Target DiscoveredTarget Event EventAction diff --git a/internal/controller/discovery/discovery.go b/internal/controller/discovery/discovery.go new file mode 100644 index 0000000..3dc51bd --- /dev/null +++ b/internal/controller/discovery/discovery.go @@ -0,0 +1,17 @@ +package discovery + +// Package discovery implements the discovery runtime subsystem. +// +// The discovery subsystem is responsible for: +// - Receiving discovery data from external providers (loaders, webhooks). +// - Supervising discovery pipelines and restart semantics. +// - Applying discovered state to Kubernetes Targets. +// +// The package is structured into the following subpackages: +// - core: message contracts, snapshot/event types, and transport helpers. +// - pipeline: supervision, restart policies, and lifecycle control. +// - reconciler: snapshot + event target state application logic. +// - loaders: target discovery providers (HTTP, webhook, etc.). +// - registry: key -> channel registry. +// +// At the moment, the targetsource controller imports specific subpackages explicitly. diff --git a/internal/controller/discovery/loaders/loaders.go b/internal/controller/discovery/loaders/factory.go similarity index 100% rename from internal/controller/discovery/loaders/loaders.go rename to internal/controller/discovery/loaders/factory.go diff --git a/internal/controller/discovery/loaders/http/http.go b/internal/controller/discovery/loaders/http/loader.go similarity index 100% rename from internal/controller/discovery/loaders/http/http.go rename to internal/controller/discovery/loaders/http/loader.go diff --git a/internal/controller/discovery/loaders/http/http_test.go b/internal/controller/discovery/loaders/http/loader_test.go similarity index 100% rename from internal/controller/discovery/loaders/http/http_test.go rename to internal/controller/discovery/loaders/http/loader_test.go diff --git a/internal/controller/discovery/supervisor.go b/internal/controller/discovery/pipeline/supervisor.go similarity index 95% rename from internal/controller/discovery/supervisor.go rename to internal/controller/discovery/pipeline/supervisor.go index 710381e..042d305 100644 --- a/internal/controller/discovery/supervisor.go +++ b/internal/controller/discovery/pipeline/supervisor.go @@ -1,4 +1,4 @@ -package discovery +package pipeline import ( "context" @@ -25,12 +25,13 @@ type Supervisor struct { stopped bool } -// RestartPolicy defines the restart behavior for a component +// RestartPolicy defines restart behavior of a component type RestartPolicy struct { MaxRestarts int Backoff time.Duration } +// ComponentSpec defines a supervised component type ComponentSpec struct { Name string Run func(ctx context.Context) error diff --git a/internal/controller/discovery/client.go b/internal/controller/discovery/reconciler/client.go similarity index 68% rename from internal/controller/discovery/client.go rename to internal/controller/discovery/reconciler/client.go index 25100bd..4bbbbc1 100644 --- a/internal/controller/discovery/client.go +++ b/internal/controller/discovery/reconciler/client.go @@ -1,4 +1,4 @@ -package discovery +package reconciler import ( "context" @@ -9,10 +9,17 @@ import ( "github.com/gnmic/operator/internal/controller/discovery/core" ) -func fetchExistingTargets(ctx context.Context, c client.Client, ts *gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { +func fetchExistingTargets( + ctx context.Context, + c client.Client, + ts *gnmicv1alpha1.TargetSource, +) ([]gnmicv1alpha1.Target, error) { + var targetList gnmicv1alpha1.TargetList - err := c.List(ctx, &targetList, + err := c.List( + ctx, + &targetList, client.InNamespace(ts.Namespace), client.MatchingLabels{ core.LabelTargetSourceName: ts.Name, diff --git a/internal/controller/discovery/message_processor.go b/internal/controller/discovery/reconciler/message_processor.go similarity index 99% rename from internal/controller/discovery/message_processor.go rename to internal/controller/discovery/reconciler/message_processor.go index 65c8b44..0c205bd 100644 --- a/internal/controller/discovery/message_processor.go +++ b/internal/controller/discovery/reconciler/message_processor.go @@ -1,4 +1,4 @@ -package discovery +package reconciler import ( "context" diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 49f9683..35946d2 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -29,9 +29,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" - "github.com/gnmic/operator/internal/controller/discovery" "github.com/gnmic/operator/internal/controller/discovery/core" "github.com/gnmic/operator/internal/controller/discovery/loaders" + "github.com/gnmic/operator/internal/controller/discovery/pipeline" + "github.com/gnmic/operator/internal/controller/discovery/reconciler" "github.com/gnmic/operator/internal/controller/discovery/registry" "github.com/go-logr/logr" ) @@ -168,7 +169,7 @@ func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSour // 3. Permanent failure of required components shuts down the pipeline // 4. Shutdown ordering: cancel ctx -> wait for goroutines to exit -> close channel -> unregister func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource, logger logr.Logger) error { - supervisor := discovery.NewSupervisor(context.Background()) + supervisor := pipeline.NewSupervisor(context.Background()) targetChannel := make(chan []core.DiscoveryMessage, r.BufferSize) if err := r.DiscoveryRegistry.Register(key, targetChannel); err != nil { @@ -176,7 +177,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName } // Create target targetHandler instance - targetHandler := discovery.NewMessageProcessor( + targetHandler := reconciler.NewMessageProcessor( r.Client, r.Scheme, targetSource, @@ -184,9 +185,9 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName ) // Start target handler handlerReady := make(chan struct{}) - supervisor.StartSupervisedComponent(discovery.ComponentSpec{ + supervisor.StartSupervisedComponent(pipeline.ComponentSpec{ Name: "target-handler", - Policy: discovery.RestartPolicy{ + Policy: pipeline.RestartPolicy{ MaxRestarts: pipelineMaxRestarts, Backoff: pipelineBackoff, }, @@ -217,9 +218,9 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName return err } - supervisor.StartSupervisedComponent(discovery.ComponentSpec{ + supervisor.StartSupervisedComponent(pipeline.ComponentSpec{ Name: "loader", - Policy: discovery.RestartPolicy{ + Policy: pipeline.RestartPolicy{ MaxRestarts: pipelineMaxRestarts, Backoff: pipelineBackoff, }, From 0c80394ab358c662fe519b872ed7219c2f7e384c Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Wed, 29 Apr 2026 12:01:40 +0000 Subject: [PATCH 082/153] rename target handler to target reconciler --- .../discovery/reconciler/message_processor.go | 8 ++++---- internal/controller/targetsource_controller.go | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/controller/discovery/reconciler/message_processor.go b/internal/controller/discovery/reconciler/message_processor.go index 0c205bd..2c4632c 100644 --- a/internal/controller/discovery/reconciler/message_processor.go +++ b/internal/controller/discovery/reconciler/message_processor.go @@ -53,20 +53,20 @@ func (m *MessageProcessor) Run(ctx context.Context) error { "name", m.targetSource.Name, "namespace", m.targetSource.Namespace, ) - logger.Info("target handler started") + logger.Info("target reconciler started") for m.ctx.Err() == nil { select { case batch, ok := <-m.in: if !ok { // Channel closed, pipeline is shutting down - logger.Info("input channel closed, stopping target handler") + logger.Info("input channel closed, stopping target reconciler") return nil } m.queue = append(m.queue, batch...) case <-ctx.Done(): - logger.Info("context canceled, stopping target handler") + logger.Info("context canceled, stopping target reconciler") return nil } @@ -88,7 +88,7 @@ func (m *MessageProcessor) Run(ctx context.Context) error { } } - logger.Info("target handler stopped") + logger.Info("target reconciler stopped") return nil } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 35946d2..6c9ad31 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -164,7 +164,7 @@ func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSour // startDiscoveryPipeline creates and starts a discover pipeline for a TargetSource // // Pipeline semantics: -// 1. target-handler is mandatory and must start first +// 1. target reconciler is mandatory and must start first // 2. loader is optional and conditional on spec // 3. Permanent failure of required components shuts down the pipeline // 4. Shutdown ordering: cancel ctx -> wait for goroutines to exit -> close channel -> unregister @@ -176,14 +176,14 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName return err } - // Create target targetHandler instance - targetHandler := reconciler.NewMessageProcessor( + // Create target reconciler instance + targetReconciler := reconciler.NewMessageProcessor( r.Client, r.Scheme, targetSource, targetChannel, ) - // Start target handler + // Start target reconciler handlerReady := make(chan struct{}) supervisor.StartSupervisedComponent(pipeline.ComponentSpec{ Name: "target-handler", @@ -194,7 +194,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName EscalatesOnFailure: true, Run: func(ctx context.Context) error { close(handlerReady) // Signals that handler started successfully - return targetHandler.Run(ctx) + return targetReconciler.Run(ctx) }, }) // Wait for handler to be ready before starting loader From 04208bf078b170160a6ef72eda6b6ddaa3630070 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Wed, 29 Apr 2026 12:20:58 +0000 Subject: [PATCH 083/153] rename handler to reconciler --- internal/controller/targetsource_controller.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 6c9ad31..9078af2 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -184,22 +184,22 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName targetChannel, ) // Start target reconciler - handlerReady := make(chan struct{}) + reconcilerReady := make(chan struct{}) supervisor.StartSupervisedComponent(pipeline.ComponentSpec{ - Name: "target-handler", + Name: "target-reconciler", Policy: pipeline.RestartPolicy{ MaxRestarts: pipelineMaxRestarts, Backoff: pipelineBackoff, }, EscalatesOnFailure: true, Run: func(ctx context.Context) error { - close(handlerReady) // Signals that handler started successfully + close(reconcilerReady) // Signals that reconciler started successfully return targetReconciler.Run(ctx) }, }) - // Wait for handler to be ready before starting loader + // Wait for reconciler to be ready before starting loader select { - case <-handlerReady: + case <-reconcilerReady: case <-supervisor.Done(): return nil } From c3818ce6f7693360496866d7ba1694f7ce702f32 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Wed, 29 Apr 2026 12:21:46 +0000 Subject: [PATCH 084/153] clarify interface files --- .../discovery/core/{loader.go => loader_interface.go} | 2 +- internal/controller/discovery/core/message.go | 4 ---- internal/controller/discovery/core/message_interface.go | 5 +++++ 3 files changed, 6 insertions(+), 5 deletions(-) rename internal/controller/discovery/core/{loader.go => loader_interface.go} (91%) create mode 100644 internal/controller/discovery/core/message_interface.go diff --git a/internal/controller/discovery/core/loader.go b/internal/controller/discovery/core/loader_interface.go similarity index 91% rename from internal/controller/discovery/core/loader.go rename to internal/controller/discovery/core/loader_interface.go index 8964be8..72f1898 100644 --- a/internal/controller/discovery/core/loader.go +++ b/internal/controller/discovery/core/loader_interface.go @@ -14,7 +14,7 @@ type Loader interface { Name() string // Start begins discovery and pushes target snapshots or events into the out channel - // The loader must stop cleanly when ctx is cancelled + // The loader must stop cleanly when ctx is canceled Start( ctx context.Context, targetsourceName types.NamespacedName, diff --git a/internal/controller/discovery/core/message.go b/internal/controller/discovery/core/message.go index 0836bc6..af4f6c1 100644 --- a/internal/controller/discovery/core/message.go +++ b/internal/controller/discovery/core/message.go @@ -1,8 +1,4 @@ package core -type DiscoveryMessage interface { - isDiscoveryMessage() -} - func (DiscoveryEvent) isDiscoveryMessage() {} func (DiscoverySnapshot) isDiscoveryMessage() {} diff --git a/internal/controller/discovery/core/message_interface.go b/internal/controller/discovery/core/message_interface.go new file mode 100644 index 0000000..07b819e --- /dev/null +++ b/internal/controller/discovery/core/message_interface.go @@ -0,0 +1,5 @@ +package core + +type DiscoveryMessage interface { + isDiscoveryMessage() +} From e4df0d4a6245d71d48539414b0f3ab45136de874 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Wed, 29 Apr 2026 12:35:14 +0000 Subject: [PATCH 085/153] define EventAction to be go idomatic --- internal/controller/discovery/core/types.go | 20 +++++++++++-------- .../discovery/reconciler/message_processor.go | 6 +++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 28ec503..1ae2f7a 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -4,6 +4,18 @@ type LoaderConfig struct { ChunkSize int } +// EventAction represents the type of a discovery event +type EventAction int + +const ( + // EventDelete indicates that a target should be removed + EventDelete EventAction = iota + // EventCreate indicates that a target should be created + EventCreate + // EventUpdate indicates that a target should be updated + EventUpdate +) + // DiscoveredTarget represents a target discovered from an external source // before it is materialized as a Kubernetes Target CR type DiscoveredTarget struct { @@ -12,14 +24,6 @@ type DiscoveredTarget struct { Labels map[string]string } -type EventAction int - -const ( - DELETE EventAction = 0 - CREATE EventAction = 1 - UPDATE EventAction = 2 -) - type DiscoveryEvent struct { Target DiscoveredTarget Event EventAction diff --git a/internal/controller/discovery/reconciler/message_processor.go b/internal/controller/discovery/reconciler/message_processor.go index 2c4632c..a0e91e5 100644 --- a/internal/controller/discovery/reconciler/message_processor.go +++ b/internal/controller/discovery/reconciler/message_processor.go @@ -258,11 +258,11 @@ func (m *MessageProcessor) processEvent(ctx context.Context, event core.Discover func (m *MessageProcessor) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { switch event.Event { - case core.CREATE: + case core.EventCreate: logger.Info("Would create target", "name", event.Target.Name, "address", event.Target.Address, "labels", event.Target.Labels) - case core.UPDATE: + case core.EventUpdate: logger.Info("Would update target", "name", event.Target.Name, "address", event.Target.Address, "labels", event.Target.Labels) - case core.DELETE: + case core.EventDelete: logger.Info("Would delete target", "name", event.Target.Name) } return nil From 86c0af066faef2af3e75d68d3285c16dc6978bbe Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Wed, 29 Apr 2026 13:49:19 +0000 Subject: [PATCH 086/153] add webhook activation info to metadata of DiscoveryRegistry --- cmd/main.go | 2 +- internal/apiserver/apiserver.go | 2 +- internal/controller/discovery/core/types.go | 5 +++++ .../controller/discovery/registry/registry.go | 14 +++++++------- internal/controller/targetsource_controller.go | 18 +++++++++++------- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index e4bad31..4cf6e94 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -86,7 +86,7 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - discoveryRegistry := registry.NewRegistry[types.NamespacedName, []core.DiscoveryMessage]() + discoveryRegistry := registry.NewRegistry[types.NamespacedName, core.DiscoveryRegistryValue]() mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, diff --git a/internal/apiserver/apiserver.go b/internal/apiserver/apiserver.go index 17e5c82..a7ca16a 100644 --- a/internal/apiserver/apiserver.go +++ b/internal/apiserver/apiserver.go @@ -14,7 +14,7 @@ type APIServer struct { Server *http.Server clusterReconciler *controller.ClusterReconciler - DiscoveryRegistry *registry.Registry[types.NamespacedName, []core.DiscoveryMessage] + DiscoveryRegistry *registry.Registry[types.NamespacedName, core.DiscoveryRegistryValue] } func New(addr string, clusterReconciler *controller.ClusterReconciler) *APIServer { diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 1ae2f7a..68c9c7e 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -1,5 +1,10 @@ package core +type DiscoveryRegistryValue struct { + Channel chan<- []DiscoveryMessage + WebhookEnabled bool +} + type LoaderConfig struct { ChunkSize int } diff --git a/internal/controller/discovery/registry/registry.go b/internal/controller/discovery/registry/registry.go index 093bd2c..f2630e8 100644 --- a/internal/controller/discovery/registry/registry.go +++ b/internal/controller/discovery/registry/registry.go @@ -10,20 +10,20 @@ import ( // DO NOT USE a pointer type as K type Registry[K comparable, V any] struct { mu sync.RWMutex - m map[K]chan<- V + m map[K]V } func NewRegistry[K comparable, V any]() *Registry[K, V] { - return &Registry[K, V]{m: make(map[K]chan<- V)} + return &Registry[K, V]{m: make(map[K]V)} } -func (r *Registry[K, V]) Register(key K, ch chan<- V) error { +func (r *Registry[K, V]) Register(key K, value V) error { r.mu.Lock() defer r.mu.Unlock() if _, exists := r.m[key]; exists { return fmt.Errorf("already registered: %v", key) } - r.m[key] = ch + r.m[key] = value return nil } @@ -33,9 +33,9 @@ func (r *Registry[K, V]) Unregister(key K) { r.mu.Unlock() } -func (r *Registry[K, V]) Get(key K) (chan<- V, bool) { +func (r *Registry[K, V]) Get(key K) (V, bool) { r.mu.RLock() - ch, ok := r.m[key] + value, ok := r.m[key] r.mu.RUnlock() - return ch, ok + return value, ok } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 9078af2..c7e6460 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -66,7 +66,7 @@ type TargetSourceReconciler struct { BufferSize int ChunkSize int - DiscoveryRegistry *registry.Registry[types.NamespacedName, []core.DiscoveryMessage] + DiscoveryRegistry *registry.Registry[types.NamespacedName, core.DiscoveryRegistryValue] } // +kubebuilder:rbac:groups=operator.gnmic.dev,resources=targetsources,verbs=get;list;watch;create;update;patch;delete @@ -108,7 +108,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, err } - logger.Info("Discover pipeline started") + logger.Info("Discovery pipeline started") return ctrl.Result{}, nil } @@ -161,7 +161,7 @@ func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSour return nil } -// startDiscoveryPipeline creates and starts a discover pipeline for a TargetSource +// startDiscoveryPipeline creates and starts a discovery pipeline for a TargetSource // // Pipeline semantics: // 1. target reconciler is mandatory and must start first @@ -169,10 +169,16 @@ func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSour // 3. Permanent failure of required components shuts down the pipeline // 4. Shutdown ordering: cancel ctx -> wait for goroutines to exit -> close channel -> unregister func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource, logger logr.Logger) error { + loaderConfigured := targetSource.Spec.Provider != nil + webhookActivated := targetSource.Spec.Webhook.Enabled != nil && *targetSource.Spec.Webhook.Enabled + supervisor := pipeline.NewSupervisor(context.Background()) targetChannel := make(chan []core.DiscoveryMessage, r.BufferSize) - if err := r.DiscoveryRegistry.Register(key, targetChannel); err != nil { + if err := r.DiscoveryRegistry.Register(key, core.DiscoveryRegistryValue{ + Channel: targetChannel, + WebhookEnabled: webhookActivated, + }); err != nil { return err } @@ -205,8 +211,6 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName } // Create loader instance - loaderConfigured := targetSource.Spec.Provider != nil - webhookConfigured := targetSource.Spec.Webhook.Enabled != nil if loaderConfigured { loader, err := loaders.NewLoader( key, @@ -224,7 +228,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName MaxRestarts: pipelineMaxRestarts, Backoff: pipelineBackoff, }, - EscalatesOnFailure: !webhookConfigured, + EscalatesOnFailure: !webhookActivated, Run: func(ctx context.Context) error { return loader.Start(ctx, key, targetSource.Spec, targetChannel) }, From 284b1f290bd7f1c33f6213bba5399fb16ac0dae9 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 10:33:40 -0600 Subject: [PATCH 087/153] moved reconciler files to discovery --- internal/controller/discovery/{reconciler => }/client.go | 2 +- .../discovery/{reconciler => }/message_processor.go | 2 +- internal/controller/targetsource_controller.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename internal/controller/discovery/{reconciler => }/client.go (96%) rename internal/controller/discovery/{reconciler => }/message_processor.go (99%) diff --git a/internal/controller/discovery/reconciler/client.go b/internal/controller/discovery/client.go similarity index 96% rename from internal/controller/discovery/reconciler/client.go rename to internal/controller/discovery/client.go index 4bbbbc1..2deb477 100644 --- a/internal/controller/discovery/reconciler/client.go +++ b/internal/controller/discovery/client.go @@ -1,4 +1,4 @@ -package reconciler +package discovery import ( "context" diff --git a/internal/controller/discovery/reconciler/message_processor.go b/internal/controller/discovery/message_processor.go similarity index 99% rename from internal/controller/discovery/reconciler/message_processor.go rename to internal/controller/discovery/message_processor.go index a0e91e5..6e69c99 100644 --- a/internal/controller/discovery/reconciler/message_processor.go +++ b/internal/controller/discovery/message_processor.go @@ -1,4 +1,4 @@ -package reconciler +package discovery import ( "context" diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index c7e6460..84f9a6f 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -29,10 +29,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" + "github.com/gnmic/operator/internal/controller/discovery" "github.com/gnmic/operator/internal/controller/discovery/core" "github.com/gnmic/operator/internal/controller/discovery/loaders" "github.com/gnmic/operator/internal/controller/discovery/pipeline" - "github.com/gnmic/operator/internal/controller/discovery/reconciler" "github.com/gnmic/operator/internal/controller/discovery/registry" "github.com/go-logr/logr" ) @@ -183,7 +183,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName } // Create target reconciler instance - targetReconciler := reconciler.NewMessageProcessor( + targetReconciler := discovery.NewMessageProcessor( r.Client, r.Scheme, targetSource, From b59897c253b5db8858a03026ae187ac6c8959d19 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 10:34:55 -0600 Subject: [PATCH 088/153] renamed messageProcessor to targetReconciler --- ...sage_processor.go => target_reconciler.go} | 96 +++++++++---------- .../controller/targetsource_controller.go | 2 +- 2 files changed, 49 insertions(+), 49 deletions(-) rename internal/controller/discovery/{message_processor.go => target_reconciler.go} (72%) diff --git a/internal/controller/discovery/message_processor.go b/internal/controller/discovery/target_reconciler.go similarity index 72% rename from internal/controller/discovery/message_processor.go rename to internal/controller/discovery/target_reconciler.go index 6e69c99..4f3711c 100644 --- a/internal/controller/discovery/message_processor.go +++ b/internal/controller/discovery/target_reconciler.go @@ -20,8 +20,8 @@ type snapshotBuffer struct { complete bool } -// MessageProcessor consumes discovered targets and applies them to Kubernetes -type MessageProcessor struct { +// TargetReconciler consumes discovered targets and applies them to Kubernetes +type TargetReconciler struct { ctx context.Context client client.Client scheme *runtime.Scheme @@ -33,9 +33,9 @@ type MessageProcessor struct { deferredEvents []core.DiscoveryEvent } -// NewMessageProcessor wires a MessageProcessor instance -func NewMessageProcessor(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []core.DiscoveryMessage) *MessageProcessor { - return &MessageProcessor{ +// NewTargetReconciler wires a TargetReconciler instance +func NewTargetReconciler(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []core.DiscoveryMessage) *TargetReconciler { + return &TargetReconciler{ client: c, scheme: s, targetSource: ts, @@ -45,40 +45,40 @@ func NewMessageProcessor(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.T // Run is a long‑running loop that receives target snapshots // and reconciles Target CRs accordingly -func (m *MessageProcessor) Run(ctx context.Context) error { - m.ctx = ctx +func (r *TargetReconciler) Run(ctx context.Context) error { + r.ctx = ctx - logger := log.FromContext(m.ctx). + logger := log.FromContext(r.ctx). WithValues( - "name", m.targetSource.Name, - "namespace", m.targetSource.Namespace, + "name", r.targetSource.Name, + "namespace", r.targetSource.Namespace, ) logger.Info("target reconciler started") - for m.ctx.Err() == nil { + for r.ctx.Err() == nil { select { - case batch, ok := <-m.in: + case batch, ok := <-r.in: if !ok { // Channel closed, pipeline is shutting down logger.Info("input channel closed, stopping target reconciler") return nil } - m.queue = append(m.queue, batch...) + r.queue = append(r.queue, batch...) case <-ctx.Done(): logger.Info("context canceled, stopping target reconciler") return nil } - for len(m.queue) > 0 { + for len(r.queue) > 0 { if ctx.Err() != nil { return nil // why return nil? } - msg := m.queue[0] - m.queue = m.queue[1:] + msg := r.queue[0] + r.queue = r.queue[1:] - if err := m.processMessage(m.ctx, msg, logger); err != nil { + if err := r.processMessage(r.ctx, msg, logger); err != nil { // Returning error lets the supervisor (controller) // tear down and restart the pipeline via reconciliation // Q: when to return an error vs just log and continue? @@ -92,7 +92,7 @@ func (m *MessageProcessor) Run(ctx context.Context) error { return nil } -func (m *MessageProcessor) processMessage(ctx context.Context, message core.DiscoveryMessage, logger logr.Logger) error { +func (r *TargetReconciler) processMessage(ctx context.Context, message core.DiscoveryMessage, logger logr.Logger) error { if err := ctx.Err(); err != nil { return err } @@ -107,7 +107,7 @@ func (m *MessageProcessor) processMessage(ctx context.Context, message core.Disc "index", msg.ChunkIndex, "targetCount", len(msg.Targets), ) - return m.processSnapshot(ctx, msg, logger) + return r.processSnapshot(ctx, msg, logger) case core.DiscoveryEvent: // Process individual event-driven update @@ -115,7 +115,7 @@ func (m *MessageProcessor) processMessage(ctx context.Context, message core.Disc "received discovery event", "target", msg.Target.Name, ) - return m.processEvent(ctx, msg, logger) + return r.processEvent(ctx, msg, logger) default: return fmt.Errorf("unknonw discovery message type %T", msg) @@ -123,18 +123,18 @@ func (m *MessageProcessor) processMessage(ctx context.Context, message core.Disc } // processSnapshot takes a complete snapshot of discovered targets and reconciles Target CRs accordingly -func (m *MessageProcessor) processSnapshot(ctx context.Context, chunk core.DiscoverySnapshot, logger logr.Logger) error { - if m.activeSnapshot == nil { - m.startNewSnapshot(chunk, logger) +func (r *TargetReconciler) processSnapshot(ctx context.Context, chunk core.DiscoverySnapshot, logger logr.Logger) error { + if r.activeSnapshot == nil { + r.startNewSnapshot(chunk, logger) return nil } - snapshot := m.activeSnapshot + snapshot := r.activeSnapshot // Check if a new snapshot arrived if snapshot.snapshotID != chunk.SnapshotID { // If current snapshot is complete apply it first if snapshot.complete { - if err := m.applySnapshot(ctx, snapshot, logger); err != nil { + if err := r.applySnapshot(ctx, snapshot, logger); err != nil { return err } } else { @@ -147,40 +147,40 @@ func (m *MessageProcessor) processSnapshot(ctx context.Context, chunk core.Disco } // Start collecting the new snapshot - m.startNewSnapshot(chunk, logger) + r.startNewSnapshot(chunk, logger) return nil } - return m.collectSnapshot(chunk, logger) + return r.collectSnapshot(chunk, logger) } -func (m *MessageProcessor) startNewSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) { - m.activeSnapshot = &snapshotBuffer{ +func (r *TargetReconciler) startNewSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) { + r.activeSnapshot = &snapshotBuffer{ snapshotID: chunk.SnapshotID, totalChunks: chunk.TotalChunks, received: make(map[int][]core.DiscoveredTarget), complete: false, } // Delete buffered events that will be current with new snapshot - m.deferredEvents = nil + r.deferredEvents = nil - m.collectSnapshot(chunk, logger) + r.collectSnapshot(chunk, logger) } -func (m *MessageProcessor) collectSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) error { - snapshot := m.activeSnapshot +func (r *TargetReconciler) collectSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) error { + snapshot := r.activeSnapshot if chunk.TotalChunks != snapshot.totalChunks { logger.Error(nil, "snapshot totalChunks mismatch", "snapshotID", snapshot.snapshotID) } if chunk.ChunkIndex < 0 || chunk.ChunkIndex >= snapshot.totalChunks { logger.Error(nil, "snapshot chunk index out of range", "index", chunk.ChunkIndex) - m.activeSnapshot = nil + r.activeSnapshot = nil return nil } if _, exists := snapshot.received[chunk.ChunkIndex]; exists { logger.Error(nil, "duplicate snapshot chunk", "index", chunk.ChunkIndex) - m.activeSnapshot = nil + r.activeSnapshot = nil return nil } @@ -193,10 +193,10 @@ func (m *MessageProcessor) collectSnapshot(chunk core.DiscoverySnapshot, logger return nil } -func (m *MessageProcessor) applySnapshot(ctx context.Context, snapshot *snapshotBuffer, logger logr.Logger) error { +func (r *TargetReconciler) applySnapshot(ctx context.Context, snapshot *snapshotBuffer, logger logr.Logger) error { select { case <-ctx.Done(): - m.activeSnapshot = nil + r.activeSnapshot = nil return nil default: } @@ -205,7 +205,7 @@ func (m *MessageProcessor) applySnapshot(ctx context.Context, snapshot *snapshot for i := 0; i < snapshot.totalChunks; i++ { select { case <-ctx.Done(): - m.activeSnapshot = nil + r.activeSnapshot = nil return nil default: } @@ -213,7 +213,7 @@ func (m *MessageProcessor) applySnapshot(ctx context.Context, snapshot *snapshot chunk, ok := snapshot.received[i] if !ok { logger.Error(nil, "missing snapshot chunk", "index", i) - m.activeSnapshot = nil + r.activeSnapshot = nil return nil } allTargets = append(allTargets, chunk...) @@ -229,34 +229,34 @@ func (m *MessageProcessor) applySnapshot(ctx context.Context, snapshot *snapshot // a.applyTargets // Replay deferred events - for _, event := range m.deferredEvents { + for _, event := range r.deferredEvents { select { case <-ctx.Done(): return nil default: } - if err := m.applyEvent(ctx, event, logger); err != nil { + if err := r.applyEvent(ctx, event, logger); err != nil { return err } } - m.activeSnapshot = nil - m.deferredEvents = nil + r.activeSnapshot = nil + r.deferredEvents = nil return nil } -func (m *MessageProcessor) processEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { +func (r *TargetReconciler) processEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { // If snapshot collecting is active defer events - if m.activeSnapshot != nil { - m.deferredEvents = append(m.deferredEvents, event) + if r.activeSnapshot != nil { + r.deferredEvents = append(r.deferredEvents, event) return nil } // Apply events - return m.applyEvent(ctx, event, logger) + return r.applyEvent(ctx, event, logger) } -func (m *MessageProcessor) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { +func (r *TargetReconciler) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { switch event.Event { case core.EventCreate: logger.Info("Would create target", "name", event.Target.Name, "address", event.Target.Address, "labels", event.Target.Labels) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 84f9a6f..65a4cf9 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -183,7 +183,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName } // Create target reconciler instance - targetReconciler := discovery.NewMessageProcessor( + targetReconciler := discovery.NewTargetReconciler( r.Client, r.Scheme, targetSource, From c268808d67eb8df1d7328c0658b36bd369eda489 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 10:38:23 -0600 Subject: [PATCH 089/153] moved registry.go to discovery --- internal/controller/discovery/{registry => }/registry.go | 2 +- internal/controller/targetsource_controller.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) rename internal/controller/discovery/{registry => }/registry.go (97%) diff --git a/internal/controller/discovery/registry/registry.go b/internal/controller/discovery/registry.go similarity index 97% rename from internal/controller/discovery/registry/registry.go rename to internal/controller/discovery/registry.go index f2630e8..0afa2b2 100644 --- a/internal/controller/discovery/registry/registry.go +++ b/internal/controller/discovery/registry.go @@ -1,4 +1,4 @@ -package registry +package discovery import ( "fmt" diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 65a4cf9..3b62b6d 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -33,7 +33,6 @@ import ( "github.com/gnmic/operator/internal/controller/discovery/core" "github.com/gnmic/operator/internal/controller/discovery/loaders" "github.com/gnmic/operator/internal/controller/discovery/pipeline" - "github.com/gnmic/operator/internal/controller/discovery/registry" "github.com/go-logr/logr" ) @@ -66,7 +65,7 @@ type TargetSourceReconciler struct { BufferSize int ChunkSize int - DiscoveryRegistry *registry.Registry[types.NamespacedName, core.DiscoveryRegistryValue] + DiscoveryRegistry *discovery.Registry[types.NamespacedName, core.DiscoveryRegistryValue] } // +kubebuilder:rbac:groups=operator.gnmic.dev,resources=targetsources,verbs=get;list;watch;create;update;patch;delete From 02958966b77f80ee3fc1f0e447b98967e54e9c2a Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 10:39:32 -0600 Subject: [PATCH 090/153] moved supervisor to discovery --- .../controller/discovery/{pipeline => }/supervisor.go | 2 +- internal/controller/targetsource_controller.go | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) rename internal/controller/discovery/{pipeline => }/supervisor.go (99%) diff --git a/internal/controller/discovery/pipeline/supervisor.go b/internal/controller/discovery/supervisor.go similarity index 99% rename from internal/controller/discovery/pipeline/supervisor.go rename to internal/controller/discovery/supervisor.go index 042d305..56fa687 100644 --- a/internal/controller/discovery/pipeline/supervisor.go +++ b/internal/controller/discovery/supervisor.go @@ -1,4 +1,4 @@ -package pipeline +package discovery import ( "context" diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 3b62b6d..301e421 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -32,7 +32,6 @@ import ( "github.com/gnmic/operator/internal/controller/discovery" "github.com/gnmic/operator/internal/controller/discovery/core" "github.com/gnmic/operator/internal/controller/discovery/loaders" - "github.com/gnmic/operator/internal/controller/discovery/pipeline" "github.com/go-logr/logr" ) @@ -171,7 +170,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName loaderConfigured := targetSource.Spec.Provider != nil webhookActivated := targetSource.Spec.Webhook.Enabled != nil && *targetSource.Spec.Webhook.Enabled - supervisor := pipeline.NewSupervisor(context.Background()) + supervisor := discovery.NewSupervisor(context.Background()) targetChannel := make(chan []core.DiscoveryMessage, r.BufferSize) if err := r.DiscoveryRegistry.Register(key, core.DiscoveryRegistryValue{ @@ -190,9 +189,9 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName ) // Start target reconciler reconcilerReady := make(chan struct{}) - supervisor.StartSupervisedComponent(pipeline.ComponentSpec{ + supervisor.StartSupervisedComponent(discovery.ComponentSpec{ Name: "target-reconciler", - Policy: pipeline.RestartPolicy{ + Policy: discovery.RestartPolicy{ MaxRestarts: pipelineMaxRestarts, Backoff: pipelineBackoff, }, @@ -221,9 +220,9 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName return err } - supervisor.StartSupervisedComponent(pipeline.ComponentSpec{ + supervisor.StartSupervisedComponent(discovery.ComponentSpec{ Name: "loader", - Policy: pipeline.RestartPolicy{ + Policy: discovery.RestartPolicy{ MaxRestarts: pipelineMaxRestarts, Backoff: pipelineBackoff, }, From 4d32c40fb2e319fa2ff77a9c05f576ba6e0dba4d Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 10:40:26 -0600 Subject: [PATCH 091/153] moved factory.go to discovery/loaders.go --- .../controller/discovery/{loaders/factory.go => loaders.go} | 2 +- internal/controller/targetsource_controller.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) rename internal/controller/discovery/{loaders/factory.go => loaders.go} (97%) diff --git a/internal/controller/discovery/loaders/factory.go b/internal/controller/discovery/loaders.go similarity index 97% rename from internal/controller/discovery/loaders/factory.go rename to internal/controller/discovery/loaders.go index 45bf9c1..0d8ddd3 100644 --- a/internal/controller/discovery/loaders/factory.go +++ b/internal/controller/discovery/loaders.go @@ -1,4 +1,4 @@ -package loaders +package discovery import ( "fmt" diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 301e421..9ba2c94 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -31,7 +31,6 @@ import ( gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery" "github.com/gnmic/operator/internal/controller/discovery/core" - "github.com/gnmic/operator/internal/controller/discovery/loaders" "github.com/go-logr/logr" ) @@ -210,7 +209,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName // Create loader instance if loaderConfigured { - loader, err := loaders.NewLoader( + loader, err := discovery.NewLoader( key, targetSource.Spec, core.LoaderConfig{ChunkSize: r.ChunkSize}, From 7671c1a20aa7a48a26cf306c55ef0698c1ec448f Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 10:54:58 -0600 Subject: [PATCH 092/153] moved send.go to loaders package --- .../discovery/loaders/http/loader.go | 3 ++- .../discovery/{core => loaders}/send.go | 22 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) rename internal/controller/discovery/{core => loaders}/send.go (67%) diff --git a/internal/controller/discovery/loaders/http/loader.go b/internal/controller/discovery/loaders/http/loader.go index 09bb7d6..1e5fc37 100644 --- a/internal/controller/discovery/loaders/http/loader.go +++ b/internal/controller/discovery/loaders/http/loader.go @@ -10,6 +10,7 @@ import ( gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery/core" + "github.com/gnmic/operator/internal/controller/discovery/loaders" "github.com/google/uuid" ) @@ -66,7 +67,7 @@ func (l *Loader) Start( }, } - if err := core.SendSnapshot(ctx, out, targets, snapshotID, l.cfg.ChunkSize); err != nil { + if err := loaders.SendSnapshot(ctx, out, targets, snapshotID, l.cfg.ChunkSize); err != nil { return err } } diff --git a/internal/controller/discovery/core/send.go b/internal/controller/discovery/loaders/send.go similarity index 67% rename from internal/controller/discovery/core/send.go rename to internal/controller/discovery/loaders/send.go index f24b50c..1377432 100644 --- a/internal/controller/discovery/core/send.go +++ b/internal/controller/discovery/loaders/send.go @@ -1,12 +1,14 @@ -package core +package loaders import ( "context" "fmt" + + "github.com/gnmic/operator/internal/controller/discovery/core" ) // sendMessages sends discovery messages over a channel in a context-aware manner -func sendMessages(ctx context.Context, out chan<- []DiscoveryMessage, messages []DiscoveryMessage) error { +func sendMessages(ctx context.Context, out chan<- []core.DiscoveryMessage, messages []core.DiscoveryMessage) error { select { case <-ctx.Done(): return ctx.Err() @@ -30,14 +32,14 @@ func forEachChunk(total, chunkSize int, fn func(start, end int) error) error { } // createDiscoverySnapshots takes a list of discovered targets and returns chunked DiscoverySnapshots -func createDiscoverySnapshots(targets []DiscoveredTarget, snapshotID string, chunkSize int) []DiscoverySnapshot { - var snapshots []DiscoverySnapshot +func createDiscoverySnapshots(targets []core.DiscoveredTarget, snapshotID string, chunkSize int) []core.DiscoverySnapshot { + var snapshots []core.DiscoverySnapshot totalTargets := len(targets) totalChunks := (totalTargets + chunkSize - 1) / chunkSize _ = forEachChunk(totalTargets, chunkSize, func(i, end int) error { chunk := targets[i:end] - snapshots = append(snapshots, DiscoverySnapshot{ + snapshots = append(snapshots, core.DiscoverySnapshot{ Targets: chunk, SnapshotID: snapshotID, ChunkIndex: i / chunkSize, @@ -50,7 +52,7 @@ func createDiscoverySnapshots(targets []DiscoveredTarget, snapshotID string, chu } // SendSnapshot sends discovered targets as a snapshot over a channel in chunks -func SendSnapshot(ctx context.Context, out chan<- []DiscoveryMessage, targets []DiscoveredTarget, snapshotID string, chunkSize int) error { +func SendSnapshot(ctx context.Context, out chan<- []core.DiscoveryMessage, targets []core.DiscoveredTarget, snapshotID string, chunkSize int) error { if len(targets) == 0 { return fmt.Errorf("no targets in Snapshot") } @@ -58,7 +60,7 @@ func SendSnapshot(ctx context.Context, out chan<- []DiscoveryMessage, targets [] snapshots := createDiscoverySnapshots(targets, snapshotID, chunkSize) for _, snapshot := range snapshots { // Convert DiscoverySnapshot to DiscoveryMessage - messages := make([]DiscoveryMessage, 1) + messages := make([]core.DiscoveryMessage, 1) messages[0] = snapshot if err := sendMessages(ctx, out, messages); err != nil { @@ -69,8 +71,8 @@ func SendSnapshot(ctx context.Context, out chan<- []DiscoveryMessage, targets [] return nil } -func eventsToMessages(events []DiscoveryEvent) []DiscoveryMessage { - message := make([]DiscoveryMessage, len(events)) +func eventsToMessages(events []core.DiscoveryEvent) []core.DiscoveryMessage { + message := make([]core.DiscoveryMessage, len(events)) for i, event := range events { message[i] = event } @@ -78,7 +80,7 @@ func eventsToMessages(events []DiscoveryEvent) []DiscoveryMessage { } // SendEvents sends discovery messages over channel in a context-aware manner -func SendEvents(ctx context.Context, out chan<- []DiscoveryMessage, events []DiscoveryEvent, chunkSize int) error { +func SendEvents(ctx context.Context, out chan<- []core.DiscoveryMessage, events []core.DiscoveryEvent, chunkSize int) error { if len(events) == 0 { return fmt.Errorf("no events to process") } From 5f1e9cbe91d28e837ff7fbfae4029df45f27c001 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 10:55:59 -0600 Subject: [PATCH 093/153] eliminated message.go --- internal/controller/discovery/core/message.go | 4 ---- internal/controller/discovery/core/message_interface.go | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 internal/controller/discovery/core/message.go diff --git a/internal/controller/discovery/core/message.go b/internal/controller/discovery/core/message.go deleted file mode 100644 index af4f6c1..0000000 --- a/internal/controller/discovery/core/message.go +++ /dev/null @@ -1,4 +0,0 @@ -package core - -func (DiscoveryEvent) isDiscoveryMessage() {} -func (DiscoverySnapshot) isDiscoveryMessage() {} diff --git a/internal/controller/discovery/core/message_interface.go b/internal/controller/discovery/core/message_interface.go index 07b819e..0836bc6 100644 --- a/internal/controller/discovery/core/message_interface.go +++ b/internal/controller/discovery/core/message_interface.go @@ -3,3 +3,6 @@ package core type DiscoveryMessage interface { isDiscoveryMessage() } + +func (DiscoveryEvent) isDiscoveryMessage() {} +func (DiscoverySnapshot) isDiscoveryMessage() {} From 6d6753731ca36cdafa5a251e164ed1b70eafd3dc Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 10:56:39 -0600 Subject: [PATCH 094/153] moved const.go to discovery.go --- internal/controller/discovery/client.go | 3 +-- internal/controller/discovery/{core => }/const.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) rename internal/controller/discovery/{core => }/const.go (81%) diff --git a/internal/controller/discovery/client.go b/internal/controller/discovery/client.go index 2deb477..cb02161 100644 --- a/internal/controller/discovery/client.go +++ b/internal/controller/discovery/client.go @@ -6,7 +6,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" - "github.com/gnmic/operator/internal/controller/discovery/core" ) func fetchExistingTargets( @@ -22,7 +21,7 @@ func fetchExistingTargets( &targetList, client.InNamespace(ts.Namespace), client.MatchingLabels{ - core.LabelTargetSourceName: ts.Name, + LabelTargetSourceName: ts.Name, }, ) if err != nil { diff --git a/internal/controller/discovery/core/const.go b/internal/controller/discovery/const.go similarity index 81% rename from internal/controller/discovery/core/const.go rename to internal/controller/discovery/const.go index 82a5962..ac7a57f 100644 --- a/internal/controller/discovery/core/const.go +++ b/internal/controller/discovery/const.go @@ -1,4 +1,4 @@ -package core +package discovery const ( // Labels From 391463097c6caab4b89c72de9789efe8b346e8bf Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 11:28:29 -0600 Subject: [PATCH 095/153] renamed core package within targetsource controller --- internal/controller/targetsource_controller.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 9ba2c94..e52b02b 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -30,7 +30,7 @@ import ( gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery" - "github.com/gnmic/operator/internal/controller/discovery/core" + discoveryTypes "github.com/gnmic/operator/internal/controller/discovery/core" "github.com/go-logr/logr" ) @@ -63,7 +63,7 @@ type TargetSourceReconciler struct { BufferSize int ChunkSize int - DiscoveryRegistry *discovery.Registry[types.NamespacedName, core.DiscoveryRegistryValue] + DiscoveryRegistry *discovery.Registry[types.NamespacedName, discoveryTypes.DiscoveryRegistryValue] } // +kubebuilder:rbac:groups=operator.gnmic.dev,resources=targetsources,verbs=get;list;watch;create;update;patch;delete @@ -171,8 +171,8 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName supervisor := discovery.NewSupervisor(context.Background()) - targetChannel := make(chan []core.DiscoveryMessage, r.BufferSize) - if err := r.DiscoveryRegistry.Register(key, core.DiscoveryRegistryValue{ + targetChannel := make(chan []discoveryTypes.DiscoveryMessage, r.BufferSize) + if err := r.DiscoveryRegistry.Register(key, discoveryTypes.DiscoveryRegistryValue{ Channel: targetChannel, WebhookEnabled: webhookActivated, }); err != nil { @@ -212,7 +212,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName loader, err := discovery.NewLoader( key, targetSource.Spec, - core.LoaderConfig{ChunkSize: r.ChunkSize}, + discoveryTypes.LoaderConfig{ChunkSize: r.ChunkSize}, ) if err != nil { supervisor.Stop() From 46a201fc1d9f0dc9cc73825477f789fc3cb3e860 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 11:34:42 -0600 Subject: [PATCH 096/153] changed events to delete / apply --- internal/controller/discovery/core/types.go | 6 ++---- internal/controller/discovery/target_reconciler.go | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 68c9c7e..2c37fc7 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -15,10 +15,8 @@ type EventAction int const ( // EventDelete indicates that a target should be removed EventDelete EventAction = iota - // EventCreate indicates that a target should be created - EventCreate - // EventUpdate indicates that a target should be updated - EventUpdate + // EventApply indicates that a target should be applied (created or updated) + EventApply ) // DiscoveredTarget represents a target discovered from an external source diff --git a/internal/controller/discovery/target_reconciler.go b/internal/controller/discovery/target_reconciler.go index 4f3711c..86470c6 100644 --- a/internal/controller/discovery/target_reconciler.go +++ b/internal/controller/discovery/target_reconciler.go @@ -258,12 +258,10 @@ func (r *TargetReconciler) processEvent(ctx context.Context, event core.Discover func (r *TargetReconciler) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { switch event.Event { - case core.EventCreate: - logger.Info("Would create target", "name", event.Target.Name, "address", event.Target.Address, "labels", event.Target.Labels) - case core.EventUpdate: - logger.Info("Would update target", "name", event.Target.Name, "address", event.Target.Address, "labels", event.Target.Labels) case core.EventDelete: logger.Info("Would delete target", "name", event.Target.Name) + case core.EventApply: + logger.Info("Would apply target", "name", event.Target.Name, "address", event.Target.Address, "labels", event.Target.Labels) } return nil } From 7b17f7e77644abff70f5796704e36b10bf03da15 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 11:37:39 -0600 Subject: [PATCH 097/153] moved send.go into separate utils for loaders --- internal/controller/discovery/loaders/http/loader.go | 4 ++-- internal/controller/discovery/loaders/{ => utils}/send.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename internal/controller/discovery/loaders/{ => utils}/send.go (99%) diff --git a/internal/controller/discovery/loaders/http/loader.go b/internal/controller/discovery/loaders/http/loader.go index 1e5fc37..d7d5961 100644 --- a/internal/controller/discovery/loaders/http/loader.go +++ b/internal/controller/discovery/loaders/http/loader.go @@ -10,7 +10,7 @@ import ( gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery/core" - "github.com/gnmic/operator/internal/controller/discovery/loaders" + loaderUtils "github.com/gnmic/operator/internal/controller/discovery/loaders/utils" "github.com/google/uuid" ) @@ -67,7 +67,7 @@ func (l *Loader) Start( }, } - if err := loaders.SendSnapshot(ctx, out, targets, snapshotID, l.cfg.ChunkSize); err != nil { + if err := loaderUtils.SendSnapshot(ctx, out, targets, snapshotID, l.cfg.ChunkSize); err != nil { return err } } diff --git a/internal/controller/discovery/loaders/send.go b/internal/controller/discovery/loaders/utils/send.go similarity index 99% rename from internal/controller/discovery/loaders/send.go rename to internal/controller/discovery/loaders/utils/send.go index 1377432..3cfba8d 100644 --- a/internal/controller/discovery/loaders/send.go +++ b/internal/controller/discovery/loaders/utils/send.go @@ -1,4 +1,4 @@ -package loaders +package utils import ( "context" From 4540163d4137a27a291846a5960ecf09844bf5f8 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 11:45:43 -0600 Subject: [PATCH 098/153] replaced legacy registry package --- cmd/main.go | 4 ++-- internal/apiserver/apiserver.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 4cf6e94..aaf398a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -42,7 +42,7 @@ import ( "github.com/gnmic/operator/internal/apiserver" "github.com/gnmic/operator/internal/controller" "github.com/gnmic/operator/internal/controller/discovery/core" - "github.com/gnmic/operator/internal/controller/discovery/registry" + "github.com/gnmic/operator/internal/controller/discovery" webhookv1alpha1 "github.com/gnmic/operator/internal/webhook/v1alpha1" //+kubebuilder:scaffold:imports ) @@ -86,7 +86,7 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - discoveryRegistry := registry.NewRegistry[types.NamespacedName, core.DiscoveryRegistryValue]() + discoveryRegistry := discovery.NewRegistry[types.NamespacedName, core.DiscoveryRegistryValue]() mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, diff --git a/internal/apiserver/apiserver.go b/internal/apiserver/apiserver.go index a7ca16a..705b277 100644 --- a/internal/apiserver/apiserver.go +++ b/internal/apiserver/apiserver.go @@ -6,7 +6,7 @@ import ( "github.com/gnmic/operator/internal/controller" "github.com/gnmic/operator/internal/controller/discovery/core" - "github.com/gnmic/operator/internal/controller/discovery/registry" + "github.com/gnmic/operator/internal/controller/discovery" "k8s.io/apimachinery/pkg/types" ) @@ -14,7 +14,7 @@ type APIServer struct { Server *http.Server clusterReconciler *controller.ClusterReconciler - DiscoveryRegistry *registry.Registry[types.NamespacedName, core.DiscoveryRegistryValue] + DiscoveryRegistry *discovery.Registry[types.NamespacedName, core.DiscoveryRegistryValue] } func New(addr string, clusterReconciler *controller.ClusterReconciler) *APIServer { From 183abe226c6b3a7f39967a5c299ebc073b323094 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 14:30:09 -0600 Subject: [PATCH 099/153] moved client/mapper functions out of target_reconciler.go --- internal/controller/discovery/client.go | 51 +++++++++++++++-- internal/controller/discovery/mapper.go | 5 ++ .../controller/discovery/target_reconciler.go | 56 ++----------------- 3 files changed, 54 insertions(+), 58 deletions(-) diff --git a/internal/controller/discovery/client.go b/internal/controller/discovery/client.go index cb02161..a9d790f 100644 --- a/internal/controller/discovery/client.go +++ b/internal/controller/discovery/client.go @@ -3,17 +3,17 @@ package discovery import ( "context" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" ) -func fetchExistingTargets( - ctx context.Context, - c client.Client, - ts *gnmicv1alpha1.TargetSource, -) ([]gnmicv1alpha1.Target, error) { - +func fetchExistingTargets(ctx context.Context, c client.Client, ts *gnmicv1alpha1.TargetSource) ([]gnmicv1alpha1.Target, error) { var targetList gnmicv1alpha1.TargetList err := c.List( @@ -30,3 +30,42 @@ func fetchExistingTargets( return targetList.Items, nil } + +func applyTarget(ctx context.Context, c client.Client, s *runtime.Scheme, desired *gnmicv1alpha1.Target, ts *gnmicv1alpha1.TargetSource) error { + existing := &gnmicv1alpha1.Target{ + ObjectMeta: metav1.ObjectMeta{ + Name: desired.Name, + Namespace: desired.Namespace, + }, + } + + _, err := controllerutil.CreateOrUpdate(ctx, c, existing, func() error { + existing.Spec = desired.Spec + existing.Labels = desired.Labels + + return controllerutil.SetControllerReference(ts, existing, s) + }) + + return err +} + +func deleteTarget(ctx context.Context, c client.Client, name string, namespace string) error { + existing := &gnmicv1alpha1.Target{} + + err := c.Get(ctx, types.NamespacedName{ + Name: name, + Namespace: namespace, + }, existing) + if apierrors.IsNotFound(err) { + return nil + } else if err != nil { + return err + } + + err = c.Delete(ctx, existing) + if apierrors.IsNotFound(err) { + return nil + } + + return err +} diff --git a/internal/controller/discovery/mapper.go b/internal/controller/discovery/mapper.go index 89a081f..f34fe36 100644 --- a/internal/controller/discovery/mapper.go +++ b/internal/controller/discovery/mapper.go @@ -73,3 +73,8 @@ func generateEvents(existing []gnmicv1alpha1.Target, discovered []core.Discovere return events } + +func normalizeTarget(t core.DiscoveredTarget, tsName string) core.DiscoveredTarget { + t.Name = tsName + "-" + t.Name + return t +} diff --git a/internal/controller/discovery/target_reconciler.go b/internal/controller/discovery/target_reconciler.go index a924132..e8e24b5 100644 --- a/internal/controller/discovery/target_reconciler.go +++ b/internal/controller/discovery/target_reconciler.go @@ -4,12 +4,8 @@ import ( "context" "fmt" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" @@ -113,7 +109,7 @@ func (r *TargetReconciler) processMessage(ctx context.Context, message core.Disc ) for i := range msg.Targets { - msg.Targets[i] = r.normalizeTarget(msg.Targets[i]) + msg.Targets[i] = normalizeTarget(msg.Targets[i], r.targetSource.Namespace) } return r.processSnapshot(ctx, msg, logger) @@ -125,7 +121,7 @@ func (r *TargetReconciler) processMessage(ctx context.Context, message core.Disc "target", msg.Target.Name, ) - msg.Target = r.normalizeTarget(msg.Target) + msg.Target = normalizeTarget(msg.Target, r.targetSource.Namespace) return r.processEvent(ctx, msg, logger) default: @@ -299,7 +295,7 @@ func (r *TargetReconciler) applySnapshot(ctx context.Context, snapshot *snapshot func (r *TargetReconciler) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { switch event.Event { case core.EventDelete: - if err := r.deleteTarget(ctx, event.Target.Name); err != nil { + if err := deleteTarget(ctx, r.client, event.Target.Name, r.targetSource.Namespace); err != nil { logger.Error(err, "error deleting target", "targetName", event.Target.Name, ) @@ -311,7 +307,7 @@ func (r *TargetReconciler) applyEvent(ctx context.Context, event core.DiscoveryE case core.EventApply: target := generateTargetResource(event.Target, r.targetSource) - if err := r.applyTarget(ctx, target); err != nil { + if err := applyTarget(ctx, r.client, r.scheme, target, r.targetSource); err != nil { logger.Error(err, "error applying target", "targetName", event.Target.Name, ) @@ -324,47 +320,3 @@ func (r *TargetReconciler) applyEvent(ctx context.Context, event core.DiscoveryE return nil } - -func (r *TargetReconciler) applyTarget(ctx context.Context, desired *gnmicv1alpha1.Target) error { - existing := &gnmicv1alpha1.Target{ - ObjectMeta: metav1.ObjectMeta{ - Name: desired.Name, - Namespace: desired.Namespace, - }, - } - - _, err := controllerutil.CreateOrUpdate(ctx, r.client, existing, func() error { - existing.Spec = desired.Spec - existing.Labels = desired.Labels - - return controllerutil.SetControllerReference(r.targetSource, existing, r.scheme) - }) - - return err -} - -func (r *TargetReconciler) deleteTarget(ctx context.Context, name string) error { - existing := &gnmicv1alpha1.Target{} - - err := r.client.Get(ctx, types.NamespacedName{ - Name: name, - Namespace: r.targetSource.Namespace, - }, existing) - if apierrors.IsNotFound(err) { - return nil - } else if err != nil { - return err - } - - err = r.client.Delete(ctx, existing) - if apierrors.IsNotFound(err) { - return nil - } - - return err -} - -func (r *TargetReconciler) normalizeTarget(t core.DiscoveredTarget) core.DiscoveredTarget { - t.Name = r.targetSource.Name + "-" + t.Name - return t -} From 0811afd19a5da16c71962ab62be8b047a71f7f0d Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 29 Apr 2026 14:54:09 -0600 Subject: [PATCH 100/153] renamed functions --- .../controller/discovery/target_reconciler.go | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/controller/discovery/target_reconciler.go b/internal/controller/discovery/target_reconciler.go index e8e24b5..34158c7 100644 --- a/internal/controller/discovery/target_reconciler.go +++ b/internal/controller/discovery/target_reconciler.go @@ -112,7 +112,7 @@ func (r *TargetReconciler) processMessage(ctx context.Context, message core.Disc msg.Targets[i] = normalizeTarget(msg.Targets[i], r.targetSource.Namespace) } - return r.processSnapshot(ctx, msg, logger) + return r.handleSnapshot(ctx, msg, logger) case core.DiscoveryEvent: // Process individual event-driven update @@ -122,15 +122,15 @@ func (r *TargetReconciler) processMessage(ctx context.Context, message core.Disc ) msg.Target = normalizeTarget(msg.Target, r.targetSource.Namespace) - return r.processEvent(ctx, msg, logger) + return r.handleEvent(ctx, msg, logger) default: return fmt.Errorf("unknonw discovery message type %T", msg) } } -// processSnapshot takes a complete snapshot of discovered targets and reconciles Target CRs accordingly -func (r *TargetReconciler) processSnapshot(ctx context.Context, chunk core.DiscoverySnapshot, logger logr.Logger) error { +// handleSnapshot takes a complete snapshot of discovered targets and reconciles Target CRs accordingly +func (r *TargetReconciler) handleSnapshot(ctx context.Context, chunk core.DiscoverySnapshot, logger logr.Logger) error { if r.activeSnapshot == nil { r.startNewSnapshot(chunk, logger) return nil @@ -141,7 +141,7 @@ func (r *TargetReconciler) processSnapshot(ctx context.Context, chunk core.Disco if snapshot.snapshotID != chunk.SnapshotID { // If current snapshot is complete apply it first if snapshot.complete { - if err := r.applySnapshot(ctx, snapshot, logger); err != nil { + if err := r.reconcileSnapshot(ctx, snapshot, logger); err != nil { return err } } else { @@ -200,7 +200,7 @@ func (r *TargetReconciler) collectSnapshot(chunk core.DiscoverySnapshot, logger return nil } -func (r *TargetReconciler) processEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { +func (r *TargetReconciler) handleEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { // If snapshot collecting is active defer events if r.activeSnapshot != nil { r.deferredEvents = append(r.deferredEvents, event) @@ -208,10 +208,10 @@ func (r *TargetReconciler) processEvent(ctx context.Context, event core.Discover } // Apply events - return r.applyEvent(ctx, event, logger) + return r.reconcileEvent(ctx, event, logger) } -func (r *TargetReconciler) applySnapshot(ctx context.Context, snapshot *snapshotBuffer, logger logr.Logger) error { +func (r *TargetReconciler) reconcileSnapshot(ctx context.Context, snapshot *snapshotBuffer, logger logr.Logger) error { select { case <-ctx.Done(): r.activeSnapshot = nil @@ -272,7 +272,7 @@ func (r *TargetReconciler) applySnapshot(ctx context.Context, snapshot *snapshot ) for _, e := range events { - r.processEvent(ctx, e, logger) + r.handleEvent(ctx, e, logger) } // Replay deferred events @@ -282,7 +282,7 @@ func (r *TargetReconciler) applySnapshot(ctx context.Context, snapshot *snapshot return nil default: } - if err := r.applyEvent(ctx, event, logger); err != nil { + if err := r.reconcileEvent(ctx, event, logger); err != nil { return err } } @@ -292,7 +292,7 @@ func (r *TargetReconciler) applySnapshot(ctx context.Context, snapshot *snapshot return nil } -func (r *TargetReconciler) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { +func (r *TargetReconciler) reconcileEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { switch event.Event { case core.EventDelete: if err := deleteTarget(ctx, r.client, event.Target.Name, r.targetSource.Namespace); err != nil { From c728fa2f340066c1f261769ab379ba223e12d62c Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 30 Apr 2026 08:21:11 +0000 Subject: [PATCH 101/153] add supervisor restart policy to targetsource spec configuration --- api/v1alpha1/targetsource_types.go | 13 ++++- api/v1alpha1/zz_generated.deepcopy.go | 30 ++++++++++++ .../operator.gnmic.dev_targetsources.yaml | 7 +++ internal/controller/discovery/defaults.go | 12 +++++ .../controller/targetsource_controller.go | 49 ++++++++++++------- 5 files changed, 92 insertions(+), 19 deletions(-) create mode 100644 internal/controller/discovery/defaults.go diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index a936e66..7c8f74c 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -26,9 +26,12 @@ type TargetSourceSpec struct { Provider *ProviderSpec `json:"provider"` // +kubebuilder:validation:Optional Webhook WebhookSpec `json:"webhook,omitempty"` - // + // +kubebuilder:validation:Optional TargetLabels map[string]string `json:"targetLabels,omitempty"` + // +kubebuilder:validation:Optional + RestartPolicy *RestartPolicySpec `json:"restartPolicy,omitempty"` + // +kubebuilder:validation:MinLength=1 TargetProfile string `json:"targetProfile"` } @@ -54,6 +57,14 @@ type ConsulConfig struct { URL string `json:"url,omitempty"` } +type RestartPolicySpec struct { + // +kubebuilder:validation:Optional + MaxRestarts *int `json:"maxRestarts,omitempty"` + + // +kubebuilder:validation:Optional + BackoffSeconds *int `json:"backoffSeconds,omitempty"` +} + // TargetSourceStatus defines the observed state of TargetSource type TargetSourceStatus struct { Status string `json:"status"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 608d47e..df08573 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -843,6 +843,31 @@ func (in *ProviderSpec) DeepCopy() *ProviderSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RestartPolicySpec) DeepCopyInto(out *RestartPolicySpec) { + *out = *in + if in.MaxRestarts != nil { + in, out := &in.MaxRestarts, &out.MaxRestarts + *out = new(int) + **out = **in + } + if in.BackoffSeconds != nil { + in, out := &in.BackoffSeconds, &out.BackoffSeconds + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestartPolicySpec. +func (in *RestartPolicySpec) DeepCopy() *RestartPolicySpec { + if in == nil { + return nil + } + out := new(RestartPolicySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceConfig) DeepCopyInto(out *ServiceConfig) { *out = *in @@ -1300,6 +1325,11 @@ func (in *TargetSourceSpec) DeepCopyInto(out *TargetSourceSpec) { (*out)[key] = val } } + if in.RestartPolicy != nil { + in, out := &in.RestartPolicy, &out.RestartPolicy + *out = new(RestartPolicySpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSourceSpec. diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index b385d8e..6464ea2 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -60,6 +60,13 @@ spec: - message: exactly one of the fields in [http consul] must be set rule: '[has(self.http),has(self.consul)].filter(x,x==true).size() == 1' + restartPolicy: + properties: + backoffSeconds: + type: integer + maxRestarts: + type: integer + type: object targetLabels: additionalProperties: type: string diff --git a/internal/controller/discovery/defaults.go b/internal/controller/discovery/defaults.go new file mode 100644 index 0000000..dc6f046 --- /dev/null +++ b/internal/controller/discovery/defaults.go @@ -0,0 +1,12 @@ +package discovery + +import "time" + +// DefaultRestartPolicy defines the default restart behavior +// for the discovery components +func DefaultRestartPolicy() RestartPolicy { + return RestartPolicy{ + MaxRestarts: 5, + Backoff: 3 * time.Second, + } +} diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 06b4fac..fddebda 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -35,11 +35,6 @@ import ( "github.com/go-logr/logr" ) -const ( - pipelineMaxRestarts = 5 - pipelineBackoff = 3 * time.Second -) - // pipelineHandle represents a controller-owned handle to a running pipeline // The controller never manipulates internals; it only invokes cancel() type pipelineHandle struct { @@ -158,6 +153,29 @@ func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSour return nil } +// resolveRestartPolicy merges an optional spec override with the controller’s default restart policy +func resolveRestartPolicy( + override *gnmicv1alpha1.RestartPolicySpec, +) discovery.RestartPolicy { + defaults := discovery.DefaultRestartPolicy() + + if override == nil { + return defaults + } + + resolved := defaults + + if override.MaxRestarts != nil { + resolved.MaxRestarts = *override.MaxRestarts + } + + if override.BackoffSeconds != nil { + resolved.Backoff = time.Duration(*override.BackoffSeconds) * time.Second + } + + return resolved +} + // startDiscoveryPipeline creates and starts a discovery pipeline for a TargetSource // // Pipeline semantics: @@ -168,6 +186,7 @@ func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSour func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource, logger logr.Logger) error { loaderConfigured := targetSource.Spec.Provider != nil webhookActivated := targetSource.Spec.Webhook.Enabled != nil && *targetSource.Spec.Webhook.Enabled + restartPolicy := resolveRestartPolicy(targetSource.Spec.RestartPolicy) supervisor := discovery.NewSupervisor(context.Background()) @@ -187,22 +206,19 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName targetChannel, ) // Start target reconciler - reconcilerReady := make(chan struct{}) + targetReconcilerReady := make(chan struct{}) supervisor.StartSupervisedComponent(discovery.ComponentSpec{ - Name: "target-reconciler", - Policy: discovery.RestartPolicy{ - MaxRestarts: pipelineMaxRestarts, - Backoff: pipelineBackoff, - }, + Name: "target-reconciler", + Policy: restartPolicy, EscalatesOnFailure: true, Run: func(ctx context.Context) error { - close(reconcilerReady) // Signals that reconciler started successfully + close(targetReconcilerReady) // Signals that reconciler started successfully return targetReconciler.Run(ctx) }, }) // Wait for reconciler to be ready before starting loader select { - case <-reconcilerReady: + case <-targetReconcilerReady: case <-supervisor.Done(): return nil } @@ -220,11 +236,8 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName } supervisor.StartSupervisedComponent(discovery.ComponentSpec{ - Name: "loader", - Policy: discovery.RestartPolicy{ - MaxRestarts: pipelineMaxRestarts, - Backoff: pipelineBackoff, - }, + Name: "loader", + Policy: restartPolicy, EscalatesOnFailure: !webhookActivated, Run: func(ctx context.Context) error { return loader.Start(ctx, key, targetSource.Spec, targetChannel) From 589bc9f8cf0643af82f40c4e126ec2e72fc7e67e Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 30 Apr 2026 08:31:37 +0000 Subject: [PATCH 102/153] add targetsource example for lab --- lab/dev/resources/targetsources/cts1.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 lab/dev/resources/targetsources/cts1.yml diff --git a/lab/dev/resources/targetsources/cts1.yml b/lab/dev/resources/targetsources/cts1.yml new file mode 100644 index 0000000..682930c --- /dev/null +++ b/lab/dev/resources/targetsources/cts1.yml @@ -0,0 +1,18 @@ +apiVersion: operator.gnmic.dev/v1alpha1 +kind: TargetSource +metadata: + name: http-discovery +spec: + provider: + http: + url: http://srbsci-121:8081/api/dcim/devices/?export=test + webhook: + enabled: true + targetLabels: + source: inventory + site: siteA + tags: "inventory,siteA,http-discovery" + restartPolicy: + maxRestarts: 2 + backoffSeconds: 4 + targetProfile: eos \ No newline at end of file From a5dde06e8df6dafe7a72f5650e35584ef22b2662 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 30 Apr 2026 08:37:04 +0000 Subject: [PATCH 103/153] remove targetsource example to not add unnecassary logging to main --- lab/dev/resources/targetsources/cts1.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 lab/dev/resources/targetsources/cts1.yml diff --git a/lab/dev/resources/targetsources/cts1.yml b/lab/dev/resources/targetsources/cts1.yml deleted file mode 100644 index 682930c..0000000 --- a/lab/dev/resources/targetsources/cts1.yml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: operator.gnmic.dev/v1alpha1 -kind: TargetSource -metadata: - name: http-discovery -spec: - provider: - http: - url: http://srbsci-121:8081/api/dcim/devices/?export=test - webhook: - enabled: true - targetLabels: - source: inventory - site: siteA - tags: "inventory,siteA,http-discovery" - restartPolicy: - maxRestarts: 2 - backoffSeconds: 4 - targetProfile: eos \ No newline at end of file From 4be9c27a8a547ded1d79f9d6a542da2ad148fe2b Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 30 Apr 2026 08:42:06 +0000 Subject: [PATCH 104/153] update gitignore to not push targetsources in order to prevent logging in main branch --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 29d31af..7515fa3 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,9 @@ notes/ docs/public docs/resources/_gen/ docs/.hugo_build.lock -test/integration/clab-* \ No newline at end of file +test/integration/clab-* + +# Only for development and testing purposes +# To be removed after development of targetsource +# ignored in order to not add unnecassary logging messages +lab/dev/resources/targetsources \ No newline at end of file From 7337541e70e7bbf0867eb2a1e66a7c6ffacc3799 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 30 Apr 2026 09:56:06 +0000 Subject: [PATCH 105/153] add component info to logging --- internal/controller/discovery/target_reconciler.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/controller/discovery/target_reconciler.go b/internal/controller/discovery/target_reconciler.go index 86470c6..3a9f327 100644 --- a/internal/controller/discovery/target_reconciler.go +++ b/internal/controller/discovery/target_reconciler.go @@ -50,6 +50,7 @@ func (r *TargetReconciler) Run(ctx context.Context) error { logger := log.FromContext(r.ctx). WithValues( + "component", "target reconciler", "name", r.targetSource.Name, "namespace", r.targetSource.Namespace, ) From 41d54987415e9dc7c31096b12eff8e9022680405 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 30 Apr 2026 10:02:20 +0000 Subject: [PATCH 106/153] make snapshot id a bit smaller --- internal/controller/discovery/loaders/http/loader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/discovery/loaders/http/loader.go b/internal/controller/discovery/loaders/http/loader.go index bc87855..84cb70b 100644 --- a/internal/controller/discovery/loaders/http/loader.go +++ b/internal/controller/discovery/loaders/http/loader.go @@ -77,7 +77,7 @@ func (l *Loader) Start( return } - snapshotID := fmt.Sprintf("snapshot-%s-%s-%s", targetsourceNN.Namespace, targetsourceNN.Name, uuid.NewString()) + snapshotID := fmt.Sprintf("%s-%s-%s", targetsourceNN.Namespace, targetsourceNN.Name, uuid.NewString()) if err := loaderUtils.SendSnapshot(ctx, out, targets, snapshotID, l.cfg.ChunkSize); err != nil { logger.Error(err, "failed to send discovery snapshot") return From 3ec3203efa7a6e484902ba17dd4eed9085c52277 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 30 Apr 2026 12:21:19 +0000 Subject: [PATCH 107/153] if context is canceled return with ctx.Err() not a clean exit --- internal/controller/discovery/target_reconciler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/discovery/target_reconciler.go b/internal/controller/discovery/target_reconciler.go index 3a9f327..2f623c3 100644 --- a/internal/controller/discovery/target_reconciler.go +++ b/internal/controller/discovery/target_reconciler.go @@ -73,7 +73,7 @@ func (r *TargetReconciler) Run(ctx context.Context) error { for len(r.queue) > 0 { if ctx.Err() != nil { - return nil // why return nil? + return ctx.Err() } msg := r.queue[0] From 0eaffdcfc63c32bd6d63e46f3081a12015bd76e4 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 30 Apr 2026 12:34:18 +0000 Subject: [PATCH 108/153] applied kubebuilder best-practise logging --- .../discovery/loaders/http/loader.go | 6 +- internal/controller/discovery/supervisor.go | 22 ++++--- .../controller/discovery/target_reconciler.go | 59 ++++++++++++++----- .../controller/targetsource_controller.go | 31 ++++++++-- 4 files changed, 89 insertions(+), 29 deletions(-) diff --git a/internal/controller/discovery/loaders/http/loader.go b/internal/controller/discovery/loaders/http/loader.go index d7d5961..67c61e1 100644 --- a/internal/controller/discovery/loaders/http/loader.go +++ b/internal/controller/discovery/loaders/http/loader.go @@ -39,7 +39,11 @@ func (l *Loader) Start( "targetsource", targetsourceNN, ) - logger.Info("HTTP loader started") + logger.Info( + "HTTP loader started", + "targetsource", targetsourceNN.Name, + "namespace", targetsourceNN.Namespace, + ) // Only for debugging: emit a static snapshot every 30 seconds ticker := time.NewTicker(30 * time.Second) diff --git a/internal/controller/discovery/supervisor.go b/internal/controller/discovery/supervisor.go index 56fa687..22ec227 100644 --- a/internal/controller/discovery/supervisor.go +++ b/internal/controller/discovery/supervisor.go @@ -78,7 +78,10 @@ func (s *Supervisor) StartSupervisedComponent(component ComponentSpec) { failures := 0 for { - logger.Info("starting component") + logger.Info( + "Starting supervised component", + "component", component.Name, + ) err := component.Run(s.ctx) if s.ctx.Err() != nil { @@ -87,21 +90,26 @@ func (s *Supervisor) StartSupervisedComponent(component ComponentSpec) { } failures++ - logger.Error(err, - "component failed to run", + logger.Error( + err, + "Supervised component failed", + "component", component.Name, "attempt", failures, - "max", component.Policy.MaxRestarts, + "maxRestarts", component.Policy.MaxRestarts, ) if failures >= component.Policy.MaxRestarts { if component.EscalatesOnFailure { - logger.Error(err, - "component permanently failed; shutting down pipeline", + logger.Error( + err, + "Supervised component permanently failed; stopped discovery pipeline", + "component", component.Name, ) s.Stop() } else { logger.Info( - "optional component permanently failed; continuing without it", + "Optional component permanently failed; continuing without it", + "component", component.Name, ) } return diff --git a/internal/controller/discovery/target_reconciler.go b/internal/controller/discovery/target_reconciler.go index 2f623c3..67d9611 100644 --- a/internal/controller/discovery/target_reconciler.go +++ b/internal/controller/discovery/target_reconciler.go @@ -54,20 +54,30 @@ func (r *TargetReconciler) Run(ctx context.Context) error { "name", r.targetSource.Name, "namespace", r.targetSource.Namespace, ) - logger.Info("target reconciler started") + logger.Info( + "Target reconciler started", + "targetsource", r.targetSource.Name, + "namespace", r.targetSource.Namespace, + ) for r.ctx.Err() == nil { select { case batch, ok := <-r.in: if !ok { // Channel closed, pipeline is shutting down - logger.Info("input channel closed, stopping target reconciler") + logger.Info( + "Input channel closed; stopping target reconciler", + "targetsource", r.targetSource.Name, + ) return nil } r.queue = append(r.queue, batch...) case <-ctx.Done(): - logger.Info("context canceled, stopping target reconciler") + logger.Info( + "Context was canceled; stopping target reconciler", + "targetsource", r.targetSource.Name, + ) return nil } @@ -103,23 +113,24 @@ func (r *TargetReconciler) processMessage(ctx context.Context, message core.Disc case core.DiscoverySnapshot: // Collect snapshot chunks logger.Info( - "received snapshot chunk", + "Received discovery snapshot chunk", "snapshotID", msg.SnapshotID, - "index", msg.ChunkIndex, - "targetCount", len(msg.Targets), + "chunkIndex", msg.ChunkIndex, + "targets", len(msg.Targets), ) return r.processSnapshot(ctx, msg, logger) case core.DiscoveryEvent: // Process individual event-driven update logger.Info( - "received discovery event", + "Received discovery event", + "event", msg.Event, "target", msg.Target.Name, ) return r.processEvent(ctx, msg, logger) default: - return fmt.Errorf("unknonw discovery message type %T", msg) + return fmt.Errorf("Unknown discovery message type %T", msg) } } @@ -142,7 +153,7 @@ func (r *TargetReconciler) processSnapshot(ctx context.Context, chunk core.Disco // If a new snapshot is started before the old one completed // the old one can be discarded logger.Info( - "discarding incomplete snapshot", + "Discarded incomplete discovery snapshot", "snapshotID", snapshot.snapshotID, ) } @@ -172,7 +183,11 @@ func (r *TargetReconciler) collectSnapshot(chunk core.DiscoverySnapshot, logger snapshot := r.activeSnapshot if chunk.TotalChunks != snapshot.totalChunks { - logger.Error(nil, "snapshot totalChunks mismatch", "snapshotID", snapshot.snapshotID) + logger.Error( + nil, + "Snapshot totalChunks mismatch", + "snapshotID", snapshot.snapshotID, + ) } if chunk.ChunkIndex < 0 || chunk.ChunkIndex >= snapshot.totalChunks { logger.Error(nil, "snapshot chunk index out of range", "index", chunk.ChunkIndex) @@ -180,7 +195,11 @@ func (r *TargetReconciler) collectSnapshot(chunk core.DiscoverySnapshot, logger return nil } if _, exists := snapshot.received[chunk.ChunkIndex]; exists { - logger.Error(nil, "duplicate snapshot chunk", "index", chunk.ChunkIndex) + logger.Error( + nil, + "Duplicate snapshot chunk received", + "chunkIndex", chunk.ChunkIndex, + ) r.activeSnapshot = nil return nil } @@ -221,9 +240,9 @@ func (r *TargetReconciler) applySnapshot(ctx context.Context, snapshot *snapshot } logger.Info( - "applying snapshot", + "Applying discovery snapshot", "snapshotID", snapshot.snapshotID, - "targetCount", len(allTargets), + "targets", len(allTargets), ) // apply all targets @@ -260,9 +279,19 @@ func (r *TargetReconciler) processEvent(ctx context.Context, event core.Discover func (r *TargetReconciler) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { switch event.Event { case core.EventDelete: - logger.Info("Would delete target", "name", event.Target.Name) + logger.Info( + "Deleting Target", + "target", event.Target.Name, + "targetsource", r.targetSource.Name, + ) case core.EventApply: - logger.Info("Would apply target", "name", event.Target.Name, "address", event.Target.Address, "labels", event.Target.Labels) + logger.Info( + "Applying Target", + "target", event.Target.Name, + "address", event.Target.Address, + "labels", event.Target.Labels, + "targetsource", r.targetSource.Name, + ) } return nil } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index fddebda..c82ad08 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -71,13 +71,20 @@ type TargetSourceReconciler struct { // move the current state of the cluster closer to the desired state. func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx). - WithName("targetsource controller"). - WithValues("targetsource", req.NamespacedName) + WithName("targetsource-controller"). + WithValues( + "targetsource", req.NamespacedName.Name, + "namespace", req.NamespacedName.Namespace, + ) targetSource, err := r.fetchTargetSource(ctx, req.NamespacedName) // If the TargetSource no longer exists, ensure runtime cleanup if apierrors.IsNotFound(err) { - logger.Info("TargetSource not found; stopping discovery pipeline") + logger.Info( + "TargetSource not found; stopped discovery pipeline", + "targetsource", req.NamespacedName.Name, + "namespace", req.NamespacedName.Namespace, + ) r.stopDiscoveryPipeline(req.NamespacedName) return ctrl.Result{}, nil } else if err != nil { @@ -100,7 +107,11 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, err } - logger.Info("Discovery pipeline started") + logger.Info( + "Started discovery pipeline", + "targetsource", req.NamespacedName.Name, + "namespace", req.NamespacedName.Namespace, + ) return ctrl.Result{}, nil } @@ -124,7 +135,11 @@ func (r *TargetSourceReconciler) hasPipelineRunning(key types.NamespacedName) bo // reconcileDeletion stops the discovery pipeline and removes the finalizer func (r *TargetSourceReconciler) reconcileDeletion(ctx context.Context, key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource) (ctrl.Result, error) { logger := log.FromContext(ctx) - logger.Info("TargetSource is being deleted, stopping pipeline", "name", key) + logger.Info( + "TargetSource was marked for deletion; stopping discovery pipeline", + "targetsource", key.Name, + "namespace", key.Namespace, + ) r.stopDiscoveryPipeline(key) @@ -250,10 +265,14 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName <-supervisor.Done() supervisor.Wait() // Wait for components to exit - logger.Info("Pipeline stopped; cleaning up") close(targetChannel) r.DiscoveryRegistry.Unregister(key) r.stopDiscoveryPipeline(key) + logger.Info( + "Discovery pipeline stopped; cleaned up resources", + "targetsource", key.Name, + "namespace", key.Namespace, + ) }() r.mu.Lock() From e447b3bd18867bd9d5cd1df19262dec14d0785dd Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 30 Apr 2026 12:36:18 +0000 Subject: [PATCH 109/153] improved logging --- .../controller/discovery/loaders/http/loader.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/internal/controller/discovery/loaders/http/loader.go b/internal/controller/discovery/loaders/http/loader.go index c95f1b3..4b58223 100644 --- a/internal/controller/discovery/loaders/http/loader.go +++ b/internal/controller/discovery/loaders/http/loader.go @@ -77,13 +77,20 @@ func (l *Loader) Start( spec.Provider.HTTP.Token, ) if err != nil { - logger.Error(err, "failed to fetch targets from HTTP endpoint") + logger.Error( + err, + "Failed to fetch targets from HTTP endpoint", + "url", spec.Provider.HTTP.URL, + ) return } snapshotID := fmt.Sprintf("%s-%s-%s", targetsourceNN.Namespace, targetsourceNN.Name, uuid.NewString()) if err := loaderUtils.SendSnapshot(ctx, out, targets, snapshotID, l.cfg.ChunkSize); err != nil { - logger.Error(err, "failed to send discovery snapshot") + logger.Error( + err, + "Failed to send discovery snapshot", + ) return } } @@ -95,7 +102,11 @@ func (l *Loader) Start( for { select { case <-ctx.Done(): - logger.Info("HTTP loader stopped") + logger.Info( + "HTTP loader stopped", + "targetsource", targetsourceNN.Name, + "namespace", targetsourceNN.Namespace, + ) return nil case <-ticker.C: From fca37e0e87076946731fe0918846d75ab20d0356 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 30 Apr 2026 12:39:48 +0000 Subject: [PATCH 110/153] improved logging --- .../discovery/loaders/http/loader.go | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/internal/controller/discovery/loaders/http/loader.go b/internal/controller/discovery/loaders/http/loader.go index 4b58223..95470fc 100644 --- a/internal/controller/discovery/loaders/http/loader.go +++ b/internal/controller/discovery/loaders/http/loader.go @@ -44,15 +44,12 @@ func (l *Loader) Start( logger := log.FromContext(ctx).WithValues( "component", "loader", "name", l.Name(), - "targetsource", targetsourceNN, - ) - - logger.Info( - "HTTP loader started", "targetsource", targetsourceNN.Name, "namespace", targetsourceNN.Namespace, ) + logger.Info("HTTP loader started") + // Input Validation of spec if spec.Provider == nil || spec.Provider.HTTP == nil { return errors.New("HTTP loader requires spec.provider.http to be set") @@ -66,7 +63,11 @@ func (l *Loader) Start( ticker := time.NewTicker(interval) defer ticker.Stop() - logger.Info("HTTP pull loader started", "interval", interval.String()) + logger.Info( + "HTTP polling discovery started", + "interval", interval.String(), + "url", spec.Provider.HTTP.URL, + ) // helper function to fetch targets and emit discovery messages fetchAndEmit := func() { @@ -90,9 +91,17 @@ func (l *Loader) Start( logger.Error( err, "Failed to send discovery snapshot", + "snapshotID", snapshotID, + "targets", len(targets), ) return } + + logger.Info( + "Discovery snapshot sent", + "snapshotID", snapshotID, + "targets", len(targets), + ) } // Immediate fetch on startup @@ -102,11 +111,7 @@ func (l *Loader) Start( for { select { case <-ctx.Done(): - logger.Info( - "HTTP loader stopped", - "targetsource", targetsourceNN.Name, - "namespace", targetsourceNN.Namespace, - ) + logger.Info("HTTP loader stopped") return nil case <-ticker.C: From fd4abe7f086416c0bd4b52249a03625ac6d72124 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 30 Apr 2026 12:55:58 +0000 Subject: [PATCH 111/153] improved logging --- .../controller/discovery/target_reconciler.go | 34 +++++++++---------- .../controller/targetsource_controller.go | 29 ++++++++-------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/internal/controller/discovery/target_reconciler.go b/internal/controller/discovery/target_reconciler.go index 67d9611..39382ab 100644 --- a/internal/controller/discovery/target_reconciler.go +++ b/internal/controller/discovery/target_reconciler.go @@ -48,36 +48,26 @@ func NewTargetReconciler(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.T func (r *TargetReconciler) Run(ctx context.Context) error { r.ctx = ctx - logger := log.FromContext(r.ctx). - WithValues( - "component", "target reconciler", - "name", r.targetSource.Name, - "namespace", r.targetSource.Namespace, - ) - logger.Info( - "Target reconciler started", + logger := log.FromContext(ctx).WithValues( + "component", "target-reconciler", "targetsource", r.targetSource.Name, "namespace", r.targetSource.Namespace, ) + logger.Info("Target reconciler started") + for r.ctx.Err() == nil { select { case batch, ok := <-r.in: if !ok { // Channel closed, pipeline is shutting down - logger.Info( - "Input channel closed; stopping target reconciler", - "targetsource", r.targetSource.Name, - ) + logger.Info("Input channel closed; stopping target reconciler") return nil } r.queue = append(r.queue, batch...) case <-ctx.Done(): - logger.Info( - "Context was canceled; stopping target reconciler", - "targetsource", r.targetSource.Name, - ) + logger.Info("Context was canceled; stopping target reconciler") return nil } @@ -190,7 +180,11 @@ func (r *TargetReconciler) collectSnapshot(chunk core.DiscoverySnapshot, logger ) } if chunk.ChunkIndex < 0 || chunk.ChunkIndex >= snapshot.totalChunks { - logger.Error(nil, "snapshot chunk index out of range", "index", chunk.ChunkIndex) + logger.Error( + nil, + "Snapshot chunk index out of range", + "chunkIndex", chunk.ChunkIndex, + ) r.activeSnapshot = nil return nil } @@ -232,7 +226,11 @@ func (r *TargetReconciler) applySnapshot(ctx context.Context, snapshot *snapshot chunk, ok := snapshot.received[i] if !ok { - logger.Error(nil, "missing snapshot chunk", "index", i) + logger.Error( + nil, + "Missing snapshot chunk", + "chunkIndex", i, + ) r.activeSnapshot = nil return nil } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index c82ad08..f36d47d 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -80,11 +80,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request targetSource, err := r.fetchTargetSource(ctx, req.NamespacedName) // If the TargetSource no longer exists, ensure runtime cleanup if apierrors.IsNotFound(err) { - logger.Info( - "TargetSource not found; stopped discovery pipeline", - "targetsource", req.NamespacedName.Name, - "namespace", req.NamespacedName.Namespace, - ) + logger.Info("TargetSource not found; stopped discovery pipeline") r.stopDiscoveryPipeline(req.NamespacedName) return ctrl.Result{}, nil } else if err != nil { @@ -100,6 +96,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request } if r.hasPipelineRunning(req.NamespacedName) { + logger.Info("Discovery pipeline already running; reconciliation completed") return ctrl.Result{}, nil } @@ -107,11 +104,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, err } - logger.Info( - "Started discovery pipeline", - "targetsource", req.NamespacedName.Name, - "namespace", req.NamespacedName.Namespace, - ) + logger.Info("Started discovery pipeline") return ctrl.Result{}, nil } @@ -134,13 +127,11 @@ func (r *TargetSourceReconciler) hasPipelineRunning(key types.NamespacedName) bo // reconcileDeletion stops the discovery pipeline and removes the finalizer func (r *TargetSourceReconciler) reconcileDeletion(ctx context.Context, key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource) (ctrl.Result, error) { - logger := log.FromContext(ctx) - logger.Info( - "TargetSource was marked for deletion; stopping discovery pipeline", + logger := log.FromContext(ctx).WithValues( "targetsource", key.Name, "namespace", key.Namespace, ) - + logger.Info("TargetSource was marked for deletion; stopping discovery pipeline") r.stopDiscoveryPipeline(key) // Remove finalizer if exists @@ -149,6 +140,8 @@ func (r *TargetSourceReconciler) reconcileDeletion(ctx context.Context, key type if err := r.Update(ctx, targetSource); err != nil { return ctrl.Result{}, err } + + logger.Info("Removed TargetSource finalizer") } return ctrl.Result{}, nil @@ -165,6 +158,12 @@ func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSour return err } + log.FromContext(ctx).Info( + "Added TargetSource finalizer", + "targetsource", targetSource.Name, + "namespace", targetSource.Namespace, + ) + return nil } @@ -234,7 +233,9 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName // Wait for reconciler to be ready before starting loader select { case <-targetReconcilerReady: + logger.Info("Target reconciler started") case <-supervisor.Done(): + logger.Info("Supervisor stopped before target reconciler became ready") return nil } From a6bc11447919eac352164b959bcac482c2b0a115 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 30 Apr 2026 13:23:47 +0000 Subject: [PATCH 112/153] simplified pipeline context handling --- internal/controller/discovery/core/types.go | 12 +++- internal/controller/discovery/registry.go | 8 +++ .../controller/targetsource_controller.go | 68 ++++++------------- 3 files changed, 38 insertions(+), 50 deletions(-) diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 2c37fc7..2f89fdf 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -1,8 +1,18 @@ package core +import "context" + +// DiscoveryRegistryValue represents the controller-owned runtime state +// of a discovery pipeline for a single TargetSource type DiscoveryRegistryValue struct { - Channel chan<- []DiscoveryMessage + // Channel is the outbound communication channel used by discovery + // components (loaders, webhooks, etc.) to emit discovery messages + Channel chan<- []DiscoveryMessage + // WebhookEnabled indicates whether webhook-based discovery is enabled + // for this TargetSource WebhookEnabled bool + // Stop cancels the discovery pipeline associated with this registry entry + Stop context.CancelFunc } type LoaderConfig struct { diff --git a/internal/controller/discovery/registry.go b/internal/controller/discovery/registry.go index 0afa2b2..2193665 100644 --- a/internal/controller/discovery/registry.go +++ b/internal/controller/discovery/registry.go @@ -39,3 +39,11 @@ func (r *Registry[K, V]) Get(key K) (V, bool) { r.mu.RUnlock() return value, ok } + +func (r *Registry[K, V]) Exists(key K) bool { + r.mu.RLock() + defer r.mu.RUnlock() + + _, exists := r.m[key] + return exists +} diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index f36d47d..dca0570 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -18,7 +18,6 @@ package controller import ( "context" - "sync" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -35,12 +34,6 @@ import ( "github.com/go-logr/logr" ) -// pipelineHandle represents a controller-owned handle to a running pipeline -// The controller never manipulates internals; it only invokes cancel() -type pipelineHandle struct { - cancel context.CancelFunc -} - // TargetSourceReconciler reconciles a TargetSource object // // Responsibilities: @@ -52,14 +45,13 @@ type TargetSourceReconciler struct { client.Client Scheme *runtime.Scheme - mu sync.Mutex - // runningPipelines tracks currently active pipelines by NamespacedName - runningPipelines map[types.NamespacedName]pipelineHandle - BufferSize int ChunkSize int - DiscoveryRegistry *discovery.Registry[types.NamespacedName, discoveryTypes.DiscoveryRegistryValue] + DiscoveryRegistry *discovery.Registry[ + types.NamespacedName, + discoveryTypes.DiscoveryRegistryValue, + ] } // +kubebuilder:rbac:groups=operator.gnmic.dev,resources=targetsources,verbs=get;list;watch;create;update;patch;delete @@ -80,8 +72,11 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request targetSource, err := r.fetchTargetSource(ctx, req.NamespacedName) // If the TargetSource no longer exists, ensure runtime cleanup if apierrors.IsNotFound(err) { + if pipeline, ok := r.DiscoveryRegistry.Get(req.NamespacedName); ok { + pipeline.Stop() + r.DiscoveryRegistry.Unregister(req.NamespacedName) + } logger.Info("TargetSource not found; stopped discovery pipeline") - r.stopDiscoveryPipeline(req.NamespacedName) return ctrl.Result{}, nil } else if err != nil { return ctrl.Result{}, err @@ -95,7 +90,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, err } - if r.hasPipelineRunning(req.NamespacedName) { + if r.DiscoveryRegistry.Exists(req.NamespacedName) { logger.Info("Discovery pipeline already running; reconciliation completed") return ctrl.Result{}, nil } @@ -117,14 +112,6 @@ func (r *TargetSourceReconciler) fetchTargetSource(ctx context.Context, key type return &targetSource, nil } -// hasPipelineRunning checks if a discovery pipeline is already running for the given key -func (r *TargetSourceReconciler) hasPipelineRunning(key types.NamespacedName) bool { - r.mu.Lock() - defer r.mu.Unlock() - _, exists := r.runningPipelines[key] - return exists -} - // reconcileDeletion stops the discovery pipeline and removes the finalizer func (r *TargetSourceReconciler) reconcileDeletion(ctx context.Context, key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource) (ctrl.Result, error) { logger := log.FromContext(ctx).WithValues( @@ -132,7 +119,10 @@ func (r *TargetSourceReconciler) reconcileDeletion(ctx context.Context, key type "namespace", key.Namespace, ) logger.Info("TargetSource was marked for deletion; stopping discovery pipeline") - r.stopDiscoveryPipeline(key) + if pipeline, ok := r.DiscoveryRegistry.Get(key); ok { + pipeline.Stop() + r.DiscoveryRegistry.Unregister(key) + } // Remove finalizer if exists if controllerutil.ContainsFinalizer(targetSource, LabelTargetSourceFinalizer) { @@ -197,7 +187,11 @@ func resolveRestartPolicy( // 2. loader is optional and conditional on spec // 3. Permanent failure of required components shuts down the pipeline // 4. Shutdown ordering: cancel ctx -> wait for goroutines to exit -> close channel -> unregister -func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource, logger logr.Logger) error { +func (r *TargetSourceReconciler) startDiscoveryPipeline( + key types.NamespacedName, + targetSource *gnmicv1alpha1.TargetSource, + logger logr.Logger, +) error { loaderConfigured := targetSource.Spec.Provider != nil webhookActivated := targetSource.Spec.Webhook.Enabled != nil && *targetSource.Spec.Webhook.Enabled restartPolicy := resolveRestartPolicy(targetSource.Spec.RestartPolicy) @@ -208,6 +202,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName if err := r.DiscoveryRegistry.Register(key, discoveryTypes.DiscoveryRegistryValue{ Channel: targetChannel, WebhookEnabled: webhookActivated, + Stop: supervisor.Stop, }); err != nil { return err } @@ -268,7 +263,6 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName close(targetChannel) r.DiscoveryRegistry.Unregister(key) - r.stopDiscoveryPipeline(key) logger.Info( "Discovery pipeline stopped; cleaned up resources", "targetsource", key.Name, @@ -276,35 +270,11 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline(key types.NamespacedName ) }() - r.mu.Lock() - r.runningPipelines[key] = pipelineHandle{ - cancel: func() { - supervisor.Stop() - }, - } - r.mu.Unlock() - return nil } -// stopDiscoveryPipeline stops and removes a running discovery pipeline -func (r *TargetSourceReconciler) stopDiscoveryPipeline(key types.NamespacedName) { - r.mu.Lock() - running, ok := r.runningPipelines[key] - if ok { - delete(r.runningPipelines, key) - } - r.mu.Unlock() - - if ok { - running.cancel() - } -} - // SetupWithManager sets up the controller with the Manager. func (r *TargetSourceReconciler) SetupWithManager(mgr ctrl.Manager) error { - r.runningPipelines = make(map[types.NamespacedName]pipelineHandle) - return ctrl.NewControllerManagedBy(mgr). For(&gnmicv1alpha1.TargetSource{}). Named("targetsource"). From 535ee49438fb9f4a3b45449a8321d6ab92966e42 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 1 May 2026 07:06:36 +0000 Subject: [PATCH 113/153] rename target reconciler to message processor --- ...get_reconciler.go => message_processor.go} | 108 +++++++++--------- .../controller/targetsource_controller.go | 2 +- 2 files changed, 55 insertions(+), 55 deletions(-) rename internal/controller/discovery/{target_reconciler.go => message_processor.go} (68%) diff --git a/internal/controller/discovery/target_reconciler.go b/internal/controller/discovery/message_processor.go similarity index 68% rename from internal/controller/discovery/target_reconciler.go rename to internal/controller/discovery/message_processor.go index 39382ab..ed66940 100644 --- a/internal/controller/discovery/target_reconciler.go +++ b/internal/controller/discovery/message_processor.go @@ -20,8 +20,8 @@ type snapshotBuffer struct { complete bool } -// TargetReconciler consumes discovered targets and applies them to Kubernetes -type TargetReconciler struct { +// MessageProcessor consumes discovery messages and applies them to Kubernetes +type MessageProcessor struct { ctx context.Context client client.Client scheme *runtime.Scheme @@ -33,9 +33,9 @@ type TargetReconciler struct { deferredEvents []core.DiscoveryEvent } -// NewTargetReconciler wires a TargetReconciler instance -func NewTargetReconciler(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []core.DiscoveryMessage) *TargetReconciler { - return &TargetReconciler{ +// NewMessageProcessor wires a MessageProcessor instance +func NewMessageProcessor(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []core.DiscoveryMessage) *MessageProcessor { + return &MessageProcessor{ client: c, scheme: s, targetSource: ts, @@ -45,41 +45,41 @@ func NewTargetReconciler(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.T // Run is a long‑running loop that receives target snapshots // and reconciles Target CRs accordingly -func (r *TargetReconciler) Run(ctx context.Context) error { - r.ctx = ctx +func (m *MessageProcessor) Run(ctx context.Context) error { + m.ctx = ctx logger := log.FromContext(ctx).WithValues( - "component", "target-reconciler", - "targetsource", r.targetSource.Name, - "namespace", r.targetSource.Namespace, + "component", "message-processor", + "targetsource", m.targetSource.Name, + "namespace", m.targetSource.Namespace, ) - logger.Info("Target reconciler started") + logger.Info("Message processor started") - for r.ctx.Err() == nil { + for m.ctx.Err() == nil { select { - case batch, ok := <-r.in: + case batch, ok := <-m.in: if !ok { // Channel closed, pipeline is shutting down - logger.Info("Input channel closed; stopping target reconciler") + logger.Info("Input channel closed; stopping message processor") return nil } - r.queue = append(r.queue, batch...) + m.queue = append(m.queue, batch...) case <-ctx.Done(): - logger.Info("Context was canceled; stopping target reconciler") + logger.Info("Context was canceled; stopping message processor") return nil } - for len(r.queue) > 0 { + for len(m.queue) > 0 { if ctx.Err() != nil { return ctx.Err() } - msg := r.queue[0] - r.queue = r.queue[1:] + msg := m.queue[0] + m.queue = m.queue[1:] - if err := r.processMessage(r.ctx, msg, logger); err != nil { + if err := m.processMessage(m.ctx, msg, logger); err != nil { // Returning error lets the supervisor (controller) // tear down and restart the pipeline via reconciliation // Q: when to return an error vs just log and continue? @@ -89,11 +89,11 @@ func (r *TargetReconciler) Run(ctx context.Context) error { } } - logger.Info("target reconciler stopped") + logger.Info("Message processor stopped") return nil } -func (r *TargetReconciler) processMessage(ctx context.Context, message core.DiscoveryMessage, logger logr.Logger) error { +func (m *MessageProcessor) processMessage(ctx context.Context, message core.DiscoveryMessage, logger logr.Logger) error { if err := ctx.Err(); err != nil { return err } @@ -108,7 +108,7 @@ func (r *TargetReconciler) processMessage(ctx context.Context, message core.Disc "chunkIndex", msg.ChunkIndex, "targets", len(msg.Targets), ) - return r.processSnapshot(ctx, msg, logger) + return m.processSnapshot(ctx, msg, logger) case core.DiscoveryEvent: // Process individual event-driven update @@ -117,7 +117,7 @@ func (r *TargetReconciler) processMessage(ctx context.Context, message core.Disc "event", msg.Event, "target", msg.Target.Name, ) - return r.processEvent(ctx, msg, logger) + return m.processEvent(ctx, msg, logger) default: return fmt.Errorf("Unknown discovery message type %T", msg) @@ -125,18 +125,18 @@ func (r *TargetReconciler) processMessage(ctx context.Context, message core.Disc } // processSnapshot takes a complete snapshot of discovered targets and reconciles Target CRs accordingly -func (r *TargetReconciler) processSnapshot(ctx context.Context, chunk core.DiscoverySnapshot, logger logr.Logger) error { - if r.activeSnapshot == nil { - r.startNewSnapshot(chunk, logger) +func (m *MessageProcessor) processSnapshot(ctx context.Context, chunk core.DiscoverySnapshot, logger logr.Logger) error { + if m.activeSnapshot == nil { + m.startNewSnapshot(chunk, logger) return nil } - snapshot := r.activeSnapshot + snapshot := m.activeSnapshot // Check if a new snapshot arrived if snapshot.snapshotID != chunk.SnapshotID { // If current snapshot is complete apply it first if snapshot.complete { - if err := r.applySnapshot(ctx, snapshot, logger); err != nil { + if err := m.applySnapshot(ctx, snapshot, logger); err != nil { return err } } else { @@ -149,28 +149,28 @@ func (r *TargetReconciler) processSnapshot(ctx context.Context, chunk core.Disco } // Start collecting the new snapshot - r.startNewSnapshot(chunk, logger) + m.startNewSnapshot(chunk, logger) return nil } - return r.collectSnapshot(chunk, logger) + return m.collectSnapshot(chunk, logger) } -func (r *TargetReconciler) startNewSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) { - r.activeSnapshot = &snapshotBuffer{ +func (m *MessageProcessor) startNewSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) { + m.activeSnapshot = &snapshotBuffer{ snapshotID: chunk.SnapshotID, totalChunks: chunk.TotalChunks, received: make(map[int][]core.DiscoveredTarget), complete: false, } // Delete buffered events that will be current with new snapshot - r.deferredEvents = nil + m.deferredEvents = nil - r.collectSnapshot(chunk, logger) + m.collectSnapshot(chunk, logger) } -func (r *TargetReconciler) collectSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) error { - snapshot := r.activeSnapshot +func (m *MessageProcessor) collectSnapshot(chunk core.DiscoverySnapshot, logger logr.Logger) error { + snapshot := m.activeSnapshot if chunk.TotalChunks != snapshot.totalChunks { logger.Error( @@ -185,7 +185,7 @@ func (r *TargetReconciler) collectSnapshot(chunk core.DiscoverySnapshot, logger "Snapshot chunk index out of range", "chunkIndex", chunk.ChunkIndex, ) - r.activeSnapshot = nil + m.activeSnapshot = nil return nil } if _, exists := snapshot.received[chunk.ChunkIndex]; exists { @@ -194,7 +194,7 @@ func (r *TargetReconciler) collectSnapshot(chunk core.DiscoverySnapshot, logger "Duplicate snapshot chunk received", "chunkIndex", chunk.ChunkIndex, ) - r.activeSnapshot = nil + m.activeSnapshot = nil return nil } @@ -207,10 +207,10 @@ func (r *TargetReconciler) collectSnapshot(chunk core.DiscoverySnapshot, logger return nil } -func (r *TargetReconciler) applySnapshot(ctx context.Context, snapshot *snapshotBuffer, logger logr.Logger) error { +func (m *MessageProcessor) applySnapshot(ctx context.Context, snapshot *snapshotBuffer, logger logr.Logger) error { select { case <-ctx.Done(): - r.activeSnapshot = nil + m.activeSnapshot = nil return nil default: } @@ -219,7 +219,7 @@ func (r *TargetReconciler) applySnapshot(ctx context.Context, snapshot *snapshot for i := 0; i < snapshot.totalChunks; i++ { select { case <-ctx.Done(): - r.activeSnapshot = nil + m.activeSnapshot = nil return nil default: } @@ -231,7 +231,7 @@ func (r *TargetReconciler) applySnapshot(ctx context.Context, snapshot *snapshot "Missing snapshot chunk", "chunkIndex", i, ) - r.activeSnapshot = nil + m.activeSnapshot = nil return nil } allTargets = append(allTargets, chunk...) @@ -247,40 +247,40 @@ func (r *TargetReconciler) applySnapshot(ctx context.Context, snapshot *snapshot // a.applyTargets // Replay deferred events - for _, event := range r.deferredEvents { + for _, event := range m.deferredEvents { select { case <-ctx.Done(): return nil default: } - if err := r.applyEvent(ctx, event, logger); err != nil { + if err := m.applyEvent(ctx, event, logger); err != nil { return err } } - r.activeSnapshot = nil - r.deferredEvents = nil + m.activeSnapshot = nil + m.deferredEvents = nil return nil } -func (r *TargetReconciler) processEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { +func (m *MessageProcessor) processEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { // If snapshot collecting is active defer events - if r.activeSnapshot != nil { - r.deferredEvents = append(r.deferredEvents, event) + if m.activeSnapshot != nil { + m.deferredEvents = append(m.deferredEvents, event) return nil } // Apply events - return r.applyEvent(ctx, event, logger) + return m.applyEvent(ctx, event, logger) } -func (r *TargetReconciler) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { +func (m *MessageProcessor) applyEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { switch event.Event { case core.EventDelete: logger.Info( "Deleting Target", "target", event.Target.Name, - "targetsource", r.targetSource.Name, + "targetsource", m.targetSource.Name, ) case core.EventApply: logger.Info( @@ -288,7 +288,7 @@ func (r *TargetReconciler) applyEvent(ctx context.Context, event core.DiscoveryE "target", event.Target.Name, "address", event.Target.Address, "labels", event.Target.Labels, - "targetsource", r.targetSource.Name, + "targetsource", m.targetSource.Name, ) } return nil diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index dca0570..b1755b0 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -208,7 +208,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline( } // Create target reconciler instance - targetReconciler := discovery.NewTargetReconciler( + targetReconciler := discovery.NewMessageProcessor( r.Client, r.Scheme, targetSource, From c09c68f5c0c8a0f042b3192458bd4a8f1fe671cf Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 1 May 2026 07:16:44 +0000 Subject: [PATCH 114/153] rename pipeline to runtime --- .../controller/targetsource_controller.go | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index b1755b0..d4442cc 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -18,7 +18,6 @@ package controller import ( "context" - "time" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -37,9 +36,9 @@ import ( // TargetSourceReconciler reconciles a TargetSource object // // Responsibilities: -// - Ensure at most one pipeline per TargetSource -// - Start pipelines on reconcile -// - Stop pipelines on deletion or NotFound +// - Ensure at most one runtime per TargetSource +// - Start runtimes on reconcile +// - Stop runtimes on deletion or NotFound // - Delegate runtime failure handling to the Supervisor type TargetSourceReconciler struct { client.Client @@ -72,11 +71,11 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request targetSource, err := r.fetchTargetSource(ctx, req.NamespacedName) // If the TargetSource no longer exists, ensure runtime cleanup if apierrors.IsNotFound(err) { - if pipeline, ok := r.DiscoveryRegistry.Get(req.NamespacedName); ok { - pipeline.Stop() + if runtime, ok := r.DiscoveryRegistry.Get(req.NamespacedName); ok { + runtime.Stop() r.DiscoveryRegistry.Unregister(req.NamespacedName) } - logger.Info("TargetSource not found; stopped discovery pipeline") + logger.Info("TargetSource not found; stopped discovery runtime") return ctrl.Result{}, nil } else if err != nil { return ctrl.Result{}, err @@ -91,15 +90,15 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request } if r.DiscoveryRegistry.Exists(req.NamespacedName) { - logger.Info("Discovery pipeline already running; reconciliation completed") + logger.Info("Discovery runtime already running; reconciliation completed") return ctrl.Result{}, nil } - if err := r.startDiscoveryPipeline(req.NamespacedName, targetSource, logger); err != nil { + if err := r.startDiscoveryRuntime(req.NamespacedName, targetSource, logger); err != nil { return ctrl.Result{}, err } - logger.Info("Started discovery pipeline") + logger.Info("Started discovery runtime") return ctrl.Result{}, nil } @@ -112,15 +111,15 @@ func (r *TargetSourceReconciler) fetchTargetSource(ctx context.Context, key type return &targetSource, nil } -// reconcileDeletion stops the discovery pipeline and removes the finalizer +// reconcileDeletion stops the discovery runtime and removes the finalizer func (r *TargetSourceReconciler) reconcileDeletion(ctx context.Context, key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource) (ctrl.Result, error) { logger := log.FromContext(ctx).WithValues( "targetsource", key.Name, "namespace", key.Namespace, ) - logger.Info("TargetSource was marked for deletion; stopping discovery pipeline") - if pipeline, ok := r.DiscoveryRegistry.Get(key); ok { - pipeline.Stop() + logger.Info("TargetSource was marked for deletion; stopping discovery runtime") + if runtime, ok := r.DiscoveryRegistry.Get(key); ok { + runtime.Stop() r.DiscoveryRegistry.Unregister(key) } @@ -157,6 +156,7 @@ func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSour return nil } + // resolveRestartPolicy merges an optional spec override with the controller’s default restart policy func resolveRestartPolicy( override *gnmicv1alpha1.RestartPolicySpec, @@ -179,24 +179,19 @@ func resolveRestartPolicy( return resolved } - -// startDiscoveryPipeline creates and starts a discovery pipeline for a TargetSource +// startDiscoveryRuntime creates and starts a discovery runtime for a TargetSource // -// Pipeline semantics: +// Runtime semantics: // 1. target reconciler is mandatory and must start first // 2. loader is optional and conditional on spec -// 3. Permanent failure of required components shuts down the pipeline +// 3. Permanent failure of required components shuts down the runtime // 4. Shutdown ordering: cancel ctx -> wait for goroutines to exit -> close channel -> unregister -func (r *TargetSourceReconciler) startDiscoveryPipeline( +func (r *TargetSourceReconciler) startDiscoveryRuntime( key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource, logger logr.Logger, ) error { - loaderConfigured := targetSource.Spec.Provider != nil webhookActivated := targetSource.Spec.Webhook.Enabled != nil && *targetSource.Spec.Webhook.Enabled - restartPolicy := resolveRestartPolicy(targetSource.Spec.RestartPolicy) - - supervisor := discovery.NewSupervisor(context.Background()) targetChannel := make(chan []discoveryTypes.DiscoveryMessage, r.BufferSize) if err := r.DiscoveryRegistry.Register(key, discoveryTypes.DiscoveryRegistryValue{ @@ -264,7 +259,7 @@ func (r *TargetSourceReconciler) startDiscoveryPipeline( close(targetChannel) r.DiscoveryRegistry.Unregister(key) logger.Info( - "Discovery pipeline stopped; cleaned up resources", + "Discovery runtime stopped; cleaned up resources", "targetsource", key.Name, "namespace", key.Namespace, ) From e4c01bac6d1abcdec28af44c20a65b6d4af477e0 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 1 May 2026 08:00:34 +0000 Subject: [PATCH 115/153] removed supervisor --- api/v1alpha1/targetsource_types.go | 11 -- .../discovery/core/loader_interface.go | 4 +- internal/controller/discovery/core/types.go | 5 +- internal/controller/discovery/defaults.go | 12 -- .../discovery/loaders/http/loader.go | 2 +- internal/controller/discovery/supervisor.go | 125 --------------- .../controller/targetsource_controller.go | 145 +++++++----------- 7 files changed, 59 insertions(+), 245 deletions(-) delete mode 100644 internal/controller/discovery/defaults.go delete mode 100644 internal/controller/discovery/supervisor.go diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 7c8f74c..ae719c1 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -29,9 +29,6 @@ type TargetSourceSpec struct { // +kubebuilder:validation:Optional TargetLabels map[string]string `json:"targetLabels,omitempty"` - // +kubebuilder:validation:Optional - RestartPolicy *RestartPolicySpec `json:"restartPolicy,omitempty"` - // +kubebuilder:validation:MinLength=1 TargetProfile string `json:"targetProfile"` } @@ -57,14 +54,6 @@ type ConsulConfig struct { URL string `json:"url,omitempty"` } -type RestartPolicySpec struct { - // +kubebuilder:validation:Optional - MaxRestarts *int `json:"maxRestarts,omitempty"` - - // +kubebuilder:validation:Optional - BackoffSeconds *int `json:"backoffSeconds,omitempty"` -} - // TargetSourceStatus defines the observed state of TargetSource type TargetSourceStatus struct { Status string `json:"status"` diff --git a/internal/controller/discovery/core/loader_interface.go b/internal/controller/discovery/core/loader_interface.go index 72f1898..bebd725 100644 --- a/internal/controller/discovery/core/loader_interface.go +++ b/internal/controller/discovery/core/loader_interface.go @@ -13,9 +13,9 @@ type Loader interface { // Name returns the unique loader identifier e.g. "pull" Name() string - // Start begins discovery and pushes target snapshots or events into the out channel + // Run begins discovery and pushes target snapshots or events into the out channel // The loader must stop cleanly when ctx is canceled - Start( + Run( ctx context.Context, targetsourceName types.NamespacedName, spec gnmicv1alpha1.TargetSourceSpec, diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 2f89fdf..94b4e85 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -8,10 +8,7 @@ type DiscoveryRegistryValue struct { // Channel is the outbound communication channel used by discovery // components (loaders, webhooks, etc.) to emit discovery messages Channel chan<- []DiscoveryMessage - // WebhookEnabled indicates whether webhook-based discovery is enabled - // for this TargetSource - WebhookEnabled bool - // Stop cancels the discovery pipeline associated with this registry entry + // Stop cancels the discovery context associated with this registry entry Stop context.CancelFunc } diff --git a/internal/controller/discovery/defaults.go b/internal/controller/discovery/defaults.go deleted file mode 100644 index dc6f046..0000000 --- a/internal/controller/discovery/defaults.go +++ /dev/null @@ -1,12 +0,0 @@ -package discovery - -import "time" - -// DefaultRestartPolicy defines the default restart behavior -// for the discovery components -func DefaultRestartPolicy() RestartPolicy { - return RestartPolicy{ - MaxRestarts: 5, - Backoff: 3 * time.Second, - } -} diff --git a/internal/controller/discovery/loaders/http/loader.go b/internal/controller/discovery/loaders/http/loader.go index 67c61e1..383e974 100644 --- a/internal/controller/discovery/loaders/http/loader.go +++ b/internal/controller/discovery/loaders/http/loader.go @@ -27,7 +27,7 @@ func (l *Loader) Name() string { return "http" } -func (l *Loader) Start( +func (l *Loader) Run( ctx context.Context, targetsourceNN types.NamespacedName, spec gnmicv1alpha1.TargetSourceSpec, diff --git a/internal/controller/discovery/supervisor.go b/internal/controller/discovery/supervisor.go deleted file mode 100644 index 22ec227..0000000 --- a/internal/controller/discovery/supervisor.go +++ /dev/null @@ -1,125 +0,0 @@ -package discovery - -import ( - "context" - "sync" - "time" - - "sigs.k8s.io/controller-runtime/pkg/log" -) - -// Supervisor coordinates the runtime lifecycle of pipeline components -// -// Guarantees: -// - Each component is restarted independently -// - Permanent failure escalates according to policy -// - Stop() cancels all components -// - Wait() blocks until all goroutines exit -type Supervisor struct { - ctx context.Context - cancel context.CancelFunc - - wg sync.WaitGroup - - mu sync.Mutex - stopped bool -} - -// RestartPolicy defines restart behavior of a component -type RestartPolicy struct { - MaxRestarts int - Backoff time.Duration -} - -// ComponentSpec defines a supervised component -type ComponentSpec struct { - Name string - Run func(ctx context.Context) error - Policy RestartPolicy - // EscalatesOnFailure indicates whether a permanent failure of this component should shut down the entire pipeline - EscalatesOnFailure bool -} - -// NewSupervisor creates a new Supervisor with a cancellable context -func NewSupervisor(parentCtx context.Context) *Supervisor { - ctx, cancel := context.WithCancel(parentCtx) - return &Supervisor{ - ctx: ctx, - cancel: cancel, - } -} - -// Stop signals all supervised components to stop by canceling the context -func (s *Supervisor) Stop() { - s.mu.Lock() - defer s.mu.Unlock() - - if s.stopped { - return - } - s.stopped = true - s.cancel() -} - -// Done returns a channel that is closed when the pipeline is stopped -func (s *Supervisor) Done() <-chan struct{} { return s.ctx.Done() } - -// Wait blocks until all supervised components have exited -func (s *Supervisor) Wait() { s.wg.Wait() } - -// StartSupervisedComponent starts and supervises a component -func (s *Supervisor) StartSupervisedComponent(component ComponentSpec) { - s.wg.Add(1) - - go func() { - defer s.wg.Done() - - logger := log.FromContext(s.ctx).WithValues("component", component.Name) - failures := 0 - - for { - logger.Info( - "Starting supervised component", - "component", component.Name, - ) - err := component.Run(s.ctx) - - if s.ctx.Err() != nil { - logger.Info("component stopped due to pipeline shutdown") - return - } - - failures++ - logger.Error( - err, - "Supervised component failed", - "component", component.Name, - "attempt", failures, - "maxRestarts", component.Policy.MaxRestarts, - ) - - if failures >= component.Policy.MaxRestarts { - if component.EscalatesOnFailure { - logger.Error( - err, - "Supervised component permanently failed; stopped discovery pipeline", - "component", component.Name, - ) - s.Stop() - } else { - logger.Info( - "Optional component permanently failed; continuing without it", - "component", component.Name, - ) - } - return - } - - select { - case <-time.After(component.Policy.Backoff): - case <-s.ctx.Done(): - return - } - } - }() -} diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index d4442cc..afc088b 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -36,10 +36,10 @@ import ( // TargetSourceReconciler reconciles a TargetSource object // // Responsibilities: -// - Ensure at most one runtime per TargetSource -// - Start runtimes on reconcile -// - Stop runtimes on deletion or NotFound -// - Delegate runtime failure handling to the Supervisor +// - Ensure at most one discovery runtime per TargetSource +// - Start runtime on reconcile if not already running +// - Restart runtime on reconcile if spec changed +// - Stop runtime on deletion or NotFound type TargetSourceReconciler struct { client.Client Scheme *runtime.Scheme @@ -94,7 +94,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, nil } - if err := r.startDiscoveryRuntime(req.NamespacedName, targetSource, logger); err != nil { + if err := r.startDiscovery(req.NamespacedName, targetSource, logger); err != nil { return ctrl.Result{}, err } @@ -156,113 +156,78 @@ func (r *TargetSourceReconciler) ensureFinalizer(ctx context.Context, targetSour return nil } - -// resolveRestartPolicy merges an optional spec override with the controller’s default restart policy -func resolveRestartPolicy( - override *gnmicv1alpha1.RestartPolicySpec, -) discovery.RestartPolicy { - defaults := discovery.DefaultRestartPolicy() - - if override == nil { - return defaults - } - - resolved := defaults - - if override.MaxRestarts != nil { - resolved.MaxRestarts = *override.MaxRestarts - } - - if override.BackoffSeconds != nil { - resolved.Backoff = time.Duration(*override.BackoffSeconds) * time.Second - } - - return resolved -} -// startDiscoveryRuntime creates and starts a discovery runtime for a TargetSource +// startDiscovery creates and starts a discovery runtime for a TargetSource // -// Runtime semantics: -// 1. target reconciler is mandatory and must start first -// 2. loader is optional and conditional on spec -// 3. Permanent failure of required components shuts down the runtime -// 4. Shutdown ordering: cancel ctx -> wait for goroutines to exit -> close channel -> unregister -func (r *TargetSourceReconciler) startDiscoveryRuntime( +// Invariant: +// - MessageProcessor and Loader must run for the lifetime of the TargetSource +// - Any unexpected exit is treated as a bug and triggers full shutdown +func (r *TargetSourceReconciler) startDiscovery( key types.NamespacedName, targetSource *gnmicv1alpha1.TargetSource, logger logr.Logger, ) error { - webhookActivated := targetSource.Spec.Webhook.Enabled != nil && *targetSource.Spec.Webhook.Enabled - targetChannel := make(chan []discoveryTypes.DiscoveryMessage, r.BufferSize) + ctx, cancel := context.WithCancel(context.Background()) + + // Register discovery runtime of targetsource if err := r.DiscoveryRegistry.Register(key, discoveryTypes.DiscoveryRegistryValue{ - Channel: targetChannel, - WebhookEnabled: webhookActivated, - Stop: supervisor.Stop, + Channel: targetChannel, + Stop: cancel, }); err != nil { return err } - // Create target reconciler instance - targetReconciler := discovery.NewMessageProcessor( + // Cleanup function to cleanup discovery runtime of targetsource + cleanup := func() { + cancel() + r.DiscoveryRegistry.Unregister(key) + close(targetChannel) + } + + // Start message processor + messageProcessor := discovery.NewMessageProcessor( r.Client, r.Scheme, targetSource, targetChannel, ) - // Start target reconciler - targetReconcilerReady := make(chan struct{}) - supervisor.StartSupervisedComponent(discovery.ComponentSpec{ - Name: "target-reconciler", - Policy: restartPolicy, - EscalatesOnFailure: true, - Run: func(ctx context.Context) error { - close(targetReconcilerReady) // Signals that reconciler started successfully - return targetReconciler.Run(ctx) - }, - }) - // Wait for reconciler to be ready before starting loader - select { - case <-targetReconcilerReady: - logger.Info("Target reconciler started") - case <-supervisor.Done(): - logger.Info("Supervisor stopped before target reconciler became ready") - return nil - } + go func() { + logger.Info("Message processor started") - // Create loader instance - if loaderConfigured { - loader, err := discovery.NewLoader( - key, - targetSource.Spec, - discoveryTypes.LoaderConfig{ChunkSize: r.ChunkSize}, - ) - if err != nil { - supervisor.Stop() - return err + if err := messageProcessor.Run(ctx); err != nil { + logger.Error(err, "Message processor exited unecpectedly") + } else { + logger.Error(nil, "Message processor exited unexpectedly without error") } - supervisor.StartSupervisedComponent(discovery.ComponentSpec{ - Name: "loader", - Policy: restartPolicy, - EscalatesOnFailure: !webhookActivated, - Run: func(ctx context.Context) error { - return loader.Start(ctx, key, targetSource.Spec, targetChannel) - }, - }) - } + // Any exit is considered a bug that should stop the discovery runtime + cleanup() + }() - // Monitor supervisor in a separate goroutine to handle shutdown and cleanup + // Start target loader + // webhookActivated := targetSource.Spec.Webhook.Enabled != nil && *targetSource.Spec.Webhook.Enabled + loaderConfig := discoveryTypes.LoaderConfig{ + ChunkSize: r.ChunkSize, + } + loader, err := discovery.NewLoader( + key, + targetSource.Spec, + loaderConfig, + ) + if err != nil { + logger.Error(err, "Target loader could not be created") + cleanup() + return err + } go func() { - <-supervisor.Done() - supervisor.Wait() // Wait for components to exit + if err := loader.Run(ctx, key, targetSource.Spec, targetChannel); err != nil { + logger.Error(err, "Target loader exited unexpectedly") + } else { + logger.Error(nil, "Target loader exited unexpectedly without error") + } - close(targetChannel) - r.DiscoveryRegistry.Unregister(key) - logger.Info( - "Discovery runtime stopped; cleaned up resources", - "targetsource", key.Name, - "namespace", key.Namespace, - ) + // Any exit is considered a bug that should stop the discovery runtime + cleanup() }() return nil From 77dbd7e12dd19d33a38113d280b522a7fdea1d99 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 1 May 2026 08:13:35 +0000 Subject: [PATCH 116/153] tidy loader configuration abstraction --- .../discovery/core/loader_interface.go | 10 +-------- internal/controller/discovery/core/types.go | 11 ++++++++-- internal/controller/discovery/loaders.go | 12 +++++------ .../discovery/loaders/http/loader.go | 21 +++++++------------ .../controller/targetsource_controller.go | 12 +++++------ 5 files changed, 27 insertions(+), 39 deletions(-) diff --git a/internal/controller/discovery/core/loader_interface.go b/internal/controller/discovery/core/loader_interface.go index bebd725..895258a 100644 --- a/internal/controller/discovery/core/loader_interface.go +++ b/internal/controller/discovery/core/loader_interface.go @@ -2,9 +2,6 @@ package core import ( "context" - - gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" - "k8s.io/apimachinery/pkg/types" ) // Loader defines a pluggable TargetSource loader interface @@ -15,10 +12,5 @@ type Loader interface { // Run begins discovery and pushes target snapshots or events into the out channel // The loader must stop cleanly when ctx is canceled - Run( - ctx context.Context, - targetsourceName types.NamespacedName, - spec gnmicv1alpha1.TargetSourceSpec, - out chan<- []DiscoveryMessage, - ) error + Run(ctx context.Context, out chan<- []DiscoveryMessage) error } diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 94b4e85..5028972 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -1,6 +1,11 @@ package core -import "context" +import ( + "context" + + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" + "k8s.io/apimachinery/pkg/types" +) // DiscoveryRegistryValue represents the controller-owned runtime state // of a discovery pipeline for a single TargetSource @@ -13,7 +18,9 @@ type DiscoveryRegistryValue struct { } type LoaderConfig struct { - ChunkSize int + TargetsourceNN types.NamespacedName + Spec *gnmicv1alpha1.TargetSourceSpec + ChunkSize int } // EventAction represents the type of a discovery event diff --git a/internal/controller/discovery/loaders.go b/internal/controller/discovery/loaders.go index 0d8ddd3..6c3e133 100644 --- a/internal/controller/discovery/loaders.go +++ b/internal/controller/discovery/loaders.go @@ -3,22 +3,20 @@ package discovery import ( "fmt" - gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery/core" http "github.com/gnmic/operator/internal/controller/discovery/loaders/http" - "k8s.io/apimachinery/pkg/types" ) // NewLoader creates a loader by name -func NewLoader(name types.NamespacedName, spec gnmicv1alpha1.TargetSourceSpec, cfg core.LoaderConfig) (core.Loader, error) { +func NewLoader(cfg core.LoaderConfig) (core.Loader, error) { switch { - case spec.Provider.HTTP != nil: + case cfg.Spec.Provider.HTTP != nil: return http.New(cfg), nil - case spec.Provider.Consul != nil: - return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", name) + case cfg.Spec.Provider.Consul != nil: + return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", cfg.TargetsourceNN) default: - return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", name) + return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", cfg.TargetsourceNN) } } diff --git a/internal/controller/discovery/loaders/http/loader.go b/internal/controller/discovery/loaders/http/loader.go index 383e974..17812aa 100644 --- a/internal/controller/discovery/loaders/http/loader.go +++ b/internal/controller/discovery/loaders/http/loader.go @@ -5,10 +5,8 @@ import ( "fmt" "time" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/log" - gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery/core" loaderUtils "github.com/gnmic/operator/internal/controller/discovery/loaders/utils" "github.com/google/uuid" @@ -27,22 +25,17 @@ func (l *Loader) Name() string { return "http" } -func (l *Loader) Run( - ctx context.Context, - targetsourceNN types.NamespacedName, - spec gnmicv1alpha1.TargetSourceSpec, - out chan<- []core.DiscoveryMessage, -) error { +func (l *Loader) Run(ctx context.Context, out chan<- []core.DiscoveryMessage) error { logger := log.FromContext(ctx).WithValues( "component", "loader", "name", l.Name(), - "targetsource", targetsourceNN, + "targetsource", l.cfg.TargetsourceNN, ) logger.Info( "HTTP loader started", - "targetsource", targetsourceNN.Name, - "namespace", targetsourceNN.Namespace, + "targetsource", l.cfg.TargetsourceNN.Name, + "namespace", l.cfg.TargetsourceNN.Namespace, ) // Only for debugging: emit a static snapshot every 30 seconds @@ -57,17 +50,17 @@ func (l *Loader) Run( case <-ticker.C: // Example snapshot (placeholder) - snapshotID := fmt.Sprintf("snapshot-%s-%s", targetsourceNN, uuid.NewString()) + snapshotID := fmt.Sprintf("%s-%s-%s", l.cfg.TargetsourceNN.Namespace, l.cfg.TargetsourceNN.Name, uuid.NewString()) targets := []core.DiscoveredTarget{ { Name: "ceos1", Address: "clab-3-nodes-ceos1:6030", - Labels: map[string]string{"TargetSource": targetsourceNN.String()}, + Labels: map[string]string{"TargetSource": l.cfg.TargetsourceNN.String()}, }, { Name: "leaf1", Address: "clab-3-nodes-leaf1:57400", - Labels: map[string]string{"TargetSource": targetsourceNN.String()}, + Labels: map[string]string{"TargetSource": l.cfg.TargetsourceNN.String()}, }, } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index afc088b..0064570 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -207,20 +207,18 @@ func (r *TargetSourceReconciler) startDiscovery( // Start target loader // webhookActivated := targetSource.Spec.Webhook.Enabled != nil && *targetSource.Spec.Webhook.Enabled loaderConfig := discoveryTypes.LoaderConfig{ - ChunkSize: r.ChunkSize, + TargetsourceNN: key, + Spec: &targetSource.Spec, + ChunkSize: r.ChunkSize, } - loader, err := discovery.NewLoader( - key, - targetSource.Spec, - loaderConfig, - ) + loader, err := discovery.NewLoader(loaderConfig) if err != nil { logger.Error(err, "Target loader could not be created") cleanup() return err } go func() { - if err := loader.Run(ctx, key, targetSource.Spec, targetChannel); err != nil { + if err := loader.Run(ctx, targetChannel); err != nil { logger.Error(err, "Target loader exited unexpectedly") } else { logger.Error(nil, "Target loader exited unexpectedly without error") From fe900e38774051956054d70af6ee24da88beb71f Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 1 May 2026 08:14:40 +0000 Subject: [PATCH 117/153] regenearte manifests without restartPolicy --- api/v1alpha1/zz_generated.deepcopy.go | 30 ------------------- .../operator.gnmic.dev_targetsources.yaml | 7 ----- 2 files changed, 37 deletions(-) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index df08573..608d47e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -843,31 +843,6 @@ func (in *ProviderSpec) DeepCopy() *ProviderSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RestartPolicySpec) DeepCopyInto(out *RestartPolicySpec) { - *out = *in - if in.MaxRestarts != nil { - in, out := &in.MaxRestarts, &out.MaxRestarts - *out = new(int) - **out = **in - } - if in.BackoffSeconds != nil { - in, out := &in.BackoffSeconds, &out.BackoffSeconds - *out = new(int) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestartPolicySpec. -func (in *RestartPolicySpec) DeepCopy() *RestartPolicySpec { - if in == nil { - return nil - } - out := new(RestartPolicySpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceConfig) DeepCopyInto(out *ServiceConfig) { *out = *in @@ -1325,11 +1300,6 @@ func (in *TargetSourceSpec) DeepCopyInto(out *TargetSourceSpec) { (*out)[key] = val } } - if in.RestartPolicy != nil { - in, out := &in.RestartPolicy, &out.RestartPolicy - *out = new(RestartPolicySpec) - (*in).DeepCopyInto(*out) - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSourceSpec. diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 6464ea2..b385d8e 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -60,13 +60,6 @@ spec: - message: exactly one of the fields in [http consul] must be set rule: '[has(self.http),has(self.consul)].filter(x,x==true).size() == 1' - restartPolicy: - properties: - backoffSeconds: - type: integer - maxRestarts: - type: integer - type: object targetLabels: additionalProperties: type: string From c1d7a91bc7e79f87736454dc31e530d3cf89b7fd Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 1 May 2026 08:29:54 +0000 Subject: [PATCH 118/153] tidy up comments --- internal/controller/discovery/discovery.go | 4 +--- internal/controller/discovery/loaders.go | 1 + internal/controller/targetsource_controller.go | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/controller/discovery/discovery.go b/internal/controller/discovery/discovery.go index 3dc51bd..491cdfb 100644 --- a/internal/controller/discovery/discovery.go +++ b/internal/controller/discovery/discovery.go @@ -4,13 +4,11 @@ package discovery // // The discovery subsystem is responsible for: // - Receiving discovery data from external providers (loaders, webhooks). -// - Supervising discovery pipelines and restart semantics. // - Applying discovered state to Kubernetes Targets. // // The package is structured into the following subpackages: // - core: message contracts, snapshot/event types, and transport helpers. -// - pipeline: supervision, restart policies, and lifecycle control. -// - reconciler: snapshot + event target state application logic. +// - message processor: snapshot + event target state application logic. // - loaders: target discovery providers (HTTP, webhook, etc.). // - registry: key -> channel registry. // diff --git a/internal/controller/discovery/loaders.go b/internal/controller/discovery/loaders.go index 6c3e133..9704b16 100644 --- a/internal/controller/discovery/loaders.go +++ b/internal/controller/discovery/loaders.go @@ -12,6 +12,7 @@ func NewLoader(cfg core.LoaderConfig) (core.Loader, error) { switch { case cfg.Spec.Provider.HTTP != nil: + // webhookActivated := targetSource.Spec.Webhook.Enabled != nil && *targetSource.Spec.Webhook.Enabled return http.New(cfg), nil case cfg.Spec.Provider.Consul != nil: return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", cfg.TargetsourceNN) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 0064570..2ba18a2 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -205,7 +205,6 @@ func (r *TargetSourceReconciler) startDiscovery( }() // Start target loader - // webhookActivated := targetSource.Spec.Webhook.Enabled != nil && *targetSource.Spec.Webhook.Enabled loaderConfig := discoveryTypes.LoaderConfig{ TargetsourceNN: key, Spec: &targetSource.Spec, From 05c7538ce47a4b81fd245b11435cb13481c4c671 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 1 May 2026 13:23:34 +0000 Subject: [PATCH 119/153] move webhook spec into provider and rename it to acceptPush --- api/v1alpha1/targetsource_types.go | 10 +++------- internal/controller/discovery/core/types.go | 5 ++++- internal/controller/discovery/loaders.go | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index ae719c1..3d69743 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -24,8 +24,7 @@ import ( // +kubebuilder:validation:Required type TargetSourceSpec struct { Provider *ProviderSpec `json:"provider"` - // +kubebuilder:validation:Optional - Webhook WebhookSpec `json:"webhook,omitempty"` + // +kubebuilder:validation:Optional TargetLabels map[string]string `json:"targetLabels,omitempty"` @@ -39,14 +38,11 @@ type ProviderSpec struct { Consul *ConsulConfig `json:"consul,omitempty"` } -type WebhookSpec struct { - // +kubebuilder:validation:Optional - Enabled *bool `json:"enabled,omitempty"` -} - type HTTPConfig struct { // +kubebuilder:validation:MinLength=1 URL string `json:"url"` + // +kubebuilder:validation:Optional + AcceptPush bool `json:"acceptPush,omitempty"` } type ConsulConfig struct { diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 5028972..1dfcc9f 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -8,19 +8,22 @@ import ( ) // DiscoveryRegistryValue represents the controller-owned runtime state -// of a discovery pipeline for a single TargetSource +// with its configuration for a single TargetSource type DiscoveryRegistryValue struct { // Channel is the outbound communication channel used by discovery // components (loaders, webhooks, etc.) to emit discovery messages Channel chan<- []DiscoveryMessage // Stop cancels the discovery context associated with this registry entry Stop context.CancelFunc + + LoaderConfig *LoaderConfig } type LoaderConfig struct { TargetsourceNN types.NamespacedName Spec *gnmicv1alpha1.TargetSourceSpec ChunkSize int + AcceptPush bool } // EventAction represents the type of a discovery event diff --git a/internal/controller/discovery/loaders.go b/internal/controller/discovery/loaders.go index 9704b16..487c76b 100644 --- a/internal/controller/discovery/loaders.go +++ b/internal/controller/discovery/loaders.go @@ -12,7 +12,7 @@ func NewLoader(cfg core.LoaderConfig) (core.Loader, error) { switch { case cfg.Spec.Provider.HTTP != nil: - // webhookActivated := targetSource.Spec.Webhook.Enabled != nil && *targetSource.Spec.Webhook.Enabled + cfg.AcceptPush = cfg.Spec.Provider.HTTP.AcceptPush return http.New(cfg), nil case cfg.Spec.Provider.Consul != nil: return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", cfg.TargetsourceNN) From 061d4b83daac3a5c26fbe12151aa353a88d1a213 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 1 May 2026 13:24:00 +0000 Subject: [PATCH 120/153] regenerate manifests --- api/v1alpha1/zz_generated.deepcopy.go | 21 ------------------- .../operator.gnmic.dev_targetsources.yaml | 7 ++----- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 608d47e..61e81fd 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1292,7 +1292,6 @@ func (in *TargetSourceSpec) DeepCopyInto(out *TargetSourceSpec) { *out = new(ProviderSpec) (*in).DeepCopyInto(*out) } - in.Webhook.DeepCopyInto(&out.Webhook) if in.TargetLabels != nil { in, out := &in.TargetLabels, &out.TargetLabels *out = make(map[string]string, len(*in)) @@ -1478,23 +1477,3 @@ func (in *TunnelTargetPolicyStatus) DeepCopy() *TunnelTargetPolicyStatus { in.DeepCopyInto(out) return out } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WebhookSpec) DeepCopyInto(out *WebhookSpec) { - *out = *in - if in.Enabled != nil { - in, out := &in.Enabled, &out.Enabled - *out = new(bool) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookSpec. -func (in *WebhookSpec) DeepCopy() *WebhookSpec { - if in == nil { - return nil - } - out := new(WebhookSpec) - in.DeepCopyInto(out) - return out -} diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index b385d8e..37d6919 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -49,6 +49,8 @@ spec: type: object http: properties: + acceptPush: + type: boolean url: minLength: 1 type: string @@ -67,11 +69,6 @@ spec: targetProfile: minLength: 1 type: string - webhook: - properties: - enabled: - type: boolean - type: object required: - provider - targetProfile From 41655a0d4e835bc2ff0b8a5a1cdaf55aa4bdfd7a Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 1 May 2026 14:12:05 +0000 Subject: [PATCH 121/153] remove spec from laoder config --- internal/controller/discovery/core/types.go | 2 -- internal/controller/discovery/loaders.go | 9 +++++---- internal/controller/targetsource_controller.go | 16 ++++++++-------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 1dfcc9f..993c84e 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -3,7 +3,6 @@ package core import ( "context" - gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "k8s.io/apimachinery/pkg/types" ) @@ -21,7 +20,6 @@ type DiscoveryRegistryValue struct { type LoaderConfig struct { TargetsourceNN types.NamespacedName - Spec *gnmicv1alpha1.TargetSourceSpec ChunkSize int AcceptPush bool } diff --git a/internal/controller/discovery/loaders.go b/internal/controller/discovery/loaders.go index 487c76b..d179d3e 100644 --- a/internal/controller/discovery/loaders.go +++ b/internal/controller/discovery/loaders.go @@ -3,18 +3,19 @@ package discovery import ( "fmt" + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery/core" http "github.com/gnmic/operator/internal/controller/discovery/loaders/http" ) // NewLoader creates a loader by name -func NewLoader(cfg core.LoaderConfig) (core.Loader, error) { +func NewLoader(cfg core.LoaderConfig, spec *gnmicv1alpha1.TargetSourceSpec) (core.Loader, error) { switch { - case cfg.Spec.Provider.HTTP != nil: - cfg.AcceptPush = cfg.Spec.Provider.HTTP.AcceptPush + case spec.Provider.HTTP != nil: + cfg.AcceptPush = spec.Provider.HTTP.AcceptPush return http.New(cfg), nil - case cfg.Spec.Provider.Consul != nil: + case spec.Provider.Consul != nil: return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", cfg.TargetsourceNN) default: return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", cfg.TargetsourceNN) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 2ba18a2..1cf962d 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -168,11 +168,16 @@ func (r *TargetSourceReconciler) startDiscovery( ) error { targetChannel := make(chan []discoveryTypes.DiscoveryMessage, r.BufferSize) ctx, cancel := context.WithCancel(context.Background()) + loaderConfig := discoveryTypes.LoaderConfig{ + TargetsourceNN: key, + ChunkSize: r.ChunkSize, + } // Register discovery runtime of targetsource if err := r.DiscoveryRegistry.Register(key, discoveryTypes.DiscoveryRegistryValue{ - Channel: targetChannel, - Stop: cancel, + Channel: targetChannel, + Stop: cancel, + LoaderConfig: &loaderConfig, }); err != nil { return err } @@ -205,12 +210,7 @@ func (r *TargetSourceReconciler) startDiscovery( }() // Start target loader - loaderConfig := discoveryTypes.LoaderConfig{ - TargetsourceNN: key, - Spec: &targetSource.Spec, - ChunkSize: r.ChunkSize, - } - loader, err := discovery.NewLoader(loaderConfig) + loader, err := discovery.NewLoader(loaderConfig, &targetSource.Spec) if err != nil { logger.Error(err, "Target loader could not be created") cleanup() From 97849ae9d9a7afdc13aab966882433f0a59f0f7c Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 1 May 2026 15:08:55 +0000 Subject: [PATCH 122/153] update LoaderConfig in registry --- internal/apiserver/apiserver.go | 2 +- internal/controller/discovery/core/types.go | 4 +- internal/controller/discovery/loaders.go | 8 ++-- .../discovery/loaders/http/loader.go | 20 +++++----- .../controller/targetsource_controller.go | 40 +++++++++++-------- 5 files changed, 40 insertions(+), 34 deletions(-) diff --git a/internal/apiserver/apiserver.go b/internal/apiserver/apiserver.go index 705b277..5eb88b8 100644 --- a/internal/apiserver/apiserver.go +++ b/internal/apiserver/apiserver.go @@ -5,8 +5,8 @@ import ( "net/http" "github.com/gnmic/operator/internal/controller" - "github.com/gnmic/operator/internal/controller/discovery/core" "github.com/gnmic/operator/internal/controller/discovery" + "github.com/gnmic/operator/internal/controller/discovery/core" "k8s.io/apimachinery/pkg/types" ) diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 993c84e..99605b9 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -15,10 +15,10 @@ type DiscoveryRegistryValue struct { // Stop cancels the discovery context associated with this registry entry Stop context.CancelFunc - LoaderConfig *LoaderConfig + CommonLoaderConfig *CommonLoaderConfig } -type LoaderConfig struct { +type CommonLoaderConfig struct { TargetsourceNN types.NamespacedName ChunkSize int AcceptPush bool diff --git a/internal/controller/discovery/loaders.go b/internal/controller/discovery/loaders.go index d179d3e..7f2c656 100644 --- a/internal/controller/discovery/loaders.go +++ b/internal/controller/discovery/loaders.go @@ -9,16 +9,16 @@ import ( ) // NewLoader creates a loader by name -func NewLoader(cfg core.LoaderConfig, spec *gnmicv1alpha1.TargetSourceSpec) (core.Loader, error) { +func NewLoader(cfg core.CommonLoaderConfig, spec *gnmicv1alpha1.TargetSourceSpec) (core.Loader, core.CommonLoaderConfig, error) { switch { case spec.Provider.HTTP != nil: cfg.AcceptPush = spec.Provider.HTTP.AcceptPush - return http.New(cfg), nil + return http.New(cfg), cfg, nil case spec.Provider.Consul != nil: - return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", cfg.TargetsourceNN) + return nil, cfg, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", cfg.TargetsourceNN) default: - return nil, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", cfg.TargetsourceNN) + return nil, cfg, fmt.Errorf("unknown targetsource loader, check TargetSource CRD for %s", cfg.TargetsourceNN) } } diff --git a/internal/controller/discovery/loaders/http/loader.go b/internal/controller/discovery/loaders/http/loader.go index 17812aa..3325adb 100644 --- a/internal/controller/discovery/loaders/http/loader.go +++ b/internal/controller/discovery/loaders/http/loader.go @@ -13,12 +13,12 @@ import ( ) type Loader struct { - cfg core.LoaderConfig + commonCfg core.CommonLoaderConfig } // New instantiates the http loader with the provided config -func New(cfg core.LoaderConfig) core.Loader { - return &Loader{cfg: cfg} +func New(cfg core.CommonLoaderConfig) core.Loader { + return &Loader{commonCfg: cfg} } func (l *Loader) Name() string { @@ -29,13 +29,13 @@ func (l *Loader) Run(ctx context.Context, out chan<- []core.DiscoveryMessage) er logger := log.FromContext(ctx).WithValues( "component", "loader", "name", l.Name(), - "targetsource", l.cfg.TargetsourceNN, + "targetsource", l.commonCfg.TargetsourceNN, ) logger.Info( "HTTP loader started", - "targetsource", l.cfg.TargetsourceNN.Name, - "namespace", l.cfg.TargetsourceNN.Namespace, + "targetsource", l.commonCfg.TargetsourceNN.Name, + "namespace", l.commonCfg.TargetsourceNN.Namespace, ) // Only for debugging: emit a static snapshot every 30 seconds @@ -50,21 +50,21 @@ func (l *Loader) Run(ctx context.Context, out chan<- []core.DiscoveryMessage) er case <-ticker.C: // Example snapshot (placeholder) - snapshotID := fmt.Sprintf("%s-%s-%s", l.cfg.TargetsourceNN.Namespace, l.cfg.TargetsourceNN.Name, uuid.NewString()) + snapshotID := fmt.Sprintf("%s-%s-%s", l.commonCfg.TargetsourceNN.Namespace, l.commonCfg.TargetsourceNN.Name, uuid.NewString()) targets := []core.DiscoveredTarget{ { Name: "ceos1", Address: "clab-3-nodes-ceos1:6030", - Labels: map[string]string{"TargetSource": l.cfg.TargetsourceNN.String()}, + Labels: map[string]string{"TargetSource": l.commonCfg.TargetsourceNN.String()}, }, { Name: "leaf1", Address: "clab-3-nodes-leaf1:57400", - Labels: map[string]string{"TargetSource": l.cfg.TargetsourceNN.String()}, + Labels: map[string]string{"TargetSource": l.commonCfg.TargetsourceNN.String()}, }, } - if err := loaderUtils.SendSnapshot(ctx, out, targets, snapshotID, l.cfg.ChunkSize); err != nil { + if err := loaderUtils.SendSnapshot(ctx, out, targets, snapshotID, l.commonCfg.ChunkSize); err != nil { return err } } diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 1cf962d..8207103 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -168,20 +168,11 @@ func (r *TargetSourceReconciler) startDiscovery( ) error { targetChannel := make(chan []discoveryTypes.DiscoveryMessage, r.BufferSize) ctx, cancel := context.WithCancel(context.Background()) - loaderConfig := discoveryTypes.LoaderConfig{ + loaderConfig := discoveryTypes.CommonLoaderConfig{ TargetsourceNN: key, ChunkSize: r.ChunkSize, } - // Register discovery runtime of targetsource - if err := r.DiscoveryRegistry.Register(key, discoveryTypes.DiscoveryRegistryValue{ - Channel: targetChannel, - Stop: cancel, - LoaderConfig: &loaderConfig, - }); err != nil { - return err - } - // Cleanup function to cleanup discovery runtime of targetsource cleanup := func() { cancel() @@ -189,13 +180,34 @@ func (r *TargetSourceReconciler) startDiscovery( close(targetChannel) } - // Start message processor messageProcessor := discovery.NewMessageProcessor( r.Client, r.Scheme, targetSource, targetChannel, ) + loader, loaderConfig, err := discovery.NewLoader(discoveryTypes.CommonLoaderConfig{ + TargetsourceNN: key, + ChunkSize: r.ChunkSize, + }, + &targetSource.Spec, + ) + if err != nil { + logger.Error(err, "Target loader could not be created") + cleanup() + return err + } + + // Register discovery runtime of targetsource + if err := r.DiscoveryRegistry.Register(key, discoveryTypes.DiscoveryRegistryValue{ + Channel: targetChannel, + Stop: cancel, + CommonLoaderConfig: &loaderConfig, + }); err != nil { + return err + } + + // Start message processor go func() { logger.Info("Message processor started") @@ -210,12 +222,6 @@ func (r *TargetSourceReconciler) startDiscovery( }() // Start target loader - loader, err := discovery.NewLoader(loaderConfig, &targetSource.Spec) - if err != nil { - logger.Error(err, "Target loader could not be created") - cleanup() - return err - } go func() { if err := loader.Run(ctx, targetChannel); err != nil { logger.Error(err, "Target loader exited unexpectedly") From 1bdc2945f0324de47a6bb10e15d2096aba3c272e Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 4 May 2026 08:49:17 -0600 Subject: [PATCH 123/153] moved updateStatus function to client.go --- internal/controller/discovery/client.go | 18 +++++++++ .../controller/targetsource_controller.go | 39 +------------------ 2 files changed, 20 insertions(+), 37 deletions(-) diff --git a/internal/controller/discovery/client.go b/internal/controller/discovery/client.go index a9d790f..e5cc5ea 100644 --- a/internal/controller/discovery/client.go +++ b/internal/controller/discovery/client.go @@ -7,6 +7,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -69,3 +70,20 @@ func deleteTarget(ctx context.Context, c client.Client, name string, namespace s return err } + +// updateTargetSourceStatus updates the status of the TargetSource Object ts. The only fields updated are targetCount and LastSync, which takes the current timestamp. +func updateTargetSourceStatus(ctx context.Context, c client.Client, ts *gnmicv1alpha1.TargetSource, targetCount int32) error { + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + latest := &gnmicv1alpha1.TargetSource{} + if err := c.Get(ctx, client.ObjectKeyFromObject(ts), latest); err != nil { + return err + } + + latest.Status.TargetsCount = targetCount + latest.Status.LastSync = metav1.Now() + + return c.Status().Update(ctx, latest) + }) + + return err +} diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 97a1432..f23a4a0 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -20,10 +20,8 @@ import ( "context" apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -95,8 +93,8 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request if targetSource.Generation != targetSource.Status.ObservedGeneration { r.reconcileDeletion(ctx, req.NamespacedName, targetSource) } else { - logger.Info("Discovery runtime already running; reconciliation completed, updating status") - return ctrl.Result{}, r.updateStatus(ctx, targetSource) + logger.Info("Discovery runtime already running; reconciliation completed") + return ctrl.Result{}, nil } } @@ -109,10 +107,6 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, err } - if err := r.updateStatus(ctx, targetSource); err != nil { - return ctrl.Result{}, err - } - logger.Info("Started discovery runtime") return ctrl.Result{}, nil } @@ -251,35 +245,6 @@ func (r *TargetSourceReconciler) startDiscovery( return nil } -func (r *TargetSourceReconciler) updateStatus(ctx context.Context, ts *gnmicv1alpha1.TargetSource) error { - // Update TargetSource Status field - var targetList gnmicv1alpha1.TargetList - - err := r.Client.List(ctx, &targetList, - client.InNamespace(ts.Namespace), - client.MatchingLabels{ - "gnmic.io/source": ts.Name, - }, - ) - if err != nil { - return err - } - - err = retry.RetryOnConflict(retry.DefaultRetry, func() error { - latest := &gnmicv1alpha1.TargetSource{} - if err := r.Get(ctx, client.ObjectKeyFromObject(ts), latest); err != nil { - return err - } - - latest.Status.TargetsCount = int32(len(targetList.Items)) - latest.Status.LastSync = metav1.Now() - - return r.Status().Update(ctx, latest) - }) - - return err -} - // SetupWithManager sets up the controller with the Manager. func (r *TargetSourceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). From 92552400b583ed5bd8d93cdfea79af00799ff797 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 4 May 2026 09:07:00 -0600 Subject: [PATCH 124/153] changed updateStauts handling --- .../controller/discovery/message_processor.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internal/controller/discovery/message_processor.go b/internal/controller/discovery/message_processor.go index 14ff401..af8da1f 100644 --- a/internal/controller/discovery/message_processor.go +++ b/internal/controller/discovery/message_processor.go @@ -31,6 +31,7 @@ type MessageProcessor struct { activeSnapshot *snapshotBuffer // Events are deferred while snapshot is in progress deferredEvents []core.DiscoveryEvent + targetCount int32 } // NewMessageProcessor wires a MessageProcessor instance @@ -305,6 +306,9 @@ func (m *MessageProcessor) applySnapshot(ctx context.Context, snapshot *snapshot } } + m.targetCount = int32(len(allTargets)) + m.updateStatus(logger) + m.activeSnapshot = nil m.deferredEvents = nil return nil @@ -321,6 +325,8 @@ func (m *MessageProcessor) applyEvent(ctx context.Context, event core.DiscoveryE logger.Info("deleted target object", "name", event.Target.Name, ) + m.targetCount-- + m.updateStatus(logger) } case core.EventApply: target := generateTargetResource(event.Target, m.targetSource) @@ -333,8 +339,20 @@ func (m *MessageProcessor) applyEvent(ctx context.Context, event core.DiscoveryE logger.Info("applied target object", "name", event.Target.Name, ) + m.targetCount++ + m.updateStatus(logger) } } return nil } + +func (m *MessageProcessor) updateStatus(logger logr.Logger) { + if err := updateTargetSourceStatus(m.ctx, m.client, m.targetSource, m.targetCount); err != nil { + logger.Error(err, "error updating TargetSource status") + } else { + logger.Info("updated target source status", + "targetCount", m.targetCount, + ) + } +} From 29c4974c83070f48779d4dfde8af89cc3e82f1d4 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 4 May 2026 09:16:42 -0600 Subject: [PATCH 125/153] removed owned targets from targetsource reconciliation --- internal/controller/targetsource_controller.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index f23a4a0..6d88b3d 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -249,7 +249,6 @@ func (r *TargetSourceReconciler) startDiscovery( func (r *TargetSourceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&gnmicv1alpha1.TargetSource{}). - Owns(&gnmicv1alpha1.Target{}). Named("targetsource"). Complete(r) } From 24cc376d1b09193839922b847c5003eb1bf0f9d5 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 4 May 2026 09:37:27 -0600 Subject: [PATCH 126/153] added predicate for targetsource reconciliation --- internal/controller/targetsource_controller.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 6d88b3d..16b1f2c 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -23,9 +23,11 @@ import ( "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/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery" @@ -248,7 +250,10 @@ func (r *TargetSourceReconciler) startDiscovery( // SetupWithManager sets up the controller with the Manager. func (r *TargetSourceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&gnmicv1alpha1.TargetSource{}). + For( + &gnmicv1alpha1.TargetSource{}, + builder.WithPredicates(predicate.GenerationChangedPredicate{}), + ). Named("targetsource"). Complete(r) } From 8da80521138dcd787aaabab6b7bf65bc0e4b067d Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 4 May 2026 09:37:42 -0600 Subject: [PATCH 127/153] changed updateStatus calling for event --- .../controller/discovery/message_processor.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/internal/controller/discovery/message_processor.go b/internal/controller/discovery/message_processor.go index af8da1f..314684a 100644 --- a/internal/controller/discovery/message_processor.go +++ b/internal/controller/discovery/message_processor.go @@ -223,7 +223,19 @@ func (m *MessageProcessor) processEvent(ctx context.Context, event core.Discover } // Apply events - return m.applyEvent(ctx, event, logger) + err := m.applyEvent(ctx, event, logger) + if err == nil { + switch event.Event { + case core.EventApply: + m.targetCount++ + m.updateStatus(logger) + case core.EventDelete: + m.targetCount-- + m.updateStatus(logger) + } + } + + return err } func (m *MessageProcessor) applySnapshot(ctx context.Context, snapshot *snapshotBuffer, logger logr.Logger) error { @@ -325,8 +337,6 @@ func (m *MessageProcessor) applyEvent(ctx context.Context, event core.DiscoveryE logger.Info("deleted target object", "name", event.Target.Name, ) - m.targetCount-- - m.updateStatus(logger) } case core.EventApply: target := generateTargetResource(event.Target, m.targetSource) @@ -339,8 +349,6 @@ func (m *MessageProcessor) applyEvent(ctx context.Context, event core.DiscoveryE logger.Info("applied target object", "name", event.Target.Name, ) - m.targetCount++ - m.updateStatus(logger) } } From f4d6bac190752ad488194cd4fa8f4745cb2d4c8d Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 4 May 2026 09:51:14 -0600 Subject: [PATCH 128/153] added comments to mapper.go --- internal/controller/discovery/mapper.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/internal/controller/discovery/mapper.go b/internal/controller/discovery/mapper.go index f34fe36..6cc28ae 100644 --- a/internal/controller/discovery/mapper.go +++ b/internal/controller/discovery/mapper.go @@ -1,8 +1,5 @@ package discovery -// This file makes diff between existing and new targets -// file decides which targets to create/update/delete - import ( "maps" "strings" @@ -13,7 +10,9 @@ import ( "github.com/gnmic/operator/internal/controller/discovery/core" ) +// generateTargetResource converts a DiscoveredTarget into a Kubernetes Target Object based on the TargetSource Spec func generateTargetResource(d core.DiscoveredTarget, ts *gnmicv1alpha1.TargetSource) *gnmicv1alpha1.Target { + // Create object instance t := &gnmicv1alpha1.Target{ ObjectMeta: metav1.ObjectMeta{ Name: d.Name, @@ -22,29 +21,35 @@ func generateTargetResource(d core.DiscoveredTarget, ts *gnmicv1alpha1.TargetSou }, } + // Add Address from DiscoveredTarget t.Spec.Address = d.Address + // Add default Target Profile from the TargetSource Spec TargetProfile t.Spec.Profile = ts.Spec.TargetProfile + // Copy TargetLabels from TargetSource Spec maps.Copy(t.Labels, ts.Spec.TargetLabels) + // Handle labels from Source of Truth for k, v := range d.Labels { if strings.HasPrefix(k, ExternalLabelPrefix) { switch k { - case ExternalLabelTargetProfile: + case ExternalLabelTargetProfile: // Overwrite TargetProfile if specified by SoT t.Spec.Profile = v default: - // handle unknown label + // TODO: handle unknown label } - } else { + } else { // Copy all other labels into the Target t.Labels[k] = v } } + // Add TargetSource Label to the Target (precedence over all labels) t.Labels[LabelTargetSourceName] = ts.Name return t } +// generateEvents returns a list of DiscoveryEvents. Needed for snapshot handling to determine which devices get deleted and which applied. func generateEvents(existing []gnmicv1alpha1.Target, discovered []core.DiscoveredTarget) []core.DiscoveryEvent { var events []core.DiscoveryEvent @@ -53,6 +58,7 @@ func generateEvents(existing []gnmicv1alpha1.Target, discovered []core.Discovere discoveredMap[d.Name] = d } + // Create delete events for targets which are present in existing but not in discovered for _, e := range existing { if _, found := discoveredMap[e.Name]; !found { events = append(events, core.DiscoveryEvent{ @@ -64,6 +70,7 @@ func generateEvents(existing []gnmicv1alpha1.Target, discovered []core.Discovere } } + // Create apply events for all targets in discovered for _, d := range discovered { events = append(events, core.DiscoveryEvent{ Target: d, From 41c1fec8ef46c89ae2b5e1a9f463e774a5d0283e Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 4 May 2026 09:59:48 -0600 Subject: [PATCH 129/153] added more comments --- internal/controller/discovery/mapper.go | 1 + internal/controller/discovery/message_processor.go | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/internal/controller/discovery/mapper.go b/internal/controller/discovery/mapper.go index 6cc28ae..36ce541 100644 --- a/internal/controller/discovery/mapper.go +++ b/internal/controller/discovery/mapper.go @@ -81,6 +81,7 @@ func generateEvents(existing []gnmicv1alpha1.Target, discovered []core.Discovere return events } +// normalizeTarget adds the prefix to the target name for identification in Kubernetes func normalizeTarget(t core.DiscoveredTarget, tsName string) core.DiscoveredTarget { t.Name = tsName + "-" + t.Name return t diff --git a/internal/controller/discovery/message_processor.go b/internal/controller/discovery/message_processor.go index 314684a..2899940 100644 --- a/internal/controller/discovery/message_processor.go +++ b/internal/controller/discovery/message_processor.go @@ -94,6 +94,7 @@ func (m *MessageProcessor) Run(ctx context.Context) error { return nil } +// processMessage handles all of the incoming messages from the channel func (m *MessageProcessor) processMessage(ctx context.Context, message core.DiscoveryMessage, logger logr.Logger) error { if err := ctx.Err(); err != nil { return err @@ -215,6 +216,7 @@ func (m *MessageProcessor) collectSnapshot(chunk core.DiscoverySnapshot, logger return nil } +// processEvent handles a single DiscoveryEvent message. If a snapshot is in the queue, the events get deferred and applied after. func (m *MessageProcessor) processEvent(ctx context.Context, event core.DiscoveryEvent, logger logr.Logger) error { // If snapshot collecting is active defer events if m.activeSnapshot != nil { @@ -238,6 +240,7 @@ func (m *MessageProcessor) processEvent(ctx context.Context, event core.Discover return err } +// applySnapshot is in charge of getting the Events for the discovered targets and applying them through applyEvent func (m *MessageProcessor) applySnapshot(ctx context.Context, snapshot *snapshotBuffer, logger logr.Logger) error { select { case <-ctx.Done(): @@ -318,6 +321,7 @@ func (m *MessageProcessor) applySnapshot(ctx context.Context, snapshot *snapshot } } + // Because of idempotency, allTargets = desired state = targets existing in Kubernetes. Overwrites the counter to "reset" it. m.targetCount = int32(len(allTargets)) m.updateStatus(logger) From 4a469085c184ff03bd7785a846ce5ec8d41287e7 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 4 May 2026 10:01:55 -0600 Subject: [PATCH 130/153] added initial targetCount fetch to deal with process restarts --- internal/controller/discovery/message_processor.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/controller/discovery/message_processor.go b/internal/controller/discovery/message_processor.go index 2899940..9d26be6 100644 --- a/internal/controller/discovery/message_processor.go +++ b/internal/controller/discovery/message_processor.go @@ -57,6 +57,12 @@ func (m *MessageProcessor) Run(ctx context.Context) error { logger.Info("Message processor started") + if existing, err := fetchExistingTargets(m.ctx, m.client, m.targetSource); err != nil { + logger.Error(err, "error fetching existing targets") + } else { + m.targetCount = int32(len(existing)) + } + for m.ctx.Err() == nil { select { case batch, ok := <-m.in: From a0b55b3278ff0edfba282109163cd0baae18bb65 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 4 May 2026 10:02:09 -0600 Subject: [PATCH 131/153] added comment --- internal/controller/discovery/message_processor.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/controller/discovery/message_processor.go b/internal/controller/discovery/message_processor.go index 9d26be6..9f8aa04 100644 --- a/internal/controller/discovery/message_processor.go +++ b/internal/controller/discovery/message_processor.go @@ -57,6 +57,7 @@ func (m *MessageProcessor) Run(ctx context.Context) error { logger.Info("Message processor started") + // Update internal counter in case of a process restart if existing, err := fetchExistingTargets(m.ctx, m.client, m.targetSource); err != nil { logger.Error(err, "error fetching existing targets") } else { From 426e27ae1e39a33a963d6e24ea25362b56683f6f Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Mon, 4 May 2026 18:15:49 +0000 Subject: [PATCH 132/153] fix: use defined variable --- internal/controller/targetsource_controller.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 8207103..c65e254 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -186,10 +186,7 @@ func (r *TargetSourceReconciler) startDiscovery( targetSource, targetChannel, ) - loader, loaderConfig, err := discovery.NewLoader(discoveryTypes.CommonLoaderConfig{ - TargetsourceNN: key, - ChunkSize: r.ChunkSize, - }, + loader, loaderConfig, err := discovery.NewLoader(loaderConfig, &targetSource.Spec, ) if err != nil { From d14b10cb8b2f6dfeb7ef5419c0b02e3d109d0f7e Mon Sep 17 00:00:00 2001 From: mcdillson Date: Mon, 4 May 2026 23:41:51 +0000 Subject: [PATCH 133/153] fixed cluster variable for netbox deployment --- netbox.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox.mk b/netbox.mk index 555c2f1..a133554 100644 --- a/netbox.mk +++ b/netbox.mk @@ -23,7 +23,7 @@ ifndef NETBOX_CLUSTER_NAME $(error NETBOX_CLUSTER_NAME is required. Usage: make netbox-deploy-cluster NETBOX_CLUSTER_NAME=cluster-name) endif kind get clusters | grep -q "$(NETBOX_CLUSTER_NAME)" || kind create cluster --name $(NETBOX_CLUSTER_NAME) - kubectl config use-context kind-$(CLUSTER_NAME) + kubectl config use-context kind-$(NETBOX_CLUSTER_NAME) .PHONY: netbox-undeploy netbox-undeploy: ## Undeploy the netbox cluster From f04c13024ee9f36f77e3586fcecb578d610ead41 Mon Sep 17 00:00:00 2001 From: mcdillson Date: Mon, 4 May 2026 23:42:36 +0000 Subject: [PATCH 134/153] added netbox integration test setup --- test.mk | 23 +++++++++++ .../netbox/initializers/device-roles.yaml | 3 ++ .../netbox/initializers/device-types.yaml | 16 ++++++++ .../netbox/initializers/devices.yaml | 36 +++++++++++++++++ .../netbox/initializers/interfaces.yaml | 40 +++++++++++++++++++ .../netbox/initializers/ip-addresses.yaml | 24 +++++++++++ .../netbox/initializers/manufacturers.yaml | 4 ++ .../netbox/initializers/sites.yaml | 2 + test/integration/t2.clab.yaml | 28 +++++++++++++ 9 files changed, 176 insertions(+) create mode 100644 test/integration/netbox/initializers/device-roles.yaml create mode 100644 test/integration/netbox/initializers/device-types.yaml create mode 100644 test/integration/netbox/initializers/devices.yaml create mode 100644 test/integration/netbox/initializers/interfaces.yaml create mode 100644 test/integration/netbox/initializers/ip-addresses.yaml create mode 100644 test/integration/netbox/initializers/manufacturers.yaml create mode 100644 test/integration/netbox/initializers/sites.yaml create mode 100644 test/integration/t2.clab.yaml diff --git a/test.mk b/test.mk index 3497c2b..e998a6a 100644 --- a/test.mk +++ b/test.mk @@ -85,6 +85,29 @@ deploy-test-topology: ## Deploy a test topology for testing undeploy-test-topology: ## Undeploy a test topology for testing sudo containerlab destroy -t test/integration/t1.clab.yaml -c +.PHONY: deploy-test-netbox-instance +deploy-test-netbox-instance: + $(MAKE) netbox-setup \ + NETBOX_CLUSTER_NAME=test-kind \ + NETBOX_PASSWORD=Netbox123 + +.PHONY: deploy-test-netbox-instance +deploy-test-netbox-topology: + sudo containerlab deploy -t test/integration/t2.clab.yaml -c + kubectl port-forward svc/netbox 8082:80 -n netbox --context kind-test-kind --address=0.0.0.0 >/dev/null 2>&1 & + +.PHONY: sync-netbox-test-data +sync-netbox-test-data: + $(MAKE) netbox-sync-data \ + NETBOX_CLUSTER_NAME=test-kind \ + NETBOX_URL=http://localhost:8082 \ + NETBOX_INIT=test/integration/netbox/initializers + +.PHONY: undeploy-test-netbox-instance +undeploy-test-netbox-instance: + $(MAKE) netbox-delete \ + NETBOX_CLUSTER_NAME=test-kind + .PHONY: apply-test-targets apply-test-targets: ## Apply the test targets for testing kubectl apply -f test/integration/resources/targets/profile diff --git a/test/integration/netbox/initializers/device-roles.yaml b/test/integration/netbox/initializers/device-roles.yaml new file mode 100644 index 0000000..9167dab --- /dev/null +++ b/test/integration/netbox/initializers/device-roles.yaml @@ -0,0 +1,3 @@ +- name: Router + slug: router + color: ff0000 diff --git a/test/integration/netbox/initializers/device-types.yaml b/test/integration/netbox/initializers/device-types.yaml new file mode 100644 index 0000000..a6279ed --- /dev/null +++ b/test/integration/netbox/initializers/device-types.yaml @@ -0,0 +1,16 @@ +- model: ixr-d2l + slug: arista-ixr-d2l + manufacturer: + name: Arista +- model: ixr-d2l + slug: nokia-ixr-d2l + manufacturer: + name: Nokia +- model: ixr-d2l-leaf + slug: nokia-ixr-d2l-leaf + manufacturer: + name: Nokia +- model: ixr-d3l + slug: nokia-ixr-d3l + manufacturer: + name: Nokia diff --git a/test/integration/netbox/initializers/devices.yaml b/test/integration/netbox/initializers/devices.yaml new file mode 100644 index 0000000..17ed036 --- /dev/null +++ b/test/integration/netbox/initializers/devices.yaml @@ -0,0 +1,36 @@ +- name: ceos1 + role: + slug: router + manufacturer: + name: Arista + device_type: + slug: arista-ixr-d2l + site: + name: Lab +- name: leaf1 + role: + slug: router + manufacturer: + name: Nokia + device_type: + slug: nokia-ixr-d2l + site: + name: Lab +- name: leaf2 + role: + slug: router + manufacturer: + name: Nokia + device_type: + slug: nokia-ixr-d2l-leaf + site: + name: Lab +- name: spine1 + role: + slug: router + manufacturer: + name: Nokia + device_type: + slug: nokia-ixr-d3l + site: + name: Lab diff --git a/test/integration/netbox/initializers/interfaces.yaml b/test/integration/netbox/initializers/interfaces.yaml new file mode 100644 index 0000000..05e8d24 --- /dev/null +++ b/test/integration/netbox/initializers/interfaces.yaml @@ -0,0 +1,40 @@ +- device: + name: spine1 + name: e1-1 + type: 1000base-t +- device: + name: leaf1 + name: e1-49 + type: 1000base-t +- device: + name: spine1 + name: e1-2 + type: 1000base-t +- device: + name: leaf2 + name: e1-49 + type: 1000base-t +- device: + name: spine1 + name: e1-3 + type: 1000base-t +- device: + name: ceos1 + name: eth1 + type: 1000base-t +- device: + name: spine1 + name: mgmt0 + type: 1000base-t +- device: + name: leaf1 + name: mgmt0 + type: 1000base-t +- device: + name: leaf2 + name: mgmt0 + type: 1000base-t +- device: + name: ceos1 + name: mgmt0 + type: 1000base-t diff --git a/test/integration/netbox/initializers/ip-addresses.yaml b/test/integration/netbox/initializers/ip-addresses.yaml new file mode 100644 index 0000000..de95cc8 --- /dev/null +++ b/test/integration/netbox/initializers/ip-addresses.yaml @@ -0,0 +1,24 @@ +- address: 172.18.1.10/32 + assigned_object: + device: + name: spine1 + name: mgmt0 + status: active + primary: true + dns_name: t2-nodes-spine1 +- address: 172.18.1.11/32 + assigned_object: + device: + name: leaf1 + name: mgmt0 + status: active + primary: true + dns_name: t2-nodes-leaf1 +- address: 172.18.1.12/32 + assigned_object: + device: + name: leaf2 + name: mgmt0 + status: active + primary: true + dns_name: t2-nodes-leaf2 diff --git a/test/integration/netbox/initializers/manufacturers.yaml b/test/integration/netbox/initializers/manufacturers.yaml new file mode 100644 index 0000000..68627af --- /dev/null +++ b/test/integration/netbox/initializers/manufacturers.yaml @@ -0,0 +1,4 @@ +- name: Nokia + slug: nokia +- name: Arista + slug: arista diff --git a/test/integration/netbox/initializers/sites.yaml b/test/integration/netbox/initializers/sites.yaml new file mode 100644 index 0000000..bc8ed18 --- /dev/null +++ b/test/integration/netbox/initializers/sites.yaml @@ -0,0 +1,2 @@ +- name: Lab + slug: lab diff --git a/test/integration/t2.clab.yaml b/test/integration/t2.clab.yaml new file mode 100644 index 0000000..f79f63c --- /dev/null +++ b/test/integration/t2.clab.yaml @@ -0,0 +1,28 @@ +name: t2 + +mgmt: + network: kind + +topology: + defaults: + kind: nokia_srlinux + image: ghcr.io/nokia/srlinux:25.10.1 + + kinds: + nokia_srlinux: + image: ghcr.io/nokia/srlinux:25.10.1 + type: ixr-d2l + + nodes: + spine1: + type: ixr-d3l + mgmt-ipv4: 172.18.1.10 + + leaf1: + mgmt-ipv4: 172.18.1.11 + leaf2: + mgmt-ipv4: 172.18.1.12 + + links: + - endpoints: ["spine1:e1-1", "leaf1:e1-49"] + - endpoints: ["spine1:e1-2", "leaf2:e1-49"] From 765c6edfcd277f9ee17c8f2c65405837bcabdaa5 Mon Sep 17 00:00:00 2001 From: mcdillson Date: Mon, 4 May 2026 23:56:04 +0000 Subject: [PATCH 135/153] renamed sync-test-netbox-data --- test.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.mk b/test.mk index e998a6a..a43da30 100644 --- a/test.mk +++ b/test.mk @@ -97,7 +97,7 @@ deploy-test-netbox-topology: kubectl port-forward svc/netbox 8082:80 -n netbox --context kind-test-kind --address=0.0.0.0 >/dev/null 2>&1 & .PHONY: sync-netbox-test-data -sync-netbox-test-data: +sync-test-netbox-data: $(MAKE) netbox-sync-data \ NETBOX_CLUSTER_NAME=test-kind \ NETBOX_URL=http://localhost:8082 \ From 935c49f051a8ed563f8f83e4241cee9caedcb0ed Mon Sep 17 00:00:00 2001 From: mcdillson Date: Mon, 4 May 2026 23:57:29 +0000 Subject: [PATCH 136/153] moved netbox clab topology into netbox folder --- test/integration/{t2.clab.yaml => netbox/netbox.clab.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/integration/{t2.clab.yaml => netbox/netbox.clab.yaml} (100%) diff --git a/test/integration/t2.clab.yaml b/test/integration/netbox/netbox.clab.yaml similarity index 100% rename from test/integration/t2.clab.yaml rename to test/integration/netbox/netbox.clab.yaml From 5fb77005116167e9a691823bd7c36a0b939b17f2 Mon Sep 17 00:00:00 2001 From: mcdillson Date: Mon, 4 May 2026 23:58:50 +0000 Subject: [PATCH 137/153] added targetsource integration resource --- test.mk | 4 ++++ test/integration/resources/targetsources/netbox.yaml | 11 +++++++++++ 2 files changed, 15 insertions(+) create mode 100644 test/integration/resources/targetsources/netbox.yaml diff --git a/test.mk b/test.mk index a43da30..c064825 100644 --- a/test.mk +++ b/test.mk @@ -108,6 +108,10 @@ undeploy-test-netbox-instance: $(MAKE) netbox-delete \ NETBOX_CLUSTER_NAME=test-kind +.PHONY apply-test-targetsources +apply-test-targetsources: + kubectl apply -f test/integration/resources/targetsources + .PHONY: apply-test-targets apply-test-targets: ## Apply the test targets for testing kubectl apply -f test/integration/resources/targets/profile diff --git a/test/integration/resources/targetsources/netbox.yaml b/test/integration/resources/targetsources/netbox.yaml new file mode 100644 index 0000000..0b8fd23 --- /dev/null +++ b/test/integration/resources/targetsources/netbox.yaml @@ -0,0 +1,11 @@ +apiVersion: operator.gnmic.dev/v1alpha1 +kind: TargetSource +metadata: + name: netbox-ts +spec: + provider: + http: + url: http://localhost:8082/targets + targetLabels: + integration-test: netbox + profile: netbox-default \ No newline at end of file From 1c349ae28ee27c502f6c4c4c8b70fa5699dd7629 Mon Sep 17 00:00:00 2001 From: mcdillson Date: Tue, 5 May 2026 00:01:50 +0000 Subject: [PATCH 138/153] added comments + fixed netbox test topology path --- test.mk | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/test.mk b/test.mk index c064825..1060623 100644 --- a/test.mk +++ b/test.mk @@ -86,30 +86,34 @@ undeploy-test-topology: ## Undeploy a test topology for testing sudo containerlab destroy -t test/integration/t1.clab.yaml -c .PHONY: deploy-test-netbox-instance -deploy-test-netbox-instance: +deploy-test-netbox-instance: ## Deploy the test netbox instance for testing $(MAKE) netbox-setup \ NETBOX_CLUSTER_NAME=test-kind \ NETBOX_PASSWORD=Netbox123 .PHONY: deploy-test-netbox-instance -deploy-test-netbox-topology: - sudo containerlab deploy -t test/integration/t2.clab.yaml -c +deploy-test-netbox-topology: ## Deploy the netbox test topology for testing + sudo containerlab deploy -t test/integration/netbox/netbox.clab.yaml -c kubectl port-forward svc/netbox 8082:80 -n netbox --context kind-test-kind --address=0.0.0.0 >/dev/null 2>&1 & .PHONY: sync-netbox-test-data -sync-test-netbox-data: +sync-test-netbox-data: ## Sync the netbox instance with the test topology for testing $(MAKE) netbox-sync-data \ NETBOX_CLUSTER_NAME=test-kind \ NETBOX_URL=http://localhost:8082 \ NETBOX_INIT=test/integration/netbox/initializers .PHONY: undeploy-test-netbox-instance -undeploy-test-netbox-instance: +undeploy-test-netbox-instance: ## Undeploy the netbox instance from the test cluster $(MAKE) netbox-delete \ NETBOX_CLUSTER_NAME=test-kind +.PHONY: undeploy-test-netbox-topology +undeploy-test-netbox-topology: ## Undeploy the netbox test topology for testing + sudo containerlab destroy -t test/integration/netbox/netbox.clab.yaml -c + .PHONY apply-test-targetsources -apply-test-targetsources: +apply-test-targetsources: ## Apply the test targetsources for testing kubectl apply -f test/integration/resources/targetsources .PHONY: apply-test-targets From 012a6a5ed4d3db4f74c683306dfdfcdc3961fbbb Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 4 May 2026 18:47:33 -0600 Subject: [PATCH 139/153] fixed missing separator --- test.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.mk b/test.mk index 1060623..270f846 100644 --- a/test.mk +++ b/test.mk @@ -112,7 +112,7 @@ undeploy-test-netbox-instance: ## Undeploy the netbox instance from the test clu undeploy-test-netbox-topology: ## Undeploy the netbox test topology for testing sudo containerlab destroy -t test/integration/netbox/netbox.clab.yaml -c -.PHONY apply-test-targetsources +.PHONY: apply-test-targetsources apply-test-targetsources: ## Apply the test targetsources for testing kubectl apply -f test/integration/resources/targetsources From 09aaaa4127c135eab4baf64cca1a38a861a39c73 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 4 May 2026 18:49:00 -0600 Subject: [PATCH 140/153] fixed targetProfile key --- test/integration/resources/targetsources/netbox.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/resources/targetsources/netbox.yaml b/test/integration/resources/targetsources/netbox.yaml index 0b8fd23..39ab922 100644 --- a/test/integration/resources/targetsources/netbox.yaml +++ b/test/integration/resources/targetsources/netbox.yaml @@ -8,4 +8,4 @@ spec: url: http://localhost:8082/targets targetLabels: integration-test: netbox - profile: netbox-default \ No newline at end of file + targetProfile: netbox-default \ No newline at end of file From 4bcd01e644d3707a4342855b9ce632d9ab9273d3 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 4 May 2026 19:46:43 -0600 Subject: [PATCH 141/153] fixed name normalization --- internal/controller/discovery/message_processor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/controller/discovery/message_processor.go b/internal/controller/discovery/message_processor.go index 9f8aa04..a918ce2 100644 --- a/internal/controller/discovery/message_processor.go +++ b/internal/controller/discovery/message_processor.go @@ -119,7 +119,7 @@ func (m *MessageProcessor) processMessage(ctx context.Context, message core.Disc ) for i := range msg.Targets { - msg.Targets[i] = normalizeTarget(msg.Targets[i], m.targetSource.Namespace) + msg.Targets[i] = normalizeTarget(msg.Targets[i], m.targetSource.Name) } return m.processSnapshot(ctx, msg, logger) @@ -132,7 +132,7 @@ func (m *MessageProcessor) processMessage(ctx context.Context, message core.Disc "target", msg.Target.Name, ) - msg.Target = normalizeTarget(msg.Target, m.targetSource.Namespace) + msg.Target = normalizeTarget(msg.Target, m.targetSource.Name) return m.processEvent(ctx, msg, logger) default: From b9ab471f42d6348c83638e993b29809ec4bbf5d4 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Tue, 5 May 2026 08:27:45 -0600 Subject: [PATCH 142/153] eliminated recursive make calls --- test.mk | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/test.mk b/test.mk index 270f846..b2d550e 100644 --- a/test.mk +++ b/test.mk @@ -4,6 +4,8 @@ GNMIC_VERSION ?= 0.44.1 KUBECTL_VERSION ?= v1.31.0 TEST_CLUSTER_NAME ?= test-kind CERT_MANAGER_VERSION ?= v1.19.3 +NETBOX_TEST_PORT ?= 8082 + .PHONY: install-kubectl install-kubectl: ## Install kubectl if not present @@ -86,27 +88,24 @@ undeploy-test-topology: ## Undeploy a test topology for testing sudo containerlab destroy -t test/integration/t1.clab.yaml -c .PHONY: deploy-test-netbox-instance -deploy-test-netbox-instance: ## Deploy the test netbox instance for testing - $(MAKE) netbox-setup \ - NETBOX_CLUSTER_NAME=test-kind \ - NETBOX_PASSWORD=Netbox123 +deploy-test-netbox-instance: NETBOX_CLUSTER_NAME=$(TEST_CLUSTER_NAME) ## Deploy the test netbox instance for testing +deploy-test-netbox-instance: NETBOX_PASSWORD=Netbox123 +deploy-test-netbox-instance: netbox-setup .PHONY: deploy-test-netbox-instance deploy-test-netbox-topology: ## Deploy the netbox test topology for testing sudo containerlab deploy -t test/integration/netbox/netbox.clab.yaml -c - kubectl port-forward svc/netbox 8082:80 -n netbox --context kind-test-kind --address=0.0.0.0 >/dev/null 2>&1 & + kubectl port-forward svc/netbox $(NETBOX_TEST_PORT):80 -n netbox --context kind-$(TEST_CLUSTER_NAME) --address=0.0.0.0 >/dev/null 2>&1 & .PHONY: sync-netbox-test-data -sync-test-netbox-data: ## Sync the netbox instance with the test topology for testing - $(MAKE) netbox-sync-data \ - NETBOX_CLUSTER_NAME=test-kind \ - NETBOX_URL=http://localhost:8082 \ - NETBOX_INIT=test/integration/netbox/initializers +sync-test-netbox-data: NETBOX_CLUSTER_NAME=$(TEST_CLUSTER_NAME) ## Sync the netbox instance with the test topology for testing +sync-test-netbox-data: NETBOX_URL=http://localhost:$(NETBOX_TEST_PORT) +sync-test-netbox-data: NETBOX_INIT=test/integration/netbox/initializers +sync-test-netbox-data: netbox-sync-data .PHONY: undeploy-test-netbox-instance -undeploy-test-netbox-instance: ## Undeploy the netbox instance from the test cluster - $(MAKE) netbox-delete \ - NETBOX_CLUSTER_NAME=test-kind +undeploy-test-netbox-instance: NETBOX_CLUSTER_NAME=$(TEST_CLUSTER_NAME) ## Undeploy the netbox instance from the test cluster +undeploy-test-netbox-instance: netbox-delete .PHONY: undeploy-test-netbox-topology undeploy-test-netbox-topology: ## Undeploy the netbox test topology for testing From 1a6239af453bbf8a1d8b12365a531f22671fdbbc Mon Sep 17 00:00:00 2001 From: mcdillson Date: Tue, 5 May 2026 14:32:54 +0000 Subject: [PATCH 143/153] added recursive clab folder to gitignore --- .gitignore | 2 +- test/integration/netbox/netbox.clab.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 29d31af..ee83f89 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,4 @@ notes/ docs/public docs/resources/_gen/ docs/.hugo_build.lock -test/integration/clab-* \ No newline at end of file +test/integration/**/clab-* \ No newline at end of file diff --git a/test/integration/netbox/netbox.clab.yaml b/test/integration/netbox/netbox.clab.yaml index f79f63c..ddd1705 100644 --- a/test/integration/netbox/netbox.clab.yaml +++ b/test/integration/netbox/netbox.clab.yaml @@ -1,4 +1,4 @@ -name: t2 +name: netbox mgmt: network: kind From 4f879aa50cfd86f67ae87b88213ce8aafae53293 Mon Sep 17 00:00:00 2001 From: mcdillson Date: Tue, 5 May 2026 14:34:00 +0000 Subject: [PATCH 144/153] added resources for http static server test --- lab/dev/http/targets.json | 13 ++++++++++ .../integration/http/resources/configmap.yaml | 22 +++++++++++++++++ .../http/resources/deployment.yaml | 24 +++++++++++++++++++ test/integration/http/resources/service.yaml | 10 ++++++++ .../resources/targetsources/http.yaml | 11 +++++++++ 5 files changed, 80 insertions(+) create mode 100644 lab/dev/http/targets.json create mode 100644 test/integration/http/resources/configmap.yaml create mode 100644 test/integration/http/resources/deployment.yaml create mode 100644 test/integration/http/resources/service.yaml create mode 100644 test/integration/resources/targetsources/http.yaml diff --git a/lab/dev/http/targets.json b/lab/dev/http/targets.json new file mode 100644 index 0000000..882faae --- /dev/null +++ b/lab/dev/http/targets.json @@ -0,0 +1,13 @@ +[ + { + "address": "10.0.0.1:57000", + "name": "router1" + }, + { + "address": "10.0.0.2:57000", + "name": "router2", + "labels": { + "test": "asdf" + } + } +] \ No newline at end of file diff --git a/test/integration/http/resources/configmap.yaml b/test/integration/http/resources/configmap.yaml new file mode 100644 index 0000000..0e8b35c --- /dev/null +++ b/test/integration/http/resources/configmap.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: targets-config +data: + targets.json: | + [ + { + "address": "10.0.0.1:57000", + "name": "router1", + "labels": { + "label1": "test" + } + }, + { + "address": "10.0.0.2:57000", + "name": "router2", + "labels": { + "label2": "test2" + } + } + ] \ No newline at end of file diff --git a/test/integration/http/resources/deployment.yaml b/test/integration/http/resources/deployment.yaml new file mode 100644 index 0000000..3dc1f61 --- /dev/null +++ b/test/integration/http/resources/deployment.yaml @@ -0,0 +1,24 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: targets-server +spec: + replicas: 1 + selector: + matchLabels: + app: targets-server + template: + metadata: + labels: + app: targets-server + spec: + containers: + - name: nginx + image: nginx:alpine + volumeMounts: + - name: data + mountPath: /usr/share/nginx/html + volumes: + - name: data + configMap: + name: targets-config \ No newline at end of file diff --git a/test/integration/http/resources/service.yaml b/test/integration/http/resources/service.yaml new file mode 100644 index 0000000..03f0efa --- /dev/null +++ b/test/integration/http/resources/service.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: targets +spec: + selector: + app: targets-server + ports: + - port: 80 + targetPort: 80 \ No newline at end of file diff --git a/test/integration/resources/targetsources/http.yaml b/test/integration/resources/targetsources/http.yaml new file mode 100644 index 0000000..199fcf3 --- /dev/null +++ b/test/integration/resources/targetsources/http.yaml @@ -0,0 +1,11 @@ +apiVersion: operator.gnmic.dev/v1alpha1 +kind: TargetSource +metadata: + name: http-ts +spec: + provider: + http: + url: http://targets.default.svc/targets.json + targetLabels: + integration-test: http + targetProfile: http-default \ No newline at end of file From a53440605e5345b20c2182c67fe5e3b2d0ea32c8 Mon Sep 17 00:00:00 2001 From: mcdillson Date: Tue, 5 May 2026 14:38:25 +0000 Subject: [PATCH 145/153] added make targets for http testing pod --- test.mk | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test.mk b/test.mk index b2d550e..f92c4ec 100644 --- a/test.mk +++ b/test.mk @@ -87,6 +87,14 @@ deploy-test-topology: ## Deploy a test topology for testing undeploy-test-topology: ## Undeploy a test topology for testing sudo containerlab destroy -t test/integration/t1.clab.yaml -c +.PHONY: deploy-test-http-server +deploy-test-http-server: ## Deploy a test http pod with a static file inventory for testing + kubectl apply -f test/integration/http/resources/ + +.PHONY: undeploy-test-http-server +undeploy-test-http-server: ## Undeploy the http pod for testing + kubectl delete -f test/integration/http/resources/ + .PHONY: deploy-test-netbox-instance deploy-test-netbox-instance: NETBOX_CLUSTER_NAME=$(TEST_CLUSTER_NAME) ## Deploy the test netbox instance for testing deploy-test-netbox-instance: NETBOX_PASSWORD=Netbox123 From 9ff3ba1a2fdaea9630c0049c106b22bca5d91186 Mon Sep 17 00:00:00 2001 From: mcdillson Date: Fri, 8 May 2026 20:53:05 +0000 Subject: [PATCH 146/153] fixed make target name --- test.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.mk b/test.mk index f92c4ec..23c5983 100644 --- a/test.mk +++ b/test.mk @@ -100,7 +100,7 @@ deploy-test-netbox-instance: NETBOX_CLUSTER_NAME=$(TEST_CLUSTER_NAME) ## Deploy deploy-test-netbox-instance: NETBOX_PASSWORD=Netbox123 deploy-test-netbox-instance: netbox-setup -.PHONY: deploy-test-netbox-instance +.PHONY: deploy-test-netbox-topology deploy-test-netbox-topology: ## Deploy the netbox test topology for testing sudo containerlab deploy -t test/integration/netbox/netbox.clab.yaml -c kubectl port-forward svc/netbox $(NETBOX_TEST_PORT):80 -n netbox --context kind-$(TEST_CLUSTER_NAME) --address=0.0.0.0 >/dev/null 2>&1 & From 0031b5ac20d3f7597db061658691f3ada8d0005e Mon Sep 17 00:00:00 2001 From: mcdillson Date: Fri, 8 May 2026 20:54:10 +0000 Subject: [PATCH 147/153] added http targetsource integration test to Makefile --- Makefile | 3 ++- test.mk | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fdcc2b2..98c42e7 100644 --- a/Makefile +++ b/Makefile @@ -308,9 +308,10 @@ delete-targetsources-dev-lab: ## Delete the target sources for the development l ##@ Testing Lab .PHONY: run-integration-tests -run-integration-tests: docker-build undeploy-test-cluster deploy-test-cluster install-test-cluster-dependencies load-test-image deploy install-kubectl install-gnmic install-containerlab deploy-test-topology apply-test-resources +run-integration-tests: docker-build undeploy-test-cluster deploy-test-cluster install-test-cluster-dependencies load-test-image deploy deploy-test-http-server install-kubectl install-gnmic install-containerlab deploy-test-topology apply-test-resources kubectl wait --for=condition=Ready cluster --all --timeout=180s kubectl wait --for=condition=Ready pipeline --all --timeout=180s + kubectl wait --for=jsonpath='{.status.targetsCount}'=3 targetsource --all --timeout=180s kubectl wait --for=jsonpath='{.status.connectionState}'=READY target --all --timeout=180s kubectl get subscriptions -o yaml kubectl get outputs -o yaml diff --git a/test.mk b/test.mk index 23c5983..fb30c30 100644 --- a/test.mk +++ b/test.mk @@ -153,5 +153,5 @@ apply-test-clusters: ## Apply the test clusters for testing kubectl apply -f test/integration/resources/clusters .PHONY: apply-test-resources -apply-test-resources: apply-test-targets apply-test-subscriptions apply-test-outputs apply-test-pipelines apply-test-clusters +apply-test-resources: apply-test-targets apply-test-subscriptions apply-test-outputs apply-test-pipelines apply-test-clusters apply-test-targetsources From c3dc34ca86cb126a8f4bc70abdc850fcbba7c505 Mon Sep 17 00:00:00 2001 From: mcdillson Date: Fri, 8 May 2026 20:56:16 +0000 Subject: [PATCH 148/153] changed resource names + mapped target inventory to clab --- .../integration/http/resources/configmap.yaml | 24 +++++++++++++------ .../http/resources/deployment.yaml | 8 +++---- test/integration/http/resources/service.yaml | 4 ++-- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/test/integration/http/resources/configmap.yaml b/test/integration/http/resources/configmap.yaml index 0e8b35c..f017566 100644 --- a/test/integration/http/resources/configmap.yaml +++ b/test/integration/http/resources/configmap.yaml @@ -1,22 +1,32 @@ apiVersion: v1 kind: ConfigMap metadata: - name: targets-config + name: http-target-cfg data: targets.json: | [ { - "address": "10.0.0.1:57000", - "name": "router1", + "address": "clab-t1-spine1:57400", + "name": "spine1", "labels": { - "label1": "test" + "vendor": "nokia_srlinux", + "role": "spine" } }, { - "address": "10.0.0.2:57000", - "name": "router2", + "address": "clab-t1-leaf1:57400", + "name": "leaf1", "labels": { - "label2": "test2" + "vendor": "nokia_srlinux", + "role": "leaf" + } + }, + { + "address": "clab-t1-leaf2:57400", + "name": "leaf2", + "labels": { + "vendor": "nokia_srlinux", + "role": "leaf" } } ] \ No newline at end of file diff --git a/test/integration/http/resources/deployment.yaml b/test/integration/http/resources/deployment.yaml index 3dc1f61..785c1e3 100644 --- a/test/integration/http/resources/deployment.yaml +++ b/test/integration/http/resources/deployment.yaml @@ -1,16 +1,16 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: targets-server + name: http-target-inv spec: replicas: 1 selector: matchLabels: - app: targets-server + app: http-target-inv template: metadata: labels: - app: targets-server + app: http-target-inv spec: containers: - name: nginx @@ -21,4 +21,4 @@ spec: volumes: - name: data configMap: - name: targets-config \ No newline at end of file + name: http-target-cfg \ No newline at end of file diff --git a/test/integration/http/resources/service.yaml b/test/integration/http/resources/service.yaml index 03f0efa..d4be4e7 100644 --- a/test/integration/http/resources/service.yaml +++ b/test/integration/http/resources/service.yaml @@ -1,10 +1,10 @@ apiVersion: v1 kind: Service metadata: - name: targets + name: http-svc spec: selector: - app: targets-server + app: http-target-inv ports: - port: 80 targetPort: 80 \ No newline at end of file From efead83350041b874e39627d9e146ad83ef42f96 Mon Sep 17 00:00:00 2001 From: mcdillson Date: Fri, 8 May 2026 20:57:11 +0000 Subject: [PATCH 149/153] fixed http target url and profile --- test/integration/resources/targetsources/http.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/resources/targetsources/http.yaml b/test/integration/resources/targetsources/http.yaml index 199fcf3..422cfdc 100644 --- a/test/integration/resources/targetsources/http.yaml +++ b/test/integration/resources/targetsources/http.yaml @@ -5,7 +5,7 @@ metadata: spec: provider: http: - url: http://targets.default.svc/targets.json + url: http://http-svc.default.svc/targets.json targetLabels: - integration-test: http - targetProfile: http-default \ No newline at end of file + integrationtest: http + targetProfile: default \ No newline at end of file From 4d461a3cbaee9e4310288bf5ffa579b7b42a04ed Mon Sep 17 00:00:00 2001 From: mcdillson Date: Fri, 8 May 2026 20:57:37 +0000 Subject: [PATCH 150/153] removed netbox targetsource for integration test --- test/integration/resources/targetsources/netbox.yaml | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 test/integration/resources/targetsources/netbox.yaml diff --git a/test/integration/resources/targetsources/netbox.yaml b/test/integration/resources/targetsources/netbox.yaml deleted file mode 100644 index 39ab922..0000000 --- a/test/integration/resources/targetsources/netbox.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: operator.gnmic.dev/v1alpha1 -kind: TargetSource -metadata: - name: netbox-ts -spec: - provider: - http: - url: http://localhost:8082/targets - targetLabels: - integration-test: netbox - targetProfile: netbox-default \ No newline at end of file From f7c627a0b88ac15740e59a6ad154281b400bff26 Mon Sep 17 00:00:00 2001 From: mcdillson Date: Fri, 8 May 2026 20:58:09 +0000 Subject: [PATCH 151/153] mapped operator resources to new http test --- test/integration/resources/clusters/cluster1.yaml | 2 +- test/integration/resources/pipelines/pipeline1.yaml | 2 ++ test/integration/resources/pipelines/pipeline2.yaml | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/integration/resources/clusters/cluster1.yaml b/test/integration/resources/clusters/cluster1.yaml index 513b948..c01cc56 100644 --- a/test/integration/resources/clusters/cluster1.yaml +++ b/test/integration/resources/clusters/cluster1.yaml @@ -13,4 +13,4 @@ spec: memory: "500Mi" cpu: "1" targetDistribution: - podCapacity: 5 \ No newline at end of file + podCapacity: 10 \ No newline at end of file diff --git a/test/integration/resources/pipelines/pipeline1.yaml b/test/integration/resources/pipelines/pipeline1.yaml index 0dc67a3..82c0289 100644 --- a/test/integration/resources/pipelines/pipeline1.yaml +++ b/test/integration/resources/pipelines/pipeline1.yaml @@ -12,6 +12,8 @@ spec: - matchLabels: vendor: nokia_srlinux role: spine + - matchLabels: + operator.gnmic.dev/targetsource: http-ts subscriptionSelectors: - matchLabels: vendor: nokia_srlinux diff --git a/test/integration/resources/pipelines/pipeline2.yaml b/test/integration/resources/pipelines/pipeline2.yaml index 7420d7d..a361833 100644 --- a/test/integration/resources/pipelines/pipeline2.yaml +++ b/test/integration/resources/pipelines/pipeline2.yaml @@ -12,6 +12,8 @@ spec: - matchLabels: vendor: nokia_srlinux role: spine + - matchLabels: + operator.gnmic.dev/targetsource: http-ts subscriptionSelectors: - matchLabels: vendor: nokia_srlinux From f37cc4abc5a41018f8d5762c5faf7d1e2b839be8 Mon Sep 17 00:00:00 2001 From: mcdillson Date: Fri, 8 May 2026 21:00:08 +0000 Subject: [PATCH 152/153] removed netbox clab and changed address --- .../netbox/initializers/ip-addresses.yaml | 6 ++-- test/integration/netbox/netbox.clab.yaml | 28 ------------------- 2 files changed, 3 insertions(+), 31 deletions(-) delete mode 100644 test/integration/netbox/netbox.clab.yaml diff --git a/test/integration/netbox/initializers/ip-addresses.yaml b/test/integration/netbox/initializers/ip-addresses.yaml index de95cc8..73453a8 100644 --- a/test/integration/netbox/initializers/ip-addresses.yaml +++ b/test/integration/netbox/initializers/ip-addresses.yaml @@ -1,4 +1,4 @@ -- address: 172.18.1.10/32 +- address: clab-t1-spine1 assigned_object: device: name: spine1 @@ -6,7 +6,7 @@ status: active primary: true dns_name: t2-nodes-spine1 -- address: 172.18.1.11/32 +- address: clab-t1-leaf1 assigned_object: device: name: leaf1 @@ -14,7 +14,7 @@ status: active primary: true dns_name: t2-nodes-leaf1 -- address: 172.18.1.12/32 +- address: clab-t1-leaf2 assigned_object: device: name: leaf2 diff --git a/test/integration/netbox/netbox.clab.yaml b/test/integration/netbox/netbox.clab.yaml deleted file mode 100644 index ddd1705..0000000 --- a/test/integration/netbox/netbox.clab.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: netbox - -mgmt: - network: kind - -topology: - defaults: - kind: nokia_srlinux - image: ghcr.io/nokia/srlinux:25.10.1 - - kinds: - nokia_srlinux: - image: ghcr.io/nokia/srlinux:25.10.1 - type: ixr-d2l - - nodes: - spine1: - type: ixr-d3l - mgmt-ipv4: 172.18.1.10 - - leaf1: - mgmt-ipv4: 172.18.1.11 - leaf2: - mgmt-ipv4: 172.18.1.12 - - links: - - endpoints: ["spine1:e1-1", "leaf1:e1-49"] - - endpoints: ["spine1:e1-2", "leaf2:e1-49"] From 61ef95dc3d5fa92ce952f50ad8067f03d1373b0e Mon Sep 17 00:00:00 2001 From: mcdillson Date: Fri, 8 May 2026 21:23:56 +0000 Subject: [PATCH 153/153] generated manifests --- api/v1alpha1/zz_generated.deepcopy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3848412..61e81fd 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ limitations under the License. package v1alpha1 import ( - v1 "k8s.io/api/core/v1" + "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" )