Files
releases/plugins/plugins_test.go
T
Ashutosh Narkar d12e959922 Add support to enable ND builtin cache via discovery (#5468)
This commit adds support for enabling/disabling the ND builtin
cache via the OPA `discovery` plugin.

Fixes: #5457

Signed-off-by: Ashutosh Narkar <anarkar4387@gmail.com>
Signed-off-by: Philip Conrad <philipaconrad@gmail.com>
Co-authored-by: Philip Conrad <philipaconrad@gmail.com>
2022-12-19 16:30:11 -05:00

506 lines
13 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"
internal_tracing "github.com/open-policy-agent/opa/internal/distributedtracing"
"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"
inmem "github.com/open-policy-agent/opa/storage/inmem/test"
"github.com/open-policy-agent/opa/topdown/cache"
prom "github.com/prometheus/client_golang/prometheus"
)
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(*cache.Config) {
l1Called = true
})
if m.registeredCacheTriggers[0] == nil {
t.Fatal("First listener failed to register")
}
l2Called := false
m.RegisterCacheTrigger(func(*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")
}
err = m.Reconfigure(m.Config)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
if l1Called == false || l2Called == false {
t.Fatal("Listeners should hav been called")
}
}
func TestManagerNDCacheTriggers(t *testing.T) {
m, err := New([]byte{}, "test", inmem.New())
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
l1Called := false
m.RegisterNDCacheTrigger(func(bool) {
l1Called = true
})
if m.registeredNDCacheTriggers[0] == nil {
t.Fatal("First listener failed to register")
}
l2Called := false
m.RegisterNDCacheTrigger(func(bool) {
l2Called = true
})
if m.registeredNDCacheTriggers[0] == nil || m.registeredNDCacheTriggers[1] == nil {
t.Fatal("Second listener failed to register")
}
if l1Called == true || l2Called == true {
t.Fatal("Listeners should not be called yet")
}
err = m.Reconfigure(m.Config)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
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
const message = "foo"
m.UpdatePluginStatus("p1", &Status{State: StateOK, Message: message})
currentStatus = m.PluginStatus()
if len(currentStatus) != 1 || currentStatus["p1"].State != StateOK || currentStatus["p1"].Message != message {
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 != message || 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 != message || 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(context.Context) error {
p.m.UpdatePluginStatus("p1", &Status{State: StateOK})
return nil
}
func (p *testPlugin) Stop(context.Context) {
p.m.UpdatePluginStatus("p1", &Status{State: StateNotReady})
}
func (p *testPlugin) Reconfigure(context.Context, 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")
}
}
func TestManagerWithNDCachingConfig(t *testing.T) {
m, err := New([]byte(`{"nd_builtin_cache": true}`), "test", inmem.New())
if err != nil {
t.Fatal(err)
}
expected := true
if !m.Config.NDBuiltinCache == expected {
t.Fatalf("want %+v got %+v", expected, m.Config.NDBuiltinCache)
}
// config error
_, err = New([]byte(`{"nd_builtin_cache": "x"}`), "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 (*mockForInitStartOrdering) Stop(context.Context) {}
func (*mockForInitStartOrdering) Reconfigure(context.Context, interface{}) {}
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.Get().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)
}
const fieldKey = "foo"
const fieldValue = "bar"
mgr.ConsoleLogger().WithFields(map[string]interface{}{fieldKey: fieldValue}).Info("Some message")
entries := consoleLogger.Entries()
exp := []test.LogEntry{
{
Level: logging.Info,
Fields: map[string]interface{}{fieldKey: fieldValue},
Message: "Some message",
},
}
if !reflect.DeepEqual(exp, entries) {
t.Fatalf("want %v but got %v", exp, entries)
}
}
func TestPluginManagerPrometheusRegister(t *testing.T) {
register := prometheusRegisterMock{Collectors: map[prom.Collector]bool{}}
mgr, err := New([]byte(`{}`), "", inmem.New(), WithPrometheusRegister(register))
if err != nil {
t.Fatal(err)
}
counter := prom.NewCounter(prom.CounterOpts{})
if err := mgr.PrometheusRegister().Register(counter); err != nil {
t.Fatal(err)
}
if register.Collectors[counter] != true {
t.Fatalf("Counter metric was not registered on prometheus")
}
}
func TestPluginManagerTracerProvider(t *testing.T) {
_, tracerProvider, err := internal_tracing.Init(context.TODO(), []byte(`{ "distributed_tracing": { "type": "grpc" } }`), "test")
if err != nil {
t.Fatal(err)
}
m, err := New([]byte(`{}`), "test", inmem.New(), WithTracerProvider(tracerProvider))
if err != nil {
t.Fatal(err)
}
if m.TracerProvider() != tracerProvider {
t.Fatal("TracerProvider was not configured on plugin manager")
}
}
func TestPluginManagerServerInitialized(t *testing.T) {
// Verify that ServerInitializedChannel is closed when
// ServerInitialized is called.
m1, err := New([]byte{}, "test1", inmem.New())
if err != nil {
t.Fatal(err)
}
initChannel1 := m1.ServerInitializedChannel()
m1.ServerInitialized()
// Verify that ServerInitialized is idempotent and will not panic
m1.ServerInitialized()
select {
case <-initChannel1:
break
default:
t.Fatal("expected ServerInitializedChannel to be closed")
}
// Verify that ServerInitializedChannel is open when
// ServerInitialized is not called.
m2, err := New([]byte{}, "test2", inmem.New())
if err != nil {
t.Fatal(err)
}
initChannel2 := m2.ServerInitializedChannel()
select {
case <-initChannel2:
t.Fatal("expected ServerInitializedChannel to be open and have no messages")
default:
break
}
}
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{}) {
}
type prometheusRegisterMock struct {
Collectors map[prom.Collector]bool
}
func (p prometheusRegisterMock) Register(collector prom.Collector) error {
p.Collectors[collector] = true
return nil
}
func (p prometheusRegisterMock) MustRegister(collector ...prom.Collector) {
for _, c := range collector {
p.Collectors[c] = true
}
}
func (p prometheusRegisterMock) Unregister(collector prom.Collector) bool {
delete(p.Collectors, collector)
return true
}