mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
f77322b3fb
Go 1.23 is no longer supported as per Go release policy. Changes: - Use Go v1.24.6 as the project SDK requirement - Apply lint fixes for Go 1.24 - Fix "non-constant format string in call" issues as seen in CI. Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
1432 lines
37 KiB
Go
1432 lines
37 KiB
Go
// Copyright 2018 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package status
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"reflect"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/testutil"
|
|
|
|
"github.com/open-policy-agent/opa/v1/metrics"
|
|
"github.com/open-policy-agent/opa/v1/plugins"
|
|
"github.com/open-policy-agent/opa/v1/plugins/bundle"
|
|
lstat "github.com/open-policy-agent/opa/v1/plugins/logs/status"
|
|
inmem "github.com/open-policy-agent/opa/v1/storage/inmem/test"
|
|
"github.com/open-policy-agent/opa/v1/util"
|
|
"github.com/open-policy-agent/opa/v1/util/test"
|
|
"github.com/open-policy-agent/opa/v1/version"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
if version.Version == "" {
|
|
version.Version = "unit-test"
|
|
}
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestStatusUpdateBuffer(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
name string
|
|
numberOfStatusUpdates int
|
|
expectedStatusUpdates int
|
|
expectedNameDropped string
|
|
}{
|
|
{
|
|
name: "add multiple events dropping the oldest",
|
|
numberOfStatusUpdates: 11,
|
|
expectedStatusUpdates: 1,
|
|
expectedNameDropped: "0",
|
|
},
|
|
{
|
|
name: "don't drop anything",
|
|
numberOfStatusUpdates: 5,
|
|
expectedStatusUpdates: 1,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
fixture := newTestFixture(t, nil)
|
|
ctx := t.Context()
|
|
|
|
err := fixture.plugin.Start(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer fixture.plugin.Stop(ctx)
|
|
|
|
for i := range tc.numberOfStatusUpdates {
|
|
s := bundle.Status{
|
|
Name: strconv.Itoa(i),
|
|
}
|
|
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{
|
|
"test": &s,
|
|
})
|
|
}
|
|
|
|
if len(fixture.plugin.bulkBundleCh) != tc.expectedStatusUpdates {
|
|
t.Fatalf("expected %d updates, got %d", tc.expectedStatusUpdates, len(fixture.plugin.bulkBundleCh))
|
|
}
|
|
for _, v := range <-fixture.plugin.bulkBundleCh {
|
|
if v.Name == tc.expectedNameDropped {
|
|
t.Fatalf("expected %s dropped", tc.expectedNameDropped)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestConfigValueParse(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
input string
|
|
expectedNoConfig bool
|
|
expectedValue []float64
|
|
}{
|
|
{
|
|
note: "empty config",
|
|
input: `{}`,
|
|
expectedNoConfig: true,
|
|
},
|
|
{
|
|
note: "empty prometheus config",
|
|
input: `{"prometheus": false}`,
|
|
expectedNoConfig: true,
|
|
},
|
|
{
|
|
note: "no specific prometheus config, expected default config buckets",
|
|
input: `{"prometheus": true}`,
|
|
expectedValue: defaultBundleLoadStageBuckets,
|
|
},
|
|
{
|
|
note: "no specific prometheus config, expected default config buckets",
|
|
input: `{"prometheus": true, "prometheus_config": {}}`,
|
|
expectedValue: defaultBundleLoadStageBuckets,
|
|
},
|
|
{
|
|
note: "no specific collectors config, expected default config buckets",
|
|
input: `{"prometheus": true, "prometheus_config": {"collectors": {}}}`,
|
|
expectedValue: defaultBundleLoadStageBuckets,
|
|
},
|
|
{
|
|
note: "specified prometheus config, expected value same as config",
|
|
input: `{"prometheus": true, "prometheus_config": {"collectors": {"bundle_loading_duration_ns": {}}}}`,
|
|
expectedValue: defaultBundleLoadStageBuckets,
|
|
},
|
|
{
|
|
note: "specified prometheus config, expected value same as config",
|
|
input: `{"prometheus": true, "prometheus_config": {"collectors": {"bundle_loading_duration_ns": {"buckets": []}}}}`,
|
|
expectedValue: []float64{},
|
|
},
|
|
{
|
|
note: "specified prometheus config, expected value same as config",
|
|
input: `{"prometheus": true, "prometheus_config": {"collectors": {"bundle_loading_duration_ns": {"buckets":[1, 1000, 1000_000, 1e8]}}}}`,
|
|
expectedValue: []float64{1, 1000, 1000_000, 1e8},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
config, err := ParseConfig([]byte(tc.input), []string{}, []string{"status"})
|
|
if err != nil {
|
|
t.Errorf("expected no error: %v", err)
|
|
}
|
|
if tc.expectedNoConfig && config != nil {
|
|
t.Errorf("expected parsed config is nil, got %v", config)
|
|
}
|
|
if !tc.expectedNoConfig && !slices.Equal(config.PrometheusConfig.Collectors.BundleLoadDurationNanoseconds.Buckets, tc.expectedValue) {
|
|
t.Errorf("expected %v, got %v", tc.expectedValue, config.PrometheusConfig.Collectors.BundleLoadDurationNanoseconds.Buckets)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPluginPrometheus(t *testing.T) {
|
|
fixture := newTestFixture(t, nil, func(c *Config) {
|
|
c.Prometheus = true
|
|
})
|
|
fixture.server.ch = make(chan UpdateRequestV1)
|
|
defer fixture.server.stop()
|
|
|
|
ctx := t.Context()
|
|
|
|
err := fixture.plugin.Start(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer fixture.plugin.Stop(ctx)
|
|
<-fixture.server.ch
|
|
|
|
status := testStatus()
|
|
|
|
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{"bundle": status})
|
|
<-fixture.server.ch
|
|
|
|
registerMock := fixture.manager.PrometheusRegister().(*prometheusRegisterMock)
|
|
|
|
assertOpInformationGauge(t, registerMock)
|
|
|
|
if registerMock.Collectors[fixture.plugin.collectors.pluginStatus] != true {
|
|
t.Fatalf("Plugin status metric was not registered on prometheus")
|
|
}
|
|
if registerMock.Collectors[fixture.plugin.collectors.loaded] != true {
|
|
t.Fatalf("Loaded metric was not registered on prometheus")
|
|
}
|
|
if registerMock.Collectors[fixture.plugin.collectors.failLoad] != true {
|
|
t.Fatalf("FailLoad metric was not registered on prometheus")
|
|
}
|
|
if registerMock.Collectors[fixture.plugin.collectors.lastRequest] != true {
|
|
t.Fatalf("Last request metric was not registered on prometheus")
|
|
}
|
|
if registerMock.Collectors[fixture.plugin.collectors.lastSuccessfulActivation] != true {
|
|
t.Fatalf("Last Successful Activation metric was not registered on prometheus")
|
|
}
|
|
if registerMock.Collectors[fixture.plugin.collectors.lastSuccessfulDownload] != true {
|
|
t.Fatalf("Last Successful Download metric was not registered on prometheus")
|
|
}
|
|
if registerMock.Collectors[fixture.plugin.collectors.lastSuccessfulRequest] != true {
|
|
t.Fatalf("Last Successful Request metric was not registered on prometheus")
|
|
}
|
|
if registerMock.Collectors[fixture.plugin.collectors.bundleLoadDuration] != true {
|
|
t.Fatalf("Bundle Load Duration metric was not registered on prometheus")
|
|
}
|
|
if len(registerMock.Collectors) != 9 {
|
|
t.Fatalf("Number of collectors expected (%v), got %v", 9, len(registerMock.Collectors))
|
|
}
|
|
|
|
lastRequestMetricResult := time.UnixMilli(int64(testutil.ToFloat64(fixture.plugin.collectors.lastRequest) / 1e6))
|
|
if !lastRequestMetricResult.Equal(status.LastRequest) {
|
|
t.Fatalf("Last request expected (%v), got %v", status.LastRequest.UTC(), lastRequestMetricResult.UTC())
|
|
}
|
|
|
|
lastSuccessfulRequestMetricResult := time.UnixMilli(int64(testutil.ToFloat64(fixture.plugin.collectors.lastSuccessfulRequest) / 1e6))
|
|
if !lastSuccessfulRequestMetricResult.Equal(status.LastSuccessfulRequest) {
|
|
t.Fatalf("Last request expected (%v), got %v", status.LastSuccessfulRequest.UTC(), lastSuccessfulRequestMetricResult.UTC())
|
|
}
|
|
|
|
lastSuccessfulDownloadMetricResult := time.UnixMilli(int64(testutil.ToFloat64(fixture.plugin.collectors.lastSuccessfulDownload) / 1e6))
|
|
if !lastSuccessfulDownloadMetricResult.Equal(status.LastSuccessfulDownload) {
|
|
t.Fatalf("Last request expected (%v), got %v", status.LastSuccessfulDownload.UTC(), lastSuccessfulDownloadMetricResult.UTC())
|
|
}
|
|
|
|
lastSuccessfulActivationMetricResult := time.UnixMilli(int64(testutil.ToFloat64(fixture.plugin.collectors.lastSuccessfulActivation) / 1e6))
|
|
if !lastSuccessfulActivationMetricResult.Equal(status.LastSuccessfulActivation) {
|
|
t.Fatalf("Last request expected (%v), got %v", status.LastSuccessfulActivation.UTC(), lastSuccessfulActivationMetricResult.UTC())
|
|
}
|
|
|
|
bundlesLoaded := testutil.CollectAndCount(fixture.plugin.collectors.loaded)
|
|
if bundlesLoaded != 1 {
|
|
t.Fatalf("Unexpected number of bundle loads (%v), got %v", 1, bundlesLoaded)
|
|
}
|
|
|
|
bundlesFailedToLoad := testutil.CollectAndCount(fixture.plugin.collectors.failLoad)
|
|
if bundlesFailedToLoad != 0 {
|
|
t.Fatalf("Unexpected number of bundle fails load (%v), got %v", 0, bundlesFailedToLoad)
|
|
}
|
|
|
|
pluginsStatus := testutil.CollectAndCount(fixture.plugin.collectors.pluginStatus)
|
|
if pluginsStatus != 1 {
|
|
t.Fatalf("Unexpected number of plugins (%v), got %v", 1, pluginsStatus)
|
|
}
|
|
|
|
// Assert that metrics are purged when prometheus is disabled
|
|
prometheusDisabledConfig := newConfig(fixture.manager, func(c *Config) {
|
|
c.Prometheus = false
|
|
})
|
|
fixture.plugin.Reconfigure(ctx, prometheusDisabledConfig)
|
|
eventually(t, func() bool { return fixture.plugin.config.Prometheus == false })
|
|
|
|
if len(registerMock.Collectors) != 0 {
|
|
t.Fatalf("Number of collectors expected (%v), got %v", 0, len(registerMock.Collectors))
|
|
}
|
|
|
|
// Assert that metrics are re-registered when prometheus is re-enabled
|
|
prometheusReenabledConfig := newConfig(fixture.manager, func(c *Config) {
|
|
c.Prometheus = true
|
|
})
|
|
fixture.plugin.Reconfigure(ctx, prometheusReenabledConfig)
|
|
eventually(t, func() bool { return fixture.plugin.config.Prometheus == true })
|
|
|
|
if len(registerMock.Collectors) != 9 {
|
|
t.Fatalf("Number of collectors expected (%v), got %v", 9, len(registerMock.Collectors))
|
|
}
|
|
}
|
|
|
|
func eventually(t *testing.T, predicate func() bool) {
|
|
t.Helper()
|
|
if !test.Eventually(t, 1*time.Second, predicate) {
|
|
t.Fatal("check took too long")
|
|
}
|
|
}
|
|
|
|
func assertOpInformationGauge(t *testing.T, registerMock *prometheusRegisterMock) {
|
|
gauges := filterGauges(registerMock)
|
|
if len(gauges) != 1 {
|
|
t.Fatalf("Expected one registered gauge on prometheus but got %v", len(gauges))
|
|
}
|
|
|
|
gauge := gauges[0]
|
|
|
|
fqName := getName(gauge)
|
|
if fqName != "opa_info" {
|
|
t.Fatalf("Expected gauge to have name opa_info but was %s", fqName)
|
|
}
|
|
|
|
labels := getConstLabels(gauge)
|
|
versionAct := labels["version"]
|
|
if versionAct != version.Version {
|
|
t.Fatalf("Expected gauge to have version label with value %s but was %s", version.Version, versionAct)
|
|
}
|
|
}
|
|
|
|
func getName(gauge prometheus.Gauge) string {
|
|
desc := reflect.Indirect(reflect.ValueOf(gauge.Desc()))
|
|
fqName := desc.FieldByName("fqName").String()
|
|
return fqName
|
|
}
|
|
|
|
func getConstLabels(gauge prometheus.Gauge) prometheus.Labels {
|
|
desc := reflect.Indirect(reflect.ValueOf(gauge.Desc()))
|
|
constLabelPairs := desc.FieldByName("constLabelPairs")
|
|
|
|
// put all label pairs into a map for easier comparison.
|
|
labels := make(prometheus.Labels, constLabelPairs.Len())
|
|
for i := range constLabelPairs.Len() {
|
|
name := constLabelPairs.Index(i).Elem().FieldByName("Name").Elem().String()
|
|
value := constLabelPairs.Index(i).Elem().FieldByName("Value").Elem().String()
|
|
labels[name] = value
|
|
}
|
|
return labels
|
|
}
|
|
|
|
func filterGauges(registerMock *prometheusRegisterMock) []prometheus.Gauge {
|
|
fltd := make([]prometheus.Gauge, 0)
|
|
|
|
for m := range registerMock.Collectors {
|
|
switch metric := m.(type) {
|
|
case prometheus.Gauge:
|
|
fltd = append(fltd, metric)
|
|
}
|
|
}
|
|
return fltd
|
|
}
|
|
|
|
func TestMetricsBundleWithoutRevision(t *testing.T) {
|
|
fixture := newTestFixture(t, nil, func(c *Config) {
|
|
c.Prometheus = true
|
|
})
|
|
fixture.server.ch = make(chan UpdateRequestV1)
|
|
defer fixture.server.stop()
|
|
|
|
ctx := t.Context()
|
|
|
|
err := fixture.plugin.Start(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer fixture.plugin.Stop(ctx)
|
|
<-fixture.server.ch
|
|
|
|
status := testStatus()
|
|
status.ActiveRevision = ""
|
|
|
|
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{"bundle": status})
|
|
<-fixture.server.ch
|
|
|
|
bundlesLoaded := testutil.CollectAndCount(fixture.plugin.collectors.loaded)
|
|
if bundlesLoaded != 1 {
|
|
t.Fatalf("Unexpected number of bundle loads (%v), got %v", 1, bundlesLoaded)
|
|
}
|
|
|
|
bundlesFailedToLoad := testutil.CollectAndCount(fixture.plugin.collectors.failLoad)
|
|
if bundlesFailedToLoad != 0 {
|
|
t.Fatalf("Unexpected number of bundle fails load (%v), got %v", 0, bundlesFailedToLoad)
|
|
}
|
|
}
|
|
|
|
func TestPluginStart(t *testing.T) {
|
|
|
|
fixture := newTestFixture(t, nil)
|
|
fixture.server.ch = make(chan UpdateRequestV1)
|
|
defer fixture.server.stop()
|
|
|
|
ctx := t.Context()
|
|
|
|
err := fixture.plugin.Start(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer fixture.plugin.Stop(ctx)
|
|
|
|
// Start will trigger a status update when the plugin state switches
|
|
// from "not ready" to "ok".
|
|
result := <-fixture.server.ch
|
|
|
|
exp := UpdateRequestV1{
|
|
Labels: map[string]string{
|
|
"id": "test-instance-id",
|
|
"app": "example-app",
|
|
"version": version.Version,
|
|
},
|
|
Plugins: map[string]*plugins.Status{
|
|
"status": {State: plugins.StateOK},
|
|
},
|
|
}
|
|
|
|
if !result.Equal(exp) {
|
|
t.Fatalf("Expected: %v but got: %v", exp, result)
|
|
}
|
|
|
|
status := testStatus()
|
|
|
|
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{"test": status})
|
|
result = <-fixture.server.ch
|
|
|
|
exp.Bundles = map[string]*bundle.Status{"test": status}
|
|
|
|
if !result.Equal(exp) {
|
|
t.Fatalf("Expected: %v but got: %v", exp, result)
|
|
}
|
|
}
|
|
|
|
func TestPluginNoLogging(t *testing.T) {
|
|
// Given no custom plugin, no service(s) and no console logging configured,
|
|
// this should not be an error, but neither do we need to initiate the plugin
|
|
cases := []struct {
|
|
note string
|
|
config []byte
|
|
}{
|
|
{
|
|
note: "no plugin attributes",
|
|
config: []byte(`{}`),
|
|
},
|
|
{
|
|
note: "empty plugin configuration",
|
|
config: []byte(`{"status": {}}`),
|
|
},
|
|
{
|
|
note: "only disabled console logger",
|
|
config: []byte(`{"status": {"console": "false"}}`),
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
config, err := ParseConfig(tc.config, []string{}, nil)
|
|
if err != nil {
|
|
t.Errorf("expected no error: %v", err)
|
|
}
|
|
if config != nil {
|
|
t.Errorf("excected no config for a no-op logging plugin")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPluginStartTriggerManualStart(t *testing.T) {
|
|
|
|
fixture := newTestFixture(t, nil)
|
|
fixture.server.ch = make(chan UpdateRequestV1)
|
|
defer fixture.server.stop()
|
|
|
|
ctx := t.Context()
|
|
tr := plugins.TriggerManual
|
|
fixture.plugin.config.Trigger = &tr
|
|
|
|
// Start will trigger a status update when the plugin state switches
|
|
// from "not ready" to "ok". This status update will be sent only after a manual trigger
|
|
err := fixture.plugin.Start(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer fixture.plugin.Stop(ctx)
|
|
|
|
// trigger the status update
|
|
go func() {
|
|
_ = fixture.plugin.Trigger(ctx)
|
|
}()
|
|
|
|
result := <-fixture.server.ch
|
|
|
|
exp := UpdateRequestV1{
|
|
Labels: map[string]string{
|
|
"id": "test-instance-id",
|
|
"app": "example-app",
|
|
"version": version.Version,
|
|
},
|
|
}
|
|
|
|
if !maps.Equal(result.Labels, exp.Labels) {
|
|
t.Fatalf("Expected: %v but got: %v", exp, result)
|
|
}
|
|
}
|
|
|
|
func TestPluginStartTriggerManual(t *testing.T) {
|
|
fixture := newTestFixture(t, nil)
|
|
fixture.server.ch = make(chan UpdateRequestV1)
|
|
defer fixture.server.stop()
|
|
tr := plugins.TriggerManual
|
|
fixture.plugin.config.Trigger = &tr
|
|
|
|
status := testStatus()
|
|
|
|
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{"test": status})
|
|
|
|
statuses := <-fixture.plugin.bulkBundleCh
|
|
fixture.plugin.lastBundleStatuses = statuses
|
|
|
|
// trigger the status update
|
|
go func() {
|
|
_ = fixture.plugin.Trigger(t.Context())
|
|
}()
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
update := <-fixture.plugin.trigger
|
|
err := fixture.plugin.oneShot(update.ctx)
|
|
errCh <- err
|
|
}()
|
|
|
|
result := <-fixture.server.ch
|
|
if err := <-errCh; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
exp := UpdateRequestV1{
|
|
Labels: map[string]string{
|
|
"id": "test-instance-id",
|
|
"app": "example-app",
|
|
"version": version.Version,
|
|
},
|
|
}
|
|
exp.Bundles = map[string]*bundle.Status{"test": status}
|
|
|
|
if !maps.EqualFunc(result.Bundles, exp.Bundles, (*bundle.Status).Equal) {
|
|
t.Fatalf("Expected: %v but got: %v", exp, result)
|
|
}
|
|
}
|
|
|
|
func TestPluginStartTriggerManualMultiple(t *testing.T) {
|
|
|
|
fixture := newTestFixture(t, nil)
|
|
fixture.server.ch = make(chan UpdateRequestV1)
|
|
defer fixture.server.stop()
|
|
|
|
ctx := t.Context()
|
|
tr := plugins.TriggerManual
|
|
fixture.plugin.config.Trigger = &tr
|
|
|
|
err := fixture.plugin.Start(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer fixture.plugin.Stop(ctx)
|
|
|
|
status := testStatus()
|
|
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{"test": status})
|
|
fixture.plugin.UpdateDiscoveryStatus(*status)
|
|
|
|
exp := UpdateRequestV1{
|
|
Labels: map[string]string{
|
|
"id": "test-instance-id",
|
|
"app": "example-app",
|
|
"version": version.Version,
|
|
},
|
|
Plugins: map[string]*plugins.Status{
|
|
"status": {State: plugins.StateOK},
|
|
},
|
|
}
|
|
|
|
// trigger the status update
|
|
go func() {
|
|
_ = fixture.plugin.Trigger(ctx)
|
|
}()
|
|
|
|
result := <-fixture.server.ch
|
|
|
|
exp.Bundles = map[string]*bundle.Status{"test": status}
|
|
exp.Discovery = status
|
|
|
|
if !result.Equal(exp) {
|
|
t.Fatalf("Expected: %v but got: %v", exp, result)
|
|
}
|
|
}
|
|
|
|
func TestPluginStartTriggerManualWithTimeout(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
|
|
defer cancel()
|
|
|
|
s := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
|
|
time.Sleep(3 * time.Second) // this should cause the context deadline to exceed
|
|
}))
|
|
|
|
managerConfig := fmt.Appendf(nil, `{
|
|
"labels": {
|
|
"app": "example-app"
|
|
},
|
|
"services": [
|
|
{
|
|
"name": "example",
|
|
"url": %q
|
|
}
|
|
]}`, s.URL)
|
|
|
|
manager, err := plugins.New(managerConfig, "test-instance-id", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pluginConfig := []byte(`{
|
|
"service": "example",
|
|
}`)
|
|
|
|
config, _ := ParseConfig(pluginConfig, manager.Services(), nil)
|
|
tr := plugins.TriggerManual
|
|
config.Trigger = &tr
|
|
|
|
p := New(config, manager)
|
|
|
|
// Start will trigger a status update when the plugin state switches
|
|
// from "not ready" to "ok". This status update will be sent only after a manual trigger
|
|
err = p.Start(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer p.Stop(ctx)
|
|
|
|
// trigger the status update
|
|
done := make(chan struct{})
|
|
go func() {
|
|
// this call should block till the context deadline exceeds
|
|
_ = p.Trigger(ctx)
|
|
close(done)
|
|
}()
|
|
<-done
|
|
|
|
if ctx.Err() == nil {
|
|
t.Fatal("Expected error but got nil")
|
|
}
|
|
|
|
exp := "context deadline exceeded"
|
|
if ctx.Err().Error() != exp {
|
|
t.Fatalf("Expected error %v but got %v", exp, ctx.Err().Error())
|
|
}
|
|
}
|
|
|
|
func TestPluginStartTriggerManualWithError(t *testing.T) {
|
|
ctx := t.Context()
|
|
|
|
managerConfig := []byte(`{
|
|
"labels": {
|
|
"app": "example-app"
|
|
},
|
|
"services": [
|
|
{
|
|
"name": "example",
|
|
"url": "http://localhost:12345"
|
|
}
|
|
]}`)
|
|
|
|
manager, err := plugins.New(managerConfig, "test-instance-id", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pluginConfig := []byte(`{
|
|
"service": "example",
|
|
}`)
|
|
|
|
config, _ := ParseConfig(pluginConfig, manager.Services(), nil)
|
|
tr := plugins.TriggerManual
|
|
config.Trigger = &tr
|
|
|
|
p := New(config, manager)
|
|
|
|
// Start will trigger a status update when the plugin state switches
|
|
// from "not ready" to "ok". This status update will be sent only after a manual trigger
|
|
err = p.Start(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer p.Stop(ctx)
|
|
|
|
// trigger the status update
|
|
// this call should result in an error from the bad service config
|
|
err = p.Trigger(ctx)
|
|
if err == nil {
|
|
t.Fatal("Expected error but got nil")
|
|
}
|
|
|
|
exp := "connection refused"
|
|
if !strings.Contains(err.Error(), exp) {
|
|
t.Fatalf("Unexpected error message %v", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestPluginStartBulkUpdate(t *testing.T) {
|
|
|
|
fixture := newTestFixture(t, nil)
|
|
fixture.server.ch = make(chan UpdateRequestV1)
|
|
defer fixture.server.stop()
|
|
|
|
ctx := t.Context()
|
|
|
|
err := fixture.plugin.Start(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer fixture.plugin.Stop(ctx)
|
|
|
|
// Start will trigger a status update when the plugin state switches
|
|
// from "not ready" to "ok".
|
|
<-fixture.server.ch // Discard first request.
|
|
|
|
exp := UpdateRequestV1{
|
|
Labels: map[string]string{
|
|
"id": "test-instance-id",
|
|
"app": "example-app",
|
|
"version": version.Version,
|
|
},
|
|
Plugins: map[string]*plugins.Status{
|
|
"status": {State: plugins.StateOK},
|
|
},
|
|
}
|
|
|
|
status := testStatus()
|
|
|
|
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{status.Name: status})
|
|
result := <-fixture.server.ch
|
|
|
|
exp.Bundles = map[string]*bundle.Status{status.Name: status}
|
|
|
|
if !result.Equal(exp) {
|
|
t.Fatalf("Expected: %v but got: %v", exp, result)
|
|
}
|
|
}
|
|
|
|
func TestPluginStartBulkUpdateMultiple(t *testing.T) {
|
|
|
|
fixture := newTestFixture(t, nil)
|
|
fixture.server.ch = make(chan UpdateRequestV1)
|
|
defer fixture.server.stop()
|
|
|
|
ctx := t.Context()
|
|
|
|
err := fixture.plugin.Start(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer fixture.plugin.Stop(ctx)
|
|
|
|
// Ignore the plugin updating its status (tested elsewhere)
|
|
<-fixture.server.ch
|
|
|
|
statuses := map[string]*bundle.Status{}
|
|
tDownload, _ := time.Parse(time.RFC3339Nano, "2018-01-01T00:00:00.0000000Z")
|
|
tActivate, _ := time.Parse(time.RFC3339Nano, "2018-01-01T00:00:01.0000000Z")
|
|
for i := range 20 {
|
|
name := fmt.Sprintf("test-bundle-%d", i)
|
|
statuses[name] = &bundle.Status{
|
|
Name: name,
|
|
ActiveRevision: fmt.Sprintf("v%d", i),
|
|
LastSuccessfulDownload: tDownload,
|
|
LastSuccessfulActivation: tActivate,
|
|
}
|
|
}
|
|
|
|
fixture.plugin.BulkUpdateBundleStatus(statuses)
|
|
result := <-fixture.server.ch
|
|
|
|
expLabels := map[string]string{
|
|
"id": "test-instance-id",
|
|
"app": "example-app",
|
|
"version": version.Version,
|
|
}
|
|
|
|
if !maps.Equal(result.Labels, expLabels) {
|
|
t.Fatalf("Unexpected status labels: %+v", result.Labels)
|
|
}
|
|
|
|
if len(result.Bundles) != len(statuses) {
|
|
t.Fatalf("Expected %d statuses, got %d", len(statuses), len(result.Bundles))
|
|
}
|
|
|
|
for name, s := range statuses {
|
|
actualStatus := result.Bundles[name]
|
|
if actualStatus.Name != s.Name ||
|
|
actualStatus.LastSuccessfulActivation != s.LastSuccessfulActivation ||
|
|
actualStatus.LastSuccessfulDownload != s.LastSuccessfulDownload ||
|
|
actualStatus.ActiveRevision != s.ActiveRevision {
|
|
t.Errorf("Bundle %s has unexpected status:\n\n %v\n\nExpected:\n%v\n\n", name, actualStatus, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPluginStartDiscovery(t *testing.T) {
|
|
|
|
fixture := newTestFixture(t, nil)
|
|
fixture.server.ch = make(chan UpdateRequestV1)
|
|
defer fixture.server.stop()
|
|
|
|
ctx := t.Context()
|
|
|
|
err := fixture.plugin.Start(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer fixture.plugin.Stop(ctx)
|
|
|
|
// Ignore the plugin updating its status (tested elsewhere)
|
|
<-fixture.server.ch
|
|
|
|
status := testStatus()
|
|
|
|
fixture.plugin.UpdateDiscoveryStatus(*status)
|
|
result := <-fixture.server.ch
|
|
|
|
exp := UpdateRequestV1{
|
|
Labels: map[string]string{
|
|
"id": "test-instance-id",
|
|
"app": "example-app",
|
|
"version": version.Version,
|
|
},
|
|
Discovery: status,
|
|
Plugins: map[string]*plugins.Status{
|
|
"status": {State: plugins.StateOK},
|
|
},
|
|
}
|
|
|
|
if !result.Equal(exp) {
|
|
t.Fatalf("Expected: %+v but got: %+v", exp, result)
|
|
}
|
|
}
|
|
|
|
func TestPluginStartDecisionLogs(t *testing.T) {
|
|
|
|
fixture := newTestFixture(t, nil)
|
|
fixture.server.ch = make(chan UpdateRequestV1)
|
|
defer fixture.server.stop()
|
|
|
|
ctx := t.Context()
|
|
|
|
err := fixture.plugin.Start(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer fixture.plugin.Stop(ctx)
|
|
|
|
// Ignore the plugin updating its status (tested elsewhere)
|
|
<-fixture.server.ch
|
|
|
|
status := &lstat.Status{
|
|
Code: "decision_log_error",
|
|
Message: "Upload Failed",
|
|
HTTPCode: "400",
|
|
}
|
|
|
|
fixture.plugin.UpdateDecisionLogsStatus(*status)
|
|
result := <-fixture.server.ch
|
|
|
|
exp := UpdateRequestV1{
|
|
Labels: map[string]string{
|
|
"id": "test-instance-id",
|
|
"app": "example-app",
|
|
"version": version.Version,
|
|
},
|
|
DecisionLogs: status,
|
|
Plugins: map[string]*plugins.Status{
|
|
"status": {State: plugins.StateOK},
|
|
},
|
|
}
|
|
|
|
if !result.Equal(exp) {
|
|
t.Fatalf("Expected: %+v but got: %+v", exp, result)
|
|
}
|
|
}
|
|
|
|
func TestPluginBadAuth(t *testing.T) {
|
|
fixture := newTestFixture(t, nil)
|
|
ctx := t.Context()
|
|
fixture.server.expCode = 401
|
|
defer fixture.server.stop()
|
|
fixture.plugin.lastBundleStatuses = map[string]*bundle.Status{}
|
|
err := fixture.plugin.oneShot(ctx)
|
|
if err == nil {
|
|
t.Fatal("Expected error")
|
|
}
|
|
if err.Error() != "status update failed, server replied with HTTP 401 Unauthorized" {
|
|
t.Fatalf("Unexpected error contents: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPluginBadPath(t *testing.T) {
|
|
fixture := newTestFixture(t, nil)
|
|
ctx := t.Context()
|
|
fixture.server.expCode = 404
|
|
defer fixture.server.stop()
|
|
fixture.plugin.lastBundleStatuses = map[string]*bundle.Status{}
|
|
err := fixture.plugin.oneShot(ctx)
|
|
if err == nil {
|
|
t.Fatal("Expected error")
|
|
}
|
|
if err.Error() != "status update failed, server replied with HTTP 404 Not Found" {
|
|
t.Fatalf("Unexpected error contents: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPluginBadStatus(t *testing.T) {
|
|
fixture := newTestFixture(t, nil)
|
|
ctx := t.Context()
|
|
fixture.server.expCode = 500
|
|
defer fixture.server.stop()
|
|
fixture.plugin.lastBundleStatuses = map[string]*bundle.Status{}
|
|
err := fixture.plugin.oneShot(ctx)
|
|
if err == nil {
|
|
t.Fatal("Expected error")
|
|
}
|
|
if err.Error() != "status update failed, server replied with HTTP 500 Internal Server Error" {
|
|
t.Fatalf("Unexpected error contents: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPluginNonstandardStatus(t *testing.T) {
|
|
fixture := newTestFixture(t, nil)
|
|
ctx := t.Context()
|
|
fixture.server.expCode = 599
|
|
defer fixture.server.stop()
|
|
fixture.plugin.lastBundleStatuses = map[string]*bundle.Status{}
|
|
err := fixture.plugin.oneShot(ctx)
|
|
if err == nil {
|
|
t.Fatal("Expected error")
|
|
}
|
|
if err.Error() != "status update failed, server replied with HTTP 599 " {
|
|
t.Fatalf("Unexpected error contents: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPlugin2xxStatus(t *testing.T) {
|
|
fixture := newTestFixture(t, nil)
|
|
ctx := t.Context()
|
|
fixture.server.expCode = 204
|
|
defer fixture.server.stop()
|
|
fixture.plugin.lastBundleStatuses = map[string]*bundle.Status{}
|
|
err := fixture.plugin.oneShot(ctx)
|
|
if err != nil {
|
|
t.Fatal("Expected no error")
|
|
}
|
|
}
|
|
|
|
func TestPluginReconfigure(t *testing.T) {
|
|
ctx := t.Context()
|
|
fixture := newTestFixture(t, nil, func(c *Config) {
|
|
c.Prometheus = true
|
|
})
|
|
defer fixture.server.stop()
|
|
|
|
if err := fixture.plugin.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// expect initial prometheus config is filled with default BundleLoadDurationNanoseconds bucket
|
|
initialPrometheusConfig := &PrometheusConfig{
|
|
Collectors: &Collectors{
|
|
BundleLoadDurationNanoseconds: &BundleLoadDurationNanoseconds{
|
|
Buckets: defaultBundleLoadStageBuckets,
|
|
},
|
|
},
|
|
}
|
|
if !reflect.DeepEqual(fixture.plugin.config.PrometheusConfig, initialPrometheusConfig) {
|
|
t.Fatalf("Expected initial prometheus config %+v: test but got %+v", initialPrometheusConfig, fixture.plugin.config.PrometheusConfig)
|
|
}
|
|
|
|
pluginConfig := []byte(`{
|
|
"service": "example",
|
|
"partition_name": "test",
|
|
"prometheus": true,
|
|
"prometheus_config": {
|
|
"collectors": {
|
|
"bundle_loading_duration_ns": {
|
|
"buckets":[1, 1000, 1000_000, 1e8]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
config, _ := ParseConfig(pluginConfig, fixture.manager.Services(), nil)
|
|
|
|
fixture.plugin.Reconfigure(ctx, config)
|
|
fixture.plugin.Stop(ctx)
|
|
|
|
// after reconfigure, expect partition name and prometheus config are updated
|
|
if fixture.plugin.config.PartitionName != "test" {
|
|
t.Fatalf("Expected partition name: test but got %v", fixture.plugin.config.PartitionName)
|
|
}
|
|
|
|
expectedPromConfig := &PrometheusConfig{
|
|
Collectors: &Collectors{
|
|
BundleLoadDurationNanoseconds: &BundleLoadDurationNanoseconds{
|
|
Buckets: []float64{1, 1000, 1000_000, 1e8},
|
|
},
|
|
},
|
|
}
|
|
if !reflect.DeepEqual(fixture.plugin.config.PrometheusConfig, expectedPromConfig) {
|
|
t.Fatalf("Expected prometheus config %+v: test but got %+v", expectedPromConfig, fixture.plugin.config.PrometheusConfig)
|
|
}
|
|
}
|
|
|
|
func TestMetrics(t *testing.T) {
|
|
fixture := newTestFixture(t, metrics.New())
|
|
fixture.server.ch = make(chan UpdateRequestV1)
|
|
defer fixture.server.stop()
|
|
|
|
ctx := t.Context()
|
|
|
|
err := fixture.plugin.Start(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer fixture.plugin.Stop(ctx)
|
|
|
|
// Ignore the plugin updating its status (tested elsewhere)
|
|
<-fixture.server.ch
|
|
|
|
status := testStatus()
|
|
|
|
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{"bundle": status})
|
|
result := <-fixture.server.ch
|
|
|
|
exp := map[string]any{"<built-in>": map[string]any{}}
|
|
|
|
if !reflect.DeepEqual(result.Metrics, exp) {
|
|
t.Fatalf("Expected %v but got %v", exp, result.Metrics)
|
|
}
|
|
}
|
|
|
|
func TestParseConfigUseDefaultServiceNoConsole(t *testing.T) {
|
|
services := []string{
|
|
"s0",
|
|
"s1",
|
|
"s3",
|
|
}
|
|
|
|
loggerConfig := []byte(`{
|
|
"console": false
|
|
}`)
|
|
|
|
config, err := ParseConfig(loggerConfig, services, nil)
|
|
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %s", err)
|
|
}
|
|
|
|
if config.Service != services[0] {
|
|
t.Errorf("Expected %s service in config, actual = '%s'", services[0], config.Service)
|
|
}
|
|
}
|
|
|
|
func TestParseConfigDefaultServiceWithConsole(t *testing.T) {
|
|
services := []string{
|
|
"s0",
|
|
"s1",
|
|
"s3",
|
|
}
|
|
|
|
loggerConfig := []byte(`{
|
|
"console": true
|
|
}`)
|
|
|
|
config, err := ParseConfig(loggerConfig, services, nil)
|
|
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %s", err)
|
|
}
|
|
|
|
if config.Service != "" {
|
|
t.Errorf("Expected no service in config, actual = '%s'", config.Service)
|
|
}
|
|
}
|
|
|
|
func TestParseConfigTriggerMode(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
config []byte
|
|
expected plugins.TriggerMode
|
|
wantErr bool
|
|
err error
|
|
}{
|
|
{
|
|
note: "default trigger mode",
|
|
config: []byte(`{}`),
|
|
expected: plugins.DefaultTriggerMode,
|
|
},
|
|
{
|
|
note: "manual trigger mode",
|
|
config: []byte(`{"trigger": "manual"}`),
|
|
expected: plugins.TriggerManual,
|
|
},
|
|
{
|
|
note: "trigger mode mismatch",
|
|
config: []byte(`{"trigger": "manual"}`),
|
|
expected: plugins.TriggerPeriodic,
|
|
wantErr: true,
|
|
err: errors.New("invalid status config: trigger mode mismatch: periodic and manual (hint: check discovery configuration)"),
|
|
},
|
|
{
|
|
note: "bad trigger mode",
|
|
config: []byte(`{"trigger": "foo"}`),
|
|
expected: "foo",
|
|
wantErr: true,
|
|
err: errors.New("invalid status config: invalid trigger mode \"foo\" (want \"periodic\" or \"manual\")"),
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c, err := NewConfigBuilder().WithBytes(tc.config).WithServices([]string{"s0"}).WithTriggerMode(&tc.expected).Parse()
|
|
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatal("Expected error but got nil")
|
|
}
|
|
|
|
if tc.err != nil && tc.err.Error() != err.Error() {
|
|
t.Fatalf("Expected error message %v but got %v", tc.err.Error(), err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
|
|
if *c.Trigger != tc.expected {
|
|
t.Fatalf("Expected trigger mode %v but got %v", tc.expected, *c.Trigger)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type testFixture struct {
|
|
manager *plugins.Manager
|
|
plugin *Plugin
|
|
server *testServer
|
|
}
|
|
|
|
type testPluginCustomizer func(c *Config)
|
|
|
|
func newPlugin(t *testing.T, url string, m metrics.Metrics, options ...testPluginCustomizer) (*plugins.Manager, *Plugin) {
|
|
|
|
managerConfig := fmt.Appendf(nil, `{
|
|
"labels": {
|
|
"app": "example-app"
|
|
},
|
|
"services": [
|
|
{
|
|
"name": "example",
|
|
"url": %q,
|
|
"credentials": {
|
|
"bearer": {
|
|
"scheme": "Bearer",
|
|
"token": "secret"
|
|
}
|
|
}
|
|
}
|
|
]}`, url)
|
|
|
|
registerMock := &prometheusRegisterMock{
|
|
Collectors: map[prometheus.Collector]bool{},
|
|
}
|
|
manager, err := plugins.New(managerConfig, "test-instance-id", inmem.New(), plugins.WithPrometheusRegister(registerMock))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := newConfig(manager, options...)
|
|
|
|
p := New(config, manager).WithMetrics(m)
|
|
|
|
return manager, p
|
|
}
|
|
|
|
func newTestFixture(t *testing.T, m metrics.Metrics, options ...testPluginCustomizer) testFixture {
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
expCode: 200,
|
|
}
|
|
|
|
ts.start()
|
|
|
|
manager, p := newPlugin(t, ts.server.URL, m, options...)
|
|
|
|
return testFixture{
|
|
manager: manager,
|
|
plugin: p,
|
|
server: &ts,
|
|
}
|
|
|
|
}
|
|
|
|
func newConfig(manager *plugins.Manager, options ...testPluginCustomizer) *Config {
|
|
pluginConfig := []byte(`{
|
|
"service": "example",
|
|
}`)
|
|
|
|
config, _ := ParseConfig(pluginConfig, manager.Services(), nil)
|
|
for _, option := range options {
|
|
option(config)
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
type testServer struct {
|
|
t *testing.T
|
|
expCode int
|
|
server *httptest.Server
|
|
ch chan UpdateRequestV1
|
|
}
|
|
|
|
func (t *testServer) handle(w http.ResponseWriter, r *http.Request) {
|
|
|
|
status := UpdateRequestV1{}
|
|
|
|
if err := util.NewJSONDecoder(r.Body).Decode(&status); err != nil {
|
|
t.t.Fatal(err)
|
|
}
|
|
|
|
if t.ch != nil {
|
|
t.ch <- status
|
|
}
|
|
|
|
w.WriteHeader(t.expCode)
|
|
}
|
|
|
|
func (t *testServer) start() {
|
|
t.server = httptest.NewServer(http.HandlerFunc(t.handle))
|
|
}
|
|
|
|
func (t *testServer) stop() {
|
|
t.server.Close()
|
|
}
|
|
|
|
func testStatus() *bundle.Status {
|
|
tDownload, _ := time.Parse(time.RFC3339Nano, "2018-01-01T00:00:00.0000000Z")
|
|
tActivate, _ := time.Parse(time.RFC3339Nano, "2018-01-01T00:00:01.0000000Z")
|
|
tSuccessfulRequest, _ := time.Parse(time.RFC3339Nano, "2018-01-01T00:00:02.0000000Z")
|
|
tRequest, _ := time.Parse(time.RFC3339Nano, "2018-01-01T00:00:03.0000000Z")
|
|
|
|
status := bundle.Status{
|
|
Name: "example/authz",
|
|
ActiveRevision: "quickbrawnfaux",
|
|
LastSuccessfulDownload: tDownload,
|
|
LastSuccessfulActivation: tActivate,
|
|
LastRequest: tRequest,
|
|
LastSuccessfulRequest: tSuccessfulRequest,
|
|
}
|
|
|
|
return &status
|
|
}
|
|
|
|
type testPlugin struct {
|
|
reqs []UpdateRequestV1
|
|
}
|
|
|
|
func (*testPlugin) Start(context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (*testPlugin) Stop(context.Context) {
|
|
}
|
|
|
|
func (*testPlugin) Reconfigure(context.Context, any) {
|
|
}
|
|
|
|
func (p *testPlugin) Log(_ context.Context, req *UpdateRequestV1) error {
|
|
p.reqs = append(p.reqs, *req)
|
|
return nil
|
|
}
|
|
|
|
func TestPluginCustomBackend(t *testing.T) {
|
|
ctx := t.Context()
|
|
manager, _ := plugins.New(nil, "test-instance-id", inmem.New())
|
|
|
|
backend := &testPlugin{}
|
|
manager.Register("test_plugin", backend)
|
|
|
|
config, err := ParseConfig([]byte(`{"plugin": "test_plugin"}`), nil, []string{"test_plugin"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
plugin := New(config, manager)
|
|
err = plugin.oneShot(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = plugin.oneShot(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(backend.reqs) != 2 {
|
|
t.Fatalf("Unexpected number of reqs: expected 2, got %d: %v", len(backend.reqs), backend.reqs)
|
|
}
|
|
}
|
|
|
|
type prometheusRegisterMock struct {
|
|
Collectors map[prometheus.Collector]bool
|
|
}
|
|
|
|
func (p prometheusRegisterMock) Register(collector prometheus.Collector) error {
|
|
p.Collectors[collector] = true
|
|
return nil
|
|
}
|
|
|
|
func (p prometheusRegisterMock) MustRegister(collector ...prometheus.Collector) {
|
|
for _, c := range collector {
|
|
p.Collectors[c] = true
|
|
}
|
|
}
|
|
|
|
func (p prometheusRegisterMock) Unregister(collector prometheus.Collector) bool {
|
|
delete(p.Collectors, collector)
|
|
return true
|
|
}
|
|
|
|
func TestPluginTerminatesAfterGracefulShutdownPeriodWithoutStatus(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := t.Context()
|
|
|
|
fixture := newTestFixture(t, nil)
|
|
defer fixture.server.stop()
|
|
|
|
if err := fixture.plugin.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, time.Second)
|
|
defer cancel()
|
|
|
|
fixture.plugin.Stop(timeoutCtx)
|
|
if timeoutCtx.Err() != nil {
|
|
t.Fatal("Stop did not exit before context expiration")
|
|
}
|
|
}
|
|
|
|
func TestPluginTerminatesAfterGracefulShutdownPeriodWithStatus(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := t.Context()
|
|
|
|
fixture := newTestFixture(t, nil)
|
|
fixture.server.ch = make(chan UpdateRequestV1, 1)
|
|
defer fixture.server.stop()
|
|
|
|
// simplified status loop just to return done
|
|
// but doesn't read from the channels
|
|
go func() {
|
|
for done := range fixture.plugin.stop {
|
|
done <- struct{}{}
|
|
return
|
|
}
|
|
}()
|
|
|
|
status := testStatus()
|
|
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{status.Name: status})
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, time.Second)
|
|
defer cancel()
|
|
|
|
fixture.plugin.Stop(timeoutCtx)
|
|
if timeoutCtx.Err() != nil {
|
|
t.Fatal("Stop did not exit before context expiration")
|
|
}
|
|
|
|
result := <-fixture.server.ch
|
|
|
|
exp := UpdateRequestV1{
|
|
Labels: map[string]string{
|
|
"id": "test-instance-id",
|
|
"app": "example-app",
|
|
"version": version.Version,
|
|
},
|
|
Bundles: map[string]*bundle.Status{status.Name: status},
|
|
}
|
|
|
|
if !result.Equal(exp) {
|
|
t.Fatalf("Expected: %v but got: %v", exp, result)
|
|
}
|
|
}
|
|
|
|
func TestSlowServer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
received := make(chan struct{})
|
|
wait := make(chan struct{})
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// notify test the server got the request
|
|
received <- struct{}{}
|
|
|
|
// block until the test is ready to move on, so that multiple status updates can be sent
|
|
<-wait
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, plugin := newPlugin(t, server.URL, nil)
|
|
|
|
// just start the loop, calling Start will also send a plugin status update that isn't needed for this test
|
|
go plugin.loop(t.Context())
|
|
|
|
status := bundle.Status{
|
|
Name: "test",
|
|
}
|
|
plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{status.Name: &status})
|
|
|
|
// wait for server to get stuck
|
|
<-received
|
|
|
|
if len(plugin.bulkBundleCh) != 0 {
|
|
t.Fatalf("Unexpected bulk bundle status: %v", plugin.bulkBundleCh)
|
|
}
|
|
|
|
status = bundle.Status{
|
|
Name: "I will be dropped",
|
|
}
|
|
plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{status.Name: &status})
|
|
expectedStatusName := "I won't be dropped"
|
|
status = bundle.Status{
|
|
Name: expectedStatusName,
|
|
}
|
|
plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{status.Name: &status})
|
|
|
|
currentStatus := <-plugin.bulkBundleCh
|
|
if len(currentStatus) > 1 {
|
|
t.Fatalf("Only one status expected but got: %v", currentStatus)
|
|
}
|
|
if _, ok := currentStatus[expectedStatusName]; !ok {
|
|
t.Fatalf("Expected status name not found: %v", currentStatus)
|
|
}
|
|
|
|
// stop blocking server
|
|
wait <- struct{}{}
|
|
}
|