Files
akvorado/outlet/metadata/provider/static/source_test.go
Vincent Bernat e20645c92e outlet/metadata: synchronous fetching of metadata
As we are not constrained by time that much in the outlet, we can
simplify the fetching of metadata by doing it synchronously. We still
keep the breaker design to avoid continously polling a source that is
not responsive, so we still can loose some data if we are not able to
poll metadata. We also keep the background cache refresh. We also
introduce a grace time of 1 minute to avoid loosing data during start.

For the static provider, we wait for the remote data sources to be
ready. For the gNMI provider, there are target windows of availability
during which the cached data can be polled. The SNMP provider is loosing
its ability to coalesce requests.
2025-07-27 21:44:28 +02:00

239 lines
5.4 KiB
Go

// SPDX-FileCopyrightText: 2024 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
package static
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"testing"
"time"
"akvorado/common/helpers"
"akvorado/common/remotedatasourcefetcher"
"akvorado/common/reporter"
"akvorado/outlet/metadata/provider"
)
func TestInitStaticExporters(t *testing.T) {
r := reporter.NewMock(t)
conf := Configuration{
Exporters: helpers.MustNewSubnetMap(map[string]ExporterConfiguration{
"::ffff:203.0.113.0/120": {
Exporter: provider.Exporter{
Name: "something",
},
Default: provider.Interface{
Name: "iface1",
Description: "description 1",
Speed: 10000,
},
},
}),
}
p := &Provider{
r: r,
exportersMap: map[string][]exporterInfo{},
}
p.exporters.Store(conf.Exporters)
expected := map[string][]exporterInfo{}
if diff := helpers.Diff(p.exportersMap, expected); diff != "" {
t.Fatalf("static provider (-got, +want):\n%s", diff)
}
expected["static"] = []exporterInfo{
{
ExporterSubnet: "203.0.113.0/24",
Exporter: provider.Exporter{
Name: "something",
},
Default: provider.Interface{
Name: "iface1",
Description: "description 1",
Speed: 10000,
},
Interfaces: []exporterInterface{},
},
}
p.initStaticExporters()
if diff := helpers.Diff(p.exportersMap, expected); diff != "" {
t.Fatalf("static provider (-got, +want):\n%s", diff)
}
}
func TestRemoteExporterSources(t *testing.T) {
// Mux to answer requests
ready := make(chan bool)
mux := http.NewServeMux()
mux.Handle("/exporters.json", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
select {
case <-ready:
default:
w.WriteHeader(404)
return
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(200)
w.Write([]byte(`
{
"exporters": [
{
"exportersubnet": "2001:db8:2::/48",
"name": "exporter1",
"default": {
"name": "default",
"description": "default",
"speed": 100
},
"interfaces": [
{
"ifindex": 1,
"name": "iface1",
"description": "foo:desc1",
"speed": 1000
}
]
},
{
"exportersubnet": "10.0.0.1",
"name": "exporter2",
"default": {
"name": "default",
"description": "default",
"speed": 100
},
"interfaces": [
{
"ifindex": 2,
"name": "iface2",
"description": "foo:desc2",
"speed": 1000
}
]
},
{
"exportersubnet": "10.0.0.1/32",
"name": "exporter3",
"default": {
"name": "default",
"description": "default",
"speed": 100
},
"interfaces": [
{
"ifindex": 3,
"name": "iface3",
"description": "foo:desc3",
"speed": 1000
}
]
}
]
}
`))
}))
// Setup an HTTP server to serve the JSON
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Listen() error:\n%+v", err)
}
server := &http.Server{
Addr: listener.Addr().String(),
Handler: mux,
}
address := listener.Addr()
go server.Serve(listener)
defer server.Shutdown(context.Background())
r := reporter.NewMock(t)
config := Configuration{
Exporters: helpers.MustNewSubnetMap(map[string]ExporterConfiguration{
"2001:db8:1::/48": {
Exporter: provider.Exporter{
Name: "nodefault",
},
IfIndexes: map[uint]provider.Interface{
10: {
Name: "Gi10",
Description: "10th interface",
Speed: 1000,
},
},
},
}),
ExporterSourcesTimeout: 10 * time.Millisecond,
ExporterSources: map[string]remotedatasourcefetcher.RemoteDataSource{
"local": {
URL: fmt.Sprintf("http://%s/exporters.json", address),
Method: "GET",
Headers: map[string]string{
"X-Foo": "hello",
},
Timeout: 20 * time.Millisecond,
Interval: 100 * time.Millisecond,
Transform: remotedatasourcefetcher.MustParseTransformQuery(`
.exporters[]
`),
},
},
}
p, _ := config.New(r)
// Query when json is not ready yet, we should get a timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
answer1, err := p.Query(ctx, provider.Query{
ExporterIP: netip.MustParseAddr("2001:db8:1::10"),
IfIndex: 9,
})
if err == nil {
t.Fatalf("Query() should have been in error:\n%+v", answer1)
}
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("Query() error:\n%+v", err)
}
close(ready)
time.Sleep(100 * time.Millisecond)
gotMetrics := r.GetMetrics("akvorado_common_remotedatasourcefetcher_data_")
expectedMetrics := map[string]string{
`total{source="local",type="metadata"}`: "3",
}
if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" {
t.Fatalf("Metrics (-got, +want):\n%s", diff)
}
// We now should be able to resolve our new exporter from remote source
got, _ := p.Query(context.Background(), provider.Query{
ExporterIP: netip.MustParseAddr("2001:db8:2::10"),
IfIndex: 1,
})
expected := provider.Answer{
Found: true,
Exporter: provider.Exporter{
Name: "exporter1",
},
Interface: provider.Interface{
Name: "iface1",
Description: "foo:desc1",
Speed: 1000,
},
}
if diff := helpers.Diff(got, expected); diff != "" {
t.Fatalf("static provider (-got, +want):\n%s", diff)
}
}