diff --git a/pkg/proxy/discovery/metadata_test.go b/pkg/proxy/discovery/metadata_test.go new file mode 100644 index 0000000..2604f3d --- /dev/null +++ b/pkg/proxy/discovery/metadata_test.go @@ -0,0 +1,56 @@ +package discovery + +import ( + "context" + "os" + "path/filepath" + "testing" +) + +// pack_versions must list every linux-inventory-v.yaml in pack_dir, +// ascending, ignoring unrelated files — the server picks a version from this +// list to pin in discovery_inventory requests, since content_pack_version is +// a required request param with no "latest" default. +func TestCollectMetadata_ReportsPackVersions(t *testing.T) { + dir := t.TempDir() + for _, name := range []string{ + "linux-inventory-v2.yaml", + "linux-inventory-v10.yaml", + "linux-inventory-vX.yaml", // non-numeric — ignored + "windows-inventory-v1.yaml", + "README.md", + } { + if err := os.WriteFile(filepath.Join(dir, name), []byte("x"), 0o600); err != nil { + t.Fatalf("writing %s: %v", name, err) + } + } + + p, _ := newTestProxy(t, map[string]any{"pack_dir": dir}, nil) + + meta, err := p.CollectMetadata(context.Background()) + if err != nil { + t.Fatalf("CollectMetadata: %v", err) + } + versions, ok := meta["pack_versions"].([]int) + if !ok { + t.Fatalf("pack_versions missing or wrong type: %#v", meta["pack_versions"]) + } + if len(versions) != 2 || versions[0] != 2 || versions[1] != 10 { + t.Fatalf("pack_versions = %v, want [2 10]", versions) + } +} + +// Without a pack_dir there is nothing to report — the key must be absent, +// not an empty list, so the server treats it the same as an older agent +// that predates the field. +func TestCollectMetadata_NoPackDirOmitsPackVersions(t *testing.T) { + p, _ := newTestProxy(t, map[string]any{}, nil) + + meta, err := p.CollectMetadata(context.Background()) + if err != nil { + t.Fatalf("CollectMetadata: %v", err) + } + if _, present := meta["pack_versions"]; present { + t.Fatalf("pack_versions should be absent without pack_dir, got %#v", meta["pack_versions"]) + } +} diff --git a/pkg/proxy/discovery/proxy.go b/pkg/proxy/discovery/proxy.go index c095d0e..e3fa41c 100644 --- a/pkg/proxy/discovery/proxy.go +++ b/pkg/proxy/discovery/proxy.go @@ -10,6 +10,9 @@ import ( "net/netip" "os" "path/filepath" + "regexp" + "sort" + "strconv" "strings" "sync" "time" @@ -658,9 +661,60 @@ func (p *Proxy) Actions() []string { } // CollectMetadata satisfies proxy.MetadataCollector so the supported actions -// reach the server alongside the datasource inventory. +// reach the server alongside the datasource inventory. pack_versions lists +// the content pack versions present in pack_dir so the server can pick a +// version to pin in discovery_inventory requests (content_pack_version is a +// required request param with no "latest" default). Presence means the file +// exists — signature verification still happens at execution time. func (p *Proxy) CollectMetadata(ctx context.Context) (map[string]any, error) { - return map[string]any{"actions": p.Actions()}, nil + meta := map[string]any{"actions": p.Actions()} + if versions := p.packVersions(); len(versions) > 0 { + meta["pack_versions"] = versions + } + return meta, nil +} + +// packVersionPattern matches cached pack filenames (linux-inventory-v.yaml), +// mirroring the path resolvePack reads. +var packVersionPattern = regexp.MustCompile(`^linux-inventory-v(\d+)\.yaml$`) + +// packVersions lists content pack versions available in pack_dir, ascending. +func (p *Proxy) packVersions() []int { + p.mu.RLock() + packDir := p.cfg.PackDir + p.mu.RUnlock() + if packDir == "" { + return nil + } + + entries, err := os.ReadDir(packDir) + if err != nil { + // A configured-but-not-yet-created pack_dir is a normal fresh-install + // state (packs arrive via the distribution pipeline) — don't warn on + // every metadata cycle for it. + if !os.IsNotExist(err) { + p.logger.Warn("discovery: cannot list pack_dir for metadata", "err", err) + } + return nil + } + + var versions []int + for _, e := range entries { + if e.IsDir() { + continue + } + m := packVersionPattern.FindStringSubmatch(e.Name()) + if m == nil { + continue + } + v, err := strconv.Atoi(m[1]) + if err != nil || v <= 0 { + continue + } + versions = append(versions, v) + } + sort.Ints(versions) + return versions } func (p *Proxy) Close() error {