Files
akvorado/outlet/metadata/root_test.go
Vincent Bernat 23518c3e2e
Some checks failed
CI / 🤖 Check dependabot status (push) Has been cancelled
CI / 🐧 Test on Linux (${{ github.ref_type == 'tag' }}, misc) (push) Has been cancelled
CI / 🐧 Test on Linux (coverage) (push) Has been cancelled
CI / 🐧 Test on Linux (regular) (push) Has been cancelled
CI / ❄️ Build on Nix (push) Has been cancelled
CI / 🍏 Build and test on macOS (push) Has been cancelled
CI / 🧪 End-to-end testing (push) Has been cancelled
CI / 🔍 Upload code coverage (push) Has been cancelled
CI / 🔬 Test only Go (push) Has been cancelled
CI / 🔬 Test only JS (${{ needs.dependabot.outputs.package-ecosystem }}, 20) (push) Has been cancelled
CI / 🔬 Test only JS (${{ needs.dependabot.outputs.package-ecosystem }}, 22) (push) Has been cancelled
CI / 🔬 Test only JS (${{ needs.dependabot.outputs.package-ecosystem }}, 24) (push) Has been cancelled
CI / ⚖️ Check licenses (push) Has been cancelled
CI / 🐋 Build Docker images (push) Has been cancelled
CI / 🐋 Tag Docker images (push) Has been cancelled
CI / 🚀 Publish release (push) Has been cancelled
Update Nix dependency hashes / Update dependency hashes (push) Has been cancelled
common/helpers: add a NetIPTo6() function
This should be netip.To6() but it does not exist and it was rejected.
There is a benchmark showing the improvment of such optimisation:

BenchmarkNetIPTo6/safe_v4-12            170152954                7.054 ns/op
BenchmarkNetIPTo6/unsafe_v4-12          764772190                1.553 ns/op

See https://github.com/golang/go/issues/54365.
2025-11-03 21:36:33 +01:00

356 lines
10 KiB
Go

// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
package metadata
import (
"context"
"errors"
"net/netip"
"path/filepath"
"testing"
"testing/synctest"
"time"
"akvorado/common/daemon"
"akvorado/common/helpers"
"akvorado/common/reporter"
"akvorado/common/schema"
"akvorado/outlet/metadata/provider"
"akvorado/outlet/metadata/provider/static"
)
func expectMockLookup(t *testing.T, c *Component, exporter string, ifIndex uint, expected provider.Answer) {
t.Helper()
ip := helpers.NetIPTo6(netip.MustParseAddr(exporter))
got := c.Lookup(time.Now(), ip, ifIndex)
if diff := helpers.Diff(got, expected); diff != "" {
t.Fatalf("Lookup() (-got, +want):\n%s", diff)
}
}
func TestLookup(t *testing.T) {
r := reporter.NewMock(t)
c := NewMock(t, r, DefaultConfiguration(), Dependencies{Daemon: daemon.NewMock(t)})
expectMockLookup(t, c, "127.0.0.1", 765, provider.Answer{
Found: true,
Exporter: provider.Exporter{
Name: "127_0_0_1",
},
Interface: provider.Interface{
Name: "Gi0/0/765",
Description: "Interface 765",
Speed: 1000,
},
})
expectMockLookup(t, c, "127.0.0.1", 1010, provider.Answer{
Found: true,
Exporter: provider.Exporter{
Name: "127_0_0_1",
Group: "metadata group",
Region: "metadata region",
Role: "metadata role",
Site: "metadata site",
Tenant: "metadata tenant",
},
Interface: provider.Interface{
Name: "Gi0/0/1010",
Description: "Interface 1010",
Speed: 1000,
},
})
expectMockLookup(t, c, "127.0.0.1", 2010, provider.Answer{
Found: true,
Exporter: provider.Exporter{
Name: "127_0_0_1",
Group: "metadata group",
Region: "metadata region",
Role: "metadata role",
Site: "metadata site",
Tenant: "metadata tenant",
},
Interface: provider.Interface{
Name: "Gi0/0/2010",
Description: "Interface 2010",
Speed: 1000,
Boundary: schema.InterfaceBoundaryExternal,
Connectivity: "metadata connectivity",
Provider: "metadata provider",
},
})
// With a simple lookup, this is not possible to distinguish between a
// transient error or a fatal error. Only the caching subsystem knows.
expectMockLookup(t, c, "127.0.0.1", 999, provider.Answer{})
expectMockLookup(t, c, "127.0.0.1", 998, provider.Answer{})
}
func TestComponentSaveLoad(t *testing.T) {
configuration := DefaultConfiguration()
configuration.CachePersistFile = filepath.Join(t.TempDir(), "cache")
t.Run("save", func(t *testing.T) {
r := reporter.NewMock(t)
c := NewMock(t, r, configuration, Dependencies{Daemon: daemon.NewMock(t)})
expectMockLookup(t, c, "127.0.0.1", 765, provider.Answer{
Found: true,
Exporter: provider.Exporter{
Name: "127_0_0_1",
},
Interface: provider.Interface{
Name: "Gi0/0/765",
Description: "Interface 765",
Speed: 1000,
},
})
})
t.Run("load", func(t *testing.T) {
r := reporter.NewMock(t)
// Switch to the empty provider to check if we get answers from the cache.
configuration.Providers = []ProviderConfiguration{{Config: emptyProviderConfiguration{}}}
c := NewMock(t, r, configuration, Dependencies{Daemon: daemon.NewMock(t)})
expectMockLookup(t, c, "127.0.0.1", 765, provider.Answer{
Found: true,
Exporter: provider.Exporter{
Name: "127_0_0_1",
},
Interface: provider.Interface{
Name: "Gi0/0/765",
Description: "Interface 765",
Speed: 1000,
},
})
})
}
func TestAutoRefresh(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
r := reporter.NewMock(t)
configuration := DefaultConfiguration()
c := NewMock(t, r, configuration, Dependencies{Daemon: daemon.NewMock(t)})
expectMockLookup(t, c, "127.0.0.1", 765, provider.Answer{
Found: true,
Exporter: provider.Exporter{
Name: "127_0_0_1",
},
Interface: provider.Interface{
Name: "Gi0/0/765",
Description: "Interface 765",
Speed: 1000,
},
})
// Keep it in the cache!
time.Sleep(25 * time.Minute)
c.Lookup(time.Now(), netip.MustParseAddr("::ffff:127.0.0.1"), 765)
time.Sleep(25 * time.Minute)
c.Lookup(time.Now(), netip.MustParseAddr("::ffff:127.0.0.1"), 765)
// Go forward, we expect the entry to have been refreshed and be still present
time.Sleep(11 * time.Minute)
time.Sleep(2 * time.Minute)
expectMockLookup(t, c, "127.0.0.1", 765, provider.Answer{
Found: true,
Exporter: provider.Exporter{
Name: "127_0_0_1",
},
Interface: provider.Interface{
Name: "Gi0/0/765",
Description: "Interface 765",
Speed: 1000,
},
})
gotMetrics := r.GetMetrics("akvorado_outlet_metadata_cache_")
for _, runs := range []string{"29", "30", "31"} { // 63/2
expectedMetrics := map[string]string{
`expired_entries_total`: "0",
`misses_total`: "1", // First lookup misses
`hits_total`: "3", // Subsequent ones hits
`size_entries`: "1",
`refresh_runs_total`: runs,
`refreshes_total`: "1", // One refresh (after 1 hour)
}
if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" && runs == "19" {
t.Fatalf("Metrics (-got, +want):\n%s", diff)
} else if diff == "" {
break
}
}
})
}
func TestConfigCheck(t *testing.T) {
t.Run("refresh", func(t *testing.T) {
configuration := DefaultConfiguration()
configuration.CacheDuration = 10 * time.Minute
configuration.CacheRefresh = 5 * time.Minute
configuration.CacheCheckInterval = time.Minute
configuration.Providers = []ProviderConfiguration{{Config: mockProviderConfiguration{}}}
if _, err := New(reporter.NewMock(t), configuration, Dependencies{Daemon: daemon.NewMock(t)}); err == nil {
t.Fatal("New() should trigger an error")
}
})
t.Run("interval", func(t *testing.T) {
configuration := DefaultConfiguration()
configuration.CacheDuration = 10 * time.Minute
configuration.CacheRefresh = 15 * time.Minute
configuration.CacheCheckInterval = 12 * time.Minute
configuration.Providers = []ProviderConfiguration{{Config: mockProviderConfiguration{}}}
if _, err := New(reporter.NewMock(t), configuration, Dependencies{Daemon: daemon.NewMock(t)}); err == nil {
t.Fatal("New() should trigger an error")
}
})
t.Run("refresh disabled", func(t *testing.T) {
configuration := DefaultConfiguration()
configuration.CacheDuration = 10 * time.Minute
configuration.CacheRefresh = 0
configuration.CacheCheckInterval = 2 * time.Minute
configuration.Providers = []ProviderConfiguration{{Config: mockProviderConfiguration{}}}
if _, err := New(reporter.NewMock(t), configuration, Dependencies{Daemon: daemon.NewMock(t)}); err != nil {
t.Fatalf("New() error:\n%+v", err)
}
})
}
func TestStartStopSimple(t *testing.T) {
r := reporter.NewMock(t)
configuration := DefaultConfiguration()
NewMock(t, r, configuration, Dependencies{Daemon: daemon.NewMock(t)})
}
type errorProvider struct{}
func (ep errorProvider) Query(_ context.Context, _ provider.Query) (provider.Answer, error) {
return provider.Answer{}, errors.New("noooo")
}
type errorProviderConfiguration struct{}
func (epc errorProviderConfiguration) New(context.Context, *reporter.Reporter) (provider.Provider, error) {
return errorProvider{}, nil
}
func TestProviderBreaker(t *testing.T) {
cases := []struct {
Name string
ProviderConfiguration provider.Configuration
ExpectedCount string
}{
{"always successful provider", mockProviderConfiguration{}, "0"},
{"never successful provider", errorProviderConfiguration{}, "10"},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
r := reporter.NewMock(t)
configuration := DefaultConfiguration()
configuration.Providers = []ProviderConfiguration{{Config: tc.ProviderConfiguration}}
c := NewMock(t, r, configuration, Dependencies{Daemon: daemon.NewMock(t)})
c.metrics.providerBreakerOpenCount.WithLabelValues("127.0.0.1").Add(0)
for range 30 {
c.Lookup(time.Now(), netip.MustParseAddr("::ffff:127.0.0.1"), 765)
}
for range 5 {
c.Lookup(time.Now(), netip.MustParseAddr("::ffff:127.0.0.2"), 765)
}
time.Sleep(50 * time.Millisecond)
gotMetrics := r.GetMetrics("akvorado_outlet_metadata_provider_", "breaker_opens_total")
expectedMetrics := map[string]string{
`breaker_opens_total{exporter="127.0.0.1"}`: tc.ExpectedCount,
}
if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" {
t.Errorf("Metrics (-got, +want):\n%s", diff)
}
})
}
}
func TestMultipleProviders(t *testing.T) {
r := reporter.NewMock(t)
staticConfiguration1 := static.Configuration{
Exporters: helpers.MustNewSubnetMap(map[string]static.ExporterConfiguration{
"2001:db8:1::/48": {
Exporter: provider.Exporter{
Name: "static1",
},
IfIndexes: map[uint]provider.Interface{
10: {
Name: "Gi10",
Description: "10th interface",
Speed: 1000,
},
11: {
Name: "Gi11",
Description: "11th interface",
Speed: 1000,
},
},
},
}),
}
staticConfiguration2 := static.Configuration{
Exporters: helpers.MustNewSubnetMap(map[string]static.ExporterConfiguration{
"2001:db8:2::/48": {
Exporter: provider.Exporter{
Name: "static2",
},
IfIndexes: map[uint]provider.Interface{
12: {
Name: "Gi12",
Description: "12th interface",
Speed: 1000,
},
13: {
Name: "Gi13",
Description: "13th interface",
Speed: 1000,
},
},
},
}),
}
configuration := DefaultConfiguration()
configuration.Providers = []ProviderConfiguration{
{Config: staticConfiguration1},
{Config: staticConfiguration2},
}
c := NewMock(t, r, configuration, Dependencies{Daemon: daemon.NewMock(t)})
c.Lookup(time.Now(), netip.MustParseAddr("2001:db8:1::1"), 10)
c.Lookup(time.Now(), netip.MustParseAddr("2001:db8:2::2"), 12)
time.Sleep(30 * time.Millisecond)
got1 := c.Lookup(time.Now(), netip.MustParseAddr("2001:db8:1::1"), 10)
got2 := c.Lookup(time.Now(), netip.MustParseAddr("2001:db8:2::2"), 12)
got := []provider.Answer{got1, got2}
expected := []provider.Answer{
{
Found: true,
Exporter: provider.Exporter{
Name: "static1",
},
Interface: provider.Interface{
Name: "Gi10",
Description: "10th interface",
Speed: 1000,
},
}, {
Found: true,
Exporter: provider.Exporter{
Name: "static2",
},
Interface: provider.Interface{
Name: "Gi12",
Description: "12th interface",
Speed: 1000,
},
},
}
if diff := helpers.Diff(got, expected); diff != "" {
t.Fatalf("Lookup() (-got, +want):\n%s", diff)
}
}