You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/target/repository.go

829 lines
29 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package target
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"github.com/hashicorp/boundary/globals"
talias "github.com/hashicorp/boundary/internal/alias/target"
"github.com/hashicorp/boundary/internal/boundary"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/boundary/internal/util"
"github.com/hashicorp/go-dbw"
)
// RepositoryFactory enables `target.Repository` object instantiation,
// and is used by the various service packages/controller object to do so.
type RepositoryFactory func(...Option) (*Repository, error)
// Cloneable provides a cloning interface
type Cloneable interface {
Clone() Target
}
// Repository is the target database repository
type Repository struct {
reader db.Reader
writer db.Writer
kms *kms.Kms
// defaultLimit provides a default for limiting the number of results returned from the repo
defaultLimit int
// permissions provides a set of user permissions - these directly correlate to what the user
// has access to in terms of actions and resources and we use it to build queries.
// These are passed in on the repository constructor using `WithPermissions`, meaning the
// `Repository` object is contextualized to whatever the request context is.
permissions []perms.Permission
}
// NewRepository creates a new target Repository.
// Supports the following options:
// - WithLimit: sets a limit on the number of results returned by various repo operations.
// - WithPermissions: defines the permissions the user has to perform different
// actions and access resources within the created repo object.
func NewRepository(ctx context.Context, r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repository, error) {
const op = "target.NewRepository"
if r == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil reader")
}
if w == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil writer")
}
if kms == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil kms")
}
opts := GetOpts(opt...)
if opts.WithLimit == 0 {
// zero signals the boundary defaults should be used.
opts.WithLimit = db.DefaultLimit
}
for _, p := range opts.WithPermissions {
if p.Resource != resource.Target {
return nil, errors.New(ctx, errors.InvalidParameter, op, "permission for incorrect resource found")
}
}
return &Repository{
reader: r,
writer: w,
kms: kms,
defaultLimit: opts.WithLimit,
permissions: opts.WithPermissions,
}, nil
}
// LookupTarget will look up a target in the repository and return the target
// with its host source ids and credential source ids. If the target is not
// found, it will return nil, nil, nil, nil. No options are currently supported.
func (r *Repository) LookupTarget(ctx context.Context, publicIdOrName string, opt ...Option) (Target, error) {
const op = "target.(Repository).LookupTarget"
opts := GetOpts(opt...)
if publicIdOrName == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
}
var where []string
var whereArgs []any
nameEmpty := opts.WithName == ""
projectIdEmpty := opts.WithProjectId == ""
projectNameEmpty := opts.WithProjectName == ""
if !nameEmpty {
if opts.WithName != publicIdOrName {
return nil, errors.New(ctx, errors.InvalidParameter, op, "name passed in but does not match publicId")
}
where, whereArgs = append(where, "lower(name) = lower(?)"), append(whereArgs, opts.WithName)
switch {
case projectIdEmpty && projectNameEmpty:
return nil, errors.New(ctx, errors.InvalidParameter, op, "using name but both project ID and project name are empty")
case !projectIdEmpty && !projectNameEmpty:
return nil, errors.New(ctx, errors.InvalidParameter, op, "using name but both project ID and project name are set")
case !projectIdEmpty:
where, whereArgs = append(where, "project_id = ?"), append(whereArgs, opts.WithProjectId)
case !projectNameEmpty:
where, whereArgs = append(where, "project_id = (select public_id from iam_scope where lower(name) = lower(?))"), append(whereArgs, opts.WithProjectName)
default:
return nil, errors.New(ctx, errors.InvalidParameter, op, "unknown combination of parameters")
}
} else {
switch {
case !projectIdEmpty:
return nil, errors.New(ctx, errors.InvalidParameter, op, "passed in project ID when using target ID for lookup")
case !projectNameEmpty:
return nil, errors.New(ctx, errors.InvalidParameter, op, "passed in project name when using target ID for lookup")
}
}
target := allocTargetView()
target.PublicId = publicIdOrName
var address string
var hostSources []HostSource
var credSources []CredentialSource
var aliases []*talias.Alias
_, err := r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(read db.Reader, w db.Writer) error {
var lookupErr error
switch where {
case nil:
lookupErr = read.LookupById(ctx, &target)
default:
target.PublicId = ""
lookupErr = read.LookupWhere(ctx, &target, strings.Join(where, " and "), whereArgs)
}
if lookupErr != nil {
return errors.Wrap(ctx, lookupErr, op, errors.WithMsg(fmt.Sprintf("failed for %s", publicIdOrName)))
}
var err error
if hostSources, err = fetchHostSources(ctx, read, target.PublicId); err != nil {
return errors.Wrap(ctx, err, op)
}
if credSources, err = fetchCredentialSources(ctx, read, target.PublicId); err != nil {
return errors.Wrap(ctx, err, op)
}
if aliases, err = fetchTargetAliases(ctx, read, target.PublicId); err != nil {
return errors.Wrap(ctx, err, op)
}
targetAddress, err := fetchAddress(ctx, read, target.PublicId)
if err != nil && !errors.IsNotFoundError(err) {
return errors.Wrap(ctx, err, op)
}
if targetAddress != nil {
address = targetAddress.GetAddress()
}
return nil
},
)
if err != nil {
if errors.IsNotFoundError(err) {
return nil, nil
}
return nil, errors.Wrap(ctx, err, op)
}
subtype, err := target.targetSubtype(ctx, address)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
subtype.SetHostSources(hostSources)
subtype.SetCredentialSources(credSources)
subtype.SetAliases(aliases)
return subtype, nil
}
// FetchAuthzProtectedEntitiesByScope implements boundary.AuthzProtectedEntityProvider
func (r *Repository) FetchAuthzProtectedEntitiesByScope(ctx context.Context, projectIds []string) (map[string][]boundary.AuthzProtectedEntity, error) {
const op = "target.(Repository).FetchAuthzProtectedEntitiesByScope"
var where string
var args []any
inClauseCnt := 0
switch len(projectIds) {
case 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "no projects given")
case 1:
if projectIds[0] != scope.Global.String() {
inClauseCnt += 1
where, args = fmt.Sprintf("where project_id = @%d", inClauseCnt), append(args, sql.Named(fmt.Sprintf("%d", inClauseCnt), projectIds[0]))
}
default:
idsInClause := make([]string, 0, len(projectIds))
for _, id := range projectIds {
inClauseCnt += 1
idsInClause, args = append(idsInClause, fmt.Sprintf("@%d", inClauseCnt)), append(args, sql.Named(fmt.Sprintf("%d", inClauseCnt), id))
}
where = fmt.Sprintf("where project_id in (%s)", strings.Join(idsInClause, ","))
}
q := targetPublicIdList
query := fmt.Sprintf(q, where)
rows, err := r.reader.Query(ctx, query, args)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
defer rows.Close()
targetsMap := map[string][]boundary.AuthzProtectedEntity{}
for rows.Next() {
var tv targetView
if err := r.reader.ScanRows(ctx, rows, &tv); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("scan row failed"))
}
targetsMap[tv.GetProjectId()] = append(targetsMap[tv.GetProjectId()], tv)
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("next rows error"))
}
return targetsMap, nil
}
// listTargets lists targets based on the data in the WithPermissions option
// provided to the Repository constructor. If no permissions are available, this function
// is a no-op.
// Supported options:
// - WithLimit which overrides the limit set in the Repository object
// - WithStartPageAfterItem which sets where to start listing from
func (r *Repository) listTargets(ctx context.Context, opt ...Option) ([]Target, time.Time, error) {
if len(r.permissions) == 0 {
return []Target{}, time.Time{}, nil
}
where, args := r.listPermissionWhereClauses()
permissionWhereClause := "(" + strings.Join(where, " or ") + ")"
opts := GetOpts(opt...)
limit := r.defaultLimit
if opts.WithLimit != 0 {
limit = opts.WithLimit
}
query := fmt.Sprintf(listTargetsTemplate, permissionWhereClause, limit)
if opts.WithStartPageAfterItem != nil {
query = fmt.Sprintf(listTargetsPageTemplate, permissionWhereClause, limit)
args = append(args,
sql.Named("last_item_create_time", opts.WithStartPageAfterItem.GetCreateTime()),
sql.Named("last_item_id", opts.WithStartPageAfterItem.GetPublicId()),
)
}
return r.queryTargets(ctx, query, args, limit)
}
// listTargetsRefresh lists targets based on the data in the WithPermissions option
// provided to the Repository constructor. If no permissions are available, this function
// is a no-op.
// Supported options:
// - WithLimit which overrides the limit set in the Repository object
// - WithStartPageAfterItem which sets where to start listing from
func (r *Repository) listTargetsRefresh(ctx context.Context, updatedAfter time.Time, opt ...Option) ([]Target, time.Time, error) {
const op = "target.(Repository).listTargetsRefresh"
if updatedAfter.IsZero() {
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing updated after time")
}
if len(r.permissions) == 0 {
return []Target{}, time.Time{}, nil
}
where, args := r.listPermissionWhereClauses()
permissionWhereClause := "(" + strings.Join(where, " or ") + ")"
opts := GetOpts(opt...)
limit := r.defaultLimit
if opts.WithLimit != 0 {
limit = opts.WithLimit
}
query := fmt.Sprintf(refreshTargetsTemplate, permissionWhereClause, limit)
args = append(args,
sql.Named("updated_after_time", timestamp.New(updatedAfter)),
)
if opts.WithStartPageAfterItem != nil {
query = fmt.Sprintf(refreshTargetsPageTemplate, permissionWhereClause, limit)
args = append(args,
sql.Named("last_item_update_time", opts.WithStartPageAfterItem.GetUpdateTime()),
sql.Named("last_item_id", opts.WithStartPageAfterItem.GetPublicId()),
)
}
return r.queryTargets(ctx, query, args, limit)
}
func (r *Repository) queryTargets(ctx context.Context, query string, args []any, limit int) ([]Target, time.Time, error) {
const op = "target.(Repository).queryTargets"
var targets []Target
var transactionTimestamp time.Time
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
rows, err := r.Query(ctx, query, args)
if err != nil {
return err
}
defer rows.Close()
var foundTargets []*targetView
for rows.Next() {
if err := r.ScanRows(ctx, rows, &foundTargets); err != nil {
return err
}
}
if err := rows.Err(); err != nil {
return err
}
var targetIds []string
for _, t := range foundTargets {
targetIds = append(targetIds, t.GetPublicId())
}
var foundAddresses []*Address
if err := r.SearchWhere(ctx,
&foundAddresses,
"target_id in (?)",
[]any{targetIds},
db.WithLimit(limit),
); err != nil {
return err
}
addresses := make(map[string]string)
for _, addr := range foundAddresses {
addresses[addr.TargetId()] = addr.Address()
}
for _, t := range foundTargets {
var address string
if v, ok := addresses[t.GetPublicId()]; ok {
address = v
}
subtype, err := t.targetSubtype(ctx, address)
if errors.Is(err, errTargetSubtypeNotFound) {
// In cases where we have mixed target types and the controller
// doesn't support all of them, we want to ignore if we can't find
// the target subtype and continue listing the others we do support.
continue
}
if err != nil {
return err
}
targets = append(targets, subtype)
}
transactionTimestamp, err = r.Now(ctx)
return err
}); err != nil {
return nil, time.Time{}, errors.Wrap(ctx, err, op)
}
return targets, transactionTimestamp, nil
}
func (r *Repository) listPermissionWhereClauses() ([]string, []any) {
var where []string
var args []any
inClauseCnt := 0
for _, p := range r.permissions {
if p.Action != action.List {
continue
}
inClauseCnt++
var clauses []string
clauses = append(clauses, fmt.Sprintf("project_id = @project_id_%d", inClauseCnt))
args = append(args, sql.Named(fmt.Sprintf("project_id_%d", inClauseCnt), p.GrantScopeId))
if len(p.ResourceIds) > 0 {
clauses = append(clauses, fmt.Sprintf("public_id = any(@public_id_%d)", inClauseCnt))
args = append(args, sql.Named(fmt.Sprintf("public_id_%d", inClauseCnt), "{"+strings.Join(p.ResourceIds, ",")+"}"))
}
where = append(where, fmt.Sprintf("(%s)", strings.Join(clauses, " and ")))
}
return where, args
}
// listDeletedIds lists the public IDs of any targets deleted since the timestamp provided,
// and the timestamp of the transaction within which the targets were listed.
func (r *Repository) listDeletedIds(ctx context.Context, since time.Time) ([]string, time.Time, error) {
const op = "target.(Repository).listDeletedIds"
var deleteTargets []*deletedTarget
var transactionTimestamp time.Time
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, _ db.Writer) error {
if err := r.SearchWhere(ctx, &deleteTargets, "delete_time >= ?", []any{since}); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted targets"))
}
var err error
transactionTimestamp, err = r.Now(ctx)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to get transaction timestamp"))
}
return nil
}); err != nil {
return nil, time.Time{}, err
}
var targetIds []string
for _, t := range deleteTargets {
targetIds = append(targetIds, t.PublicId)
}
return targetIds, transactionTimestamp, nil
}
// estimatedCount returns an estimate of the total number of items across all targets.
func (r *Repository) estimatedCount(ctx context.Context) (int, error) {
const op = "target.(Repository).estimatedCount"
rows, err := r.reader.Query(ctx, estimateCountTargets, nil)
if err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total targets"))
}
var count int
for rows.Next() {
if err := r.reader.ScanRows(ctx, rows, &count); err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total targets"))
}
}
if err := rows.Err(); err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total targets"))
}
return count, nil
}
// DeleteTarget will delete a target from the repository.
func (r *Repository) DeleteTarget(ctx context.Context, publicId string, _ ...Option) (int, error) {
const op = "target.(Repository).DeleteTarget"
if publicId == "" {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
}
t := allocTargetView()
t.PublicId = publicId
if err := r.reader.LookupByPublicId(ctx, &t); err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", publicId)))
}
var metadata oplog.Metadata
var deleteTarget Target
alloc, ok := subtypeRegistry.allocFunc(t.Subtype())
if !ok {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s is an unsupported target type %s", publicId, t.Type))
}
deleteTarget = alloc()
deleteTarget.SetPublicId(ctx, publicId)
metadata = deleteTarget.Oplog(oplog.OpType_OP_TYPE_DELETE)
oplogWrapper, err := r.kms.GetWrapper(ctx, t.ProjectId, kms.KeyPurposeOplog)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
var rowsDeleted int
var deleteResource any
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(_ db.Reader, w db.Writer) error {
deleteResource = deleteTarget.(Cloneable).Clone()
rowsDeleted, err = w.Delete(
ctx,
deleteResource,
db.WithOplog(oplogWrapper, metadata),
)
if err != nil {
return errors.Wrap(ctx, err, op)
}
if rowsDeleted > 1 {
// return err, which will result in a rollback of the delete
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)
}
return rowsDeleted, nil
}
// CreateTarget inserts into the repository and returns the new Target with
// its list of host sets and credential libraries.
// WithPublicId and WithAliases are supported options.
func (r *Repository) CreateTarget(ctx context.Context, target Target, opt ...Option) (Target, error) {
const op = "target.(Repository).CreateTarget"
opts := GetOpts(opt...)
if target == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing target")
}
vet, ok := subtypeRegistry.vetFunc(target.GetType())
if !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported target type %s", target.GetType()))
}
if err := vet(ctx, target); err != nil {
return nil, err
}
if target.GetProjectId() == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing project id")
}
if target.GetName() == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing name")
}
if target.GetPublicId() != "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty")
}
t := target.Clone()
if opts.WithPublicId != "" {
if err := t.SetPublicId(ctx, opts.WithPublicId); err != nil {
return nil, err
}
} else {
prefix, ok := subtypeRegistry.idPrefix(target.GetType())
if !ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported target type %s", target.GetType()))
}
id, err := db.NewPublicId(ctx, prefix)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
t.SetPublicId(ctx, id)
}
var address *Address
var err error
if t.GetAddress() != "" {
host, err := util.ParseAddress(ctx, t.GetAddress())
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidAddress), errors.WithMsg("invalid address"))
}
t.SetAddress(host)
address, err = NewAddress(ctx, t.GetPublicId(), t.GetAddress())
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
}
oplogWrapper, err := r.kms.GetWrapper(ctx, target.GetProjectId(), kms.KeyPurposeOplog)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
metadata := t.Oplog(oplog.OpType_OP_TYPE_CREATE)
var createdAliases []*talias.Alias
var returnedTarget Target
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(read db.Reader, w db.Writer) error {
targetTicket, err := w.GetTicket(ctx, t)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
msgs := make([]*oplog.Message, 0, 2)
var targetOplogMsg oplog.Message
returnedTarget = t.Clone()
if err := w.Create(ctx, returnedTarget, db.NewOplogMsg(&targetOplogMsg)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create target"))
}
msgs = append(msgs, &targetOplogMsg)
if address != nil {
var targetAddressOplogMsg oplog.Message
if err := w.Create(ctx, address, db.NewOplogMsg(&targetAddressOplogMsg)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create target address"))
}
msgs = append(msgs, &targetAddressOplogMsg)
}
createdAliases = make([]*talias.Alias, 0, len(opts.withAliases))
for _, a := range opts.withAliases {
a = a.Clone()
aliasId, err := db.NewPublicId(ctx, globals.TargetAliasPrefix)
if err != nil {
return errors.Wrap(ctx, err, op)
}
a.PublicId = aliasId
a.DestinationId = t.GetPublicId()
var targetAliasOplogMsg oplog.Message
if err := w.Create(ctx, a, db.NewOplogMsg(&targetAliasOplogMsg)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create target alias"))
}
createdAliases = append(createdAliases, a)
msgs = append(msgs, &targetAliasOplogMsg)
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, targetTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
return nil
},
)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s target id", t.GetPublicId())))
}
returnedTarget.SetAliases(createdAliases)
return returnedTarget, nil
}
// UpdateTarget will update a target in the repository and return the written
// target. fieldMaskPaths provides field_mask.proto paths for fields that should
// be updated. Fields will be set to NULL if the field is a zero value and
// included in fieldMask. Name, Description, and WorkerFilter are the only
// updatable fields. If no updatable fields are included in the fieldMaskPaths,
// then an error is returned.
func (r *Repository) UpdateTarget(ctx context.Context, target Target, version uint32, fieldMaskPaths []string, _ ...Option) (Target, int, error) {
const op = "target.(Repository).UpdateTarget"
if target == nil {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing target")
}
if target.GetPublicId() == "" {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing target public id")
}
if target.GetProjectId() == "" {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing project id")
}
if version == 0 {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
vet, ok := subtypeRegistry.vetForUpdateFunc(target.GetType())
if !ok {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported target type %s", target.GetType()))
}
if err := vet(ctx, target, fieldMaskPaths); err != nil {
return nil, db.NoRowsAffected, err
}
for _, f := range fieldMaskPaths {
switch {
case strings.EqualFold("name", f):
case strings.EqualFold("description", f):
case strings.EqualFold("defaultport", f):
case strings.EqualFold("defaultclientport", f):
case strings.EqualFold("sessionmaxseconds", f):
case strings.EqualFold("sessionconnectionlimit", f):
case strings.EqualFold("workerfilter", f):
case strings.EqualFold("egressworkerfilter", f):
case strings.EqualFold("ingressworkerfilter", f):
case strings.EqualFold("address", f):
case strings.EqualFold("storagebucketid", f):
case strings.EqualFold("enablesessionrecording", f):
default:
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, fmt.Sprintf("invalid field mask: %s", f))
}
}
var dbMask, nullFields []string
dbMask, nullFields = dbw.BuildUpdatePaths(
map[string]any{
"Name": target.GetName(),
"Description": target.GetDescription(),
"DefaultPort": target.GetDefaultPort(),
"DefaultClientPort": target.GetDefaultClientPort(),
"SessionMaxSeconds": target.GetSessionMaxSeconds(),
"SessionConnectionLimit": target.GetSessionConnectionLimit(),
"WorkerFilter": target.GetWorkerFilter(),
"EgressWorkerFilter": target.GetEgressWorkerFilter(),
"IngressWorkerFilter": target.GetIngressWorkerFilter(),
"Address": target.GetAddress(),
"StorageBucketId": target.GetStorageBucketId(),
"EnableSessionRecording": target.GetEnableSessionRecording(),
},
fieldMaskPaths,
[]string{"SessionMaxSeconds", "SessionConnectionLimit", "EnableSessionRecording"},
)
if len(dbMask) == 0 && len(nullFields) == 0 {
return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "empty field mask")
}
// The Address field is not a part of the target schema in the database. It
// is a part of a different table called target_address, which is why the
// Address field must be filtered out of the dbMask & nullFields slices.
var addressEndpoint string
var updateAddress, deleteAddress bool
var filteredDbMask, filteredNullFields []string
for _, f := range dbMask {
switch {
case strings.EqualFold("Address", f):
updateAddress = true
address, err := util.ParseAddress(ctx, target.GetAddress())
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidAddress), errors.WithMsg("invalid address"))
}
target.SetAddress(address)
addressEndpoint = target.GetAddress()
default:
filteredDbMask = append(filteredDbMask, f)
}
}
for _, f := range nullFields {
switch {
case strings.EqualFold("Address", f):
deleteAddress = true
default:
filteredNullFields = append(filteredNullFields, f)
}
}
// If the Address field is the only present change, then we must still
// update the target's version because a target address is a child object
// of the target.
if (len(filteredDbMask) == 0 && len(filteredNullFields) == 0) && (updateAddress || deleteAddress) {
target.SetVersion(version + 1)
filteredDbMask = append(filteredDbMask, "Version")
}
oplogWrapper, err := r.kms.GetWrapper(ctx, target.GetProjectId(), kms.KeyPurposeOplog)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
var rowsUpdated int
var returnedTarget Target
var hostSources []HostSource
var credSources []CredentialSource
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(read db.Reader, w db.Writer) error {
t := target.Clone()
rowsUpdated, err = w.Update(ctx, t, filteredDbMask, filteredNullFields,
db.WithOplog(oplogWrapper, t.Oplog(oplog.OpType_OP_TYPE_UPDATE)),
db.WithVersion(&version),
)
if err != nil {
return errors.Wrap(ctx, err, op)
}
if rowsUpdated > 1 {
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 target resource would have been updated")
}
if hostSources, err = fetchHostSources(ctx, read, t.GetPublicId()); err != nil {
return errors.Wrap(ctx, err, op)
}
if credSources, err = fetchCredentialSources(ctx, read, t.GetPublicId()); err != nil {
return errors.Wrap(ctx, err, op)
}
var address *Address
switch {
case updateAddress:
if len(hostSources) > 0 {
return errors.New(ctx, errors.Conflict, op, "unable to set address because one or more host sources is assigned to the given target")
}
address, err = NewAddress(ctx, t.GetPublicId(), addressEndpoint)
if err != nil {
return errors.Wrap(ctx, err, op)
}
if err := w.Create(ctx, address,
db.WithOplog(oplogWrapper, address.oplog(oplog.OpType_OP_TYPE_UPDATE)),
db.WithOnConflict(&db.OnConflict{
Target: db.Columns{"target_id"},
Action: db.UpdateAll(true),
})); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create target address"))
}
case deleteAddress:
var err error
address = allocTargetAddress()
address.TargetAddress.TargetId = t.GetPublicId()
rowsDeleted, err := w.Delete(ctx, address, db.WithOplog(oplogWrapper, address.oplog(oplog.OpType_OP_TYPE_DELETE)))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete target address"))
}
if rowsDeleted > 1 {
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 target resource would have been deleted")
}
// If the only update was deleting an address, consider this as one "row" being updated.
if rowsUpdated == 0 && rowsDeleted == 1 {
rowsUpdated = 1
}
default:
address, err = fetchAddress(ctx, read, t.GetPublicId())
if err != nil && !errors.IsNotFoundError(err) {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to fetch target address"))
}
}
if address != nil {
t.SetAddress(address.GetAddress())
}
returnedTarget = t.Clone()
return nil
},
)
if err != nil {
if errors.IsUniqueError(err) {
return nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf("target %s already exists in project %s", target.GetName(), target.GetProjectId()))
}
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", target.GetPublicId())))
}
returnedTarget.SetHostSources(hostSources)
returnedTarget.SetCredentialSources(credSources)
return returnedTarget, rowsUpdated, nil
}