mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-13 03:42:35 -06:00
0dfc7641c9
Previously, the bundle plugin was only updating the status struct if: * a download or activation error occurred. * a new bundle was successfully activated. As a result, if a transient bundle download/activation error occcured, OPA would never report that the error condition had been cleared in the event that the service began serving HTTP 304s. These changes update the bundle plugin to always set the error status regardles of whether an error occurred or a new bundle was activated. Also, refactor the bundle plugin to set all of the activation state (timestamp, revision, etag) in one place. Fixes #794 Signed-off-by: Torin Sandall <torinsandall@gmail.com>
614 lines
13 KiB
Go
614 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 bundle
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/open-policy-agent/opa/ast"
|
|
"github.com/open-policy-agent/opa/bundle"
|
|
"github.com/open-policy-agent/opa/plugins"
|
|
"github.com/open-policy-agent/opa/storage"
|
|
"github.com/open-policy-agent/opa/storage/inmem"
|
|
"github.com/open-policy-agent/opa/util"
|
|
)
|
|
|
|
func TestNew(t *testing.T) {
|
|
|
|
store := inmem.New()
|
|
|
|
manager, err := plugins.New([]byte(`{
|
|
"services": [
|
|
{
|
|
"name": "foo"
|
|
}
|
|
]
|
|
}`), "test-instance-id", store)
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := []struct {
|
|
input string
|
|
wantErr bool
|
|
expMin time.Duration
|
|
expMax time.Duration
|
|
}{
|
|
{
|
|
input: `{
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
input: `{
|
|
"name": "a/b/c",
|
|
"service": "missing service"
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
input: `{
|
|
"name": "bad/delays",
|
|
"service": "foo",
|
|
"polling": {
|
|
"min_delay_seconds": 10,
|
|
"max_delay_seconds": 1
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
input: `{
|
|
"name": "defaults",
|
|
"service": "foo"
|
|
}`,
|
|
expMin: time.Second * time.Duration(defaultMinDelaySeconds),
|
|
expMax: time.Second * time.Duration(defaultMaxDelaySeconds),
|
|
},
|
|
{
|
|
input: `{
|
|
"name": "missing/min",
|
|
"service": "foo",
|
|
"polling": {
|
|
"max_delay_seconds": 10
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
input: `{
|
|
"name": "missing/max",
|
|
"service": "foo",
|
|
"polling": {
|
|
"min_delay_seconds": 1
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
input: `{
|
|
"name": "user/min/max",
|
|
"service": "foo",
|
|
"polling": {
|
|
"min_delay_seconds": 10,
|
|
"max_delay_seconds": 30
|
|
}
|
|
}`,
|
|
expMin: time.Second * 10,
|
|
expMax: time.Second * 30,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
p, err := New([]byte(test.input), manager)
|
|
if err != nil && !test.wantErr {
|
|
t.Errorf("Unexpected error on: %v, err: %v", test.input, err)
|
|
}
|
|
if err == nil {
|
|
if time.Duration(*p.config.Polling.MinDelaySeconds) != test.expMin {
|
|
t.Errorf("For %q expected min %v but got %v", p.config.Name, test.expMin, time.Duration(*p.config.Polling.MinDelaySeconds))
|
|
}
|
|
if time.Duration(*p.config.Polling.MaxDelaySeconds) != test.expMax {
|
|
t.Errorf("For %q expected min %v but got %v", p.config.Name, test.expMax, time.Duration(*p.config.Polling.MaxDelaySeconds))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPluginStart(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
fixture := newTestFixture(t)
|
|
defer fixture.server.stop()
|
|
|
|
if err := fixture.plugin.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
txn := storage.NewTransactionOrDie(ctx, fixture.store, storage.WriteParams)
|
|
done := make(chan struct{})
|
|
|
|
fixture.store.Register(ctx, txn, storage.TriggerConfig{
|
|
OnCommit: func(ctx context.Context, txn storage.Transaction, event storage.TriggerEvent) {
|
|
if !event.DataChanged() || !event.PolicyChanged() {
|
|
return
|
|
}
|
|
ids, err := fixture.store.ListPolicies(ctx, txn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if len(ids) != 1 {
|
|
t.Fatal("Expected 1 policy")
|
|
}
|
|
bs, err := fixture.store.GetPolicy(ctx, txn, ids[0])
|
|
exp := []byte("package foo\n\ncorge=1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if !bytes.Equal(bs, exp) {
|
|
t.Fatalf("Bad policy content. Exp:\n%v\n\nGot:\n\n%v", string(exp), string(bs))
|
|
}
|
|
data, err := fixture.store.Read(ctx, txn, storage.Path{})
|
|
expData := util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}, "system": {"bundle": {"manifest": {"revision": "quickbrownfaux"}}}}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if !reflect.DeepEqual(data, expData) {
|
|
t.Fatalf("Bad data content. Exp:\n%v\n\nGot:\n\n%v", expData, data)
|
|
}
|
|
done <- struct{}{}
|
|
},
|
|
})
|
|
|
|
if err := fixture.store.Commit(ctx, txn); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
<-done
|
|
fixture.plugin.Stop(ctx)
|
|
}
|
|
|
|
func TestPluginEtagCaching(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
fixture := newTestFixture(t)
|
|
fixture.server.expEtag = "some etag value"
|
|
defer fixture.server.stop()
|
|
|
|
updated, err := fixture.plugin.oneShot(ctx)
|
|
if err != nil {
|
|
t.Fatal("Unexpected:", err)
|
|
} else if !updated {
|
|
t.Fatal("expected update")
|
|
}
|
|
|
|
updated, err = fixture.plugin.oneShot(ctx)
|
|
if err != nil {
|
|
t.Fatal("Unexpected:", err)
|
|
} else if updated {
|
|
t.Fatal("expected not update")
|
|
}
|
|
}
|
|
|
|
func TestPluginFailureAuthn(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
fixture := newTestFixture(t)
|
|
fixture.server.expAuth = "Bearer anothersecret"
|
|
defer fixture.server.stop()
|
|
|
|
_, err := fixture.plugin.oneShot(ctx)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|
|
|
|
func TestPluginFailureNotFound(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
fixture := newTestFixture(t)
|
|
delete(fixture.server.bundles, "test/bundle1")
|
|
defer fixture.server.stop()
|
|
|
|
_, err := fixture.plugin.oneShot(ctx)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|
|
|
|
func TestPluginFailureUnexpected(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
fixture := newTestFixture(t)
|
|
fixture.server.expCode = 500
|
|
defer fixture.server.stop()
|
|
|
|
_, err := fixture.plugin.oneShot(ctx)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|
|
|
|
func TestPluginFailureCompile(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
fixture := newTestFixture(t)
|
|
defer fixture.server.stop()
|
|
|
|
_, err := fixture.plugin.oneShot(ctx)
|
|
if err != nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
|
|
fixture.server.bundles["test/bundle1"] = bundle.Bundle{
|
|
Data: map[string]interface{}{},
|
|
Modules: []bundle.ModuleFile{
|
|
{
|
|
Path: `/example.rego`,
|
|
Raw: []byte("package foo\n\np[x]"),
|
|
},
|
|
},
|
|
}
|
|
|
|
_, err = fixture.plugin.oneShot(ctx)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
|
|
// ensure data/policy is intact
|
|
txn := storage.NewTransactionOrDie(ctx, fixture.store, storage.TransactionParams{})
|
|
|
|
_, err = fixture.store.GetPolicy(ctx, txn, "/example.rego")
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
data, err := fixture.store.Read(ctx, txn, storage.Path{})
|
|
if err != nil || data == nil {
|
|
t.Fatalf("Expected data to be intact but got: %v, err: %v", data, err)
|
|
}
|
|
}
|
|
|
|
func TestPluginActivatationRemovesOld(t *testing.T) {
|
|
|
|
managerConfig := []byte(`{
|
|
"services": [
|
|
{
|
|
"name": "example",
|
|
"url": "http://localhost"
|
|
}
|
|
]
|
|
}`)
|
|
store := inmem.New()
|
|
manager, err := plugins.New(managerConfig, "test-instance-id", store)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
p, err := New([]byte(`{"name": "test", "service": "example"}`), manager)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
module1 := `package example
|
|
|
|
p = 1`
|
|
|
|
b := bundle.Bundle{
|
|
Data: map[string]interface{}{
|
|
"foo": "bar",
|
|
},
|
|
Modules: []bundle.ModuleFile{
|
|
{
|
|
Path: "/example.rego",
|
|
Raw: []byte(module1),
|
|
Parsed: ast.MustParseModule(module1),
|
|
},
|
|
},
|
|
}
|
|
|
|
if err := p.activate(ctx, "firstetag", b); err != nil {
|
|
t.Fatal("Unexpected:", err)
|
|
}
|
|
|
|
module2 := `package example
|
|
|
|
p = 2`
|
|
|
|
b2 := bundle.Bundle{
|
|
Data: map[string]interface{}{
|
|
"baz": "qux",
|
|
},
|
|
Modules: []bundle.ModuleFile{
|
|
{
|
|
Path: "/example2.rego",
|
|
Raw: []byte(module2),
|
|
Parsed: ast.MustParseModule(module2),
|
|
},
|
|
},
|
|
}
|
|
|
|
if err := p.activate(ctx, "secondetag", b2); err != nil {
|
|
t.Fatal("Unexpected:", err)
|
|
}
|
|
|
|
err = storage.Txn(ctx, store, storage.TransactionParams{}, func(txn storage.Transaction) error {
|
|
ids, err := store.ListPolicies(ctx, txn)
|
|
if err != nil {
|
|
return err
|
|
} else if !reflect.DeepEqual([]string{"/example2.rego"}, ids) {
|
|
return fmt.Errorf("expected updated policy ids")
|
|
}
|
|
data, err := store.Read(ctx, txn, storage.Path{})
|
|
// remove system key to make comparison simpler
|
|
delete(data.(map[string]interface{}), "system")
|
|
if err != nil {
|
|
return err
|
|
} else if !reflect.DeepEqual(data, map[string]interface{}{"baz": "qux"}) {
|
|
return fmt.Errorf("expected updated data")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatal("Unexpected:", err)
|
|
}
|
|
}
|
|
|
|
func TestPluginListener(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
fixture := newTestFixture(t)
|
|
defer fixture.server.stop()
|
|
|
|
b := fixture.server.bundles["test/bundle1"]
|
|
ch := make(chan Status, 1)
|
|
|
|
fixture.plugin.Register("test", func(status Status) {
|
|
ch <- status
|
|
})
|
|
|
|
// Test that initial bundle is ok.
|
|
fixture.plugin.oneShot(ctx)
|
|
s1 := <-ch
|
|
|
|
if s1.ActiveRevision != "quickbrownfaux" || s1.Code != "" {
|
|
t.Fatal("Unexpected status update, got:", s1)
|
|
}
|
|
|
|
// Test that next update is failed.
|
|
b.Manifest.Revision = "slowgreenburd"
|
|
b.Modules[0] = bundle.ModuleFile{
|
|
Path: "/foo.rego",
|
|
Raw: []byte("package gork\np[x]"),
|
|
}
|
|
fixture.server.bundles["test/bundle1"] = b
|
|
|
|
fixture.plugin.oneShot(ctx)
|
|
s2 := <-ch
|
|
|
|
if s2.ActiveRevision != "quickbrownfaux" || s2.Code == "" || s2.Message == "" || len(s2.Errors) == 0 {
|
|
t.Fatal("Unexpected status update, got:", s2)
|
|
}
|
|
|
|
// Test that new update is successful.
|
|
b.Manifest.Revision = "fancybluederg"
|
|
b.Modules[0] = bundle.ModuleFile{
|
|
Path: "/foo.rego",
|
|
Raw: []byte("package gork\np[1]"),
|
|
}
|
|
fixture.server.bundles["test/bundle1"] = b
|
|
fixture.server.expEtag = "etagvalue"
|
|
|
|
fixture.plugin.oneShot(ctx)
|
|
s3 := <-ch
|
|
|
|
if s3.ActiveRevision != "fancybluederg" || s3.Code != "" || s3.Message != "" || len(s3.Errors) != 0 {
|
|
t.Fatal("Unexpected status update, got:", s3)
|
|
}
|
|
|
|
// Test that 304 results in status update.
|
|
fixture.plugin.oneShot(ctx)
|
|
s4 := <-ch
|
|
|
|
if !reflect.DeepEqual(s3, s4) {
|
|
t.Fatalf("Expected: %v but got: %v", s3, s4)
|
|
}
|
|
|
|
}
|
|
|
|
func TestPluginListenerErrorClearedOn304(t *testing.T) {
|
|
ctx := context.Background()
|
|
fixture := newTestFixture(t)
|
|
defer fixture.server.stop()
|
|
|
|
// b := fixture.server.bundles["test/bundle1"]
|
|
ch := make(chan Status, 1)
|
|
|
|
fixture.plugin.Register("test", func(status Status) {
|
|
ch <- status
|
|
})
|
|
|
|
// Test that initial bundle is ok.
|
|
fixture.server.expEtag = "etagvalue"
|
|
fixture.plugin.oneShot(ctx)
|
|
s1 := <-ch
|
|
|
|
if s1.ActiveRevision != "quickbrownfaux" || s1.Code != "" {
|
|
t.Fatal("Unexpected status update, got:", s1)
|
|
}
|
|
|
|
// Test that service error triggers failure notification.
|
|
fixture.server.expCode = 500
|
|
fixture.plugin.oneShot(ctx)
|
|
s2 := <-ch
|
|
|
|
if s2.ActiveRevision != "quickbrownfaux" || s2.Code == "" {
|
|
t.Fatal("Unexpected status update, got:", s2)
|
|
}
|
|
|
|
// Test that service recovery triggers healthy notification.
|
|
fixture.server.expCode = 304
|
|
fixture.plugin.oneShot(ctx)
|
|
s3 := <-ch
|
|
|
|
if s3.ActiveRevision != "quickbrownfaux" || s3.Code != "" {
|
|
t.Fatal("Unexpected status update, got:", s3)
|
|
}
|
|
}
|
|
|
|
type testFixture struct {
|
|
store storage.Store
|
|
manager *plugins.Manager
|
|
plugin *Plugin
|
|
server *testServer
|
|
}
|
|
|
|
func newTestFixture(t *testing.T) testFixture {
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
expAuth: "Bearer secret",
|
|
bundles: map[string]bundle.Bundle{
|
|
"test/bundle1": {
|
|
Manifest: bundle.Manifest{
|
|
Revision: "quickbrownfaux",
|
|
},
|
|
Data: map[string]interface{}{
|
|
"foo": map[string]interface{}{
|
|
"bar": json.Number("1"),
|
|
"baz": "qux",
|
|
},
|
|
},
|
|
Modules: []bundle.ModuleFile{
|
|
{
|
|
Path: `/example.rego`,
|
|
Raw: []byte("package foo\n\ncorge=1"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
ts.start()
|
|
|
|
managerConfig := []byte(fmt.Sprintf(`{
|
|
"services": [
|
|
{
|
|
"name": "example",
|
|
"url": %q,
|
|
"credentials": {
|
|
"bearer": {
|
|
"scheme": "Bearer",
|
|
"token": "secret"
|
|
}
|
|
}
|
|
}
|
|
]}`, ts.server.URL))
|
|
|
|
store := inmem.New()
|
|
|
|
manager, err := plugins.New(managerConfig, "test-instance-id", store)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pluginConfig := []byte(fmt.Sprintf(`{
|
|
"name": "test/bundle1",
|
|
"service": "example",
|
|
"polling": {
|
|
"min_delay_seconds": 1,
|
|
"max_delay_seconds": 1
|
|
}
|
|
}`))
|
|
|
|
p, err := New(pluginConfig, manager)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return testFixture{
|
|
store: store,
|
|
manager: manager,
|
|
plugin: p,
|
|
server: &ts,
|
|
}
|
|
|
|
}
|
|
|
|
type testServer struct {
|
|
t *testing.T
|
|
expCode int
|
|
expEtag string
|
|
expAuth string
|
|
bundles map[string]bundle.Bundle
|
|
server *httptest.Server
|
|
}
|
|
|
|
func (t *testServer) handle(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if t.expCode != 0 {
|
|
w.WriteHeader(t.expCode)
|
|
return
|
|
}
|
|
|
|
if t.expAuth != "" {
|
|
if r.Header.Get("Authorization") != t.expAuth {
|
|
w.WriteHeader(401)
|
|
return
|
|
}
|
|
}
|
|
|
|
name := strings.TrimPrefix(r.URL.Path, "/bundles/")
|
|
b, ok := t.bundles[name]
|
|
if !ok {
|
|
w.WriteHeader(404)
|
|
return
|
|
}
|
|
|
|
if t.expEtag != "" {
|
|
etag := r.Header.Get("If-None-Match")
|
|
if etag == t.expEtag {
|
|
w.WriteHeader(304)
|
|
return
|
|
}
|
|
}
|
|
|
|
w.Header().Add("Content-Type", "application/gzip")
|
|
|
|
if t.expEtag != "" {
|
|
w.Header().Add("Etag", t.expEtag)
|
|
}
|
|
|
|
w.WriteHeader(200)
|
|
|
|
var buf bytes.Buffer
|
|
|
|
if err := bundle.Write(&buf, b); err != nil {
|
|
w.WriteHeader(500)
|
|
}
|
|
|
|
if _, err := w.Write(buf.Bytes()); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (t *testServer) start() {
|
|
t.server = httptest.NewServer(http.HandlerFunc(t.handle))
|
|
}
|
|
|
|
func (t *testServer) stop() {
|
|
t.server.Close()
|
|
}
|