github.com/weaviate/weaviate@v1.24.6/usecases/schema/tenant.go (about) 1 // _ _ 2 // __ _____ __ ___ ___ __ _| |_ ___ 3 // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \ 4 // \ V V / __/ (_| |\ V /| | (_| | || __/ 5 // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___| 6 // 7 // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved. 8 // 9 // CONTACT: hello@weaviate.io 10 // 11 12 package schema 13 14 import ( 15 "context" 16 "encoding/json" 17 "fmt" 18 "regexp" 19 "strings" 20 21 "github.com/weaviate/weaviate/entities/models" 22 "github.com/weaviate/weaviate/entities/schema" 23 uco "github.com/weaviate/weaviate/usecases/objects" 24 "github.com/weaviate/weaviate/usecases/schema/migrate" 25 "github.com/weaviate/weaviate/usecases/sharding" 26 ) 27 28 var regexTenantName = regexp.MustCompile(`^` + schema.ShardNameRegexCore + `$`) 29 30 // tenantsPath is the main path used for authorization 31 const tenantsPath = "schema/tenants" 32 33 // AddTenants is used to add new tenants to a class 34 // Class must exist and has partitioning enabled 35 func (m *Manager) AddTenants(ctx context.Context, 36 principal *models.Principal, 37 class string, 38 tenants []*models.Tenant, 39 ) (created []*models.Tenant, err error) { 40 if err = m.Authorizer.Authorize(principal, "update", tenantsPath); err != nil { 41 return 42 } 43 44 validated, err := validateTenants(tenants) 45 if err != nil { 46 return 47 } 48 if err = validateActivityStatuses(validated, true); err != nil { 49 return 50 } 51 cls := m.getClassByName(class) 52 if cls == nil { 53 err = fmt.Errorf("class %q: %w", class, ErrNotFound) 54 return 55 } 56 if !schema.MultiTenancyEnabled(cls) { 57 err = fmt.Errorf("multi-tenancy is not enabled for class %q", class) 58 return 59 } 60 61 names := make([]string, len(validated)) 62 for i, tenant := range validated { 63 names[i] = tenant.Name 64 } 65 66 // create transaction payload 67 partitions, err := m.getPartitions(cls, names) 68 if err != nil { 69 err = fmt.Errorf("get partitions from class %q: %w", class, err) 70 return 71 } 72 if len(partitions) != len(names) { 73 m.logger.WithField("action", "add_tenants"). 74 WithField("#partitions", len(partitions)). 75 WithField("#requested", len(names)). 76 Tracef("number of partitions for class %q does not match number of requested tenants", class) 77 } 78 request := AddTenantsPayload{ 79 Class: class, 80 Tenants: make([]TenantCreate, 0, len(partitions)), 81 } 82 for i, name := range names { 83 part, ok := partitions[name] 84 if ok { 85 request.Tenants = append(request.Tenants, TenantCreate{ 86 Name: name, 87 Nodes: part, 88 Status: schema.ActivityStatus(validated[i].ActivityStatus), 89 }) 90 } 91 } 92 93 // open cluster-wide transaction 94 tx, err := m.cluster.BeginTransaction(ctx, addTenants, 95 request, DefaultTxTTL) 96 if err != nil { 97 err = fmt.Errorf("open cluster-wide transaction: %w", err) 98 return 99 } 100 101 if err := m.cluster.CommitWriteTransaction(ctx, tx); err != nil { 102 m.logger.WithError(err).Errorf("not every node was able to commit") 103 } 104 105 err = m.onAddTenants(ctx, cls, request) // actual update 106 if err != nil { 107 m.logger.WithField("action", "add_tenants"). 108 WithField("n", len(request.Tenants)). 109 WithField("class", cls.Class).Error(err) 110 } 111 112 created = validated 113 return 114 } 115 116 func (m *Manager) getPartitions(cls *models.Class, shards []string) (map[string][]string, error) { 117 rf := int64(1) 118 if cls.ReplicationConfig != nil && cls.ReplicationConfig.Factor > rf { 119 rf = cls.ReplicationConfig.Factor 120 } 121 m.schemaCache.RLock() 122 defer m.schemaCache.RUnlock() 123 st := m.schemaCache.ShardingState[cls.Class] 124 if st == nil { 125 return nil, fmt.Errorf("sharding state %w", ErrNotFound) 126 } 127 return st.GetPartitions(m.clusterState, shards, rf) 128 } 129 130 func validateTenants(tenants []*models.Tenant) (validated []*models.Tenant, err error) { 131 uniq := make(map[string]*models.Tenant) 132 for i, requested := range tenants { 133 if !regexTenantName.MatchString(requested.Name) { 134 msg := "tenant name should only contain alphanumeric characters (a-z, A-Z, 0-9), " + 135 "underscore (_), and hyphen (-), with a length between 1 and 64 characters" 136 err = uco.NewErrInvalidUserInput("tenant name at index %d: %s", i, msg) 137 return 138 } 139 _, found := uniq[requested.Name] 140 if !found { 141 uniq[requested.Name] = requested 142 } 143 } 144 validated = make([]*models.Tenant, len(uniq)) 145 i := 0 146 for _, tenant := range uniq { 147 validated[i] = tenant 148 i++ 149 } 150 return 151 } 152 153 func validateActivityStatuses(tenants []*models.Tenant, allowEmpty bool) error { 154 msgs := make([]string, 0, len(tenants)) 155 156 for _, tenant := range tenants { 157 switch status := tenant.ActivityStatus; status { 158 case models.TenantActivityStatusHOT, models.TenantActivityStatusCOLD: 159 // ok 160 case models.TenantActivityStatusWARM, models.TenantActivityStatusFROZEN: 161 msgs = append(msgs, fmt.Sprintf( 162 "not yet supported activity status '%s' for tenant %q", status, tenant.Name)) 163 default: 164 if status == "" && allowEmpty { 165 continue 166 } 167 msgs = append(msgs, fmt.Sprintf( 168 "invalid activity status '%s' for tenant %q", status, tenant.Name)) 169 } 170 } 171 172 if len(msgs) != 0 { 173 return uco.NewErrInvalidUserInput(strings.Join(msgs, ", ")) 174 } 175 return nil 176 } 177 178 func (m *Manager) onAddTenants(ctx context.Context, class *models.Class, request AddTenantsPayload, 179 ) error { 180 st := sharding.State{ 181 Physical: make(map[string]sharding.Physical, len(request.Tenants)), 182 } 183 st.SetLocalName(m.clusterState.LocalName()) 184 pairs := make([]KeyValuePair, 0, len(request.Tenants)) 185 for _, p := range request.Tenants { 186 if _, ok := st.Physical[p.Name]; !ok { 187 p := st.AddPartition(p.Name, p.Nodes, p.Status) 188 data, err := json.Marshal(p) 189 if err != nil { 190 return fmt.Errorf("cannot marshal partition %s: %w", p.Name, err) 191 } 192 pairs = append(pairs, KeyValuePair{p.Name, data}) 193 } 194 } 195 creates := make([]*migrate.CreateTenantPayload, 0, len(request.Tenants)) 196 for _, p := range request.Tenants { 197 if st.IsLocalShard(p.Name) { 198 creates = append(creates, &migrate.CreateTenantPayload{ 199 Name: p.Name, 200 Status: p.Status, 201 }) 202 } 203 } 204 205 commit, err := m.migrator.NewTenants(ctx, class, creates) 206 if err != nil { 207 return fmt.Errorf("migrator.new_tenants: %w", err) 208 } 209 210 m.logger. 211 WithField("action", "schema.add_tenants"). 212 Debug("saving updated schema to configuration store") 213 214 if err = m.repo.NewShards(ctx, class.Class, pairs); err != nil { 215 commit(false) // rollback adding new tenant 216 return err 217 } 218 commit(true) // commit new adding new tenant 219 m.schemaCache.LockGuard(func() { 220 ost := m.schemaCache.ShardingState[request.Class] 221 for name, p := range st.Physical { 222 if ost.Physical == nil { 223 m.schemaCache.ShardingState[request.Class].Physical = make(map[string]sharding.Physical) 224 } 225 ost.Physical[name] = p 226 } 227 }) 228 229 return nil 230 } 231 232 // UpdateTenants is used to set activity status of tenants of a class. 233 // 234 // Class must exist and has partitioning enabled 235 func (m *Manager) UpdateTenants(ctx context.Context, principal *models.Principal, 236 class string, tenants []*models.Tenant, 237 ) error { 238 if err := m.Authorizer.Authorize(principal, "update", tenantsPath); err != nil { 239 return err 240 } 241 validated, err := validateTenants(tenants) 242 if err != nil { 243 return err 244 } 245 if err := validateActivityStatuses(validated, false); err != nil { 246 return err 247 } 248 cls := m.getClassByName(class) 249 if cls == nil { 250 return fmt.Errorf("class %q: %w", class, ErrNotFound) 251 } 252 if !schema.MultiTenancyEnabled(cls) { 253 return fmt.Errorf("multi-tenancy is not enabled for class %q", class) 254 } 255 256 request := UpdateTenantsPayload{ 257 Class: class, 258 Tenants: make([]TenantUpdate, len(tenants)), 259 } 260 for i, tenant := range tenants { 261 request.Tenants[i] = TenantUpdate{Name: tenant.Name, Status: tenant.ActivityStatus} 262 } 263 264 // open cluster-wide transaction 265 tx, err := m.cluster.BeginTransaction(ctx, updateTenants, 266 request, DefaultTxTTL) 267 if err != nil { 268 return fmt.Errorf("open cluster-wide transaction: %w", err) 269 } 270 271 if err := m.cluster.CommitWriteTransaction(ctx, tx); err != nil { 272 m.logger.WithError(err).Errorf("not every node was able to commit") 273 } 274 275 return m.onUpdateTenants(ctx, cls, request) // actual update 276 } 277 278 func (m *Manager) onUpdateTenants(ctx context.Context, class *models.Class, request UpdateTenantsPayload, 279 ) error { 280 ssCopy := sharding.State{Physical: make(map[string]sharding.Physical)} 281 ssCopy.SetLocalName(m.clusterState.LocalName()) 282 283 if err := m.schemaCache.RLockGuard(func() error { 284 ss, ok := m.schemaCache.ShardingState[class.Class] 285 if !ok { 286 return fmt.Errorf("sharding state for class '%s' not found", class.Class) 287 } 288 for _, tu := range request.Tenants { 289 physical, ok := ss.Physical[tu.Name] 290 if !ok { 291 return fmt.Errorf("tenant '%s' not found", tu.Name) 292 } 293 // skip if status does not change 294 if physical.ActivityStatus() == tu.Status { 295 continue 296 } 297 ssCopy.Physical[tu.Name] = physical.DeepCopy() 298 } 299 return nil 300 }); err != nil { 301 return err 302 } 303 304 schemaUpdates := make([]KeyValuePair, 0, len(ssCopy.Physical)) 305 migratorUpdates := make([]*migrate.UpdateTenantPayload, 0, len(ssCopy.Physical)) 306 for _, tu := range request.Tenants { 307 physical, ok := ssCopy.Physical[tu.Name] 308 if !ok { // not present due to status not changed, skip 309 continue 310 } 311 312 physical.Status = tu.Status 313 ssCopy.Physical[tu.Name] = physical 314 data, err := json.Marshal(physical) 315 if err != nil { 316 return fmt.Errorf("cannot marshal shard %s: %w", tu.Name, err) 317 } 318 schemaUpdates = append(schemaUpdates, KeyValuePair{tu.Name, data}) 319 320 // skip if not local 321 if ssCopy.IsLocalShard(tu.Name) { 322 migratorUpdates = append(migratorUpdates, &migrate.UpdateTenantPayload{ 323 Name: tu.Name, 324 Status: tu.Status, 325 }) 326 } 327 } 328 329 commit, err := m.migrator.UpdateTenants(ctx, class, migratorUpdates) 330 if err != nil { 331 m.logger.WithField("action", "update_tenants"). 332 WithField("class", request.Class).Error(err) 333 } 334 335 m.logger. 336 WithField("action", "schema.update_tenants"). 337 WithField("n", len(request.Tenants)).Debugf("persist schema updates") 338 339 if err := m.repo.UpdateShards(ctx, class.Class, schemaUpdates); err != nil { 340 commit(false) // rollback update of tenants 341 return err 342 } 343 commit(true) // commit update of tenants 344 345 // update cache 346 m.schemaCache.LockGuard(func() { 347 if ss := m.schemaCache.ShardingState[request.Class]; ss != nil { 348 for name, physical := range ssCopy.Physical { 349 ss.Physical[name] = physical 350 } 351 } 352 }) 353 354 return nil 355 } 356 357 // DeleteTenants is used to delete tenants of a class. 358 // 359 // Class must exist and has partitioning enabled 360 func (m *Manager) DeleteTenants(ctx context.Context, principal *models.Principal, class string, tenants []string) error { 361 if err := m.Authorizer.Authorize(principal, "delete", tenantsPath); err != nil { 362 return err 363 } 364 for i, name := range tenants { 365 if name == "" { 366 return fmt.Errorf("empty tenant name at index %d", i) 367 } 368 } 369 cls := m.getClassByName(class) 370 if cls == nil { 371 return fmt.Errorf("class %q: %w", class, ErrNotFound) 372 } 373 if !schema.MultiTenancyEnabled(cls) { 374 return fmt.Errorf("multi-tenancy is not enabled for class %q", class) 375 } 376 377 request := DeleteTenantsPayload{ 378 Class: class, 379 Tenants: tenants, 380 } 381 382 // open cluster-wide transaction 383 tx, err := m.cluster.BeginTransaction(ctx, deleteTenants, 384 request, DefaultTxTTL) 385 if err != nil { 386 return fmt.Errorf("open cluster-wide transaction: %w", err) 387 } 388 389 if err := m.cluster.CommitWriteTransaction(ctx, tx); err != nil { 390 m.logger.WithError(err).Errorf("not every node was able to commit") 391 } 392 393 return m.onDeleteTenants(ctx, cls, request) // actual update 394 } 395 396 func (m *Manager) onDeleteTenants(ctx context.Context, class *models.Class, req DeleteTenantsPayload, 397 ) error { 398 commit, err := m.migrator.DeleteTenants(ctx, class, req.Tenants) 399 if err != nil { 400 m.logger.WithField("action", "delete_tenants"). 401 WithField("class", req.Class).Error(err) 402 } 403 404 m.logger. 405 WithField("action", "schema.delete_tenants"). 406 WithField("n", len(req.Tenants)).Debugf("persist schema updates") 407 408 if err := m.repo.DeleteShards(ctx, class.Class, req.Tenants); err != nil { 409 commit(false) // rollback deletion of tenants 410 return err 411 } 412 commit(true) // commit deletion of tenants 413 414 // update cache 415 m.schemaCache.LockGuard(func() { 416 if ss := m.schemaCache.ShardingState[req.Class]; ss != nil { 417 for _, p := range req.Tenants { 418 ss.DeletePartition(p) 419 } 420 } 421 }) 422 423 return nil 424 } 425 426 // GetTenants is used to get tenants of a class. 427 // 428 // Class must exist and has partitioning enabled 429 func (m *Manager) GetTenants(ctx context.Context, principal *models.Principal, class string) ([]*models.Tenant, error) { 430 if err := m.Authorizer.Authorize(principal, "get", tenantsPath); err != nil { 431 return nil, err 432 } 433 // validation 434 cls := m.getClassByName(class) 435 if cls == nil { 436 return nil, fmt.Errorf("class %q: %w", class, ErrNotFound) 437 } 438 if !schema.MultiTenancyEnabled(cls) { 439 return nil, fmt.Errorf("multi-tenancy is not enabled for class %q", class) 440 } 441 442 var tenants []*models.Tenant 443 m.schemaCache.RLockGuard(func() error { 444 if ss := m.schemaCache.ShardingState[cls.Class]; ss != nil { 445 tenants = make([]*models.Tenant, len(ss.Physical)) 446 i := 0 447 for tenant := range ss.Physical { 448 tenants[i] = &models.Tenant{ 449 Name: tenant, 450 ActivityStatus: schema.ActivityStatus(ss.Physical[tenant].Status), 451 } 452 i++ 453 } 454 } 455 return nil 456 }) 457 458 return tenants, nil 459 }