github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/repo/update.go (about)

     1  package repo
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    10  
    11  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    12  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
    13  
    14  	"github.com/kyma-incubator/compass/components/director/pkg/persistence"
    15  	"github.com/pkg/errors"
    16  )
    17  
    18  // UpdaterGlobal is an interface for updating global entities without tenant or entities with tenant embedded in them.
    19  type UpdaterGlobal interface {
    20  	UpdateSingleGlobal(ctx context.Context, dbEntity interface{}) error
    21  	UpdateSingleWithVersionGlobal(ctx context.Context, dbEntity interface{}) error
    22  	SetIDColumns(idColumns []string)
    23  	SetUpdatableColumns(updatableColumns []string)
    24  	TechnicalUpdate(ctx context.Context, dbEntity interface{}) error
    25  	Clone() UpdaterGlobal
    26  }
    27  
    28  // Updater is an interface for updating entities with externally managed tenant accesses (m2m table or view)
    29  type Updater interface {
    30  	UpdateSingle(ctx context.Context, resourceType resource.Type, tenant string, dbEntity interface{}) error
    31  	UpdateSingleWithVersion(ctx context.Context, resourceType resource.Type, tenant string, dbEntity interface{}) error
    32  }
    33  
    34  type updater struct {
    35  	tableName        string
    36  	updatableColumns []string
    37  	idColumns        []string
    38  }
    39  
    40  type updaterGlobal struct {
    41  	tableName        string
    42  	resourceType     resource.Type
    43  	tenantColumn     *string
    44  	updatableColumns []string
    45  	idColumns        []string
    46  }
    47  
    48  // NewUpdater is a constructor for Updater about entities with externally managed tenant accesses (m2m table or view)
    49  func NewUpdater(tableName string, updatableColumns []string, idColumns []string) Updater {
    50  	return &updater{
    51  		tableName:        tableName,
    52  		updatableColumns: updatableColumns,
    53  		idColumns:        idColumns,
    54  	}
    55  }
    56  
    57  // NewUpdaterGlobal is a constructor for UpdaterGlobal about global entities without tenant.
    58  func NewUpdaterGlobal(resourceType resource.Type, tableName string, updatableColumns []string, idColumns []string) UpdaterGlobal {
    59  	return &updaterGlobal{
    60  		resourceType:     resourceType,
    61  		tableName:        tableName,
    62  		updatableColumns: updatableColumns,
    63  		idColumns:        idColumns,
    64  	}
    65  }
    66  
    67  // NewUpdaterWithEmbeddedTenant is a constructor for UpdaterGlobal about entities with tenant embedded in them.
    68  func NewUpdaterWithEmbeddedTenant(resourceType resource.Type, tableName string, updatableColumns []string, tenantColumn string, idColumns []string) UpdaterGlobal {
    69  	return &updaterGlobal{
    70  		resourceType:     resourceType,
    71  		tableName:        tableName,
    72  		updatableColumns: updatableColumns,
    73  		tenantColumn:     &tenantColumn,
    74  		idColumns:        idColumns,
    75  	}
    76  }
    77  
    78  // UpdateSingleWithVersion updates the entity while checking its version as a way of optimistic locking.
    79  // It is suitable for entities with externally managed tenant accesses (m2m table or view).
    80  //
    81  // UpdateSingleWithVersion performs get of the resource with owner check before updating the entity with version.
    82  // This is needed in order to distinguish the generic Unauthorized error due to the tenant has no owner access to the entity
    83  // and the case of concurrent modification where the version differs. In both cases the affected rows would be 0.
    84  func (u *updater) UpdateSingleWithVersion(ctx context.Context, resourceType resource.Type, tenant string, dbEntity interface{}) error {
    85  	if dbEntity == nil {
    86  		return apperrors.NewInternalError("item cannot be nil")
    87  	}
    88  
    89  	var id string
    90  	if identifiable, ok := dbEntity.(Identifiable); ok {
    91  		id = identifiable.GetID()
    92  	}
    93  
    94  	if len(id) == 0 {
    95  		return apperrors.NewInternalError("id cannot be empty, check if the entity implements Identifiable")
    96  	}
    97  
    98  	exister := NewExistQuerierWithOwnerCheck(u.tableName)
    99  	found, err := exister.Exists(ctx, resourceType, tenant, Conditions{NewEqualCondition("id", id)})
   100  	if err != nil {
   101  		return err
   102  	}
   103  	if !found {
   104  		return apperrors.NewInvalidOperationError("entity does not exist or caller tenant does not have owner access")
   105  	}
   106  
   107  	return u.updateSingleWithVersion(ctx, tenant, dbEntity, resourceType)
   108  }
   109  
   110  // UpdateSingle updates the given entity if the tenant has owner access to it.
   111  // It is suitable for entities with externally managed tenant accesses (m2m table or view).
   112  func (u *updater) UpdateSingle(ctx context.Context, resourceType resource.Type, tenant string, dbEntity interface{}) error {
   113  	return u.updateSingleWithFields(ctx, dbEntity, tenant, buildFieldsToSet(u.updatableColumns), resourceType)
   114  }
   115  
   116  func (u *updater) updateSingleWithVersion(ctx context.Context, tenant string, dbEntity interface{}, resourceType resource.Type) error {
   117  	fieldsToSet := buildFieldsToSet(u.updatableColumns)
   118  	fieldsToSet = append(fieldsToSet, "version = version+1")
   119  
   120  	if err := u.updateSingleWithFields(ctx, dbEntity, tenant, fieldsToSet, resourceType); err != nil {
   121  		if apperrors.IsConcurrentUpdate(err) {
   122  			return apperrors.NewConcurrentUpdate()
   123  		}
   124  		return err
   125  	}
   126  	return nil
   127  }
   128  
   129  func (u *updater) updateSingleWithFields(ctx context.Context, dbEntity interface{}, tenant string, fieldsToSet []string, resourceType resource.Type) error {
   130  	if dbEntity == nil {
   131  		return apperrors.NewInternalError("item cannot be nil")
   132  	}
   133  
   134  	persist, err := persistence.FromCtx(ctx)
   135  	if err != nil {
   136  		return err
   137  	}
   138  
   139  	query, err := u.buildQuery(fieldsToSet, tenant, resourceType)
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	if entityWithExternalTenant, ok := dbEntity.(EntityWithExternalTenant); ok {
   145  		dbEntity = entityWithExternalTenant.DecorateWithTenantID(tenant)
   146  	}
   147  
   148  	log.C(ctx).Debugf("Executing DB query: %s", query)
   149  	res, err := persist.NamedExecContext(ctx, query, dbEntity)
   150  	if err = persistence.MapSQLError(ctx, err, resourceType, resource.Update, "while updating single entity from '%s' table", u.tableName); err != nil {
   151  		return err
   152  	}
   153  
   154  	affected, err := res.RowsAffected()
   155  	if err != nil {
   156  		return errors.Wrap(err, "while checking affected rows")
   157  	}
   158  
   159  	return assertSingleRowAffected(resourceType, affected, true)
   160  }
   161  
   162  func (u *updater) buildQuery(fieldsToSet []string, tenant string, resourceType resource.Type) (string, error) {
   163  	var stmtBuilder strings.Builder
   164  	stmtBuilder.WriteString(fmt.Sprintf("UPDATE %s SET %s WHERE", u.tableName, strings.Join(fieldsToSet, ", ")))
   165  	if len(u.idColumns) > 0 {
   166  		var preparedIDColumns []string
   167  		for _, idCol := range u.idColumns {
   168  			preparedIDColumns = append(preparedIDColumns, fmt.Sprintf("%s = :%s", idCol, idCol))
   169  		}
   170  		stmtBuilder.WriteString(fmt.Sprintf(" %s", strings.Join(preparedIDColumns, " AND ")))
   171  		stmtBuilder.WriteString(" AND")
   172  	}
   173  
   174  	tenantIsolationCondition, err := NewTenantIsolationConditionForNamedArgs(resourceType, tenant, true)
   175  	if err != nil {
   176  		return "", err
   177  	}
   178  
   179  	stmtBuilder.WriteString(" ")
   180  	stmtBuilder.WriteString(tenantIsolationCondition.GetQueryPart())
   181  
   182  	return stmtBuilder.String(), nil
   183  }
   184  
   185  // UpdateSingleGlobal updates the given entity. In case of configured tenant column it checks if it matches the tenant inside the dbEntity.
   186  // It is suitable for entities without tenant or entities with tenant embedded in them.
   187  func (u *updaterGlobal) UpdateSingleGlobal(ctx context.Context, dbEntity interface{}) error {
   188  	return u.updateSingleWithFields(ctx, dbEntity, buildFieldsToSet(u.updatableColumns))
   189  }
   190  
   191  // UpdateSingleWithVersionGlobal updates the entity while checking its version as a way of optimistic locking.
   192  // In case of configured tenant column it checks if it matches the tenant inside the dbEntity.
   193  // It is suitable for entities without tenant or entities with tenant embedded in them.
   194  func (u *updaterGlobal) UpdateSingleWithVersionGlobal(ctx context.Context, dbEntity interface{}) error {
   195  	return u.updateSingleWithVersion(ctx, dbEntity)
   196  }
   197  
   198  // SetIDColumns is a setter for idColumns.
   199  func (u *updaterGlobal) SetIDColumns(idColumns []string) {
   200  	u.idColumns = idColumns
   201  }
   202  
   203  // SetUpdatableColumns is a setter for updatableColumns.
   204  func (u *updaterGlobal) SetUpdatableColumns(updatableColumns []string) {
   205  	u.updatableColumns = updatableColumns
   206  }
   207  
   208  // TechnicalUpdate is a global single update which maintains the updated at property of the entity.
   209  func (u *updaterGlobal) TechnicalUpdate(ctx context.Context, dbEntity interface{}) error {
   210  	entity, ok := dbEntity.(Entity)
   211  	if ok && entity.GetDeletedAt().IsZero() {
   212  		entity.SetUpdatedAt(time.Now())
   213  		dbEntity = entity
   214  	}
   215  	return u.updateSingleWithFields(ctx, dbEntity, buildFieldsToSet(u.updatableColumns))
   216  }
   217  
   218  // Clone clones the updater.
   219  func (u *updaterGlobal) Clone() UpdaterGlobal {
   220  	var clonedUpdater updaterGlobal
   221  
   222  	clonedUpdater.tableName = u.tableName
   223  	clonedUpdater.resourceType = u.resourceType
   224  	clonedUpdater.updatableColumns = append(clonedUpdater.updatableColumns, u.updatableColumns...)
   225  	clonedUpdater.tenantColumn = u.tenantColumn
   226  	clonedUpdater.idColumns = append(clonedUpdater.idColumns, u.idColumns...)
   227  
   228  	return &clonedUpdater
   229  }
   230  
   231  func (u *updaterGlobal) updateSingleWithVersion(ctx context.Context, dbEntity interface{}) error {
   232  	fieldsToSet := buildFieldsToSet(u.updatableColumns)
   233  	fieldsToSet = append(fieldsToSet, "version = version+1")
   234  
   235  	if err := u.updateSingleWithFields(ctx, dbEntity, fieldsToSet); err != nil {
   236  		if apperrors.IsConcurrentUpdate(err) {
   237  			return apperrors.NewConcurrentUpdate()
   238  		}
   239  		return err
   240  	}
   241  	return nil
   242  }
   243  
   244  func (u *updaterGlobal) updateSingleWithFields(ctx context.Context, dbEntity interface{}, fieldsToSet []string) error {
   245  	if dbEntity == nil {
   246  		return apperrors.NewInternalError("item cannot be nil")
   247  	}
   248  
   249  	persist, err := persistence.FromCtx(ctx)
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	query := u.buildQuery(fieldsToSet)
   255  
   256  	log.C(ctx).Debugf("Executing DB query: %s", query)
   257  	res, err := persist.NamedExecContext(ctx, query, dbEntity)
   258  	if err = persistence.MapSQLError(ctx, err, u.resourceType, resource.Update, "while updating single entity from '%s' table", u.tableName); err != nil {
   259  		return err
   260  	}
   261  
   262  	affected, err := res.RowsAffected()
   263  	if err != nil {
   264  		return errors.Wrap(err, "while checking affected rows")
   265  	}
   266  
   267  	isTenantScopedUpdate := u.tenantColumn != nil
   268  
   269  	return assertSingleRowAffected(u.resourceType, affected, isTenantScopedUpdate)
   270  }
   271  
   272  func (u *updaterGlobal) buildQuery(fieldsToSet []string) string {
   273  	var stmtBuilder strings.Builder
   274  	stmtBuilder.WriteString(fmt.Sprintf("UPDATE %s SET %s WHERE", u.tableName, strings.Join(fieldsToSet, ", ")))
   275  	if len(u.idColumns) > 0 {
   276  		var preparedIDColumns []string
   277  		for _, idCol := range u.idColumns {
   278  			preparedIDColumns = append(preparedIDColumns, fmt.Sprintf("%s = :%s", idCol, idCol))
   279  		}
   280  		stmtBuilder.WriteString(fmt.Sprintf(" %s", strings.Join(preparedIDColumns, " AND ")))
   281  		if u.tenantColumn != nil {
   282  			stmtBuilder.WriteString(" AND")
   283  		}
   284  	}
   285  
   286  	if u.tenantColumn != nil {
   287  		stmtBuilder.WriteString(fmt.Sprintf(" %s = :%s", *u.tenantColumn, *u.tenantColumn))
   288  	}
   289  
   290  	return stmtBuilder.String()
   291  }
   292  
   293  func buildFieldsToSet(updatableColumns []string) []string {
   294  	fieldsToSet := make([]string, 0, len(updatableColumns)+1)
   295  	for _, c := range updatableColumns {
   296  		fieldsToSet = append(fieldsToSet, fmt.Sprintf("%s = :%s", c, c))
   297  	}
   298  	return fieldsToSet
   299  }
   300  
   301  func assertSingleRowAffected(resourceType resource.Type, affected int64, isTenantScopedUpdate bool) error {
   302  	if affected == 0 && isTenantScopedUpdate {
   303  		return apperrors.NewUnauthorizedError(apperrors.ShouldBeOwnerMsg)
   304  	}
   305  
   306  	if affected != 1 {
   307  		if resourceType == resource.BundleReference {
   308  			return apperrors.NewCannotUpdateObjectInManyBundles()
   309  		}
   310  		return apperrors.NewInternalError(apperrors.ShouldUpdateSingleRowButUpdatedMsgF, affected)
   311  	}
   312  	return nil
   313  }