mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-13 03:42:35 -06:00
1a4227b7dc
* runtime_test: avoid race condition This had been flagged by our nightly race deteector run. Now, we'll wait for the server to have stopped before checking its log output. * plugins: avoid races, bump github.com/sirupsen/logrus To fix that other one, I've first tried updating logrus (there was a mention of fixed races in the changelog), but to no avail. Setting up the hook before any plugin would log from that test resolved the issue. No harm in updating logrus, though, let's keep that: 1.6.0 -> 1.8.1 * plugins/bundle: fix race Golang for-range loops need special care when using a reference to the second variable (v in `for k, v := range m`). We had been copying the value of m[k], which is a pointer to Status, we had not been -- as was intended -- copying the values of the struct that the pointer had been pointing to. Tests needed to be adapted for this, the s4 update will NOT contain any bundle-activation-related metrics, as no bundle was activated, and its status is a fresh copy. * workflow: add race detector to PR checks When run from nightly, we use ubuntu-latest; whereas the other checks in the pull-request workflow use ubuntu-18.04. I don't think it matters at all for the race detector, since that one runs only from another docker container, using the golang image. Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
359 lines
9.5 KiB
Go
359 lines
9.5 KiB
Go
// Copyright 2020 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 plugins
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/open-policy-agent/opa/internal/storage/mock"
|
|
"github.com/open-policy-agent/opa/logging"
|
|
"github.com/open-policy-agent/opa/logging/test"
|
|
"github.com/open-policy-agent/opa/plugins/rest"
|
|
"github.com/open-policy-agent/opa/storage/inmem"
|
|
"github.com/open-policy-agent/opa/topdown/cache"
|
|
)
|
|
|
|
func TestManagerCacheTriggers(t *testing.T) {
|
|
m, err := New([]byte{}, "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
l1Called := false
|
|
m.RegisterCacheTrigger(func(c *cache.Config) {
|
|
l1Called = true
|
|
})
|
|
|
|
if m.registeredCacheTriggers[0] == nil {
|
|
t.Fatal("First listener failed to register")
|
|
}
|
|
|
|
l2Called := false
|
|
m.RegisterCacheTrigger(func(c *cache.Config) {
|
|
l2Called = true
|
|
})
|
|
|
|
if m.registeredCacheTriggers[0] == nil || m.registeredCacheTriggers[1] == nil {
|
|
t.Fatal("Second listener failed to register")
|
|
}
|
|
|
|
if l1Called == true || l2Called == true {
|
|
t.Fatal("Listeners should not be called yet")
|
|
}
|
|
|
|
m.Reconfigure(m.Config)
|
|
|
|
if l1Called == false || l2Called == false {
|
|
t.Fatal("Listeners should hav been called")
|
|
}
|
|
}
|
|
|
|
func TestManagerPluginStatusListener(t *testing.T) {
|
|
m, err := New([]byte{}, "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
// Start by registering a single listener and validate that it was registered correctly
|
|
var l1Status map[string]*Status
|
|
m.RegisterPluginStatusListener("l1", func(status map[string]*Status) {
|
|
l1Status = status
|
|
})
|
|
if len(m.pluginStatusListeners) != 1 || m.pluginStatusListeners["l1"] == nil {
|
|
t.Fatalf("Expected a single listener named 'l1' got: %+v", m.pluginStatusListeners)
|
|
}
|
|
|
|
// Register a second one, validate both are there
|
|
var l2Status map[string]*Status
|
|
m.RegisterPluginStatusListener("l2", func(status map[string]*Status) {
|
|
l2Status = status
|
|
})
|
|
if len(m.pluginStatusListeners) != 2 || m.pluginStatusListeners["l2"] == nil {
|
|
t.Fatalf("Expected a two listeners named 'l1' and 'l2' got: %+v", m.pluginStatusListeners)
|
|
}
|
|
|
|
// Ensure starting statuses are empty by default
|
|
currentStatus := m.PluginStatus()
|
|
if len(currentStatus) != 0 {
|
|
t.Fatalf("Expected 0 statuses in current plugin status map, got: %+v", currentStatus)
|
|
}
|
|
|
|
// Push an update to a plugin, ensure current status is reflected and listeners were called
|
|
m.UpdatePluginStatus("p1", &Status{State: StateOK, Message: "foo"})
|
|
currentStatus = m.PluginStatus()
|
|
if len(currentStatus) != 1 || currentStatus["p1"].State != StateOK || currentStatus["p1"].Message != "foo" {
|
|
t.Fatalf("Expected 1 statuses in current plugin status map with state OK and message 'foo', got: %+v", currentStatus)
|
|
}
|
|
if !reflect.DeepEqual(currentStatus, l1Status) || !reflect.DeepEqual(l1Status, l2Status) {
|
|
t.Fatalf("Unexpected status in updates:\n\n\texpecting: %+v\n\n\tgot: l1: %+v l2: %+v\n", currentStatus, l1Status, l2Status)
|
|
}
|
|
|
|
// Unregister the first listener, ensure it is removed
|
|
m.UnregisterPluginStatusListener("l1")
|
|
if len(m.pluginStatusListeners) != 1 || m.pluginStatusListeners["l2"] == nil {
|
|
t.Fatalf("Expected a single listeners named 'l2' got: %+v", m.pluginStatusListeners)
|
|
}
|
|
|
|
// Send another update, ensure the status is ok and the remaining listener is still called
|
|
m.UpdatePluginStatus("p2", &Status{State: StateErr})
|
|
currentStatus = m.PluginStatus()
|
|
if len(currentStatus) != 2 || currentStatus["p1"].State != StateOK || currentStatus["p1"].Message != "foo" || currentStatus["p2"].State != StateErr {
|
|
t.Fatalf("Unexpected current plugin status, got: %+v", currentStatus)
|
|
}
|
|
if !reflect.DeepEqual(currentStatus, l2Status) {
|
|
t.Fatalf("Unexpected status in updates:\n\n\texpecting: %+v\n\n\tgot: %+v\n", currentStatus, l2Status)
|
|
}
|
|
|
|
// Unregister the last listener
|
|
m.UnregisterPluginStatusListener("l2")
|
|
if len(m.pluginStatusListeners) != 0 {
|
|
t.Fatalf("Expected zero listeners got: %+v", m.pluginStatusListeners)
|
|
}
|
|
|
|
// Ensure updates can still be sent with no listeners
|
|
m.UpdatePluginStatus("p2", &Status{State: StateOK})
|
|
currentStatus = m.PluginStatus()
|
|
if len(currentStatus) != 2 || currentStatus["p1"].State != StateOK || currentStatus["p1"].Message != "foo" || currentStatus["p2"].State != StateOK {
|
|
t.Fatalf("Unexpected current plugin status, got: %+v", currentStatus)
|
|
}
|
|
}
|
|
|
|
func TestPluginStatusUpdateOnStartAndStop(t *testing.T) {
|
|
m, err := New([]byte{}, "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
m.Register("p1", &testPlugin{m})
|
|
|
|
err = m.Start(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
m.Stop(context.Background())
|
|
}
|
|
|
|
type testPlugin struct {
|
|
m *Manager
|
|
}
|
|
|
|
func (p *testPlugin) Start(ctx context.Context) error {
|
|
p.m.UpdatePluginStatus("p1", &Status{State: StateOK})
|
|
return nil
|
|
}
|
|
|
|
func (p *testPlugin) Stop(ctx context.Context) {
|
|
p.m.UpdatePluginStatus("p1", &Status{State: StateNotReady})
|
|
}
|
|
|
|
func (p *testPlugin) Reconfigure(ctx context.Context, config interface{}) {
|
|
p.m.UpdatePluginStatus("p1", &Status{State: StateNotReady})
|
|
}
|
|
|
|
func TestPluginManagerLazyInitBeforePluginStart(t *testing.T) {
|
|
|
|
m, err := New([]byte(`{"plugins": {"someplugin": {"enabled": true}}}`), "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mock := &mockForInitStartOrdering{Manager: m}
|
|
|
|
m.Register("someplugin", mock)
|
|
|
|
if err := m.Start(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !mock.Started {
|
|
t.Fatal("expected plugin to be started")
|
|
}
|
|
|
|
}
|
|
|
|
func TestPluginManagerInitBeforePluginStart(t *testing.T) {
|
|
|
|
m, err := New([]byte(`{"plugins": {"someplugin": {}}}`), "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := m.Init(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mock := &mockForInitStartOrdering{Manager: m}
|
|
|
|
m.Register("someplugin", mock)
|
|
|
|
if err := m.Start(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !mock.Started {
|
|
t.Fatal("expected plugin to be started")
|
|
}
|
|
|
|
}
|
|
|
|
func TestPluginManagerInitIdempotence(t *testing.T) {
|
|
|
|
mockStore := mock.New()
|
|
|
|
m, err := New([]byte(`{"plugins": {"someplugin": {}}}`), "test", mockStore)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
if err := m.Init(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
exp := len(mockStore.Transactions)
|
|
|
|
if err := m.Init(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(mockStore.Transactions) != exp {
|
|
t.Fatal("expected num txns to be:", exp, "but got:", len(mockStore.Transactions))
|
|
}
|
|
|
|
}
|
|
|
|
func TestManagerWithCachingConfig(t *testing.T) {
|
|
m, err := New([]byte(`{"caching": {"inter_query_builtin_cache": {"max_size_bytes": 100}}}`), "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected, _ := cache.ParseCachingConfig(nil)
|
|
limit := int64(100)
|
|
expected.InterQueryBuiltinCache.MaxSizeBytes = &limit
|
|
|
|
if !reflect.DeepEqual(m.InterQueryBuiltinCacheConfig(), expected) {
|
|
t.Fatalf("want %+v got %+v", expected, m.interQueryBuiltinCacheConfig)
|
|
}
|
|
|
|
// config error
|
|
_, err = New([]byte(`{"caching": {"inter_query_builtin_cache": {"max_size_bytes": "100"}}}`), "test", inmem.New())
|
|
if err == nil {
|
|
t.Fatal("expected error but got nil")
|
|
}
|
|
}
|
|
|
|
type mockForInitStartOrdering struct {
|
|
Manager *Manager
|
|
Started bool
|
|
}
|
|
|
|
func (m *mockForInitStartOrdering) Start(ctx context.Context) error {
|
|
m.Started = true
|
|
if m.Manager.initialized {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("expected manager to be initialized")
|
|
}
|
|
|
|
func (m *mockForInitStartOrdering) Stop(ctx context.Context) { return }
|
|
func (m *mockForInitStartOrdering) Reconfigure(ctx context.Context, config interface{}) { return }
|
|
|
|
func TestPluginManagerAuthPlugin(t *testing.T) {
|
|
m, err := New([]byte(`{"plugins": {"someplugin": {}}}`), "test", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := m.Init(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mock := &myAuthPluginMock{}
|
|
|
|
m.Register("someplugin", mock)
|
|
|
|
authPlugin := m.AuthPlugin("someplugin")
|
|
|
|
if authPlugin == nil {
|
|
t.Fatal("expected to receive HTTPAuthPlugin")
|
|
}
|
|
|
|
switch authPlugin.(type) {
|
|
case *myAuthPluginMock:
|
|
return
|
|
default:
|
|
t.Fatal("expected HTTPAuthPlugin to be myAuthPluginMock")
|
|
}
|
|
}
|
|
|
|
func TestPluginManagerLogger(t *testing.T) {
|
|
|
|
logger := logging.NewStandardLogger().WithFields(map[string]interface{}{"context": "myloggincontext"})
|
|
|
|
m, err := New([]byte(`{}`), "test", inmem.New(), Logger(logger))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if m.Logger() != logger {
|
|
t.Fatal("Logger was not configured on plugin manager")
|
|
}
|
|
}
|
|
|
|
func TestPluginManagerConsoleLogger(t *testing.T) {
|
|
consoleLogger := test.New()
|
|
|
|
mgr, err := New([]byte(`{}`), "", inmem.New(), ConsoleLogger(consoleLogger))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mgr.ConsoleLogger().WithFields(map[string]interface{}{"foo": "bar"}).Info("Some message")
|
|
|
|
entries := consoleLogger.Entries()
|
|
|
|
exp := []test.LogEntry{
|
|
{
|
|
Level: logging.Info,
|
|
Fields: map[string]interface{}{"foo": "bar"},
|
|
Message: "Some message",
|
|
},
|
|
}
|
|
|
|
if !reflect.DeepEqual(exp, entries) {
|
|
t.Fatalf("want %v but got %v", exp, entries)
|
|
}
|
|
}
|
|
|
|
type myAuthPluginMock struct{}
|
|
|
|
func (m *myAuthPluginMock) NewClient(c rest.Config) (*http.Client, error) {
|
|
tlsConfig, err := rest.DefaultTLSConfig(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return rest.DefaultRoundTripperClient(
|
|
tlsConfig,
|
|
10,
|
|
), nil
|
|
}
|
|
func (*myAuthPluginMock) Prepare(*http.Request) error {
|
|
return nil
|
|
}
|
|
func (*myAuthPluginMock) Start(context.Context) error {
|
|
return nil
|
|
}
|
|
func (*myAuthPluginMock) Stop(context.Context) {
|
|
}
|
|
func (*myAuthPluginMock) Reconfigure(context.Context, interface{}) {
|
|
}
|