mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-19 06:51:18 -06:00
778594ec79
Adds OPA version to the config labels, so that the version appears in status updates. Previously, decision logs had a top-level version field. This removes that field because version will be contained in the labels instead. Signed-off-by: Andy Curtis <arcurtis@gmail.com>
564 lines
13 KiB
Go
564 lines
13 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 logs
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/open-policy-agent/opa/ast"
|
|
"github.com/open-policy-agent/opa/metrics"
|
|
"github.com/open-policy-agent/opa/plugins"
|
|
"github.com/open-policy-agent/opa/server"
|
|
"github.com/open-policy-agent/opa/storage/inmem"
|
|
"github.com/open-policy-agent/opa/version"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
// call flag.Parse() here if TestMain uses flags
|
|
setVersion("XY.Z")
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
type testPlugin struct {
|
|
events []EventV1
|
|
}
|
|
|
|
func (p *testPlugin) Start(context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (p *testPlugin) Stop(context.Context) {
|
|
}
|
|
|
|
func (p *testPlugin) Reconfigure(context.Context, interface{}) {
|
|
}
|
|
|
|
func (p *testPlugin) Log(_ context.Context, event EventV1) {
|
|
p.events = append(p.events, event)
|
|
}
|
|
|
|
func TestPluginCustomBackend(t *testing.T) {
|
|
ctx := context.Background()
|
|
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)
|
|
plugin.Log(ctx, &server.Info{Revision: "A"})
|
|
plugin.Log(ctx, &server.Info{Revision: "B"})
|
|
|
|
if len(backend.events) != 2 || backend.events[0].Revision != "A" || backend.events[1].Revision != "B" {
|
|
t.Fatal("Unexpected events:", backend.events)
|
|
}
|
|
}
|
|
|
|
func TestPluginErrorNoResult(t *testing.T) {
|
|
ctx := context.Background()
|
|
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)
|
|
plugin.Log(ctx, &server.Info{Error: fmt.Errorf("some error")})
|
|
plugin.Log(ctx, &server.Info{Error: ast.Errors{&ast.Error{
|
|
Code: "some_error",
|
|
}}})
|
|
|
|
if len(backend.events) != 2 || backend.events[0].Error == nil || backend.events[1].Error == nil {
|
|
t.Fatal("Unexpected events:", backend.events)
|
|
}
|
|
}
|
|
|
|
func TestPluginQueriesAndPaths(t *testing.T) {
|
|
ctx := context.Background()
|
|
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)
|
|
plugin.Log(ctx, &server.Info{Path: "data.foo"})
|
|
plugin.Log(ctx, &server.Info{Path: "data.foo.bar"})
|
|
plugin.Log(ctx, &server.Info{Query: "a = data.foo"})
|
|
|
|
exp := []struct {
|
|
query string
|
|
path string
|
|
}{
|
|
// TODO(tsandall): we need to fix how POST /v1/data (and
|
|
// friends) are represented here. Currently we can't tell the
|
|
// difference between /v1/data and /v1/data/data. The decision
|
|
// log event paths should be slash prefixed to avoid ambiguity.
|
|
// {path: "data"},
|
|
{path: "foo"},
|
|
{path: "foo/bar"},
|
|
{query: "a = data.foo"},
|
|
}
|
|
|
|
if len(exp) != len(backend.events) {
|
|
t.Fatalf("Expected %d events but got %v", len(exp), len(backend.events))
|
|
}
|
|
|
|
for i, e := range exp {
|
|
if e.query != backend.events[i].Query || e.path != backend.events[i].Path {
|
|
t.Fatalf("Unexpected event %d, want %v but got %v", i, e, backend.events[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPluginStartSameInput(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
fixture := newTestFixture(t)
|
|
defer fixture.server.stop()
|
|
|
|
fixture.server.ch = make(chan []EventV1, 4)
|
|
var result interface{} = false
|
|
|
|
ts, err := time.Parse(time.RFC3339Nano, "2018-01-01T12:00:00.123456Z")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
testMetrics := getWellKnownMetrics()
|
|
|
|
var input interface{} = map[string]interface{}{"method": "GET"}
|
|
|
|
for i := 0; i < 400; i++ {
|
|
fixture.plugin.Log(ctx, &server.Info{
|
|
Revision: fmt.Sprint(i),
|
|
DecisionID: fmt.Sprint(i),
|
|
Path: "data.tda.bar",
|
|
Input: &input,
|
|
Results: &result,
|
|
RemoteAddr: "test",
|
|
Timestamp: ts,
|
|
Metrics: testMetrics,
|
|
})
|
|
}
|
|
|
|
_, err = fixture.plugin.oneShot(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
chunk1 := <-fixture.server.ch
|
|
chunk2 := <-fixture.server.ch
|
|
chunk3 := <-fixture.server.ch
|
|
chunk4 := <-fixture.server.ch
|
|
expLen1 := 122
|
|
expLen2 := 121
|
|
expLen3 := 121
|
|
expLen4 := 36
|
|
|
|
if len(chunk1) != expLen1 || len(chunk2) != expLen2 || len(chunk3) != expLen3 || len(chunk4) != expLen4 {
|
|
t.Fatalf("Expected chunk lens %v, %v, %v and %v but got: %v, %v, %v and %v", expLen1, expLen2, expLen3, expLen4, len(chunk1), len(chunk2), len(chunk3), len(chunk4))
|
|
}
|
|
|
|
var expInput interface{} = map[string]interface{}{"method": "GET"}
|
|
|
|
msAsFloat64 := map[string]interface{}{}
|
|
for k, v := range testMetrics.All() {
|
|
msAsFloat64[k] = float64(v.(uint64))
|
|
}
|
|
|
|
exp := EventV1{
|
|
Labels: map[string]string{
|
|
"id": "test-instance-id",
|
|
"app": "example-app",
|
|
"version": getVersion(),
|
|
},
|
|
Revision: "399",
|
|
DecisionID: "399",
|
|
Path: "tda/bar",
|
|
Input: &expInput,
|
|
Result: &result,
|
|
RequestedBy: "test",
|
|
Timestamp: ts,
|
|
Metrics: msAsFloat64,
|
|
}
|
|
|
|
if !reflect.DeepEqual(chunk4[expLen4-1], exp) {
|
|
t.Fatalf("Expected %+v but got %+v", exp, chunk4[expLen4-1])
|
|
}
|
|
}
|
|
|
|
func TestPluginStartChangingInputValues(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
fixture := newTestFixture(t)
|
|
defer fixture.server.stop()
|
|
|
|
fixture.server.ch = make(chan []EventV1, 4)
|
|
var result interface{} = false
|
|
|
|
ts, err := time.Parse(time.RFC3339Nano, "2018-01-01T12:00:00.123456Z")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var input interface{}
|
|
|
|
for i := 0; i < 400; i++ {
|
|
input = map[string]interface{}{"method": getValueForMethod(i), "path": getValueForPath(i), "user": getValueForUser(i)}
|
|
|
|
fixture.plugin.Log(ctx, &server.Info{
|
|
Revision: fmt.Sprint(i),
|
|
DecisionID: fmt.Sprint(i),
|
|
Path: "data.foo.bar",
|
|
Input: &input,
|
|
Results: &result,
|
|
RemoteAddr: "test",
|
|
Timestamp: ts,
|
|
})
|
|
}
|
|
|
|
_, err = fixture.plugin.oneShot(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
chunk1 := <-fixture.server.ch
|
|
chunk2 := <-fixture.server.ch
|
|
chunk3 := <-fixture.server.ch
|
|
chunk4 := <-fixture.server.ch
|
|
expLen1 := 124
|
|
expLen2 := 123
|
|
expLen3 := 123
|
|
expLen4 := 30
|
|
|
|
if len(chunk1) != expLen1 || len(chunk2) != expLen2 || len((chunk3)) != expLen3 || len(chunk4) != expLen4 {
|
|
t.Fatalf("Expected chunk lens %v, %v, %v and %v but got: %v, %v, %v and %v", expLen1, expLen2, expLen3, expLen4, len(chunk1), len(chunk2), len(chunk3), len(chunk4))
|
|
}
|
|
|
|
var expInput interface{} = input
|
|
|
|
exp := EventV1{
|
|
Labels: map[string]string{
|
|
"id": "test-instance-id",
|
|
"app": "example-app",
|
|
"version": getVersion(),
|
|
},
|
|
Revision: "399",
|
|
DecisionID: "399",
|
|
Path: "foo/bar",
|
|
Input: &expInput,
|
|
Result: &result,
|
|
RequestedBy: "test",
|
|
Timestamp: ts,
|
|
}
|
|
|
|
if !reflect.DeepEqual(chunk4[expLen4-1], exp) {
|
|
t.Fatalf("Expected %+v but got %+v", exp, chunk4[expLen4-1])
|
|
}
|
|
}
|
|
|
|
func TestPluginStartChangingInputKeysAndValues(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
fixture := newTestFixture(t)
|
|
defer fixture.server.stop()
|
|
|
|
fixture.server.ch = make(chan []EventV1, 5)
|
|
var result interface{} = false
|
|
|
|
ts, err := time.Parse(time.RFC3339Nano, "2018-01-01T12:00:00.123456Z")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var input interface{}
|
|
|
|
for i := 0; i < 250; i++ {
|
|
input = generateInputMap(i)
|
|
|
|
fixture.plugin.Log(ctx, &server.Info{
|
|
Revision: fmt.Sprint(i),
|
|
DecisionID: fmt.Sprint(i),
|
|
Path: "data.foo.bar",
|
|
Input: &input,
|
|
Results: &result,
|
|
RemoteAddr: "test",
|
|
Timestamp: ts,
|
|
})
|
|
}
|
|
|
|
_, err = fixture.plugin.oneShot(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
<-fixture.server.ch
|
|
chunk2 := <-fixture.server.ch
|
|
|
|
var expInput interface{} = input
|
|
|
|
exp := EventV1{
|
|
Labels: map[string]string{
|
|
"id": "test-instance-id",
|
|
"app": "example-app",
|
|
"version": getVersion(),
|
|
},
|
|
Revision: "249",
|
|
DecisionID: "249",
|
|
Path: "foo/bar",
|
|
Input: &expInput,
|
|
Result: &result,
|
|
RequestedBy: "test",
|
|
Timestamp: ts,
|
|
}
|
|
|
|
if !reflect.DeepEqual(chunk2[len(chunk2)-1], exp) {
|
|
t.Fatalf("Expected %+v but got %+v", exp, chunk2[len(chunk2)-1])
|
|
}
|
|
}
|
|
|
|
func TestPluginRequeue(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
fixture := newTestFixture(t)
|
|
defer fixture.server.stop()
|
|
|
|
fixture.server.ch = make(chan []EventV1, 1)
|
|
|
|
var input interface{} = map[string]interface{}{"method": "GET"}
|
|
var result1 interface{} = false
|
|
|
|
fixture.plugin.Log(ctx, &server.Info{
|
|
DecisionID: "abc",
|
|
Path: "data.foo.bar",
|
|
Input: &input,
|
|
Results: &result1,
|
|
RemoteAddr: "test",
|
|
Timestamp: time.Now().UTC(),
|
|
})
|
|
|
|
fixture.server.expCode = 500
|
|
_, err := fixture.plugin.oneShot(ctx)
|
|
if err == nil {
|
|
t.Fatal("Expected error")
|
|
}
|
|
|
|
events1 := <-fixture.server.ch
|
|
|
|
fixture.server.expCode = 200
|
|
|
|
_, err = fixture.plugin.oneShot(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
events2 := <-fixture.server.ch
|
|
|
|
if !reflect.DeepEqual(events1, events2) {
|
|
t.Fatalf("Expected %v but got: %v", events1, events2)
|
|
}
|
|
|
|
uploaded, err := fixture.plugin.oneShot(ctx)
|
|
if uploaded || err != nil {
|
|
t.Fatalf("Unexpected error or upload, err: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPluginReconfigure(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
fixture := newTestFixture(t)
|
|
defer fixture.server.stop()
|
|
|
|
if err := fixture.plugin.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
minDelay := 2
|
|
maxDelay := 3
|
|
|
|
pluginConfig := []byte(fmt.Sprintf(`{
|
|
"service": "example",
|
|
"reporting": {
|
|
"min_delay_seconds": %v,
|
|
"max_delay_seconds": %v
|
|
}
|
|
}`, minDelay, maxDelay))
|
|
|
|
config, _ := ParseConfig(pluginConfig, fixture.manager.Services(), nil)
|
|
|
|
fixture.plugin.Reconfigure(ctx, config)
|
|
fixture.plugin.Stop(ctx)
|
|
|
|
actualMin := time.Duration(*fixture.plugin.config.Reporting.MinDelaySeconds) / time.Nanosecond
|
|
expectedMin := time.Duration(minDelay) * time.Second
|
|
|
|
if actualMin != expectedMin {
|
|
t.Fatalf("Expected minimum polling interval: %v but got %v", expectedMin, actualMin)
|
|
}
|
|
|
|
actualMax := time.Duration(*fixture.plugin.config.Reporting.MaxDelaySeconds) / time.Nanosecond
|
|
expectedMax := time.Duration(maxDelay) * time.Second
|
|
|
|
if actualMax != expectedMax {
|
|
t.Fatalf("Expected maximum polling interval: %v but got %v", expectedMax, actualMax)
|
|
}
|
|
}
|
|
|
|
type testFixture struct {
|
|
manager *plugins.Manager
|
|
plugin *Plugin
|
|
server *testServer
|
|
}
|
|
|
|
func newTestFixture(t *testing.T) testFixture {
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
expCode: 200,
|
|
}
|
|
|
|
ts.start()
|
|
|
|
managerConfig := []byte(fmt.Sprintf(`{
|
|
"labels": {
|
|
"app": "example-app"
|
|
},
|
|
"services": [
|
|
{
|
|
"name": "example",
|
|
"url": %q,
|
|
"credentials": {
|
|
"bearer": {
|
|
"scheme": "Bearer",
|
|
"token": "secret"
|
|
}
|
|
}
|
|
}
|
|
]}`, ts.server.URL))
|
|
|
|
manager, err := plugins.New(managerConfig, "test-instance-id", inmem.New())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pluginConfig := []byte(fmt.Sprintf(`{
|
|
"service": "example",
|
|
}`))
|
|
|
|
config, _ := ParseConfig([]byte(pluginConfig), manager.Services(), nil)
|
|
|
|
p := New(config, manager)
|
|
|
|
return testFixture{
|
|
manager: manager,
|
|
plugin: p,
|
|
server: &ts,
|
|
}
|
|
|
|
}
|
|
|
|
type testServer struct {
|
|
t *testing.T
|
|
expCode int
|
|
server *httptest.Server
|
|
ch chan []EventV1
|
|
}
|
|
|
|
func (t *testServer) handle(w http.ResponseWriter, r *http.Request) {
|
|
gr, err := gzip.NewReader(r.Body)
|
|
if err != nil {
|
|
t.t.Fatal(err)
|
|
}
|
|
var events []EventV1
|
|
if err := json.NewDecoder(gr).Decode(&events); err != nil {
|
|
t.t.Fatal(err)
|
|
}
|
|
if err := gr.Close(); err != nil {
|
|
t.t.Fatal(err)
|
|
}
|
|
t.t.Logf("decision log test server received %d events", len(events))
|
|
t.ch <- events
|
|
w.WriteHeader(t.expCode)
|
|
}
|
|
|
|
func (t *testServer) start() {
|
|
t.server = httptest.NewServer(http.HandlerFunc(t.handle))
|
|
}
|
|
|
|
func (t *testServer) stop() {
|
|
t.server.Close()
|
|
}
|
|
|
|
func getValueForMethod(idx int) string {
|
|
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH"}
|
|
return methods[idx%len(methods)]
|
|
}
|
|
|
|
func getValueForPath(idx int) string {
|
|
paths := []string{"/blah1", "/blah2", "/blah3", "/blah4"}
|
|
return paths[idx%len(paths)]
|
|
}
|
|
|
|
func getValueForUser(idx int) string {
|
|
users := []string{"Alice", "Bob", "Charlie", "David", "Ed"}
|
|
return users[idx%len(users)]
|
|
}
|
|
|
|
func generateInputMap(idx int) map[string]interface{} {
|
|
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
result := make(map[string]interface{})
|
|
|
|
for i := 0; i < 20; i++ {
|
|
n := idx % len(letters)
|
|
key := string(letters[n])
|
|
result[key] = fmt.Sprint(idx)
|
|
}
|
|
return result
|
|
|
|
}
|
|
|
|
func setVersion(opaVersion string) {
|
|
if version.Version == "" {
|
|
version.Version = opaVersion
|
|
}
|
|
}
|
|
|
|
func getVersion() string {
|
|
return version.Version
|
|
}
|
|
|
|
func getWellKnownMetrics() metrics.Metrics {
|
|
m := metrics.New()
|
|
m.Counter("test_counter").Incr()
|
|
return m
|
|
}
|