From edcda75b79b19a0995294efcee7274cf9469e6a3 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 10 Apr 2026 12:54:33 +0000 Subject: [PATCH 01/31] added type for discovered targets --- internal/controller/targetsource/types.go | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 internal/controller/targetsource/types.go diff --git a/internal/controller/targetsource/types.go b/internal/controller/targetsource/types.go new file mode 100644 index 0000000..95cd421 --- /dev/null +++ b/internal/controller/targetsource/types.go @@ -0,0 +1,9 @@ +package targetsource + +// DiscoveredTarget represents a target discovered from an external source +// before it is materialized as a Kubernetes Target CR +type DiscoveredTarget struct { + Name string + Address string + Labels map[string]string +} From fb43e404c76704a04c8bff7b25af857fa8d519b0 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 10 Apr 2026 12:55:31 +0000 Subject: [PATCH 02/31] added target loader interface --- internal/controller/targetsource/loaders.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/internal/controller/targetsource/loaders.go b/internal/controller/targetsource/loaders.go index 817194d..d4f84e1 100644 --- a/internal/controller/targetsource/loaders.go +++ b/internal/controller/targetsource/loaders.go @@ -1,4 +1,20 @@ package targetsource -// This file defines the loader interface -// targets received from loaders are sent via channel to the controller for reconciliation +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<- []DiscoveredTarget, + ) error +} From e45f5ec36b81e96666efc1bd3c56ccd316c0938c Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 10 Apr 2026 12:56:00 +0000 Subject: [PATCH 03/31] added loader registry for dynamic target loader creation --- internal/controller/targetsource/loaders.go | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/internal/controller/targetsource/loaders.go b/internal/controller/targetsource/loaders.go index d4f84e1..309bf1a 100644 --- a/internal/controller/targetsource/loaders.go +++ b/internal/controller/targetsource/loaders.go @@ -2,6 +2,8 @@ package targetsource import ( "context" + "fmt" + "sync" ) // Loader defines a pluggable TargetSource loader interface @@ -18,3 +20,32 @@ type Loader interface { out chan<- []DiscoveredTarget, ) 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) (Loader, error) { + registryMu.RLock() + defer registryMu.RUnlock() + + factory, ok := registry[name] + if !ok { + return nil, fmt.Errorf("unknown targetsource loader: %q", name) + } + return factory(), nil +} From e28828367647b52c320de148019545d096ce7f7b Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 10 Apr 2026 13:56:26 +0000 Subject: [PATCH 04/31] added target_manager for target lifecycle management --- .../controller/targetsource/target_manager.go | 59 +++++++++++++++++++ internal/controller/targetsource/types.go | 9 +++ 2 files changed, 68 insertions(+) create mode 100644 internal/controller/targetsource/target_manager.go diff --git a/internal/controller/targetsource/target_manager.go b/internal/controller/targetsource/target_manager.go new file mode 100644 index 0000000..0992f7b --- /dev/null +++ b/internal/controller/targetsource/target_manager.go @@ -0,0 +1,59 @@ +package targetsource + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" +) + +// NewTargetManager wires a TargetManager instance. +func NewTargetManager(c client.Client, sourceName string, in <-chan []DiscoveredTarget) *TargetManager { + return &TargetManager{ + client: c, + targetsource: sourceName, + in: in, + } +} + +// Run is a long‑running loop that receives target snapshots +// and reconciles Target CRs accordingly +func (m *TargetManager) Run(ctx context.Context) error { + logger := log.FromContext(ctx). + WithValues("targetSource", m.targetsource) + + logger.Info("target manager started") + + for { + select { + case <-ctx.Done(): + logger.Info("target manager stopped") + return nil + + case targets := <-m.in: + logger.Info( + "received discovered targets", + "count", len(targets), + ) + + // 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/types.go b/internal/controller/targetsource/types.go index 95cd421..51f7468 100644 --- a/internal/controller/targetsource/types.go +++ b/internal/controller/targetsource/types.go @@ -1,5 +1,7 @@ package targetsource +import "sigs.k8s.io/controller-runtime/pkg/client" + // DiscoveredTarget represents a target discovered from an external source // before it is materialized as a Kubernetes Target CR type DiscoveredTarget struct { @@ -7,3 +9,10 @@ type DiscoveredTarget struct { Address string Labels map[string]string } + +// TargetManager consumes discovered targets and applies them to Kubernetes. +type TargetManager struct { + client client.Client + targetsource string + in <-chan []DiscoveredTarget +} From ce5f701f762bdb5baeca6f90f3218de0ee4a3195 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 10 Apr 2026 14:03:58 +0000 Subject: [PATCH 05/31] wire target_manager and loader together --- .../controller/targetsource_controller.go | 67 ++++++++++++++----- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 06e07ab..fb491fa 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -18,19 +18,26 @@ package controller import ( "context" + "sync" - "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" + "github.com/gnmic/operator/internal/controller/targetsource" ) +type runningSource struct { + cancel context.CancelFunc +} + // TargetSourceReconciler reconciles a TargetSource object type TargetSourceReconciler struct { client.Client - Scheme *runtime.Scheme + + mu sync.Mutex + running map[client.ObjectKey]runningSource } // +kubebuilder:rbac:groups=operator.gnmic.dev,resources=targetsources,verbs=get;list;watch;create;update;patch;delete @@ -50,26 +57,56 @@ 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 - // TODO: - // 1. Start go routines for loader of target source - // 2. Retrieve list of targets from go channel - // 3. Fetch existing Targets from Kubernetes API - // 4. Compare and determine which Targets to create/update/delete - // 5. Create/update/delete Target CRs accordingly - // 6. Update TargetSource status with sync results + // 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 + + r.mu.Lock() + _, exists := r.running[req.NamespacedName] + r.mu.Unlock() + + // If an targetsource loader exists, return immediately without starting + // any new loader or target manager + if exists { + return ctrl.Result{}, nil + } + + loader, err := targetsource.NewLoader("http_pull") // TODO: determine loader type from TargetSource spec + if err != nil { + return ctrl.Result{}, err + } + + runtimeCtx, cancel := context.WithCancel(context.Background()) + target_channel := make(chan []targetsource.DiscoveredTarget) + + // start loader + go loader.Start(runtimeCtx, targetSource.Name, target_channel) + + // start target manager + manager := targetsource.NewTargetManager( + r.Client, + targetSource.Name, + target_channel, + ) + go manager.Run(runtimeCtx) + + r.mu.Lock() + r.running[req.NamespacedName] = runningSource{cancel: cancel} + r.mu.Unlock() + + logger.Info("TargetSource pipeline started", "name", targetSource.Name) return ctrl.Result{}, nil } // 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{}). Named("targetsource"). From bee64d79117da96e1552119844804a0fb30ad2e3 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 10 Apr 2026 14:18:53 +0000 Subject: [PATCH 06/31] add scheme back to the reconciler --- internal/controller/targetsource_controller.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index fb491fa..249f411 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -20,6 +20,7 @@ import ( "context" "sync" + "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" @@ -35,6 +36,7 @@ type runningSource struct { // TargetSourceReconciler reconciles a TargetSource object type TargetSourceReconciler struct { client.Client + Scheme *runtime.Scheme mu sync.Mutex running map[client.ObjectKey]runningSource From 09fd1eff2ea771db3da9532a08756d9ca3c722e0 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Mon, 13 Apr 2026 12:03:26 +0000 Subject: [PATCH 07/31] add example return output for a poc --- .../targetsource/loaders/http_pull/loader.go | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/internal/controller/targetsource/loaders/http_pull/loader.go b/internal/controller/targetsource/loaders/http_pull/loader.go index 6185fe6..58efba4 100644 --- a/internal/controller/targetsource/loaders/http_pull/loader.go +++ b/internal/controller/targetsource/loaders/http_pull/loader.go @@ -1,3 +1,74 @@ package http_pull -// this file implements the logic to load targets from HTTP endpoint +import ( + "context" + "time" + + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/gnmic/operator/internal/controller/targetsource" +) + +type Loader struct{} + +// New instantiates the http_pull loader +func New() targetsource.Loader { + return &Loader{} +} + +func (l *Loader) Name() string { + return "http_pull" +} + +func (l *Loader) Start( + ctx context.Context, + targetsourceName string, + out chan<- []targetsource.DiscoveredTarget, +) error { + logger := log.FromContext(ctx).WithValues("loader", l.Name()) + + logger.Info("HTTP pull loader started") + + // Only for debugging: emit a static snapshot every 30 seconds + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + logger.Info("HTTP pull loader stopped") + return nil + + case <-ticker.C: + // Example snapshot (placeholder) + targets := []targetsource.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}, + }, + } + + // 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 + } + } + } +} + +func init() { + targetsource.Register("http_pull", New) +} From 5eb0088f77116f0f93ea987a3e8352645d3152b1 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Mon, 13 Apr 2026 12:09:22 +0000 Subject: [PATCH 08/31] comment templated code --- .../controller/targetsource/target_manager.go | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/internal/controller/targetsource/target_manager.go b/internal/controller/targetsource/target_manager.go index 0992f7b..e6b20b9 100644 --- a/internal/controller/targetsource/target_manager.go +++ b/internal/controller/targetsource/target_manager.go @@ -5,8 +5,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" - - gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" ) // NewTargetManager wires a TargetManager instance. @@ -39,16 +37,16 @@ func (m *TargetManager) Run(ctx context.Context) error { ) // 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 - } + // 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 From 91f4985180869732c401aa0803ed55e3cf27ddd9 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Mon, 13 Apr 2026 13:13:56 +0000 Subject: [PATCH 09/31] register loaders --- internal/controller/targetsource/loaders/all/all.go | 6 ++++++ internal/controller/targetsource_controller.go | 1 + 2 files changed, 7 insertions(+) create mode 100644 internal/controller/targetsource/loaders/all/all.go diff --git a/internal/controller/targetsource/loaders/all/all.go b/internal/controller/targetsource/loaders/all/all.go new file mode 100644 index 0000000..629c5d9 --- /dev/null +++ b/internal/controller/targetsource/loaders/all/all.go @@ -0,0 +1,6 @@ +package all + +import ( + _ "github.com/gnmic/operator/internal/controller/targetsource/loaders/http_pull" + // _ "github.com/gnmic/operator/internal/controller/targetsource/loaders/http_push" +) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 249f411..1120a18 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/loaders/all" ) type runningSource struct { From 3e1620a1fcaaa8924398df8b92db560d9b63361f Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Mon, 13 Apr 2026 13:28:38 +0000 Subject: [PATCH 10/31] makefile support for applying targetsources --- Makefile | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e988a43..fdcc2b2 100644 --- a/Makefile +++ b/Makefile @@ -251,10 +251,10 @@ configure-nodes-dev-lab: ## Configure the nodes in the development lab cluster gnmic -a clab-3-nodes-leaf2:57400 -u $(TARGET_USERNAME) -p $(TARGET_PASSWORD) --skip-verify set --request-file lab/dev/configs/leaf2.yaml .PHONY: apply-resources-dev-lab -apply-resources-dev-lab: apply-targets-dev-lab apply-subscriptions-dev-lab apply-outputs-dev-lab apply-pipelines-dev-lab apply-clusters-dev-lab ## Apply the resources for the development lab cluster +apply-resources-dev-lab: apply-targets-dev-lab apply-subscriptions-dev-lab apply-outputs-dev-lab apply-pipelines-dev-lab apply-clusters-dev-lab apply-targetsources-dev-lab ## Apply the resources for the development lab cluster .PHONY: delete-resources-dev-lab -delete-resources-dev-lab: delete-clusters-dev-lab delete-targets-dev-lab delete-subscriptions-dev-lab delete-outputs-dev-lab delete-pipelines-dev-lab ## Delete the resources for the development lab cluster +delete-resources-dev-lab: delete-clusters-dev-lab delete-targets-dev-lab delete-subscriptions-dev-lab delete-outputs-dev-lab delete-pipelines-dev-lab delete-targetsources-dev-lab ## Delete the resources for the development lab cluster .PHONY: apply-targets-dev-lab apply-targets-dev-lab: ## Apply the targets for the development lab cluster @@ -288,6 +288,7 @@ apply-pipelines-dev-lab: ## Apply the pipelines for the development lab cluster .PHONY: delete-pipelines-dev-lab delete-pipelines-dev-lab: ## Delete the pipelines for the development lab cluster kubectl delete -f lab/dev/resources/pipelines + .PHONY: apply-clusters-dev-lab apply-clusters-dev-lab: ## Apply the clusters for the development lab cluster kubectl apply -f lab/dev/resources/clusters @@ -296,6 +297,14 @@ apply-clusters-dev-lab: ## Apply the clusters for the development lab cluster delete-clusters-dev-lab: ## Delete the clusters for the development lab cluster kubectl delete -f lab/dev/resources/clusters +.PHONY: apply-targetsources-dev-lab +apply-targetsources-dev-lab: ## Apply the target sources for the development lab cluster + kubectl apply -f lab/dev/resources/targetsources + +.PHONY: delete-targetsources-dev-lab +delete-targetsources-dev-lab: ## Delete the target sources for the development lab cluster + kubectl delete -f lab/dev/resources/targetsources + ##@ Testing Lab .PHONY: run-integration-tests From 9fe3b10f5d223e4f9665881fd0595e87f63ad768 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Mon, 13 Apr 2026 14:16:33 +0000 Subject: [PATCH 11/31] add type for targetsourcespec --- api/v1alpha1/targetsource_types.go | 2 ++ api/v1alpha1/zz_generated.deepcopy.go | 5 +++++ config/crd/bases/operator.gnmic.dev_targetsources.yaml | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 99da5d5..037b581 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -22,12 +22,14 @@ import ( // TargetSourceSpec defines the desired state of TargetSource type TargetSourceSpec struct { + HTTPPull *HTTPConfig `json:"http_pull,omitempty"` HTTP *HTTPConfig `json:"http,omitempty"` Consul *ConsulConfig `json:"consul,omitempty"` ConfigMap string `json:"configMap,omitempty"` PodSelector metav1.LabelSelector `json:"podSelector,omitempty"` ServiceSelector metav1.LabelSelector `json:"serviceSelector,omitempty"` // + Type string `json:"type,omitempty"` Labels map[string]string `json:"labels,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 07b0239..3e19bde 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1262,6 +1262,11 @@ func (in *TargetSourceList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TargetSourceSpec) DeepCopyInto(out *TargetSourceSpec) { *out = *in + if in.HTTPPull != nil { + in, out := &in.HTTPPull, &out.HTTPPull + *out = new(HTTPConfig) + **out = **in + } if in.HTTP != nil { in, out := &in.HTTP, &out.HTTP *out = new(HTTPConfig) diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 789ff3f..3070c0c 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -51,6 +51,11 @@ spec: url: type: string type: object + http_pull: + properties: + url: + type: string + type: object labels: additionalProperties: type: string @@ -153,6 +158,8 @@ spec: type: object type: object x-kubernetes-map-type: atomic + type: + type: string type: object status: description: TargetSourceStatus defines the observed state of TargetSource From 8ab89e2cf75ce1c65bccd223b282ff0b25841357 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Mon, 13 Apr 2026 14:16:59 +0000 Subject: [PATCH 12/31] determine targetsource loader based on spec --- internal/controller/targetsource_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 1120a18..96d7c91 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -78,7 +78,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, nil } - loader, err := targetsource.NewLoader("http_pull") // TODO: determine loader type from TargetSource spec + loader, err := targetsource.NewLoader(targetSource.Spec.Type) // TODO: pass configuration to loader based on spec if err != nil { return ctrl.Result{}, err } From ccaffc15fbbbdec5a4c13b48134e52d1e5d2b667 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Mon, 13 Apr 2026 14:17:48 +0000 Subject: [PATCH 13/31] add type for custom TargetSource CR --- lab/dev/resources/targetsources/ctest1.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 lab/dev/resources/targetsources/ctest1.yaml diff --git a/lab/dev/resources/targetsources/ctest1.yaml b/lab/dev/resources/targetsources/ctest1.yaml new file mode 100644 index 0000000..2360603 --- /dev/null +++ b/lab/dev/resources/targetsources/ctest1.yaml @@ -0,0 +1,10 @@ +apiVersion: operator.gnmic.dev/v1alpha1 +kind: TargetSource +metadata: + name: http-discovery +spec: + http: + url: http://inventory-service:8080/targets + labels: + source: inventory + type: http_pull \ No newline at end of file From 05a03eb06d495f5ef0ede18b173db2324eb2d65b Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Tue, 14 Apr 2026 16:28:23 -0600 Subject: [PATCH 14/31] changed struct layout to use provider section and made fields obligatory --- api/v1alpha1/targetsource_types.go | 16 ++- api/v1alpha1/zz_generated.deepcopy.go | 40 ++++-- .../operator.gnmic.dev_targetsources.yaml | 122 +++--------------- 3 files changed, 56 insertions(+), 122 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 99da5d5..d2a977c 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -21,14 +21,20 @@ import ( ) // TargetSourceSpec defines the desired state of TargetSource +// +kubebuilder:validation:Required type TargetSourceSpec struct { - HTTP *HTTPConfig `json:"http,omitempty"` - Consul *ConsulConfig `json:"consul,omitempty"` - ConfigMap string `json:"configMap,omitempty"` - PodSelector metav1.LabelSelector `json:"podSelector,omitempty"` - ServiceSelector metav1.LabelSelector `json:"serviceSelector,omitempty"` + Provider *ProviderSpec `json:"provider"` // Labels map[string]string `json:"labels,omitempty"` + + // +kubebuilder:validation:MinLength=1 + Profile string `json:"profile"` +} + +// +kubebuilder:validation:MaxProperties=1 +type ProviderSpec struct { + HTTP *HTTPConfig `json:"http,omitempty"` + Consul *ConsulConfig `json:"consul,omitempty"` } type HTTPConfig struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 07b0239..8d9e746 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -818,6 +818,31 @@ func (in *ProcessorStatus) DeepCopy() *ProcessorStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) { + *out = *in + if in.HTTP != nil { + in, out := &in.HTTP, &out.HTTP + *out = new(HTTPConfig) + **out = **in + } + if in.Consul != nil { + in, out := &in.Consul, &out.Consul + *out = new(ConsulConfig) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderSpec. +func (in *ProviderSpec) DeepCopy() *ProviderSpec { + if in == nil { + return nil + } + out := new(ProviderSpec) + 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 @@ -1262,18 +1287,11 @@ func (in *TargetSourceList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TargetSourceSpec) DeepCopyInto(out *TargetSourceSpec) { *out = *in - if in.HTTP != nil { - in, out := &in.HTTP, &out.HTTP - *out = new(HTTPConfig) - **out = **in - } - if in.Consul != nil { - in, out := &in.Consul, &out.Consul - *out = new(ConsulConfig) - **out = **in + if in.Provider != nil { + in, out := &in.Provider, &out.Provider + *out = new(ProviderSpec) + (*in).DeepCopyInto(*out) } - in.PodSelector.DeepCopyInto(&out.PodSelector) - in.ServiceSelector.DeepCopyInto(&out.ServiceSelector) if in.Labels != nil { in, out := &in.Labels, &out.Labels *out = make(map[string]string, len(*in)) diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 789ff3f..1212ff9 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -39,120 +39,30 @@ spec: spec: description: TargetSourceSpec defines the desired state of TargetSource properties: - configMap: - type: string - consul: - properties: - url: - type: string - type: object - http: - properties: - url: - type: string - type: object labels: additionalProperties: type: string type: object - podSelector: - description: |- - A label selector is a label query over a set of resources. The result of matchLabels and - matchExpressions are ANDed. An empty label selector matches all objects. A null - label selector matches no objects. + profile: + minLength: 1 + type: string + provider: + maxProperties: 1 properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. - The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + consul: + properties: + url: + type: string type: object - type: object - x-kubernetes-map-type: atomic - serviceSelector: - description: |- - A label selector is a label query over a set of resources. The result of matchLabels and - matchExpressions are ANDed. An empty label selector matches all objects. A null - label selector matches no objects. - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. - The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + http: + properties: + url: + type: string type: object type: object - x-kubernetes-map-type: atomic + required: + - profile + - provider type: object status: description: TargetSourceStatus defines the observed state of TargetSource From d7750dc4e7f4337201d3e0fd84017543a719cb60 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 13 Apr 2026 14:46:40 -0600 Subject: [PATCH 15/31] 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 96d7c91..cce4862 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 4cab2ba106e72199d78f052ebeea0329d77f6d6c Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 13 Apr 2026 15:14:06 -0600 Subject: [PATCH 16/31] 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 69813161914dbe70bd77b05606a0754e8a6627e0 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 13 Apr 2026 15:16:45 -0600 Subject: [PATCH 17/31] 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 cce4862..6ade566 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 be4514a9718354b6b2486e0583346fd72c62b158 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Mon, 13 Apr 2026 15:20:15 -0600 Subject: [PATCH 18/31] 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 47b934231b16b0c92392775397a36b9406fd7051 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Tue, 14 Apr 2026 16:28:23 -0600 Subject: [PATCH 19/31] changed struct layout to use provider section and made fields obligatory --- api/v1alpha1/targetsource_types.go | 17 ++- api/v1alpha1/zz_generated.deepcopy.go | 47 ++++--- .../operator.gnmic.dev_targetsources.yaml | 129 +++--------------- 3 files changed, 57 insertions(+), 136 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 037b581..3cf029b 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -21,16 +21,21 @@ import ( ) // TargetSourceSpec defines the desired state of TargetSource +// +kubebuilder:validation:Required type TargetSourceSpec struct { - HTTPPull *HTTPConfig `json:"http_pull,omitempty"` - HTTP *HTTPConfig `json:"http,omitempty"` - Consul *ConsulConfig `json:"consul,omitempty"` - ConfigMap string `json:"configMap,omitempty"` - PodSelector metav1.LabelSelector `json:"podSelector,omitempty"` - ServiceSelector metav1.LabelSelector `json:"serviceSelector,omitempty"` + Provider *ProviderSpec `json:"provider"` // Type string `json:"type,omitempty"` Labels map[string]string `json:"labels,omitempty"` + + // +kubebuilder:validation:MinLength=1 + Profile string `json:"profile"` +} + +// +kubebuilder:validation:MaxProperties=1 +type ProviderSpec struct { + HTTP *HTTPConfig `json:"http,omitempty"` + Consul *ConsulConfig `json:"consul,omitempty"` } type HTTPConfig struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3e19bde..267b997 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 ( - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -818,6 +818,31 @@ func (in *ProcessorStatus) DeepCopy() *ProcessorStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) { + *out = *in + if in.HTTP != nil { + in, out := &in.HTTP, &out.HTTP + *out = new(HTTPConfig) + **out = **in + } + if in.Consul != nil { + in, out := &in.Consul, &out.Consul + *out = new(ConsulConfig) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderSpec. +func (in *ProviderSpec) DeepCopy() *ProviderSpec { + if in == nil { + return nil + } + out := new(ProviderSpec) + 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 @@ -1262,23 +1287,11 @@ func (in *TargetSourceList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TargetSourceSpec) DeepCopyInto(out *TargetSourceSpec) { *out = *in - if in.HTTPPull != nil { - in, out := &in.HTTPPull, &out.HTTPPull - *out = new(HTTPConfig) - **out = **in - } - if in.HTTP != nil { - in, out := &in.HTTP, &out.HTTP - *out = new(HTTPConfig) - **out = **in - } - if in.Consul != nil { - in, out := &in.Consul, &out.Consul - *out = new(ConsulConfig) - **out = **in + if in.Provider != nil { + in, out := &in.Provider, &out.Provider + *out = new(ProviderSpec) + (*in).DeepCopyInto(*out) } - in.PodSelector.DeepCopyInto(&out.PodSelector) - in.ServiceSelector.DeepCopyInto(&out.ServiceSelector) if in.Labels != nil { in, out := &in.Labels, &out.Labels *out = make(map[string]string, len(*in)) diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 3070c0c..1212ff9 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -39,127 +39,30 @@ spec: spec: description: TargetSourceSpec defines the desired state of TargetSource properties: - configMap: - type: string - consul: - properties: - url: - type: string - type: object - http: - properties: - url: - type: string - type: object - http_pull: - properties: - url: - type: string - type: object labels: additionalProperties: type: string type: object - podSelector: - description: |- - A label selector is a label query over a set of resources. The result of matchLabels and - matchExpressions are ANDed. An empty label selector matches all objects. A null - label selector matches no objects. + profile: + minLength: 1 + type: string + provider: + maxProperties: 1 properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. - The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + consul: + properties: + url: + type: string type: object - type: object - x-kubernetes-map-type: atomic - serviceSelector: - description: |- - A label selector is a label query over a set of resources. The result of matchLabels and - matchExpressions are ANDed. An empty label selector matches all objects. A null - label selector matches no objects. - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. - The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. + http: + properties: + url: + type: string type: object type: object - x-kubernetes-map-type: atomic - type: - type: string + required: + - profile + - provider type: object status: description: TargetSourceStatus defines the observed state of TargetSource From 5ccfaceb89299159226952892823fc967fab32be Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 15 Apr 2026 09:28:49 -0600 Subject: [PATCH 20/31] 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 +++++++++++++++++++ .../targetsource/loaders/http_pull/loader.go | 20 +++++++--------- .../controller/targetsource/loaders_test.go | 1 - .../controller/targetsource_controller.go | 5 ++-- 9 files changed, 60 insertions(+), 17 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 (98%) rename internal/controller/targetsource/{ => core}/types.go (96%) create mode 100644 internal/controller/targetsource/factory.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 98% rename from internal/controller/targetsource/target_manager.go rename to internal/controller/targetsource/core/target_manager.go index be34fa2..150d7e6 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 96% rename from internal/controller/targetsource/types.go rename to internal/controller/targetsource/core/types.go index 43928f2..b962c06 100644 --- a/internal/controller/targetsource/types.go +++ b/internal/controller/targetsource/core/types.go @@ -1,4 +1,4 @@ -package targetsource +package core import "sigs.k8s.io/controller-runtime/pkg/client" 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/http_pull/loader.go b/internal/controller/targetsource/loaders/http_pull/loader.go index dee5e13..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: 1, + Event: core.CREATE, }, { - Target: targetsource.DiscoveredTarget{ + Target: core.DiscoveredTarget{ Name: "leaf1", Address: "clab-3-nodes-leaf1:57400", Labels: map[string]string{"TargetSource": targetsourceName}, }, - Event: 1, + 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 6ade566..7a18726 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" ) @@ -85,13 +86,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, targetSource.Name, targetChannel, From c2a9245805b0aef32eccffdcbae05b1420b3bc62 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 15 Apr 2026 10:52:36 -0600 Subject: [PATCH 21/31] renamed targetsource package to discovery --- internal/controller/discovery/client.go | 27 +++++++++++++++++++ .../core/loader_interface.go} | 0 .../{targetsource => discovery}/core/types.go | 9 ------- .../factory.go => discovery/loader.go} | 6 ++--- .../{targetsource => discovery}/loaders.go | 0 .../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 | 21 ++++++++++++--- .../targetsource/core/loaders_test.go | 1 - .../controller/targetsource/mapper_test.go | 1 - .../controller/targetsource_controller.go | 10 +++---- 16 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 internal/controller/discovery/client.go rename internal/controller/{targetsource/core/loaders.go => discovery/core/loader_interface.go} (100%) rename internal/controller/{targetsource => discovery}/core/types.go (63%) rename internal/controller/{targetsource/factory.go => discovery/loader.go} (77%) rename internal/controller/{targetsource => discovery}/loaders.go (100%) 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 (84%) create mode 100644 internal/controller/discovery/mapper_test.go rename internal/controller/{targetsource/core => discovery}/target_manager.go (64%) delete mode 100644 internal/controller/targetsource/core/loaders_test.go delete mode 100644 internal/controller/targetsource/mapper_test.go diff --git a/internal/controller/discovery/client.go b/internal/controller/discovery/client.go new file mode 100644 index 0000000..3bc7ef7 --- /dev/null +++ b/internal/controller/discovery/client.go @@ -0,0 +1,27 @@ +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/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 63% rename from internal/controller/targetsource/core/types.go rename to internal/controller/discovery/core/types.go index b962c06..406a22b 100644 --- a/internal/controller/targetsource/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -1,7 +1,5 @@ package core -import "sigs.k8s.io/controller-runtime/pkg/client" - // DiscoveredTarget represents a target discovered from an external source // before it is materialized as a Kubernetes Target CR type DiscoveredTarget struct { @@ -22,10 +20,3 @@ type DiscoveryMessage struct { Target DiscoveredTarget Event DiscoveryEvent } - -// TargetManager consumes discovered targets and applies them to Kubernetes. -type TargetManager struct { - client client.Client - targetsource string - 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.go b/internal/controller/discovery/loaders.go similarity index 100% rename from internal/controller/targetsource/loaders.go rename to internal/controller/discovery/loaders.go 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 84% rename from internal/controller/targetsource/mapper.go rename to internal/controller/discovery/mapper.go index fded27d..18470b2 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 64% rename from internal/controller/targetsource/core/target_manager.go rename to internal/controller/discovery/target_manager.go index 150d7e6..245942d 100644 --- a/internal/controller/targetsource/core/target_manager.go +++ b/internal/controller/discovery/target_manager.go @@ -1,17 +1,30 @@ -package core +package discovery import ( "context" + "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" "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, sourceName string, in <-chan []DiscoveryMessage) *TargetManager { +func NewTargetManager(c client.Client, s *runtime.Scheme, ts *gnmicv1alpha1.TargetSource, in <-chan []core.DiscoveryMessage) *TargetManager { return &TargetManager{ client: c, - targetsource: sourceName, + scheme: s, + targetSource: ts, in: in, } } @@ -20,7 +33,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") 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 7a18726..df9e79c 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 { @@ -79,7 +79,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 } @@ -92,7 +92,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, targetSource.Name, targetChannel, From f951046a04a61f4452c7131e88608b45723f977c Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 15 Apr 2026 11:22:19 -0600 Subject: [PATCH 22/31] fixed code after semi-merge --- internal/controller/discovery/loaders.go | 54 ------------------- .../controller/targetsource_controller.go | 3 +- 2 files changed, 2 insertions(+), 55 deletions(-) delete mode 100644 internal/controller/discovery/loaders.go diff --git a/internal/controller/discovery/loaders.go b/internal/controller/discovery/loaders.go deleted file mode 100644 index 62344ca..0000000 --- a/internal/controller/discovery/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[loaderName] - if !ok { - 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 df9e79c..2ef51d5 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -94,7 +94,8 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request // start target manager manager := discovery.NewTargetManager( r.Client, - targetSource.Name, + r.Scheme, + &targetSource, targetChannel, ) go manager.Run(runtimeCtx) From d79dff1ce029804bb34961c78135befc30d78d39 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 15 Apr 2026 11:29:40 -0600 Subject: [PATCH 23/31] regenerated 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 267b997..8d9e746 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" ) From cc0be6ea664d1f3bc304ea73f296b3cb63edd341 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 16 Apr 2026 08:47:46 +0000 Subject: [PATCH 24/31] update test targetsource according to the new definition --- lab/dev/resources/targetsources/ctest1.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lab/dev/resources/targetsources/ctest1.yaml b/lab/dev/resources/targetsources/ctest1.yaml index 2360603..e0aea43 100644 --- a/lab/dev/resources/targetsources/ctest1.yaml +++ b/lab/dev/resources/targetsources/ctest1.yaml @@ -3,8 +3,10 @@ kind: TargetSource metadata: name: http-discovery spec: - http: - url: http://inventory-service:8080/targets + provider: + http: + url: http://inventory-service:8080/targets labels: source: inventory - type: http_pull \ No newline at end of file + type: http + profile: eos \ No newline at end of file From 78fd8b26e6069550ed081a71e7ef4cfdf458ceb2 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 16 Apr 2026 09:40:06 +0000 Subject: [PATCH 25/31] pass TargetSourceSpec to loader --- internal/controller/discovery/core/loader_interface.go | 3 +++ internal/controller/discovery/loaders/http_pull/loader.go | 2 ++ internal/controller/targetsource_controller.go | 4 ++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/controller/discovery/core/loader_interface.go b/internal/controller/discovery/core/loader_interface.go index 7007349..2b87a0a 100644 --- a/internal/controller/discovery/core/loader_interface.go +++ b/internal/controller/discovery/core/loader_interface.go @@ -2,6 +2,8 @@ package core import ( "context" + + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" ) // Loader defines a pluggable TargetSource loader interface @@ -15,6 +17,7 @@ type Loader interface { Start( ctx context.Context, targetsourceName string, + spec gnmicv1alpha1.TargetSourceSpec, out chan<- []DiscoveryMessage, ) error } diff --git a/internal/controller/discovery/loaders/http_pull/loader.go b/internal/controller/discovery/loaders/http_pull/loader.go index e987d78..22868b2 100644 --- a/internal/controller/discovery/loaders/http_pull/loader.go +++ b/internal/controller/discovery/loaders/http_pull/loader.go @@ -6,6 +6,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" + gnmicv1alpha1 "github.com/gnmic/operator/api/v1alpha1" "github.com/gnmic/operator/internal/controller/discovery/core" ) @@ -23,6 +24,7 @@ func (l *Loader) Name() string { func (l *Loader) Start( ctx context.Context, targetsourceName string, + spec gnmicv1alpha1.TargetSourceSpec, out chan<- []core.DiscoveryMessage, ) error { logger := log.FromContext(ctx).WithValues("loader", l.Name()) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 2ef51d5..d800393 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -79,7 +79,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, nil } - loader, err := discovery.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) if err != nil { return ctrl.Result{}, err } @@ -89,7 +89,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request targetChannel := make(chan []core.DiscoveryMessage, 10) // start loader - go loader.Start(runtimeCtx, targetSource.Name, targetChannel) + go loader.Start(runtimeCtx, targetSource.Name, targetSource.Spec, targetChannel) // start target manager manager := discovery.NewTargetManager( From 9a85123e90d8063d4bc844db69261f0535a836f4 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 16 Apr 2026 14:56:18 +0000 Subject: [PATCH 26/31] handle TargetSource deletion --- .../controller/targetsource_controller.go | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index d800393..81f5277 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -23,6 +23,7 @@ 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" @@ -31,6 +32,8 @@ import ( _ "github.com/gnmic/operator/internal/controller/discovery/loaders/all" ) +const targetSourceFinalizer = "operator.gnmic.dev/targetsource-finalizer" + type runningSource struct { cancel context.CancelFunc } @@ -56,11 +59,40 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request var targetSource gnmicv1alpha1.TargetSource if err := r.Get(ctx, req.NamespacedName, &targetSource); err != nil { + // If the TargetSource no longer exists, ensure runtime cleanup + if client.IgnoreNotFound(err) == nil { + r.stopDiscovery(req.NamespacedName) + } return ctrl.Result{}, client.IgnoreNotFound(err) } 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) + + r.stopDiscovery(req.NamespacedName) + + // 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 + } + } + } + + // 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 + } + // TODO: // 1. Check if a pipeline is already running for this TargetSource // 2. If not, create and start a new pipeline: @@ -109,6 +141,18 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, nil } +// stopDiscovery stops and removes a running discovery pipeline +// 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.cancel() + delete(r.running, key) + } +} + // SetupWithManager sets up the controller with the Manager. func (r *TargetSourceReconciler) SetupWithManager(mgr ctrl.Manager) error { r.running = make(map[client.ObjectKey]runningSource) From 7a94e3f5addb5b244daf5f2a84ffab0457e76bc7 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 16 Apr 2026 15:10:59 +0000 Subject: [PATCH 27/31] exit reconciliation after handling the CR deletion --- internal/controller/targetsource_controller.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 81f5277..8cd6f68 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -81,6 +81,8 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, err } } + + return ctrl.Result{}, nil } // Ensure finalizer is set From 6c9354d2556aa07ef26caf06d72a026c06e326af Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Fri, 17 Apr 2026 15:41:06 -0600 Subject: [PATCH 28/31] renamed fields affecting target resources --- api/v1alpha1/targetsource_types.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index d2a977c..57b12d7 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -25,10 +25,10 @@ import ( type TargetSourceSpec struct { Provider *ProviderSpec `json:"provider"` // - Labels map[string]string `json:"labels,omitempty"` + TargetLabels map[string]string `json:"labels,omitempty"` // +kubebuilder:validation:MinLength=1 - Profile string `json:"profile"` + TargetProfile string `json:"profile"` } // +kubebuilder:validation:MaxProperties=1 From 1223e516d8feb8573248e5133bb254ed0bd78fc4 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Fri, 17 Apr 2026 15:43:40 -0600 Subject: [PATCH 29/31] manifest generation after changes --- api/v1alpha1/zz_generated.deepcopy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8d9e746..61e81fd 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1292,8 +1292,8 @@ func (in *TargetSourceSpec) DeepCopyInto(out *TargetSourceSpec) { *out = new(ProviderSpec) (*in).DeepCopyInto(*out) } - if in.Labels != nil { - in, out := &in.Labels, &out.Labels + if in.TargetLabels != nil { + in, out := &in.TargetLabels, &out.TargetLabels *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val From b600ffe607e657b83c1ed91f3be0ee12e623ff82 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Fri, 17 Apr 2026 16:14:58 -0600 Subject: [PATCH 30/31] changed semantics of kubebuilder validation --- api/v1alpha1/targetsource_types.go | 10 +++++--- .../operator.gnmic.dev_targetsources.yaml | 25 ++++++++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 57b12d7..feea000 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -25,23 +25,25 @@ import ( type TargetSourceSpec struct { Provider *ProviderSpec `json:"provider"` // - TargetLabels map[string]string `json:"labels,omitempty"` + TargetLabels map[string]string `json:"targetLabels,omitempty"` // +kubebuilder:validation:MinLength=1 - TargetProfile string `json:"profile"` + TargetProfile string `json:"targetProfile"` } -// +kubebuilder:validation:MaxProperties=1 +// +kubebuilder:validation:ExactlyOneOf=http;consul type ProviderSpec struct { HTTP *HTTPConfig `json:"http,omitempty"` Consul *ConsulConfig `json:"consul,omitempty"` } type HTTPConfig struct { - URL string `json:"url,omitempty"` + // +kubebuilder:validation:MinLength=1 + URL string `json:"url"` } type ConsulConfig struct { + // +kubebuilder:validation:MinLength=1 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 1212ff9..f373822 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -39,30 +39,37 @@ spec: spec: description: TargetSourceSpec defines the desired state of TargetSource properties: - labels: - additionalProperties: - type: string - type: object - profile: - minLength: 1 - type: string provider: - maxProperties: 1 properties: consul: properties: url: + minLength: 1 type: string type: object http: properties: url: + minLength: 1 type: string + required: + - url type: object type: object + x-kubernetes-validations: + - 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' + targetLabels: + additionalProperties: + type: string + type: object + targetProfile: + minLength: 1 + type: string required: - - profile - provider + - targetProfile type: object status: description: TargetSourceStatus defines the observed state of TargetSource From 2e863cf73cd4809777980aee8ab6429251895924 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Wed, 22 Apr 2026 11:48:12 +0000 Subject: [PATCH 31/31] remove http_ from http_push and http_pull --- internal/controller/discovery/core/loader_interface.go | 2 +- internal/controller/discovery/loader.go | 4 ++-- internal/controller/discovery/loaders/all/all.go | 4 ++-- .../controller/discovery/loaders/http_pull/loader_test.go | 1 - .../controller/discovery/loaders/http_push/loader_test.go | 1 - .../discovery/loaders/{http_pull => pull}/loader.go | 6 +++--- internal/controller/discovery/loaders/pull/loader_test.go | 1 + .../discovery/loaders/{http_push => push}/loader.go | 2 +- internal/controller/discovery/loaders/push/loader_test.go | 1 + 9 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 internal/controller/discovery/loaders/http_pull/loader_test.go delete mode 100644 internal/controller/discovery/loaders/http_push/loader_test.go rename internal/controller/discovery/loaders/{http_pull => pull}/loader.go (95%) create mode 100644 internal/controller/discovery/loaders/pull/loader_test.go rename internal/controller/discovery/loaders/{http_push => push}/loader.go (86%) create mode 100644 internal/controller/discovery/loaders/push/loader_test.go diff --git a/internal/controller/discovery/core/loader_interface.go b/internal/controller/discovery/core/loader_interface.go index 2b87a0a..82036a8 100644 --- a/internal/controller/discovery/core/loader_interface.go +++ b/internal/controller/discovery/core/loader_interface.go @@ -9,7 +9,7 @@ import ( // 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 returns the unique loader identifier e.g. "pull" Name() string // Start begins discovery and pushes target snapshots into the out channel diff --git a/internal/controller/discovery/loader.go b/internal/controller/discovery/loader.go index ad1e83f..3d45f42 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" - "github.com/gnmic/operator/internal/controller/discovery/loaders/http_pull" + pull "github.com/gnmic/operator/internal/controller/discovery/loaders/pull" ) // 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 http_pull.New(), nil + return pull.New(), 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 c53b98a..d05604b 100644 --- a/internal/controller/discovery/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/discovery/loaders/http_pull" - // _ "github.com/gnmic/operator/internal/controller/targetsource/loaders/http_push" + _ "github.com/gnmic/operator/internal/controller/discovery/loaders/pull" + // _ "github.com/gnmic/operator/internal/controller/targetsource/loaders/push" ) diff --git a/internal/controller/discovery/loaders/http_pull/loader_test.go b/internal/controller/discovery/loaders/http_pull/loader_test.go deleted file mode 100644 index d606d4d..0000000 --- a/internal/controller/discovery/loaders/http_pull/loader_test.go +++ /dev/null @@ -1 +0,0 @@ -package http_pull diff --git a/internal/controller/discovery/loaders/http_push/loader_test.go b/internal/controller/discovery/loaders/http_push/loader_test.go deleted file mode 100644 index bb7d848..0000000 --- a/internal/controller/discovery/loaders/http_push/loader_test.go +++ /dev/null @@ -1 +0,0 @@ -package http_push diff --git a/internal/controller/discovery/loaders/http_pull/loader.go b/internal/controller/discovery/loaders/pull/loader.go similarity index 95% rename from internal/controller/discovery/loaders/http_pull/loader.go rename to internal/controller/discovery/loaders/pull/loader.go index 22868b2..5540ea2 100644 --- a/internal/controller/discovery/loaders/http_pull/loader.go +++ b/internal/controller/discovery/loaders/pull/loader.go @@ -1,4 +1,4 @@ -package http_pull +package pull import ( "context" @@ -12,13 +12,13 @@ import ( type Loader struct{} -// New instantiates the http_pull loader +// New instantiates the pull loader func New() core.Loader { return &Loader{} } func (l *Loader) Name() string { - return "http_pull" + return "pull" } func (l *Loader) Start( diff --git a/internal/controller/discovery/loaders/pull/loader_test.go b/internal/controller/discovery/loaders/pull/loader_test.go new file mode 100644 index 0000000..0493bec --- /dev/null +++ b/internal/controller/discovery/loaders/pull/loader_test.go @@ -0,0 +1 @@ +package pull diff --git a/internal/controller/discovery/loaders/http_push/loader.go b/internal/controller/discovery/loaders/push/loader.go similarity index 86% rename from internal/controller/discovery/loaders/http_push/loader.go rename to internal/controller/discovery/loaders/push/loader.go index 95dc1e9..92f0ccc 100644 --- a/internal/controller/discovery/loaders/http_push/loader.go +++ b/internal/controller/discovery/loaders/push/loader.go @@ -1,4 +1,4 @@ -package http_push +package push // this file implements the logic receive target updates via HTTP push // REST API defined internal/apiserver diff --git a/internal/controller/discovery/loaders/push/loader_test.go b/internal/controller/discovery/loaders/push/loader_test.go new file mode 100644 index 0000000..63fdf61 --- /dev/null +++ b/internal/controller/discovery/loaders/push/loader_test.go @@ -0,0 +1 @@ +package push