diff --git a/internal/cmd/base/servers.go b/internal/cmd/base/servers.go index d4f7ff3a4..bfa843e42 100644 --- a/internal/cmd/base/servers.go +++ b/internal/cmd/base/servers.go @@ -125,7 +125,7 @@ type Server struct { DevTargetDefaultPort int DevTargetSessionMaxSeconds int DevTargetSessionConnectionLimit int - DevLoopbackHostPluginId string + DevLoopbackPluginId string EnabledPlugins []EnabledPlugin HostPlugins map[string]plgpb.HostPluginServiceClient diff --git a/internal/cmd/commands/dev/dev.go b/internal/cmd/commands/dev/dev.go index eb75f6d39..596ddfd3e 100644 --- a/internal/cmd/commands/dev/dev.go +++ b/internal/cmd/commands/dev/dev.go @@ -703,7 +703,7 @@ func (c *Command) Run(args []string) int { var opts []base.Option if c.flagCreateLoopbackHostPlugin { - c.DevLoopbackHostPluginId = "pl_1234567890" + c.DevLoopbackPluginId = "pl_1234567890" c.EnabledPlugins = append(c.EnabledPlugins, base.EnabledPluginHostLoopback) c.Config.Controller.Scheduler.JobRunIntervalDuration = 100 * time.Millisecond } diff --git a/internal/daemon/controller/controller.go b/internal/daemon/controller/controller.go index e7a40286a..7c617be60 100644 --- a/internal/daemon/controller/controller.go +++ b/internal/daemon/controller/controller.go @@ -34,6 +34,7 @@ import ( kmsjob "github.com/hashicorp/boundary/internal/kms/job" "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/scheduler" "github.com/hashicorp/boundary/internal/scheduler/cleaner" "github.com/hashicorp/boundary/internal/scheduler/job" @@ -251,10 +252,14 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { } switch enabledPlugin { case base.EnabledPluginHostLoopback: - plg := pluginhost.NewWrappingPluginClient(pluginhost.NewLoopbackPlugin()) + lp, err := loopback.NewLoopbackPlugin() + if err != nil { + return nil, fmt.Errorf("error creating loopback plugin: %w", err) + } + plg := loopback.NewWrappingPluginClient(lp) opts := []host.Option{ host.WithDescription("Provides an initial loopback host plugin in Boundary"), - host.WithPublicId(conf.DevLoopbackHostPluginId), + host.WithPublicId(conf.DevLoopbackPluginId), } if _, err = conf.RegisterHostPlugin(ctx, "loopback", plg, opts...); err != nil { return nil, err diff --git a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go index 91ee2e65b..ee4bb7526 100644 --- a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go +++ b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/scheduler" "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/boundary/internal/types/subtypes" @@ -838,7 +839,7 @@ func TestCreate_Plugin(t *testing.T) { plg := host.TestPlugin(t, conn, name) pluginHostRepo := func() (*plugin.Repository, error) { return plugin.NewRepository(rw, rw, kms, sche, map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{ + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{ OnCreateCatalogFn: func(ctx context.Context, req *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) { return nil, nil }, @@ -1372,8 +1373,10 @@ func TestUpdate_Plugin(t *testing.T) { name := "test" plg := host.TestPlugin(t, conn, name) + lp, err := loopback.NewLoopbackPlugin() + require.NoError(t, err) plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(plugin.NewLoopbackPlugin()), + plg.GetPublicId(): loopback.NewWrappingPluginClient(lp), } repoFn := func() (*static.Repository, error) { diff --git a/internal/daemon/controller/handlers/host_sets/host_set_service_test.go b/internal/daemon/controller/handlers/host_sets/host_set_service_test.go index e28687af0..3717d554d 100644 --- a/internal/daemon/controller/handlers/host_sets/host_set_service_test.go +++ b/internal/daemon/controller/handlers/host_sets/host_set_service_test.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" hostplugin "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/scheduler" "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/boundary/internal/types/subtypes" @@ -166,7 +167,7 @@ func TestGet_Plugin(t *testing.T) { prefEndpoints := []string{"cidr:1.2.3.4", "cidr:2.3.4.5/24"} plg := hostplugin.TestPlugin(t, conn, name) plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), } pluginRepoFn := func() (*plugin.Repository, error) { return plugin.NewRepository(rw, rw, kms, sche, plgm) @@ -380,7 +381,7 @@ func TestList_Plugin(t *testing.T) { name := "test" plg := hostplugin.TestPlugin(t, conn, name) plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), } pluginRepoFn := func() (*plugin.Repository, error) { return plugin.NewRepository(rw, rw, kms, sche, plgm) @@ -568,7 +569,7 @@ func TestDelete_Plugin(t *testing.T) { name := "test" plg := hostplugin.TestPlugin(t, conn, name) plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), } hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) @@ -837,7 +838,7 @@ func TestCreate_Plugin(t *testing.T) { plg := hostplugin.TestPlugin(t, conn, name) plgRepoFn := func() (*plugin.Repository, error) { return plugin.NewRepository(rw, rw, kms, sche, map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{ + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{ OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { return nil, nil }, @@ -1476,7 +1477,7 @@ func TestUpdate_Plugin(t *testing.T) { name := "test" plg := hostplugin.TestPlugin(t, conn, name) plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), } repoFn := func() (*static.Repository, error) { diff --git a/internal/daemon/controller/handlers/hosts/host_service_test.go b/internal/daemon/controller/handlers/hosts/host_service_test.go index eb21f042e..1b7406431 100644 --- a/internal/daemon/controller/handlers/hosts/host_service_test.go +++ b/internal/daemon/controller/handlers/hosts/host_service_test.go @@ -24,6 +24,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" hostplugin "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/scheduler" "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/boundary/internal/types/subtypes" @@ -153,7 +154,7 @@ func TestGet_Plugin(t *testing.T) { plg := hostplugin.TestPlugin(t, conn, "test") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), } rw := db.New(conn) @@ -384,7 +385,7 @@ func TestList_Plugin(t *testing.T) { org, proj := iam.TestScopes(t, iamRepo) plg := hostplugin.TestPlugin(t, conn, "test") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), } rw := db.New(conn) diff --git a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go index e8fa272a5..2fa175b92 100644 --- a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go +++ b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go @@ -38,6 +38,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/scheduler" "github.com/hashicorp/boundary/internal/server" @@ -1368,7 +1369,7 @@ func TestAddTargetHostSources(t *testing.T) { plg := host.TestPlugin(t, conn, "test") pluginHc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) pluginHs := plugin.TestSet(t, conn, kms, sche, pluginHc, map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), }) ctx := context.Background() @@ -1530,7 +1531,7 @@ func TestSetTargetHostSources(t *testing.T) { plg := host.TestPlugin(t, conn, "test") pluginHc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) pluginHs := plugin.TestSet(t, conn, kms, sche, pluginHc, map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), }) ctx := context.Background() @@ -1680,7 +1681,7 @@ func TestRemoveTargetHostSources(t *testing.T) { plg := host.TestPlugin(t, conn, "test") pluginHc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) pluginHs := plugin.TestSet(t, conn, kms, sche, pluginHc, map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), }) ctx := context.Background() @@ -2456,7 +2457,7 @@ func TestAuthorizeSession(t *testing.T) { plg := host.TestPlugin(t, conn, "test") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(plugin.TestPluginServer{ + plg.GetPublicId(): loopback.NewWrappingPluginClient(loopback.TestPluginServer{ ListHostsFn: func(_ context.Context, req *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) { var setIds []string for _, set := range req.GetSets() { diff --git a/internal/daemon/controller/testing.go b/internal/daemon/controller/testing.go index 2aac1eccc..4a5383240 100644 --- a/internal/daemon/controller/testing.go +++ b/internal/daemon/controller/testing.go @@ -612,7 +612,7 @@ func TestControllerConfig(t testing.TB, ctx context.Context, tc *TestController, tc.b.DevOidcAccountId = DefaultTestOidcAccountId tc.b.DevUnprivilegedPasswordAccountId = DefaultTestUnprivilegedPasswordAccountId tc.b.DevUnprivilegedOidcAccountId = DefaultTestUnprivilegedOidcAccountId - tc.b.DevLoopbackHostPluginId = DefaultTestPluginId + tc.b.DevLoopbackPluginId = DefaultTestPluginId tc.b.EnabledPlugins = append(tc.b.EnabledPlugins, base.EnabledPluginHostLoopback) diff --git a/internal/host/plugin/host_set_member_test.go b/internal/host/plugin/host_set_member_test.go index 56bd57940..22a5ba8b4 100644 --- a/internal/host/plugin/host_set_member_test.go +++ b/internal/host/plugin/host_set_member_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" hostplg "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/scheduler" plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" "github.com/stretchr/testify/assert" @@ -35,7 +36,7 @@ func TestHostSetMember_InsertDelete(t *testing.T) { plg := hostplg.TestPlugin(t, conn, "create") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(&plgpb.UnimplementedHostPluginServiceServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&plgpb.UnimplementedHostPluginServiceServer{}), } repo, err := NewRepository(rw, rw, kms, sched, plgm) diff --git a/internal/host/plugin/immutable_fields_test.go b/internal/host/plugin/immutable_fields_test.go index 342f7cc60..0ca5a22c6 100644 --- a/internal/host/plugin/immutable_fields_test.go +++ b/internal/host/plugin/immutable_fields_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/scheduler" plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" "github.com/stretchr/testify/assert" @@ -108,7 +109,7 @@ func TestPluginSet_ImmutableFields(t *testing.T) { plg := host.TestPlugin(t, conn, "test") cat := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) new := TestSet(t, conn, kmsCache, sched, cat, map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(&TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), }) tests := []struct { diff --git a/internal/host/plugin/job_orphaned_host_cleanup_test.go b/internal/host/plugin/job_orphaned_host_cleanup_test.go index 2976ffc15..f86e54c68 100644 --- a/internal/host/plugin/job_orphaned_host_cleanup_test.go +++ b/internal/host/plugin/job_orphaned_host_cleanup_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" hostplg "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/scheduler" plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" "github.com/stretchr/testify/assert" @@ -105,10 +106,10 @@ func TestOrphanedHostCleanupJob_Run(t *testing.T) { kmsCache := kms.TestKms(t, conn, wrapper) sched := scheduler.TestScheduler(t, conn, wrapper) - plgServer := &TestPluginServer{} + plgServer := &loopback.TestPluginServer{} plg := hostplg.TestPlugin(t, conn, "run") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(plgServer), + plg.GetPublicId(): loopback.NewWrappingPluginClient(plgServer), } r, err := newOrphanedHostCleanupJob(ctx, rw, rw, kmsCache) diff --git a/internal/host/plugin/job_set_sync_test.go b/internal/host/plugin/job_set_sync_test.go index c47023a93..3ed2c4ce5 100644 --- a/internal/host/plugin/job_set_sync_test.go +++ b/internal/host/plugin/job_set_sync_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" hostplg "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/scheduler" plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" assertpkg "github.com/stretchr/testify/assert" @@ -31,7 +32,7 @@ func TestNewSetSyncJob(t *testing.T) { plg := hostplg.TestPlugin(t, conn, "lookup") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(&TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), } type args struct { @@ -142,10 +143,10 @@ func TestSetSyncJob_Run(t *testing.T) { kmsCache := kms.TestKms(t, conn, wrapper) sched := scheduler.TestScheduler(t, conn, wrapper) - plgServer := &TestPluginServer{} + plgServer := &loopback.TestPluginServer{} plg := hostplg.TestPlugin(t, conn, "run") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(plgServer), + plg.GetPublicId(): loopback.NewWrappingPluginClient(plgServer), } r, err := newSetSyncJob(ctx, rw, rw, kmsCache, plgm) @@ -413,7 +414,7 @@ func TestSetSyncJob_NextRunIn(t *testing.T) { _, prj := iam.TestScopes(t, iamRepo) plg := hostplg.TestPlugin(t, conn, "lookup") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(&TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), } catalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) hostSet := TestSet(t, conn, kmsCache, sched, catalog, plgm) diff --git a/internal/host/plugin/repository_host_catalog_test.go b/internal/host/plugin/repository_host_catalog_test.go index 2a7ad6143..48c920ba6 100644 --- a/internal/host/plugin/repository_host_catalog_test.go +++ b/internal/host/plugin/repository_host_catalog_test.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/boundary/internal/libs/patchstruct" "github.com/hashicorp/boundary/internal/oplog" hostplugin "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/scheduler" "github.com/hashicorp/boundary/internal/scheduler/job" plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" @@ -270,8 +271,8 @@ func TestRepository_CreateCatalog(t *testing.T) { var pluginCalled bool plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): &WrappingPluginClient{ - Server: &TestPluginServer{ + plg.GetPublicId(): &loopback.WrappingPluginClient{ + Server: &loopback.TestPluginServer{ NormalizeCatalogDataFn: func(_ context.Context, req *plgpb.NormalizeCatalogDataRequest) (*plgpb.NormalizeCatalogDataResponse, error) { if req.Attributes == nil { return new(plgpb.NormalizeCatalogDataResponse), nil @@ -296,7 +297,7 @@ func TestRepository_CreateCatalog(t *testing.T) { }, }, }, - unimplementedPlugin.GetPublicId(): &WrappingPluginClient{Server: &TestPluginServer{ + unimplementedPlugin.GetPublicId(): &loopback.WrappingPluginClient{Server: &loopback.TestPluginServer{ OnCreateCatalogFn: func(ctx context.Context, req *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) { pluginCalled = true gotPluginAttrs = req.GetCatalog().GetAttributes() @@ -372,8 +373,8 @@ func TestRepository_CreateCatalog(t *testing.T) { kms := kms.TestKms(t, conn, wrapper) var pluginCalled bool plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): &WrappingPluginClient{ - Server: &TestPluginServer{ + plg.GetPublicId(): &loopback.WrappingPluginClient{ + Server: &loopback.TestPluginServer{ OnCreateCatalogFn: func(_ context.Context, req *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) { pluginCalled = true return &plgpb.OnCreateCatalogResponse{Persisted: &plgpb.HostCatalogPersisted{Secrets: req.GetCatalog().GetSecrets()}}, nil @@ -418,8 +419,8 @@ func TestRepository_CreateCatalog(t *testing.T) { kms := kms.TestKms(t, conn, wrapper) var pluginCalled bool plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): &WrappingPluginClient{ - Server: &TestPluginServer{ + plg.GetPublicId(): &loopback.WrappingPluginClient{ + Server: &loopback.TestPluginServer{ OnCreateCatalogFn: func(_ context.Context, req *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) { pluginCalled = true return &plgpb.OnCreateCatalogResponse{Persisted: &plgpb.HostCatalogPersisted{Secrets: req.GetCatalog().GetSecrets()}}, nil @@ -496,8 +497,8 @@ func TestRepository_UpdateCatalog(t *testing.T) { var pluginError error testPlugin := hostplugin.TestPlugin(t, dbConn, "test") testPluginMap := map[string]plgpb.HostPluginServiceClient{ - testPlugin.GetPublicId(): &WrappingPluginClient{ - Server: &TestPluginServer{ + testPlugin.GetPublicId(): &loopback.WrappingPluginClient{ + Server: &loopback.TestPluginServer{ NormalizeCatalogDataFn: func(_ context.Context, req *plgpb.NormalizeCatalogDataRequest) (*plgpb.NormalizeCatalogDataResponse, error) { if req.Attributes == nil { return new(plgpb.NormalizeCatalogDataResponse), nil @@ -1264,7 +1265,7 @@ func TestRepository_LookupCatalog(t *testing.T) { _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) plg := hostplugin.TestPlugin(t, conn, "test") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): &WrappingPluginClient{Server: &TestPluginServer{}}, + plg.GetPublicId(): &loopback.WrappingPluginClient{Server: &loopback.TestPluginServer{}}, } cat := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) badId, err := newHostCatalogId(ctx) @@ -1331,7 +1332,7 @@ func TestRepository_ListCatalogs_Multiple_Scopes(t *testing.T) { sched := scheduler.TestScheduler(t, conn, wrapper) plg := hostplugin.TestPlugin(t, conn, "test") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): &WrappingPluginClient{Server: &TestPluginServer{}}, + plg.GetPublicId(): &loopback.WrappingPluginClient{Server: &loopback.TestPluginServer{}}, } repo, err := NewRepository(rw, rw, kms, sched, plgm) assert.NoError(t, err) @@ -1363,9 +1364,9 @@ func TestRepository_DeleteCatalog(t *testing.T) { sched := scheduler.TestScheduler(t, conn, wrapper) _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) plg := hostplugin.TestPlugin(t, conn, "test") - pluginInstance := &TestPluginServer{} + pluginInstance := &loopback.TestPluginServer{} plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): &WrappingPluginClient{Server: pluginInstance}, + plg.GetPublicId(): &loopback.WrappingPluginClient{Server: pluginInstance}, } cat := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) cat2 := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) @@ -1450,7 +1451,7 @@ func TestRepository_DeleteCatalogX(t *testing.T) { _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) plg := hostplugin.TestPlugin(t, conn, "test") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): &WrappingPluginClient{Server: &TestPluginServer{}}, + plg.GetPublicId(): &loopback.WrappingPluginClient{Server: &loopback.TestPluginServer{}}, } cat := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) badId, err := newHostCatalogId(ctx) @@ -1538,7 +1539,7 @@ func TestRepository_UpdateCatalog_SyncSets(t *testing.T) { testPlugin := hostplugin.TestPlugin(t, dbConn, "test") dummyPluginMap := map[string]plgpb.HostPluginServiceClient{ - testPlugin.GetPublicId(): &WrappingPluginClient{Server: &plgpb.UnimplementedHostPluginServiceServer{}}, + testPlugin.GetPublicId(): &loopback.WrappingPluginClient{Server: &plgpb.UnimplementedHostPluginServiceServer{}}, } // Set up a test catalog and the secrets for it diff --git a/internal/host/plugin/repository_host_set_test.go b/internal/host/plugin/repository_host_set_test.go index e123e4655..8a79c3c52 100644 --- a/internal/host/plugin/repository_host_set_test.go +++ b/internal/host/plugin/repository_host_set_test.go @@ -24,6 +24,7 @@ import ( "github.com/hashicorp/boundary/internal/libs/patchstruct" "github.com/hashicorp/boundary/internal/oplog" hostplg "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/scheduler" plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" "github.com/mitchellh/mapstructure" @@ -278,7 +279,7 @@ func TestRepository_CreateSet(t *testing.T) { } var pluginCalled bool plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(TestPluginServer{ + plg.GetPublicId(): loopback.NewWrappingPluginClient(loopback.TestPluginServer{ NormalizeSetDataFn: func(_ context.Context, req *plgpb.NormalizeSetDataRequest) (*plgpb.NormalizeSetDataResponse, error) { if req.Attributes == nil { return new(plgpb.NormalizeSetDataResponse), nil @@ -302,7 +303,7 @@ func TestRepository_CreateSet(t *testing.T) { return &plgpb.OnCreateSetResponse{}, nil }, }), - unimplementedPlugin.GetPublicId(): NewWrappingPluginClient(TestPluginServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { + unimplementedPlugin.GetPublicId(): loopback.NewWrappingPluginClient(loopback.TestPluginServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { pluginCalled = true pluginReceivedAttrs = req.GetSet().GetAttributes() return plgpb.UnimplementedHostPluginServiceServer{}.OnCreateSet(ctx, req) @@ -356,7 +357,7 @@ func TestRepository_CreateSet(t *testing.T) { assert, require := assert.New(t), require.New(t) var pluginCalled bool plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(TestPluginServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { + plg.GetPublicId(): loopback.NewWrappingPluginClient(loopback.TestPluginServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { pluginCalled = true return &plgpb.OnCreateSetResponse{}, nil }}), @@ -399,7 +400,7 @@ func TestRepository_CreateSet(t *testing.T) { assert, require := assert.New(t), require.New(t) var pluginCalled bool plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(TestPluginServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { + plg.GetPublicId(): loopback.NewWrappingPluginClient(loopback.TestPluginServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { pluginCalled = true return &plgpb.OnCreateSetResponse{}, nil }}), @@ -461,7 +462,7 @@ func TestRepository_UpdateSet(t *testing.T) { testPlugin := hostplg.TestPlugin(t, dbConn, "test") dummyPluginMap := map[string]plgpb.HostPluginServiceClient{ - testPlugin.GetPublicId(): &WrappingPluginClient{Server: &plgpb.UnimplementedHostPluginServiceServer{}}, + testPlugin.GetPublicId(): &loopback.WrappingPluginClient{Server: &plgpb.UnimplementedHostPluginServiceServer{}}, } // Set up a test catalog and the secrets for it @@ -1199,8 +1200,8 @@ func TestRepository_UpdateSet(t *testing.T) { // well. var gotOnUpdateCallCount int testPluginMap := map[string]plgpb.HostPluginServiceClient{ - testPlugin.GetPublicId(): &WrappingPluginClient{ - Server: &TestPluginServer{ + testPlugin.GetPublicId(): &loopback.WrappingPluginClient{ + Server: &loopback.TestPluginServer{ NormalizeSetDataFn: func(_ context.Context, req *plgpb.NormalizeSetDataRequest) (*plgpb.NormalizeSetDataResponse, error) { if req.Attributes == nil { return new(plgpb.NormalizeSetDataResponse), nil @@ -1298,8 +1299,8 @@ func TestRepository_UpdateSet(t *testing.T) { t.Run("Unset Empty PreferredEndpoint", func(t *testing.T) { var gotOnUpdateCallCount int testPluginMap := map[string]plgpb.HostPluginServiceClient{ - testPlugin.GetPublicId(): &WrappingPluginClient{ - Server: &TestPluginServer{ + testPlugin.GetPublicId(): &loopback.WrappingPluginClient{ + Server: &loopback.TestPluginServer{ OnUpdateSetFn: func(_ context.Context, req *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error) { gotOnUpdateCallCount++ for _, check := range []checkPluginReqFunc{ @@ -1351,12 +1352,12 @@ func TestRepository_LookupSet(t *testing.T) { _, prj := iam.TestScopes(t, iamRepo) plg := hostplg.TestPlugin(t, conn, "lookup") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(&TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), } catalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) hostSet := TestSet(t, conn, kms, sched, catalog, map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(&TestPluginServer{ + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{ ListHostsFn: func(ctx context.Context, req *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) { require.NotEmpty(t, req.GetSets()) require.NotNil(t, req.GetCatalog()) @@ -1422,7 +1423,7 @@ func TestRepository_Endpoints(t *testing.T) { hostlessCatalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(&TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), } catalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) @@ -1556,7 +1557,7 @@ func TestRepository_ListSets(t *testing.T) { _, prj := iam.TestScopes(t, iamRepo) plg := hostplg.TestPlugin(t, conn, "list") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(&TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), } catalogA := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) catalogB := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) @@ -1628,7 +1629,7 @@ func TestRepository_ListSets_Limits(t *testing.T) { _, prj := iam.TestScopes(t, iamRepo) plg := hostplg.TestPlugin(t, conn, "listlimit") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(&TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), } catalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) count := 10 @@ -1708,7 +1709,7 @@ func TestRepository_DeleteSet(t *testing.T) { plg := hostplg.TestPlugin(t, conn, "create") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(TestPluginServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { + plg.GetPublicId(): loopback.NewWrappingPluginClient(loopback.TestPluginServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { return &plgpb.OnCreateSetResponse{}, nil }}), } diff --git a/internal/host/plugin/repository_host_test.go b/internal/host/plugin/repository_host_test.go index 144da9961..a27c58df3 100644 --- a/internal/host/plugin/repository_host_test.go +++ b/internal/host/plugin/repository_host_test.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/boundary/internal/oplog" hostplg "github.com/hashicorp/boundary/internal/plugin/host" hostplgstore "github.com/hashicorp/boundary/internal/plugin/host/store" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/scheduler" plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" "github.com/stretchr/testify/assert" @@ -39,7 +40,7 @@ func TestJob_UpsertHosts(t *testing.T) { plg := hostplg.TestPlugin(t, conn, "create") plgm := map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): NewWrappingPluginClient(&plgpb.UnimplementedHostPluginServiceServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&plgpb.UnimplementedHostPluginServiceServer{}), } catalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) diff --git a/internal/host/plugin/testing.go b/internal/host/plugin/testing.go index b1ba852d1..29fe2166b 100644 --- a/internal/host/plugin/testing.go +++ b/internal/host/plugin/testing.go @@ -231,82 +231,3 @@ func testGetIpAddress(t testing.TB) string { } } } - -var _ plgpb.HostPluginServiceServer = (*TestPluginServer)(nil) - -// TestPluginServer provides a host plugin service server where each method can be overwritten for tests. -type TestPluginServer struct { - NormalizeCatalogDataFn func(context.Context, *plgpb.NormalizeCatalogDataRequest) (*plgpb.NormalizeCatalogDataResponse, error) - OnCreateCatalogFn func(context.Context, *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) - OnUpdateCatalogFn func(context.Context, *plgpb.OnUpdateCatalogRequest) (*plgpb.OnUpdateCatalogResponse, error) - OnDeleteCatalogFn func(context.Context, *plgpb.OnDeleteCatalogRequest) (*plgpb.OnDeleteCatalogResponse, error) - NormalizeSetDataFn func(context.Context, *plgpb.NormalizeSetDataRequest) (*plgpb.NormalizeSetDataResponse, error) - OnCreateSetFn func(context.Context, *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) - OnUpdateSetFn func(context.Context, *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error) - OnDeleteSetFn func(context.Context, *plgpb.OnDeleteSetRequest) (*plgpb.OnDeleteSetResponse, error) - ListHostsFn func(context.Context, *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) - plgpb.UnimplementedHostPluginServiceServer -} - -func (t TestPluginServer) NormalizeCatalogData(ctx context.Context, req *plgpb.NormalizeCatalogDataRequest) (*plgpb.NormalizeCatalogDataResponse, error) { - if t.NormalizeCatalogDataFn == nil { - return t.UnimplementedHostPluginServiceServer.NormalizeCatalogData(ctx, req) - } - return t.NormalizeCatalogDataFn(ctx, req) -} - -func (t TestPluginServer) OnCreateCatalog(ctx context.Context, req *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) { - if t.OnCreateCatalogFn == nil { - return t.UnimplementedHostPluginServiceServer.OnCreateCatalog(ctx, req) - } - return t.OnCreateCatalogFn(ctx, req) -} - -func (t TestPluginServer) OnUpdateCatalog(ctx context.Context, req *plgpb.OnUpdateCatalogRequest) (*plgpb.OnUpdateCatalogResponse, error) { - if t.OnUpdateCatalogFn == nil { - return t.UnimplementedHostPluginServiceServer.OnUpdateCatalog(ctx, req) - } - return t.OnUpdateCatalogFn(ctx, req) -} - -func (t TestPluginServer) OnDeleteCatalog(ctx context.Context, req *plgpb.OnDeleteCatalogRequest) (*plgpb.OnDeleteCatalogResponse, error) { - if t.OnDeleteCatalogFn == nil { - return t.UnimplementedHostPluginServiceServer.OnDeleteCatalog(ctx, req) - } - return t.OnDeleteCatalogFn(ctx, req) -} - -func (t TestPluginServer) NormalizeSetData(ctx context.Context, req *plgpb.NormalizeSetDataRequest) (*plgpb.NormalizeSetDataResponse, error) { - if t.NormalizeSetDataFn == nil { - return t.UnimplementedHostPluginServiceServer.NormalizeSetData(ctx, req) - } - return t.NormalizeSetDataFn(ctx, req) -} - -func (t TestPluginServer) OnCreateSet(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { - if t.OnCreateSetFn == nil { - return t.UnimplementedHostPluginServiceServer.OnCreateSet(ctx, req) - } - return t.OnCreateSetFn(ctx, req) -} - -func (t TestPluginServer) OnUpdateSet(ctx context.Context, req *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error) { - if t.OnUpdateSetFn == nil { - return t.UnimplementedHostPluginServiceServer.OnUpdateSet(ctx, req) - } - return t.OnUpdateSetFn(ctx, req) -} - -func (t TestPluginServer) OnDeleteSet(ctx context.Context, req *plgpb.OnDeleteSetRequest) (*plgpb.OnDeleteSetResponse, error) { - if t.OnDeleteSetFn == nil { - return t.UnimplementedHostPluginServiceServer.OnDeleteSet(ctx, req) - } - return t.OnDeleteSetFn(ctx, req) -} - -func (t TestPluginServer) ListHosts(ctx context.Context, req *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) { - if t.ListHostsFn == nil { - return t.UnimplementedHostPluginServiceServer.ListHosts(ctx, req) - } - return t.ListHostsFn(ctx, req) -} diff --git a/internal/host/plugin/testing_test.go b/internal/host/plugin/testing_test.go index 1642fa44c..2335a3e41 100644 --- a/internal/host/plugin/testing_test.go +++ b/internal/host/plugin/testing_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/scheduler" plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" "github.com/stretchr/testify/assert" @@ -52,7 +53,7 @@ func Test_TestSet(t *testing.T) { assert.NotEmpty(plg.GetPublicId()) c := TestCatalog(t, conn, prj.GetPublicId(), plg.GetPublicId()) - set := TestSet(t, conn, kmsCache, sched, c, map[string]plgpb.HostPluginServiceClient{plg.GetPublicId(): NewWrappingPluginClient(&TestPluginServer{})}, WithName("foo"), WithDescription("bar")) + set := TestSet(t, conn, kmsCache, sched, c, map[string]plgpb.HostPluginServiceClient{plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{})}, WithName("foo"), WithDescription("bar")) assert.NotEmpty(set.GetPublicId()) db.AssertPublicId(t, globals.PluginHostSetPrefix, set.GetPublicId()) assert.Equal("foo", set.GetName()) @@ -93,7 +94,7 @@ func Test_TestSetMembers(t *testing.T) { assert.NotEmpty(plg.GetPublicId()) c := TestCatalog(t, conn, prj.GetPublicId(), plg.GetPublicId()) - s := TestSet(t, conn, kmsCache, sched, c, map[string]plgpb.HostPluginServiceClient{plg.GetPublicId(): NewWrappingPluginClient(&TestPluginServer{})}) + s := TestSet(t, conn, kmsCache, sched, c, map[string]plgpb.HostPluginServiceClient{plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{})}) h := TestHost(t, conn, c.GetPublicId(), plg.GetPublicId()) members := TestSetMembers(t, conn, s.PublicId, []*Host{h}) @@ -115,8 +116,8 @@ func Test_TestRunSetSync(t *testing.T) { plg := host.TestPlugin(t, conn, "test") require.NotNil(plg) assert.NotEmpty(plg.GetPublicId()) - pluginServer := &TestPluginServer{} - plgm := map[string]plgpb.HostPluginServiceClient{plg.GetPublicId(): NewWrappingPluginClient(pluginServer)} + pluginServer := &loopback.TestPluginServer{} + plgm := map[string]plgpb.HostPluginServiceClient{plg.GetPublicId(): loopback.NewWrappingPluginClient(pluginServer)} c := TestCatalog(t, conn, prj.GetPublicId(), plg.GetPublicId()) s1 := TestSet(t, conn, kmsCache, sched, c, plgm) diff --git a/internal/host/preferred_endpoint_test.go b/internal/host/preferred_endpoint_test.go index cb499d608..f0d912b96 100644 --- a/internal/host/preferred_endpoint_test.go +++ b/internal/host/preferred_endpoint_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" hostplg "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/plugin/loopback" "github.com/hashicorp/boundary/internal/scheduler" plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" "github.com/stretchr/testify/assert" @@ -33,7 +34,7 @@ func TestPreferredEndpoint_Create(t *testing.T) { plg := hostplg.TestPlugin(t, conn, "create") catalog := plugin.TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) set := plugin.TestSet(t, conn, kmsCache, sched, catalog, map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&loopback.TestPluginServer{}), }) type args struct { @@ -164,7 +165,7 @@ func TestPreferredEndpoint_Delete(t *testing.T) { plg := hostplg.TestPlugin(t, conn, "create") catalog := plugin.TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) set := plugin.TestSet(t, conn, kmsCache, sched, catalog, map[string]plgpb.HostPluginServiceClient{ - plg.GetPublicId(): plugin.NewWrappingPluginClient(&plgpb.UnimplementedHostPluginServiceServer{}), + plg.GetPublicId(): loopback.NewWrappingPluginClient(&plgpb.UnimplementedHostPluginServiceServer{}), }) peFunc := func(priority uint32, condition string) *host.PreferredEndpoint { diff --git a/internal/host/plugin/client.go b/internal/plugin/loopback/client.go similarity index 50% rename from internal/host/plugin/client.go rename to internal/plugin/loopback/client.go index 707032fac..4079c7eb0 100644 --- a/internal/host/plugin/client.go +++ b/internal/plugin/loopback/client.go @@ -1,10 +1,11 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package plugin +package loopback import ( "context" + "errors" plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" "google.golang.org/grpc" @@ -13,47 +14,83 @@ import ( var _ plgpb.HostPluginServiceClient = (*WrappingPluginClient)(nil) // WrappingPluginClient provides a wrapper around a Server implementation that -// can be used when loading a plugin in-memory +// can be used when loading a plugin in-memory. Supports HostServiceServer type WrappingPluginClient struct { - Server plgpb.HostPluginServiceServer + Server any } -func NewWrappingPluginClient(s plgpb.HostPluginServiceServer) *WrappingPluginClient { +func NewWrappingPluginClient(s any) *WrappingPluginClient { return &WrappingPluginClient{Server: s} } func (tpc *WrappingPluginClient) NormalizeCatalogData(ctx context.Context, req *plgpb.NormalizeCatalogDataRequest, opts ...grpc.CallOption) (*plgpb.NormalizeCatalogDataResponse, error) { - return tpc.Server.NormalizeCatalogData(ctx, req) + svc, ok := tpc.Server.(plgpb.HostPluginServiceServer) + if !ok { + return nil, errors.New("wrapping client not initialized with a HostServiceServer") + } + return svc.NormalizeCatalogData(ctx, req) } func (tpc *WrappingPluginClient) OnCreateCatalog(ctx context.Context, req *plgpb.OnCreateCatalogRequest, opts ...grpc.CallOption) (*plgpb.OnCreateCatalogResponse, error) { - return tpc.Server.OnCreateCatalog(ctx, req) + svc, ok := tpc.Server.(plgpb.HostPluginServiceServer) + if !ok { + return nil, errors.New("wrapping client not initialized with a HostServiceServer") + } + return svc.OnCreateCatalog(ctx, req) } func (tpc *WrappingPluginClient) OnUpdateCatalog(ctx context.Context, req *plgpb.OnUpdateCatalogRequest, opts ...grpc.CallOption) (*plgpb.OnUpdateCatalogResponse, error) { - return tpc.Server.OnUpdateCatalog(ctx, req) + svc, ok := tpc.Server.(plgpb.HostPluginServiceServer) + if !ok { + return nil, errors.New("wrapping client not initialized with a HostServiceServer") + } + return svc.OnUpdateCatalog(ctx, req) } func (tpc *WrappingPluginClient) OnDeleteCatalog(ctx context.Context, req *plgpb.OnDeleteCatalogRequest, opts ...grpc.CallOption) (*plgpb.OnDeleteCatalogResponse, error) { - return tpc.Server.OnDeleteCatalog(ctx, req) + svc, ok := tpc.Server.(plgpb.HostPluginServiceServer) + if !ok { + return nil, errors.New("wrapping client not initialized with a HostServiceServer") + } + return svc.OnDeleteCatalog(ctx, req) } func (tpc *WrappingPluginClient) NormalizeSetData(ctx context.Context, req *plgpb.NormalizeSetDataRequest, opts ...grpc.CallOption) (*plgpb.NormalizeSetDataResponse, error) { - return tpc.Server.NormalizeSetData(ctx, req) + svc, ok := tpc.Server.(plgpb.HostPluginServiceServer) + if !ok { + return nil, errors.New("wrapping client not initialized with a HostServiceServer") + } + return svc.NormalizeSetData(ctx, req) } func (tpc *WrappingPluginClient) OnCreateSet(ctx context.Context, req *plgpb.OnCreateSetRequest, opts ...grpc.CallOption) (*plgpb.OnCreateSetResponse, error) { - return tpc.Server.OnCreateSet(ctx, req) + svc, ok := tpc.Server.(plgpb.HostPluginServiceServer) + if !ok { + return nil, errors.New("wrapping client not initialized with a HostServiceServer") + } + return svc.OnCreateSet(ctx, req) } func (tpc *WrappingPluginClient) OnUpdateSet(ctx context.Context, req *plgpb.OnUpdateSetRequest, opts ...grpc.CallOption) (*plgpb.OnUpdateSetResponse, error) { - return tpc.Server.OnUpdateSet(ctx, req) + svc, ok := tpc.Server.(plgpb.HostPluginServiceServer) + if !ok { + return nil, errors.New("wrapping client not initialized with a HostServiceServer") + } + return svc.OnUpdateSet(ctx, req) } func (tpc *WrappingPluginClient) OnDeleteSet(ctx context.Context, req *plgpb.OnDeleteSetRequest, opts ...grpc.CallOption) (*plgpb.OnDeleteSetResponse, error) { - return tpc.Server.OnDeleteSet(ctx, req) + svc, ok := tpc.Server.(plgpb.HostPluginServiceServer) + if !ok { + return nil, errors.New("wrapping client not initialized with a HostServiceServer") + } + return svc.OnDeleteSet(ctx, req) } func (tpc *WrappingPluginClient) ListHosts(ctx context.Context, req *plgpb.ListHostsRequest, opts ...grpc.CallOption) (*plgpb.ListHostsResponse, error) { - return tpc.Server.ListHosts(ctx, req) + svc, ok := tpc.Server.(plgpb.HostPluginServiceServer) + if !ok { + return nil, errors.New("wrapping client not initialized with a HostServiceServer") + } + return svc.ListHosts(ctx, req) } diff --git a/internal/plugin/loopback/host.go b/internal/plugin/loopback/host.go new file mode 100644 index 000000000..bf7edacc0 --- /dev/null +++ b/internal/plugin/loopback/host.go @@ -0,0 +1,253 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package loopback + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/errors" + plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/mitchellh/mapstructure" +) + +var _ plgpb.HostPluginServiceServer = (*TestPluginHostServer)(nil) + +// TestPluginHostServer provides a host plugin service server where each method can be overwritten for tests. +type TestPluginHostServer struct { + NormalizeCatalogDataFn func(context.Context, *plgpb.NormalizeCatalogDataRequest) (*plgpb.NormalizeCatalogDataResponse, error) + OnCreateCatalogFn func(context.Context, *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) + OnUpdateCatalogFn func(context.Context, *plgpb.OnUpdateCatalogRequest) (*plgpb.OnUpdateCatalogResponse, error) + OnDeleteCatalogFn func(context.Context, *plgpb.OnDeleteCatalogRequest) (*plgpb.OnDeleteCatalogResponse, error) + NormalizeSetDataFn func(context.Context, *plgpb.NormalizeSetDataRequest) (*plgpb.NormalizeSetDataResponse, error) + OnCreateSetFn func(context.Context, *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) + OnUpdateSetFn func(context.Context, *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error) + OnDeleteSetFn func(context.Context, *plgpb.OnDeleteSetRequest) (*plgpb.OnDeleteSetResponse, error) + ListHostsFn func(context.Context, *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) + plgpb.UnimplementedHostPluginServiceServer +} + +func (t TestPluginHostServer) NormalizeCatalogData(ctx context.Context, req *plgpb.NormalizeCatalogDataRequest) (*plgpb.NormalizeCatalogDataResponse, error) { + if t.NormalizeCatalogDataFn == nil { + return t.UnimplementedHostPluginServiceServer.NormalizeCatalogData(ctx, req) + } + return t.NormalizeCatalogDataFn(ctx, req) +} + +func (t TestPluginHostServer) OnCreateCatalog(ctx context.Context, req *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) { + if t.OnCreateCatalogFn == nil { + return t.UnimplementedHostPluginServiceServer.OnCreateCatalog(ctx, req) + } + return t.OnCreateCatalogFn(ctx, req) +} + +func (t TestPluginHostServer) OnUpdateCatalog(ctx context.Context, req *plgpb.OnUpdateCatalogRequest) (*plgpb.OnUpdateCatalogResponse, error) { + if t.OnUpdateCatalogFn == nil { + return t.UnimplementedHostPluginServiceServer.OnUpdateCatalog(ctx, req) + } + return t.OnUpdateCatalogFn(ctx, req) +} + +func (t TestPluginHostServer) OnDeleteCatalog(ctx context.Context, req *plgpb.OnDeleteCatalogRequest) (*plgpb.OnDeleteCatalogResponse, error) { + if t.OnDeleteCatalogFn == nil { + return t.UnimplementedHostPluginServiceServer.OnDeleteCatalog(ctx, req) + } + return t.OnDeleteCatalogFn(ctx, req) +} + +func (t TestPluginHostServer) NormalizeSetData(ctx context.Context, req *plgpb.NormalizeSetDataRequest) (*plgpb.NormalizeSetDataResponse, error) { + if t.NormalizeSetDataFn == nil { + return t.UnimplementedHostPluginServiceServer.NormalizeSetData(ctx, req) + } + return t.NormalizeSetDataFn(ctx, req) +} + +func (t TestPluginHostServer) OnCreateSet(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { + if t.OnCreateSetFn == nil { + return t.UnimplementedHostPluginServiceServer.OnCreateSet(ctx, req) + } + return t.OnCreateSetFn(ctx, req) +} + +func (t TestPluginHostServer) OnUpdateSet(ctx context.Context, req *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error) { + if t.OnUpdateSetFn == nil { + return t.UnimplementedHostPluginServiceServer.OnUpdateSet(ctx, req) + } + return t.OnUpdateSetFn(ctx, req) +} + +func (t TestPluginHostServer) OnDeleteSet(ctx context.Context, req *plgpb.OnDeleteSetRequest) (*plgpb.OnDeleteSetResponse, error) { + if t.OnDeleteSetFn == nil { + return t.UnimplementedHostPluginServiceServer.OnDeleteSet(ctx, req) + } + return t.OnDeleteSetFn(ctx, req) +} + +func (t TestPluginHostServer) ListHosts(ctx context.Context, req *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) { + if t.ListHostsFn == nil { + return t.UnimplementedHostPluginServiceServer.ListHosts(ctx, req) + } + return t.ListHostsFn(ctx, req) +} + +type loopbackPluginHostInfo struct { + ExternalId string `mapstructure:"external_id"` + IpAddresses []string `mapstructure:"ip_addresses"` + DnsNames []string `mapstructure:"dns_names"` +} + +// LoopbackHost provides a host plugin with functionality useful for certain +// kinds of testing. +// +// It is not (currently) thread-safe. +// +// Over time, if useful, it can be enhanced to do things like handle multiple +// hosts per set. +type LoopbackHost struct { + hostMap map[string][]*loopbackPluginHostInfo +} + +func (l *LoopbackHost) onCreateCatalog(ctx context.Context, req *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) { + const op = "loopback.(loopbackPlugin).onCreateCatalog" + if req == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "req is nil") + } + if cat := req.GetCatalog(); cat != nil { + if secrets := cat.GetSecrets(); secrets != nil { + return &plgpb.OnCreateCatalogResponse{ + Persisted: &plgpb.HostCatalogPersisted{ + Secrets: secrets, + }, + }, nil + } + } + return &plgpb.OnCreateCatalogResponse{}, nil +} + +func (l *LoopbackHost) onUpdateCatalog(ctx context.Context, req *plgpb.OnUpdateCatalogRequest) (*plgpb.OnUpdateCatalogResponse, error) { + const op = "loopback.(loopbackPlugin).onUpdateCatalog" + if req == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "req is nil") + } + if cat := req.GetNewCatalog(); cat != nil { + if secrets := cat.GetSecrets(); secrets != nil { + return &plgpb.OnUpdateCatalogResponse{ + Persisted: &plgpb.HostCatalogPersisted{ + Secrets: secrets, + }, + }, nil + } + } + return &plgpb.OnUpdateCatalogResponse{}, nil +} + +func (l *LoopbackHost) onCreateSet(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { + const op = "loopback.(loopbackPlugin).onCreateSet" + if req == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "req is nil") + } + set := req.GetSet() + if set == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "set is nil") + } + if attrs := set.GetAttributes(); attrs != nil { + attrsMap := attrs.AsMap() + if field := attrsMap[loopbackPluginHostInfoAttrField]; field != nil { + switch t := field.(type) { + case []any: + for _, h := range t { + hostInfo := new(loopbackPluginHostInfo) + if err := mapstructure.Decode(h, hostInfo); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + l.hostMap[set.GetId()] = append(l.hostMap[set.GetId()], hostInfo) + } + case map[string]any: + hostInfo := new(loopbackPluginHostInfo) + if err := mapstructure.Decode(t, hostInfo); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + l.hostMap[set.GetId()] = append(l.hostMap[set.GetId()], hostInfo) + default: + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown host info type %T", t)) + } + } + } + return &plgpb.OnCreateSetResponse{}, nil +} + +func (l *LoopbackHost) onUpdateSet(ctx context.Context, req *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error) { + const op = "loopback.(loopbackPlugin).onCreateSet" + if req == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "req is nil") + } + set := req.GetNewSet() + if set == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "set is nil") + } + var hosts []*loopbackPluginHostInfo + if attrs := set.GetAttributes(); attrs != nil { + attrsMap := attrs.AsMap() + if field := attrsMap[loopbackPluginHostInfoAttrField]; field != nil { + switch t := field.(type) { + case []any: + for _, h := range t { + hostInfo := new(loopbackPluginHostInfo) + if err := mapstructure.Decode(h, hostInfo); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + hosts = append(hosts, hostInfo) + } + case map[string]any: + hostInfo := new(loopbackPluginHostInfo) + if err := mapstructure.Decode(t, hostInfo); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + hosts = append(hosts, hostInfo) + default: + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown host info type %T", t)) + } + } + } + if hosts != nil { + l.hostMap[set.GetId()] = hosts + } + return &plgpb.OnUpdateSetResponse{}, nil +} + +func (l *LoopbackHost) onDeleteSet(ctx context.Context, req *plgpb.OnDeleteSetRequest) (*plgpb.OnDeleteSetResponse, error) { + const op = "loopback.(loopbackPlugin).onDeleteSet" + if req == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "req is nil") + } + set := req.GetSet() + if set == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "set is nil") + } + delete(l.hostMap, set.GetId()) + return nil, nil +} + +func (l *LoopbackHost) listHosts(ctx context.Context, req *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) { + const op = "loopback.(loopbackPlugin).listHosts" + if req == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "req is nil") + } + resp := new(plgpb.ListHostsResponse) + for _, set := range req.GetSets() { + hostInfos := l.hostMap[set.GetId()] + if len(hostInfos) == 0 { + continue + } + for _, host := range hostInfos { + resp.Hosts = append(resp.Hosts, &plgpb.ListHostsResponseHost{ + SetIds: []string{set.GetId()}, + ExternalId: host.ExternalId, + IpAddresses: host.IpAddresses, + DnsNames: host.DnsNames, + }) + } + } + return resp, nil +} diff --git a/internal/plugin/loopback/host_test.go b/internal/plugin/loopback/host_test.go new file mode 100644 index 000000000..bde3c1a05 --- /dev/null +++ b/internal/plugin/loopback/host_test.go @@ -0,0 +1,375 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package loopback + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets" + plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" + ta "github.com/stretchr/testify/assert" + tr "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" +) + +// TestLoopbackHostPlugin is a quick test of basic host service functionality. +func TestLoopbackHostPlugin(t *testing.T) { + require, assert := tr.New(t), ta.New(t) + ctx := context.Background() + + plg, err := NewLoopbackPlugin() + require.NoError(err) + secretsMap := map[string]any{ + "key1": "key2", + "baz": true, + } + secrets, err := structpb.NewStruct(secretsMap) + require.NoError(err) + + // First, test that if we give it secrets, those secrets come back as + // persisted data + catResp, err := plg.OnCreateCatalog(ctx, &plgpb.OnCreateCatalogRequest{ + Catalog: &hostcatalogs.HostCatalog{ + Secrets: secrets, + }, + }) + require.NoError(err) + require.NotNil(catResp) + require.NotNil(catResp.GetPersisted()) + require.NotNil(catResp.GetPersisted().GetSecrets()) + assert.EqualValues(secretsMap, catResp.GetPersisted().GetSecrets().AsMap()) + + newSecretsMap := map[string]any{ + "key1": "key2", + "baz": true, + } + newSecrets, err := structpb.NewStruct(newSecretsMap) + require.NoError(err) + + // First, test that if we give it secrets, those secrets come back as + // persisted data + upResp, err := plg.OnUpdateCatalog(ctx, &plgpb.OnUpdateCatalogRequest{ + CurrentCatalog: &hostcatalogs.HostCatalog{}, + NewCatalog: &hostcatalogs.HostCatalog{ + Secrets: newSecrets, + }, + Persisted: &plgpb.HostCatalogPersisted{ + Secrets: secrets, + }, + }) + require.NoError(err) + require.NotNil(upResp) + require.NotNil(upResp.GetPersisted()) + require.NotNil(upResp.GetPersisted().GetSecrets()) + assert.EqualValues(newSecretsMap, upResp.GetPersisted().GetSecrets().AsMap()) + + // Add data to some sets + hostInfo1 := map[string]any{ + loopbackPluginHostInfoAttrField: map[string]any{ + "set_ids": []any{"set1"}, + "external_id": "host1", + "ip_addresses": []any{"1.2.3.4", "2.3.4.5"}, + "dns_names": []any{"foo.com"}, + }, + } + attrs, err := structpb.NewStruct(hostInfo1) + require.NoError(err) + _, err = plg.OnCreateSet(ctx, &plgpb.OnCreateSetRequest{ + Set: &hostsets.HostSet{ + Id: "set1", + Attrs: &hostsets.HostSet_Attributes{ + Attributes: attrs, + }, + }, + }) + require.NoError(err) + hostInfo2 := map[string]any{ + loopbackPluginHostInfoAttrField: map[string]any{ + "set_ids": []any{"set2"}, + "external_id": "host2", + "ip_addresses": []any{"5.6.7.8", "6.7.8.9"}, + "dns_names": []any{"bar.com"}, + }, + } + attrs, err = structpb.NewStruct(hostInfo2) + require.NoError(err) + _, err = plg.OnCreateSet(ctx, &plgpb.OnCreateSetRequest{ + Set: &hostsets.HostSet{ + Id: "set2", + Attrs: &hostsets.HostSet_Attributes{ + Attributes: attrs, + }, + }, + }) + require.NoError(err) + + // Define test struct and validation function + type testInfo struct { + name string + sets []string + found []map[string]any + } + validateSets := func(t *testing.T, tt testInfo) { + require, assert := tr.New(t), ta.New(t) + var hostSets []*hostsets.HostSet + for _, set := range tt.sets { + hostSets = append(hostSets, &hostsets.HostSet{Id: set}) + } + resp, err := plg.ListHosts(ctx, &plgpb.ListHostsRequest{ + Sets: hostSets, + }) + require.NoError(err) + if len(tt.found) == 0 { + assert.Len(resp.GetHosts(), 0) + return + } + + require.Greater(len(resp.GetHosts()), 0) + + var found []map[string]any + for _, host := range resp.GetHosts() { + hostMap := map[string]any{ + "external_id": host.GetExternalId(), + } + var sets []any + for _, set := range host.SetIds { + sets = append(sets, set) + } + var ips []any + for _, ip := range host.GetIpAddresses() { + ips = append(ips, ip) + } + var names []any + for _, name := range host.GetDnsNames() { + names = append(names, name) + } + if len(sets) > 0 { + hostMap["set_ids"] = sets + } + if len(ips) > 0 { + hostMap["ip_addresses"] = ips + } + if len(names) > 0 { + hostMap["dns_names"] = names + } + found = append(found, hostMap) + } + assert.ElementsMatch(tt.found, found) + } + + // First set of tests: check that we can look up sets individually and + // together + setTests := []testInfo{ + { + name: "set 1", + sets: []string{"set1"}, + found: []map[string]any{ + hostInfo1[loopbackPluginHostInfoAttrField].(map[string]any), + }, + }, + { + name: "set 2", + sets: []string{"set2"}, + found: []map[string]any{ + hostInfo2[loopbackPluginHostInfoAttrField].(map[string]any), + }, + }, + { + name: "sets 1 and 2", + sets: []string{"set1", "set2"}, + found: []map[string]any{ + hostInfo1[loopbackPluginHostInfoAttrField].(map[string]any), + hostInfo2[loopbackPluginHostInfoAttrField].(map[string]any), + }, + }, + } + for _, tt := range setTests { + t.Run(tt.name, func(t *testing.T) { + validateSets(t, tt) + }) + } + + // Remove a set + _, err = plg.OnDeleteSet(ctx, &plgpb.OnDeleteSetRequest{ + Set: &hostsets.HostSet{ + Id: "set1", + }, + }) + require.NoError(err) + + // Run tests again, making sure we no longer find that host either + // individually or together + setTests = []testInfo{ + { + name: "set 1 deleted", + sets: []string{"set1"}, + found: []map[string]any{}, + }, + { + name: "set 2 not deleted", + sets: []string{"set2"}, + found: []map[string]any{ + hostInfo2[loopbackPluginHostInfoAttrField].(map[string]any), + }, + }, + { + name: "sets 1 and 2 set 1 deleted", + sets: []string{"set1", "set2"}, + found: []map[string]any{ + hostInfo2[loopbackPluginHostInfoAttrField].(map[string]any), + }, + }, + } + for _, tt := range setTests { + t.Run(tt.name, func(t *testing.T) { + validateSets(t, tt) + }) + } +} + +func TestLoopbackHostPluginArrays(t *testing.T) { + require := tr.New(t) + ctx := context.Background() + + plg, err := NewLoopbackPlugin() + require.NoError(err) + + // Add data to some sets + hostInfo1 := map[string]any{ + loopbackPluginHostInfoAttrField: []any{ + map[string]any{ + "set_ids": []any{"set1"}, + "external_id": "host1a", + "ip_addresses": []any{"1.2.3.4", "2.3.4.5"}, + "dns_names": []any{"foo.com"}, + }, + map[string]any{ + "set_ids": []any{"set1"}, + "external_id": "host1b", + "ip_addresses": []any{"3.4.5.6", "4.5.6.7"}, + "dns_names": []any{"bar.com"}, + }, + }, + } + attrs, err := structpb.NewStruct(hostInfo1) + require.NoError(err) + _, err = plg.OnCreateSet(ctx, &plgpb.OnCreateSetRequest{ + Set: &hostsets.HostSet{ + Id: "set1", + Attrs: &hostsets.HostSet_Attributes{ + Attributes: attrs, + }, + }, + }) + require.NoError(err) + hostInfo2 := map[string]any{ + loopbackPluginHostInfoAttrField: []any{ + map[string]any{ + "set_ids": []any{"set2"}, + "external_id": "host2a", + "ip_addresses": []any{"10.20.30.40", "20.30.40.50"}, + "dns_names": []any{"foz.com"}, + }, + map[string]any{ + "set_ids": []any{"set2"}, + "external_id": "host2b", + "ip_addresses": []any{"30.40.50.60", "40.50.60.70"}, + "dns_names": []any{"baz.com"}, + }, + }, + } + attrs, err = structpb.NewStruct(hostInfo2) + require.NoError(err) + _, err = plg.OnCreateSet(ctx, &plgpb.OnCreateSetRequest{ + Set: &hostsets.HostSet{ + Id: "set2", + Attrs: &hostsets.HostSet_Attributes{ + Attributes: attrs, + }, + }, + }) + require.NoError(err) + + // Define test struct and validation function + type testInfo struct { + name string + sets []string + found []any + } + validateSets := func(t *testing.T, tt testInfo) { + require, assert := tr.New(t), ta.New(t) + var hostSets []*hostsets.HostSet + for _, set := range tt.sets { + hostSets = append(hostSets, &hostsets.HostSet{Id: set}) + } + resp, err := plg.ListHosts(ctx, &plgpb.ListHostsRequest{ + Sets: hostSets, + }) + require.NoError(err) + if len(tt.found) == 0 { + assert.Len(resp.GetHosts(), 0) + return + } + + require.Greater(len(resp.GetHosts()), 0) + + var found []any + for _, host := range resp.GetHosts() { + hostMap := map[string]any{ + "external_id": host.GetExternalId(), + } + var sets []any + for _, set := range host.SetIds { + sets = append(sets, set) + } + var ips []any + for _, ip := range host.GetIpAddresses() { + ips = append(ips, ip) + } + var names []any + for _, name := range host.GetDnsNames() { + names = append(names, name) + } + if len(sets) > 0 { + hostMap["set_ids"] = sets + } + if len(ips) > 0 { + hostMap["ip_addresses"] = ips + } + if len(names) > 0 { + hostMap["dns_names"] = names + } + found = append(found, hostMap) + } + assert.ElementsMatch(tt.found, found) + } + + // First set of tests: check that we can look up sets individually and + // together + setTests := []testInfo{ + { + name: "set 1", + sets: []string{"set1"}, + found: hostInfo1[loopbackPluginHostInfoAttrField].([]any), + }, + { + name: "set 2", + sets: []string{"set2"}, + found: hostInfo2[loopbackPluginHostInfoAttrField].([]any), + }, + { + name: "sets 1 and 2", + sets: []string{"set1", "set2"}, + found: append(hostInfo1[loopbackPluginHostInfoAttrField].([]any), + hostInfo2[loopbackPluginHostInfoAttrField].([]any)...), + }, + } + for _, tt := range setTests { + t.Run(tt.name, func(t *testing.T) { + validateSets(t, tt) + }) + } +} diff --git a/internal/host/plugin/loopback.go b/internal/plugin/loopback/loopback.go similarity index 52% rename from internal/host/plugin/loopback.go rename to internal/plugin/loopback/loopback.go index 074c49ad2..2e6f04c82 100644 --- a/internal/host/plugin/loopback.go +++ b/internal/plugin/loopback/loopback.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package plugin +package loopback import ( "context" @@ -14,14 +14,94 @@ import ( const loopbackPluginHostInfoAttrField = "host_info" -var _ plgpb.HostPluginServiceServer = (*loopbackPlugin)(nil) +var ( + _ plgpb.HostPluginServiceServer = (*LoopbackPlugin)(nil) + _ plgpb.HostPluginServiceServer = (*TestPluginServer)(nil) +) -// loopbackPlugin provides a host plugin with functionality useful for certain +// TestPluginServer provides a host plugin service server where each method can be overwritten for tests. +type TestPluginServer struct { + NormalizeCatalogDataFn func(context.Context, *plgpb.NormalizeCatalogDataRequest) (*plgpb.NormalizeCatalogDataResponse, error) + OnCreateCatalogFn func(context.Context, *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) + OnUpdateCatalogFn func(context.Context, *plgpb.OnUpdateCatalogRequest) (*plgpb.OnUpdateCatalogResponse, error) + OnDeleteCatalogFn func(context.Context, *plgpb.OnDeleteCatalogRequest) (*plgpb.OnDeleteCatalogResponse, error) + NormalizeSetDataFn func(context.Context, *plgpb.NormalizeSetDataRequest) (*plgpb.NormalizeSetDataResponse, error) + OnCreateSetFn func(context.Context, *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) + OnUpdateSetFn func(context.Context, *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error) + OnDeleteSetFn func(context.Context, *plgpb.OnDeleteSetRequest) (*plgpb.OnDeleteSetResponse, error) + ListHostsFn func(context.Context, *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) + plgpb.UnimplementedHostPluginServiceServer +} + +func (t TestPluginServer) NormalizeCatalogData(ctx context.Context, req *plgpb.NormalizeCatalogDataRequest) (*plgpb.NormalizeCatalogDataResponse, error) { + if t.NormalizeCatalogDataFn == nil { + return t.UnimplementedHostPluginServiceServer.NormalizeCatalogData(ctx, req) + } + return t.NormalizeCatalogDataFn(ctx, req) +} + +func (t TestPluginServer) OnCreateCatalog(ctx context.Context, req *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) { + if t.OnCreateCatalogFn == nil { + return t.UnimplementedHostPluginServiceServer.OnCreateCatalog(ctx, req) + } + return t.OnCreateCatalogFn(ctx, req) +} + +func (t TestPluginServer) OnUpdateCatalog(ctx context.Context, req *plgpb.OnUpdateCatalogRequest) (*plgpb.OnUpdateCatalogResponse, error) { + if t.OnUpdateCatalogFn == nil { + return t.UnimplementedHostPluginServiceServer.OnUpdateCatalog(ctx, req) + } + return t.OnUpdateCatalogFn(ctx, req) +} + +func (t TestPluginServer) OnDeleteCatalog(ctx context.Context, req *plgpb.OnDeleteCatalogRequest) (*plgpb.OnDeleteCatalogResponse, error) { + if t.OnDeleteCatalogFn == nil { + return t.UnimplementedHostPluginServiceServer.OnDeleteCatalog(ctx, req) + } + return t.OnDeleteCatalogFn(ctx, req) +} + +func (t TestPluginServer) NormalizeSetData(ctx context.Context, req *plgpb.NormalizeSetDataRequest) (*plgpb.NormalizeSetDataResponse, error) { + if t.NormalizeSetDataFn == nil { + return t.UnimplementedHostPluginServiceServer.NormalizeSetData(ctx, req) + } + return t.NormalizeSetDataFn(ctx, req) +} + +func (t TestPluginServer) OnCreateSet(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { + if t.OnCreateSetFn == nil { + return t.UnimplementedHostPluginServiceServer.OnCreateSet(ctx, req) + } + return t.OnCreateSetFn(ctx, req) +} + +func (t TestPluginServer) OnUpdateSet(ctx context.Context, req *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error) { + if t.OnUpdateSetFn == nil { + return t.UnimplementedHostPluginServiceServer.OnUpdateSet(ctx, req) + } + return t.OnUpdateSetFn(ctx, req) +} + +func (t TestPluginServer) OnDeleteSet(ctx context.Context, req *plgpb.OnDeleteSetRequest) (*plgpb.OnDeleteSetResponse, error) { + if t.OnDeleteSetFn == nil { + return t.UnimplementedHostPluginServiceServer.OnDeleteSet(ctx, req) + } + return t.OnDeleteSetFn(ctx, req) +} + +func (t TestPluginServer) ListHosts(ctx context.Context, req *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) { + if t.ListHostsFn == nil { + return t.UnimplementedHostPluginServiceServer.ListHosts(ctx, req) + } + return t.ListHostsFn(ctx, req) +} + +// LoopbackPlugin provides a host plugin with functionality useful for certain // kinds of testing. It is not (currently) thread-safe. // // Over time, if useful, it can be enhanced to do things like handle multiple // hosts per set. -type loopbackPlugin struct { +type LoopbackPlugin struct { *TestPluginServer hostMap map[string][]*loopbackPluginHostInfo @@ -34,8 +114,8 @@ type loopbackPluginHostInfo struct { } // NewLoopbackPlugin returns a new loopback plugin -func NewLoopbackPlugin() plgpb.HostPluginServiceServer { - ret := &loopbackPlugin{ +func NewLoopbackPlugin() (*LoopbackPlugin, error) { + ret := &LoopbackPlugin{ TestPluginServer: new(TestPluginServer), hostMap: make(map[string][]*loopbackPluginHostInfo), } @@ -45,11 +125,12 @@ func NewLoopbackPlugin() plgpb.HostPluginServiceServer { ret.OnUpdateSetFn = ret.onUpdateSet ret.OnDeleteSetFn = ret.onDeleteSet ret.ListHostsFn = ret.listHosts - return ret + + return ret, nil } -func (l *loopbackPlugin) onCreateCatalog(ctx context.Context, req *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) { - const op = "plugin.(loopbackPlugin).onCreateCatalog" +func (l *LoopbackPlugin) onCreateCatalog(ctx context.Context, req *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) { + const op = "loopback.(loopbackPlugin).onCreateCatalog" if req == nil { return nil, errors.New(ctx, errors.InvalidParameter, op, "req is nil") } @@ -65,8 +146,8 @@ func (l *loopbackPlugin) onCreateCatalog(ctx context.Context, req *plgpb.OnCreat return &plgpb.OnCreateCatalogResponse{}, nil } -func (l *loopbackPlugin) onUpdateCatalog(ctx context.Context, req *plgpb.OnUpdateCatalogRequest) (*plgpb.OnUpdateCatalogResponse, error) { - const op = "plugin.(loopbackPlugin).onUpdateCatalog" +func (l *LoopbackPlugin) onUpdateCatalog(ctx context.Context, req *plgpb.OnUpdateCatalogRequest) (*plgpb.OnUpdateCatalogResponse, error) { + const op = "loopback.(loopbackPlugin).onUpdateCatalog" if req == nil { return nil, errors.New(ctx, errors.InvalidParameter, op, "req is nil") } @@ -82,8 +163,8 @@ func (l *loopbackPlugin) onUpdateCatalog(ctx context.Context, req *plgpb.OnUpdat return &plgpb.OnUpdateCatalogResponse{}, nil } -func (l *loopbackPlugin) onCreateSet(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { - const op = "plugin.(loopbackPlugin).onCreateSet" +func (l *LoopbackPlugin) onCreateSet(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) { + const op = "loopback.(loopbackPlugin).onCreateSet" if req == nil { return nil, errors.New(ctx, errors.InvalidParameter, op, "req is nil") } @@ -117,8 +198,8 @@ func (l *loopbackPlugin) onCreateSet(ctx context.Context, req *plgpb.OnCreateSet return &plgpb.OnCreateSetResponse{}, nil } -func (l *loopbackPlugin) onUpdateSet(ctx context.Context, req *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error) { - const op = "plugin.(loopbackPlugin).onCreateSet" +func (l *LoopbackPlugin) onUpdateSet(ctx context.Context, req *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error) { + const op = "loopback.(loopbackPlugin).onCreateSet" if req == nil { return nil, errors.New(ctx, errors.InvalidParameter, op, "req is nil") } @@ -156,8 +237,8 @@ func (l *loopbackPlugin) onUpdateSet(ctx context.Context, req *plgpb.OnUpdateSet return &plgpb.OnUpdateSetResponse{}, nil } -func (l *loopbackPlugin) onDeleteSet(ctx context.Context, req *plgpb.OnDeleteSetRequest) (*plgpb.OnDeleteSetResponse, error) { - const op = "plugin.(loopbackPlugin).onDeleteSet" +func (l *LoopbackPlugin) onDeleteSet(ctx context.Context, req *plgpb.OnDeleteSetRequest) (*plgpb.OnDeleteSetResponse, error) { + const op = "loopback.(loopbackPlugin).onDeleteSet" if req == nil { return nil, errors.New(ctx, errors.InvalidParameter, op, "req is nil") } @@ -169,8 +250,8 @@ func (l *loopbackPlugin) onDeleteSet(ctx context.Context, req *plgpb.OnDeleteSet return nil, nil } -func (l *loopbackPlugin) listHosts(ctx context.Context, req *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) { - const op = "plugin.(loopbackPlugin).listHosts" +func (l *LoopbackPlugin) listHosts(ctx context.Context, req *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) { + const op = "loopback.(loopbackPlugin).listHosts" if req == nil { return nil, errors.New(ctx, errors.InvalidParameter, op, "req is nil") } diff --git a/internal/host/plugin/loopback_test.go b/internal/plugin/loopback/loopback_test.go similarity index 98% rename from internal/host/plugin/loopback_test.go rename to internal/plugin/loopback/loopback_test.go index 215c5559a..8952e5fde 100644 --- a/internal/host/plugin/loopback_test.go +++ b/internal/plugin/loopback/loopback_test.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package plugin +package loopback import ( "context" @@ -20,7 +20,8 @@ func TestLoopbackPlugin(t *testing.T) { require, assert := tr.New(t), ta.New(t) ctx := context.Background() - plg := NewLoopbackPlugin() + plg, err := NewLoopbackPlugin() + require.NoError(err) secretsMap := map[string]any{ "key1": "key2", "baz": true, @@ -233,7 +234,8 @@ func TestLoopbackPluginArrays(t *testing.T) { require := tr.New(t) ctx := context.Background() - plg := NewLoopbackPlugin() + plg, err := NewLoopbackPlugin() + require.NoError(err) // Add data to some sets hostInfo1 := map[string]any{ diff --git a/internal/plugin/loopback/options.go b/internal/plugin/loopback/options.go new file mode 100644 index 000000000..c189a9ff7 --- /dev/null +++ b/internal/plugin/loopback/options.go @@ -0,0 +1,69 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package loopback + +import ( + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets" + "google.golang.org/grpc/codes" +) + +// PluginMockError is used to mock an error when interacting with an external object store. +type PluginMockError struct { + bucketName string + bucketPrefix string + objectKey string + errMsg string + errCode codes.Code +} + +func (e PluginMockError) match(bucket *storagebuckets.StorageBucket, key string) bool { + if key != "" && e.objectKey != key { + return false + } + if e.bucketName != bucket.BucketName { + return false + } + if bucket.BucketPrefix != "" && e.bucketPrefix != bucket.BucketPrefix { + return false + } + return true +} + +type TestOption func(*TestOptions) error + +type TestOptions struct { + withMockBuckets map[BucketName]Bucket + withMockError []PluginMockError +} + +// getTestOpts - iterate the inbound Options and return a struct +func getTestOpts(opt ...TestOption) (TestOptions, error) { + opts := getDefaultTestOptions() + for _, o := range opt { + if err := o(&opts); err != nil { + return opts, err + } + } + return opts, nil +} + +func getDefaultTestOptions() TestOptions { + return TestOptions{} +} + +// WithMockBuckets provides an option to create mocked external object store buckets. +func WithMockBuckets(buckets map[BucketName]Bucket) TestOption { + return func(o *TestOptions) error { + o.withMockBuckets = buckets + return nil + } +} + +// WithMockError provides an option to mock an error when interacting with an external object store. +func WithMockError(err PluginMockError) TestOption { + return func(o *TestOptions) error { + o.withMockError = append(o.withMockError, err) + return nil + } +} diff --git a/internal/plugin/loopback/storage.go b/internal/plugin/loopback/storage.go new file mode 100644 index 000000000..8ff18ef8e --- /dev/null +++ b/internal/plugin/loopback/storage.go @@ -0,0 +1,395 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package loopback + +import ( + "bytes" + "context" + "crypto/sha256" + "io" + "sync" + "time" + + plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var _ plgpb.StoragePluginServiceServer = (*TestPluginStorageServer)(nil) + +// TestPluginStorageServer provides a storage plugin service server where each method can be overwritten for tests. +type TestPluginStorageServer struct { + OnCreateStorageBucketFn func(context.Context, *plgpb.OnCreateStorageBucketRequest) (*plgpb.OnCreateStorageBucketResponse, error) + OnUpdateStorageBucketFn func(context.Context, *plgpb.OnUpdateStorageBucketRequest) (*plgpb.OnUpdateStorageBucketResponse, error) + OnDeleteStorageBucketFn func(context.Context, *plgpb.OnDeleteStorageBucketRequest) (*plgpb.OnDeleteStorageBucketResponse, error) + ValidatePermissionsFn func(context.Context, *plgpb.ValidatePermissionsRequest) (*plgpb.ValidatePermissionsResponse, error) + HeadObjectFn func(context.Context, *plgpb.HeadObjectRequest) (*plgpb.HeadObjectResponse, error) + GetObjectFn func(*plgpb.GetObjectRequest, plgpb.StoragePluginService_GetObjectServer) error + PutObjectFn func(plgpb.StoragePluginService_PutObjectServer) error + plgpb.UnimplementedStoragePluginServiceServer +} + +func (t TestPluginStorageServer) OnCreateStorageBucket(ctx context.Context, req *plgpb.OnCreateStorageBucketRequest) (*plgpb.OnCreateStorageBucketResponse, error) { + if t.OnCreateStorageBucketFn == nil { + return t.UnimplementedStoragePluginServiceServer.OnCreateStorageBucket(ctx, req) + } + return t.OnCreateStorageBucketFn(ctx, req) +} + +func (t TestPluginStorageServer) OnUpdateStorageBucket(ctx context.Context, req *plgpb.OnUpdateStorageBucketRequest) (*plgpb.OnUpdateStorageBucketResponse, error) { + if t.OnUpdateStorageBucketFn == nil { + return t.UnimplementedStoragePluginServiceServer.OnUpdateStorageBucket(ctx, req) + } + return t.OnUpdateStorageBucketFn(ctx, req) +} + +func (t TestPluginStorageServer) OnDeleteStorageBucket(ctx context.Context, req *plgpb.OnDeleteStorageBucketRequest) (*plgpb.OnDeleteStorageBucketResponse, error) { + if t.OnDeleteStorageBucketFn == nil { + return t.UnimplementedStoragePluginServiceServer.OnDeleteStorageBucket(ctx, req) + } + return t.OnDeleteStorageBucketFn(ctx, req) +} + +func (t TestPluginStorageServer) ValidatePermissions(ctx context.Context, req *plgpb.ValidatePermissionsRequest) (*plgpb.ValidatePermissionsResponse, error) { + if t.ValidatePermissionsFn == nil { + return t.UnimplementedStoragePluginServiceServer.ValidatePermissions(ctx, req) + } + return t.ValidatePermissionsFn(ctx, req) +} + +func (t TestPluginStorageServer) HeadObject(ctx context.Context, req *plgpb.HeadObjectRequest) (*plgpb.HeadObjectResponse, error) { + if t.HeadObjectFn == nil { + return t.UnimplementedStoragePluginServiceServer.HeadObject(ctx, req) + } + return t.HeadObjectFn(ctx, req) +} + +func (t TestPluginStorageServer) GetObject(req *plgpb.GetObjectRequest, stream plgpb.StoragePluginService_GetObjectServer) error { + if t.GetObjectFn == nil { + return t.UnimplementedStoragePluginServiceServer.GetObject(req, stream) + } + return t.GetObjectFn(req, stream) +} + +func (t TestPluginStorageServer) PutObject(stream plgpb.StoragePluginService_PutObjectServer) error { + if t.PutObjectFn == nil { + return t.UnimplementedStoragePluginServiceServer.PutObject(stream) + } + return t.PutObjectFn(stream) +} + +type ( + BucketName string + ObjectName string + Chunk []byte +) + +type Bucket map[ObjectName]*storagePluginStorageInfo + +// storagePluginStorageInfo is an in-memory represenation of an object +// that would be normally stored in an external object store. +type storagePluginStorageInfo struct { + DataChunks []Chunk `mapstructure:"dataChunks"` + + lastModified *time.Time `mapstructure:"lastModified"` + contentLength *int64 `mapstructure:"contentLength"` +} + +// LoopbackStorage provides a storage plugin with functionality useful for certain +// kinds of testing. +// +// It is thread-safe. +type LoopbackStorage struct { + m sync.Mutex + + buckets map[BucketName]Bucket + errs []PluginMockError +} + +func (l *LoopbackStorage) onCreateStorageBucket(ctx context.Context, req *plgpb.OnCreateStorageBucketRequest) (*plgpb.OnCreateStorageBucketResponse, error) { + const op = "loopback.(LoopbackPlugin).onCreateStorageBucket" + if req == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: request is nil", op) + } + if req.Bucket == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op) + } + if req.GetBucket().Secrets == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: missing secrets", op) + } + l.m.Lock() + defer l.m.Unlock() + if _, ok := l.buckets[BucketName(req.GetBucket().GetBucketName())]; !ok { + return nil, status.Errorf(codes.NotFound, "%s: bucket not found", op) + } + for _, err := range l.errs { + if err.match(req.GetBucket(), "") { + return nil, status.Errorf(err.errCode, err.errMsg) + } + } + return &plgpb.OnCreateStorageBucketResponse{ + Persisted: &plgpb.StorageBucketPersisted{ + Data: req.GetBucket().GetSecrets(), + }, + }, nil +} + +func (l *LoopbackStorage) onUpdateStorageBucket(ctx context.Context, req *plgpb.OnUpdateStorageBucketRequest) (*plgpb.OnUpdateStorageBucketResponse, error) { + const op = "loopback.(LoopbackPlugin).onUpdateStorageBucket" + if req == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: request is nil", op) + } + if req.NewBucket == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op) + } + if req.GetNewBucket().Secrets == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: missing secrets", op) + } + l.m.Lock() + defer l.m.Unlock() + if _, ok := l.buckets[BucketName(req.GetNewBucket().GetBucketName())]; !ok { + return nil, status.Errorf(codes.NotFound, "%s: bucket not found", op) + } + for _, err := range l.errs { + if err.match(req.GetNewBucket(), "") { + return nil, status.Errorf(err.errCode, "%s: %s", op, err.errMsg) + } + } + return &plgpb.OnUpdateStorageBucketResponse{ + Persisted: &plgpb.StorageBucketPersisted{ + Data: req.GetNewBucket().GetSecrets(), + }, + }, nil +} + +func (l *LoopbackStorage) onDeleteStorageBucket(ctx context.Context, req *plgpb.OnDeleteStorageBucketRequest) (*plgpb.OnDeleteStorageBucketResponse, error) { + const op = "loopback.(LoopbackPlugin).onDeleteStorageBucket" + if req == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: request is nil", op) + } + if req.Bucket == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op) + } + if req.GetBucket().Secrets == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: missing secrets", op) + } + return &plgpb.OnDeleteStorageBucketResponse{}, nil +} + +func (l *LoopbackStorage) validatePermissions(ctx context.Context, req *plgpb.ValidatePermissionsRequest) (*plgpb.ValidatePermissionsResponse, error) { + const op = "loopback.(LoopbackPlugin).validatePermissions" + if req == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: request is nil", op) + } + if req.Bucket == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op) + } + if req.GetBucket().Secrets == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: missing secrets", op) + } + l.m.Lock() + defer l.m.Unlock() + if _, ok := l.buckets[BucketName(req.GetBucket().GetBucketName())]; !ok { + return nil, status.Errorf(codes.NotFound, "%s: bucket not found", op) + } + for _, err := range l.errs { + if err.match(req.GetBucket(), "") { + return nil, status.Errorf(err.errCode, "%s: %s", op, err.errMsg) + } + } + return &plgpb.ValidatePermissionsResponse{}, nil +} + +func (l *LoopbackStorage) headObject(ctx context.Context, req *plgpb.HeadObjectRequest) (*plgpb.HeadObjectResponse, error) { + const op = "loopback.(LoopbackPlugin).headObject" + if req == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: request is nil", op) + } + if req.Bucket == nil { + return nil, status.Errorf(codes.InvalidArgument, "$%s: missing storage bucket", op) + } + if req.GetBucket().Secrets == nil { + return nil, status.Errorf(codes.InvalidArgument, "%s: missing secrets", op) + } + if req.Key == "" { + return nil, status.Errorf(codes.InvalidArgument, "%s; missing object key", op) + } + l.m.Lock() + defer l.m.Unlock() + bucket, ok := l.buckets[BucketName(req.GetBucket().GetBucketName())] + if !ok { + return nil, status.Errorf(codes.NotFound, "%s: bucket not found", op) + } + objectPath := ObjectName(req.GetBucket().GetBucketPrefix() + req.GetKey()) + object, ok := bucket[objectPath] + if !ok { + return nil, status.Errorf(codes.NotFound, "%s: object %s not found", op, objectPath) + } + for _, err := range l.errs { + if err.match(req.GetBucket(), req.GetKey()) { + return nil, status.Errorf(err.errCode, "%s: %s", op, err.errMsg) + } + } + return &plgpb.HeadObjectResponse{ + ContentLength: *object.contentLength, + LastModified: timestamppb.New(*object.lastModified), + }, nil +} + +func (l *LoopbackStorage) getObject(req *plgpb.GetObjectRequest, stream plgpb.StoragePluginService_GetObjectServer) error { + const op = "loopback.(LoopbackPlugin).getObject" + if req == nil { + return status.Errorf(codes.InvalidArgument, "%s: request is nil", op) + } + if req.Bucket == nil { + return status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op) + } + if req.GetBucket().Secrets == nil { + return status.Errorf(codes.InvalidArgument, "%s: missing secrets", op) + } + if req.Key == "" { + return status.Errorf(codes.InvalidArgument, "%s; missing object key", op) + } + l.m.Lock() + defer l.m.Unlock() + bucket, ok := l.buckets[BucketName(req.GetBucket().GetBucketName())] + if !ok { + return status.Errorf(codes.NotFound, "%s: bucket not found", op) + } + objectPath := ObjectName(req.GetBucket().GetBucketPrefix() + req.GetKey()) + object, ok := bucket[objectPath] + if !ok { + return status.Errorf(codes.NotFound, "%s: object %s not found", op, objectPath) + } + for _, err := range l.errs { + if err.match(req.GetBucket(), req.GetKey()) { + return status.Errorf(err.errCode, "%s: %s", op, err.errMsg) + } + } + go func() { + for _, chunk := range object.DataChunks { + if err := stream.Send(&plgpb.GetObjectResponse{ + FileChunk: chunk, + }); err != nil { + stream.SendMsg(status.Errorf(codes.Internal, "%s: failed to send object data: %v", op, err)) + return + } + } + stream.SendMsg(io.EOF) + }() + return nil +} + +func (l *LoopbackStorage) putObject(stream plgpb.StoragePluginService_PutObjectServer) error { + const op = "loopback.(LoopbackPlugin).putObject" + if stream == nil { + return status.Errorf(codes.Internal, "%s: missing stream", op) + } + go func() { + var ( + request *plgpb.PutObjectRequest_Request + objectChunks []Chunk + objectData []byte + ) + for { + req, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + _ = stream.SendMsg(status.Errorf(codes.Internal, "%s: failed to recv message from stream: %v", op, err)) + return + } + switch data := req.Data.(type) { + case *plgpb.PutObjectRequest_Request: + if data.Request == nil { + _ = stream.SendMsg(status.Errorf(codes.InvalidArgument, "%s: missing request metadata", op)) + return + } + if data.Request.GetBucket() == nil { + _ = stream.SendMsg(status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op)) + return + } + if data.Request.GetBucket().GetSecrets() == nil { + _ = stream.SendMsg(status.Errorf(codes.InvalidArgument, "%s: missing secrets", op)) + return + } + if data.Request.Key == "" { + _ = stream.SendMsg(status.Errorf(codes.InvalidArgument, "%s; missing object key", op)) + return + } + request = data + case *plgpb.PutObjectRequest_FileChunk: + objectChunks = append(objectChunks, data.FileChunk) + objectData = append(objectData, data.FileChunk...) + case nil: + continue + default: + _ = stream.SendMsg(status.Errorf(codes.Internal, "%s: unknown message type: %v", op, data)) + return + } + } + if request == nil { + _ = stream.SendMsg(status.Errorf(codes.InvalidArgument, "%s: request is nil", op)) + return + } + if len(objectData) <= 0 { + _ = stream.SendMsg(status.Errorf(codes.InvalidArgument, "%s: missing object data", op)) + return + } + l.m.Lock() + defer l.m.Unlock() + bucket, ok := l.buckets[BucketName(request.Request.GetBucket().GetBucketName())] + if !ok { + _ = stream.SendMsg(status.Errorf(codes.NotFound, "%s: bucket not found", op)) + return + } + for _, err := range l.errs { + if err.match(request.Request.GetBucket(), request.Request.GetKey()) { + _ = stream.SendMsg(status.Errorf(err.errCode, "%s: %s", op, err.errMsg)) + return + } + } + hash := sha256.New() + if _, err := io.Copy(hash, bytes.NewReader(objectData)); err != nil { + _ = stream.SendMsg(status.Errorf(codes.Internal, "%s: failed to hash object: %v", op, err)) + return + } + contentLength := int64(len(objectData)) + lastModified := time.Now() + objectPath := ObjectName(request.Request.GetBucket().GetBucketPrefix() + request.Request.GetKey()) + bucket[objectPath] = &storagePluginStorageInfo{ + DataChunks: objectChunks, + contentLength: &contentLength, + lastModified: &lastModified, + } + if err := stream.SendAndClose(&plgpb.PutObjectResponse{ + ChecksumSha_256: hash.Sum(nil), + }); err != nil { + _ = stream.SendMsg(status.Errorf(codes.Internal, "%s: failed to close stream: %v", op, err)) + return + } + }() + return nil +} + +func MockObject(data []Chunk) *storagePluginStorageInfo { + lastModified := time.Now() + dataChunks := make([]Chunk, len(data)) + var contentLength int64 + for i, chunk := range data { + dataChunks[i] = copyBytes(chunk) + contentLength += int64(len(chunk)) + } + return &storagePluginStorageInfo{ + DataChunks: dataChunks, + contentLength: &contentLength, + lastModified: &lastModified, + } +} + +func copyBytes(in []byte) []byte { + return append([]byte{}, in...) +} diff --git a/internal/plugin/loopback/storage_test.go b/internal/plugin/loopback/storage_test.go new file mode 100644 index 000000000..da046c8a9 --- /dev/null +++ b/internal/plugin/loopback/storage_test.go @@ -0,0 +1,1001 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package loopback + +import ( + "bytes" + "context" + "crypto/sha256" + "io" + "sync" + "testing" + + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets" + plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" + ta "github.com/stretchr/testify/assert" + tr "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" +) + +func TestLoopbackOnCreateStorageBucket(t *testing.T) { + require, assert := tr.New(t), ta.New(t) + + plg, err := NewLoopbackPlugin( + WithMockBuckets(map[BucketName]Bucket{ + "aws_s3_mock": {}, + "aws_s3_err": {}, + }), + WithMockError(PluginMockError{ + bucketName: "aws_s3_err", + errMsg: "invalid credentials", + errCode: codes.PermissionDenied, + }), + ) + assert.NoError(err) + + client := NewWrappingPluginStorageClient(plg) + secrets := &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "AWS_ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"), + "AWS_SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"), + }, + } + + tests := []struct { + name string + request *plgpb.OnCreateStorageBucketRequest + expectedErr codes.Code + }{ + { + name: "missing request", + expectedErr: codes.InvalidArgument, + }, + { + name: "missing storage bucket", + request: &plgpb.OnCreateStorageBucketRequest{}, + expectedErr: codes.InvalidArgument, + }, + { + name: "missing secrets", + request: &plgpb.OnCreateStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + }, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "unknown bucket", + request: &plgpb.OnCreateStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "bucket_dne", + Secrets: secrets, + }, + }, + expectedErr: codes.NotFound, + }, + { + name: "mocked error", + request: &plgpb.OnCreateStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_err", + Secrets: secrets, + }, + }, + expectedErr: codes.PermissionDenied, + }, + { + name: "valid credentials", + request: &plgpb.OnCreateStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + response, err := client.OnCreateStorageBucket(context.Background(), tt.request) + if tt.expectedErr != codes.OK { + assert.Error(err) + assert.Nil(response) + s, ok := status.FromError(err) + require.True(ok, "invalid error type") + assert.Equal(tt.expectedErr, s.Code()) + return + } + + assert.NoError(err) + require.NotNil(response) + assert.NotNil(response.GetPersisted()) + assert.NotNil(response.GetPersisted().GetData()) + assert.EqualValues(tt.request.GetBucket().GetSecrets().AsMap(), response.GetPersisted().GetData().AsMap()) + }) + } +} + +func TestLoopbackOnUpdateStorageBucket(t *testing.T) { + require, assert := tr.New(t), ta.New(t) + + plg, err := NewLoopbackPlugin( + WithMockBuckets(map[BucketName]Bucket{ + "aws_s3_mock": {}, + "aws_s3_err": {}, + }), + WithMockError(PluginMockError{ + bucketName: "aws_s3_err", + errMsg: "invalid credentials", + errCode: codes.PermissionDenied, + }), + ) + assert.NoError(err) + + client := NewWrappingPluginStorageClient(plg) + secrets := &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "AWS_ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"), + "AWS_SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"), + }, + } + + tests := []struct { + name string + request *plgpb.OnUpdateStorageBucketRequest + expectedErr codes.Code + }{ + { + name: "missing request", + expectedErr: codes.InvalidArgument, + }, + { + name: "missing storage bucket", + request: &plgpb.OnUpdateStorageBucketRequest{}, + expectedErr: codes.InvalidArgument, + }, + { + name: "missing secrets", + request: &plgpb.OnUpdateStorageBucketRequest{ + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + }, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "unknown bucket", + request: &plgpb.OnUpdateStorageBucketRequest{ + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "bucket_dne", + Secrets: secrets, + }, + }, + expectedErr: codes.NotFound, + }, + { + name: "mocked error", + request: &plgpb.OnUpdateStorageBucketRequest{ + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_err", + Secrets: secrets, + }, + }, + expectedErr: codes.PermissionDenied, + }, + { + name: "valid credentials", + request: &plgpb.OnUpdateStorageBucketRequest{ + NewBucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + response, err := client.OnUpdateStorageBucket(context.Background(), tt.request) + if tt.expectedErr != codes.OK { + assert.Error(err) + assert.Nil(response) + s, ok := status.FromError(err) + require.True(ok, "invalid error type") + assert.Equal(tt.expectedErr, s.Code()) + return + } + + assert.NoError(err) + require.NotNil(response) + assert.NotNil(response.GetPersisted()) + assert.NotNil(response.GetPersisted().GetData()) + assert.EqualValues(tt.request.GetNewBucket().GetSecrets().AsMap(), response.GetPersisted().GetData().AsMap()) + }) + } +} + +func TestLoopbackOnDeleteStorageBucket(t *testing.T) { + require, assert := tr.New(t), ta.New(t) + + plg, err := NewLoopbackPlugin() + assert.NoError(err) + + client := NewWrappingPluginStorageClient(plg) + secrets := &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "AWS_ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"), + "AWS_SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"), + }, + } + + tests := []struct { + name string + request *plgpb.OnDeleteStorageBucketRequest + expectedErr codes.Code + }{ + { + name: "missing request", + expectedErr: codes.InvalidArgument, + }, + { + name: "missing storage bucket", + request: &plgpb.OnDeleteStorageBucketRequest{}, + expectedErr: codes.InvalidArgument, + }, + { + name: "missing secrets", + request: &plgpb.OnDeleteStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + }, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "valid credentials", + request: &plgpb.OnDeleteStorageBucketRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + response, err := client.OnDeleteStorageBucket(context.Background(), tt.request) + if tt.expectedErr != codes.OK { + assert.Error(err) + assert.Nil(response) + s, ok := status.FromError(err) + require.True(ok, "invalid error type") + assert.Equal(s.Code(), tt.expectedErr) + return + } + + assert.NoError(err) + require.NotNil(response) + }) + } +} + +func TestLoopbackValidatePermissions(t *testing.T) { + require, assert := tr.New(t), ta.New(t) + + plg, err := NewLoopbackPlugin( + WithMockBuckets(map[BucketName]Bucket{ + "aws_s3_mock": {}, + "aws_s3_err": {}, + }), + WithMockError(PluginMockError{ + bucketName: "aws_s3_err", + errMsg: "invalid credentials", + errCode: codes.PermissionDenied, + }), + ) + assert.NoError(err) + + client := NewWrappingPluginStorageClient(plg) + secrets := &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "AWS_ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"), + "AWS_SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"), + }, + } + + tests := []struct { + name string + request *plgpb.ValidatePermissionsRequest + expectedErr codes.Code + }{ + { + name: "missing request", + expectedErr: codes.InvalidArgument, + }, + { + name: "missing storage bucket", + request: &plgpb.ValidatePermissionsRequest{}, + expectedErr: codes.InvalidArgument, + }, + { + name: "missing secrets", + request: &plgpb.ValidatePermissionsRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + }, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "unknown bucket", + request: &plgpb.ValidatePermissionsRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "bucket_dne", + Secrets: secrets, + }, + }, + expectedErr: codes.NotFound, + }, + { + name: "mocked error", + request: &plgpb.ValidatePermissionsRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_err", + Secrets: secrets, + }, + }, + expectedErr: codes.PermissionDenied, + }, + { + name: "valid credentials", + request: &plgpb.ValidatePermissionsRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + response, err := client.ValidatePermissions(context.Background(), tt.request) + if tt.expectedErr != codes.OK { + assert.Error(err) + assert.Nil(response) + s, ok := status.FromError(err) + require.True(ok, "invalid error type") + assert.Equal(tt.expectedErr, s.Code()) + return + } + + assert.NoError(err) + require.NotNil(response) + }) + } +} + +func TestLoopbackHeadObject(t *testing.T) { + require, assert := tr.New(t), ta.New(t) + + mockStorageMapData := map[BucketName]Bucket{ + "aws_s3_mock": { + "mock_object": MockObject([]Chunk{ + []byte("THIS IS A MOCKED OBJECT"), + }), + }, + "aws_s3_err": { + "mock_object": MockObject([]Chunk{ + []byte("THIS IS A MOCKED OBJECT"), + }), + }, + } + + plg, err := NewLoopbackPlugin( + WithMockBuckets(mockStorageMapData), + WithMockError(PluginMockError{ + bucketName: "aws_s3_err", + objectKey: "mock_object", + errMsg: "invalid credentials", + errCode: codes.PermissionDenied, + }), + ) + assert.NoError(err) + + client := NewWrappingPluginStorageClient(plg) + secrets := &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "AWS_ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"), + "AWS_SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"), + }, + } + + tests := []struct { + name string + request *plgpb.HeadObjectRequest + expectedErr codes.Code + expectedObj *storagePluginStorageInfo + }{ + { + name: "missing request", + expectedErr: codes.InvalidArgument, + }, + { + name: "missing storage bucket", + request: &plgpb.HeadObjectRequest{}, + expectedErr: codes.InvalidArgument, + }, + { + name: "missing secrets", + request: &plgpb.HeadObjectRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + }, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "emtpy object key", + request: &plgpb.HeadObjectRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "bucket not found", + request: &plgpb.HeadObjectRequest{ + Key: "mock_object", + Bucket: &storagebuckets.StorageBucket{ + BucketName: "invalid_bucket", + Secrets: secrets, + }, + }, + expectedErr: codes.NotFound, + }, + { + name: "object not found", + request: &plgpb.HeadObjectRequest{ + Key: "invalid_object_key", + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + }, + expectedErr: codes.NotFound, + }, + { + name: "mocked error", + request: &plgpb.HeadObjectRequest{ + Key: "mock_object", + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_err", + Secrets: secrets, + }, + }, + expectedErr: codes.PermissionDenied, + }, + { + name: "head retrieved", + request: &plgpb.HeadObjectRequest{ + Key: "mock_object", + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + }, + expectedObj: mockStorageMapData["aws_s3_mock"]["mock_object"], + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := client.HeadObject(context.Background(), tt.request) + if tt.expectedErr != codes.OK { + assert.Error(err) + assert.Nil(resp) + s, ok := status.FromError(err) + require.True(ok, "invalid error type") + assert.Equal(s.Code(), tt.expectedErr) + return + } + assert.NoError(err) + assert.NotNil(resp) + assert.NotNil(tt.expectedObj) + assert.Equal(*tt.expectedObj.contentLength, resp.ContentLength) + assert.Equal(tt.expectedObj.lastModified.UTC().String(), resp.LastModified.AsTime().String()) + }) + } +} + +func TestLoopbackGetObject(t *testing.T) { + require, assert := tr.New(t), ta.New(t) + + mockStorageMapData := map[BucketName]Bucket{ + "aws_s3_mock": { + "mock_object": MockObject([]Chunk{ + []byte("THIS IS A MOCKED OBJECT"), + }), + }, + "aws_s3_err": { + "mock_object": MockObject([]Chunk{ + []byte("THIS IS A MOCKED OBJECT"), + }), + }, + } + + plg, err := NewLoopbackPlugin( + WithMockBuckets(mockStorageMapData), + WithMockError(PluginMockError{ + bucketName: "aws_s3_err", + objectKey: "mock_object", + errMsg: "invalid credentials", + errCode: codes.PermissionDenied, + }), + ) + assert.NoError(err) + + client := NewWrappingPluginStorageClient(plg) + + secrets := &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "AWS_ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"), + "AWS_SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"), + }, + } + + tests := []struct { + name string + request *plgpb.GetObjectRequest + expectedErr codes.Code + expectedObj *storagePluginStorageInfo + }{ + { + name: "missing request", + expectedErr: codes.InvalidArgument, + }, + { + name: "missing storage bucket", + request: &plgpb.GetObjectRequest{}, + expectedErr: codes.InvalidArgument, + }, + { + name: "missing secrets", + request: &plgpb.GetObjectRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + }, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "emtpy object key", + request: &plgpb.GetObjectRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "bucket not found", + request: &plgpb.GetObjectRequest{ + Key: "mock_object", + Bucket: &storagebuckets.StorageBucket{ + BucketName: "invalid_bucket", + Secrets: secrets, + }, + }, + expectedErr: codes.NotFound, + }, + { + name: "object not found", + request: &plgpb.GetObjectRequest{ + Key: "invalid_object_key", + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + }, + expectedErr: codes.NotFound, + }, + { + name: "mocked error", + request: &plgpb.GetObjectRequest{ + Key: "mock_object", + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_err", + Secrets: secrets, + }, + }, + expectedErr: codes.PermissionDenied, + }, + { + name: "object retrieved", + request: &plgpb.GetObjectRequest{ + Key: "mock_object", + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + }, + expectedObj: mockStorageMapData["aws_s3_mock"]["mock_object"], + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stream, err := client.GetObject(context.Background(), tt.request) + if tt.expectedErr != codes.OK { + assert.Error(err) + s, ok := status.FromError(err) + require.True(ok, "invalid error type") + assert.Equal(s.Code(), tt.expectedErr) + return + } + var actualData []byte + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for { + response, err := stream.Recv() + if err == io.EOF { + break + } + assert.NoError(err) + require.NotNil(response) + actualData = append(actualData, response.GetFileChunk()...) + } + }() + wg.Wait() + + assert.NotEmpty(actualData) + assert.NotNil(tt.expectedObj) + assert.NotEmpty(tt.expectedObj.DataChunks) + + var expectedData []byte + for _, chunk := range tt.expectedObj.DataChunks { + expectedData = append(expectedData, chunk...) + } + assert.EqualValues(expectedData, actualData) + }) + } +} + +func TestLoopbackPutObject(t *testing.T) { + require, assert := tr.New(t), ta.New(t) + + mockStorageMapData := map[BucketName]Bucket{ + "aws_s3_mock": {}, + "aws_s3_err": {}, + } + + plg, err := NewLoopbackPlugin( + WithMockBuckets(mockStorageMapData), + WithMockError(PluginMockError{ + bucketName: "aws_s3_err", + objectKey: "mock_object", + errMsg: "invalid credentials", + errCode: codes.PermissionDenied, + }), + ) + assert.NoError(err) + + client := NewWrappingPluginStorageClient(plg) + + secrets := &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "AWS_ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"), + "AWS_SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"), + }, + } + + tests := []struct { + name string + request *plgpb.PutObjectRequest + dataChunks []Chunk + expectedErr codes.Code + }{ + { + name: "missing request metadata", + request: &plgpb.PutObjectRequest{ + Data: &plgpb.PutObjectRequest_Request{}, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "missing storage bucket", + request: &plgpb.PutObjectRequest{ + Data: &plgpb.PutObjectRequest_Request{ + Request: &plgpb.PutObjectMetadata{}, + }, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "missing secrets", + request: &plgpb.PutObjectRequest{ + Data: &plgpb.PutObjectRequest_Request{ + Request: &plgpb.PutObjectMetadata{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + }, + }, + }, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "missing object key", + request: &plgpb.PutObjectRequest{ + Data: &plgpb.PutObjectRequest_Request{ + Request: &plgpb.PutObjectMetadata{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + }, + }, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "empty object data", + dataChunks: []Chunk{ + []byte(""), + }, + request: &plgpb.PutObjectRequest{ + Data: &plgpb.PutObjectRequest_Request{ + Request: &plgpb.PutObjectMetadata{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + Key: "mock_object", + }, + }, + }, + expectedErr: codes.InvalidArgument, + }, + { + name: "bucket not found", + dataChunks: []Chunk{ + []byte("THIS IS A MOCKED OBJECT"), + }, + request: &plgpb.PutObjectRequest{ + Data: &plgpb.PutObjectRequest_Request{ + Request: &plgpb.PutObjectMetadata{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "invalid_bucket", + Secrets: secrets, + }, + Key: "mock_object", + }, + }, + }, + expectedErr: codes.NotFound, + }, + { + name: "mock error", + dataChunks: []Chunk{ + []byte("THIS IS A MOCKED OBJECT"), + }, + request: &plgpb.PutObjectRequest{ + Data: &plgpb.PutObjectRequest_Request{ + Request: &plgpb.PutObjectMetadata{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_err", + Secrets: secrets, + }, + Key: "mock_object", + }, + }, + }, + expectedErr: codes.PermissionDenied, + }, + { + name: "valid object", + dataChunks: []Chunk{ + []byte("THIS IS A MOCKED OBJECT"), + }, + request: &plgpb.PutObjectRequest{ + Data: &plgpb.PutObjectRequest_Request{ + Request: &plgpb.PutObjectMetadata{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + Key: "mock_object", + }, + }, + }, + }, + { + name: "valid object w/ prefix", + dataChunks: []Chunk{ + []byte("THIS IS A MOCKED OBJECT"), + }, + request: &plgpb.PutObjectRequest{ + Data: &plgpb.PutObjectRequest_Request{ + Request: &plgpb.PutObjectMetadata{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + BucketPrefix: "/filtered/", + Secrets: secrets, + }, + Key: "mock_object", + }, + }, + }, + }, + { + name: "valid object w/ multiple chunks", + dataChunks: []Chunk{ + []byte("PART A: 1234567890"), + []byte("PART B: 0987654321"), + []byte("PART C: qwertyuiop"), + []byte("PART D: poiuytrewq"), + []byte("PART E: asdfghjkl"), + []byte("PART F: lkjhgfdsa"), + []byte("PART G: zxcvbnm"), + []byte("PART H: mnbvcxz"), + }, + request: &plgpb.PutObjectRequest{ + Data: &plgpb.PutObjectRequest_Request{ + Request: &plgpb.PutObjectMetadata{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + Secrets: secrets, + }, + Key: "multi_chunk_object", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stream, err := client.PutObject(context.Background()) + assert.NoError(err) + assert.NotNil(stream) + + var objectData []byte + var closeResponse *plgpb.PutObjectResponse + var closeErr error + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + err = stream.Send(tt.request) + assert.NoError(err) + + for _, chunk := range tt.dataChunks { + objectData = append(objectData, chunk...) + err = stream.Send(&plgpb.PutObjectRequest{ + Data: &plgpb.PutObjectRequest_FileChunk{ + FileChunk: chunk, + }, + }) + assert.NoError(err) + } + + closeResponse, closeErr = stream.CloseAndRecv() + }() + wg.Wait() + + if tt.expectedErr != codes.OK { + assert.Error(closeErr) + assert.Nil(closeResponse) + s, ok := status.FromError(closeErr) + require.True(ok, "invalid error type") + assert.Equal(s.Code(), tt.expectedErr) + return + } + + assert.NoError(closeErr) + require.NotNil(closeResponse) + + var actualObject *storagePluginStorageInfo + objectPath := ObjectName(tt.request.GetRequest().GetBucket().GetBucketPrefix() + tt.request.GetRequest().GetKey()) + if obj, ok := mockStorageMapData[BucketName(tt.request.GetRequest().GetBucket().GetBucketName())][objectPath]; ok { + actualObject = obj + } + require.NotNil(actualObject) + assert.EqualValues(tt.dataChunks, actualObject.DataChunks) + assert.NotEmpty(closeResponse.ChecksumSha_256) + + hash := sha256.New() + _, err = io.Copy(hash, bytes.NewReader(objectData)) + require.NoError(err) + assert.Equal(hash.Sum(nil), closeResponse.ChecksumSha_256) + }) + } +} + +func TestLoopbackStoragePlugin(t *testing.T) { + require, assert := tr.New(t), ta.New(t) + + mockStorageMapData := map[BucketName]Bucket{ + "aws_s3_mock": {}, + } + + plg, err := NewLoopbackPlugin(WithMockBuckets(mockStorageMapData)) + assert.NoError(err) + + client := NewWrappingPluginStorageClient(plg) + + secrets := &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"), + "SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"), + }, + } + + bucket := &storagebuckets.StorageBucket{ + BucketName: "aws_s3_mock", + BucketPrefix: "/filtered/path/", + Secrets: secrets, + } + objectData := []byte("THIS IS A MOCKED OBJECT") + + putObjectStream, err := client.PutObject(context.Background()) + assert.NoError(err) + + err = putObjectStream.Send(&plgpb.PutObjectRequest{ + Data: &plgpb.PutObjectRequest_Request{ + Request: &plgpb.PutObjectMetadata{ + Bucket: bucket, + Key: "mock_object", + }, + }, + }) + assert.NoError(err) + err = putObjectStream.Send(&plgpb.PutObjectRequest{ + Data: &plgpb.PutObjectRequest_FileChunk{ + FileChunk: objectData, + }, + }) + assert.NoError(err) + + putResponse, err := putObjectStream.CloseAndRecv() + require.NoError(err) + require.NotNil(putResponse) + assert.NotEmpty(putResponse.GetChecksumSha_256()) + hash := sha256.New() + _, err = io.Copy(hash, bytes.NewReader(objectData)) + require.NoError(err) + assert.EqualValues(hash.Sum(nil), putResponse.GetChecksumSha_256()) + + headResponse, err := plg.HeadObject(context.Background(), &plgpb.HeadObjectRequest{ + Bucket: bucket, + Key: "mock_object", + }) + require.NoError(err) + require.NotNil(headResponse) + require.NotNil(headResponse.LastModified) + require.Equal(int64(len(objectData)), headResponse.ContentLength) + + getObjectStream, err := client.GetObject(context.Background(), &plgpb.GetObjectRequest{ + Bucket: bucket, + Key: "mock_object", + }) + assert.NoError(err) + + var getObjectData []byte + for { + response, err := getObjectStream.Recv() + if err == io.EOF { + break + } + assert.NoError(err) + require.NotNil(response) + + getObjectData = append(getObjectData, response.GetFileChunk()...) + } + require.NotEmpty(getObjectData) + require.NotEmpty(objectData) + assert.EqualValues(objectData, getObjectData) +} diff --git a/internal/plugin/loopback/testing_grpc_stream.go b/internal/plugin/loopback/testing_grpc_stream.go new file mode 100644 index 000000000..23e0b0e96 --- /dev/null +++ b/internal/plugin/loopback/testing_grpc_stream.go @@ -0,0 +1,502 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package loopback + +import ( + "context" + "fmt" + "io" + "sync" + + plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "google.golang.org/grpc/metadata" +) + +// getObjectStreamResponse is used to mock a message sent from the server to the client. +type getObjectStreamResponse struct { + msg *plgpb.GetObjectResponse + err error +} + +// getObjectStream is used to mock the interactions between +// the client and server for the GetObject method. +type getObjectStream struct { + client plgpb.StoragePluginService_GetObjectClient + server plgpb.StoragePluginService_GetObjectServer + + // messages is used to mock the server sending messages to the client. + messages chan *getObjectStreamResponse + + m *sync.Mutex + streamClosed bool +} + +// IsStreamClosed returns true if the stream is closed. +func (s *getObjectStream) IsStreamClosed() bool { + s.m.Lock() + defer s.m.Unlock() + return s.streamClosed +} + +// Close closes the channels of the stream and sets the streamClosed flag to true. +// A closeStream is used to prevent the channels from being closed multiple times. +func (s *getObjectStream) Close() { + s.m.Lock() + defer s.m.Unlock() + if s.streamClosed { + return + } + close(s.messages) + s.streamClosed = true +} + +// getObjectClient is used to mock the client stream +// interactions for the GetObject method. +type getObjectClient struct { + // sentFromServer is used to mock the server sending messages to the client. + sentFromServer chan *getObjectStreamResponse + + // closeStream is used to close the channels of the stream. + // This is used to prevent the channels from being closed multiple times. + // This is needed because the channel can be closed by the client or the server. + closeStream func() + + // isStreamClosed is used to check if the stream is closed. + // This is needed because the channel can be closed by the client or the server. + isStreamClosed func() bool +} + +// Recv will block until a message is received from the server. +// Recv will return io.EOF if the server closes the stream. +// Recv will return an error if the server sends an error. +func (c *getObjectClient) Recv() (*plgpb.GetObjectResponse, error) { + resp, ok := <-c.sentFromServer + if !ok { + return nil, io.EOF + } + return resp.msg, resp.err +} + +// Header should not be used. +// Header is implemeted to satisfy the grpc.ClientStream interface. +// Header will always return an empty metadata and an nil error. +func (c *getObjectClient) Header() (metadata.MD, error) { + return make(metadata.MD), nil +} + +// Trailer should not be used. +// Trailer is implemeted to satisfy the grpc.ClientStream interface. +// Trailer will always return an empty metadata. +func (c *getObjectClient) Trailer() metadata.MD { + return make(metadata.MD) +} + +// CloseSend will close the channel used to retrieve messages from +// the server. This will cause the Recv method to return io.EOF. +// CloseSend will return an error if the channel is already closed. +func (c *getObjectClient) CloseSend() error { + if c.isStreamClosed() { + return fmt.Errorf("stream is closed") + } + c.closeStream() + return nil +} + +// Context will always return a Background context. +func (c *getObjectClient) Context() context.Context { + return context.Background() +} + +// SendMsg should not be used. +// SendMsg is implemeted to satisfy the grpc.ClientStream interface. +// SendMsg will always return an nil error. +func (c *getObjectClient) SendMsg(m interface{}) error { + return nil +} + +// RecvMsg should not be used. +// RecvMsg is implemeted to satisfy the grpc.ClientStream interface. +// RecvMsg will always return an nil error. +func (c *getObjectClient) RecvMsg(m interface{}) error { + return nil +} + +// getObjectServer is used to mock the server stream +// interactions for the GetObject method. +type getObjectServer struct { + // sendToClient is used to mock the server sending messages to the client. + sendToClient chan *getObjectStreamResponse + + // closeStream is used to close the channels of the stream. + // This is used to prevent the channels from being closed multiple times. + // This is needed because the channel can be closed by the client or the server. + closeStream func() + + // isStreamClosed is used to check if the stream is closed. + // This is needed because the channel can be closed by the client or the server. + isStreamClosed func() bool +} + +// Send will send a message to the client. +// Send will return an error if the client closes the stream. +// Send will return an error if the response is nil. +func (s *getObjectServer) Send(resp *plgpb.GetObjectResponse) error { + if resp == nil { + return fmt.Errorf(`parameter arg "resp GetObjectResponse" cannot be nil`) + } + if s.isStreamClosed() { + return fmt.Errorf("stream is closed") + } + s.sendToClient <- &getObjectStreamResponse{msg: resp} + return nil +} + +// SetHeader should not be used. +// SetHeader is implemeted to satisfy the grpc.ServerStream interface. +// SetHeader will always return an nil error. +func (s *getObjectServer) SetHeader(metadata.MD) error { + return nil +} + +// SendHeader should not be used. +// SendHeader is implemeted to satisfy the grpc.ServerStream interface. +// SendHeader will always return an nil error. +func (s *getObjectServer) SendHeader(metadata.MD) error { + return nil +} + +// SetTrailer should not be used. +// SetTrailer is implemeted to satisfy the grpc.ServerStream interface. +// SetTrailer will always return an nil error. +func (s *getObjectServer) SetTrailer(metadata.MD) { +} + +// Context will always return a Background context. +func (s *getObjectServer) Context() context.Context { + return context.Background() +} + +// SendMsg allows sending GetObjectResponse messages to the client. +// SendMsg allows sending errors other than io.EOF to the client. +// Sending an error message will close the stream. +// SendMsg returns an invalid argument error if the message is not +// an error or GetObjectResponse. +// SendMsg will return an error if the stream is closed. +func (s *getObjectServer) SendMsg(m interface{}) error { + switch msg := m.(type) { + case *plgpb.GetObjectResponse: + if s.isStreamClosed() { + return fmt.Errorf("stream is closed") + } + s.sendToClient <- &getObjectStreamResponse{msg: msg} + case error: + if s.isStreamClosed() { + return fmt.Errorf("stream is closed") + } + defer s.closeStream() + s.sendToClient <- &getObjectStreamResponse{err: msg} + default: + return fmt.Errorf("invalid argument %v", m) + } + return nil +} + +// RecvMsg should not be used. +// RecvMsg is implemeted to satisfy the grpc.ServerStream interface. +// RecvMsg will always return an nil error. +func (s *getObjectServer) RecvMsg(m interface{}) error { + return nil +} + +// newGetObjectStream will create a mock stream for the GetObject method. +// The client and server stream is mocked by creating a GetObjectResponse +// channel and an error channel that is shared between the client and server. +func newGetObjectStream() *getObjectStream { + stream := &getObjectStream{ + m: new(sync.Mutex), + messages: make(chan *getObjectStreamResponse), + } + stream.client = &getObjectClient{ + sentFromServer: stream.messages, + closeStream: stream.Close, + isStreamClosed: stream.IsStreamClosed, + } + stream.server = &getObjectServer{ + sendToClient: stream.messages, + closeStream: stream.Close, + isStreamClosed: stream.IsStreamClosed, + } + return stream +} + +// putObjectStreamRequest is used to mock a message sent from the client to the server. +type putObjectStreamRequest struct { + msg *plgpb.PutObjectRequest + err error +} + +// puObjectStreamResponse is used to mock a message sent from the server to the client. +type putObjectStreamResponse struct { + msg *plgpb.PutObjectResponse + err error +} + +// putObjectStream is used to mock the interactions between +// the client and server for the PutObject method. +type putObjectStream struct { + client plgpb.StoragePluginService_PutObjectClient + server plgpb.StoragePluginService_PutObjectServer + + // requests is used to mock the client sending messages to the server. + requests chan *putObjectStreamRequest + + // responses is used to mock the server sending messages to the client. + responses chan *putObjectStreamResponse + + m *sync.Mutex + clientClosed bool + serverClosed bool +} + +func (s *putObjectStream) CloseClient() { + s.m.Lock() + defer s.m.Unlock() + if s.clientClosed { + return + } + close(s.requests) + s.clientClosed = true +} + +func (s *putObjectStream) IsClientClosed() bool { + s.m.Lock() + defer s.m.Unlock() + return s.clientClosed +} + +func (s *putObjectStream) CloseServer() { + s.m.Lock() + defer s.m.Unlock() + if s.serverClosed { + return + } + close(s.responses) + s.serverClosed = true +} + +func (s *putObjectStream) IsServerClosed() bool { + s.m.Lock() + defer s.m.Unlock() + return s.serverClosed +} + +// putObjectClient is used to mock the client stream +// interactions for the PutObject method. +type putObjectClient struct { + sendToServer chan *putObjectStreamRequest + sentFromServer chan *putObjectStreamResponse + + // closeStream is used to close the channels of the stream. + // This is used to prevent the channels from being closed multiple times. + // This is needed because the channel can be closed by the client or the server. + closeStream func() + + // isStreamClosed is used to check if the stream is closed. + // This is needed because the channel can be closed by the client or the server. + isStreamClosed func() bool +} + +// Send will send a message to the server. +// Send will return an error if the request is nil. +// Send will return an error if the stream is closed. +func (c *putObjectClient) Send(req *plgpb.PutObjectRequest) error { + if req == nil { + return fmt.Errorf(`parameter arg "req PutObjectRequest" cannot be nil`) + } + if c.isStreamClosed() { + return fmt.Errorf("stream is closed") + } + c.sendToServer <- &putObjectStreamRequest{msg: req} + return nil +} + +// CloseAndRecv will return an PutObjectResponse if the server returns one. +// CloseAndRecv will return an error if the server returns an error. +// CloseAndRecv will return an io.EOF error if the server closes the stream. +// CloseAndRecv will close the stream used to send messages to the server. +func (c *putObjectClient) CloseAndRecv() (*plgpb.PutObjectResponse, error) { + c.closeStream() + resp, ok := <-c.sentFromServer + if !ok { + return nil, io.EOF + } + return resp.msg, resp.err +} + +// Header should not be used. +// Header is implemeted to satisfy the grpc.ClientStream interface. +// Header will always return an empty metadata and an nil error. +func (c *putObjectClient) Header() (metadata.MD, error) { + return make(metadata.MD), nil +} + +// Trailer should not be used. +// Trailer is implemeted to satisfy the grpc.ClientStream interface. +// Trailer will always return an empty metadata. +func (c *putObjectClient) Trailer() metadata.MD { + return make(metadata.MD) +} + +// CloseSend will close the channel used to retrieve messages from +// the server. This will cause the CloseAndRecv method to return io.EOF. +// CloseSend will always return a nill error. +func (c *putObjectClient) CloseSend() error { + if c.isStreamClosed() { + return fmt.Errorf("stream is closed") + } + c.closeStream() + return nil +} + +// Context will always return a Background context. +func (c *putObjectClient) Context() context.Context { + return context.Background() +} + +// SendMsg should not be used. +// SendMsg is implemeted to satisfy the grpc.ClientStream interface. +// SendMsg will always return an nil error. +func (c *putObjectClient) SendMsg(m interface{}) error { + return nil +} + +// RecvMsg should not be used. +// RecvMsg is implemeted to satisfy the grpc.ClientStream interface. +// RecvMsg will always return an nil error. +func (c *putObjectClient) RecvMsg(m interface{}) error { + return nil +} + +// putObjectServer is used to mock the server stream +// interactions for the PutObject method. +type putObjectServer struct { + sentFromClient chan *putObjectStreamRequest + sentToClient chan *putObjectStreamResponse + + // closeStream is used to close the channels of the stream. + // This is used to prevent the channels from being closed multiple times. + // This is needed because the channel can be closed by the client or the server. + closeStream func() + + // isStreamClosed is used to check if the stream is closed. + // This is needed because the channel can be closed by the client or the server. + isStreamClosed func() bool +} + +// SendAndClose will send a PutObjectResponse to the client. +// SendAndClose will return an error if the response is nil. +// SendAndClose will close the stream used to send messages from the client. +// SendAndClose will return an error if the stream is closed. +func (s *putObjectServer) SendAndClose(resp *plgpb.PutObjectResponse) error { + if resp == nil { + return fmt.Errorf(`parameter arg "resp PutObjectResponse" cannot be nil`) + } + if s.isStreamClosed() { + return fmt.Errorf("stream is closed") + } + s.sentToClient <- &putObjectStreamResponse{msg: resp} + s.closeStream() + return nil +} + +// Recv will read a PutObjectRequest from the stream. +// Recv will return an io.EOF error if the stream is closed. +func (s *putObjectServer) Recv() (*plgpb.PutObjectRequest, error) { + req, ok := <-s.sentFromClient + if !ok { + return nil, io.EOF + } + return req.msg, req.err +} + +// SetHeader should not be used. +// SetHeader is implemeted to satisfy the grpc.ServerStream interface. +// SetHeader will always return an nil error. +func (s *putObjectServer) SetHeader(metadata.MD) error { + return nil +} + +// SendHeader should not be used. +// SendHeader is implemeted to satisfy the grpc.ServerStream interface. +// SendHeader will always return an nil error. +func (s *putObjectServer) SendHeader(metadata.MD) error { + return nil +} + +// SetTrailer should not be used. +// SetTrailer is implemeted to satisfy the grpc.ServerStream interface. +func (s *putObjectServer) SetTrailer(metadata.MD) { +} + +// Context will always return a Background context. +func (s *putObjectServer) Context() context.Context { + return context.Background() +} + +// SendMsg allows sending errors other than io.EOF to the client. +// Sending an error message will close the stream. +// SendMsg allows sending PutObjectResponse messages to the client. +// SendMsg returns an invalid argument error if the message is not +// an error or PutObjectResponse. +// SendMsg will return an error if the stream is closed. +func (s *putObjectServer) SendMsg(m interface{}) error { + switch msg := m.(type) { + case *plgpb.PutObjectResponse: + if s.isStreamClosed() { + return fmt.Errorf("stream is closed") + } + s.sentToClient <- &putObjectStreamResponse{msg: msg} + case error: + if s.isStreamClosed() { + return fmt.Errorf("stream is closed") + } + s.sentToClient <- &putObjectStreamResponse{err: msg} + defer s.closeStream() + default: + return fmt.Errorf("invalid argument %v", m) + } + return nil +} + +// RecvMsg should not be used. +// RecvMsg is implemeted to satisfy the grpc.ServerStream interface. +// RecvMsg will always return an nil error. +func (s *putObjectServer) RecvMsg(m interface{}) error { + return nil +} + +// newPutObjectStream will create a mock stream for the PutObject method. +// The client and server stream is mocked by creating a PutObjectRequest, +// PutObjectResponse and an error channel that is shared between the client +// and server. +func newPutObjectStream() *putObjectStream { + stream := &putObjectStream{ + m: new(sync.Mutex), + requests: make(chan *putObjectStreamRequest), + responses: make(chan *putObjectStreamResponse), + } + stream.client = &putObjectClient{ + sendToServer: stream.requests, + sentFromServer: stream.responses, + closeStream: stream.CloseClient, + isStreamClosed: stream.IsClientClosed, + } + stream.server = &putObjectServer{ + sentFromClient: stream.requests, + sentToClient: stream.responses, + closeStream: stream.CloseServer, + isStreamClosed: stream.IsServerClosed, + } + return stream +} diff --git a/internal/plugin/loopback/testing_grpc_stream_test.go b/internal/plugin/loopback/testing_grpc_stream_test.go new file mode 100644 index 000000000..dd5aec9e7 --- /dev/null +++ b/internal/plugin/loopback/testing_grpc_stream_test.go @@ -0,0 +1,602 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package loopback + +import ( + "fmt" + "io" + "sync" + "testing" + + plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" + tr "github.com/stretchr/testify/require" +) + +func Test_GetObjectStream_Client(t *testing.T) { + require := tr.New(t) + + stream := newGetObjectStream() + require.NotNil(stream) + require.NotNil(stream.client) + + // Validate Header is not nil and does not return an error + header, err := stream.client.Header() + require.NotNil(header) + require.NoError(err) + + // Validate Trailer is not nil + trailer := stream.client.Trailer() + require.NotNil(trailer) + + // Validate Context is not nil + ctx := stream.client.Context() + require.NotNil(ctx) + + // validate SendMsg does not write to the channel messages + err = stream.client.SendMsg(nil) + require.NoError(err) + require.Empty(stream.messages) + + // Validate RecvMsg does not read from the channel messages + streamSize := len(stream.messages) + var msg *getObjectStreamResponse + err = stream.client.RecvMsg(msg) + require.NoError(err) + require.Nil(msg) + require.Equal(streamSize, len(stream.messages)) + + // Validate Recv reads a message from the channel messages + var wg sync.WaitGroup + wg.Add(1) + go func() { + stream.messages <- &getObjectStreamResponse{ + msg: &plgpb.GetObjectResponse{}, + } + }() + go func() { + defer wg.Done() + resp, err := stream.client.Recv() + require.NoError(err) + require.NotNil(resp) + }() + wg.Wait() + + // Validates Recv reads a error from the channel messages + wg.Add(1) + go func() { + stream.messages <- &getObjectStreamResponse{ + err: fmt.Errorf("mock error"), + } + }() + go func() { + defer wg.Done() + resp, err := stream.client.Recv() + require.Error(err) + require.Nil(resp) + }() + wg.Wait() + + // Validate CloseSend does not return an error + // Validate streamClosed is set to true + // Validate the channel messages is closed + var ok bool + err = stream.client.CloseSend() + require.NoError(err) + require.True(stream.streamClosed) + _, ok = <-stream.messages + require.False(ok) + + // Validate CloseSend does return an error when the channel is closed + // Validate streamClosed is set to true + // Validate the channel messages is closed + err = stream.client.CloseSend() + require.Error(err) + require.Contains(err.Error(), "stream is closed") + require.True(stream.streamClosed) + _, ok = <-stream.messages + require.False(ok) + + // Validate Recv returns EOF error when the channel is closed + resp, err := stream.client.Recv() + require.Equal(io.EOF, err) + require.Nil(resp) +} + +func Test_GetObjectStream_Server(t *testing.T) { + require := tr.New(t) + + stream := newGetObjectStream() + require.NotNil(stream) + require.NotNil(stream.server) + + // Validate SetHeader does not return an error + err := stream.server.SetHeader(nil) + require.NoError(err) + + // Validate SendHeader does not return an error + err = stream.server.SendHeader(nil) + require.NoError(err) + + // Validate Context returns a context + ctx := stream.server.Context() + require.NotNil(ctx) + + // Validate RecvMsg does not read from the channel messages + streamSize := len(stream.messages) + var msg *getObjectStreamResponse + err = stream.server.RecvMsg(msg) + require.NoError(err) + require.Nil(msg) + require.Equal(streamSize, len(stream.messages)) + + // Validate Send writes to the channel messages + var wg sync.WaitGroup + go func() { + err = stream.server.Send(&plgpb.GetObjectResponse{}) + require.NoError(err) + }() + wg.Add(1) + go func() { + defer wg.Done() + resp, ok := <-stream.messages + require.True(ok) + require.NotNil(resp) + require.NotNil(resp.msg) + require.Nil(resp.err) + }() + wg.Wait() + + // Validate Send returns an error when the channel is closed + stream.Close() + require.True(stream.streamClosed) + err = stream.server.Send(&plgpb.GetObjectResponse{}) + require.Error(err) + require.Contains(err.Error(), "stream is closed") + + // Validate SendMsg returns an error when the channel is closed + err = stream.server.SendMsg(&plgpb.GetObjectResponse{}) + require.Error(err) + require.Contains(err.Error(), "stream is closed") + err = stream.server.SendMsg(fmt.Errorf("mock error")) + require.Error(err) + require.Contains(err.Error(), "stream is closed") + + // Reset stream + stream = newGetObjectStream() + + // Validate SendMsg returns an error for unknown message type + err = stream.server.SendMsg(map[string]string{}) + require.False(stream.streamClosed) + require.Error(err) + require.Contains(err.Error(), "invalid argument") + + // Validate SendMsg writes to the channel messages for GetObjectResponse type + go func() { + err = stream.server.SendMsg(&plgpb.GetObjectResponse{}) + require.NoError(err) + }() + wg.Add(1) + go func() { + defer wg.Done() + resp, ok := <-stream.messages + require.True(ok) + require.NotNil(resp) + require.NotNil(resp.msg) + require.Nil(resp.err) + }() + wg.Wait() + + // Validate SendMsg closes the channel messages for error type + go func() { + err = stream.server.SendMsg(fmt.Errorf("mock error")) + require.NoError(err) + }() + wg.Add(1) + go func() { + defer wg.Done() + resp, ok := <-stream.messages + require.True(ok) + require.NotNil(resp) + require.NotNil(resp.err) + require.Nil(resp.msg) + }() + wg.Wait() + require.True(stream.streamClosed) + _, ok := <-stream.messages + require.False(ok) +} + +func TestNewGetObjectStream(t *testing.T) { + require := tr.New(t) + + stream := newGetObjectStream() + require.NotNil(stream) + require.NotNil(stream.m) + require.NotNil(stream.client) + require.NotNil(stream.server) + require.NotNil(stream.messages) + require.False(stream.streamClosed) + require.False(stream.IsStreamClosed()) + + // Validate Close() closes the channel messages + stream.Close() + require.True(stream.streamClosed) + require.True(stream.IsStreamClosed()) + _, ok := <-stream.messages + require.False(ok) +} + +func Test_PutObjectStream_Client(t *testing.T) { + require := tr.New(t) + + stream := newPutObjectStream() + require.NotNil(stream) + require.NotNil(stream.client) + + // Validate Header is not nil and does not return an error + header, err := stream.client.Header() + require.NotNil(header) + require.NoError(err) + + // Validate Trailer is not nil + trailer := stream.client.Trailer() + require.NotNil(trailer) + + // Validate Context is not nil + ctx := stream.client.Context() + require.NotNil(ctx) + + // Validate SendMsg does not write to the channel requests + err = stream.client.SendMsg(nil) + require.NoError(err) + require.Empty(stream.requests) + + // Validate RecvMsg does not read from the channel requests + streamSize := len(stream.requests) + var msg *putObjectStreamResponse + err = stream.client.RecvMsg(msg) + require.NoError(err) + require.Nil(msg) + require.Equal(streamSize, len(stream.requests)) + + // Validate Send returns an error when the request is nil + err = stream.client.Send(nil) + require.Error(err) + + // Validate Send writes to the channel requests + var wg sync.WaitGroup + go func() { + err = stream.client.Send(&plgpb.PutObjectRequest{}) + require.NoError(err) + }() + wg.Add(1) + go func() { + defer wg.Done() + req, ok := <-stream.requests + require.True(ok) + require.NotNil(req) + require.NotNil(req.msg) + require.Nil(req.err) + }() + wg.Wait() + + // Validate CloseSend does not return an error + // Validate clientClosed is set to true + // Validate the channel requests is closed + var ok bool + err = stream.client.CloseSend() + require.NoError(err) + require.True(stream.clientClosed) + _, ok = <-stream.requests + require.False(ok) + + // Validate CloseSend does return an error when the channel is closed + // Validate clientClosed is set to true + // Validate the channel requests is closed + err = stream.client.CloseSend() + require.Error(err) + require.Contains(err.Error(), "stream is closed") + require.True(stream.clientClosed) + _, ok = <-stream.requests + require.False(ok) + + // Validate Send returns an error when the channel is closed + err = stream.client.Send(&plgpb.PutObjectRequest{}) + require.Error(err) + require.Contains(err.Error(), "stream is closed") + require.Empty(stream.requests) + + // Reset stream + stream = newPutObjectStream() + + // Validate CloseAndRecv returns an EOF error when the channel responses is closed + // Validate CloseAndRecv closes the channel requests + stream.CloseServer() + resp, err := stream.client.CloseAndRecv() + require.Equal(io.EOF, err) + require.Nil(resp) + require.True(stream.clientClosed) + require.True(stream.IsClientClosed()) + require.True(stream.serverClosed) + require.True(stream.IsServerClosed()) +} + +func Test_PutObjectStream_Server(t *testing.T) { + require := tr.New(t) + + stream := newPutObjectStream() + require.NotNil(stream) + require.NotNil(stream.server) + + // Validate SetHeader does not return an error + err := stream.server.SetHeader(nil) + require.NoError(err) + + // Validate SendHeader does not return an error + err = stream.server.SendHeader(nil) + require.NoError(err) + + // Validate Context returns a context + ctx := stream.server.Context() + require.NotNil(ctx) + + // Validate RecvMsg does not read from the channel requests + streamSize := len(stream.requests) + var msg *putObjectStreamRequest + err = stream.server.RecvMsg(msg) + require.NoError(err) + require.Nil(msg) + require.Equal(streamSize, len(stream.requests)) + + // Validate SendMsg returns an error for unknown message types + err = stream.server.SendMsg(map[string]string{}) + require.Error(err) + require.Contains(err.Error(), "invalid argument") + + // Validate SendMsg does write a message to the channel responses + var wg sync.WaitGroup + go func() { + err = stream.server.SendMsg(&plgpb.PutObjectResponse{}) + require.NoError(err) + }() + wg.Add(1) + go func() { + defer wg.Done() + resp, ok := <-stream.responses + require.True(ok) + require.NotNil(resp) + require.NotNil(resp.msg) + require.Nil(resp.err) + }() + wg.Wait() + + // Validate SendMsg does write a error to the channel responses + // Validate SendMsg closes the channel responses + go func() { + err = stream.server.SendMsg(fmt.Errorf("mock error")) + require.NoError(err) + }() + wg.Add(1) + go func() { + defer wg.Done() + resp, ok := <-stream.responses + require.True(ok) + require.NotNil(resp) + require.NotNil(resp.err) + require.Nil(resp.msg) + }() + wg.Wait() + _, ok := <-stream.responses + require.False(ok) + require.True(stream.serverClosed) + require.True(stream.IsServerClosed()) + + // Validate SendMsg does return an error when the channel responses is closed + err = stream.server.SendMsg(&plgpb.PutObjectResponse{}) + require.Error(err) + require.Contains(err.Error(), "stream is closed") + err = stream.server.SendMsg(fmt.Errorf("mock error")) + require.Error(err) + require.Contains(err.Error(), "stream is closed") + + // Validate SendAndClose returns an error when response is nil + err = stream.server.SendAndClose(nil) + require.Error(err) + + // Validate SendAndClose returns an error when the channel responses is closed + err = stream.server.SendAndClose(&plgpb.PutObjectResponse{}) + require.Error(err) + require.Contains(err.Error(), "stream is closed") + + // Reset stream + stream = newPutObjectStream() + + // Validate SendAndClose closes the responses channel + go func() { + err = stream.server.SendAndClose(&plgpb.PutObjectResponse{}) + require.NoError(err) + }() + wg.Add(1) + go func() { + defer wg.Done() + resp, ok := <-stream.responses + require.True(ok) + require.NotNil(resp) + require.NotNil(resp.msg) + require.Nil(resp.err) + }() + wg.Wait() + require.True(stream.serverClosed) + require.True(stream.IsServerClosed()) + _, ok = <-stream.responses + require.False(ok) +} + +func TestNewPutObjectStream(t *testing.T) { + require := tr.New(t) + + stream := newPutObjectStream() + require.NotNil(stream) + require.NotNil(stream.m) + require.NotNil(stream.client) + require.NotNil(stream.server) + require.NotNil(stream.requests) + require.NotNil(stream.responses) + require.False(stream.clientClosed) + require.False(stream.IsClientClosed()) + require.False(stream.serverClosed) + require.False(stream.IsServerClosed()) + + // Validate CloseClient() closes the requests channel + stream.CloseClient() + require.True(stream.clientClosed) + require.True(stream.IsClientClosed()) + _, ok := <-stream.requests + require.False(ok) + + // Validate CloseServer() closes the responses channel + stream.CloseServer() + require.True(stream.serverClosed) + require.True(stream.IsServerClosed()) + _, ok = <-stream.responses + require.False(ok) +} + +func Test_GetObjectStream(t *testing.T) { + require := tr.New(t) + + stream := newGetObjectStream() + + // Validate the server receives an error when the client closes the stream + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for { + err := stream.server.Send(&plgpb.GetObjectResponse{}) + if err == nil { + continue + } + require.Error(err) + require.Contains(err.Error(), "stream is closed") + break + } + }() + stream.client.CloseSend() + wg.Wait() + require.True(stream.streamClosed) + require.True(stream.IsStreamClosed()) + _, ok := <-stream.messages + require.False(ok) + + // Reset Stream + stream = newGetObjectStream() + + // Validate the client recieves an error when the server sends an error + // Validate the channel messages is closed + go func() { + err := stream.server.SendMsg(fmt.Errorf("mock error")) + require.NoError(err) + }() + resp, err := stream.client.Recv() + require.Nil(resp) + require.Error(err) + require.Contains(err.Error(), "mock error") + require.True(stream.streamClosed) + require.True(stream.IsStreamClosed()) + _, ok = <-stream.messages + require.False(ok) +} + +func Test_PutObjectStream(t *testing.T) { + require := tr.New(t) + + stream := newPutObjectStream() + + // Validate the client recieves the response when the server closes the stream + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + for { + resp, err := stream.client.CloseAndRecv() + require.NotNil(resp) + require.NoError(err) + break + } + }() + go func() { + defer wg.Done() + err := stream.server.SendAndClose(&plgpb.PutObjectResponse{}) + require.NoError(err) + }() + wg.Wait() + require.True(stream.clientClosed) + require.True(stream.IsClientClosed()) + require.True(stream.serverClosed) + require.True(stream.IsServerClosed()) + _, ok := <-stream.requests + require.False(ok) + _, ok = <-stream.responses + require.False(ok) + + // Reset Stream + stream = newPutObjectStream() + + // Validate the client recieves an error when the server sends an error + wg.Add(2) + go func() { + defer wg.Done() + for { + resp, err := stream.client.CloseAndRecv() + require.Nil(resp) + require.Error(err) + require.Contains(err.Error(), "mock error") + break + } + }() + go func() { + defer wg.Done() + err := stream.server.SendMsg(fmt.Errorf("mock error")) + require.NoError(err) + }() + wg.Wait() + require.True(stream.clientClosed) + require.True(stream.IsClientClosed()) + require.True(stream.serverClosed) + require.True(stream.IsServerClosed()) + _, ok = <-stream.requests + require.False(ok) + _, ok = <-stream.responses + require.False(ok) + + // Reset Stream + stream = newPutObjectStream() + + // Validate the server recieves an EOF error when the client closes the stream + wg.Add(2) + go func() { + defer wg.Done() + for { + req, err := stream.server.Recv() + if err != nil { + require.Nil(req) + require.Equal(io.EOF, err) + break + } + } + }() + go func() { + wg.Done() + err := stream.client.CloseSend() + require.NoError(err) + }() + wg.Wait() + require.True(stream.clientClosed) + require.True(stream.IsClientClosed()) + require.False(stream.serverClosed) + require.False(stream.IsServerClosed()) + _, ok = <-stream.requests + require.False(ok) +}