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 }