2021-11-30 14:37:06 -08:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
2021-09-09 11:15:14 -07:00
package plugin
import (
"context"
"fmt"
2021-11-05 10:43:58 -07:00
"strings"
2021-09-09 11:15:14 -07:00
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
2023-09-12 08:15:34 -04:00
"github.com/hashicorp/boundary/internal/event"
2021-09-09 11:15:14 -07:00
"github.com/hashicorp/boundary/internal/kms"
2021-11-05 10:43:58 -07:00
"github.com/hashicorp/boundary/internal/libs/patchstruct"
2021-09-09 11:15:14 -07:00
"github.com/hashicorp/boundary/internal/oplog"
2023-04-24 19:27:41 -04:00
plg "github.com/hashicorp/boundary/internal/plugin"
2022-01-11 15:55:26 -08:00
"github.com/hashicorp/boundary/internal/scheduler"
2022-08-18 15:39:30 -04:00
"github.com/hashicorp/boundary/internal/util"
2021-09-21 09:18:49 -07:00
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
2021-10-28 12:46:42 -07:00
pbset "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
2024-08-21 18:19:34 +01:00
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/plugins"
2021-09-21 09:18:49 -07:00
plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin"
2021-11-16 18:54:38 -05:00
"github.com/mr-tron/base58"
2021-09-21 09:18:49 -07:00
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
2021-11-05 10:43:58 -07:00
"google.golang.org/protobuf/types/known/wrapperspb"
2021-09-09 11:15:14 -07:00
)
2022-08-18 15:39:30 -04:00
// normalizeCatalogAttributes allows a plugin to normalize attributes before
// they are saved
func normalizeCatalogAttributes ( ctx context . Context , plgClient plgpb . HostPluginServiceClient , plgHc * pb . HostCatalog ) error {
const op = "plugin.(Repository).normalizeCatalogAttributes"
switch {
case util . IsNil ( plgClient ):
return errors . New ( ctx , errors . InvalidParameter , op , "plugin client is nil" )
case plgHc == nil :
return errors . New ( ctx , errors . InvalidParameter , op , "host catalog is nil" )
2024-08-21 18:19:34 +01:00
case plgHc . GetWorkerFilter (). GetValue () != "" && plgHc . GetPlugin () == nil :
return errors . New ( ctx , errors . InvalidParameter , op , "plugin data is not available on host catalog with worker filter" )
2022-08-18 15:39:30 -04:00
case plgHc . GetAttributes () == nil :
return nil
}
ret , err := plgClient . NormalizeCatalogData ( ctx , & plgpb . NormalizeCatalogDataRequest {
Attributes : plgHc . GetAttributes (),
2024-08-21 18:19:34 +01:00
Plugin : plgHc . GetPlugin (),
2022-08-18 15:39:30 -04:00
})
switch {
case err == nil :
if ret . Attributes != nil {
plgHc . Attrs = & pb . HostCatalog_Attributes {
Attributes : ret . Attributes ,
}
}
case status . Code ( err ) == codes . Unimplemented :
// Do nothing
default :
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "error asking plugin to normalize catalog data" ))
}
return nil
}
2021-09-09 11:15:14 -07:00
// CreateCatalog inserts c into the repository and returns a new
// HostCatalog containing the catalog's PublicId. c must contain a valid
2022-07-20 16:24:46 -07:00
// ProjectID and PluginID. c must not contain a PublicId. The PublicId is
2021-09-09 11:15:14 -07:00
// 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
2022-07-20 16:24:46 -07:00
// unique within c.ProjectId. If c.Secret is set, it will be stored encrypted but
2021-09-09 11:15:14 -07:00
// not included in the returned *HostCatalog.
//
// Both c.CreateTime and c.UpdateTime are ignored.
2023-04-24 19:27:41 -04:00
func ( r * Repository ) CreateCatalog ( ctx context . Context , c * HostCatalog , _ ... Option ) ( * HostCatalog , * plg . Plugin , error ) {
2021-09-09 11:15:14 -07:00
const op = "plugin.(Repository).CreateCatalog"
if c == nil {
2021-09-24 13:16:13 -07:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "nil HostCatalog" )
2021-09-09 11:15:14 -07:00
}
if c . HostCatalog == nil {
2021-09-24 13:16:13 -07:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "nil embedded HostCatalog" )
2021-09-09 11:15:14 -07:00
}
2022-07-20 16:24:46 -07:00
if c . ProjectId == "" {
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "no project id" )
2021-09-09 11:15:14 -07:00
}
if c . PublicId != "" {
2021-09-24 13:16:13 -07:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "public id not empty" )
2021-09-09 11:15:14 -07:00
}
if c . PluginId == "" {
2021-09-24 13:16:13 -07:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "no plugin id" )
2021-09-09 11:15:14 -07:00
}
if c . Attributes == nil {
2021-09-24 13:16:13 -07:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "nil attributes" )
2021-09-09 11:15:14 -07:00
}
c = c . clone ()
2021-09-24 13:16:13 -07:00
id , err := newHostCatalogId ( ctx )
2021-09-09 11:15:14 -07:00
if err != nil {
2021-09-24 13:16:13 -07:00
return nil , nil , errors . Wrap ( ctx , err , op )
2021-09-09 11:15:14 -07:00
}
c . PublicId = id
2021-11-09 14:42:48 -07:00
// Use PatchBytes' functionality that does not add keys where the values
// are nil to the resulting struct since we do not want to store nil valued
// attributes.
c . Attributes , err = patchstruct . PatchBytes ([] byte {}, c . Attributes )
if err != nil {
return nil , nil , errors . Wrap ( ctx , err , op )
}
2021-11-16 18:54:38 -05:00
// If secrets were passed in, HMAC 'em
if c . Secrets != nil && len ( c . Secrets . GetFields ()) > 0 {
2022-07-20 16:24:46 -07:00
databaseWrapper , err := r . kms . GetWrapper ( ctx , c . ProjectId , kms . KeyPurposeDatabase )
2021-11-16 18:54:38 -05:00
if err != nil {
return nil , nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get database wrapper" ))
}
if err := c . hmacSecrets ( ctx , databaseWrapper ); err != nil {
return nil , nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "error hmac'ing passed-in secrets" ))
}
}
2024-08-21 18:19:34 +01:00
plg , err := r . getPlugin ( ctx , c . GetPluginId ())
if err != nil {
return nil , nil , errors . Wrap ( ctx , err , op )
}
plgHc , err := toPluginCatalog ( ctx , c , plg )
2021-09-09 11:15:14 -07:00
if err != nil {
2021-09-24 13:16:13 -07:00
return nil , nil , errors . Wrap ( ctx , err , op )
2021-09-09 11:15:14 -07:00
}
2024-09-04 15:05:47 +01:00
plgClient , err := pluginClientFactoryFn ( ctx , plgHc , r . plugins )
if err != nil {
return nil , nil , errors . Wrap ( ctx , err , op )
2021-10-28 12:46:42 -07:00
}
2021-09-09 11:15:14 -07:00
2022-08-18 15:39:30 -04:00
if plgHc . GetAttributes () != nil {
if err := normalizeCatalogAttributes ( ctx , plgClient , plgHc ); err != nil {
return nil , nil , errors . Wrap ( ctx , err , op )
}
2023-06-20 13:38:52 -04:00
if c . Attributes , err = proto . Marshal ( plgHc . GetAttributes ()); err != nil {
return nil , nil , errors . Wrap ( ctx , err , op )
}
2022-08-18 15:39:30 -04:00
}
2022-07-20 16:24:46 -07:00
oplogWrapper , err := r . kms . GetWrapper ( ctx , c . ProjectId , kms . KeyPurposeOplog )
2021-09-21 09:18:49 -07:00
if err != nil {
2021-09-24 13:16:13 -07:00
return nil , nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get oplog wrapper" ))
2021-09-21 09:18:49 -07:00
}
2021-11-10 09:04:29 -07:00
// If the call to the plugin succeeded, we do not want to call it again if
// the transaction failed and is being retried.
var pluginCalledSuccessfully bool
var plgResp * plgpb . OnCreateCatalogResponse
2021-09-09 11:15:14 -07:00
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 )
2022-01-05 14:38:38 -05:00
ticket , err := w . GetTicket ( ctx , c )
2021-09-09 11:15:14 -07:00
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 )
2021-11-10 09:04:29 -07:00
if ! pluginCalledSuccessfully {
plgResp , err = plgClient . OnCreateCatalog ( ctx , & plgpb . OnCreateCatalogRequest { Catalog : plgHc })
if err != nil {
if status . Code ( err ) != codes . Unimplemented {
return errors . Wrap ( ctx , err , op )
}
}
pluginCalledSuccessfully = true
}
2024-09-12 16:50:49 +01:00
if len ( plgResp . GetPersisted (). GetSecrets (). GetFields ()) > 0 {
2021-11-10 09:04:29 -07:00
hcSecret , err := newHostCatalogSecret ( ctx , id , plgResp . GetPersisted (). GetSecrets ())
if err != nil {
return errors . Wrap ( ctx , err , op )
}
2022-07-20 16:24:46 -07:00
dbWrapper , err := r . kms . GetWrapper ( ctx , c . ProjectId , kms . KeyPurposeDatabase )
2021-09-09 11:15:14 -07:00
if err != nil {
2021-11-10 09:04:29 -07:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get db wrapper" ))
}
if err := hcSecret . encrypt ( ctx , dbWrapper ); err != nil {
2021-09-09 11:15:14 -07:00
return errors . Wrap ( ctx , err , op )
}
2021-11-10 09:04:29 -07:00
if hcSecret != nil {
newSecret := hcSecret . clone ()
2021-12-07 12:38:34 -08:00
var sOplogMsg oplog . Message
if err := w . Create ( ctx , newSecret , db . NewOplogMsg ( & sOplogMsg )); err != nil {
2021-11-10 09:04:29 -07:00
return errors . Wrap ( ctx , err , op )
}
2021-12-07 12:38:34 -08:00
msgs = append ( msgs , & sOplogMsg )
2021-09-09 11:15:14 -07:00
}
}
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 ) {
2022-07-20 16:24:46 -07:00
return nil , nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "in project: %s: name %s already exists" , c . ProjectId , c . Name )))
2021-09-09 11:15:14 -07:00
}
2022-07-20 16:24:46 -07:00
return nil , nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "in project: %s" , c . ProjectId )))
2021-09-09 11:15:14 -07:00
}
2021-09-24 13:16:13 -07:00
return newHostCatalog , plg , nil
2021-09-09 11:15:14 -07:00
}
2021-11-05 10:43:58 -07:00
// UpdateCatalog updates the repository entry for c.PublicId with the
// values in c for the fields listed in fieldMask. It returns a new
// HostCatalog containing the updated values and a count of the number of
// records updated. c is not changed.
//
// c must contain a valid PublicId. c.Name, c.Description, and
// c.Attributes can be updated; if c.Secrets is present, its contents
// are sent to the plugin (along with any other changes, see below)
// before the update is sent to the database.
//
// An attribute of c will be set to NULL in the database if the
// attribute in c is the zero value and it is included in fieldMask.
// Note that this does not apply to c.Attributes - a null
// c.Attributes is a no-op for modifications. Rather, if fields need
// to be reset, its field in c.Attributes should individually set to
// null.
//
// Updates are sent to OnUpdateCatalog with a full copy of both the
// current catalog, and the state of the new catalog should it be
// updated, along with any secrets included in the new request. This
// request may alter the returned persisted state. Update of the
// record in the database is aborted if this call fails.
2023-04-24 19:27:41 -04:00
func ( r * Repository ) UpdateCatalog ( ctx context . Context , c * HostCatalog , version uint32 , fieldMask [] string , _ ... Option ) ( * HostCatalog , * plg . Plugin , int , error ) {
2021-11-05 10:43:58 -07:00
const op = "plugin.(Repository).UpdateCatalog"
if c == nil {
return nil , nil , db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "nil HostCatalog" )
}
if c . HostCatalog == nil {
return nil , nil , db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "nil embedded HostCatalog" )
}
if c . PublicId == "" {
return nil , nil , db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "no public id" )
}
2022-07-20 16:24:46 -07:00
if c . ProjectId == "" {
return nil , nil , db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "no project id" )
2021-11-05 10:43:58 -07:00
}
if len ( fieldMask ) == 0 {
return nil , nil , db . NoRowsAffected , errors . New ( ctx , errors . EmptyFieldMask , op , "empty field mask" )
}
// Get the old catalog first. We patch the record first before
// sending it to the DB for updating so that we can run on
// OnUpdateCatalog. Note that the field masks are still used for
// updating.
currentCatalog , currentCatalogPersisted , err := r . getCatalog ( ctx , c . PublicId )
if err != nil {
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "error looking up catalog with id %q" , c . PublicId )))
}
if currentCatalog == nil {
return nil , nil , db . NoRowsAffected , errors . New ( ctx , errors . RecordNotFound , op , fmt . Sprintf ( "catalog with id %q not found" , c . PublicId ))
}
// Assert the version of the current catalog to make sure we're
// updating the correct one.
if currentCatalog . GetVersion () != version {
return nil , nil , db . NoRowsAffected , errors . New ( ctx , errors . VersionMismatch , op , fmt . Sprintf ( "catalog version mismatch, want=%d, got=%d" , currentCatalog . GetVersion (), version ))
}
// Clone the catalog so that we can set fields.
newCatalog := currentCatalog . clone ()
var updateAttributes bool
var dbMask , nullFields [] string
2021-11-16 18:54:38 -05:00
var alreadySetSecrets bool
2021-11-05 10:43:58 -07:00
for _ , f := range fieldMask {
switch {
case strings . EqualFold ( "name" , f ) && c . Name == "" :
nullFields = append ( nullFields , "name" )
newCatalog . Name = c . Name
case strings . EqualFold ( "name" , f ) && c . Name != "" :
dbMask = append ( dbMask , "name" )
newCatalog . Name = c . Name
case strings . EqualFold ( "description" , f ) && c . Description == "" :
nullFields = append ( nullFields , "description" )
newCatalog . Description = c . Description
case strings . EqualFold ( "description" , f ) && c . Description != "" :
dbMask = append ( dbMask , "description" )
newCatalog . Description = c . Description
case strings . EqualFold ( "attributes" , strings . Split ( f , "." )[ 0 ]):
// Flag attributes for updating. While multiple masks may be
// sent, we only need to do this once.
updateAttributes = true
case strings . EqualFold ( "secrets" , strings . Split ( f , "." )[ 0 ]):
2021-11-16 18:54:38 -05:00
if alreadySetSecrets {
continue
}
alreadySetSecrets = true
2021-11-05 10:43:58 -07:00
// While in a similar format, secrets are passed along
// wholesale (for the time being). Don't append this mask
2021-11-12 09:46:40 -07:00
// field, as secrets do not have a database entry.
2021-11-05 10:43:58 -07:00
newCatalog . Secrets = c . Secrets
2021-11-16 18:54:38 -05:00
switch {
case newCatalog . Secrets == nil ,
len ( newCatalog . Secrets . GetFields ()) == 0 :
nullFields = append ( nullFields , "SecretsHmac" )
default :
// If secrets were passed in, HMAC 'em
2022-07-20 16:24:46 -07:00
databaseWrapper , err := r . kms . GetWrapper ( ctx , c . ProjectId , kms . KeyPurposeDatabase )
2021-11-16 18:54:38 -05:00
if err != nil {
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get database wrapper" ))
}
if err := newCatalog . hmacSecrets ( ctx , databaseWrapper ); err != nil {
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( "error hmac'ing passed-in secrets" ))
}
dbMask = append ( dbMask , "SecretsHmac" )
}
2024-07-26 12:50:55 +01:00
case strings . EqualFold ( "WorkerFilter" , f ) && c . WorkerFilter == "" :
nullFields = append ( nullFields , "WorkerFilter" )
newCatalog . WorkerFilter = c . WorkerFilter
case strings . EqualFold ( "WorkerFilter" , f ) && c . WorkerFilter != "" :
dbMask = append ( dbMask , "WorkerFilter" )
newCatalog . WorkerFilter = c . WorkerFilter
2021-11-05 10:43:58 -07:00
default :
return nil , nil , db . NoRowsAffected , errors . New ( ctx , errors . InvalidFieldMask , op , fmt . Sprintf ( "invalid field mask: %s" , f ))
}
}
2021-11-30 14:37:06 -08:00
var needSetSync , runSyncJob bool
2021-11-26 13:58:19 -08:00
if updateAttributes {
2021-11-05 10:43:58 -07:00
dbMask = append ( dbMask , "attributes" )
newCatalog . Attributes , err = patchstruct . PatchBytes ( newCatalog . Attributes , c . Attributes )
if err != nil {
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( "error in catalog attribute JSON" ))
}
2021-11-30 14:37:06 -08:00
// Flag for host sets under this catalog to be synced.
needSetSync = true
2021-11-05 10:43:58 -07:00
}
2024-08-21 18:19:34 +01:00
// Get the plugin for the host catalog - this is to return it back after the
// update is complete, as well as forwarding it to the actual plugin given
// that it is necessary if we're using a worker filter. Fetch it here so
// that if there's an integrity error, we don't call the plugin.
2021-11-05 10:43:58 -07:00
plg , err := r . getPlugin ( ctx , currentCatalog . GetPluginId ())
if err != nil {
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op )
}
// Convert the catalog values to API protobuf values, which is what
// we use for the plugin hook calls.
2024-08-21 18:19:34 +01:00
currPlgHc , err := toPluginCatalog ( ctx , currentCatalog , plg )
2021-11-05 10:43:58 -07:00
if err != nil {
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op )
}
2024-08-21 18:19:34 +01:00
newPlgHc , err := toPluginCatalog ( ctx , newCatalog , plg )
2021-11-05 10:43:58 -07:00
if err != nil {
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op )
}
2024-09-04 15:05:47 +01:00
plgClient , err := pluginClientFactoryFn ( ctx , newPlgHc , r . plugins )
if err != nil {
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op )
}
2022-08-18 15:39:30 -04:00
if updateAttributes {
if newPlgHc . GetAttributes () != nil {
if err := normalizeCatalogAttributes ( ctx , plgClient , newPlgHc ); err != nil {
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op )
}
if newCatalog . Attributes , err = proto . Marshal ( newPlgHc . GetAttributes ()); err != nil {
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op )
}
}
}
2022-07-20 16:24:46 -07:00
dbWrapper , err := r . kms . GetWrapper ( ctx , newCatalog . ProjectId , kms . KeyPurposeDatabase )
2021-11-05 10:43:58 -07:00
if err != nil {
2021-11-10 09:04:29 -07:00
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get db wrapper" ))
2021-11-05 10:43:58 -07:00
}
// Get the oplog.
2022-07-20 16:24:46 -07:00
oplogWrapper , err := r . kms . GetWrapper ( ctx , newCatalog . ProjectId , kms . KeyPurposeOplog )
2021-11-05 10:43:58 -07:00
if err != nil {
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get oplog wrapper" ))
}
2021-11-10 09:04:29 -07:00
var pluginCalledSuccessfully bool
var plgResp * plgpb . OnUpdateCatalogResponse
2021-11-05 10:43:58 -07:00
var recordUpdated bool
var returnedCatalog * HostCatalog
_ , err = r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( _ db . Reader , w db . Writer ) error {
msgs := make ([] * oplog . Message , 0 , 3 )
2022-01-05 14:38:38 -05:00
ticket , err := w . GetTicket ( ctx , newCatalog )
2021-11-05 10:43:58 -07:00
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get ticket" ))
}
if len ( dbMask ) != 0 || len ( nullFields ) != 0 {
returnedCatalog = newCatalog . clone ()
var cOplogMsg oplog . Message
catalogsUpdated , err := w . Update (
ctx ,
returnedCatalog ,
dbMask ,
nullFields ,
db . NewOplogMsg ( & cOplogMsg ),
db . WithVersion ( & version ),
)
if err != nil {
return errors . Wrap ( ctx , err , op )
}
if catalogsUpdated != 1 {
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "expected 1 catalog to be deleted, got %d" , catalogsUpdated ))
}
msgs = append ( msgs , & cOplogMsg )
recordUpdated = true
} else {
// Returned catalog needs to be the old copy, as no fields in the
// catalog itself are being updated (note: secrets may still be
// updated).
returnedCatalog = currentCatalog . clone ()
}
2021-11-10 09:04:29 -07:00
if ! pluginCalledSuccessfully {
plgResp , err = plgClient . OnUpdateCatalog ( ctx , & plgpb . OnUpdateCatalogRequest {
CurrentCatalog : currPlgHc ,
NewCatalog : newPlgHc ,
Persisted : currentCatalogPersisted ,
})
if err != nil {
if status . Code ( err ) != codes . Unimplemented {
return errors . Wrap ( ctx , err , op )
}
}
pluginCalledSuccessfully = true
}
var updatedPersisted bool
if plgResp != nil && plgResp . GetPersisted (). GetSecrets () != nil {
if len ( plgResp . GetPersisted (). GetSecrets (). GetFields ()) == 0 {
2024-09-12 16:50:49 +01:00
// Flag the secret to be deleted if it exists.
2021-11-10 09:04:29 -07:00
hcSecret , err := newHostCatalogSecret ( ctx , currentCatalog . GetPublicId (), plgResp . GetPersisted (). GetSecrets ())
if err != nil {
return errors . Wrap ( ctx , err , op )
}
2021-11-05 10:43:58 -07:00
// We didn't set/encrypt the persisted data because there was
// none returned. Just delete the entry.
deletedSecret := hcSecret . clone ()
2021-12-07 12:38:34 -08:00
var sOplogMsg oplog . Message
secretsDeleted , err := w . Delete ( ctx , deletedSecret , db . NewOplogMsg ( & sOplogMsg ))
2021-11-05 10:43:58 -07:00
if err != nil {
return errors . Wrap ( ctx , err , op )
}
2024-09-12 16:50:49 +01:00
if secretsDeleted > 1 {
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "expected 0 or 1 catalog secret to be deleted, got %d" , secretsDeleted ))
}
if secretsDeleted == 1 {
updatedPersisted = true
msgs = append ( msgs , & sOplogMsg )
2021-11-05 10:43:58 -07:00
}
} else {
2021-11-10 09:04:29 -07:00
hcSecret , err := newHostCatalogSecret ( ctx , currentCatalog . GetPublicId (), plgResp . GetPersisted (). GetSecrets ())
if err != nil {
return errors . Wrap ( ctx , err , op )
}
if err := hcSecret . encrypt ( ctx , dbWrapper ); err != nil {
return errors . Wrap ( ctx , err , op )
}
2021-11-05 10:43:58 -07:00
// Update the secrets.
updatedSecret := hcSecret . clone ()
2021-12-07 12:38:34 -08:00
var sOplogMsg oplog . Message
if err := w . Create (
ctx ,
updatedSecret ,
db . WithOnConflict ( & db . OnConflict {
Target : db . Columns { "catalog_id" },
Action : db . SetColumns ([] string { "secret" , "key_id" }),
}),
db . NewOplogMsg ( & sOplogMsg ),
); err != nil {
2021-11-05 10:43:58 -07:00
return errors . Wrap ( ctx , err , op )
}
2021-11-10 09:04:29 -07:00
updatedPersisted = true
2021-12-07 12:38:34 -08:00
msgs = append ( msgs , & sOplogMsg )
2021-11-05 10:43:58 -07:00
}
2021-11-10 09:04:29 -07:00
}
2021-11-05 10:43:58 -07:00
2021-11-10 09:04:29 -07:00
if ! recordUpdated && updatedPersisted {
// we only updated secrets, so we need to increment the
// version of the host catalog manually.
returnedCatalog = newCatalog . clone ()
returnedCatalog . Version = uint32 ( version ) + 1
var cOplogMsg oplog . Message
catalogsUpdated , err := w . Update (
ctx ,
returnedCatalog ,
[] string { "version" },
[] string {},
db . NewOplogMsg ( & cOplogMsg ),
db . WithVersion ( & version ),
)
if err != nil {
return errors . Wrap ( ctx , err , op )
2021-11-05 10:43:58 -07:00
}
2021-11-10 09:04:29 -07:00
if catalogsUpdated != 1 {
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "expected 1 catalog to be updated, got %d" , catalogsUpdated ))
}
msgs = append ( msgs , & cOplogMsg )
recordUpdated = true
2021-11-05 10:43:58 -07:00
}
2021-11-30 14:37:06 -08:00
if needSetSync {
// We also need to mark all host sets in this catalog to be
// synced as well.
setsForCatalog , _ , err := r . getSets ( ctx , "" , returnedCatalog . PublicId )
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get sets for host catalog" ))
}
for _ , set := range setsForCatalog {
newSet := set . clone ()
newSet . NeedSync = true
var msg oplog . Message
n , err := w . Update ( ctx , newSet , [] string { "NeedSync" }, [] string {}, db . NewOplogMsg ( & msg ))
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to update host set" ))
}
if n > 1 {
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "expected no more than 1 host set to be updated while flagging host set id %q for synchronization, got %d" , newSet . PublicId , n ))
}
msgs = append ( msgs , & msg )
runSyncJob = true
}
}
2021-11-05 10:43:58 -07:00
if len ( msgs ) != 0 {
metadata := newCatalog . oplog ( oplog . OpType_OP_TYPE_UPDATE )
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 , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "in %s: name %s already exists" , newCatalog . PublicId , newCatalog . Name )))
}
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "in %s" , newCatalog . PublicId )))
}
2021-11-30 14:37:06 -08:00
if runSyncJob {
2022-01-11 15:55:26 -08:00
_ = r . scheduler . UpdateJobNextRunInAtLeast ( ctx , setSyncJobName , 0 , scheduler . WithRunNow ( true ))
2021-11-30 14:37:06 -08:00
}
2021-11-12 09:46:40 -07:00
// Even if we didn't update any records, if we were able to find the record
2021-11-30 14:37:06 -08:00
// with the appropriate version, returning 1 row updated is consistent with
2021-11-12 09:46:40 -07:00
// static's update catalog behavior.
numUpdated := 1
2021-11-05 10:43:58 -07:00
return returnedCatalog , plg , numUpdated , nil
}
2021-09-09 11:15:14 -07:00
// LookupCatalog returns the HostCatalog for id. Returns nil, nil if no
// HostCatalog is found for id.
2023-04-24 19:27:41 -04:00
func ( r * Repository ) LookupCatalog ( ctx context . Context , id string , _ ... Option ) ( * HostCatalog , * plg . Plugin , error ) {
2021-09-09 11:15:14 -07:00
const op = "plugin.(Repository).LookupCatalog"
if id == "" {
2021-09-24 13:16:13 -07:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "no public id" )
2021-09-09 11:15:14 -07:00
}
2021-10-28 12:46:42 -07:00
c , _ , err := r . getCatalog ( ctx , id )
2021-09-26 08:17:21 -07:00
if errors . IsNotFoundError ( err ) {
return nil , nil , nil
}
2021-09-21 09:18:49 -07:00
if err != nil {
2021-09-24 13:16:13 -07:00
return nil , nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "failed for: %s" , id )))
2021-09-09 11:15:14 -07:00
}
2021-09-24 13:16:13 -07:00
plg , err := r . getPlugin ( ctx , c . GetPluginId ())
if err != nil {
return nil , nil , errors . Wrap ( ctx , err , op )
}
return c , plg , nil
2021-09-09 11:15:14 -07:00
}
2021-10-25 09:37:05 -07:00
// 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" )
}
2021-10-28 12:46:42 -07:00
c , p , err := r . getCatalog ( ctx , id )
2021-10-25 09:37:05 -07:00
if err != nil && ! errors . IsNotFoundError ( err ) {
return db . NoRowsAffected , errors . Wrap ( ctx , err , op )
}
if c == nil {
return db . NoRowsAffected , nil
}
2024-08-21 18:19:34 +01:00
plg , err := r . getPlugin ( ctx , c . GetPluginId ())
if err != nil {
return db . NoRowsAffected , errors . Wrap ( ctx , err , op )
}
plgHc , err := toPluginCatalog ( ctx , c , plg )
2021-10-25 09:37:05 -07:00
if err != nil {
return db . NoRowsAffected , errors . Wrap ( ctx , err , op )
}
2021-10-28 12:46:42 -07:00
sets , _ , err := r . getSets ( ctx , "" , c . GetPublicId ())
2021-11-04 17:09:32 -07:00
if err != nil {
return db . NoRowsAffected , errors . Wrap ( ctx , err , op )
}
2021-10-28 12:46:42 -07:00
var plgSets [] * pbset . HostSet
for _ , s := range sets {
ps , err := toPluginSet ( ctx , s )
if err != nil {
return db . NoRowsAffected , errors . Wrap ( ctx , err , op )
}
plgSets = append ( plgSets , ps )
}
2021-10-25 09:37:05 -07:00
2024-09-04 15:05:47 +01:00
plgClient , err := pluginClientFactoryFn ( ctx , plgHc , r . plugins )
if err != nil {
return db . NoRowsAffected , errors . Wrap ( ctx , err , op )
2021-10-25 09:37:05 -07:00
}
2021-10-28 12:46:42 -07:00
_ , err = plgClient . OnDeleteCatalog ( ctx , & plgpb . OnDeleteCatalogRequest {
Catalog : plgHc ,
Sets : plgSets ,
Persisted : p ,
})
2021-10-25 09:37:05 -07:00
if err != nil {
// Even if the plugin returns an error, we ignore it and proceed with
// deleting the catalog.
2021-10-28 12:46:42 -07:00
event . WriteError ( ctx , op , err , event . WithInfoMsg ( "plugin deleting catalog" , "host plugin id" , c . GetPluginId ()))
2021-10-25 09:37:05 -07:00
}
2022-07-20 16:24:46 -07:00
oplogWrapper , err := r . kms . GetWrapper ( ctx , c . ProjectId , kms . KeyPurposeOplog )
2021-10-25 09:37:05 -07:00
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
}
2021-09-21 09:18:49 -07:00
// 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.
2021-10-28 12:46:42 -07:00
func ( r * Repository ) getCatalog ( ctx context . Context , id string ) ( * HostCatalog , * plgpb . HostCatalogPersisted , error ) {
2021-09-21 09:18:49 -07:00
const op = "plugin.(Repository).getCatalog"
2021-10-28 12:46:42 -07:00
ca := & catalogAgg {}
ca . PublicId = id
if err := r . reader . LookupByPublicId ( ctx , ca ); err != nil {
return nil , nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "failed for: %s" , id )))
2021-09-21 09:18:49 -07:00
}
2021-10-28 12:46:42 -07:00
c , s := ca . toCatalogAndPersisted ()
var p * plgpb . HostCatalogPersisted
if s != nil {
var err error
2022-07-20 16:24:46 -07:00
p , err = toPluginPersistedData ( ctx , r . kms , c . GetProjectId (), s )
2021-10-28 12:46:42 -07:00
if err != nil {
return nil , nil , errors . Wrap ( ctx , err , op )
2021-09-21 09:18:49 -07:00
}
}
2021-10-28 12:46:42 -07:00
return c , p , nil
2021-09-21 09:18:49 -07:00
}
2023-04-24 19:27:41 -04:00
func ( r * Repository ) getPlugin ( ctx context . Context , plgId string ) ( * plg . Plugin , error ) {
2021-09-26 08:17:21 -07:00
const op = "plugin.(Repository).getPlugin"
if plgId == "" {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "no plugin id" )
}
2023-04-24 19:27:41 -04:00
plg := plg . NewPlugin ()
2021-09-26 08:17:21 -07:00
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
}
2021-09-21 09:18:49 -07:00
// toPluginCatalog returns a host catalog, with it's secret if available, in the format expected
// by the host plugin system.
2024-08-21 18:19:34 +01:00
func toPluginCatalog ( ctx context . Context , in * HostCatalog , plg * plg . Plugin ) ( * pb . HostCatalog , error ) {
2021-09-21 09:18:49 -07:00
const op = "plugin.toPluginCatalog"
if in == nil {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "nil storage plugin" )
}
2024-07-30 18:23:14 +01:00
var name , description , workerFilter * wrapperspb . StringValue
2021-11-05 10:43:58 -07:00
if inName := in . GetName (); inName != "" {
name = wrapperspb . String ( inName )
}
if inDescription := in . GetDescription (); inDescription != "" {
description = wrapperspb . String ( inDescription )
}
2024-07-30 18:23:14 +01:00
if inWorkerFilter := in . GetWorkerFilter (); inWorkerFilter != "" {
workerFilter = wrapperspb . String ( inWorkerFilter )
}
2021-11-05 10:43:58 -07:00
2021-09-21 09:18:49 -07:00
hc := & pb . HostCatalog {
2024-07-30 18:23:14 +01:00
Id : in . GetPublicId (),
ScopeId : in . GetProjectId (),
Name : name ,
Description : description ,
WorkerFilter : workerFilter ,
2024-09-04 15:05:47 +01:00
PluginId : in . GetPluginId (),
2024-08-21 18:19:34 +01:00
Plugin : toPluginInfo ( plg ),
2021-09-21 09:18:49 -07:00
}
2021-11-16 18:54:38 -05:00
if len ( in . GetSecretsHmac ()) > 0 {
hc . SecretsHmac = base58 . Encode ( in . GetSecretsHmac ())
}
2021-09-21 09:18:49 -07:00
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" ))
}
2022-04-13 12:58:19 -04:00
hc . Attrs = & pb . HostCatalog_Attributes {
Attributes : attrs ,
}
2021-09-21 09:18:49 -07:00
}
2021-09-28 09:40:12 -04:00
if in . Secrets != nil {
hc . Secrets = in . Secrets
2021-09-21 09:18:49 -07:00
}
return hc , nil
}
2021-10-28 12:46:42 -07:00
2024-08-21 18:19:34 +01:00
// toPluginInfo converts a Plugin object into PluginInfo.
func toPluginInfo ( plg * plg . Plugin ) * plugins . PluginInfo {
if plg == nil {
return nil
}
return & plugins . PluginInfo {
Id : plg . GetPublicId (),
Name : plg . GetName (),
Description : plg . GetDescription (),
}
}
2023-04-24 19:27:41 -04:00
// toPluginPersistedData converts a *HostCatalogSecret from storage to a
2022-07-20 16:24:46 -07:00
// *plgpb.HostCatalogPersisted expected by a plugin. Project Id must be set.
func toPluginPersistedData ( ctx context . Context , kmsCache * kms . Kms , projectId string , cSecret * HostCatalogSecret ) ( * plgpb . HostCatalogPersisted , error ) {
2021-10-28 12:46:42 -07:00
const op = "plugin.(Repository).getPersistedDataForCatalog"
2022-07-20 16:24:46 -07:00
if projectId == "" {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "empty project id" )
2021-10-28 12:46:42 -07:00
}
if cSecret == nil {
return nil , nil
}
2022-07-20 16:24:46 -07:00
dbWrapper , err := kmsCache . GetWrapper ( ctx , projectId , kms . KeyPurposeDatabase )
2021-10-28 12:46:42 -07:00
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
}