// 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. // nolint: goconst // string duplication is for test readability. package bundle import ( "bytes" "context" "errors" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "os" "path" "path/filepath" "reflect" "sort" "strings" "testing" "time" "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/bundle" "github.com/open-policy-agent/opa/config" "github.com/open-policy-agent/opa/download" "github.com/open-policy-agent/opa/internal/file/archive" "github.com/open-policy-agent/opa/keys" "github.com/open-policy-agent/opa/logging" "github.com/open-policy-agent/opa/metrics" "github.com/open-policy-agent/opa/plugins" "github.com/open-policy-agent/opa/storage" "github.com/open-policy-agent/opa/storage/disk" "github.com/open-policy-agent/opa/storage/inmem" "github.com/open-policy-agent/opa/util" "github.com/open-policy-agent/opa/util/test" ) func TestPluginOneShot(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := New(&Config{}, manager) bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName, Metrics: metrics.New()} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) ensurePluginState(t, plugin, plugins.StateNotReady) module := "package foo\n\ncorge=1" b := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux"}, Data: util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}}`)).(map[string]interface{}), Modules: []bundle.ModuleFile{ { Path: "/foo/bar", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, Etag: "foo", } b.Manifest.Init() plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b, Metrics: metrics.New()}) ensurePluginState(t, plugin, plugins.StateOK) if status, ok := plugin.status[bundleName]; !ok { t.Fatalf("Expected to find status for %s, found nil", bundleName) } else if status.Type != bundle.SnapshotBundleType { t.Fatalf("expected snapshot bundle but got %v", status.Type) } txn := storage.NewTransactionOrDie(ctx, manager.Store) defer manager.Store.Abort(ctx, txn) ids, err := manager.Store.ListPolicies(ctx, txn) if err != nil { t.Fatal(err) } else if len(ids) != 1 { t.Fatal("Expected 1 policy") } bs, err := manager.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 := manager.Store.Read(ctx, txn, storage.Path{}) expData := util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}, "system": {"bundles": {"test-bundle": {"etag": "foo", "manifest": {"revision": "quickbrownfaux", "roots": [""]}}}}}`)) 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) } } func TestPluginStartLazyLoadInMem(t *testing.T) { ctx := context.Background() module := "package authz\n\ncorge=1" // setup fake http server with mock bundle mockBundle1 := bundle.Bundle{ Data: map[string]interface{}{"p": "x1"}, Modules: []bundle.ModuleFile{ { URL: "/bar/policy.rego", Path: "/bar/policy.rego", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, Manifest: bundle.Manifest{ Roots: &[]string{"p", "authz"}, }, } s1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := bundle.NewWriter(w).Write(mockBundle1) if err != nil { t.Fatal(err) } })) mockBundle2 := bundle.Bundle{ Data: map[string]interface{}{"q": "x2"}, Modules: []bundle.ModuleFile{}, Manifest: bundle.Manifest{ Roots: &[]string{"q"}, }, } s2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := bundle.NewWriter(w).Write(mockBundle2) if err != nil { t.Fatal(err) } })) config := []byte(fmt.Sprintf(`{ "services": { "default": { "url": %q }, "acmecorp": { "url": %q } } }`, s1.URL, s2.URL)) manager := getTestManagerWithOpts(config) defer manager.Stop(ctx) var mode plugins.TriggerMode = "manual" plugin := New(&Config{ Bundles: map[string]*Source{ "test-1": { Service: "default", SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), Config: download.Config{Trigger: &mode}, }, "test-2": { Service: "acmecorp", SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), Config: download.Config{Trigger: &mode}, }, }, }, manager) statusCh := make(chan map[string]*Status) // register for bundle updates to observe changes and start the plugin plugin.RegisterBulkListener("test-case", func(st map[string]*Status) { statusCh <- st }) err := plugin.Start(ctx) if err != nil { t.Fatal(err) } // manually trigger bundle download on all configured bundles go func() { _ = plugin.Trigger(ctx) }() // wait for bundle update and then assert on data content <-statusCh <-statusCh result, err := storage.ReadOne(ctx, manager.Store, storage.Path{"p"}) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(result, mockBundle1.Data["p"]) { t.Fatalf("expected data to be %v but got %v", mockBundle1.Data, result) } result, err = storage.ReadOne(ctx, manager.Store, storage.Path{"q"}) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(result, mockBundle2.Data["q"]) { t.Fatalf("expected data to be %v but got %v", mockBundle2.Data, result) } txn := storage.NewTransactionOrDie(ctx, manager.Store) defer manager.Store.Abort(ctx, txn) ids, err := manager.Store.ListPolicies(ctx, txn) if err != nil { t.Fatal(err) } else if len(ids) != 1 { t.Fatal("Expected 1 policy") } bs, err := manager.Store.GetPolicy(ctx, txn, ids[0]) exp := []byte("package authz\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 := manager.Store.Read(ctx, txn, storage.Path{}) expData := util.MustUnmarshalJSON([]byte(`{"p": "x1", "q": "x2", "system": {"bundles": {"test-1": {"etag": "", "manifest": {"revision": "", "roots": ["p", "authz"]}}, "test-2": {"etag": "", "manifest": {"revision": "", "roots": ["q"]}}}}}`)) 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) } } func TestPluginOneShotDiskStorageMetrics(t *testing.T) { test.WithTempFS(nil, func(dir string) { ctx := context.Background() met := metrics.New() store, err := disk.New(ctx, logging.NewNoOpLogger(), nil, disk.Options{ Dir: dir, Partitions: []storage.Path{ storage.MustParsePath("/foo"), }, }) if err != nil { t.Fatal(err) } manager := getTestManagerWithOpts(nil, store) defer manager.Stop(ctx) plugin := New(&Config{}, manager) bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName, Metrics: met} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) ensurePluginState(t, plugin, plugins.StateNotReady) module := "package foo\n\ncorge=1" b := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux"}, Data: util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}}`)).(map[string]interface{}), Modules: []bundle.ModuleFile{ { Path: "/foo/bar", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() met = metrics.New() plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b, Metrics: met}) ensurePluginState(t, plugin, plugins.StateOK) // NOTE(sr): These assertions reflect the current behaviour only! Not prescriptive. name := "disk_deleted_keys" if exp, act := 3, met.Counter(name).Value(); act.(uint64) != uint64(exp) { t.Errorf("%s: expected %v, got %v", name, exp, act) } name = "disk_written_keys" if exp, act := 6, met.Counter(name).Value(); act.(uint64) != uint64(exp) { t.Errorf("%s: expected %v, got %v", name, exp, act) } name = "disk_read_keys" if exp, act := 13, met.Counter(name).Value(); act.(uint64) != uint64(exp) { t.Errorf("%s: expected %v, got %v", name, exp, act) } name = "disk_read_bytes" if exp, act := 269, met.Counter(name).Value(); act.(uint64) != uint64(exp) { t.Errorf("%s: expected %v, got %v", name, exp, act) } for _, timer := range []string{ "disk_commit", "disk_write", "disk_read", } { if act := met.Timer(timer).Int64(); act <= 0 { t.Errorf("%s: expected non-zero timer, got %v", timer, act) } } if t.Failed() { t.Logf("all metrics: %v", met.All()) } // Ensure we can read it all back -- this is the only bundle plugin test using disk storage, // so some duplicating with TestPluginOneShot is OK: txn := storage.NewTransactionOrDie(ctx, manager.Store) defer manager.Store.Abort(ctx, txn) ids, err := manager.Store.ListPolicies(ctx, txn) if err != nil { t.Fatal(err) } else if len(ids) != 1 { t.Fatal("Expected 1 policy") } bs, err := manager.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 := manager.Store.Read(ctx, txn, storage.Path{}) expData := util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}, "system": {"bundles": {"test-bundle": {"etag": "", "manifest": {"revision": "quickbrownfaux", "roots": [""]}}}}}`)) 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) } }) } func TestPluginOneShotDeltaBundle(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := New(&Config{}, manager) bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName, Metrics: metrics.New()} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) ensurePluginState(t, plugin, plugins.StateNotReady) module := "package a\n\ncorge=1" b := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux", Roots: &[]string{"a"}}, Data: map[string]interface{}{ "a": map[string]interface{}{ "baz": "qux", }, }, Modules: []bundle.ModuleFile{ { Path: "a/policy.rego", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b, Metrics: metrics.New()}) ensurePluginState(t, plugin, plugins.StateOK) // simulate a delta bundle download // replace a value p1 := bundle.PatchOperation{ Op: "replace", Path: "a/baz", Value: "bux", } // add a new object member p2 := bundle.PatchOperation{ Op: "upsert", Path: "/a/foo", Value: []string{"hello", "world"}, } b2 := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "delta", Roots: &[]string{"a"}}, Patch: bundle.Patch{Data: []bundle.PatchOperation{p1, p2}}, Etag: "foo", } plugin.process(ctx, bundleName, download.Update{Bundle: &b2, Metrics: metrics.New()}) ensurePluginState(t, plugin, plugins.StateOK) if status, ok := plugin.status[bundleName]; !ok { t.Fatalf("Expected to find status for %s, found nil", bundleName) } else if status.Type != bundle.DeltaBundleType { t.Fatalf("expected delta bundle but got %v", status.Type) } txn := storage.NewTransactionOrDie(ctx, manager.Store) defer manager.Store.Abort(ctx, txn) ids, err := manager.Store.ListPolicies(ctx, txn) if err != nil { t.Fatal(err) } else if len(ids) != 1 { t.Fatal("Expected 1 policy") } bs, err := manager.Store.GetPolicy(ctx, txn, ids[0]) exp := []byte("package a\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 := manager.Store.Read(ctx, txn, storage.Path{}) expData := util.MustUnmarshalJSON([]byte(`{"a": {"baz": "bux", "foo": ["hello", "world"]}, "system": {"bundles": {"test-bundle": {"etag": "foo", "manifest": {"revision": "delta", "roots": ["a"]}}}}}`)) 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) } } func TestPluginStart(t *testing.T) { ctx := context.Background() manager := getTestManager() bundles := map[string]*Source{} plugin := New(&Config{Bundles: bundles}, manager) err := plugin.Start(ctx) if err != nil { t.Fatal("unexpected error:", err) } } func TestStop(t *testing.T) { var longPollTimeout int64 = 3 done := make(chan struct{}) tsURLBase := "/opa-test/" ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, tsURLBase) { t.Fatalf("Invalid request URL path: %s, expected prefix %s", r.URL.Path, tsURLBase) } close(done) // simulate long operation time.Sleep(time.Duration(longPollTimeout) * time.Second) fmt.Fprintln(w) // Note: this is an invalid bundle and will fail the download })) defer ts.Close() ctx := context.Background() manager := getTestManager() serviceName := "test-svc" err := manager.Reconfigure(&config.Config{ Services: []byte(fmt.Sprintf("{\"%s\":{ \"url\": \"%s\"}}", serviceName, ts.URL+tsURLBase)), }) if err != nil { t.Fatalf("Error configuring plugin manager: %s", err) } triggerPolling := plugins.TriggerPeriodic baseConf := download.Config{Polling: download.PollingConfig{LongPollingTimeoutSeconds: &longPollTimeout}, Trigger: &triggerPolling} plugin := Plugin{ manager: manager, status: map[string]*Status{}, etags: map[string]string{}, downloaders: map[string]Loader{}, } bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName} callback := func(ctx context.Context, u download.Update) { plugin.oneShot(ctx, bundleName, u) } plugin.downloaders[bundleName] = download.New(baseConf, plugin.manager.Client(serviceName), bundleName).WithCallback(callback) err = plugin.Start(ctx) if err != nil { t.Fatal("unexpected error:", err) } // Give time for a long poll request to be initiated <-done plugin.Stop(ctx) if plugin.status[bundleName].Code != errCode { t.Fatalf("expected error code %v but got %v", errCode, plugin.status[bundleName].Code) } if !strings.Contains(plugin.status[bundleName].Message, "context canceled") { t.Fatalf("unexpected error message %v", plugin.status[bundleName].Message) } } func TestPluginOneShotBundlePersistence(t *testing.T) { ctx := context.Background() manager := getTestManager() dir, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("unexpected error %v", err) } defer os.RemoveAll(dir) bundleName := "test-bundle" bundleSource := Source{ Persist: true, } bundles := map[string]*Source{} bundles[bundleName] = &bundleSource plugin := New(&Config{Bundles: bundles}, manager) plugin.status[bundleName] = &Status{Name: bundleName, Metrics: metrics.New()} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) plugin.bundlePersistPath = filepath.Join(dir, ".opa") ensurePluginState(t, plugin, plugins.StateNotReady) // simulate a bundle download error with no bundle on disk plugin.oneShot(ctx, bundleName, download.Update{Error: fmt.Errorf("unknown error")}) if plugin.status[bundleName].Message == "" { t.Fatal("expected error but got none") } ensurePluginState(t, plugin, plugins.StateNotReady) // download a bundle and persist to disk. Then verify the bundle persisted to disk module := "package foo\n\ncorge=1" b := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux"}, Data: util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}}`)).(map[string]interface{}), Modules: []bundle.ModuleFile{ { URL: "/foo/bar.rego", Path: "/foo/bar.rego", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, Etag: "foo", } b.Manifest.Init() var buf bytes.Buffer if err := bundle.NewWriter(&buf).UseModulePath(true).Write(b); err != nil { t.Fatal("unexpected error:", err) } plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b, Metrics: metrics.New(), Raw: &buf}) ensurePluginState(t, plugin, plugins.StateOK) result, err := loadBundleFromDisk(plugin.bundlePersistPath, bundleName, nil) if err != nil { t.Fatal("unexpected error:", err) } if !result.Equal(b) { t.Fatal("expected the downloaded bundle to be equal to the one loaded from disk") } // simulate a bundle download error and verify that the bundle on disk is activated plugin.oneShot(ctx, bundleName, download.Update{Error: fmt.Errorf("unknown error")}) ensurePluginState(t, plugin, plugins.StateOK) txn := storage.NewTransactionOrDie(ctx, manager.Store) defer manager.Store.Abort(ctx, txn) ids, err := manager.Store.ListPolicies(ctx, txn) if err != nil { t.Fatal(err) } else if len(ids) != 1 { t.Fatal("Expected 1 policy") } bs, err := manager.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 := manager.Store.Read(ctx, txn, storage.Path{}) expData := util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}, "system": {"bundles": {"test-bundle": {"etag": "foo", "manifest": {"revision": "quickbrownfaux", "roots": [""]}}}}}`)) 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) } } func TestPluginOneShotSignedBundlePersistence(t *testing.T) { ctx := context.Background() manager := getTestManager() dir, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("unexpected error %v", err) } defer os.RemoveAll(dir) bundleName := "test-bundle" vc := bundle.NewVerificationConfig(map[string]*bundle.KeyConfig{"foo": {Key: "secret", Algorithm: "HS256"}}, "foo", "", nil) bundleSource := Source{ Persist: true, Signing: vc, } bundles := map[string]*Source{} bundles[bundleName] = &bundleSource plugin := New(&Config{Bundles: bundles}, manager) plugin.status[bundleName] = &Status{Name: bundleName, Metrics: metrics.New()} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) plugin.bundlePersistPath = filepath.Join(dir, ".opa") ensurePluginState(t, plugin, plugins.StateNotReady) // simulate a bundle download error with no bundle on disk plugin.oneShot(ctx, bundleName, download.Update{Error: fmt.Errorf("unknown error")}) if plugin.status[bundleName].Message == "" { t.Fatal("expected error but got none") } ensurePluginState(t, plugin, plugins.StateNotReady) // download a signed bundle and persist to disk. Then verify the bundle persisted to disk signedTokenHS256 := `eyJhbGciOiJIUzI1NiJ9.eyJmaWxlcyI6W3sibmFtZSI6Ii5tYW5pZmVzdCIsImhhc2giOiI1MDdhMmMzOGExNDQxZGI1OGQyY2I4Nzk4MmM0MmFhOTFhNDM0MmVmNDIyYTZiNTQyZWRkZWJlZWY2ZjA0MTJmIiwiYWxnb3JpdGhtIjoiU0hBLTI1NiJ9LHsibmFtZSI6ImV4YW1wbGUxL2RhdGEuanNvbiIsImhhc2giOiI3YTM4YmY4MWYzODNmNjk0MzNhZDZlOTAwZDM1YjNlMjM4NTU5M2Y3NmE3YjdhYjVkNDM1NWI4YmE0MWVlMjRiIiwiYWxnb3JpdGhtIjoiU0hBLTI1NiJ9LHsibmFtZSI6ImV4YW1wbGUyL2RhdGEuanNvbiIsImhhc2giOiI5ZTRmMTg5YmY0MDc5ZDFiNmViNjQ0Njg3OTg2NmNkNWYzOWMyNjg4MGQ0ZmI1MThmNGUwMWNkMWJiZmU1MTNlIiwiYWxnb3JpdGhtIjoiU0hBLTI1NiJ9XX0.jCLRMyys5u8S2sTS2pWWY82IAeKDpLh3S641_BskCtY` files := [][2]string{ {"/.manifest", `{"revision": "quickbrownfaux"}`}, {"/.signatures.json", fmt.Sprintf(`{"signatures": ["%v"]}`, signedTokenHS256)}, {"/example1/data.json", `{"foo": "bar"}`}, {"/example2/data.json", `{"x": true}`}, } buf := archive.MustWriteTarGz(files) var dup bytes.Buffer tee := io.TeeReader(buf, &dup) reader := bundle.NewReader(tee).WithBundleVerificationConfig(vc).WithBundleEtag("foo") b, err := reader.Read() if err != nil { t.Fatal("unexpected error:", err) } plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b, Metrics: metrics.New(), Raw: &dup}) ensurePluginState(t, plugin, plugins.StateOK) // load signed bundle from disk result, err := loadBundleFromDisk(plugin.bundlePersistPath, bundleName, bundles[bundleName]) if err != nil { t.Fatal("unexpected error:", err) } if !result.Equal(b) { t.Fatal("expected the downloaded bundle to be equal to the one loaded from disk") } // simulate a bundle download error and verify that the bundle on disk is activated plugin.oneShot(ctx, bundleName, download.Update{Error: fmt.Errorf("unknown error")}) ensurePluginState(t, plugin, plugins.StateOK) txn := storage.NewTransactionOrDie(ctx, manager.Store) defer manager.Store.Abort(ctx, txn) ids, err := manager.Store.ListPolicies(ctx, txn) if err != nil { t.Fatal(err) } else if len(ids) != 0 { t.Fatal("Expected no policy") } data, err := manager.Store.Read(ctx, txn, storage.Path{}) if err != nil { t.Fatal(err) } expData := util.MustUnmarshalJSON([]byte(`{"example1": {"foo": "bar"}, "example2": {"x": true}, "system": {"bundles": {"test-bundle": {"etag": "foo", "manifest": {"revision": "quickbrownfaux", "roots": [""]}}}}}`)) if !reflect.DeepEqual(data, expData) { t.Fatalf("Bad data content. Exp:\n%v\n\nGot:\n\n%v", expData, data) } } func TestLoadAndActivateBundlesFromDisk(t *testing.T) { ctx := context.Background() manager := getTestManager() dir, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("unexpected error %v", err) } defer os.RemoveAll(dir) bundleName := "test-bundle" bundleSource := Source{ Persist: true, } bundleNameOther := "test-bundle-other" bundleSourceOther := Source{} bundles := map[string]*Source{} bundles[bundleName] = &bundleSource bundles[bundleNameOther] = &bundleSourceOther plugin := New(&Config{Bundles: bundles}, manager) plugin.bundlePersistPath = filepath.Join(dir, ".opa") plugin.loadAndActivateBundlesFromDisk(ctx) // persist a bundle to disk and then load it module := "package foo\n\ncorge=1" b := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux"}, Data: util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}}`)).(map[string]interface{}), Modules: []bundle.ModuleFile{ { URL: "/foo/bar.rego", Path: "/foo/bar.rego", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() var buf bytes.Buffer if err := bundle.NewWriter(&buf).UseModulePath(true).Write(b); err != nil { t.Fatal("unexpected error:", err) } err = plugin.saveBundleToDisk(bundleName, &buf) if err != nil { t.Fatalf("unexpected error %v", err) } plugin.loadAndActivateBundlesFromDisk(ctx) txn := storage.NewTransactionOrDie(ctx, manager.Store) defer manager.Store.Abort(ctx, txn) ids, err := manager.Store.ListPolicies(ctx, txn) if err != nil { t.Fatal(err) } else if len(ids) != 1 { t.Fatal("Expected 1 policy") } bs, err := manager.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 := manager.Store.Read(ctx, txn, storage.Path{}) expData := util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}, "system": {"bundles": {"test-bundle": {"etag": "", "manifest": {"revision": "quickbrownfaux", "roots": [""]}}}}}`)) 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) } } func TestLoadAndActivateDepBundlesFromDisk(t *testing.T) { ctx := context.Background() manager := getTestManager() dir, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("unexpected error %v", err) } defer os.RemoveAll(dir) bundleName := "test-bundle-main" bundleSource := Source{ Persist: true, } bundleNameOther := "test-bundle-lib" bundleSourceOther := Source{ Persist: true, } bundles := map[string]*Source{} bundles[bundleName] = &bundleSource bundles[bundleNameOther] = &bundleSourceOther plugin := New(&Config{Bundles: bundles}, manager) plugin.bundlePersistPath = filepath.Join(dir, ".opa") module1 := ` package bar import data.foo default allow = false allow { foo.is_one(1) }` module2 := ` package foo is_one(x) { x == 1 }` b1 := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfauxbar", Roots: &[]string{"bar"}}, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ { URL: "/bar/policy.rego", Path: "/bar/policy.rego", Parsed: ast.MustParseModule(module1), Raw: []byte(module1), }, }, } b1.Manifest.Init() b2 := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfauxfoo", Roots: &[]string{"foo"}}, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ { URL: "/foo/policy.rego", Path: "/foo/policy.rego", Parsed: ast.MustParseModule(module2), Raw: []byte(module2), }, }, } b2.Manifest.Init() var buf1 bytes.Buffer if err := bundle.NewWriter(&buf1).UseModulePath(true).Write(b1); err != nil { t.Fatal("unexpected error:", err) } err = plugin.saveBundleToDisk(bundleName, &buf1) if err != nil { t.Fatalf("unexpected error %v", err) } var buf2 bytes.Buffer if err := bundle.NewWriter(&buf2).UseModulePath(true).Write(b2); err != nil { t.Fatal("unexpected error:", err) } err = plugin.saveBundleToDisk(bundleNameOther, &buf2) if err != nil { t.Fatalf("unexpected error %v", err) } plugin.loadAndActivateBundlesFromDisk(ctx) txn := storage.NewTransactionOrDie(ctx, manager.Store) defer manager.Store.Abort(ctx, txn) ids, err := manager.Store.ListPolicies(ctx, txn) if err != nil { t.Fatal(err) } else if len(ids) != 2 { t.Fatal("Expected 2 policies") } } func TestLoadAndActivateDepBundlesFromDiskMaxAttempts(t *testing.T) { ctx := context.Background() manager := getTestManager() dir, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("unexpected error %v", err) } defer os.RemoveAll(dir) bundleName := "test-bundle-main" bundleSource := Source{ Persist: true, } bundles := map[string]*Source{} bundles[bundleName] = &bundleSource plugin := New(&Config{Bundles: bundles}, manager) plugin.bundlePersistPath = filepath.Join(dir, ".opa") module := ` package bar import data.foo default allow = false allow { foo.is_one(1) }` b := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux", Roots: &[]string{"bar"}}, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ { URL: "/bar/policy.rego", Path: "/bar/policy.rego", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() var buf bytes.Buffer if err := bundle.NewWriter(&buf).UseModulePath(true).Write(b); err != nil { t.Fatal("unexpected error:", err) } err = plugin.saveBundleToDisk(bundleName, &buf) if err != nil { t.Fatalf("unexpected error %v", err) } plugin.loadAndActivateBundlesFromDisk(ctx) txn := storage.NewTransactionOrDie(ctx, manager.Store) defer manager.Store.Abort(ctx, txn) ids, err := manager.Store.ListPolicies(ctx, txn) if err != nil { t.Fatal(err) } else if len(ids) != 0 { t.Fatal("Expected 0 policies") } } func TestPluginOneShotCompileError(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := New(&Config{}, manager) bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) ensurePluginState(t, plugin, plugins.StateNotReady) raw1 := "package foo\n\np[x] { x = 1 }" b1 := &bundle.Bundle{ Data: map[string]interface{}{"a": "b"}, Modules: []bundle.ModuleFile{ { Path: "/example.rego", Raw: []byte(raw1), Parsed: ast.MustParseModule(raw1), }, }, } b1.Manifest.Init() plugin.oneShot(ctx, bundleName, download.Update{Bundle: b1, Metrics: metrics.New()}) ensurePluginState(t, plugin, plugins.StateOK) b2 := &bundle.Bundle{ Data: map[string]interface{}{"a": "b"}, Modules: []bundle.ModuleFile{ { Path: "/example2.rego", Parsed: ast.MustParseModule("package foo\n\np[x]"), }, }, } b2.Manifest.Init() plugin.oneShot(ctx, bundleName, download.Update{Bundle: b2}) ensurePluginState(t, plugin, plugins.StateOK) txn := storage.NewTransactionOrDie(ctx, manager.Store) _, err := manager.Store.GetPolicy(ctx, txn, filepath.Join(bundleName, "/example.rego")) if err != nil { t.Fatalf("Unexpected error: %v", err) } data, err := manager.Store.Read(ctx, txn, storage.Path{"a"}) if err != nil || !reflect.DeepEqual("b", data) { t.Fatalf("Expected data to be intact but got: %v, err: %v", data, err) } manager.Store.Abort(ctx, txn) b3 := &bundle.Bundle{ Data: map[string]interface{}{"foo": map[string]interface{}{"p": "a"}}, Modules: []bundle.ModuleFile{ { Path: "/example3.rego", Parsed: ast.MustParseModule("package foo\np=1"), }, }, } b3.Manifest.Init() plugin.oneShot(ctx, bundleName, download.Update{Bundle: b3}) ensurePluginState(t, plugin, plugins.StateOK) txn = storage.NewTransactionOrDie(ctx, manager.Store) _, err = manager.Store.GetPolicy(ctx, txn, filepath.Join(bundleName, "/example.rego")) if err != nil { t.Fatalf("Unexpected error: %v", err) } data, err = manager.Store.Read(ctx, txn, storage.Path{"a"}) if err != nil || !reflect.DeepEqual("b", data) { t.Fatalf("Expected data to be intact but got: %v, err: %v", data, err) } } func TestPluginOneShotHTTPError(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := New(&Config{}, manager) bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) ch := make(chan Status) listenerName := "test" plugin.Register(listenerName, func(status Status) { ch <- status }) go plugin.oneShot(ctx, bundleName, download.Update{Error: download.HTTPError{StatusCode: 403}}) s := <-ch if s.HTTPCode != "403" { t.Fatal("expected http_code to be 403 instead of ", s.HTTPCode) } module := "package foo\n\ncorge=1" b := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux"}, Data: util.MustUnmarshalJSON([]byte(`{"foo": {"bar": 1, "baz": "qux"}}`)).(map[string]interface{}), Modules: []bundle.ModuleFile{ { Path: "/foo/bar", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() go plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b}) s = <-ch if s.HTTPCode != "" { t.Fatal("expected http_code to be empty instead of ", s.HTTPCode) } } func TestPluginOneShotActivationRemovesOld(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := New(&Config{}, manager) bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) ensurePluginState(t, plugin, plugins.StateNotReady) module1 := `package example p = 1` b1 := bundle.Bundle{ Data: map[string]interface{}{ "foo": "bar", }, Modules: []bundle.ModuleFile{ { Path: "/example.rego", Raw: []byte(module1), Parsed: ast.MustParseModule(module1), }, }, } b1.Manifest.Init() plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b1}) ensurePluginState(t, plugin, plugins.StateOK) 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), }, }, } b2.Manifest.Init() plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b2}) ensurePluginState(t, plugin, plugins.StateOK) err := storage.Txn(ctx, manager.Store, storage.TransactionParams{}, func(txn storage.Transaction) error { ids, err := manager.Store.ListPolicies(ctx, txn) if err != nil { return err } else if !reflect.DeepEqual([]string{filepath.Join(bundleName, "/example2.rego")}, ids) { return fmt.Errorf("expected updated policy ids") } data, err := manager.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 TestPluginOneShotActivationConflictingRoots(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := New(&Config{}, manager) ensurePluginState(t, plugin, plugins.StateNotReady) bundleNames := []string{"test-bundle1", "test-bundle2", "test-bundle3"} for _, name := range bundleNames { plugin.status[name] = &Status{Name: name} plugin.downloaders[name] = download.New(download.Config{}, plugin.manager.Client(""), name) } // Start with non-conflicting updates plugin.oneShot(ctx, bundleNames[0], download.Update{Bundle: &bundle.Bundle{ Manifest: bundle.Manifest{ Roots: &[]string{"a/b"}, }, }}) ensurePluginState(t, plugin, plugins.StateNotReady) plugin.oneShot(ctx, bundleNames[1], download.Update{Bundle: &bundle.Bundle{ Manifest: bundle.Manifest{ Roots: &[]string{"a/c"}, }, }}) ensurePluginState(t, plugin, plugins.StateNotReady) // ensure that both bundles are *not* in error status ensureBundleOverlapStatus(t, plugin, bundleNames, []bool{false, false, false}) // Add a third bundle that conflicts with one plugin.oneShot(ctx, bundleNames[2], download.Update{Bundle: &bundle.Bundle{ Manifest: bundle.Manifest{ Roots: &[]string{"a/b/aa"}, }, }}) ensurePluginState(t, plugin, plugins.StateNotReady) // ensure that both in the conflict go into error state ensureBundleOverlapStatus(t, plugin, bundleNames, []bool{false, false, true}) // Update to fix conflict plugin.oneShot(ctx, bundleNames[2], download.Update{Bundle: &bundle.Bundle{ Manifest: bundle.Manifest{ Roots: &[]string{"b"}, }, }}) ensurePluginState(t, plugin, plugins.StateOK) ensureBundleOverlapStatus(t, plugin, bundleNames, []bool{false, false, false}) // Ensure empty roots conflict with all roots plugin.oneShot(ctx, bundleNames[2], download.Update{Bundle: &bundle.Bundle{ Manifest: bundle.Manifest{ Roots: &[]string{""}, }, }}) ensurePluginState(t, plugin, plugins.StateOK) ensureBundleOverlapStatus(t, plugin, bundleNames, []bool{false, false, true}) } func TestPluginOneShotActivationPrefixMatchingRoots(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := Plugin{ manager: manager, status: map[string]*Status{}, etags: map[string]string{}, downloaders: map[string]Loader{}, } bundleNames := []string{"test-bundle1", "test-bundle2"} for _, name := range bundleNames { plugin.status[name] = &Status{Name: name} plugin.downloaders[name] = download.New(download.Config{}, plugin.manager.Client(""), name) } plugin.oneShot(ctx, bundleNames[0], download.Update{Bundle: &bundle.Bundle{ Manifest: bundle.Manifest{ Roots: &[]string{"a/b/c"}, }, }}) plugin.oneShot(ctx, bundleNames[1], download.Update{Bundle: &bundle.Bundle{ Manifest: bundle.Manifest{ Roots: &[]string{"a/b/cat"}, }, }}) ensureBundleOverlapStatus(t, &plugin, bundleNames, []bool{false, false}) // Ensure that empty roots conflict plugin.oneShot(ctx, bundleNames[1], download.Update{Bundle: &bundle.Bundle{ Manifest: bundle.Manifest{ Roots: &[]string{""}, }, }}) ensureBundleOverlapStatus(t, &plugin, bundleNames, []bool{false, true}) } func ensureBundleOverlapStatus(t *testing.T, p *Plugin, bundleNames []string, expectedErrs []bool) { t.Helper() for i, name := range bundleNames { hasErr := p.status[name].Message != "" if expectedErrs[i] && !hasErr { t.Fatalf("expected bundle %s to be in an error state", name) } else if !expectedErrs[i] && hasErr { t.Fatalf("unexpected error state for bundle %s", name) } else if hasErr && expectedErrs[i] && !strings.Contains(p.status[name].Message, "detected overlapping roots") { t.Fatalf("expected bundle overlap error for bundle %s, got: %s", name, p.status[name].Message) } } } func TestPluginListener(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := New(&Config{}, manager) bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) ch := make(chan Status) listenerName := "test" plugin.Register(listenerName, func(status Status) { ch <- status }) if len(plugin.listeners) != 1 || plugin.listeners[listenerName] == nil { t.Fatal("Listener not properly registered") } module := "package gork\np[x] { x = 1 }" b := bundle.Bundle{ Manifest: bundle.Manifest{ Revision: "quickbrownfaux", }, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ { Path: "/foo.rego", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() // Test that initial bundle is ok. Defer to separate goroutine so we can // check result with channel. go plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b}) s1 := <-ch validateStatus(t, s1, "quickbrownfaux", false) module = "package gork\np[x]" b.Manifest.Revision = "slowgreenburd" b.Modules[0] = bundle.ModuleFile{ Path: "/foo.rego", Raw: []byte(module), Parsed: ast.MustParseModule(module), } // Test that next update is failed. go plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b}) s2 := <-ch validateStatus(t, s2, "quickbrownfaux", true) module = "package gork\np[1]" b.Manifest.Revision = "fancybluederg" b.Modules[0] = bundle.ModuleFile{ Path: "/foo.rego", Raw: []byte(module), Parsed: ast.MustParseModule(module), } // Test that the new update is successful. go plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b}) s3 := <-ch validateStatus(t, s3, "fancybluederg", false) // Test that empty download update results in status update. go plugin.oneShot(ctx, bundleName, download.Update{}) s4 := <-ch // Nothing should have changed in the update validateStatus(t, s4, s3.ActiveRevision, false) plugin.Unregister(listenerName) if len(plugin.listeners) != 0 { t.Fatal("Listener not properly unregistered") } } func isErrStatus(s Status) bool { return s.Code != "" || len(s.Errors) != 0 || s.Message != "" } func validateStatus(t *testing.T, actual Status, expected string, expectStatusErr bool) { t.Helper() if expectStatusErr && !isErrStatus(actual) { t.Errorf("Expected status to be in an error state, but no error has occurred.") } else if !expectStatusErr && isErrStatus(actual) { t.Errorf("Unexpected error status %s", actual) } if actual.ActiveRevision != expected { t.Errorf("Expected status revision %s, got %s", expected, actual.ActiveRevision) } } func TestPluginListenerErrorClearedOn304(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := Plugin{ manager: manager, status: map[string]*Status{}, etags: map[string]string{}, downloaders: map[string]Loader{}, } bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) ch := make(chan Status) plugin.Register("test", func(status Status) { ch <- status }) b := bundle.Bundle{ Manifest: bundle.Manifest{ Revision: "quickbrownfaux", }, Data: map[string]interface{}{"foo": "bar"}, } b.Manifest.Init() // Test that initial bundle is ok. go plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b}) s1 := <-ch if s1.ActiveRevision != "quickbrownfaux" || s1.Code != "" { t.Fatal("Unexpected status update, got:", s1) } // Test that service error triggers failure notification. go plugin.oneShot(ctx, bundleName, download.Update{Error: fmt.Errorf("some error")}) s2 := <-ch if s2.ActiveRevision != "quickbrownfaux" || s2.Code == "" { t.Fatal("Unexpected status update, got:", s2) } // Test that service recovery triggers healthy notification. go plugin.oneShot(ctx, bundleName, download.Update{}) s3 := <-ch if s3.ActiveRevision != "quickbrownfaux" || s3.Code != "" { t.Fatal("Unexpected status update, got:", s3) } } func TestPluginBulkListener(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := Plugin{ manager: manager, status: map[string]*Status{}, etags: map[string]string{}, downloaders: map[string]Loader{}, } bundleNames := []string{ "b1", "b2", "b3", } for _, name := range bundleNames { plugin.status[name] = &Status{Name: name} plugin.downloaders[name] = download.New(download.Config{}, plugin.manager.Client(""), name) } bulkChan := make(chan map[string]*Status) listenerName := "bulk test" plugin.RegisterBulkListener(listenerName, func(status map[string]*Status) { bulkChan <- status }) if len(plugin.bulkListeners) != 1 || plugin.bulkListeners[listenerName] == nil { t.Fatal("Bulk listener not properly registered") } module := "package gork\np[x] { x = 1 }" b := bundle.Bundle{ Manifest: bundle.Manifest{ Revision: "quickbrownfaux", Roots: &[]string{"gork"}, }, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ { Path: "/foo.rego", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() // Test that initial bundle is ok. Defer to separate goroutine so we can // check result with channel. go plugin.oneShot(ctx, bundleNames[0], download.Update{Bundle: &b}) s1 := <-bulkChan s := s1[bundleNames[0]] if s.ActiveRevision != "quickbrownfaux" || s.Code != "" { t.Fatal("Unexpected status update, got:", s1) } for i := 1; i < len(bundleNames); i++ { name := bundleNames[i] s, ok := s1[name] if !ok { t.Errorf("Expected to have bundle status for %q included in update, got: %+v", name, s1) } // they should be defaults at this point if !reflect.DeepEqual(s, &Status{Name: name}) { t.Errorf("Expected bundle %q to have an empty status, got: %+v", name, s1) } } module = "package gork\np[x]" b.Manifest.Revision = "slowgreenburd" b.Modules[0] = bundle.ModuleFile{ Path: "/foo.rego", Raw: []byte(module), Parsed: ast.MustParseModule(module), } // Test that next update is failed. go plugin.oneShot(ctx, bundleNames[0], download.Update{Bundle: &b}) s2 := <-bulkChan s = s2[bundleNames[0]] if s.ActiveRevision != "quickbrownfaux" || s.Code == "" || s.Message == "" || len(s.Errors) == 0 { t.Fatal("Unexpected status update, got:", s2) } for i := 1; i < len(bundleNames); i++ { name := bundleNames[i] s, ok := s2[name] if !ok { t.Errorf("Expected to have bundle status for %q included in update, got: %+v", name, s2) } // they should be still defaults if !reflect.DeepEqual(s, &Status{Name: name}) { t.Errorf("Expected bundle %q to have an empty status, got: %+v", name, s2) } } module = "package gork\np[1]" b.Manifest.Revision = "fancybluederg" b.Modules[0] = bundle.ModuleFile{ Path: "/foo.rego", Raw: []byte(module), Parsed: ast.MustParseModule(module), } // Test that new update is successful. go plugin.oneShot(ctx, bundleNames[0], download.Update{Bundle: &b}) s3 := <-bulkChan s = s3[bundleNames[0]] if s.ActiveRevision != "fancybluederg" || s.Code != "" || s.Message != "" || len(s.Errors) != 0 { t.Fatal("Unexpected status update, got:", s3) } for i := 1; i < len(bundleNames); i++ { name := bundleNames[i] s, ok := s3[name] if !ok { t.Errorf("Expected to have bundle status for %q included in update, got: %+v", name, s3) } // they should still be defaults if !reflect.DeepEqual(s, &Status{Name: name}) { t.Errorf("Expected bundle %q to have an empty status, got: %+v", name, s3) } } // Test that empty download update results in status update. go plugin.oneShot(ctx, bundleNames[0], download.Update{}) s4 := <-bulkChan s = s4[bundleNames[0]] if s.ActiveRevision != "fancybluederg" || s.Code != "" || s.Message != "" || len(s.Errors) != 0 { t.Errorf("Unexpected same status update for bundle %q, got: %v", bundleNames[0], s) } // Test updates the other bundles module = "package p1\np[x] { x = 1 }" b1 := bundle.Bundle{ Manifest: bundle.Manifest{ Revision: "123", Roots: &[]string{"p1"}, }, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ { Path: "/foo1.rego", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b1.Manifest.Init() // Test that new update is successful. go plugin.oneShot(ctx, bundleNames[1], download.Update{Bundle: &b1}) s5 := <-bulkChan s = s5[bundleNames[1]] if s.ActiveRevision != "123" || s.Code != "" || s.Message != "" || len(s.Errors) != 0 { t.Fatal("Unexpected status update, got:", s5) } if !reflect.DeepEqual(s5[bundleNames[0]], s4[bundleNames[0]]) { t.Fatalf("Expected bundle %q to have the same status as before updating bundle %q, got: %+v", bundleNames[0], bundleNames[1], s5) } for i := 2; i < len(bundleNames); i++ { name := bundleNames[i] s, ok := s5[name] if !ok { t.Errorf("Expected to have bundle status for %q included in update, got: %+v", name, s5) } // they should still be defaults if !reflect.DeepEqual(s, &Status{Name: name}) { t.Errorf("Expected bundle %q to have an empty status, got: %+v", name, s5) } } plugin.UnregisterBulkListener(listenerName) if len(plugin.bulkListeners) != 0 { t.Fatal("Bulk listener not properly unregistered") } } func TestPluginBulkListenerStatusCopyOnly(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := Plugin{ manager: manager, status: map[string]*Status{}, etags: map[string]string{}, downloaders: map[string]Loader{}, } bundleNames := []string{ "b1", "b2", "b3", } for _, name := range bundleNames { plugin.status[name] = &Status{Name: name} plugin.downloaders[name] = download.New(download.Config{}, plugin.manager.Client(""), name) } bulkChan := make(chan map[string]*Status) plugin.RegisterBulkListener("bulk test", func(status map[string]*Status) { bulkChan <- status }) module := "package gork\np[x] { x = 1 }" b := bundle.Bundle{ Manifest: bundle.Manifest{ Revision: "quickbrownfaux", Roots: &[]string{"gork"}, }, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ { Path: "/foo.rego", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() // Test that initial bundle is ok. Defer to separate goroutine so we can // check result with channel. go plugin.oneShot(ctx, bundleNames[0], download.Update{Bundle: &b}) s1 := <-bulkChan // Modify the status map received and ensure it doesn't affect the one on the plugin delete(s1, "b1") if _, ok := plugin.status["b1"]; !ok { t.Fatalf("Expected status for 'b1' to still be in 'plugin.status'") } } func TestPluginActivateScopedBundle(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := Plugin{ manager: manager, status: map[string]*Status{}, etags: map[string]string{}, downloaders: map[string]Loader{}, } bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) // Transact test data and policies that represent data coming from // _outside_ the bundle. The test will verify that data _outside_ // the bundle is both not erased and is overwritten appropriately. // // The test data claims a/{a1-6} where even paths are policy and // odd paths are raw JSON. if err := storage.Txn(ctx, manager.Store, storage.WriteParams, func(txn storage.Transaction) error { externalData := map[string]interface{}{"a": map[string]interface{}{"a1": "x1", "a3": "x2", "a5": "x3"}} if err := manager.Store.Write(ctx, txn, storage.AddOp, storage.Path{}, externalData); err != nil { return err } if err := manager.Store.UpsertPolicy(ctx, txn, "some/id1", []byte(`package a.a2`)); err != nil { return err } if err := manager.Store.UpsertPolicy(ctx, txn, "some/id2", []byte(`package a.a4`)); err != nil { return err } return manager.Store.UpsertPolicy(ctx, txn, "some/id3", []byte(`package a.a6`)) }); err != nil { t.Fatal(err) } // Activate a bundle that is scoped to a/a1 and a/a2. This will // erase and overwrite the external data at these paths but leave // a3-6 untouched. module := "package a.a2\n\nbar=1" b := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux", Roots: &[]string{"a/a1", "a/a2"}}, Data: map[string]interface{}{ "a": map[string]interface{}{ "a1": "foo", }, }, Modules: []bundle.ModuleFile{ { Path: "bundle/id1", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b}) // Ensure a/a3-6 are intact. a1-2 are overwritten by bundle, and // that the manifest has been written to storage. expData := util.MustUnmarshalJSON([]byte(`{"a1": "foo", "a3": "x2", "a5": "x3"}`)) expIds := []string{filepath.Join(bundleName, "bundle/id1"), "some/id2", "some/id3"} validateStoreState(ctx, t, manager.Store, "/a", expData, expIds, bundleName, "quickbrownfaux", nil) // Activate a bundle that is scoped to a/a3 ad a/a6. Include a function // inside package a.a4 that we can depend on outside of the bundle scope to // exercise the compile check with remaining modules. module = "package a.a4\n\nbar=1\n\nfunc(x) = x" b = bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux-2", Roots: &[]string{"a/a3", "a/a4"}, Metadata: map[string]interface{}{ "a": map[string]interface{}{ "a1": "deadbeef", }, }, }, Data: map[string]interface{}{ "a": map[string]interface{}{ "a3": "foo", }, }, Modules: []bundle.ModuleFile{ { Path: "bundle/id2", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b}) // Ensure a/a5-a6 are intact. a3 and a4 are overwritten by bundle. expData = util.MustUnmarshalJSON([]byte(`{"a3": "foo", "a5": "x3"}`)) expIds = []string{filepath.Join(bundleName, "bundle/id2"), "some/id3"} validateStoreState(ctx, t, manager.Store, "/a", expData, expIds, bundleName, "quickbrownfaux-2", map[string]interface{}{ "a": map[string]interface{}{"a1": "deadbeef"}, }) // Upsert policy outside of bundle scope that depends on bundle. if err := storage.Txn(ctx, manager.Store, storage.WriteParams, func(txn storage.Transaction) error { return manager.Store.UpsertPolicy(ctx, txn, "not_scoped", []byte("package not_scoped\np { data.a.a4.func(1) = 1 }")) }); err != nil { t.Fatal(err) } b = bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux-3", Roots: &[]string{"a/a3", "a/a4"}}, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{}, } b.Manifest.Init() plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b}) // Ensure bundle activation failed by checking that previous revision is // still active. expIds = []string{filepath.Join(bundleName, "bundle/id2"), "not_scoped", "some/id3"} validateStoreState(ctx, t, manager.Store, "/a", expData, expIds, bundleName, "quickbrownfaux-2", map[string]interface{}{ "a": map[string]interface{}{"a1": "deadbeef"}, }) } func TestPluginSetCompilerOnContext(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := Plugin{ manager: manager, status: map[string]*Status{}, etags: map[string]string{}, downloaders: map[string]Loader{}, } bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) module := ` package test p = 1 ` b := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux"}, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ { Path: "/test.rego", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() events := []storage.TriggerEvent{} if err := storage.Txn(ctx, manager.Store, storage.WriteParams, func(txn storage.Transaction) error { _, err := manager.Store.Register(ctx, txn, storage.TriggerConfig{ OnCommit: func(_ context.Context, _ storage.Transaction, event storage.TriggerEvent) { events = append(events, event) }, }) return err }); err != nil { t.Fatal(err) } plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b}) exp := ast.MustParseModule(module) // Expect two events. One for trigger registration, one for policy update. if len(events) != 2 { t.Fatalf("Expected 2 events but got: %+v", events) } else if compiler := plugins.GetCompilerOnContext(events[1].Context); compiler == nil { t.Fatalf("Expected compiler on 2nd event but got: %+v", events) } else if !compiler.Modules[filepath.Join(bundleName, "/test.rego")].Equal(exp) { t.Fatalf("Expected module on compiler but got: %v", compiler.Modules) } } func getTestManager() *plugins.Manager { return getTestManagerWithOpts(nil) } func getTestManagerWithOpts(config []byte, stores ...storage.Store) *plugins.Manager { store := inmem.New() if len(stores) == 1 { store = stores[0] } manager, err := plugins.New(config, "test-instance-id", store) if err != nil { panic(err) } return manager } func TestPluginReconfigure(t *testing.T) { tsURLBase := "/opa-test/" ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, tsURLBase) { t.Fatalf("Invalid request URL path: %s, expected prefix %s", r.URL.Path, tsURLBase) } fmt.Fprintln(w, "") // Note: this is an invalid bundle and will fail the download })) defer ts.Close() ctx := context.Background() manager := getTestManager() serviceName := "test-svc" err := manager.Reconfigure(&config.Config{ Services: []byte(fmt.Sprintf("{\"%s\":{ \"url\": \"%s\"}}", serviceName, ts.URL+tsURLBase)), }) if err != nil { t.Fatalf("Error configuring plugin manager: %s", err) } plugin := New(&Config{}, manager) var delay int64 = 10 triggerPolling := plugins.TriggerPeriodic baseConf := download.Config{Polling: download.PollingConfig{MinDelaySeconds: &delay, MaxDelaySeconds: &delay}, Trigger: &triggerPolling} // Expect the plugin to emit a "not ready" status update each time we change the configuration updateCount := 0 manager.RegisterPluginStatusListener(t.Name(), func(status map[string]*plugins.Status) { updateCount++ bStatus, ok := status[Name] if !ok { t.Errorf("Expected to find status for %s in plugin status update, got: %+v", Name, status) } if bStatus.State != plugins.StateNotReady { t.Errorf("Expected plugin status update to have state = %s, got %s", plugins.StateNotReady, bStatus.State) } }) // Note: test stages are accumulating state with reconfigures between them, the order does matter! // Each stage defines the new config, side effects are validated. stages := []struct { name string cfg *Config }{ { name: "start with single legacy bundle", cfg: &Config{ Name: "bundle.tar.gz", Service: serviceName, Config: baseConf, // Note: the config validation and default injection will add an entry // to the Bundles map for the older style configuration. Bundles: map[string]*Source{ "bundle.tar.gz": {Config: baseConf, Service: serviceName, Resource: "/bundles/bundle.tar.gz"}, }, }, }, { name: "switch to multi-bundle", cfg: &Config{ Bundles: map[string]*Source{ "b1": {Config: baseConf, Service: serviceName, Resource: "/bundles/bundle.tar.gz"}, }, }, }, { name: "add second bundle", cfg: &Config{ Bundles: map[string]*Source{ "b1": {Config: baseConf, Service: serviceName, Resource: "/bundles/bundle1.tar.gz"}, "b2": {Config: baseConf, Service: serviceName, Resource: "/bundles/bundle2.tar.gz"}, }, }, }, { name: "remove initial bundle", cfg: &Config{ Bundles: map[string]*Source{ "b2": {Config: baseConf, Service: serviceName, Resource: "/bundles/bundle2.tar.gz"}, }, }, }, { name: "Update single bundle", cfg: &Config{ Bundles: map[string]*Source{ "b2": {Config: baseConf, Service: serviceName, Resource: "/new/path/bundles/bundle2.tar.gz"}, }, }, }, { name: "Add multiple new bundles", cfg: &Config{ Bundles: map[string]*Source{ "b3": {Config: baseConf, Service: serviceName, Resource: "/bundle3.tar.gz"}, "b4": {Config: baseConf, Service: serviceName, Resource: "/bundles/bundle4.tar.gz"}, "b5": {Config: baseConf, Service: serviceName, Resource: "/bundles/bundle5.tar.gz"}, }, }, }, { name: "Remove multiple bundles", cfg: &Config{ Bundles: map[string]*Source{ "b2": {Config: baseConf, Service: serviceName, Resource: "/new/path/bundles/bundle2.tar.gz"}, "b4": {Config: baseConf, Service: serviceName, Resource: "/bundles/bundle4.tar.gz"}, }, }, }, { name: "Update multiple bundles", cfg: &Config{ Bundles: map[string]*Source{ "b2": {Config: baseConf, Service: serviceName, Resource: "/update2/bundle2.tar.gz"}, "b4": {Config: baseConf, Service: serviceName, Resource: "/update2/bundle4.tar.gz"}, }, }, }, { name: "Remove and add bundle", cfg: &Config{ Bundles: map[string]*Source{ "b6": {Config: baseConf, Service: serviceName, Resource: "bundle6.tar.gz"}, }, }, }, { name: "Add and update bundle", cfg: &Config{ Bundles: map[string]*Source{ "b6": {Config: baseConf, Service: serviceName, Resource: "/update3/bundle6.tar.gz"}, "b7": {Config: baseConf, Service: serviceName, Resource: "bundle7.tar.gz"}, "b8": {Config: baseConf, Service: serviceName, Resource: "bundle8.tar.gz"}, }, }, }, { name: "Update and remove", cfg: &Config{ Bundles: map[string]*Source{ "b6": {Config: baseConf, Service: serviceName, Resource: "/update4/bundle6.tar.gz"}, "b8": {Config: baseConf, Service: serviceName, Resource: "bundle8.tar.gz"}, }, }, }, // Add, Update, and Remove { name: "Add update and remove", cfg: &Config{ Bundles: map[string]*Source{ "b8": {Config: baseConf, Service: serviceName, Resource: "/update5/bundle8.tar.gz"}, "b9": {Config: baseConf, Service: serviceName, Resource: "bundle9.tar.gz"}, }, }, }, } for _, stage := range stages { t.Run(stage.name, func(t *testing.T) { plugin.Reconfigure(ctx, stage.cfg) var expectedNumBundles int if stage.cfg.Name != "" { expectedNumBundles = 1 } else { expectedNumBundles = len(stage.cfg.Bundles) } if expectedNumBundles != len(plugin.downloaders) { t.Fatalf("Expected a downloader for each configured bundle, expected %d found %d", expectedNumBundles, len(plugin.downloaders)) } if expectedNumBundles != len(plugin.status) { t.Fatalf("Expected a status entry for each configured bundle, expected %d found %d", expectedNumBundles, len(plugin.status)) } for name := range stage.cfg.Bundles { if _, found := plugin.downloaders[name]; !found { t.Fatalf("bundle %q not found in downloaders map", name) } if _, found := plugin.status[name]; !found { t.Fatalf("bundle %q not found in status map", name) } } }) } if len(stages) != updateCount { t.Fatalf("Expected to have received %d updates, got %d", len(stages), updateCount) } } func TestPluginRequestVsDownloadTimestamp(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := Plugin{ manager: manager, status: map[string]*Status{}, etags: map[string]string{}, downloaders: map[string]Loader{}, } bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) b := &bundle.Bundle{} b.Manifest.Init() // simulate HTTP 200 response from downloader plugin.oneShot(ctx, bundleName, download.Update{Bundle: b}) if plugin.status[bundleName].LastSuccessfulDownload != plugin.status[bundleName].LastSuccessfulRequest || plugin.status[bundleName].LastSuccessfulDownload != plugin.status[bundleName].LastRequest { t.Fatal("expected last successful request to be same as download and request") } // The time resolution is 1ns so sleeping for 1ms should be more than enough. time.Sleep(time.Millisecond) // simulate HTTP 304 response from downloader. plugin.oneShot(ctx, bundleName, download.Update{Bundle: nil}) if plugin.status[bundleName].LastSuccessfulDownload == plugin.status[bundleName].LastSuccessfulRequest || plugin.status[bundleName].LastSuccessfulDownload == plugin.status[bundleName].LastRequest { t.Fatal("expected last successful request to differ from download and request") } // simulate HTTP 200 response from downloader plugin.oneShot(ctx, bundleName, download.Update{Bundle: b}) if plugin.status[bundleName].LastSuccessfulDownload != plugin.status[bundleName].LastSuccessfulRequest || plugin.status[bundleName].LastSuccessfulDownload != plugin.status[bundleName].LastRequest { t.Fatal("expected last successful request to be same as download and request") } // simulate error response from downloader plugin.oneShot(ctx, bundleName, download.Update{Error: errors.New("xxx")}) if plugin.status[bundleName].LastSuccessfulDownload != plugin.status[bundleName].LastSuccessfulRequest || plugin.status[bundleName].LastSuccessfulDownload == plugin.status[bundleName].LastRequest { t.Fatal("expected last successful request to be same as download but different from request") } } func TestUpgradeLegacyBundleToMuiltiBundleSameBundle(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := Plugin{ manager: manager, status: map[string]*Status{}, etags: map[string]string{}, downloaders: map[string]Loader{}, } bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) // Start with a "legacy" style config for a single bundle plugin.config = Config{ Bundles: map[string]*Source{ bundleName: { Service: "s1", }, }, Name: bundleName, Service: "s1", Prefix: nil, } module := "package a.a1\n\nbar=1" b := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux", Roots: &[]string{"a/a1", "a/a2"}}, Data: map[string]interface{}{ "a": map[string]interface{}{ "a2": "foo", }, }, Modules: []bundle.ModuleFile{ { Path: "bundle/id1", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b}) // Ensure it has been activated expData := util.MustUnmarshalJSON([]byte(`{"a2": "foo"}`)) expIds := []string{"bundle/id1"} validateStoreState(ctx, t, manager.Store, "/a", expData, expIds, bundleName, "quickbrownfaux", nil) if plugin.config.IsMultiBundle() { t.Fatalf("Expected plugin to be in non-multi bundle config mode") } // Update to the newer style config with the same bundle multiBundleConf := &Config{ Bundles: map[string]*Source{ bundleName: { Service: "s1", }, }, } plugin.Reconfigure(ctx, multiBundleConf) b.Manifest.Revision = "quickbrownfaux-2" plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b}) // The only thing that should have changed is the store id for the policy expIds = []string{"test-bundle/bundle/id1"} validateStoreState(ctx, t, manager.Store, "/a", expData, expIds, bundleName, "quickbrownfaux-2", nil) // Make sure the legacy path is gone now that we are in multi-bundle mode var actual string err := storage.Txn(ctx, plugin.manager.Store, storage.WriteParams, func(txn storage.Transaction) error { var err error if actual, err = bundle.LegacyReadRevisionFromStore(ctx, plugin.manager.Store, txn); err != nil && !storage.IsNotFound(err) { t.Fatalf("Failed to read manifest revision from store: %s", err) return err } return nil }) if err != nil { t.Fatalf("Unexpected error finishing transaction: %s", err) } if actual != "" { t.Fatalf("Expected to not find manifest revision but got %s", actual) } if !plugin.config.IsMultiBundle() { t.Fatalf("Expected plugin to be in multi bundle config mode") } } func TestUpgradeLegacyBundleToMuiltiBundleNewBundles(t *testing.T) { ctx := context.Background() manager := getTestManager() plugin := Plugin{ manager: manager, status: map[string]*Status{}, etags: map[string]string{}, downloaders: map[string]Loader{}, } bundleName := "test-bundle" plugin.status[bundleName] = &Status{Name: bundleName} plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName) tsURLBase := "/opa-test/" ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, tsURLBase) { t.Fatalf("Invalid request URL path: %s, expected prefix %s", r.URL.Path, tsURLBase) } fmt.Fprintln(w, "") // Note: this is an invalid bundle and will fail the download })) defer ts.Close() serviceName := "test-svc" err := manager.Reconfigure(&config.Config{ Services: []byte(fmt.Sprintf("{\"%s\":{ \"url\": \"%s\"}}", serviceName, ts.URL+tsURLBase)), }) if err != nil { t.Fatalf("Error configuring plugin manager: %s", err) } var delay int64 = 10 triggerPolling := plugins.TriggerPeriodic downloadConf := download.Config{Polling: download.PollingConfig{MinDelaySeconds: &delay, MaxDelaySeconds: &delay}, Trigger: &triggerPolling} // Start with a "legacy" style config for a single bundle plugin.config = Config{ Bundles: map[string]*Source{ bundleName: { Config: downloadConf, Service: serviceName, }, }, Name: bundleName, Service: serviceName, Prefix: nil, } module := "package a.a1\n\nbar=1" b := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux", Roots: &[]string{"a/a1", "a/a2"}}, Data: map[string]interface{}{ "a": map[string]interface{}{ "a2": "foo", }, }, Modules: []bundle.ModuleFile{ { Path: "bundle/id1", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b}) // Ensure it has been activated expData := util.MustUnmarshalJSON([]byte(`{"a2": "foo"}`)) expIds := []string{"bundle/id1"} validateStoreState(ctx, t, manager.Store, "/a", expData, expIds, bundleName, "quickbrownfaux", nil) if plugin.config.IsMultiBundle() { t.Fatalf("Expected plugin to be in non-multi bundle config mode") } // Update to the newer style config with a new bundle multiBundleConf := &Config{ Bundles: map[string]*Source{ "b2": { Config: downloadConf, Service: serviceName, }, }, } delete(plugin.downloaders, bundleName) plugin.downloaders["b2"] = download.New(download.Config{}, plugin.manager.Client(""), "b2") plugin.Reconfigure(ctx, multiBundleConf) module = "package a.c\n\nbar=1" b = bundle.Bundle{ Manifest: bundle.Manifest{Revision: "b2-1", Roots: &[]string{"a/b2", "a/c"}}, Data: map[string]interface{}{ "a": map[string]interface{}{ "b2": "foo", }, }, Modules: []bundle.ModuleFile{ { Path: "id1", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() plugin.oneShot(ctx, "b2", download.Update{Bundle: &b}) expData = util.MustUnmarshalJSON([]byte(`{"b2": "foo"}`)) expIds = []string{"b2/id1"} validateStoreState(ctx, t, manager.Store, "/a", expData, expIds, "b2", "b2-1", nil) // Make sure the legacy path is gone now that we are in multi-bundle mode var actual string err = storage.Txn(ctx, plugin.manager.Store, storage.WriteParams, func(txn storage.Transaction) error { var err error if actual, err = bundle.LegacyReadRevisionFromStore(ctx, plugin.manager.Store, txn); err != nil && !storage.IsNotFound(err) { t.Fatalf("Failed to read manifest revision from store: %s", err) return err } return nil }) if err != nil { t.Fatalf("Unexpected error finishing transaction: %s", err) } if actual != "" { t.Fatalf("Expected to not find manifest revision but got %s", actual) } if !plugin.config.IsMultiBundle() { t.Fatalf("Expected plugin to be in multi bundle config mode") } } func TestSaveBundleToDiskNew(t *testing.T) { manager := getTestManager() dir, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("unexpected error %v", err) } defer os.RemoveAll(dir) bundles := map[string]*Source{} plugin := New(&Config{Bundles: bundles}, manager) plugin.bundlePersistPath = filepath.Join(dir, ".opa") err = plugin.saveBundleToDisk("foo", getTestRawBundle(t)) if err != nil { t.Fatalf("unexpected error %v", err) } } func TestSaveBundleToDiskNewConfiguredPersistDir(t *testing.T) { dir, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("unexpected error %v", err) } defer os.RemoveAll(dir) manager := getTestManager() manager.Config.PersistenceDirectory = &dir bundles := map[string]*Source{} plugin := New(&Config{Bundles: bundles}, manager) err = plugin.Start(context.Background()) if err != nil { t.Fatalf("unexpected error %v", err) } err = plugin.saveBundleToDisk("foo", getTestRawBundle(t)) if err != nil { t.Fatalf("unexpected error %v", err) } expectBundlePath := filepath.Join(dir, "bundles", "foo", "bundle.tar.gz") _, err = os.Stat(expectBundlePath) if err != nil { t.Errorf("expected bundle persisted at path %v, %v", expectBundlePath, err) } } func TestSaveBundleToDiskOverWrite(t *testing.T) { manager := getTestManager() // test to check existing bundle is replaced dir, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("unexpected error %v", err) } defer os.RemoveAll(dir) bundles := map[string]*Source{} plugin := New(&Config{Bundles: bundles}, manager) plugin.bundlePersistPath = filepath.Join(dir, ".opa") bundleName := "foo" bundleDir := filepath.Join(plugin.bundlePersistPath, bundleName) err = os.MkdirAll(bundleDir, os.ModePerm) if err != nil { t.Fatalf("unexpected error %v", err) } b2 := writeTestBundleToDisk(t, bundleDir, false) module := "package a.a1\n\nbar=1" newBundle := bundle.Bundle{ Manifest: bundle.Manifest{Revision: "quickbrownfaux", Roots: &[]string{"a/a1", "a/a2"}}, Data: map[string]interface{}{ "a": map[string]interface{}{ "a2": "foo", }, }, Modules: []bundle.ModuleFile{ { Path: "bundle/id1", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } newBundle.Manifest.Init() var buf bytes.Buffer if err := bundle.NewWriter(&buf).UseModulePath(true).Write(newBundle); err != nil { t.Fatal("unexpected error:", err) } err = plugin.saveBundleToDisk("foo", &buf) if err != nil { t.Fatalf("unexpected error %v", err) } actual, err := loadBundleFromDisk(plugin.bundlePersistPath, "foo", nil) if err != nil { t.Fatalf("unexpected error %v", err) } if actual.Equal(b2) { t.Fatal("expected existing bundle to be overwritten") } } func TestSaveCurrentBundleToDisk(t *testing.T) { srcDir, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("unexpected error %v", err) } defer os.RemoveAll(srcDir) err = saveCurrentBundleToDisk(srcDir, "bundle.tar.gz", getTestRawBundle(t)) if err != nil { t.Fatalf("unexpected error %v", err) } if _, err := os.Stat(filepath.Join(srcDir, "bundle.tar.gz")); err != nil { t.Fatalf("unexpected error %v", err) } err = saveCurrentBundleToDisk(srcDir, "bundle.tar.gz", nil) if err == nil { t.Fatal("expected error but got nil") } expErrMsg := "no raw bundle bytes to persist to disk" if err.Error() != expErrMsg { t.Fatalf("expected error: %v but got: %v", expErrMsg, err) } } func TestLoadBundleFromDisk(t *testing.T) { // no bundle on disk _, err := loadBundleFromDisk("foo", "bar", nil) if err != nil { t.Fatalf("unexpected error %v", err) } // create a test bundle and load it from disk dir, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("unexpected error %v", err) } defer os.RemoveAll(dir) bundleName := "foo" bundleDir := filepath.Join(dir, bundleName) err = os.MkdirAll(bundleDir, os.ModePerm) if err != nil { t.Fatalf("unexpected error %v", err) } b := writeTestBundleToDisk(t, bundleDir, false) result, err := loadBundleFromDisk(dir, bundleName, nil) if err != nil { t.Fatal("unexpected error:", err) } if !result.Equal(b) { t.Fatal("expected the test bundle to be equal to the one loaded from disk") } } func TestLoadSignedBundleFromDisk(t *testing.T) { // no bundle on disk _, err := loadBundleFromDisk("foo", "bar", nil) if err != nil { t.Fatalf("unexpected error %v", err) } // create a test signed bundle and load it from disk dir, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("unexpected error %v", err) } defer os.RemoveAll(dir) bundleName := "foo" bundleDir := filepath.Join(dir, bundleName) err = os.MkdirAll(bundleDir, os.ModePerm) if err != nil { t.Fatalf("unexpected error %v", err) } b := writeTestBundleToDisk(t, bundleDir, true) src := Source{ Signing: bundle.NewVerificationConfig(map[string]*keys.Config{"foo": {Key: "secret", Algorithm: "HS256"}}, "foo", "", nil), } result, err := loadBundleFromDisk(dir, bundleName, &src) if err != nil { t.Fatal("unexpected error:", err) } if !result.Equal(b) { t.Fatal("expected the test bundle to be equal to the one loaded from disk") } if !reflect.DeepEqual(result.Signatures, b.Signatures) { t.Fatal("Expected signatures to be same") } } func TestGetDefaultBundlePersistPath(t *testing.T) { plugin := New(&Config{}, getTestManager()) path, err := plugin.getBundlePersistPath() if err != nil { t.Fatalf("unexpected error %v", err) } if !strings.HasSuffix(path, ".opa/bundles") { t.Fatal("expected default persist path to end with '.opa/bundles' dir") } } func TestConfiguredBundlePersistPath(t *testing.T) { persistPath := "/var/opa" manager := getTestManager() manager.Config.PersistenceDirectory = &persistPath plugin := New(&Config{}, manager) path, err := plugin.getBundlePersistPath() if err != nil { t.Fatalf("unexpected error %v", err) } if path != "/var/opa/bundles" { t.Errorf("expected configured persist path '/var/opa/bundles'") } } func TestPluginUsingFileLoader(t *testing.T) { test.WithTempFS(map[string]string{}, func(dir string) { b := bundle.Bundle{ Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ { URL: "test.rego", Raw: []byte(`package test p = 7`), }, }, } name := path.Join(dir, "bundle.tar.gz") f, err := os.Create(name) if err != nil { t.Fatal(err) } if err := bundle.NewWriter(f).Write(b); err != nil { t.Fatal(err) } f.Close() mgr := getTestManager() url := "file://" + name p := New(&Config{Bundles: map[string]*Source{ "test": { SizeLimitBytes: 1e5, Resource: url, }, }}, mgr) ch := make(chan Status) p.Register("test", func(s Status) { ch <- s }) if err := p.Start(context.Background()); err != nil { t.Fatal(err) } s := <-ch if s.LastSuccessfulActivation.IsZero() { t.Fatal("expected successful activation") } }) } func TestPluginUsingDirectoryLoader(t *testing.T) { test.WithTempFS(map[string]string{ "test.rego": `package test p := 7`, }, func(dir string) { mgr := getTestManager() url := "file://" + dir p := New(&Config{Bundles: map[string]*Source{ "test": { SizeLimitBytes: 1e5, Resource: url, }, }}, mgr) ch := make(chan Status) p.Register("test", func(s Status) { ch <- s }) if err := p.Start(context.Background()); err != nil { t.Fatal(err) } s := <-ch if s.LastSuccessfulActivation.IsZero() { t.Fatal("expected successful activation") } }) } func TestPluginReadBundleEtagFromDiskStore(t *testing.T) { // setup fake http server with mock bundle mockBundle := bundle.Bundle{ Data: map[string]interface{}{"p": "x1"}, Modules: []bundle.ModuleFile{}, } notModifiedCount := 0 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { etag := r.Header.Get("If-None-Match") if etag == "foo" { notModifiedCount++ w.WriteHeader(304) return } w.Header().Add("Etag", "foo") w.WriteHeader(200) err := bundle.NewWriter(w).Write(mockBundle) if err != nil { t.Fatal(err) } })) test.WithTempFS(nil, func(dir string) { ctx := context.Background() store, err := disk.New(ctx, logging.NewNoOpLogger(), nil, disk.Options{ Dir: dir, Partitions: []storage.Path{ storage.MustParsePath("/foo"), }, }) if err != nil { t.Fatal(err) } // setup plugin pointing at fake server manager := getTestManagerWithOpts([]byte(fmt.Sprintf(`{ "services": { "default": { "url": %q } } }`, s.URL)), store) var mode plugins.TriggerMode = "manual" plugin := New(&Config{ Bundles: map[string]*Source{ "test": { Service: "default", SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), Config: download.Config{Trigger: &mode}, }, }, }, manager) statusCh := make(chan map[string]*Status) // register for bundle updates to observe changes and start the plugin plugin.RegisterBulkListener("test-case", func(st map[string]*Status) { statusCh <- st }) err = plugin.Start(ctx) if err != nil { t.Fatal(err) } // manually trigger bundle download go func() { _ = plugin.Loaders()["test"].Trigger(ctx) }() // wait for bundle update and then verify that activated bundle etag written to store <-statusCh txn := storage.NewTransactionOrDie(ctx, manager.Store) actual, err := manager.Store.Read(ctx, txn, storage.MustParsePath("/system/bundles/test/etag")) if err != nil { t.Fatal(err) } if actual != "foo" { t.Fatalf("Expected etag foo but got %v", actual) } // Stop the "read" transaction manager.Store.Abort(ctx, txn) // Stop the plugin and reinitialize it. Verify that etag is retrieved from store in the bundle request. // The server should respond with a 304 as OPA has the right bundle loaded. plugin.Stop(ctx) plugin = New(&Config{ Bundles: map[string]*Source{ "test": { Service: "default", SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), Config: download.Config{Trigger: &mode}, }, }, }, manager) statusCh = make(chan map[string]*Status) // register for bundle updates to observe changes and start the plugin plugin.RegisterBulkListener("test-case", func(st map[string]*Status) { statusCh <- st }) err = plugin.Start(ctx) if err != nil { t.Fatal(err) } val, ok := plugin.etags["test"] if !ok { t.Fatal("Expected etag entry for bundle \"test\"") } if val != "foo" { t.Fatalf("Expected etag foo but got %v", val) } // manually trigger bundle download go func() { _ = plugin.Loaders()["test"].Trigger(ctx) }() <-statusCh if notModifiedCount != 1 { t.Fatalf("Expected one bundle response with HTTP status 304 but got %v", notModifiedCount) } // reconfigure the plugin cfg := &Config{ Bundles: map[string]*Source{ "test": { Service: "default", SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), Config: download.Config{Trigger: &mode}, Resource: "/new/path/bundles/bundle.tar.gz", }, }, } plugin.Reconfigure(ctx, cfg) // manually trigger bundle download go func() { _ = plugin.Loaders()["test"].Trigger(ctx) }() <-statusCh if notModifiedCount != 2 { t.Fatalf("Expected two bundle responses with HTTP status 304 but got %v", notModifiedCount) } val, ok = plugin.etags["test"] if !ok { t.Fatal("Expected etag entry for bundle \"test\"") } if val != "foo" { t.Fatalf("Expected etag foo but got %v", val) } }) } func TestPluginManualTrigger(t *testing.T) { ctx := context.Background() // setup fake http server with mock bundle mockBundle := bundle.Bundle{ Data: map[string]interface{}{"p": "x1"}, Modules: []bundle.ModuleFile{}, } s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := bundle.NewWriter(w).Write(mockBundle) if err != nil { t.Fatal(err) } })) // setup plugin pointing at fake server manager := getTestManagerWithOpts([]byte(fmt.Sprintf(`{ "services": { "default": { "url": %q } } }`, s.URL))) var mode plugins.TriggerMode = "manual" plugin := New(&Config{ Bundles: map[string]*Source{ "test": { Service: "default", SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), Config: download.Config{Trigger: &mode}, }, }, }, manager) statusCh := make(chan map[string]*Status) // register for bundle updates to observe changes and start the plugin plugin.RegisterBulkListener("test-case", func(st map[string]*Status) { statusCh <- st }) err := plugin.Start(ctx) if err != nil { t.Fatal(err) } // manually trigger bundle download go func() { _ = plugin.Loaders()["test"].Trigger(ctx) }() // wait for bundle update and then assert on data content <-statusCh result, err := storage.ReadOne(ctx, manager.Store, storage.Path{"p"}) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(result, mockBundle.Data["p"]) { t.Fatalf("expected data to be %v but got %v", mockBundle.Data, result) } // update data and trigger another bundle download mockBundle.Data["p"] = "x2" // manually trigger bundle download go func() { _ = plugin.Loaders()["test"].Trigger(ctx) }() // wait for bundle update and then assert on data content <-statusCh result, err = storage.ReadOne(ctx, manager.Store, storage.Path{"p"}) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(result, mockBundle.Data["p"]) { t.Fatalf("expected data to be %v but got %v", mockBundle.Data, result) } } func TestPluginManualTriggerMultipleDiskStorage(t *testing.T) { ctx := context.Background() module := "package authz\n\ncorge=1" // setup fake http server with mock bundle mockBundle1 := bundle.Bundle{ Data: map[string]interface{}{"p": "x1"}, Modules: []bundle.ModuleFile{ { URL: "/bar/policy.rego", Path: "/bar/policy.rego", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, Manifest: bundle.Manifest{ Roots: &[]string{"p", "authz"}, }, } s1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := bundle.NewWriter(w).Write(mockBundle1) if err != nil { t.Fatal(err) } })) mockBundle2 := bundle.Bundle{ Data: map[string]interface{}{"q": "x2"}, Modules: []bundle.ModuleFile{}, Manifest: bundle.Manifest{ Roots: &[]string{"q"}, }, } s2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := bundle.NewWriter(w).Write(mockBundle2) if err != nil { t.Fatal(err) } })) test.WithTempFS(nil, func(dir string) { store, err := disk.New(ctx, logging.NewNoOpLogger(), nil, disk.Options{ Dir: dir, }) if err != nil { t.Fatal(err) } config := []byte(fmt.Sprintf(`{ "services": { "default": { "url": %q }, "acmecorp": { "url": %q } } }`, s1.URL, s2.URL)) manager := getTestManagerWithOpts(config, store) defer manager.Stop(ctx) var mode plugins.TriggerMode = "manual" plugin := New(&Config{ Bundles: map[string]*Source{ "test-1": { Service: "default", SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), Config: download.Config{Trigger: &mode}, }, "test-2": { Service: "acmecorp", SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), Config: download.Config{Trigger: &mode}, }, }, }, manager) statusCh := make(chan map[string]*Status) // register for bundle updates to observe changes and start the plugin plugin.RegisterBulkListener("test-case", func(st map[string]*Status) { statusCh <- st }) err = plugin.Start(ctx) if err != nil { t.Fatal(err) } // manually trigger bundle download on all configured bundles go func() { _ = plugin.Trigger(ctx) }() // wait for bundle update and then assert on data content <-statusCh <-statusCh result, err := storage.ReadOne(ctx, manager.Store, storage.Path{"p"}) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(result, mockBundle1.Data["p"]) { t.Fatalf("expected data to be %v but got %v", mockBundle1.Data, result) } result, err = storage.ReadOne(ctx, manager.Store, storage.Path{"q"}) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(result, mockBundle2.Data["q"]) { t.Fatalf("expected data to be %v but got %v", mockBundle2.Data, result) } txn := storage.NewTransactionOrDie(ctx, manager.Store) defer manager.Store.Abort(ctx, txn) ids, err := manager.Store.ListPolicies(ctx, txn) if err != nil { t.Fatal(err) } else if len(ids) != 1 { t.Fatal("Expected 1 policy") } bs, err := manager.Store.GetPolicy(ctx, txn, ids[0]) exp := []byte("package authz\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 := manager.Store.Read(ctx, txn, storage.Path{}) expData := util.MustUnmarshalJSON([]byte(`{"p": "x1", "q": "x2", "system": {"bundles": {"test-1": {"etag": "", "manifest": {"revision": "", "roots": ["p", "authz"]}}, "test-2": {"etag": "", "manifest": {"revision": "", "roots": ["q"]}}}}}`)) 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) } }) } func TestPluginManualTriggerMultiple(t *testing.T) { ctx := context.Background() // setup fake http server with mock bundle mockBundle1 := bundle.Bundle{ Data: map[string]interface{}{"p": "x1"}, Modules: []bundle.ModuleFile{}, Manifest: bundle.Manifest{ Roots: &[]string{"p"}, }, } s1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := bundle.NewWriter(w).Write(mockBundle1) if err != nil { t.Fatal(err) } })) mockBundle2 := bundle.Bundle{ Data: map[string]interface{}{"q": "x2"}, Modules: []bundle.ModuleFile{}, Manifest: bundle.Manifest{ Roots: &[]string{"q"}, }, } s2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := bundle.NewWriter(w).Write(mockBundle2) if err != nil { t.Fatal(err) } })) // setup plugin pointing at fake server manager := getTestManagerWithOpts([]byte(fmt.Sprintf(`{ "services": { "default": { "url": %q }, "acmecorp": { "url": %q } } }`, s1.URL, s2.URL))) var mode plugins.TriggerMode = "manual" plugin := New(&Config{ Bundles: map[string]*Source{ "test-1": { Service: "default", SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), Config: download.Config{Trigger: &mode}, }, "test-2": { Service: "acmecorp", SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), Config: download.Config{Trigger: &mode}, }, }, }, manager) statusCh := make(chan map[string]*Status) // register for bundle updates to observe changes and start the plugin plugin.RegisterBulkListener("test-case", func(st map[string]*Status) { statusCh <- st }) err := plugin.Start(ctx) if err != nil { t.Fatal(err) } // manually trigger bundle download on all configured bundles go func() { _ = plugin.Trigger(ctx) }() // wait for bundle update and then assert on data content <-statusCh <-statusCh result, err := storage.ReadOne(ctx, manager.Store, storage.Path{"p"}) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(result, mockBundle1.Data["p"]) { t.Fatalf("expected data to be %v but got %v", mockBundle1.Data, result) } result, err = storage.ReadOne(ctx, manager.Store, storage.Path{"q"}) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(result, mockBundle2.Data["q"]) { t.Fatalf("expected data to be %v but got %v", mockBundle2.Data, result) } } func TestPluginManualTriggerWithTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(3 * time.Second) // this should cause the context deadline to exceed })) // setup plugin pointing at fake server manager := getTestManagerWithOpts([]byte(fmt.Sprintf(`{ "services": { "default": { "url": %q } } }`, s.URL))) var mode plugins.TriggerMode = "manual" bundleName := "test" plugin := New(&Config{ Bundles: map[string]*Source{ bundleName: { Service: "default", SizeLimitBytes: int64(bundle.DefaultSizeLimitBytes), Config: download.Config{Trigger: &mode}, }, }, }, manager) statusCh := make(chan map[string]*Status) // register for bundle updates to observe changes and start the plugin plugin.RegisterBulkListener("test-case", func(st map[string]*Status) { statusCh <- st }) err := plugin.Start(ctx) if err != nil { t.Fatal(err) } // manually trigger bundle download go func() { _ = plugin.Loaders()[bundleName].Trigger(ctx) }() // wait for bundle update u := <-statusCh if u[bundleName].Code != errCode { t.Fatalf("expected error code %v but got %v", errCode, u[bundleName].Code) } if !strings.Contains(u[bundleName].Message, "context deadline exceeded") { t.Fatalf("unexpected error message %v", u[bundleName].Message) } } func writeTestBundleToDisk(t *testing.T, srcDir string, signed bool) bundle.Bundle { t.Helper() var b bundle.Bundle if signed { b = getTestSignedBundle(t) } else { b = getTestBundle(t) } var buf bytes.Buffer if err := bundle.NewWriter(&buf).UseModulePath(true).Write(b); err != nil { t.Fatalf("unexpected error %v", err) } if err := ioutil.WriteFile(filepath.Join(srcDir, "bundle.tar.gz"), buf.Bytes(), 0644); err != nil { t.Fatalf("unexpected error %v", err) } return b } func getTestBundle(t *testing.T) bundle.Bundle { t.Helper() module := "package gork\np[x] { x = 1 }" b := bundle.Bundle{ Manifest: bundle.Manifest{ Revision: "quickbrownfaux", }, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ { Path: "/foo.rego", URL: "/foo.rego", Parsed: ast.MustParseModule(module), Raw: []byte(module), }, }, } b.Manifest.Init() return b } func getTestSignedBundle(t *testing.T) bundle.Bundle { t.Helper() b := getTestBundle(t) if err := b.GenerateSignature(bundle.NewSigningConfig("secret", "HS256", ""), "foo", false); err != nil { t.Fatal("Unexpected error:", err) } return b } func getTestRawBundle(t *testing.T) io.Reader { t.Helper() b := getTestBundle(t) var buf bytes.Buffer if err := bundle.NewWriter(&buf).UseModulePath(true).Write(b); err != nil { t.Fatal("unexpected error:", err) } return &buf } func validateStoreState(ctx context.Context, t *testing.T, store storage.Store, root string, expData interface{}, expIds []string, expBundleName string, expBundleRev string, expMetadata map[string]interface{}) { t.Helper() if err := storage.Txn(ctx, store, storage.TransactionParams{}, func(txn storage.Transaction) error { value, err := store.Read(ctx, txn, storage.MustParsePath(root)) if err != nil { return err } if !reflect.DeepEqual(value, expData) { return fmt.Errorf("Expected %v but got %v", expData, value) } ids, err := store.ListPolicies(ctx, txn) if err != nil { return err } sort.Strings(ids) sort.Strings(expIds) if !reflect.DeepEqual(ids, expIds) { return fmt.Errorf("Expected ids %v but got %v", expIds, ids) } rev, err := bundle.ReadBundleRevisionFromStore(ctx, store, txn, expBundleName) if err != nil { return fmt.Errorf("Unexpected error when reading bundle revision from store: %s", err) } if rev != expBundleRev { return fmt.Errorf("Unexpected revision found on bundle: %s", rev) } metadata, err := bundle.ReadBundleMetadataFromStore(ctx, store, txn, expBundleName) if err != nil { return fmt.Errorf("Unexpected error when reading bundle metadata from store: %s", err) } if !reflect.DeepEqual(expMetadata, metadata) { return fmt.Errorf("Unexpected metadata found on bundle: %v", metadata) } return nil }); err != nil { t.Fatal(err) } } func ensurePluginState(t *testing.T, p *Plugin, state plugins.State) { t.Helper() status, ok := p.manager.PluginStatus()[Name] if !ok { t.Fatalf("Expected to find state for %s, found nil", Name) return } if status.State != state { t.Fatalf("Unexpected status state found in plugin manager for %s:\n\n\tFound:%+v\n\n\tExpected: %s", Name, status.State, state) } }