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  }