github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/domain/tenant/service.go (about) 1 package tenant 2 3 import ( 4 "context" 5 6 "github.com/kyma-incubator/compass/components/director/internal/repo" 7 "github.com/kyma-incubator/compass/components/director/pkg/str" 8 tenantpkg "github.com/kyma-incubator/compass/components/director/pkg/tenant" 9 10 "github.com/kyma-incubator/compass/components/director/internal/model" 11 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 12 "github.com/kyma-incubator/compass/components/director/pkg/log" 13 "github.com/kyma-incubator/compass/components/director/pkg/resource" 14 "github.com/pkg/errors" 15 ) 16 17 const ( 18 // SubdomainLabelKey is the key of the tenant label for subdomain. 19 SubdomainLabelKey = "subdomain" 20 // RegionLabelKey is the key of the tenant label for region. 21 RegionLabelKey = "region" 22 // LicenseTypeLabelKey is the key of the tenant label for licensetype. 23 LicenseTypeLabelKey = "licensetype" 24 ) 25 26 // TenantMappingRepository is responsible for the repo-layer tenant operations. 27 // 28 //go:generate mockery --name=TenantMappingRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 29 type TenantMappingRepository interface { 30 UnsafeCreate(ctx context.Context, item model.BusinessTenantMapping) error 31 Upsert(ctx context.Context, item model.BusinessTenantMapping) error 32 Update(ctx context.Context, model *model.BusinessTenantMapping) error 33 Get(ctx context.Context, id string) (*model.BusinessTenantMapping, error) 34 GetByExternalTenant(ctx context.Context, externalTenant string) (*model.BusinessTenantMapping, error) 35 Exists(ctx context.Context, id string) (bool, error) 36 List(ctx context.Context) ([]*model.BusinessTenantMapping, error) 37 ListPageBySearchTerm(ctx context.Context, searchTerm string, pageSize int, cursor string) (*model.BusinessTenantMappingPage, error) 38 ExistsByExternalTenant(ctx context.Context, externalTenant string) (bool, error) 39 DeleteByExternalTenant(ctx context.Context, externalTenant string) error 40 GetLowestOwnerForResource(ctx context.Context, resourceType resource.Type, objectID string) (string, error) 41 ListByExternalTenants(ctx context.Context, externalTenant []string) ([]*model.BusinessTenantMapping, error) 42 ListByParentAndType(ctx context.Context, parentID string, tenantType tenantpkg.Type) ([]*model.BusinessTenantMapping, error) 43 ListByType(ctx context.Context, tenantType tenantpkg.Type) ([]*model.BusinessTenantMapping, error) 44 GetCustomerIDParentRecursively(ctx context.Context, tenantID string) (string, error) 45 } 46 47 // LabelUpsertService is responsible for creating, or updating already existing labels, and their label definitions. 48 // 49 //go:generate mockery --name=LabelUpsertService --output=automock --outpkg=automock --case=underscore --disable-version-string 50 type LabelUpsertService interface { 51 UpsertLabel(ctx context.Context, tenant string, labelInput *model.LabelInput) error 52 } 53 54 // LabelRepository is responsible for the repo-layer label operations. 55 // 56 //go:generate mockery --name=LabelRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 57 type LabelRepository interface { 58 ListForObject(ctx context.Context, tenant string, objectType model.LabelableObject, objectID string) (map[string]*model.Label, error) 59 } 60 61 // UIDService is responsible for generating GUIDs, which will be used as internal tenant IDs when tenants are created. 62 // 63 //go:generate mockery --name=UIDService --output=automock --outpkg=automock --case=underscore --disable-version-string 64 type UIDService interface { 65 Generate() string 66 } 67 68 type labeledService struct { 69 service 70 labelRepo LabelRepository 71 labelUpsertSvc LabelUpsertService 72 } 73 74 type service struct { 75 uidService UIDService 76 tenantMappingRepo TenantMappingRepository 77 converter BusinessTenantMappingConverter 78 } 79 80 // NewService returns a new object responsible for service-layer tenant operations. 81 func NewService(tenantMapping TenantMappingRepository, uidService UIDService, converter BusinessTenantMappingConverter) *service { 82 return &service{ 83 uidService: uidService, 84 tenantMappingRepo: tenantMapping, 85 converter: converter, 86 } 87 } 88 89 // NewServiceWithLabels returns a new entity responsible for service-layer tenant operations, including operations with labels like listing all labels related to the given tenant. 90 func NewServiceWithLabels(tenantMapping TenantMappingRepository, uidService UIDService, labelRepo LabelRepository, labelUpsertSvc LabelUpsertService, converter BusinessTenantMappingConverter) *labeledService { 91 return &labeledService{ 92 service: service{ 93 uidService: uidService, 94 tenantMappingRepo: tenantMapping, 95 converter: converter, 96 }, 97 labelRepo: labelRepo, 98 labelUpsertSvc: labelUpsertSvc, 99 } 100 } 101 102 // GetExternalTenant returns the external tenant ID of the tenant with the corresponding internal tenant ID. 103 func (s *service) GetExternalTenant(ctx context.Context, id string) (string, error) { 104 mapping, err := s.tenantMappingRepo.Get(ctx, id) 105 if err != nil { 106 return "", errors.Wrap(err, "while getting the external tenant") 107 } 108 109 return mapping.ExternalTenant, nil 110 } 111 112 // GetInternalTenant returns the internal tenant ID of the tenant with the corresponding external tenant ID. 113 func (s *service) GetInternalTenant(ctx context.Context, externalTenant string) (string, error) { 114 mapping, err := s.tenantMappingRepo.GetByExternalTenant(ctx, externalTenant) 115 if err != nil { 116 return "", errors.Wrap(err, "while getting the internal tenant") 117 } 118 119 return mapping.ID, nil 120 } 121 122 // List returns all tenants present in the Compass storage. 123 func (s *service) List(ctx context.Context) ([]*model.BusinessTenantMapping, error) { 124 return s.tenantMappingRepo.List(ctx) 125 } 126 127 // ListsByExternalIDs returns all tenants for provided external IDs. 128 func (s *service) ListsByExternalIDs(ctx context.Context, ids []string) ([]*model.BusinessTenantMapping, error) { 129 return s.tenantMappingRepo.ListByExternalTenants(ctx, ids) 130 } 131 132 // ListsByType returns all tenants for provided external IDs. 133 func (s *service) ListByType(ctx context.Context, tenantType tenantpkg.Type) ([]*model.BusinessTenantMapping, error) { 134 return s.tenantMappingRepo.ListByType(ctx, tenantType) 135 } 136 137 // ListPageBySearchTerm returns all tenants present in the Compass storage. 138 func (s *service) ListPageBySearchTerm(ctx context.Context, searchTerm string, pageSize int, cursor string) (*model.BusinessTenantMappingPage, error) { 139 return s.tenantMappingRepo.ListPageBySearchTerm(ctx, searchTerm, pageSize, cursor) 140 } 141 142 // GetTenantByExternalID returns the tenant with the provided external ID. 143 func (s *service) GetTenantByExternalID(ctx context.Context, id string) (*model.BusinessTenantMapping, error) { 144 return s.tenantMappingRepo.GetByExternalTenant(ctx, id) 145 } 146 147 // GetTenantByID returns the tenant with the provided ID. 148 func (s *service) GetTenantByID(ctx context.Context, id string) (*model.BusinessTenantMapping, error) { 149 return s.tenantMappingRepo.Get(ctx, id) 150 } 151 152 // GetLowestOwnerForResource returns the lowest tenant in the hierarchy that is owner of a given resource. 153 func (s *service) GetLowestOwnerForResource(ctx context.Context, resourceType resource.Type, objectID string) (string, error) { 154 return s.tenantMappingRepo.GetLowestOwnerForResource(ctx, resourceType, objectID) 155 } 156 157 // MultipleToTenantMapping assigns a new internal ID to all the provided tenants, and returns the BusinessTenantMappingInputs as BusinessTenantMappings. 158 func (s *service) MultipleToTenantMapping(tenantInputs []model.BusinessTenantMappingInput) []model.BusinessTenantMapping { 159 tenants := make([]model.BusinessTenantMapping, 0, len(tenantInputs)) 160 tenantIDs := make(map[string]string, len(tenantInputs)) 161 for _, tenant := range tenantInputs { 162 id := s.uidService.Generate() 163 tenants = append(tenants, *tenant.ToBusinessTenantMapping(id)) 164 tenantIDs[tenant.ExternalTenant] = id 165 } 166 for i := 0; i < len(tenants); i++ { // Convert parent ID from external to internal id reference 167 if len(tenants[i].Parent) > 0 { 168 if _, ok := tenantIDs[tenants[i].Parent]; ok { // If the parent is inserted in this request (otherwise we assume that it is already in the db) 169 tenants[i].Parent = tenantIDs[tenants[i].Parent] 170 171 var moved bool 172 tenants, moved = MoveBeforeIfShould(tenants, tenants[i].Parent, i) // Move my parent before me (to be inserted first) if it is not already 173 if moved { 174 i-- // Process the moved parent as well 175 } 176 } 177 } 178 } 179 return tenants 180 } 181 182 // Update updates tenant 183 func (s *service) Update(ctx context.Context, id string, tenantInput model.BusinessTenantMappingInput) error { 184 tenant := tenantInput.ToBusinessTenantMapping(id) 185 186 if err := s.tenantMappingRepo.Update(ctx, tenant); err != nil { 187 return errors.Wrapf(err, "while updating tenant with id %s", id) 188 } 189 190 return nil 191 } 192 193 // GetCustomerIDParentRecursively gets the top parent external ID (customer_id) for a given tenant 194 func (s *service) GetCustomerIDParentRecursively(ctx context.Context, tenantID string) (string, error) { 195 return s.tenantMappingRepo.GetCustomerIDParentRecursively(ctx, tenantID) 196 } 197 198 // CreateTenantAccessForResource creates a tenant access for a single resource.Type 199 func (s *service) CreateTenantAccessForResource(ctx context.Context, tenantAccess *model.TenantAccess) error { 200 resourceType := tenantAccess.ResourceType 201 m2mTable, ok := resourceType.TenantAccessTable() 202 if !ok { 203 return errors.Errorf("entity %q does not have access table", resourceType) 204 } 205 206 ta := s.converter.TenantAccessToEntity(tenantAccess) 207 208 if err := repo.CreateSingleTenantAccess(ctx, m2mTable, ta); err != nil { 209 return errors.Wrapf(err, "while creating tenant acccess for resource type %q with ID %q for tenant %q", string(resourceType), ta.ResourceID, ta.TenantID) 210 } 211 212 return nil 213 } 214 215 // CreateTenantAccessForResourceRecursively creates a tenant access for a single resource.Type recursively 216 func (s *service) CreateTenantAccessForResourceRecursively(ctx context.Context, tenantAccess *model.TenantAccess) error { 217 resourceType := tenantAccess.ResourceType 218 m2mTable, ok := resourceType.TenantAccessTable() 219 if !ok { 220 return errors.Errorf("entity %q does not have access table", resourceType) 221 } 222 223 ta := s.converter.TenantAccessToEntity(tenantAccess) 224 225 if err := repo.CreateTenantAccessRecursively(ctx, m2mTable, ta); err != nil { 226 return errors.Wrapf(err, "while creating tenant acccess for resource type %q with ID %q for tenant %q", string(resourceType), ta.ResourceID, ta.TenantID) 227 } 228 229 return nil 230 } 231 232 // DeleteTenantAccessForResourceRecursively deletes a tenant access for a single resource.Type recursively 233 func (s *service) DeleteTenantAccessForResourceRecursively(ctx context.Context, tenantAccess *model.TenantAccess) error { 234 resourceType := tenantAccess.ResourceType 235 m2mTable, ok := resourceType.TenantAccessTable() 236 if !ok { 237 return errors.Errorf("entity %q does not have access table", resourceType) 238 } 239 240 ta := s.converter.TenantAccessToEntity(tenantAccess) 241 242 if err := repo.DeleteTenantAccessRecursively(ctx, m2mTable, tenantAccess.InternalTenantID, []string{tenantAccess.ResourceID}); err != nil { 243 return errors.Wrapf(err, "while deleting tenant acccess for resource type %q with ID %q for tenant %q", string(resourceType), ta.ResourceID, ta.TenantID) 244 } 245 246 return nil 247 } 248 249 // GetTenantAccessForResource gets a tenant access record for the specified resource 250 func (s *service) GetTenantAccessForResource(ctx context.Context, tenantID, resourceID string, resourceType resource.Type) (*model.TenantAccess, error) { 251 m2mTable, ok := resourceType.TenantAccessTable() 252 if !ok { 253 return nil, errors.Errorf("entity %q does not have access table", resourceType) 254 } 255 256 ta, err := repo.GetSingleTenantAccess(ctx, m2mTable, tenantID, resourceID) 257 if err != nil { 258 return nil, err 259 } 260 261 tenantAccessModel := s.converter.TenantAccessFromEntity(ta) 262 tenantAccessModel.ResourceType = resourceType 263 264 return tenantAccessModel, nil 265 } 266 267 // ListByParentAndType list tenants by parent ID and tenant.Type 268 func (s *service) ListByParentAndType(ctx context.Context, parentID string, tenantType tenantpkg.Type) ([]*model.BusinessTenantMapping, error) { 269 return s.tenantMappingRepo.ListByParentAndType(ctx, parentID, tenantType) 270 } 271 272 // ExtractTenantIDForTenantScopedFormationTemplates returns the tenant ID based on its type: 273 // 1. If it's a SA -> return its parent GA id 274 // 2. If it's any other tenant type -> return its ID 275 func (s *service) ExtractTenantIDForTenantScopedFormationTemplates(ctx context.Context) (string, error) { 276 internalTenantID, err := s.getTenantFromContext(ctx) 277 if err != nil { 278 return "", err 279 } 280 281 if internalTenantID == "" { 282 return "", nil 283 } 284 285 tenantObject, err := s.GetTenantByID(ctx, internalTenantID) 286 if err != nil { 287 return "", err 288 } 289 290 if tenantObject.Type == tenantpkg.Subaccount { 291 return tenantObject.Parent, nil 292 } 293 294 return tenantObject.ID, nil 295 } 296 297 // getTenantFromContext validates and returns the tenant present in the context: 298 // - if both internalID and externalID are present -> proceed with tenant scoped formation templates (return the internalID from ctx) 299 // - if both internalID and externalID are NOT present -> -> proceed with global formation templates (return empty id) 300 // - otherwise return TenantNotFoundError 301 func (s *service) getTenantFromContext(ctx context.Context) (string, error) { 302 tntCtx, err := LoadTenantPairFromContextNoChecks(ctx) 303 if err != nil { 304 return "", err 305 } 306 307 if tntCtx.InternalID != "" && tntCtx.ExternalID != "" { 308 return tntCtx.InternalID, nil 309 } 310 311 if tntCtx.InternalID == "" && tntCtx.ExternalID == "" { 312 return "", nil 313 } 314 315 return "", apperrors.NewTenantNotFoundError(tntCtx.ExternalID) 316 } 317 318 // CreateManyIfNotExists creates all provided tenants if they do not exist. 319 // It creates or updates the subdomain and region labels of the provided tenants, no matter if they are pre-existing or not. 320 func (s *labeledService) CreateManyIfNotExists(ctx context.Context, tenantInputs ...model.BusinessTenantMappingInput) ([]string, error) { 321 return s.upsertTenants(ctx, tenantInputs, s.tenantMappingRepo.UnsafeCreate) 322 } 323 324 // UpsertMany creates all provided tenants if they do not exist. If they do exist, they are internally updated. 325 // It creates or updates the subdomain and region labels of the provided tenants, no matter if they are pre-existing or not. 326 func (s *labeledService) UpsertMany(ctx context.Context, tenantInputs ...model.BusinessTenantMappingInput) ([]string, error) { 327 return s.upsertTenants(ctx, tenantInputs, s.tenantMappingRepo.Upsert) 328 } 329 330 // UpsertSingle creates a provided tenant if it does not exist. If it does exist, it is internally updated. 331 // It creates or updates the subdomain and region labels of the provided tenant, no matter if it is pre-existing or not. 332 func (s *labeledService) UpsertSingle(ctx context.Context, tenantInput model.BusinessTenantMappingInput) (string, error) { 333 return s.upsertTenant(ctx, tenantInput, s.tenantMappingRepo.Upsert) 334 } 335 336 func (s *labeledService) upsertTenant(ctx context.Context, tenantInput model.BusinessTenantMappingInput, upsertFunc func(context.Context, model.BusinessTenantMapping) error) (string, error) { 337 id := s.uidService.Generate() 338 tenant := *tenantInput.ToBusinessTenantMapping(id) 339 subdomains, regions := tenantLocality([]model.BusinessTenantMappingInput{tenantInput}) 340 341 subdomain := "" 342 region := "" 343 if s, ok := subdomains[tenant.ExternalTenant]; ok { 344 subdomain = s 345 } 346 if r, ok := regions[tenant.ExternalTenant]; ok { 347 region = r 348 } 349 350 tenantID, err := s.createIfNotExists(ctx, tenant, subdomain, region, upsertFunc) 351 if err != nil { 352 return "", errors.Wrapf(err, "while creating tenant with external ID %s", tenant.ExternalTenant) 353 } 354 355 return tenantID, nil 356 } 357 358 func (s *labeledService) upsertTenants(ctx context.Context, tenantInputs []model.BusinessTenantMappingInput, upsertFunc func(context.Context, model.BusinessTenantMapping) error) ([]string, error) { 359 tenants := s.MultipleToTenantMapping(tenantInputs) 360 subdomains, regions := tenantLocality(tenantInputs) 361 tenantIDs := make([]string, 0, len(tenants)) 362 363 for tenantIdx, tenant := range tenants { 364 subdomain := "" 365 region := "" 366 if s, ok := subdomains[tenant.ExternalTenant]; ok { 367 subdomain = s 368 } 369 if r, ok := regions[tenant.ExternalTenant]; ok { 370 region = r 371 } 372 tenantID, err := s.createIfNotExists(ctx, tenant, subdomain, region, upsertFunc) 373 if err != nil { 374 return nil, errors.Wrapf(err, "while creating tenant with external ID %s", tenant.ExternalTenant) 375 } 376 // the tenant already exists in our DB with a different ID, and we should update all child resources to use the correct internal ID 377 tenantIDs = append(tenantIDs, tenantID) 378 if tenantID != tenant.ID { 379 for i := tenantIdx; i < len(tenants); i++ { 380 if tenants[i].Parent == tenant.ID { 381 tenants[i].Parent = tenantID 382 } 383 } 384 } 385 } 386 387 return tenantIDs, nil 388 } 389 390 func (s *labeledService) createIfNotExists(ctx context.Context, tenant model.BusinessTenantMapping, subdomain, region string, action func(context.Context, model.BusinessTenantMapping) error) (string, error) { 391 if err := action(ctx, tenant); err != nil { 392 return "", err 393 } 394 395 tenantFromDB, err := s.tenantMappingRepo.GetByExternalTenant(ctx, tenant.ExternalTenant) 396 if err != nil { 397 return "", errors.Wrapf(err, "while retrieving the internal tenant ID of tenant with external ID %s", tenant.ExternalTenant) 398 } 399 400 return tenantFromDB.ID, s.upsertLabels(ctx, tenantFromDB.ID, subdomain, region, str.PtrStrToStr(tenant.LicenseType)) 401 } 402 403 func (s *labeledService) upsertLabels(ctx context.Context, tenantID, subdomain, region, licenseType string) error { 404 if len(subdomain) > 0 { 405 if err := s.upsertLabel(ctx, tenantID, SubdomainLabelKey, subdomain); err != nil { 406 return errors.Wrapf(err, "while setting subdomain label for tenant with ID %s", tenantID) 407 } 408 } 409 if len(region) > 0 { 410 if err := s.upsertLabel(ctx, tenantID, RegionLabelKey, region); err != nil { 411 return errors.Wrapf(err, "while setting subdomain label for tenant with ID %s", tenantID) 412 } 413 } 414 if len(licenseType) > 0 { 415 if err := s.upsertLabel(ctx, tenantID, LicenseTypeLabelKey, licenseType); err != nil { 416 return errors.Wrapf(err, "while setting licenseType label for tenant with ID %s", tenantID) 417 } 418 } 419 return nil 420 } 421 422 func tenantLocality(tenants []model.BusinessTenantMappingInput) (map[string]string, map[string]string) { 423 subdomains := make(map[string]string) 424 regions := make(map[string]string) 425 for _, t := range tenants { 426 if len(t.Subdomain) > 0 { 427 subdomains[t.ExternalTenant] = t.Subdomain 428 } 429 if len(t.Region) > 0 { 430 regions[t.ExternalTenant] = t.Region 431 } 432 } 433 434 return subdomains, regions 435 } 436 437 // DeleteMany removes all tenants with the provided external tenant ids from the Compass storage. 438 func (s *service) DeleteMany(ctx context.Context, externalTenantIDs []string) error { 439 for _, externalTenantID := range externalTenantIDs { 440 err := s.tenantMappingRepo.DeleteByExternalTenant(ctx, externalTenantID) 441 if err != nil { 442 return errors.Wrap(err, "while deleting tenant") 443 } 444 } 445 446 return nil 447 } 448 449 // ListLabels returns all labels directly linked to the given tenant, like subdomain or region. 450 // That excludes labels of other resource types in the context of the given tenant, for example labels of an application in the given tenant - those labels are not returned. 451 func (s *labeledService) ListLabels(ctx context.Context, tenantID string) (map[string]*model.Label, error) { 452 log.C(ctx).Infof("getting labels for tenant with ID %s", tenantID) 453 if err := s.ensureTenantExists(ctx, tenantID); err != nil { 454 return nil, err 455 } 456 457 labels, err := s.labelRepo.ListForObject(ctx, tenantID, model.TenantLabelableObject, tenantID) 458 if err != nil { 459 return nil, errors.Wrapf(err, "whilie listing labels for tenant with ID %s", tenantID) 460 } 461 462 return labels, nil 463 } 464 465 func (s *labeledService) upsertLabel(ctx context.Context, tenantID, key, value string) error { 466 label := &model.LabelInput{ 467 Key: key, 468 Value: value, 469 ObjectID: tenantID, 470 ObjectType: model.TenantLabelableObject, 471 } 472 return s.labelUpsertSvc.UpsertLabel(ctx, tenantID, label) 473 } 474 475 func (s *service) ensureTenantExists(ctx context.Context, id string) error { 476 exists, err := s.tenantMappingRepo.Exists(ctx, id) 477 if err != nil { 478 return errors.Wrapf(err, "while checking if tenant with ID %s exists", id) 479 } 480 481 if !exists { 482 return apperrors.NewNotFoundError(resource.Tenant, id) 483 } 484 485 return nil 486 } 487 488 // MoveBeforeIfShould moves the tenant with id right before index only if it is not already before it 489 func MoveBeforeIfShould(tenants []model.BusinessTenantMapping, id string, indx int) ([]model.BusinessTenantMapping, bool) { 490 var itemIndex int 491 for i, tenant := range tenants { 492 if tenant.ID == id { 493 itemIndex = i 494 } 495 } 496 497 if itemIndex <= indx { // already before indx 498 return tenants, false 499 } 500 501 newTenants := make([]model.BusinessTenantMapping, 0, len(tenants)) 502 for i := range tenants { 503 if i == itemIndex { 504 continue 505 } 506 if i == indx { 507 newTenants = append(newTenants, tenants[itemIndex], tenants[i]) 508 continue 509 } 510 newTenants = append(newTenants, tenants[i]) 511 } 512 return newTenants, true 513 }