Added a new Endpoint struct at the top level host package level to capture host, set, and address chosen. Temporarily add a host_plugin_host table to capture the hosts returned by the plugins. This ensures the datawarehouse is able to query the table to decide what host was chosen. The view used to update the datawarehouse dimension table provides rows with host set ids and hosts which are not in that host set. This is ok since the function which updates the dimension table provides the host set and host id chosen so that relationship doesn't need to be captured in the view itself. Co-authored-by: Jeff Mitchell <jeffrey.mitchell@gmail.com>
282 lines
9.9 KiB
Go
282 lines
9.9 KiB
Go
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"github.com/hashicorp/boundary/internal/host"
|
|
"github.com/hashicorp/boundary/internal/kms"
|
|
"github.com/hashicorp/boundary/internal/oplog"
|
|
hostplugin "github.com/hashicorp/boundary/internal/plugin/host"
|
|
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
|
plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
)
|
|
|
|
// CreateCatalog inserts c into the repository and returns a new
|
|
// HostCatalog containing the catalog's PublicId. c must contain a valid
|
|
// ScopeID and PluginID. c must not contain a PublicId. The PublicId is
|
|
// generated and assigned by this method. opt is ignored.
|
|
//
|
|
// c.Secret, c.Name and c.Description are optional. If c.Name is set, it must be
|
|
// unique within c.ScopeID. If c.Secret is set, it will be stored encrypted but
|
|
// not included in the returned *HostCatalog.
|
|
//
|
|
// Both c.CreateTime and c.UpdateTime are ignored.
|
|
func (r *Repository) CreateCatalog(ctx context.Context, c *HostCatalog, _ ...Option) (*HostCatalog, *hostplugin.Plugin, error) {
|
|
const op = "plugin.(Repository).CreateCatalog"
|
|
if c == nil {
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "nil HostCatalog")
|
|
}
|
|
if c.HostCatalog == nil {
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "nil embedded HostCatalog")
|
|
}
|
|
if c.ScopeId == "" {
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "no scope id")
|
|
}
|
|
if c.PublicId != "" {
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty")
|
|
}
|
|
if c.PluginId == "" {
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "no plugin id")
|
|
}
|
|
if c.Attributes == nil {
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "nil attributes")
|
|
}
|
|
c = c.clone()
|
|
id, err := newHostCatalogId(ctx)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
c.PublicId = id
|
|
|
|
plgClient, ok := r.plugins[c.GetPluginId()]
|
|
if !ok || plgClient == nil {
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("plugin %q not available", c.GetPluginId()))
|
|
}
|
|
plgHc, err := toPluginCatalog(ctx, c)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
plgResp, err := plgClient.OnCreateCatalog(ctx, &plgpb.OnCreateCatalogRequest{Catalog: plgHc})
|
|
if err != nil {
|
|
if status.Code(err) != codes.Unimplemented {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
|
|
var hcSecret *HostCatalogSecret
|
|
if plgResp != nil && plgResp.GetPersisted().GetSecrets() != nil {
|
|
hcSecret, err = newHostCatalogSecret(ctx, id, plgResp.GetPersisted().GetSecrets())
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
dbWrapper, err := r.kms.GetWrapper(ctx, c.ScopeId, kms.KeyPurposeDatabase)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get db wrapper"))
|
|
}
|
|
if err := hcSecret.encrypt(ctx, dbWrapper); err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, c.ScopeId, kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
|
|
}
|
|
|
|
var newHostCatalog *HostCatalog
|
|
_, err = r.writer.DoTx(
|
|
ctx,
|
|
db.StdRetryCnt,
|
|
db.ExpBackoff{},
|
|
func(_ db.Reader, w db.Writer) error {
|
|
msgs := make([]*oplog.Message, 0, 3)
|
|
ticket, err := w.GetTicket(c)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
|
|
}
|
|
|
|
newHostCatalog = c.clone()
|
|
var cOplogMsg oplog.Message
|
|
if err := w.Create(ctx, newHostCatalog, db.NewOplogMsg(&cOplogMsg)); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
msgs = append(msgs, &cOplogMsg)
|
|
|
|
if hcSecret != nil {
|
|
newSecret := hcSecret.clone()
|
|
q, v := newSecret.insertQuery()
|
|
rows, err := w.Exec(ctx, q, v)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if rows > 1 {
|
|
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 catalog secret would have been created")
|
|
}
|
|
msgs = append(msgs, newSecret.oplogMessage(db.CreateOp))
|
|
}
|
|
|
|
metadata := c.oplog(oplog.OpType_OP_TYPE_CREATE)
|
|
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
|
|
}
|
|
return nil
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
if errors.IsUniqueError(err) {
|
|
return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in scope: %s: name %s already exists", c.ScopeId, c.Name)))
|
|
}
|
|
return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in scope: %s", c.ScopeId)))
|
|
}
|
|
plg, err := r.getPlugin(ctx, newHostCatalog.GetPluginId())
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
return newHostCatalog, plg, nil
|
|
}
|
|
|
|
// LookupCatalog returns the HostCatalog for id. Returns nil, nil if no
|
|
// HostCatalog is found for id.
|
|
func (r *Repository) LookupCatalog(ctx context.Context, id string, _ ...Option) (*HostCatalog, *hostplugin.Plugin, error) {
|
|
const op = "plugin.(Repository).LookupCatalog"
|
|
if id == "" {
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "no public id")
|
|
}
|
|
c, err := r.getCatalog(ctx, id)
|
|
if errors.IsNotFoundError(err) {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for: %s", id)))
|
|
}
|
|
plg, err := r.getPlugin(ctx, c.GetPluginId())
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
return c, plg, nil
|
|
}
|
|
|
|
// ListCatalogs returns a slice of HostCatalogs for the scope IDs. WithLimit is the only option supported.
|
|
func (r *Repository) ListCatalogs(ctx context.Context, scopeIds []string, opt ...host.Option) ([]*HostCatalog, []*hostplugin.Plugin, error) {
|
|
const op = "plugin.(Repository).ListCatalogs"
|
|
if len(scopeIds) == 0 {
|
|
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "no scope id")
|
|
}
|
|
opts, err := host.GetOpts(opt...)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
limit := r.defaultLimit
|
|
if opts.WithLimit != 0 {
|
|
// non-zero signals an override of the default limit for the repo.
|
|
limit = opts.WithLimit
|
|
}
|
|
var hostCatalogs []*HostCatalog
|
|
if err := r.reader.SearchWhere(ctx, &hostCatalogs, "scope_id in (?)", []interface{}{scopeIds}, db.WithLimit(limit)); err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
plgIds := make([]string, 0, len(hostCatalogs))
|
|
for _, c := range hostCatalogs {
|
|
plgIds = append(plgIds, c.PluginId)
|
|
}
|
|
var plgs []*hostplugin.Plugin
|
|
if err := r.reader.SearchWhere(ctx, &plgs, "public_id in (?)", []interface{}{plgIds}); err != nil {
|
|
return nil, nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
return hostCatalogs, plgs, 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) {
|
|
const op = "plugin.(Repository).getCatalog"
|
|
c := allocHostCatalog()
|
|
c.PublicId = id
|
|
if err := r.reader.LookupByPublicId(ctx, c); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for: %s", id)))
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// getPersistedDataForCatalog returns the persisted data for a catalog if
|
|
// present. c must have a valid Public Id and Scope Id set.
|
|
// TODO: consider merging the functions for getting catalog and persisted data into a view.
|
|
func (r *Repository) getPersistedDataForCatalog(ctx context.Context, c *HostCatalog) (*plgpb.HostCatalogPersisted, error) {
|
|
const op = "plugin.(Repository).getPersistedDataForCatalog"
|
|
if c.PublicId == "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "empty public id")
|
|
}
|
|
if c.ScopeId == "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "empty scope id")
|
|
}
|
|
cSecret := allocHostCatalogSecret()
|
|
if err := r.reader.LookupWhere(ctx, cSecret, "catalog_id=?", c.GetPublicId()); err != nil {
|
|
if errors.IsNotFoundError(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
if cSecret == nil {
|
|
return nil, nil
|
|
}
|
|
dbWrapper, err := r.kms.GetWrapper(ctx, c.ScopeId, kms.KeyPurposeDatabase)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get db wrapper"))
|
|
}
|
|
if err := cSecret.decrypt(ctx, dbWrapper); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
secrets := &structpb.Struct{}
|
|
if err := proto.Unmarshal(cSecret.GetSecret(), secrets); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unmarshaling secret"))
|
|
}
|
|
return &plgpb.HostCatalogPersisted{Secrets: secrets}, nil
|
|
}
|
|
|
|
func (r *Repository) getPlugin(ctx context.Context, plgId string) (*hostplugin.Plugin, error) {
|
|
const op = "plugin.(Repository).getPlugin"
|
|
if plgId == "" {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "no plugin id")
|
|
}
|
|
plg := hostplugin.NewPlugin()
|
|
plg.PublicId = plgId
|
|
if err := r.reader.LookupByPublicId(ctx, plg); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get host plugin with id %q", plgId)))
|
|
}
|
|
return plg, nil
|
|
}
|
|
|
|
// toPluginCatalog returns a host catalog, with it's secret if available, in the format expected
|
|
// by the host plugin system.
|
|
func toPluginCatalog(ctx context.Context, in *HostCatalog) (*pb.HostCatalog, error) {
|
|
const op = "plugin.toPluginCatalog"
|
|
if in == nil {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil storage plugin")
|
|
}
|
|
hc := &pb.HostCatalog{
|
|
Id: in.GetPublicId(),
|
|
ScopeId: in.GetScopeId(),
|
|
}
|
|
if in.GetAttributes() != nil {
|
|
attrs := &structpb.Struct{}
|
|
if err := proto.Unmarshal(in.GetAttributes(), attrs); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to unmarshal attributes"))
|
|
}
|
|
hc.Attributes = attrs
|
|
}
|
|
if in.secrets != nil {
|
|
hc.Secrets = in.secrets
|
|
}
|
|
return hc, nil
|
|
}
|