From 28ad3a3af4458660585f1a4dd61f3303d5529b63 Mon Sep 17 00:00:00 2001 From: mayankpande88 Date: Mon, 10 Aug 2026 12:58:31 +0530 Subject: [PATCH] feat(discovery): report configured scope in metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server knew a discovery datasource existed but not what it covered. Its integration row carries only routing — datasource_key, agent_type, connection_mode — while allowed_cidrs stayed in the agent's local config and was never reported. That blocks two things. A scheduler has to be told the ranges separately, so server and agent can drift: the server asks for a sweep the agent refuses as out of scope, and the refusal reads as a bug in discovery rather than a configuration mismatch. And the coverage report has no denominator — 'how many machines are there' needs the intended scope, not just whatever answered. Reported the same way #129 added pack_versions, so the channel already existed. An unrestricted datasource omits the field rather than reporting an empty list, so 'covers everything' stays distinguishable from 'covers nothing'. --- pkg/proxy/discovery/proxy.go | 26 ++++++++++++++++++ pkg/proxy/discovery/proxy_test.go | 45 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/pkg/proxy/discovery/proxy.go b/pkg/proxy/discovery/proxy.go index e3fa41c..107478b 100644 --- a/pkg/proxy/discovery/proxy.go +++ b/pkg/proxy/discovery/proxy.go @@ -671,9 +671,35 @@ func (p *Proxy) CollectMetadata(ctx context.Context) (map[string]any, error) { if versions := p.packVersions(); len(versions) > 0 { meta["pack_versions"] = versions } + if scope := p.scope(); len(scope) > 0 { + meta["allowed_cidrs"] = scope + } return meta, nil } +// scope reports the CIDRs this datasource will accept work for. +// +// Without it the server knows a discovery datasource exists but not what it +// covers, which breaks two things downstream. A scheduler has to be told the +// ranges separately, so server and agent can disagree — the server asks for a +// sweep the agent refuses as out of scope, and the refusal looks like a bug in +// discovery rather than a configuration mismatch. And the coverage report cannot be +// computed at all: "how many machines are there" needs the intended +// denominator, not just whatever happened to answer. +func (p *Proxy) scope() []string { + p.mu.RLock() + defer p.mu.RUnlock() + + out := make([]string, 0, len(p.allowedNets)+len(p.allowedHosts)) + for _, n := range p.allowedNets { + out = append(out, n.String()) + } + // Bare hostnames are kept as configured; they are resolved per request + // rather than at configure time, so there is no address to report. + out = append(out, p.allowedHosts...) + return out +} + // packVersionPattern matches cached pack filenames (linux-inventory-v.yaml), // mirroring the path resolvePack reads. var packVersionPattern = regexp.MustCompile(`^linux-inventory-v(\d+)\.yaml$`) diff --git a/pkg/proxy/discovery/proxy_test.go b/pkg/proxy/discovery/proxy_test.go index 95c85e6..c3fd955 100644 --- a/pkg/proxy/discovery/proxy_test.go +++ b/pkg/proxy/discovery/proxy_test.go @@ -640,3 +640,48 @@ func TestDiscoveryActionsReturnStructuredResult(t *testing.T) { }) } } + +// The server needs to know what each collector covers. Without it a scheduler +// must be told the ranges separately and can drift out of step with the agent, +// and the coverage report has no denominator to work from. +func TestCollectMetadata_ReportsScope(t *testing.T) { + pubB64, priv := packPubKeyB64(t) + p, _ := newTestProxy(t, map[string]any{ + "allowed_cidrs": []any{"10.0.1.0/24", "10.0.2.5", "db.corp.local"}, + "pack_public_key": pubB64, + "pack_dir": writePackDir(t, validBody, priv, 3), + }, map[string]string{"username": "nudgebee-ro", "password": "x"}) + + meta, err := p.CollectMetadata(context.Background()) + if err != nil { + t.Fatalf("collecting metadata: %v", err) + } + + scope, ok := meta["allowed_cidrs"].([]string) + if !ok { + t.Fatalf("allowed_cidrs missing or wrong type: %#v", meta["allowed_cidrs"]) + } + + got := strings.Join(scope, ",") + for _, want := range []string{"10.0.1.0/24", "10.0.2.5/32", "db.corp.local"} { + if !strings.Contains(got, want) { + t.Errorf("scope %v is missing %s", scope, want) + } + } +} + +// An unrestricted datasource reports no scope rather than an empty list, so +// the server can tell "covers everything" from "covers nothing". +func TestCollectMetadata_OmitsScopeWhenUnrestricted(t *testing.T) { + p, _ := newTestProxy(t, map[string]any{}, map[string]string{ + "username": "nudgebee-ro", "password": "x", + }) + + meta, err := p.CollectMetadata(context.Background()) + if err != nil { + t.Fatalf("collecting metadata: %v", err) + } + if _, present := meta["allowed_cidrs"]; present { + t.Errorf("unrestricted datasource reported a scope: %#v", meta["allowed_cidrs"]) + } +}