diff --git a/go.mod b/go.mod index af6584489..efbfb1cc1 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,14 @@ module github.com/hashicorp/boundary -go 1.16 +go 1.17 replace github.com/hashicorp/boundary/api => ./api replace github.com/hashicorp/boundary/sdk => ./sdk require ( - github.com/Azure/azure-sdk-for-go v57.4.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.21 // indirect - github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 // indirect - github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect - github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/armon/go-metrics v0.3.9 - github.com/aws/aws-sdk-go v1.40.44 // indirect github.com/bufbuild/buf v0.37.0 - github.com/dhui/dktest v0.3.4 // indirect github.com/fatih/color v1.12.0 github.com/fatih/structs v1.1.0 github.com/favadi/protoc-go-inject-tag v1.3.0 @@ -24,7 +17,7 @@ require ( github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe github.com/golang/protobuf v1.5.2 github.com/google/go-cmp v0.5.6 - github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.6.0 github.com/hashicorp/boundary/api v0.0.19 github.com/hashicorp/boundary/sdk v0.0.12-0.20211007161358-65f3725bf3e1 @@ -40,7 +33,6 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-retryablehttp v0.7.0 github.com/hashicorp/go-rootcerts v1.0.2 - github.com/hashicorp/go-secure-stdlib/awsutil v0.1.4 // indirect github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 github.com/hashicorp/go-secure-stdlib/configutil v0.1.2 github.com/hashicorp/go-secure-stdlib/gatedwriter v0.1.1 @@ -52,7 +44,6 @@ require ( github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1 github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 github.com/hashicorp/go-uuid v1.0.2 - github.com/hashicorp/go-version v1.3.0 // indirect github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/vault/api v1.1.1 github.com/hashicorp/vault/sdk v0.2.1 @@ -77,7 +68,6 @@ require ( github.com/pkg/errors v0.9.1 github.com/posener/complete v1.2.3 github.com/ryanuber/go-glob v1.0.0 - github.com/spf13/cobra v1.1.1 // indirect github.com/stretchr/testify v1.7.0 github.com/zalando/go-keyring v0.1.1 go.uber.org/atomic v1.9.0 @@ -85,7 +75,6 @@ require ( golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 golang.org/x/term v0.0.0-20210916214954-140adaaadfaf golang.org/x/tools v0.1.6 - google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 google.golang.org/grpc v1.40.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 @@ -95,3 +84,132 @@ require ( mvdan.cc/gofumpt v0.1.1 nhooyr.io/websocket v1.8.7 ) + +require ( + cloud.google.com/go v0.65.0 // indirect + github.com/AlecAivazis/survey/v2 v2.2.9 // indirect + github.com/Azure/azure-sdk-for-go v57.4.0+incompatible // indirect + github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.21 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.14 // indirect + github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect + github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Masterminds/goutils v1.1.0 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/semver/v3 v3.1.1 // indirect + github.com/Masterminds/sprig v2.22.0+incompatible // indirect + github.com/Microsoft/go-winio v0.4.16 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190620160927-9418d7b0cd0f // indirect + github.com/apex/log v1.9.0 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/aws/aws-sdk-go v1.40.44 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/cenkalti/backoff/v3 v3.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.1.0 // indirect + github.com/containerd/continuity v0.0.0-20200709052629-daa8e1ccc0bc // indirect + github.com/coreos/go-oidc/v3 v3.0.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + github.com/danieljoos/wincred v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dhui/dktest v0.3.4 // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/docker/cli v20.10.7+incompatible // indirect + github.com/docker/docker v20.10.7+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b // indirect + github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect + github.com/ghodss/yaml v1.0.0 // indirect + github.com/gofrs/flock v0.8.0 // indirect + github.com/gofrs/uuid v4.0.0+incompatible // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.0.0 // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/googleapis/gax-go/v2 v2.0.5 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-immutable-radix v1.1.0 // indirect + github.com/hashicorp/go-plugin v1.0.1 // indirect + github.com/hashicorp/go-secure-stdlib/awsutil v0.1.4 // indirect + github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/go-version v1.3.0 // indirect + github.com/hashicorp/golang-lru v0.5.3 // indirect + github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect + github.com/huandu/xstrings v1.3.2 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.1.1 // indirect + github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect + github.com/jackc/pgtype v1.7.0 // indirect + github.com/jefferai/go-libsecret v0.0.0-20210105015933-d08a58b018bc // indirect + github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f // indirect + github.com/jhump/protoreflect v1.8.1 // indirect + github.com/jinzhu/gorm v1.9.12 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.2 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/json-iterator/go v1.1.10 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d // indirect + github.com/klauspost/compress v1.11.7 // indirect + github.com/klauspost/pgzip v1.2.5 // indirect + github.com/lib/pq v1.8.0 // indirect + github.com/mattn/go-isatty v0.0.13 // indirect + github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.0.0 // indirect + github.com/mitchellh/iochan v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/mtibben/percent v0.2.1 // indirect + github.com/oklog/run v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/opencontainers/runc v1.0.0-rc9 // indirect + github.com/oracle/oci-go-sdk v12.5.0+incompatible // indirect + github.com/pierrec/lz4 v2.5.2+incompatible // indirect + github.com/pkg/profile v1.5.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.6.2 // indirect + github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/sirupsen/logrus v1.7.0 // indirect + github.com/spf13/cobra v1.1.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/twitchtv/twirp v7.1.0+incompatible // indirect + github.com/urfave/cli/v2 v2.3.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + go.opencensus.io v0.22.6 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.16.0 // indirect + golang.org/x/mod v0.4.2 // indirect + golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect + golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect + golang.org/x/text v0.3.6 // indirect + golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/api v0.30.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + gopkg.in/ini.v1 v1.51.0 // indirect + gopkg.in/square/go-jose.v2 v2.5.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/internal/host/plugin/repository_host_catalog.go b/internal/host/plugin/repository_host_catalog.go index f788fe7ee..a7353eaf9 100644 --- a/internal/host/plugin/repository_host_catalog.go +++ b/internal/host/plugin/repository_host_catalog.go @@ -195,6 +195,80 @@ func (r *Repository) ListCatalogs(ctx context.Context, scopeIds []string, opt .. return hostCatalogs, plgs, nil } +// DeleteCatalog deletes catalog for the provided id from the repository +// returning a count of the number of records deleted. All options are ignored. +func (r *Repository) DeleteCatalog(ctx context.Context, id string, _ ...Option) (int, error) { + const op = "plugin.(Repository).DeleteCatalog" + if id == "" { + return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no public id") + } + + c, err := r.getCatalog(ctx, id) + if err != nil && !errors.IsNotFoundError(err) { + return db.NoRowsAffected, errors.Wrap(ctx, err, op) + } + if c == nil { + return db.NoRowsAffected, nil + } + p, err := r.getPersistedDataForCatalog(ctx, c) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op) + } + + plgClient, ok := r.plugins[c.GetPluginId()] + if !ok || plgClient == nil { + return 0, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("plugin %q not available", c.GetPluginId())) + } + plgHc, err := toPluginCatalog(ctx, c) + if err != nil { + return 0, errors.Wrap(ctx, err, op) + } + // TODO: Include all the sets for this catalog in the delete catalog request. We don't need it for now + // since our currently provided plugins only read data for set configuration, but in the future that might + // change. + _, err = plgClient.OnDeleteCatalog(ctx, &plgpb.OnDeleteCatalogRequest{Catalog: plgHc, Persisted: p}) + if err != nil { + // Even if the plugin returns an error, we ignore it and proceed with + // deleting the catalog. + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, c.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + metadata := c.oplog(oplog.OpType_OP_TYPE_DELETE) + + var rowsDeleted int + var deleteCatalog *HostCatalog + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + deleteCatalog = c.clone() + var err error + rowsDeleted, err = w.Delete( + ctx, + deleteCatalog, + db.WithOplog(oplogWrapper, metadata), + ) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsDeleted > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been deleted") + } + return nil + }, + ) + + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("delete failed for %s", c.PublicId))) + } + + return rowsDeleted, nil +} + // getCatalog retrieves the *HostCatalog with the provided id. If it is not found or there // is an problem getting it from the database an error is returned instead. func (r *Repository) getCatalog(ctx context.Context, id string) (*HostCatalog, error) { diff --git a/internal/host/plugin/repository_host_catalog_test.go b/internal/host/plugin/repository_host_catalog_test.go index 58993a303..15f606bdc 100644 --- a/internal/host/plugin/repository_host_catalog_test.go +++ b/internal/host/plugin/repository_host_catalog_test.go @@ -2,6 +2,7 @@ package plugin import ( "context" + "fmt" "strings" "testing" "time" @@ -470,3 +471,154 @@ func TestRepository_ListCatalogs_Multiple_Scopes(t *testing.T) { assert.Equal(t, total, len(got)) assert.Contains(t, plgs, plg) } + +func TestRepository_DeleteCatalog(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := hostplg.TestPlugin(t, conn, "test") + pluginInstance := &TestPluginServer{} + plgm := map[string]plgpb.HostPluginServiceClient{ + plg.GetPublicId(): &WrappingPluginClient{Server: pluginInstance}, + } + cat := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) + cat2 := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) + badId, err := newHostCatalogId(ctx) + assert.NoError(t, err) + assert.NotNil(t, badId) + + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, plgm) + assert.NoError(t, err) + assert.NotNil(t, repo) + + tests := []struct { + name string + id string + pluginChecker func(*testing.T, *plgpb.OnDeleteCatalogRequest) error + want int + wantErr errors.Code + }{ + { + name: "found", + id: cat.GetPublicId(), + pluginChecker: func(t *testing.T, req *plgpb.OnDeleteCatalogRequest) error { + assert.Equal(t, cat.GetPublicId(), req.GetCatalog().GetId()) + return nil + }, + want: 1, + }, + { + name: "ignore error", + id: cat2.GetPublicId(), + pluginChecker: func(t *testing.T, req *plgpb.OnDeleteCatalogRequest) error { + assert.Equal(t, cat2.GetPublicId(), req.GetCatalog().GetId()) + return fmt.Errorf("This is a test error") + }, + want: 1, + }, + { + name: "not-found", + id: badId, + pluginChecker: func(t *testing.T, req *plgpb.OnDeleteCatalogRequest) error { + assert.Fail(t, "Should not call the plugin when catalog isn't found") + return nil + }, + want: 0, + }, + { + name: "bad-public-id", + id: "", + pluginChecker: func(t *testing.T, req *plgpb.OnDeleteCatalogRequest) error { + assert.Fail(t, "Should not call the plugin for a bad id") + return nil + }, + want: 0, + wantErr: errors.InvalidParameter, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + pluginInstance.OnDeleteCatalogFn = func(_ context.Context, request *plgpb.OnDeleteCatalogRequest) (*plgpb.OnDeleteCatalogResponse, error) { + return nil, tt.pluginChecker(t, request) + } + got, err := repo.DeleteCatalog(context.Background(), tt.id) + if tt.wantErr != 0 { + assert.Truef(t, errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.want, got, "row count") + }) + } +} + +func TestRepository_DeleteCatalogX(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := hostplg.TestPlugin(t, conn, "test") + plgm := map[string]plgpb.HostPluginServiceClient{ + plg.GetPublicId(): &WrappingPluginClient{Server: &TestPluginServer{}}, + } + cat := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) + badId, err := newHostCatalogId(ctx) + assert.NoError(t, err) + assert.NotNil(t, badId) + + tests := []struct { + name string + id string + want *HostCatalog + wantErr errors.Code + }{ + { + name: "found", + id: cat.GetPublicId(), + want: cat, + }, + { + name: "not-found", + id: badId, + want: nil, + }, + { + name: "bad-public-id", + id: "", + want: nil, + wantErr: errors.InvalidParameter, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms, plgm) + assert.NoError(err) + assert.NotNil(repo) + + got, _, err := repo.LookupCatalog(ctx, tt.id) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + return + } + assert.NoError(err) + + switch { + case tt.want == nil: + assert.Nil(got) + case tt.want != nil: + assert.NotNil(got) + assert.Equal(got, tt.want) + } + }) + } +} \ No newline at end of file diff --git a/internal/host/plugin/repository_host_set.go b/internal/host/plugin/repository_host_set.go index 06fecbeb3..b89821dce 100644 --- a/internal/host/plugin/repository_host_set.go +++ b/internal/host/plugin/repository_host_set.go @@ -249,6 +249,85 @@ func (r *Repository) ListSets(ctx context.Context, catalogId string, opt ...host return sets, plg, nil } +// DeleteSet deletes the host set for the provided id from the repository +// returning a count of the number of records deleted. All options are +// ignored. +func (r *Repository) DeleteSet(ctx context.Context, scopeId string, publicId string, _ ...Option) (int, error) { + const op = "plugin.(Repository).DeleteSet" + if publicId == "" { + return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no public id") + } + if scopeId == "" { + return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "no scope id") + } + + sets, plg, err := r.getSets(ctx, publicId, "") + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op) + } + if len(sets) != 1 { + return db.NoRowsAffected, nil + } + s := sets[0] + + c, err := r.getCatalog(ctx, s.GetCatalogId()) + if err != nil && errors.IsNotFoundError(err) { + return db.NoRowsAffected, errors.Wrap(ctx, err, op) + } + if c == nil { + return db.NoRowsAffected, nil + } + p, err := r.getPersistedDataForCatalog(ctx, c) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op) + } + + plgClient, ok := r.plugins[plg.GetPublicId()] + if !ok || plgClient == nil { + return 0, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("plugin %q not available", c.GetPluginId())) + } + plgHc, err := toPluginCatalog(ctx, c) + if err != nil { + return 0, errors.Wrap(ctx, err, op) + } + plgHs, err := toPluginSet(ctx, s) + if err != nil { + return 0, errors.Wrap(ctx, err, op) + } + _, err = plgClient.OnDeleteSet(ctx, &plgpb.OnDeleteSetRequest{Catalog: plgHc, Persisted: p, Set: plgHs}) + if err != nil { + // Even if the plugin returns an error, we ignore it and proceed + // with deleting the set. + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + var rowsDeleted int + _, err = r.writer.DoTx( + ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) (err error) { + ds := s.clone() + rowsDeleted, err = w.Delete(ctx, ds, db.WithOplog(oplogWrapper, s.oplog(oplog.OpType_OP_TYPE_DELETE))) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsDeleted > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been deleted") + } + return nil + }, + ) + + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("delete failed for %s", s.PublicId))) + } + + return rowsDeleted, nil +} + func (r *Repository) getSets(ctx context.Context, publicId string, catalogId string, opt ...host.Option) ([]*HostSet, *hostplugin.Plugin, error) { const op = "plugin.(Repository).getSets" if publicId == "" && catalogId == "" { diff --git a/internal/host/plugin/repository_host_set_test.go b/internal/host/plugin/repository_host_set_test.go index cffa56bc9..c915f5d93 100644 --- a/internal/host/plugin/repository_host_set_test.go +++ b/internal/host/plugin/repository_host_set_test.go @@ -2,6 +2,7 @@ package plugin import ( "context" + "fmt" "sort" "strings" "testing" @@ -654,6 +655,92 @@ func TestRepository_ListSets_Limits(t *testing.T) { } } +func TestRepository_DeleteSet(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, prj := iam.TestScopes(t, iamRepo) + 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) { + return &plgpb.OnCreateSetResponse{}, nil + }}), + } + c := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) + hostSet := TestSet(t, conn, kms, c, plgm) + hostSet2 := TestSet(t, conn, kms, c, plgm) + + newHostSetId, err := newHostSetId(ctx) + require.NoError(t, err) + tests := []struct { + name string + in string + pluginChecker func(*plgpb.OnDeleteSetRequest) error + want int + wantIsErr errors.Code + }{ + { + name: "With no public id", + pluginChecker: func(req *plgpb.OnDeleteSetRequest) error { + assert.Fail(t, "The plugin shouldn't be called") + return nil + }, + wantIsErr: errors.InvalidParameter, + }, + { + name: "With non existing host set id", + in: newHostSetId, + pluginChecker: func(req *plgpb.OnDeleteSetRequest) error { + assert.Fail(t, "The plugin shouldn't be called") + return nil + }, + want: 0, + }, + { + name: "With existing host set id", + in: hostSet.PublicId, + pluginChecker: func(req *plgpb.OnDeleteSetRequest) error { + assert.Equal(t, c.GetPublicId(), req.GetCatalog().GetId()) + assert.Equal(t, hostSet.GetPublicId(), req.GetSet().GetId()) + return nil + }, + want: 1, + }, + { + name: "Ignores plugin errors", + in: hostSet2.PublicId, + pluginChecker: func(req *plgpb.OnDeleteSetRequest) error { + assert.Equal(t, c.GetPublicId(), req.GetCatalog().GetId()) + assert.Equal(t, hostSet2.GetPublicId(), req.GetSet().GetId()) + return fmt.Errorf("test error") + }, + want: 1, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms, plgm) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.DeleteSet(ctx, prj.PublicId, tt.in) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "want err: %q got: %q", tt.wantIsErr, err) + assert.Zero(got) + return + } + require.NoError(err) + assert.EqualValues(tt.want, got) + }) + } +} + func printoutTable(t *testing.T, rw *db.Db) { ctx := context.Background() hsas := []*hostSetAgg{} diff --git a/internal/servers/controller/controller.go b/internal/servers/controller/controller.go index 7e6175878..ca29bbcc0 100644 --- a/internal/servers/controller/controller.go +++ b/internal/servers/controller/controller.go @@ -123,7 +123,7 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { external_host_plugins.WithHostPluginExecutionDir(conf.RawConfig.Plugins.ExecutionDir), external_host_plugins.WithLogger(hclog.NewNullLogger())) if err != nil { - return nil, fmt.Errorf("error creating azure host plugin") + return nil, fmt.Errorf("error creating azure host plugin: %w", err) } conf.ShutdownFuncs = append(conf.ShutdownFuncs, azureCleanup) if _, err := conf.RegisterHostPlugin(ctx, "azure", azureSvcClient, hostplugin.WithDescription("Built-in Azure host plugin")); err != nil { diff --git a/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go b/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go index 7531926ea..86360a124 100644 --- a/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go +++ b/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go @@ -534,13 +534,26 @@ func (s Service) updateInRepo(ctx context.Context, projId, id string, mask []str func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { const op = "host_catalogs.(Service).deleteFromRepo" - repo, err := s.staticRepoFn() - if err != nil { - return false, errors.Wrap(ctx, err, op) - } - rows, err := repo.DeleteCatalog(ctx, id) - if err != nil { - return false, errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete host")) + rows := 0 + switch host.SubtypeFromId(id) { + case static.Subtype: + repo, err := s.staticRepoFn() + if err != nil { + return false, errors.Wrap(ctx, err, op) + } + rows, err = repo.DeleteCatalog(ctx, id) + if err != nil { + return false, errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete host")) + } + case plugin.Subtype: + repo, err := s.pluginHostRepoFn() + if err != nil { + return false, errors.Wrap(ctx, err, op) + } + rows, err = repo.DeleteCatalog(ctx, id) + if err != nil { + return false, errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete host")) + } } return rows > 0, nil } diff --git a/internal/servers/controller/handlers/host_catalogs/host_catalog_service_test.go b/internal/servers/controller/handlers/host_catalogs/host_catalog_service_test.go index 01650a4f3..3d2645035 100644 --- a/internal/servers/controller/handlers/host_catalogs/host_catalog_service_test.go +++ b/internal/servers/controller/handlers/host_catalogs/host_catalog_service_test.go @@ -455,7 +455,7 @@ func TestList(t *testing.T) { } } -func TestDelete(t *testing.T) { +func TestDelete_Static(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) @@ -524,6 +524,76 @@ func TestDelete(t *testing.T) { } } +func TestDelete_Plugin(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, proj := iam.TestScopes(t, iamRepo) + rw := db.New(conn) + repo := func() (*static.Repository, error) { + return static.NewRepository(rw, rw, kms) + } + pluginHostRepo := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, map[string]plgpb.HostPluginServiceClient{}) + } + pluginRepo := func() (*host.Repository, error) { + return host.NewRepository(rw, rw, kms) + } + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + plg := host.TestPlugin(t, conn, "test") + hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) + + s, err := host_catalogs.NewService(repo, pluginHostRepo, pluginRepo, iamRepoFn) + require.NoError(t, err, "Couldn't create a new host catalog service.") + + cases := []struct { + name string + scopeId string + req *pbs.DeleteHostCatalogRequest + res *pbs.DeleteHostCatalogResponse + err error + }{ + { + name: "Delete an Existing HostCatalog", + scopeId: proj.GetPublicId(), + req: &pbs.DeleteHostCatalogRequest{ + Id: hc.GetPublicId(), + }, + }, + { + name: "Delete bad id HostCatalog", + scopeId: proj.GetPublicId(), + req: &pbs.DeleteHostCatalogRequest{ + Id: plugin.HostCatalogPrefix + "_doesntexis", + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Bad HostCatalog Id formatting", + scopeId: proj.GetPublicId(), + req: &pbs.DeleteHostCatalogRequest{ + Id: plugin.HostCatalogPrefix + "_bad_format", + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, gErr := s.DeleteHostCatalog(auth.DisabledAuthTestContext(iamRepoFn, tc.scopeId), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "DeleteHostCatalog(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + } + assert.EqualValuesf(tc.res, got, "DeleteHostCatalog(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + func TestDelete_twice(t *testing.T) { t.Parallel() assert, require := assert.New(t), require.New(t) diff --git a/internal/servers/controller/handlers/host_sets/host_set_service.go b/internal/servers/controller/handlers/host_sets/host_set_service.go index f9a02d512..cd047b20a 100644 --- a/internal/servers/controller/handlers/host_sets/host_set_service.go +++ b/internal/servers/controller/handlers/host_sets/host_set_service.go @@ -525,13 +525,26 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, catalogId string, re func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, error) { const op = "host_sets.(Service).deleteFromRepo" - repo, err := s.staticRepoFn() - if err != nil { - return false, err - } - rows, err := repo.DeleteSet(ctx, scopeId, id) - if err != nil { - return false, errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete host")) + rows := 0 + switch host.SubtypeFromId(id) { + case static.Subtype: + repo, err := s.staticRepoFn() + if err != nil { + return false, err + } + rows, err = repo.DeleteSet(ctx, scopeId, id) + if err != nil { + return false, errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete host")) + } + case plugin.Subtype: + repo, err := s.pluginRepoFn() + if err != nil { + return false, err + } + rows, err = repo.DeleteSet(ctx, scopeId, id) + if err != nil { + return false, errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete host")) + } } return rows > 0, nil } @@ -900,7 +913,7 @@ func validateUpdateRequest(ctx context.Context, req *pbs.UpdateHostSetRequest) e } func validateDeleteRequest(req *pbs.DeleteHostSetRequest) error { - return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, static.HostSetPrefix) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, static.HostSetPrefix, plugin.HostSetPrefix) } func validateListRequest(req *pbs.ListHostSetsRequest) error { diff --git a/internal/servers/controller/handlers/host_sets/host_set_service_test.go b/internal/servers/controller/handlers/host_sets/host_set_service_test.go index 6fd73e248..d7c630028 100644 --- a/internal/servers/controller/handlers/host_sets/host_set_service_test.go +++ b/internal/servers/controller/handlers/host_sets/host_set_service_test.go @@ -445,7 +445,7 @@ func TestList_Plugin(t *testing.T) { } } -func TestDelete(t *testing.T) { +func TestDelete_Static(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) @@ -494,18 +494,86 @@ func TestDelete(t *testing.T) { err: handlers.ApiErrorWithCode(codes.NotFound), }, { - name: "Delete bad host catalog id in Host Set", + name: "Bad Host Id formatting", + scopeId: proj.GetPublicId(), + req: &pbs.DeleteHostSetRequest{ + Id: static.HostSetPrefix + "_bad_format", + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, gErr := s.DeleteHostSet(auth.DisabledAuthTestContext(iamRepoFn, tc.scopeId), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "DeleteHostSet(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + } + assert.Empty(cmp.Diff(tc.res, got, protocmp.Transform()), "DeleteHostSet(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestDelete_Plugin(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + + _, proj := iam.TestScopes(t, iamRepo) + + rw := db.New(conn) + repoFn := func() (*static.Repository, error) { + return static.NewRepository(rw, rw, kms) + } + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, map[string]plgpb.HostPluginServiceClient{}) + } + name := "test" + plg := hostplugin.TestPlugin(t, conn, name) + plgm := map[string]plgpb.HostPluginServiceClient{ + plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{}), + } + + hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) + h := plugin.TestSet(t, conn, kms, hc, plgm) + + s, err := host_sets.NewService(repoFn, pluginRepoFn) + require.NoError(t, err, "Couldn't create a new host set service.") + + cases := []struct { + name string + scopeId string + req *pbs.DeleteHostSetRequest + res *pbs.DeleteHostSetResponse + err error + }{ + { + name: "Delete an Existing Host Set", scopeId: proj.GetPublicId(), req: &pbs.DeleteHostSetRequest{ Id: h.GetPublicId(), }, + }, + { + name: "Delete bad id Host Set", + scopeId: proj.GetPublicId(), + req: &pbs.DeleteHostSetRequest{ + Id: plugin.HostSetPrefix + "_doesntexis", + }, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Bad Host Id formatting", scopeId: proj.GetPublicId(), req: &pbs.DeleteHostSetRequest{ - Id: static.HostSetPrefix + "_bad_format", + Id: plugin.HostSetPrefix + "_bad_format", }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, diff --git a/internal/tests/api/hostcatalogs/host_catalog_test.go b/internal/tests/api/hostcatalogs/host_catalog_test.go index e37dbe13b..12fd5679c 100644 --- a/internal/tests/api/hostcatalogs/host_catalog_test.go +++ b/internal/tests/api/hostcatalogs/host_catalog_test.go @@ -117,6 +117,23 @@ func TestCrud(t *testing.T) { apiErr := api.AsServerError(err) assert.NotNil(apiErr) assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode()) + + // Plugin catalogs + c, err := hcClient.Create(tc.Context(), "plugin", proj.GetPublicId(), hostcatalogs.WithName("pluginfoo"), hostcatalogs.WithPluginId("pl_1234567890"), + hostcatalogs.WithAttributes(map[string]interface{}{"foo": "bar"})) + require.NoError(err) + + c, err = hcClient.Read(tc.Context(), c.Item.Id) + assert.NoError(err) + + _, err = hcClient.Delete(tc.Context(), c.Item.Id) + assert.NoError(err) + + _, err = hcClient.Delete(tc.Context(), c.Item.Id) + require.Error(err) + apiErr = api.AsServerError(err) + assert.NotNil(apiErr) + assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode()) } func TestErrors(t *testing.T) { diff --git a/internal/tests/api/hostsets/host_set_test.go b/internal/tests/api/hostsets/host_set_test.go index e44501839..eb78af142 100644 --- a/internal/tests/api/hostsets/host_set_test.go +++ b/internal/tests/api/hostsets/host_set_test.go @@ -127,7 +127,8 @@ func TestCrud(t *testing.T) { client.SetToken(token.Token) _, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(token.UserId)) - hc, err := hostcatalogs.NewClient(client).Create(tc.Context(), "static", proj.GetPublicId()) + hcClient := hostcatalogs.NewClient(client) + hc, err := hcClient.Create(tc.Context(), "static", proj.GetPublicId()) require.NoError(err) require.NotNil(hc) @@ -164,6 +165,25 @@ func TestCrud(t *testing.T) { apiErr := api.AsServerError(err) require.NotNil(apiErr) assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode()) + + // Plugin Sets + c, err := hcClient.Create(tc.Context(), "plugin", proj.GetPublicId(), hostcatalogs.WithName("pluginfoo"), hostcatalogs.WithPluginId("pl_1234567890"), + hostcatalogs.WithAttributes(map[string]interface{}{"foo": "bar"})) + require.NoError(err) + + h, err = hClient.Create(tc.Context(), c.Item.Id, hostsets.WithName("foo")) + checkHost(t, "create", h.Item, err, "foo", 1) + + h, err = hClient.Read(tc.Context(), h.Item.Id) + checkHost(t, "read", h.Item, err, "foo", 1) + + _, err = hClient.Delete(tc.Context(), h.Item.Id) + assert.NoError(err) + _, err = hClient.Delete(tc.Context(), h.Item.Id) + require.Error(err) + apiErr = api.AsServerError(err) + require.NotNil(apiErr) + assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode()) } // TODO: Get better coverage for expected errors and error formats.