mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
b2f2e73944
This introduces a new trigger mode for the decision log plugin: decision_logs.reporting.trigger=immediate The immediate trigger mode will upload events as soon as enough events are received to hit the configured upload limit. If not enough events are received within the configured min-max delay, the events received so far are flushed and uploaded. Signed-off-by: Sebastian Spaink <sebastianspaink@gmail.com>
1425 lines
37 KiB
Go
1425 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)
|
|
|
|
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\", \"manual\" or \"immediate\")"),
|
|
},
|
|
}
|
|
|
|
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{}{}
|
|
}
|