diff --git a/chart/compass/charts/tenant-fetcher/templates/deployment.yaml b/chart/compass/charts/tenant-fetcher/templates/deployment.yaml index 24ae755e1f..48f4960487 100644 --- a/chart/compass/charts/tenant-fetcher/templates/deployment.yaml +++ b/chart/compass/charts/tenant-fetcher/templates/deployment.yaml @@ -59,6 +59,8 @@ spec: value: {{ .Values.global.tenantFetcher.tenantProvider.tenantIdProperty }} - name: APP_TENANT_PROVIDER_CUSTOMER_ID_PROPERTY value: {{ .Values.global.tenantFetcher.tenantProvider.customerIdProperty }} + - name: APP_TENANT_PROVIDER_SUBACCOUNT_TENANT_ID_PROPERTY + value: {{ .Values.global.tenantFetcher.tenantProvider.subaccountTenantIdProperty }} - name: APP_TENANT_PROVIDER_SUBDOMAIN_PROPERTY value: {{ .Values.global.tenantFetcher.tenantProvider.subdomainProperty }} - name: APP_TENANT_PROVIDER @@ -70,11 +72,17 @@ spec: - name: APP_ROOT_API value: "{{ .Values.global.tenantFetcher.prefix }}" - name: APP_HANDLER_ENDPOINT - value: "{{ .Values.server.handlerEndpoint }}" + value: "{{ .Values.global.tenantFetcher.server.handlerEndpoint }}" + - name: APP_REGIONAL_HANDLER_ENDPOINT + value: "{{ .Values.global.tenantFetcher.server.regionalHandlerEndpoint }}" + - name: APP_DEPENDENCIES_ENDPOINT + value: "{{ .Values.global.tenantFetcher.server.dependenciesEndpoint }}" + - name: APP_TENANT_PATH_PARAM + value: "{{ .Values.global.tenantFetcher.server.tenantPathParam }}" + - name: APP_REGION_PATH_PARAM + value: "{{ .Values.global.tenantFetcher.server.regionPathParam }}" - name: APP_JWKS_ENDPOINT value: "{{ .Values.global.tenantFetcher.authentication.jwksEndpoint }}" - - name: APP_TENANT_PATH_PARAM - value: "{{ .Values.server.tenantPathParam }}" - name: APP_DB_USER valueFrom: secretKeyRef: diff --git a/chart/compass/charts/tenant-fetcher/values.yaml b/chart/compass/charts/tenant-fetcher/values.yaml index 3e4cef780b..963eba3798 100644 --- a/chart/compass/charts/tenant-fetcher/values.yaml +++ b/chart/compass/charts/tenant-fetcher/values.yaml @@ -22,6 +22,3 @@ database: maxOpenConnections: 2 maxIdleConnections: 1 -server: - handlerEndpoint: "/v1/callback/{tenantId}" - tenantPathParam: "tenantId" diff --git a/chart/compass/templates/tenant-fetcher-job.yaml b/chart/compass/templates/tenant-fetcher-job.yaml index 23e3d37981..9067a01c74 100644 --- a/chart/compass/templates/tenant-fetcher-job.yaml +++ b/chart/compass/templates/tenant-fetcher-job.yaml @@ -89,6 +89,8 @@ spec: value: {{ $config.endpoints.movedRuntimeByLabel }} - name: APP_TENANT_PROVIDER value: {{ $config.providerName }} + - name: APP_TENANTS_REGION + value: {{ $config.tenantsRegion }} - name: APP_CLIENT_ID valueFrom: secretKeyRef: diff --git a/chart/compass/templates/tests/tenant-fetcher/tenant-fetcher-test.yaml b/chart/compass/templates/tests/tenant-fetcher/tenant-fetcher-test.yaml index 2a8eb01974..35aed1e8c3 100644 --- a/chart/compass/templates/tests/tenant-fetcher/tenant-fetcher-test.yaml +++ b/chart/compass/templates/tests/tenant-fetcher/tenant-fetcher-test.yaml @@ -39,10 +39,10 @@ spec: value: {{ .Values.global.tenantFetcher.tenantProvider.tenantIdProperty }} - name: APP_TENANT_PROVIDER_CUSTOMER_ID_PROPERTY value: {{ .Values.global.tenantFetcher.tenantProvider.customerIdProperty }} + - name: APP_TENANT_PROVIDER_SUBACCOUNT_TENANT_ID_PROPERTY + value: {{ .Values.global.tenantFetcher.tenantProvider.subaccountTenantIdProperty }} - name: APP_TENANT_PROVIDER_SUBDOMAIN_PROPERTY value: {{ .Values.global.tenantFetcher.tenantProvider.subdomainProperty }} - - name: APP_TENANT_PROVIDER - value: "test-provider" - name: APP_TENANT value: {{ .Values.global.defaultTenant }} - name: APP_TENANT_FETCHER_URL @@ -54,9 +54,15 @@ spec: - name: APP_ROOT_API value: "{{ .Values.global.tenantFetcher.prefix }}" - name: APP_HANDLER_ENDPOINT - value: "/v1/callback/{tenantId}" + value: "{{ .Values.global.tenantFetcher.server.handlerEndpoint }}" + - name: APP_REGIONAL_HANDLER_ENDPOINT + value: "{{ .Values.global.tenantFetcher.server.regionalHandlerEndpoint }}" + - name: APP_DEPENDENCIES_ENDPOINT + value: "{{ .Values.global.tenantFetcher.server.dependenciesEndpoint }}" - name: APP_TENANT_PATH_PARAM - value: "tenantId" + value: "{{ .Values.global.tenantFetcher.server.tenantPathParam }}" + - name: APP_REGION_PATH_PARAM + value: "{{ .Values.global.tenantFetcher.server.regionPathParam }}" - name: DIRECTOR_URL value: "https://{{ .Values.global.gateway.tls.host }}.{{ .Values.global.ingress.domainName }}{{ .Values.global.director.prefix }}" - name: APP_DB_NAME diff --git a/chart/compass/values.yaml b/chart/compass/values.yaml index 0d579030a7..e1e83dca21 100644 --- a/chart/compass/values.yaml +++ b/chart/compass/values.yaml @@ -75,7 +75,7 @@ global: version: "PR-2003" director: dir: - version: "PR-2010" + version: "PR-1998" gateway: dir: version: "PR-2003" @@ -87,7 +87,7 @@ global: version: "PR-41" schema_migrator: dir: - version: "PR-2010" + version: "PR-1998" system_broker: dir: version: "PR-2003" @@ -104,7 +104,7 @@ global: version: "PR-42" e2e_tests: dir: - version: "PR-2010" + version: "PR-1998" isLocalEnv: false oauth2: host: oauth2 @@ -349,8 +349,15 @@ global: tenantProvider: tenantIdProperty: "tenantId" customerIdProperty: "customerId" + subaccountTenantIdProperty: "subaccountTenantId" subdomainProperty: "subdomain" name: "provider" + server: + handlerEndpoint: "/v1/callback/{tenantId}" + regionalHandlerEndpoint: "/v1/regional/{region}/callback/{tenantId}" + dependenciesEndpoint: "/v1/dependencies" + tenantPathParam: "tenantId" + regionPathParam: "region" ordService: host: compass-ord-service.compass-system.svc.cluster.local diff --git a/components/director/cmd/tenantfetcher-job/main.go b/components/director/cmd/tenantfetcher-job/main.go index 6ac1a19bd1..a10a530f5a 100644 --- a/components/director/cmd/tenantfetcher-job/main.go +++ b/components/director/cmd/tenantfetcher-job/main.go @@ -35,6 +35,7 @@ type config struct { Features features.Config TenantProvider string `envconfig:"APP_TENANT_PROVIDER"` + TenantsRegion string `envconfig:"default=central,APP_TENANTS_REGION"` MetricsPushEndpoint string `envconfig:"optional,APP_METRICS_PUSH_ENDPOINT"` MovedRuntimeLabelKey string `envconfig:"default=moved_runtime,APP_MOVED_RUNTIME_LABEL_KEY"` ClientTimeout time.Duration `envconfig:"default=60s"` @@ -117,5 +118,5 @@ func createTenantFetcherSvc(cfg config, transact persistence.Transactioner, kube eventAPIClient.SetMetricsPusher(metricsPusher) } - return tenantfetcher.NewService(cfg.QueryConfig, transact, kubeClient, cfg.TenantFieldMapping, cfg.MovedRuntimeByLabelFieldMapping, cfg.TenantProvider, eventAPIClient, tenantStorageSvc, runtimeService, labelDefService, cfg.MovedRuntimeLabelKey, cfg.FullResyncInterval) + return tenantfetcher.NewService(cfg.QueryConfig, transact, kubeClient, cfg.TenantFieldMapping, cfg.MovedRuntimeByLabelFieldMapping, cfg.TenantProvider, cfg.TenantsRegion, eventAPIClient, tenantStorageSvc, runtimeService, labelDefService, cfg.MovedRuntimeLabelKey, cfg.FullResyncInterval) } diff --git a/components/director/cmd/tenantfetcher-svc/main.go b/components/director/cmd/tenantfetcher-svc/main.go index ab6a174939..58e106e701 100644 --- a/components/director/cmd/tenantfetcher-svc/main.go +++ b/components/director/cmd/tenantfetcher-svc/main.go @@ -58,6 +58,15 @@ type config struct { Handler tenantfetcher.HandlerConfig Database persistence.DatabaseConfig + + SecurityConfig securityConfig +} + +type securityConfig struct { + JWKSSyncPeriod time.Duration `envconfig:"default=5m"` + AllowJWTSigningNone bool `envconfig:"APP_ALLOW_JWT_SIGNING_NONE,default=false"` + JwksEndpoint string `envconfig:"APP_JWKS_ENDPOINT"` + SubscriptionCallbackScope string `envconfig:"APP_SUBSCRIPTION_CALLBACK_SCOPE"` } func main() { @@ -107,7 +116,7 @@ func initAPIHandler(ctx context.Context, cfg config, transact persistence.Transa router := mainRouter.PathPrefix(cfg.RootAPI).Subrouter() healthCheckRouter := mainRouter.PathPrefix(cfg.RootAPI).Subrouter() - configureAuthMiddleware(ctx, router, cfg.Handler) + configureAuthMiddleware(ctx, router, cfg.SecurityConfig) registerHandler(ctx, router, cfg.Handler, transact) @@ -159,7 +168,7 @@ func createServer(ctx context.Context, cfg config, handler http.Handler, name st return runFn, shutdownFn } -func configureAuthMiddleware(ctx context.Context, router *mux.Router, cfg tenantfetcher.HandlerConfig) { +func configureAuthMiddleware(ctx context.Context, router *mux.Router, cfg securityConfig) { scopeValidator := claims.NewScopesValidator([]string{cfg.SubscriptionCallbackScope}) middleware := auth.New(cfg.JwksEndpoint, cfg.AllowJWTSigningNone, "", scopeValidator) router.Use(middleware.Handler()) @@ -186,7 +195,7 @@ func registerHandler(ctx context.Context, router *mux.Router, cfg tenantfetcher. tenantRepo := tenant.NewRepository(converter) tenantSvc := tenant.NewServiceWithLabels(tenantRepo, uidSvc, labelRepo, labelUpsertSvc) - provisioner := tenantfetcher.NewTenantProvisioner(tenantSvc) + provisioner := tenantfetcher.NewTenantProvisioner(tenantSvc, cfg.TenantProvider) tenantHandler := tenantfetcher.NewTenantsHTTPHandler(provisioner, transact, cfg) log.C(ctx).Infof("Registering Tenant Onboarding endpoint on %s...", cfg.HandlerEndpoint) @@ -194,6 +203,15 @@ func registerHandler(ctx context.Context, router *mux.Router, cfg tenantfetcher. log.C(ctx).Infof("Registering Tenant Decommissioning endpoint on %s...", cfg.HandlerEndpoint) router.HandleFunc(cfg.HandlerEndpoint, tenantHandler.DeleteByExternalID).Methods(http.MethodDelete) + + log.C(ctx).Infof("Registering Regional Tenant Onboarding endpoint on %s...", cfg.RegionalHandlerEndpoint) + router.HandleFunc(cfg.RegionalHandlerEndpoint, tenantHandler.CreateRegional).Methods(http.MethodPut) + + log.C(ctx).Infof("Registering Regional Tenant Decommissioning endpoint on %s...", cfg.RegionalHandlerEndpoint) + router.HandleFunc(cfg.RegionalHandlerEndpoint, tenantHandler.DeleteByExternalID).Methods(http.MethodDelete) + + log.C(ctx).Infof("Registering service dependencies endpoint on %s...", cfg.DependenciesEndpoint) + router.HandleFunc(cfg.DependenciesEndpoint, tenantHandler.Dependencies).Methods(http.MethodGet) } func newReadinessHandler() func(writer http.ResponseWriter, request *http.Request) { diff --git a/components/director/internal/domain/tenant/converter.go b/components/director/internal/domain/tenant/converter.go index 2eec90f16b..84c8f73f53 100644 --- a/components/director/internal/domain/tenant/converter.go +++ b/components/director/internal/domain/tenant/converter.go @@ -9,12 +9,12 @@ import ( type converter struct{} -// NewConverter missing godoc +// NewConverter returns a new Converter that can later be used to make the conversions between the GraphQL, service, and repository layer representations of a Compass tenant. func NewConverter() *converter { return &converter{} } -// ToEntity missing godoc +// ToEntity converts the provided service-layer representation of a tenant to the repository-layer one tenant.Entity. func (c *converter) ToEntity(in *model.BusinessTenantMapping) *tenant.Entity { if in == nil { return nil @@ -30,7 +30,7 @@ func (c *converter) ToEntity(in *model.BusinessTenantMapping) *tenant.Entity { } } -// FromEntity missing godoc +// FromEntity converts the provided tenant.Entity repo-layer representation of a tenant to the service-layer representation model.BusinessTenantMapping. func (c *converter) FromEntity(in *tenant.Entity) *model.BusinessTenantMapping { if in == nil { return nil @@ -47,7 +47,7 @@ func (c *converter) FromEntity(in *tenant.Entity) *model.BusinessTenantMapping { } } -// ToGraphQL missing godoc +// ToGraphQL converts the provided model.BusinessTenantMapping service-layer representation of a tenant to the GraphQL-layer representation graphql.Tenant. func (c *converter) ToGraphQL(in *model.BusinessTenantMapping) *graphql.Tenant { if in == nil { return nil @@ -61,7 +61,7 @@ func (c *converter) ToGraphQL(in *model.BusinessTenantMapping) *graphql.Tenant { } } -// MultipleToGraphQL missing godoc +// MultipleToGraphQL converts all the provided model.BusinessTenantMapping service-layer representations of a tenant to the GraphQL-layer representations graphql.Tenant. func (c *converter) MultipleToGraphQL(in []*model.BusinessTenantMapping) []*graphql.Tenant { tenants := make([]*graphql.Tenant, 0, len(in)) for _, r := range in { diff --git a/components/director/internal/domain/tenant/fixtures_test.go b/components/director/internal/domain/tenant/fixtures_test.go index d63073fe3a..b2cb25b4d6 100644 --- a/components/director/internal/domain/tenant/fixtures_test.go +++ b/components/director/internal/domain/tenant/fixtures_test.go @@ -14,12 +14,16 @@ import ( ) const ( - testExternal = "external" - testID = "foo" - testName = "bar" - testSubdomain = "subdomain" - testProvider = "Compass" - initializedColumn = "initialized" + testExternal = "external" + testID = "foo" + testName = "bar" + testParentID = "parent" + testInternalParentID = "internal-parent" + testTemporaryInternalParentID = "internal-parent-temp" + testSubdomain = "subdomain" + testRegion = "eu-1" + testProvider = "Compass" + initializedColumn = "initialized" ) var ( @@ -28,12 +32,16 @@ var ( ) func newModelBusinessTenantMapping(id, name string) *model.BusinessTenantMapping { + return newModelBusinessTenantMappingWithType(id, name, "", tenant.Account) +} + +func newModelBusinessTenantMappingWithType(id, name, parent string, tenantType tenant.Type) *model.BusinessTenantMapping { return &model.BusinessTenantMapping{ ID: id, Name: name, ExternalTenant: testExternal, - Parent: "", - Type: tenant.Account, + Parent: parent, + Type: tenantType, Provider: testProvider, Status: tenant.Active, } @@ -99,13 +107,18 @@ func fixTenantMappingCreateArgs(ent tenant.Entity) []driver.Value { return []driver.Value{ent.ID, ent.Name, ent.ExternalTenant, ent.Parent, ent.Type, ent.ProviderName, ent.Status} } -func newModelBusinessTenantMappingInput(name, subdomain string) model.BusinessTenantMappingInput { +func newModelBusinessTenantMappingInput(name, subdomain, region string) model.BusinessTenantMappingInput { + return newModelBusinessTenantMappingInputWithType(testExternal, name, "", subdomain, region, tenant.Account) +} + +func newModelBusinessTenantMappingInputWithType(tenantID, name, parent, subdomain, region string, tenantType tenant.Type) model.BusinessTenantMappingInput { return model.BusinessTenantMappingInput{ Name: name, - ExternalTenant: testExternal, + ExternalTenant: tenantID, Subdomain: subdomain, - Parent: "", - Type: string(tenant.Account), + Region: region, + Parent: parent, + Type: tenant.TypeToStr(tenantType), Provider: testProvider, } } diff --git a/components/director/internal/domain/tenant/repository.go b/components/director/internal/domain/tenant/repository.go index f608d73d75..a5ae996561 100644 --- a/components/director/internal/domain/tenant/repository.go +++ b/components/director/internal/domain/tenant/repository.go @@ -36,7 +36,7 @@ var ( initializedComputedColumn = "initialized" ) -// Converter missing godoc +// Converter converts tenants between the model.BusinessTenantMapping service-layer representation of a tenant and the repo-layer representation tenant.Entity. //go:generate mockery --name=Converter --output=automock --outpkg=automock --case=underscore type Converter interface { ToEntity(in *model.BusinessTenantMapping) *tenant.Entity @@ -54,7 +54,7 @@ type pgRepository struct { conv Converter } -// NewRepository missing godoc +// NewRepository returns a new entity responsible for repo-layer tenant operations. All of its methods require persistence.PersistenceOp it the provided context. func NewRepository(conv Converter) *pgRepository { return &pgRepository{ creator: repo.NewCreator(resource.Tenant, tableName, tableColumns), @@ -67,12 +67,12 @@ func NewRepository(conv Converter) *pgRepository { } } -// Create missing godoc +// Create adds the provided tenant into the Compass storage. func (r *pgRepository) Create(ctx context.Context, item model.BusinessTenantMapping) error { return r.creator.Create(ctx, r.conv.ToEntity(&item)) } -// Get missing godoc +// Get retrieves the active tenant with matching internal ID from the Compass storage. func (r *pgRepository) Get(ctx context.Context, id string) (*model.BusinessTenantMapping, error) { var entity tenant.Entity conditions := repo.Conditions{ @@ -85,7 +85,7 @@ func (r *pgRepository) Get(ctx context.Context, id string) (*model.BusinessTenan return r.conv.FromEntity(&entity), nil } -// GetByExternalTenant missing godoc +// GetByExternalTenant retrieves the active tenant with matching external ID from the Compass storage. func (r *pgRepository) GetByExternalTenant(ctx context.Context, externalTenant string) (*model.BusinessTenantMapping, error) { var entity tenant.Entity conditions := repo.Conditions{ @@ -97,17 +97,17 @@ func (r *pgRepository) GetByExternalTenant(ctx context.Context, externalTenant s return r.conv.FromEntity(&entity), nil } -// Exists missing godoc +// Exists checks if tenant with the provided internal ID exists in the Compass storage. func (r *pgRepository) Exists(ctx context.Context, id string) (bool, error) { return r.existQuerierGlobal.ExistsGlobal(ctx, repo.Conditions{repo.NewEqualCondition(idColumn, id)}) } -// ExistsByExternalTenant missing godoc +// ExistsByExternalTenant checks if tenant with the provided external ID exists in the Compass storage. func (r *pgRepository) ExistsByExternalTenant(ctx context.Context, externalTenant string) (bool, error) { return r.existQuerierGlobal.ExistsGlobal(ctx, repo.Conditions{repo.NewEqualCondition(externalTenantColumn, externalTenant)}) } -// List missing godoc +// List retrieves all tenants from the Compass storage. func (r *pgRepository) List(ctx context.Context) ([]*model.BusinessTenantMapping, error) { var entityCollection tenant.EntityCollection @@ -136,7 +136,7 @@ func (r *pgRepository) List(ctx context.Context) ([]*model.BusinessTenantMapping return items, nil } -// Update missing godoc +// Update updates the values of tenant with matching internal, and external IDs. func (r *pgRepository) Update(ctx context.Context, model *model.BusinessTenantMapping) error { if model == nil { return apperrors.NewInternalError("model can not be empty") @@ -147,7 +147,7 @@ func (r *pgRepository) Update(ctx context.Context, model *model.BusinessTenantMa return r.updaterGlobal.UpdateSingleGlobal(ctx, entity) } -// DeleteByExternalTenant missing godoc +// DeleteByExternalTenant removes a tenant with matching external ID from the Compass storage. func (r *pgRepository) DeleteByExternalTenant(ctx context.Context, externalTenant string) error { conditions := repo.Conditions{ repo.NewEqualCondition(externalTenantColumn, externalTenant), diff --git a/components/director/internal/domain/tenant/resolver.go b/components/director/internal/domain/tenant/resolver.go index 197aaced00..e24432185d 100644 --- a/components/director/internal/domain/tenant/resolver.go +++ b/components/director/internal/domain/tenant/resolver.go @@ -11,7 +11,7 @@ import ( "github.com/kyma-incubator/compass/components/director/pkg/graphql" ) -// BusinessTenantMappingService missing godoc +// BusinessTenantMappingService is responsible for the service-layer tenant operations. //go:generate mockery --name=BusinessTenantMappingService --output=automock --outpkg=automock --case=underscore type BusinessTenantMappingService interface { List(ctx context.Context) ([]*model.BusinessTenantMapping, error) @@ -19,13 +19,14 @@ type BusinessTenantMappingService interface { GetTenantByExternalID(ctx context.Context, externalID string) (*model.BusinessTenantMapping, error) } -// BusinessTenantMappingConverter missing godoc +// BusinessTenantMappingConverter is used to convert the internally used tenant representation model.BusinessTenantMapping +// into the external GraphQL representation graphql.Tenant. //go:generate mockery --name=BusinessTenantMappingConverter --output=automock --outpkg=automock --case=underscore type BusinessTenantMappingConverter interface { MultipleToGraphQL(in []*model.BusinessTenantMapping) []*graphql.Tenant } -// Resolver missing godoc +// Resolver is the resolver responsible for tenant-related GraphQL requests. type Resolver struct { transact persistence.Transactioner @@ -33,7 +34,7 @@ type Resolver struct { conv BusinessTenantMappingConverter } -// Tenants missing godoc +// Tenants transactionally retrieves all tenants present in the Compass storage. func (r *Resolver) Tenants(ctx context.Context) ([]*graphql.Tenant, error) { tx, err := r.transact.Begin() if err != nil { @@ -56,7 +57,7 @@ func (r *Resolver) Tenants(ctx context.Context) ([]*graphql.Tenant, error) { return gqlTenants, nil } -// Tenant missing godoc +// Tenant retrieves a tenant with the provided external ID from the Compass storage. func (r *Resolver) Tenant(ctx context.Context, externalID string) (*graphql.Tenant, error) { tx, err := r.transact.Begin() if err != nil { @@ -78,7 +79,7 @@ func (r *Resolver) Tenant(ctx context.Context, externalID string) (*graphql.Tena return gqlTenant[0], nil } -// Labels missing godoc +// Labels transactionally retrieves all existing labels of the given tenant if it exists. func (r *Resolver) Labels(ctx context.Context, obj *graphql.Tenant, key *string) (graphql.Labels, error) { if obj == nil { return nil, apperrors.NewInternalError("Tenant cannot be empty") @@ -115,7 +116,7 @@ func (r *Resolver) Labels(ctx context.Context, obj *graphql.Tenant, key *string) return resultLabels, nil } -// NewResolver missing godoc +// NewResolver returns the GraphQL resolver for tenants. func NewResolver(transact persistence.Transactioner, srv BusinessTenantMappingService, conv BusinessTenantMappingConverter) *Resolver { return &Resolver{ transact: transact, diff --git a/components/director/internal/domain/tenant/service.go b/components/director/internal/domain/tenant/service.go index f6152894ca..237911768a 100644 --- a/components/director/internal/domain/tenant/service.go +++ b/components/director/internal/domain/tenant/service.go @@ -10,9 +10,14 @@ import ( "github.com/pkg/errors" ) -const subdomainLabelKey = "subdomain" +const ( + // SubdomainLabelKey is the key of the tenant label for subdomain. + SubdomainLabelKey = "subdomain" + // RegionLabelKey is the key of the tenant label for region. + RegionLabelKey = "region" +) -// TenantMappingRepository missing godoc +// TenantMappingRepository is responsible for the repo-layer tenant operations. //go:generate mockery --name=TenantMappingRepository --output=automock --outpkg=automock --case=underscore type TenantMappingRepository interface { Create(ctx context.Context, item model.BusinessTenantMapping) error @@ -21,23 +26,22 @@ type TenantMappingRepository interface { Exists(ctx context.Context, id string) (bool, error) List(ctx context.Context) ([]*model.BusinessTenantMapping, error) ExistsByExternalTenant(ctx context.Context, externalTenant string) (bool, error) - Update(ctx context.Context, model *model.BusinessTenantMapping) error DeleteByExternalTenant(ctx context.Context, externalTenant string) error } -// LabelUpsertService missing godoc +// LabelUpsertService is responsible for creating, or updating already existing labels, and their label definitions. //go:generate mockery --name=LabelUpsertService --output=automock --outpkg=automock --case=underscore type LabelUpsertService interface { UpsertLabel(ctx context.Context, tenant string, labelInput *model.LabelInput) error } -// LabelRepository missing godoc +// LabelRepository is responsible for the repo-layer label operations. //go:generate mockery --name=LabelRepository --output=automock --outpkg=automock --case=underscore type LabelRepository interface { ListForObject(ctx context.Context, tenant string, objectType model.LabelableObject, objectID string) (map[string]*model.Label, error) } -// UIDService missing godoc +// UIDService is responsible for generating GUIDs, which will be used as internal tenant IDs when tenants are created. //go:generate mockery --name=UIDService --output=automock --outpkg=automock --case=underscore type UIDService interface { Generate() string @@ -54,7 +58,7 @@ type service struct { tenantMappingRepo TenantMappingRepository } -// NewService missing godoc +// NewService returns a new object responsible for service-layer tenant operations. func NewService(tenantMapping TenantMappingRepository, uidService UIDService) *service { return &service{ uidService: uidService, @@ -62,7 +66,7 @@ func NewService(tenantMapping TenantMappingRepository, uidService UIDService) *s } } -// NewServiceWithLabels missing godoc +// NewServiceWithLabels returns a new entity responsible for service-layer tenant operations, including operations with labels like listing all labels related to the given tenant. func NewServiceWithLabels(tenantMapping TenantMappingRepository, uidService UIDService, labelRepo LabelRepository, labelUpsertSvc LabelUpsertService) *labeledService { return &labeledService{ service: service{ @@ -74,7 +78,7 @@ func NewServiceWithLabels(tenantMapping TenantMappingRepository, uidService UIDS } } -// GetExternalTenant missing godoc +// GetExternalTenant returns the external tenant ID of the tenant with the corresponding internal tenant ID. func (s *service) GetExternalTenant(ctx context.Context, id string) (string, error) { mapping, err := s.tenantMappingRepo.Get(ctx, id) if err != nil { @@ -84,7 +88,7 @@ func (s *service) GetExternalTenant(ctx context.Context, id string) (string, err return mapping.ExternalTenant, nil } -// GetInternalTenant missing godoc +// GetInternalTenant returns the internal tenant ID of the tenant with the corresponding external tenant ID. func (s *service) GetInternalTenant(ctx context.Context, externalTenant string) (string, error) { mapping, err := s.tenantMappingRepo.GetByExternalTenant(ctx, externalTenant) if err != nil { @@ -94,17 +98,17 @@ func (s *service) GetInternalTenant(ctx context.Context, externalTenant string) return mapping.ID, nil } -// List missing godoc +// List returns all tenants present in the Compass storage. func (s *service) List(ctx context.Context) ([]*model.BusinessTenantMapping, error) { return s.tenantMappingRepo.List(ctx) } -// GetTenantByExternalID missing godoc +// GetTenantByExternalID returns the tenant with the provided external ID. func (s *service) GetTenantByExternalID(ctx context.Context, id string) (*model.BusinessTenantMapping, error) { return s.tenantMappingRepo.GetByExternalTenant(ctx, id) } -// MultipleToTenantMapping missing godoc +// MultipleToTenantMapping assigns a new internal ID to all the provided tenants, and returns the BusinessTenantMappingInputs as BusinessTenantMappings. func (s *service) MultipleToTenantMapping(tenantInputs []model.BusinessTenantMappingInput) []model.BusinessTenantMapping { tenants := make([]model.BusinessTenantMapping, 0, len(tenantInputs)) tenantIDs := make(map[string]string, len(tenantInputs)) @@ -129,57 +133,83 @@ func (s *service) MultipleToTenantMapping(tenantInputs []model.BusinessTenantMap return tenants } -// CreateManyIfNotExists missing godoc +// CreateManyIfNotExists creates all provided tenants if they do not exist. +// It creates or updates the subdomain and region labels of the provided tenants, no matter if they are pre-existing or not. func (s *labeledService) CreateManyIfNotExists(ctx context.Context, tenantInputs ...model.BusinessTenantMappingInput) error { tenants := s.MultipleToTenantMapping(tenantInputs) - subdomains := tenantSubdomains(tenantInputs) - for _, tenant := range tenants { + subdomains, regions := tenantLocality(tenantInputs) + for tenantIdx, tenant := range tenants { subdomain := "" + region := "" if s, ok := subdomains[tenant.ExternalTenant]; ok { subdomain = s } - if err := s.createIfNotExists(ctx, tenant, subdomain); err != nil { + if r, ok := regions[tenant.ExternalTenant]; ok { + region = r + } + tenantID, err := s.createIfNotExists(ctx, tenant, subdomain, region) + if err != nil { return errors.Wrapf(err, "while creating tenant with external ID %s", tenant.ExternalTenant) } + // the tenant already exists in our DB with a different ID, and we should update all child resources to use the correct internal ID + if tenantID != tenant.ID { + for i := tenantIdx; i < len(tenants); i++ { + if tenants[i].Parent == tenant.ID { + tenants[i].Parent = tenantID + } + } + } } return nil } -func (s *labeledService) createIfNotExists(ctx context.Context, tenant model.BusinessTenantMapping, subdomain string) error { - exists, err := s.tenantMappingRepo.ExistsByExternalTenant(ctx, tenant.ExternalTenant) - if err != nil { - return errors.Wrapf(err, "while checking the existence of tenant with external ID %s", tenant.ExternalTenant) +func (s *labeledService) createIfNotExists(ctx context.Context, tenant model.BusinessTenantMapping, subdomain, region string) (string, error) { + tenantFromDB, err := s.tenantMappingRepo.GetByExternalTenant(ctx, tenant.ExternalTenant) + if err != nil && !apperrors.IsNotFoundError(err) { + return "", errors.Wrapf(err, "while checking the existence of tenant with external ID %s", tenant.ExternalTenant) } - if exists { - return nil + if tenantFromDB != nil { + return tenantFromDB.ID, s.upsertLabels(ctx, tenantFromDB.ID, subdomain, region) } if err = s.tenantMappingRepo.Create(ctx, tenant); err != nil { - return errors.Wrapf(err, "while creating tenant with ID %s and external ID %s", tenant.ID, tenant.ExternalTenant) + return "", errors.Wrapf(err, "while creating tenant with ID %s and external ID %s", tenant.ID, tenant.ExternalTenant) } + return tenant.ID, s.upsertLabels(ctx, tenant.ID, subdomain, region) +} + +func (s *labeledService) upsertLabels(ctx context.Context, tenantID, subdomain, region string) error { if len(subdomain) > 0 { - if err := s.addSubdomainLabel(ctx, tenant.ID, subdomain); err != nil { - return errors.Wrapf(err, "while setting subdomain label for tenant with external ID %s", tenant.ExternalTenant) + if err := s.upsertSubdomainLabel(ctx, tenantID, subdomain); err != nil { + return errors.Wrapf(err, "while setting subdomain label for tenant with ID %s", tenantID) + } + } + if len(region) > 0 { + if err := s.upsertRegionLabel(ctx, tenantID, region); err != nil { + return errors.Wrapf(err, "while setting subdomain label for tenant with ID %s", tenantID) } } - return nil } -func tenantSubdomains(tenants []model.BusinessTenantMappingInput) map[string]string { +func tenantLocality(tenants []model.BusinessTenantMappingInput) (map[string]string, map[string]string) { subdomains := make(map[string]string) + regions := make(map[string]string) for _, t := range tenants { if len(t.Subdomain) > 0 { subdomains[t.ExternalTenant] = t.Subdomain } + if len(t.Region) > 0 { + regions[t.ExternalTenant] = t.Region + } } - return subdomains + return subdomains, regions } -// DeleteMany missing godoc +// DeleteMany removes all provided tenants from the Compass storage. func (s *service) DeleteMany(ctx context.Context, tenantInputs []model.BusinessTenantMappingInput) error { for _, tenantInput := range tenantInputs { err := s.tenantMappingRepo.DeleteByExternalTenant(ctx, tenantInput.ExternalTenant) @@ -191,7 +221,8 @@ func (s *service) DeleteMany(ctx context.Context, tenantInputs []model.BusinessT return nil } -// ListLabels missing godoc +// ListLabels returns all labels directly linked to the given tenant, like subdomain or region. +// 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. func (s *labeledService) ListLabels(ctx context.Context, tenantID string) (map[string]*model.Label, error) { log.C(ctx).Infof("getting labels for tenant with ID %s", tenantID) if err := s.ensureTenantExists(ctx, tenantID); err != nil { @@ -206,9 +237,9 @@ func (s *labeledService) ListLabels(ctx context.Context, tenantID string) (map[s return labels, nil } -func (s *labeledService) addSubdomainLabel(ctx context.Context, tenantID, subdomain string) error { +func (s *labeledService) upsertSubdomainLabel(ctx context.Context, tenantID, subdomain string) error { label := &model.LabelInput{ - Key: subdomainLabelKey, + Key: SubdomainLabelKey, Value: subdomain, ObjectID: tenantID, ObjectType: model.TenantLabelableObject, @@ -216,6 +247,16 @@ func (s *labeledService) addSubdomainLabel(ctx context.Context, tenantID, subdom return s.labelUpsertSvc.UpsertLabel(ctx, tenantID, label) } +func (s *labeledService) upsertRegionLabel(ctx context.Context, tenantID, region string) error { + label := &model.LabelInput{ + Key: RegionLabelKey, + Value: region, + ObjectID: tenantID, + ObjectType: model.TenantLabelableObject, + } + return s.labelUpsertSvc.UpsertLabel(ctx, tenantID, label) +} + func (s *service) ensureTenantExists(ctx context.Context, id string) error { exists, err := s.tenantMappingRepo.Exists(ctx, id) if err != nil { diff --git a/components/director/internal/domain/tenant/service_test.go b/components/director/internal/domain/tenant/service_test.go index 2a3294d877..d11a9c564a 100644 --- a/components/director/internal/domain/tenant/service_test.go +++ b/components/director/internal/domain/tenant/service_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/kyma-incubator/compass/components/director/pkg/apperrors" - tenant2 "github.com/kyma-incubator/compass/components/director/pkg/tenant" + tenantEntity "github.com/kyma-incubator/compass/components/director/pkg/tenant" "github.com/stretchr/testify/mock" "github.com/kyma-incubator/compass/components/director/internal/domain/tenant" @@ -186,7 +186,7 @@ func TestService_List(t *testing.T) { func TestService_DeleteMany(t *testing.T) { //GIVEN ctx := tenant.SaveToContext(context.TODO(), "test", "external-test") - tenantInput := newModelBusinessTenantMappingInput(testName, "") + tenantInput := newModelBusinessTenantMappingInput(testName, "", "") testErr := errors.New("test") testCases := []struct { Name string @@ -238,10 +238,16 @@ func TestService_CreateManyIfNotExists(t *testing.T) { //GIVEN ctx := tenant.SaveToContext(context.TODO(), "test", "external-test") - tenantInputs := []model.BusinessTenantMappingInput{newModelBusinessTenantMappingInput("test1", ""), - newModelBusinessTenantMappingInput("test2", "").WithExternalTenant("external2")} - tenantInputsWithSubdomains := []model.BusinessTenantMappingInput{newModelBusinessTenantMappingInput("test1", testSubdomain), - newModelBusinessTenantMappingInput("test2", "").WithExternalTenant("external2")} + tenantInputs := []model.BusinessTenantMappingInput{newModelBusinessTenantMappingInput("test1", "", ""), + newModelBusinessTenantMappingInput("test2", "", "").WithExternalTenant("external2")} + tenantInputsWithSubdomains := []model.BusinessTenantMappingInput{newModelBusinessTenantMappingInput("test1", testSubdomain, ""), + newModelBusinessTenantMappingInput("test2", "", "").WithExternalTenant("external2")} + tenantInputsWithRegions := []model.BusinessTenantMappingInput{newModelBusinessTenantMappingInput("test1", "", testRegion), + newModelBusinessTenantMappingInput("test2", "", testRegion).WithExternalTenant("external2")} + tenantModelInputsWithParent := []model.BusinessTenantMappingInput{newModelBusinessTenantMappingInputWithType(testID, "test1", testParentID, "", "", tenantEntity.Account), + newModelBusinessTenantMappingInputWithType(testParentID, "test2", "", "", "", tenantEntity.Customer)} + tenantWithSubdomainAndRegion := newModelBusinessTenantMappingInput("test1", testSubdomain, testRegion) + tenantModels := []model.BusinessTenantMapping{*newModelBusinessTenantMapping(testID, "test1"), newModelBusinessTenantMapping(testID, "test2").WithExternalTenant("external2")} @@ -263,6 +269,7 @@ func TestService_CreateManyIfNotExists(t *testing.T) { TenantMappingRepoFn func() *automock.TenantMappingRepository LabelRepoFn func() *automock.LabelRepository LabelUpsertSvcFn func() *automock.LabelUpsertService + UIDSvcFn func() *automock.UIDService ExpectedOutput error }{ { @@ -270,11 +277,66 @@ func TestService_CreateManyIfNotExists(t *testing.T) { tenantInputs: tenantInputs, TenantMappingRepoFn: func() *automock.TenantMappingRepository { tenantMappingRepo := &automock.TenantMappingRepository{} - tenantMappingRepo.On("ExistsByExternalTenant", ctx, tenantModels[0].ExternalTenant).Return(false, nil) - tenantMappingRepo.On("ExistsByExternalTenant", ctx, tenantModels[1].ExternalTenant).Return(true, nil) + tenantMappingRepo.On("GetByExternalTenant", ctx, tenantModels[1].ExternalTenant).Return(&tenantModels[1], nil).Once() + tenantMappingRepo.On("GetByExternalTenant", ctx, tenantModels[0].ExternalTenant).Return(nil, nil).Once() tenantMappingRepo.On("Create", ctx, tenantModels[0]).Return(nil).Once() return tenantMappingRepo }, + UIDSvcFn: uidSvcFn, + LabelRepoFn: noopLabelRepo, + LabelUpsertSvcFn: noopLabelUpsertSvc, + ExpectedOutput: nil, + }, + { + Name: "Success when tenant already exists and only labels should be updated", + tenantInputs: []model.BusinessTenantMappingInput{tenantWithSubdomainAndRegion}, + TenantMappingRepoFn: func() *automock.TenantMappingRepository { + tenantMappingRepo := &automock.TenantMappingRepository{} + tenantMappingRepo.On("GetByExternalTenant", ctx, tenantWithSubdomainAndRegion.ExternalTenant).Return(tenantWithSubdomainAndRegion.ToBusinessTenantMapping(testParentID), nil) + return tenantMappingRepo + }, + UIDSvcFn: uidSvcFn, + LabelRepoFn: noopLabelRepo, + LabelUpsertSvcFn: func() *automock.LabelUpsertService { + svc := &automock.LabelUpsertService{} + regionLabel := &model.LabelInput{ + Key: "region", + Value: testRegion, + ObjectID: testParentID, + ObjectType: model.TenantLabelableObject, + } + subdomainLabel := &model.LabelInput{ + Key: "subdomain", + Value: testSubdomain, + ObjectID: testParentID, + ObjectType: model.TenantLabelableObject, + } + svc.On("UpsertLabel", ctx, testParentID, subdomainLabel).Return(nil).Once() + svc.On("UpsertLabel", ctx, testParentID, regionLabel).Return(nil).Once() + return svc + }, + ExpectedOutput: nil, + }, + { + Name: "Success when parent tenant exists with another ID", + tenantInputs: tenantModelInputsWithParent, + TenantMappingRepoFn: func() *automock.TenantMappingRepository { + parent := tenantModelInputsWithParent[1] + modifiedTenant := tenantModelInputsWithParent[0] + modifiedTenant.Parent = testInternalParentID + + tenantMappingRepo := &automock.TenantMappingRepository{} + tenantMappingRepo.On("GetByExternalTenant", ctx, parent.ExternalTenant).Return(parent.ToBusinessTenantMapping(testInternalParentID), nil).Once() + tenantMappingRepo.On("GetByExternalTenant", ctx, modifiedTenant.ExternalTenant).Return(nil, nil).Once() + tenantMappingRepo.On("Create", ctx, *modifiedTenant.ToBusinessTenantMapping(testID)).Return(nil).Once() + return tenantMappingRepo + }, + UIDSvcFn: func() *automock.UIDService { + uidSvc := &automock.UIDService{} + uidSvc.On("Generate").Return(testID).Once() + uidSvc.On("Generate").Return(testTemporaryInternalParentID).Once() + return uidSvc + }, LabelRepoFn: noopLabelRepo, LabelUpsertSvcFn: noopLabelUpsertSvc, ExpectedOutput: nil, @@ -284,11 +346,12 @@ func TestService_CreateManyIfNotExists(t *testing.T) { tenantInputs: tenantInputsWithSubdomains, TenantMappingRepoFn: func() *automock.TenantMappingRepository { tenantMappingRepo := &automock.TenantMappingRepository{} - tenantMappingRepo.On("ExistsByExternalTenant", ctx, tenantInputsWithSubdomains[0].ExternalTenant).Return(false, nil) - tenantMappingRepo.On("ExistsByExternalTenant", ctx, tenantInputsWithSubdomains[1].ExternalTenant).Return(true, nil) + tenantMappingRepo.On("GetByExternalTenant", ctx, tenantInputsWithSubdomains[0].ExternalTenant).Return(nil, nil) + tenantMappingRepo.On("GetByExternalTenant", ctx, tenantInputsWithSubdomains[1].ExternalTenant).Return(&tenantModels[1], nil) tenantMappingRepo.On("Create", ctx, tenantModels[0]).Return(nil).Once() return tenantMappingRepo }, + UIDSvcFn: uidSvcFn, LabelRepoFn: noopLabelRepo, LabelUpsertSvcFn: func() *automock.LabelUpsertService { svc := &automock.LabelUpsertService{} @@ -303,27 +366,54 @@ func TestService_CreateManyIfNotExists(t *testing.T) { }, ExpectedOutput: nil, }, + { + Name: "Success when region should be added", + tenantInputs: tenantInputsWithRegions, + TenantMappingRepoFn: func() *automock.TenantMappingRepository { + tenantMappingRepo := &automock.TenantMappingRepository{} + tenantMappingRepo.On("GetByExternalTenant", ctx, tenantInputsWithRegions[0].ExternalTenant).Return(nil, nil) + tenantMappingRepo.On("GetByExternalTenant", ctx, tenantInputsWithRegions[1].ExternalTenant).Return(&tenantModels[1], nil) + tenantMappingRepo.On("Create", ctx, tenantModels[0]).Return(nil).Once() + return tenantMappingRepo + }, + UIDSvcFn: uidSvcFn, + LabelRepoFn: noopLabelRepo, + LabelUpsertSvcFn: func() *automock.LabelUpsertService { + svc := &automock.LabelUpsertService{} + regionLabel := &model.LabelInput{ + Key: "region", + Value: testRegion, + ObjectID: tenantModels[1].ID, + ObjectType: model.TenantLabelableObject, + } + svc.On("UpsertLabel", ctx, testID, regionLabel).Return(nil).Twice() + return svc + }, + ExpectedOutput: nil, + }, { Name: "Error when checking the existence of tenant", - tenantInputs: tenantInputs, + tenantInputs: []model.BusinessTenantMappingInput{tenantWithSubdomainAndRegion}, TenantMappingRepoFn: func() *automock.TenantMappingRepository { tenantMappingRepo := &automock.TenantMappingRepository{} - tenantMappingRepo.On("ExistsByExternalTenant", ctx, tenantModels[0].ExternalTenant).Return(false, testErr) + tenantMappingRepo.On("GetByExternalTenant", ctx, tenantWithSubdomainAndRegion.ExternalTenant).Return(nil, testErr) return tenantMappingRepo }, + UIDSvcFn: uidSvcFn, LabelRepoFn: noopLabelRepo, LabelUpsertSvcFn: noopLabelUpsertSvc, ExpectedOutput: testErr, }, { - Name: "Error when subdomain creation fails", + Name: "Error when subdomain label setting fails", tenantInputs: tenantInputsWithSubdomains, TenantMappingRepoFn: func() *automock.TenantMappingRepository { tenantMappingRepo := &automock.TenantMappingRepository{} - tenantMappingRepo.On("ExistsByExternalTenant", ctx, tenantInputsWithSubdomains[0].ExternalTenant).Return(false, nil) + tenantMappingRepo.On("GetByExternalTenant", ctx, tenantInputsWithSubdomains[0].ExternalTenant).Return(nil, nil) tenantMappingRepo.On("Create", ctx, tenantModels[0]).Return(nil).Once() return tenantMappingRepo }, + UIDSvcFn: uidSvcFn, LabelRepoFn: noopLabelRepo, LabelUpsertSvcFn: func() *automock.LabelUpsertService { svc := &automock.LabelUpsertService{} @@ -338,15 +428,40 @@ func TestService_CreateManyIfNotExists(t *testing.T) { }, ExpectedOutput: testErr, }, + { + Name: "Error when region label setting fails", + tenantInputs: tenantInputsWithRegions, + TenantMappingRepoFn: func() *automock.TenantMappingRepository { + tenantMappingRepo := &automock.TenantMappingRepository{} + tenantMappingRepo.On("GetByExternalTenant", ctx, tenantInputsWithRegions[0].ExternalTenant).Return(nil, nil) + tenantMappingRepo.On("Create", ctx, tenantModels[0]).Return(nil).Once() + return tenantMappingRepo + }, + UIDSvcFn: uidSvcFn, + LabelRepoFn: noopLabelRepo, + LabelUpsertSvcFn: func() *automock.LabelUpsertService { + svc := &automock.LabelUpsertService{} + label := &model.LabelInput{ + Key: "region", + Value: testRegion, + ObjectID: testID, + ObjectType: model.TenantLabelableObject, + } + svc.On("UpsertLabel", ctx, testID, label).Return(testErr).Once() + return svc + }, + ExpectedOutput: testErr, + }, { Name: "Error when creating the tenant", tenantInputs: tenantInputs, TenantMappingRepoFn: func() *automock.TenantMappingRepository { tenantMappingRepo := &automock.TenantMappingRepository{} - tenantMappingRepo.On("ExistsByExternalTenant", ctx, tenantModels[0].ExternalTenant).Return(false, nil) + tenantMappingRepo.On("GetByExternalTenant", ctx, tenantModels[0].ExternalTenant).Return(nil, nil) tenantMappingRepo.On("Create", ctx, tenantModels[0]).Return(testErr).Once() return tenantMappingRepo }, + UIDSvcFn: uidSvcFn, LabelRepoFn: noopLabelRepo, LabelUpsertSvcFn: noopLabelUpsertSvc, ExpectedOutput: testErr, @@ -355,10 +470,11 @@ func TestService_CreateManyIfNotExists(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.Name, func(t *testing.T) { - uidSvc := uidSvcFn() + uidSvc := testCase.UIDSvcFn() tenantMappingRepo := testCase.TenantMappingRepoFn() labelRepo := testCase.LabelRepoFn() labelUpsertSvc := testCase.LabelUpsertSvcFn() + defer mock.AssertExpectationsForObjects(t, tenantMappingRepo, uidSvc, labelRepo, labelUpsertSvc) svc := tenant.NewServiceWithLabels(tenantMappingRepo, uidSvc, labelRepo, labelUpsertSvc) @@ -372,8 +488,6 @@ func TestService_CreateManyIfNotExists(t *testing.T) { } else { assert.NoError(t, err) } - - mock.AssertExpectationsForObjects(t, tenantMappingRepo, uidSvc, labelRepo, labelUpsertSvc) }) } } @@ -415,38 +529,38 @@ func Test_MultipleToTenantMapping(t *testing.T) { ID: "0", Name: "acc1", ExternalTenant: "0", - Status: tenant2.Active, - Type: tenant2.Unknown, + Status: tenantEntity.Active, + Type: tenantEntity.Unknown, }, { ID: "4", Name: "x1", ExternalTenant: "4", - Status: tenant2.Active, - Type: tenant2.Unknown, + Status: tenantEntity.Active, + Type: tenantEntity.Unknown, }, { ID: "2", Name: "customer1", ExternalTenant: "2", Parent: "4", - Status: tenant2.Active, - Type: tenant2.Unknown, + Status: tenantEntity.Active, + Type: tenantEntity.Unknown, }, { ID: "1", Name: "acc2", ExternalTenant: "1", Parent: "2", - Status: tenant2.Active, - Type: tenant2.Unknown, + Status: tenantEntity.Active, + Type: tenantEntity.Unknown, }, { ID: "3", Name: "acc3", ExternalTenant: "3", - Status: tenant2.Active, - Type: tenant2.Unknown, + Status: tenantEntity.Active, + Type: tenantEntity.Unknown, }, }, }, @@ -605,6 +719,61 @@ func Test_ListLabels(t *testing.T) { }) } +func Test_GetTenantByExternalID(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // GIVEN + ctx := context.TODO() + expected := &model.BusinessTenantMapping{ + ID: testID, + Name: testName, + ExternalTenant: testExternal, + Status: tenantEntity.Active, + Type: tenantEntity.Account, + } + + uidSvc := &automock.UIDService{} + labelUpsertSvc := &automock.LabelUpsertService{} + labelRepo := &automock.LabelRepository{} + + tenantRepo := &automock.TenantMappingRepository{} + tenantRepo.On("GetByExternalTenant", ctx, testID).Return(expected, nil) + + defer mock.AssertExpectationsForObjects(t, tenantRepo, uidSvc, labelRepo, labelUpsertSvc) + + svc := tenant.NewServiceWithLabels(tenantRepo, uidSvc, labelRepo, labelUpsertSvc) + + // WHEN + actual, err := svc.GetTenantByExternalID(ctx, testID) + + // THEN + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) + t.Run("Returns error when retrieval from DB fails", func(t *testing.T) { + // GIVEN + ctx := context.TODO() + + uidSvc := &automock.UIDService{} + labelUpsertSvc := &automock.LabelUpsertService{} + labelRepo := &automock.LabelRepository{} + + tenantRepo := &automock.TenantMappingRepository{} + tenantRepo.On("GetByExternalTenant", ctx, testID).Return(nil, testError) + + defer mock.AssertExpectationsForObjects(t, tenantRepo, uidSvc, labelRepo, labelUpsertSvc) + + svc := tenant.NewServiceWithLabels(tenantRepo, uidSvc, labelRepo, labelUpsertSvc) + + // WHEN + actual, err := svc.GetTenantByExternalID(ctx, testID) + + // THEN + assert.Error(t, err) + assert.Nil(t, actual) + assert.Equal(t, testError, err) + }) +} + type serialUUIDService struct { i int } diff --git a/components/director/internal/domain/tenant/tenant.go b/components/director/internal/domain/tenant/tenant.go index e4071836e8..0b2874a397 100644 --- a/components/director/internal/domain/tenant/tenant.go +++ b/components/director/internal/domain/tenant/tenant.go @@ -8,16 +8,16 @@ import ( type key int -// TenantContextKey missing godoc +// TenantContextKey is the key under which the TenantCtx is saved in a given context.Context. const TenantContextKey key = iota -// TenantCtx missing godoc +// TenantCtx is the structure can be saved in a request context. It is used to determine the tenant context in which the request is being executed. type TenantCtx struct { InternalID string ExternalID string } -// LoadFromContext missing godoc +// LoadFromContext retrieves the internal tenant ID from the provided context. It returns error if such ID cannot be found. func LoadFromContext(ctx context.Context) (string, error) { tenant, ok := ctx.Value(TenantContextKey).(TenantCtx) @@ -32,7 +32,8 @@ func LoadFromContext(ctx context.Context) (string, error) { return tenant.InternalID, nil } -// SaveToContext missing godoc +// SaveToContext returns a child context of the provided context, including the provided tenant information. +// The internal tenant ID can be later retrieved from the context by calling LoadFromContext. func SaveToContext(ctx context.Context, internalID, externalID string) context.Context { tenantCtx := TenantCtx{InternalID: internalID, ExternalID: externalID} return context.WithValue(ctx, TenantContextKey, tenantCtx) diff --git a/components/director/internal/model/businesstenantmapping.go b/components/director/internal/model/businesstenantmapping.go index c1fb738fd6..e0f4831109 100644 --- a/components/director/internal/model/businesstenantmapping.go +++ b/components/director/internal/model/businesstenantmapping.go @@ -32,6 +32,7 @@ type BusinessTenantMappingInput struct { ExternalTenant string `json:"id"` Parent string `json:"parent"` Subdomain string `json:"subdomain"` + Region string `json:"region"` Type string `json:"type"` Provider string } diff --git a/components/director/internal/tenantfetcher/fixtures_test.go b/components/director/internal/tenantfetcher/fixtures_test.go index 7916cb4cb8..fb5e648e64 100644 --- a/components/director/internal/tenantfetcher/fixtures_test.go +++ b/components/director/internal/tenantfetcher/fixtures_test.go @@ -54,12 +54,13 @@ func wrapIntoEventPageJSON(eventData string) []byte { }`, fixID(), eventData)) } -func fixBusinessTenantMappingInput(name, externalTenant, provider, subdomain, parent string, tenantType tenant.Type) model.BusinessTenantMappingInput { +func fixBusinessTenantMappingInput(name, externalTenant, provider, subdomain, region, parent string, tenantType tenant.Type) model.BusinessTenantMappingInput { return model.BusinessTenantMappingInput{ Name: name, ExternalTenant: externalTenant, Provider: provider, Subdomain: subdomain, + Region: region, Parent: parent, Type: tenant.TypeToStr(tenantType), } diff --git a/components/director/internal/tenantfetcher/service.go b/components/director/internal/tenantfetcher/service.go index 8a68fc2c90..ba428e5a95 100644 --- a/components/director/internal/tenantfetcher/service.go +++ b/components/director/internal/tenantfetcher/service.go @@ -95,6 +95,7 @@ type Service struct { tenantStorageService TenantService runtimeStorageService RuntimeService providerName string + tenantsRegion string fieldMapping TenantFieldMapping movedRuntimeByLabelFieldMapping MovedRuntimeByLabelFieldMapping labelDefService LabelDefinitionService @@ -109,7 +110,7 @@ func NewService(queryConfig QueryConfig, kubeClient KubeClient, fieldMapping TenantFieldMapping, movRuntime MovedRuntimeByLabelFieldMapping, - providerName string, client EventAPIClient, + providerName string, regionName string, client EventAPIClient, tenantStorageService TenantService, runtimeStorageService RuntimeService, labelDefService LabelDefinitionService, @@ -120,6 +121,7 @@ func NewService(queryConfig QueryConfig, kubeClient: kubeClient, fieldMapping: fieldMapping, providerName: providerName, + tenantsRegion: regionName, eventAPIClient: client, tenantStorageService: tenantStorageService, runtimeStorageService: runtimeStorageService, @@ -229,7 +231,6 @@ func (s Service) SyncTenants() error { func (s Service) createTenants(ctx context.Context, currTenants map[string]string, eventsTenants []model.BusinessTenantMappingInput) error { tenantsToCreate := make([]model.BusinessTenantMappingInput, 0) - subdomainMapping := make(map[string]string) for _, eventTenant := range eventsTenants { if _, ok := currTenants[eventTenant.ExternalTenant]; ok { continue @@ -237,8 +238,8 @@ func (s Service) createTenants(ctx context.Context, currTenants map[string]strin if len(eventTenant.Parent) > 0 { eventTenant.Parent = currTenants[eventTenant.Parent] } + eventTenant.Region = s.tenantsRegion tenantsToCreate = append(tenantsToCreate, eventTenant) - subdomainMapping[eventTenant.ExternalTenant] = eventTenant.Subdomain } if len(tenantsToCreate) > 0 { if err := s.tenantStorageService.CreateManyIfNotExists(ctx, tenantsToCreate...); err != nil { diff --git a/components/director/internal/tenantfetcher/service_test.go b/components/director/internal/tenantfetcher/service_test.go index 3a378c3f09..af4cd9482c 100644 --- a/components/director/internal/tenantfetcher/service_test.go +++ b/components/director/internal/tenantfetcher/service_test.go @@ -28,6 +28,7 @@ import ( func TestService_SyncTenants(t *testing.T) { // GIVEN provider := "default" + region := "eu-1" tenantFieldMapping := tenantfetcher.TenantFieldMapping{ NameField: "name", @@ -50,9 +51,9 @@ func TestService_SyncTenants(t *testing.T) { parent2GUID := fixID() parent3GUID := fixID() - parentTenant1 := fixBusinessTenantMappingInput(parent1, parent1, provider, "", "", tenant.Customer) - parentTenant2 := fixBusinessTenantMappingInput(parent2, parent2, provider, "", "", tenant.Customer) - parentTenant3 := fixBusinessTenantMappingInput(parent3, parent3, provider, "", "", tenant.Customer) + parentTenant1 := fixBusinessTenantMappingInput(parent1, parent1, provider, "", "", "", tenant.Customer) + parentTenant2 := fixBusinessTenantMappingInput(parent2, parent2, provider, "", "", "", tenant.Customer) + parentTenant3 := fixBusinessTenantMappingInput(parent3, parent3, provider, "", "", "", tenant.Customer) parentTenants := []model.BusinessTenantMappingInput{parentTenant1, parentTenant2, parentTenant3} parentTenant1BusinessMapping := parentTenant1.ToBusinessTenantMapping(parent1GUID) @@ -64,14 +65,18 @@ func TestService_SyncTenants(t *testing.T) { busTenant2GUID := "49af7161-7dc7-472b-a969-d2f0430fc41d" busTenant3GUID := "72409a54-2b1a-4cbb-803b-515315c74d02" - busTenant1 := fixBusinessTenantMappingInput("foo", "1", provider, "subdomain-1", parent1, tenant.Account) - busTenant2 := fixBusinessTenantMappingInput("bar", "2", provider, "subdomain-2", parent2, tenant.Account) - busTenant3 := fixBusinessTenantMappingInput("baz", "3", provider, "subdomain-3", parent3, tenant.Account) - businessTenants := []model.BusinessTenantMappingInput{busTenant1, busTenant2, busTenant3} + busTenant1 := fixBusinessTenantMappingInput("foo", "1", provider, "subdomain-1", region, parent1, tenant.Account) + busTenant2 := fixBusinessTenantMappingInput("bar", "2", provider, "subdomain-2", region, parent2, tenant.Account) + busTenant3 := fixBusinessTenantMappingInput("baz", "3", provider, "subdomain-3", region, parent3, tenant.Account) - busTenant1WithParentGUID := fixBusinessTenantMappingInput("foo", "1", provider, "subdomain-1", parent1GUID, tenant.Account) - busTenant2WithParentGUID := fixBusinessTenantMappingInput("bar", "2", provider, "subdomain-2", parent2GUID, tenant.Account) - busTenant3WithParentGUID := fixBusinessTenantMappingInput("baz", "3", provider, "subdomain-3", parent3GUID, tenant.Account) + busTenantForDeletion1 := fixBusinessTenantMappingInput("foo", "1", provider, "subdomain-1", "", parent1, tenant.Account) + busTenantForDeletion2 := fixBusinessTenantMappingInput("bar", "2", provider, "subdomain-2", "", parent2, tenant.Account) + busTenantForDeletion3 := fixBusinessTenantMappingInput("baz", "3", provider, "subdomain-3", "", parent3, tenant.Account) + businessTenantsForDeletion := []model.BusinessTenantMappingInput{busTenantForDeletion1, busTenantForDeletion2, busTenantForDeletion3} + + busTenant1WithParentGUID := fixBusinessTenantMappingInput("foo", "1", provider, "subdomain-1", region, parent1GUID, tenant.Account) + busTenant2WithParentGUID := fixBusinessTenantMappingInput("bar", "2", provider, "subdomain-2", region, parent2GUID, tenant.Account) + busTenant3WithParentGUID := fixBusinessTenantMappingInput("baz", "3", provider, "subdomain-3", region, parent3GUID, tenant.Account) businessTenantsWithParentGUID := []model.BusinessTenantMappingInput{busTenant1WithParentGUID, busTenant2WithParentGUID, busTenant3WithParentGUID} businessTenant1BusinessMapping := busTenant1.ToBusinessTenantMapping(busTenant1GUID) @@ -313,7 +318,7 @@ func TestService_SyncTenants(t *testing.T) { TenantStorageSvcFn: func() *automock.TenantService { svc := &automock.TenantService{} svc.On("List", txtest.CtxWithDBMatcher()).Return(businessTenantsBusinessMappingPointers, nil).Once() - svc.On("DeleteMany", txtest.CtxWithDBMatcher(), matchArrayWithoutOrderArgument(businessTenants)).Return(nil).Once() + svc.On("DeleteMany", txtest.CtxWithDBMatcher(), matchArrayWithoutOrderArgument(businessTenantsForDeletion)).Return(nil).Once() return svc }, KubeClientFn: func() *automock.KubeClient { @@ -455,7 +460,7 @@ func TestService_SyncTenants(t *testing.T) { }, nil).Once() svc.On("CreateManyIfNotExists", txtest.CtxWithDBMatcher(), matchArrayWithoutOrderArgument(businessTenantsWithParentGUID[1:])).Return(nil).Once() - svc.On("DeleteMany", txtest.CtxWithDBMatcher(), businessTenants[0:1]).Return(nil).Once() + svc.On("DeleteMany", txtest.CtxWithDBMatcher(), businessTenantsForDeletion[0:1]).Return(nil).Once() return svc }, @@ -522,7 +527,7 @@ func TestService_SyncTenants(t *testing.T) { svc.On("CreateManyIfNotExists", txtest.CtxWithDBMatcher(), matchArrayWithoutOrderArgument(businessTenantsWithParentGUID[1:])).Return(nil).Once() - svc.On("DeleteMany", txtest.CtxWithDBMatcher(), businessTenants[0:1]).Return(nil).Once() + svc.On("DeleteMany", txtest.CtxWithDBMatcher(), businessTenantsForDeletion[0:1]).Return(nil).Once() return svc }, @@ -566,7 +571,7 @@ func TestService_SyncTenants(t *testing.T) { }, nil).Once() svc.On("CreateManyIfNotExists", txtest.CtxWithDBMatcher(), matchArrayWithoutOrderArgument(businessTenantsWithParentGUID[1:])).Return(nil).Once() - svc.On("DeleteMany", txtest.CtxWithDBMatcher(), businessTenants[0:1]).Return(nil).Once() + svc.On("DeleteMany", txtest.CtxWithDBMatcher(), businessTenantsForDeletion[0:1]).Return(nil).Once() return svc }, @@ -816,7 +821,7 @@ func TestService_SyncTenants(t *testing.T) { TenantStorageSvcFn: func() *automock.TenantService { svc := &automock.TenantService{} svc.On("List", txtest.CtxWithDBMatcher()).Return(businessTenantsBusinessMappingPointers, nil).Once() - svc.On("DeleteMany", txtest.CtxWithDBMatcher(), matchArrayWithoutOrderArgument(businessTenants)).Return(testErr).Once() + svc.On("DeleteMany", txtest.CtxWithDBMatcher(), matchArrayWithoutOrderArgument(businessTenantsForDeletion)).Return(testErr).Once() return svc }, KubeClientFn: func() *automock.KubeClient { @@ -1028,7 +1033,7 @@ func TestService_SyncTenants(t *testing.T) { LabelValue: "id", SourceTenant: "source_tenant", TargetTenant: "target_tenant", - }, provider, apiClient, tenantStorageSvc, runtimeStorageSvc, labelDefSvc, movedRuntimeLabelKey, time.Hour) + }, provider, region, apiClient, tenantStorageSvc, runtimeStorageSvc, labelDefSvc, movedRuntimeLabelKey, time.Hour) svc.SetRetryAttempts(1) // WHEN @@ -1087,7 +1092,7 @@ func TestService_SyncTenants(t *testing.T) { LabelValue: "id", SourceTenant: "source_tenant", TargetTenant: "target_tenant", - }, provider, apiClient, tenantStorageSvc, nil, nil, movedRuntimeLabelKey, time.Hour) + }, provider, region, apiClient, tenantStorageSvc, nil, nil, movedRuntimeLabelKey, time.Hour) // WHEN err := svc.SyncTenants() diff --git a/components/director/internal/tenantfetchersvc/automock/tenant_provisioner.go b/components/director/internal/tenantfetchersvc/automock/tenant_provisioner.go index 4a44170cf7..a2e4e082ca 100644 --- a/components/director/internal/tenantfetchersvc/automock/tenant_provisioner.go +++ b/components/director/internal/tenantfetchersvc/automock/tenant_provisioner.go @@ -1,11 +1,11 @@ -// Code generated by mockery (devel). DO NOT EDIT. +// Code generated by mockery 2.9.0. DO NOT EDIT. package automock import ( context "context" - model "github.com/kyma-incubator/compass/components/director/internal/model" + tenantfetchersvc "github.com/kyma-incubator/compass/components/director/internal/tenantfetchersvc" mock "github.com/stretchr/testify/mock" ) @@ -14,13 +14,27 @@ type TenantProvisioner struct { mock.Mock } -// ProvisionTenant provides a mock function with given fields: ctx, tenant -func (_m *TenantProvisioner) ProvisionTenant(ctx context.Context, tenant model.BusinessTenantMappingInput) error { - ret := _m.Called(ctx, tenant) +// ProvisionRegionalTenants provides a mock function with given fields: _a0, _a1 +func (_m *TenantProvisioner) ProvisionRegionalTenants(_a0 context.Context, _a1 tenantfetchersvc.TenantProvisioningRequest) error { + ret := _m.Called(_a0, _a1) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, model.BusinessTenantMappingInput) error); ok { - r0 = rf(ctx, tenant) + if rf, ok := ret.Get(0).(func(context.Context, tenantfetchersvc.TenantProvisioningRequest) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ProvisionTenants provides a mock function with given fields: _a0, _a1 +func (_m *TenantProvisioner) ProvisionTenants(_a0 context.Context, _a1 tenantfetchersvc.TenantProvisioningRequest) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, tenantfetchersvc.TenantProvisioningRequest) error); ok { + r0 = rf(_a0, _a1) } else { r0 = ret.Error(0) } diff --git a/components/director/internal/tenantfetchersvc/automock/tenant_service.go b/components/director/internal/tenantfetchersvc/automock/tenant_service.go index 9c4a3024d5..71cd58bf02 100644 --- a/components/director/internal/tenantfetchersvc/automock/tenant_service.go +++ b/components/director/internal/tenantfetchersvc/automock/tenant_service.go @@ -48,3 +48,17 @@ func (_m *TenantService) GetInternalTenant(ctx context.Context, externalTenant s return r0, r1 } + +// SetLabel provides a mock function with given fields: ctx, labelInput +func (_m *TenantService) SetLabel(ctx context.Context, labelInput *model.LabelInput) error { + ret := _m.Called(ctx, labelInput) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *model.LabelInput) error); ok { + r0 = rf(ctx, labelInput) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/components/director/internal/tenantfetchersvc/handler.go b/components/director/internal/tenantfetchersvc/handler.go index 69532c88da..855a1d9a5a 100644 --- a/components/director/internal/tenantfetchersvc/handler.go +++ b/components/director/internal/tenantfetchersvc/handler.go @@ -5,50 +5,46 @@ import ( "fmt" "io/ioutil" "net/http" - "time" + "github.com/gorilla/mux" "github.com/kyma-incubator/compass/components/director/pkg/apperrors" "github.com/kyma-incubator/compass/components/director/pkg/log" - tenantEntity "github.com/kyma-incubator/compass/components/director/pkg/tenant" "github.com/pkg/errors" "github.com/tidwall/gjson" - "github.com/kyma-incubator/compass/components/director/internal/model" "github.com/kyma-incubator/compass/components/director/pkg/persistence" ) const ( compassURL = "https://github.com/kyma-incubator/compass" tenantCreationFailureMsgFmt = "Failed to create tenant with ID %s" - - autogeneratedTenantProvider = "autogenerated" ) -// TenantProvisioner missing godoc +// TenantProvisioner is used to create all related to the incoming request tenants, and build their hierarchy; //go:generate mockery --name=TenantProvisioner --output=automock --outpkg=automock --case=underscore type TenantProvisioner interface { - ProvisionTenant(ctx context.Context, tenant model.BusinessTenantMappingInput) error + ProvisionTenants(context.Context, TenantProvisioningRequest) error + ProvisionRegionalTenants(context.Context, TenantProvisioningRequest) error } -// HandlerConfig missing godoc +// HandlerConfig is the configuration required by the tenant handler. +// It includes configurable parameters for incoming requests, including different tenant IDs json properties, and path parameters. type HandlerConfig struct { - HandlerEndpoint string `envconfig:"APP_HANDLER_ENDPOINT,default=/v1/callback/{tenantId}"` - TenantPathParam string `envconfig:"APP_TENANT_PATH_PARAM,default=tenantId"` - + HandlerEndpoint string `envconfig:"APP_HANDLER_ENDPOINT,default=/v1/callback/{tenantId}"` + RegionalHandlerEndpoint string `envconfig:"APP_REGIONAL_HANDLER_ENDPOINT,default=/v1/regional/{region}/callback/{tenantId}"` + DependenciesEndpoint string `envconfig:"APP_DEPENDENCIES_ENDPOINT,default=/v1/dependencies"` + TenantPathParam string `envconfig:"APP_TENANT_PATH_PARAM,default=tenantId"` + RegionPathParam string `envconfig:"APP_REGION_PATH_PARAM,default=region"` TenantProviderConfig - - JWKSSyncPeriod time.Duration `envconfig:"default=5m"` - AllowJWTSigningNone bool `envconfig:"APP_ALLOW_JWT_SIGNING_NONE,default=false"` - JwksEndpoint string `envconfig:"APP_JWKS_ENDPOINT"` - SubscriptionCallbackScope string `envconfig:"APP_SUBSCRIPTION_CALLBACK_SCOPE"` } -// TenantProviderConfig missing godoc +// TenantProviderConfig includes the configuration for tenant providers - the tenant ID json property names, the subdomain property name, and the tenant provider name. type TenantProviderConfig struct { - TenantIDProperty string `envconfig:"APP_TENANT_PROVIDER_TENANT_ID_PROPERTY,default=tenantId"` - CustomerIDProperty string `envconfig:"APP_TENANT_PROVIDER_CUSTOMER_ID_PROPERTY,default=customerId"` - SubdomainProperty string `envconfig:"APP_TENANT_PROVIDER_SUBDOMAIN_PROPERTY,default=subdomain"` - TenantProvider string `envconfig:"APP_TENANT_PROVIDER,default=external-provider"` + TenantIDProperty string `envconfig:"APP_TENANT_PROVIDER_TENANT_ID_PROPERTY,default=tenantId"` + SubaccountTenantIDProperty string `envconfig:"APP_TENANT_PROVIDER_SUBACCOUNT_TENANT_ID_PROPERTY,default=subaccountTenantId"` + CustomerIDProperty string `envconfig:"APP_TENANT_PROVIDER_CUSTOMER_ID_PROPERTY,default=customerId"` + SubdomainProperty string `envconfig:"APP_TENANT_PROVIDER_SUBDOMAIN_PROPERTY,default=subdomain"` + TenantProvider string `envconfig:"APP_TENANT_PROVIDER,default=external-provider"` } type handler struct { @@ -57,7 +53,7 @@ type handler struct { config HandlerConfig } -// NewTenantsHTTPHandler missing godoc +// NewTenantsHTTPHandler returns a new HTTP handler, responsible for creation and deletion of regional and non-regional tenants. func NewTenantsHTTPHandler(provisioner TenantProvisioner, transact persistence.Transactioner, config HandlerConfig) *handler { return &handler{ provisioner: provisioner, @@ -66,40 +62,30 @@ func NewTenantsHTTPHandler(provisioner TenantProvisioner, transact persistence.T } } -// Create missing godoc +// Create handles creation of non-regional tenants. func (h *handler) Create(writer http.ResponseWriter, request *http.Request) { - ctx := request.Context() + h.handleTenantCreationRequest(writer, request, "") +} - body, err := readBody(request) - if err != nil { - log.C(ctx).WithError(err).Errorf("Failed to read tenant information from request body: %v", err) - http.Error(writer, "Failed to read tenant information from request body", http.StatusInternalServerError) - return - } - accountTenant, err := h.tenantInfoFromBody(body) - if err != nil { - log.C(ctx).WithError(err).Errorf("Failed to extract tenant information from request body: %v", err) - http.Error(writer, fmt.Sprintf("Failed to extract tenant information from request body: %s", err.Error()), http.StatusBadRequest) - return - } +// CreateRegional handles creation of regional tenants. +func (h *handler) CreateRegional(writer http.ResponseWriter, request *http.Request) { + ctx := request.Context() - if err := h.provisionTenant(ctx, *accountTenant); err != nil { - log.C(ctx).WithError(err).Errorf("Failed to provision tenant with ID %s: %v", accountTenant.ExternalTenant, err) - http.Error(writer, fmt.Sprintf(tenantCreationFailureMsgFmt, accountTenant.ExternalTenant), http.StatusInternalServerError) + vars := mux.Vars(request) + region, ok := vars[h.config.RegionPathParam] + if !ok { + log.C(ctx).Errorf("Region path parameter is missing from request") + http.Error(writer, "Region path parameter is missing from request", http.StatusBadRequest) return } - writer.Header().Set("Content-Type", "text/plain") - writer.WriteHeader(http.StatusOK) - if _, err := writer.Write([]byte(compassURL)); err != nil { - log.C(ctx).WithError(err).Errorf("Failed to write response body for tenant request creation for tenant %s: %v", accountTenant.ExternalTenant, err) - } + h.handleTenantCreationRequest(writer, request, region) } -// DeleteByExternalID missing godoc +// DeleteByExternalID handles both regional and non-regional tenant deletion requests. func (h *handler) DeleteByExternalID(writer http.ResponseWriter, req *http.Request) { ctx := req.Context() - body, err := readBody(req) + body, err := ioutil.ReadAll(req.Body) if err != nil { log.C(ctx).WithError(err).Errorf("Failed to read tenant information from delete request body: %v", err) writer.WriteHeader(http.StatusOK) @@ -115,28 +101,65 @@ func (h *handler) DeleteByExternalID(writer http.ResponseWriter, req *http.Reque writer.WriteHeader(http.StatusOK) } -func (h *handler) tenantInfoFromBody(body []byte) (*model.BusinessTenantMappingInput, error) { +// Dependencies handler returns all external services where once created in Compass, the tenant should be created as well. +func (h *handler) Dependencies(writer http.ResponseWriter, request *http.Request) { + writer.Header().Set("Content-Type", "application/json") + if _, err := writer.Write([]byte("{}")); err != nil { + log.C(request.Context()).WithError(err).Errorf("Failed to write response body for dependencies request") + return + } +} + +func (h *handler) handleTenantCreationRequest(writer http.ResponseWriter, request *http.Request, region string) { + ctx := request.Context() + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + log.C(ctx).WithError(err).Errorf("Failed to read tenant information from request body: %v", err) + http.Error(writer, "Failed to read tenant information from request body", http.StatusInternalServerError) + return + } + provisioningReq, err := h.getProvisioningRequest(body, region) + if err != nil { + log.C(ctx).WithError(err).Errorf("Failed to extract tenant information from request body: %v", err) + http.Error(writer, fmt.Sprintf("Failed to extract tenant information from request body: %s", err.Error()), http.StatusBadRequest) + return + } + mainTenantID := provisioningReq.MainTenantID() + if err := h.provisionTenants(ctx, provisioningReq, region); err != nil { + log.C(ctx).WithError(err).Errorf("Failed to provision tenant with ID %s: %v", mainTenantID, err) + http.Error(writer, fmt.Sprintf(tenantCreationFailureMsgFmt, mainTenantID), http.StatusInternalServerError) + return + } + + writer.Header().Set("Content-Type", "text/plain") + writer.WriteHeader(http.StatusOK) + if _, err := writer.Write([]byte(compassURL)); err != nil { + log.C(ctx).WithError(err).Errorf("Failed to write response body for tenant request creation for tenant %s: %v", mainTenantID, err) + } +} + +func (h *handler) getProvisioningRequest(body []byte, region string) (*TenantProvisioningRequest, error) { properties, err := getProperties(body, map[string]bool{ - h.config.TenantIDProperty: true, - h.config.SubdomainProperty: true, - h.config.CustomerIDProperty: false, + h.config.TenantIDProperty: true, + h.config.SubaccountTenantIDProperty: false, + h.config.SubdomainProperty: true, + h.config.CustomerIDProperty: false, }) if err != nil { return nil, err } - return &model.BusinessTenantMappingInput{ - Name: properties[h.config.TenantIDProperty], - ExternalTenant: properties[h.config.TenantIDProperty], - Parent: properties[h.config.CustomerIDProperty], - Subdomain: properties[h.config.SubdomainProperty], - Type: tenantEntity.TypeToStr(tenantEntity.Account), - Provider: h.config.TenantProvider, + return &TenantProvisioningRequest{ + AccountTenantID: properties[h.config.TenantIDProperty], + SubaccountTenantID: properties[h.config.SubaccountTenantIDProperty], + CustomerTenantID: properties[h.config.CustomerIDProperty], + Subdomain: properties[h.config.SubdomainProperty], + Region: region, }, nil } -func (h *handler) provisionTenant(ctx context.Context, tenant model.BusinessTenantMappingInput) error { - externalTenantID := tenant.ExternalTenant +func (h *handler) provisionTenants(ctx context.Context, request *TenantProvisioningRequest, region string) error { tx, err := h.transact.Begin() if err != nil { return errors.Wrapf(err, "while starting DB transaction") @@ -144,16 +167,23 @@ func (h *handler) provisionTenant(ctx context.Context, tenant model.BusinessTena defer h.transact.RollbackUnlessCommitted(ctx, tx) ctx = persistence.SaveToContext(ctx, tx) - if err := h.provisioner.ProvisionTenant(ctx, tenant); err != nil && !apperrors.IsNotUniqueError(err) { - return errors.Wrapf(err, "while provisioning tenant with external ID %s", externalTenantID) + + if len(region) > 0 { + err = h.provisioner.ProvisionRegionalTenants(ctx, *request) + } else { + err = h.provisioner.ProvisionTenants(ctx, *request) + } + if err != nil && !apperrors.IsNotUniqueError(err) { + return err } if err := tx.Commit(); err != nil { - return errors.Wrapf(err, "failed to commit transaction while storing tenant with external ID %s", externalTenantID) + return errors.Wrapf(err, "failed to commit transaction while storing tenant") } return nil } + func getProperties(body []byte, props map[string]bool) (map[string]string, error) { resultProps := map[string]string{} for propName, mandatory := range props { @@ -166,20 +196,3 @@ func getProperties(body []byte, props map[string]bool) (map[string]string, error return resultProps, nil } - -func readBody(r *http.Request) ([]byte, error) { - ctx := r.Context() - - buf, err := ioutil.ReadAll(r.Body) - if err != nil { - return nil, err - } - - defer func() { - if err := r.Body.Close(); err != nil { - log.C(ctx).WithError(err).Errorf("Unable to close request body: %v", err) - } - }() - - return buf, nil -} diff --git a/components/director/internal/tenantfetchersvc/handler_test.go b/components/director/internal/tenantfetchersvc/handler_test.go index 316ae9ea2c..acea0ab8e3 100644 --- a/components/director/internal/tenantfetchersvc/handler_test.go +++ b/components/director/internal/tenantfetchersvc/handler_test.go @@ -9,28 +9,30 @@ import ( "net/http/httptest" "testing" - "github.com/kyma-incubator/compass/components/director/internal/model" + "github.com/gorilla/mux" "github.com/kyma-incubator/compass/components/director/internal/tenantfetchersvc" "github.com/kyma-incubator/compass/components/director/internal/tenantfetchersvc/automock" persistenceautomock "github.com/kyma-incubator/compass/components/director/pkg/persistence/automock" "github.com/kyma-incubator/compass/components/director/pkg/persistence/txtest" - tenantEntity "github.com/kyma-incubator/compass/components/director/pkg/tenant" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) const ( - tenantExtID = "tenant-external-id" - tenantSubdomain = "mytenant" + tenantExtID = "tenant-external-id" + tenantSubdomain = "mytenant" + tenantRegion = "myregion" + + subaccountTenantSubdomain = "myregionaltenant" + subaccountTenantExtID = "regional-tenant-external-id" + parentTenantExtID = "parent-tenant-external-id" - parentTenantIntID = "27e23549-d369-4603-b3b8-e46883ab4a60" - testProviderName = "test-provider" - autogeneratedProviderName = "autogenerated" - tenantProviderTenantIDProperty = "tenantId" - tenantProviderCustomerIDProperty = "customerId" - tenantProviderSubdomainProperty = "subdomain" + tenantProviderTenantIDProperty = "tenantId" + tenantProviderCustomerIDProperty = "customerId" + tenantProviderSubdomainProperty = "subdomain" + tenantProviderSubaccountTenantIDProperty = "subaccountTenantId" tenantCreationFailureMsgFmt = "Failed to create tenant with ID %s" compassURL = "https://github.com/kyma-incubator/compass" @@ -54,6 +56,12 @@ type tenantCreationRequest struct { Subdomain string `json:"subdomain"` } +type regionalTenantCreationRequest struct { + TenantID string `json:"subaccountTenantId"` + ParentID string `json:"tenantId"` + Subdomain string `json:"subdomain"` +} + type errReader int func (errReader) Read(p []byte) (n int, err error) { @@ -90,27 +98,20 @@ func TestService_Create(t *testing.T) { }) assert.NoError(t, err) - accountTenant := model.BusinessTenantMappingInput{ - Name: tenantExtID, - ExternalTenant: tenantExtID, - Parent: parentTenantExtID, - Type: tenantEntity.TypeToStr(tenantEntity.Account), - Provider: testProviderName, - Subdomain: tenantSubdomain, + accountProvisioningRequest := tenantfetchersvc.TenantProvisioningRequest{ + AccountTenantID: tenantExtID, + CustomerTenantID: parentTenantExtID, + Subdomain: tenantSubdomain, } - accountTenantWithoutParent := model.BusinessTenantMappingInput{ - Name: tenantExtID, - ExternalTenant: tenantExtID, - Type: tenantEntity.TypeToStr(tenantEntity.Account), - Provider: testProviderName, - Subdomain: tenantSubdomain, + accountWithoutParentProvisioningRequest := tenantfetchersvc.TenantProvisioningRequest{ + AccountTenantID: tenantExtID, + Subdomain: tenantSubdomain, } testCases := []struct { Name string TenantProvisionerFn func() *automock.TenantProvisioner TxFn func() (*persistenceautomock.PersistenceTx, *persistenceautomock.Transactioner) - HandlerCfg tenantfetchersvc.HandlerConfig Request *http.Request ExpectedErrorOutput string ExpectedSuccessOutput string @@ -121,10 +122,9 @@ func TestService_Create(t *testing.T) { TxFn: txGen.ThatSucceeds, TenantProvisionerFn: func() *automock.TenantProvisioner { provisioner := &automock.TenantProvisioner{} - provisioner.On("ProvisionTenant", txtest.CtxWithDBMatcher(), accountTenant).Return(nil).Once() + provisioner.On("ProvisionTenants", txtest.CtxWithDBMatcher(), accountProvisioningRequest).Return(nil).Once() return provisioner }, - HandlerCfg: validHandlerConfig, Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(validRequestBody)), ExpectedSuccessOutput: compassURL, ExpectedStatusCode: http.StatusOK, @@ -134,10 +134,9 @@ func TestService_Create(t *testing.T) { TxFn: txGen.ThatSucceeds, TenantProvisionerFn: func() *automock.TenantProvisioner { provisioner := &automock.TenantProvisioner{} - provisioner.On("ProvisionTenant", txtest.CtxWithDBMatcher(), accountTenantWithoutParent).Return(nil).Once() + provisioner.On("ProvisionTenants", txtest.CtxWithDBMatcher(), accountWithoutParentProvisioningRequest).Return(nil).Once() return provisioner }, - HandlerCfg: validHandlerConfig, Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(bodyWithMissingParent)), ExpectedSuccessOutput: compassURL, ExpectedStatusCode: http.StatusOK, @@ -146,13 +145,6 @@ func TestService_Create(t *testing.T) { Name: "Returns error when reading request body fails", TxFn: txGen.ThatDoesntStartTransaction, TenantProvisionerFn: func() *automock.TenantProvisioner { return &automock.TenantProvisioner{} }, - HandlerCfg: tenantfetchersvc.HandlerConfig{ - TenantProviderConfig: tenantfetchersvc.TenantProviderConfig{ - TenantProvider: testProviderName, - TenantIDProperty: tenantProviderTenantIDProperty, - CustomerIDProperty: tenantProviderCustomerIDProperty, - }, - }, Request: httptest.NewRequest(http.MethodPut, target, errReader(0)), ExpectedErrorOutput: "Failed to read tenant information from request body", ExpectedStatusCode: http.StatusInternalServerError, @@ -161,7 +153,6 @@ func TestService_Create(t *testing.T) { Name: "Returns error when request body doesn't contain tenantID", TxFn: txGen.ThatDoesntStartTransaction, TenantProvisionerFn: func() *automock.TenantProvisioner { return &automock.TenantProvisioner{} }, - HandlerCfg: validHandlerConfig, Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(bodyWithMissingTenant)), ExpectedErrorOutput: fmt.Sprintf("mandatory property %q is missing from request body", tenantProviderTenantIDProperty), ExpectedStatusCode: http.StatusBadRequest, @@ -170,7 +161,6 @@ func TestService_Create(t *testing.T) { Name: "Returns error when request body doesn't contain tenant subdomain", TxFn: txGen.ThatDoesntStartTransaction, TenantProvisionerFn: func() *automock.TenantProvisioner { return &automock.TenantProvisioner{} }, - HandlerCfg: validHandlerConfig, Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(bodyWithMissingTenantSubdomain)), ExpectedErrorOutput: fmt.Sprintf("mandatory property %q is missing from request body", tenantProviderSubdomainProperty), ExpectedStatusCode: http.StatusBadRequest, @@ -179,9 +169,8 @@ func TestService_Create(t *testing.T) { Name: "Returns error when beginning transaction fails", TxFn: txGen.ThatFailsOnBegin, TenantProvisionerFn: func() *automock.TenantProvisioner { return &automock.TenantProvisioner{} }, - HandlerCfg: validHandlerConfig, Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(validRequestBody)), - ExpectedErrorOutput: fmt.Sprintf(tenantCreationFailureMsgFmt, accountTenant.ExternalTenant), + ExpectedErrorOutput: fmt.Sprintf(tenantCreationFailureMsgFmt, accountProvisioningRequest.AccountTenantID), ExpectedStatusCode: http.StatusInternalServerError, }, { @@ -189,10 +178,9 @@ func TestService_Create(t *testing.T) { TxFn: txGen.ThatSucceeds, TenantProvisionerFn: func() *automock.TenantProvisioner { provisioner := &automock.TenantProvisioner{} - provisioner.On("ProvisionTenant", txtest.CtxWithDBMatcher(), accountTenant).Return(testError).Once() + provisioner.On("ProvisionTenants", txtest.CtxWithDBMatcher(), accountProvisioningRequest).Return(testError).Once() return provisioner }, - HandlerCfg: validHandlerConfig, Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(validRequestBody)), ExpectedStatusCode: http.StatusInternalServerError, ExpectedErrorOutput: fmt.Sprintf(tenantCreationFailureMsgFmt, tenantExtID), @@ -202,12 +190,11 @@ func TestService_Create(t *testing.T) { TxFn: txGen.ThatFailsOnCommit, TenantProvisionerFn: func() *automock.TenantProvisioner { provisioner := &automock.TenantProvisioner{} - provisioner.On("ProvisionTenant", txtest.CtxWithDBMatcher(), accountTenant).Return(nil).Once() + provisioner.On("ProvisionTenants", txtest.CtxWithDBMatcher(), accountProvisioningRequest).Return(nil).Once() return provisioner }, - HandlerCfg: validHandlerConfig, Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(validRequestBody)), - ExpectedErrorOutput: fmt.Sprintf(tenantCreationFailureMsgFmt, accountTenant.ExternalTenant), + ExpectedErrorOutput: fmt.Sprintf(tenantCreationFailureMsgFmt, accountProvisioningRequest.AccountTenantID), ExpectedStatusCode: http.StatusInternalServerError, }, } @@ -218,7 +205,7 @@ func TestService_Create(t *testing.T) { provisioner := testCase.TenantProvisionerFn() defer mock.AssertExpectationsForObjects(t, transact, provisioner) - handler := tenantfetchersvc.NewTenantsHTTPHandler(provisioner, transact, testCase.HandlerCfg) + handler := tenantfetchersvc.NewTenantsHTTPHandler(provisioner, transact, validHandlerConfig) req := testCase.Request w := httptest.NewRecorder() @@ -245,6 +232,186 @@ func TestService_Create(t *testing.T) { } } +func TestService_CreateRegional(t *testing.T) { + //GIVEN + region := "eu-1" + + txGen := txtest.NewTransactionContextGenerator(errors.New("err")) + target := "http://example.com/foo/:region" + txtest.CtxWithDBMatcher() + + validRequestBody, err := json.Marshal(regionalTenantCreationRequest{ + TenantID: subaccountTenantExtID, + ParentID: tenantExtID, + Subdomain: subaccountTenantSubdomain, + }) + assert.NoError(t, err) + + bodyWithMissingParent, err := json.Marshal(regionalTenantCreationRequest{ + TenantID: subaccountTenantExtID, + Subdomain: tenantSubdomain, + }) + assert.NoError(t, err) + + bodyWithMissingTenantSubdomain, err := json.Marshal(regionalTenantCreationRequest{ + TenantID: subaccountTenantExtID, + ParentID: tenantExtID, + }) + assert.NoError(t, err) + + validHandlerConfig := tenantfetchersvc.HandlerConfig{ + RegionPathParam: "region", + TenantProviderConfig: tenantfetchersvc.TenantProviderConfig{ + TenantProvider: testProviderName, + TenantIDProperty: tenantProviderTenantIDProperty, + SubaccountTenantIDProperty: tenantProviderSubaccountTenantIDProperty, + CustomerIDProperty: tenantProviderCustomerIDProperty, + SubdomainProperty: tenantProviderSubdomainProperty, + }, + } + regionalTenant := tenantfetchersvc.TenantProvisioningRequest{ + SubaccountTenantID: subaccountTenantExtID, + AccountTenantID: tenantExtID, + Subdomain: subaccountTenantSubdomain, + Region: region, + } + + testCases := []struct { + Name string + provisionerFn func() *automock.TenantProvisioner + TxFn func() (*persistenceautomock.PersistenceTx, *persistenceautomock.Transactioner) + Request *http.Request + Region string + ExpectedErrorOutput string + ExpectedSuccessOutput string + ExpectedStatusCode int + }{ + { + Name: "Succeeds", + TxFn: txGen.ThatSucceeds, + provisionerFn: func() *automock.TenantProvisioner { + provisioner := &automock.TenantProvisioner{} + provisioner.On("ProvisionRegionalTenants", txtest.CtxWithDBMatcher(), regionalTenant).Return(nil).Once() + return provisioner + }, + Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(validRequestBody)), + Region: region, + ExpectedSuccessOutput: compassURL, + ExpectedStatusCode: http.StatusOK, + }, + { + Name: "Returns error when region path parameter is missing", + TxFn: txGen.ThatDoesntStartTransaction, + provisionerFn: func() *automock.TenantProvisioner { return &automock.TenantProvisioner{} }, + Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(validRequestBody)), + ExpectedStatusCode: http.StatusBadRequest, + ExpectedErrorOutput: "Region path parameter is missing from request", + }, + { + Name: "Returns error when parent tenant is not found in body", + TxFn: txGen.ThatDoesntStartTransaction, + provisionerFn: func() *automock.TenantProvisioner { return &automock.TenantProvisioner{} }, + Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(bodyWithMissingParent)), + Region: region, + ExpectedStatusCode: http.StatusBadRequest, + ExpectedErrorOutput: fmt.Sprintf("mandatory property %q is missing from request body", tenantProviderTenantIDProperty), + }, + { + Name: "Returns error when reading request body fails", + TxFn: txGen.ThatDoesntStartTransaction, + provisionerFn: func() *automock.TenantProvisioner { return &automock.TenantProvisioner{} }, + Request: httptest.NewRequest(http.MethodPut, target, errReader(0)), + Region: region, + ExpectedErrorOutput: "Failed to read tenant information from request body", + ExpectedStatusCode: http.StatusInternalServerError, + }, + { + Name: "Returns error when request body doesn't contain tenant subdomain", + TxFn: txGen.ThatDoesntStartTransaction, + provisionerFn: func() *automock.TenantProvisioner { return &automock.TenantProvisioner{} }, + Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(bodyWithMissingTenantSubdomain)), + Region: region, + ExpectedErrorOutput: fmt.Sprintf("mandatory property %q is missing from request body", tenantProviderSubdomainProperty), + ExpectedStatusCode: http.StatusBadRequest, + }, + { + Name: "Returns error when beginning transaction fails", + TxFn: txGen.ThatFailsOnBegin, + provisionerFn: func() *automock.TenantProvisioner { return &automock.TenantProvisioner{} }, + Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(validRequestBody)), + Region: region, + ExpectedErrorOutput: fmt.Sprintf(tenantCreationFailureMsgFmt, regionalTenant.SubaccountTenantID), + ExpectedStatusCode: http.StatusInternalServerError, + }, + { + Name: "Returns error when tenant creation fails", + TxFn: txGen.ThatDoesntExpectCommit, + provisionerFn: func() *automock.TenantProvisioner { + provisioner := &automock.TenantProvisioner{} + provisioner.On("ProvisionRegionalTenants", txtest.CtxWithDBMatcher(), regionalTenant).Return(testError).Once() + return provisioner + }, + Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(validRequestBody)), + Region: region, + ExpectedStatusCode: http.StatusInternalServerError, + ExpectedErrorOutput: fmt.Sprintf(tenantCreationFailureMsgFmt, subaccountTenantExtID), + }, + { + Name: "Returns error when transaction commit fails", + TxFn: txGen.ThatFailsOnCommit, + provisionerFn: func() *automock.TenantProvisioner { + provisioner := &automock.TenantProvisioner{} + provisioner.On("ProvisionRegionalTenants", txtest.CtxWithDBMatcher(), regionalTenant).Return(nil).Once() + return provisioner + }, + Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(validRequestBody)), + Region: region, + ExpectedErrorOutput: fmt.Sprintf(tenantCreationFailureMsgFmt, regionalTenant.SubaccountTenantID), + ExpectedStatusCode: http.StatusInternalServerError, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + _, transact := testCase.TxFn() + provisioner := testCase.provisionerFn() + defer mock.AssertExpectationsForObjects(t, transact, provisioner) + + handler := tenantfetchersvc.NewTenantsHTTPHandler(provisioner, transact, validHandlerConfig) + req := testCase.Request + + if len(testCase.Region) > 0 { + vars := map[string]string{ + "region": testCase.Region, + } + req = mux.SetURLVars(req, vars) + } + + w := httptest.NewRecorder() + + //WHEN + handler.CreateRegional(w, req) + + // THEN + resp := w.Result() + body, err := ioutil.ReadAll(resp.Body) + assert.NoError(t, err) + + if len(testCase.ExpectedErrorOutput) > 0 { + assert.Contains(t, string(body), testCase.ExpectedErrorOutput) + } else { + assert.NoError(t, err) + } + + if testCase.ExpectedSuccessOutput != "" { + assert.Equal(t, testCase.ExpectedSuccessOutput, string(body)) + } + + assert.Equal(t, testCase.ExpectedStatusCode, resp.StatusCode) + }) + } +} + func TestService_Delete(t *testing.T) { //GIVEN testErr := errors.New("test error") @@ -269,7 +436,6 @@ func TestService_Delete(t *testing.T) { //WHEN handler.DeleteByExternalID(w, req) - // THEN resp := w.Result() assert.Equal(t, http.StatusOK, resp.StatusCode) diff --git a/components/director/internal/tenantfetchersvc/provisioner.go b/components/director/internal/tenantfetchersvc/provisioner.go index 51b06c1751..a2ff404cfb 100644 --- a/components/director/internal/tenantfetchersvc/provisioner.go +++ b/components/director/internal/tenantfetchersvc/provisioner.go @@ -2,90 +2,109 @@ package tenantfetchersvc import ( "context" - "database/sql" + "fmt" "github.com/kyma-incubator/compass/components/director/internal/model" - "github.com/kyma-incubator/compass/components/director/pkg/apperrors" - "github.com/kyma-incubator/compass/components/director/pkg/log" tenantEntity "github.com/kyma-incubator/compass/components/director/pkg/tenant" - "github.com/pkg/errors" ) -// TenantService missing godoc +const autogeneratedTenantProvider = "autogenerated" + +// TenantService provides functionality for retrieving, and creating tenants. //go:generate mockery --name=TenantService --output=automock --outpkg=automock --case=underscore --unroll-variadic=False type TenantService interface { GetInternalTenant(ctx context.Context, externalTenant string) (string, error) CreateManyIfNotExists(ctx context.Context, tenantInputs ...model.BusinessTenantMappingInput) error } +// TenantProvisioningRequest represents the information provided during tenant provisioning request in Compass, which includes tenant IDs, subdomain, and region of the tenant. +// The tenant which triggered the provisioning request is only one, and one of the tenant IDs in the request is its external ID, where the other tenant IDs are external IDs from its parents hierarchy. +type TenantProvisioningRequest struct { + AccountTenantID string + SubaccountTenantID string + CustomerTenantID string + Subdomain string + Region string +} + +// MainTenantID is used to determine the external tenant ID of the tenant for which the provisioning request was triggered. +func (r *TenantProvisioningRequest) MainTenantID() string { + if len(r.SubaccountTenantID) > 0 { + return r.SubaccountTenantID + } + + return r.AccountTenantID +} + type provisioner struct { - tenantSvc TenantService + tenantSvc TenantService + tenantProvider string } -// NewTenantProvisioner missing godoc -func NewTenantProvisioner(tenantSvc TenantService) *provisioner { +// NewTenantProvisioner returns a TenantProvisioner initialized with the provided TenantService, and tenant provider. +// All tenants, created by the provisioner, besides the Customer ones, will have the value of tenantProvider as a provider. +func NewTenantProvisioner(tenantSvc TenantService, tenantProvider string) *provisioner { return &provisioner{ - tenantSvc: tenantSvc, + tenantSvc: tenantSvc, + tenantProvider: tenantProvider, } } -// ProvisionTenant missing godoc -func (p *provisioner) ProvisionTenant(ctx context.Context, tenant model.BusinessTenantMappingInput) error { - externalTenantID := tenant.ExternalTenant - parentExternalID := tenant.Parent - if len(parentExternalID) > 0 { - parentInternalID, err := p.ensureParentExists(ctx, parentExternalID, externalTenantID) - if err != nil { - return errors.Wrapf(err, "while ensuring parent tenant with ID %s exists", parentExternalID) - } - tenant.Parent = parentInternalID +// ProvisionTenants creates all non-existing tenants from the provided request. with the information present in the request. +func (p *provisioner) ProvisionTenants(ctx context.Context, request TenantProvisioningRequest) error { + if len(request.SubaccountTenantID) > 0 { + return fmt.Errorf("tenant with ID %s is of type %s and supports only regional provisioning", request.SubaccountTenantID, tenantEntity.Subaccount) } - if err := p.tenantSvc.CreateManyIfNotExists(ctx, tenant); err != nil { - if !apperrors.IsNotUniqueError(err) { - return errors.Wrapf(err, tenantCreationFailureMsgFmt, externalTenantID) - } - } + return p.tenantSvc.CreateManyIfNotExists(ctx, p.tenantsFromRequest(request)...) +} - return nil +// ProvisionRegionalTenants creates all non-existing tenants from the provided request with the information present in the request, in the provided region.. +func (p *provisioner) ProvisionRegionalTenants(ctx context.Context, request TenantProvisioningRequest) error { + return p.tenantSvc.CreateManyIfNotExists(ctx, p.tenantsFromRequest(request)...) } -func (p *provisioner) ensureParentExists(ctx context.Context, parentTenantID, childTenantID string) (string, error) { - log.C(ctx).Infof("Ensuring parent tenant with external ID %s for tenant with external ID %s exists", parentTenantID, childTenantID) - id, err := p.tenantSvc.GetInternalTenant(ctx, parentTenantID) - if err != nil && !apperrors.IsNotFoundError(err) && err != sql.ErrNoRows { - return "", errors.Wrapf(err, "failed to retrieve internal ID of parent with external ID %s", parentTenantID) - } - if id != "" { - log.C(ctx).Infof("Parent tenant with external ID %s already exists", parentTenantID) - return id, nil +func (p *provisioner) tenantsFromRequest(request TenantProvisioningRequest) []model.BusinessTenantMappingInput { + tenants := make([]model.BusinessTenantMappingInput, 0, 3) + customerID := request.CustomerTenantID + accountID := request.AccountTenantID + + if len(request.CustomerTenantID) > 0 { + tenants = append(tenants, p.newCustomerTenant(request.CustomerTenantID)) } - log.C(ctx).Infof("Creating parent tenant with external ID %s", parentTenantID) - err = p.tenantSvc.CreateManyIfNotExists(ctx, p.customerTenant(parentTenantID)) - if err != nil && apperrors.IsNotUniqueError(err) { - log.C(ctx).Infof("Parent tenant with external ID %s already exists", parentTenantID) - return p.tenantSvc.GetInternalTenant(ctx, parentTenantID) - } else if err != nil { - return "", errors.Wrapf(err, "failed to create parent tenant with ID %s", parentTenantID) + accountTenant := p.newAccountTenant(request.AccountTenantID, customerID, request.Subdomain, request.Region) + if len(request.SubaccountTenantID) > 0 { // This means that the request is for Subaccount provisioning, therefore the subdomain and the region are for the subaccount and not for the GA + accountTenant.Subdomain = "" + accountTenant.Region = "" } + tenants = append(tenants, accountTenant) - internalID, err := p.tenantSvc.GetInternalTenant(ctx, parentTenantID) - if err != nil { - return "", errors.Wrapf(err, "failed to retrieve internal ID of parent with external ID %s", parentTenantID) + if len(request.SubaccountTenantID) > 0 { + tenants = append(tenants, p.newSubaccountTenant(request.SubaccountTenantID, accountID, request.Subdomain, request.Region)) } + return tenants +} + +func (p *provisioner) newCustomerTenant(tenantID string) model.BusinessTenantMappingInput { + return p.newTenant(tenantID, "", "", "", autogeneratedTenantProvider, tenantEntity.Customer) +} +func (p *provisioner) newAccountTenant(tenantID, parent, subdomain, region string) model.BusinessTenantMappingInput { + return p.newTenant(tenantID, parent, subdomain, region, p.tenantProvider, tenantEntity.Account) +} - log.C(ctx).Infof("Successfully created parent tenant with external ID %s and internal ID %s", parentTenantID, internalID) - return internalID, nil +func (p *provisioner) newSubaccountTenant(tenantID, parent, subdomain, region string) model.BusinessTenantMappingInput { + return p.newTenant(tenantID, parent, subdomain, region, p.tenantProvider, tenantEntity.Subaccount) } -func (p *provisioner) customerTenant(tenantID string) model.BusinessTenantMappingInput { +func (p *provisioner) newTenant(tenantID, parent, subdomain, region, provider string, tenantType tenantEntity.Type) model.BusinessTenantMappingInput { return model.BusinessTenantMappingInput{ Name: tenantID, ExternalTenant: tenantID, - Parent: "", - Subdomain: "", - Type: tenantEntity.TypeToStr(tenantEntity.Customer), - Provider: autogeneratedTenantProvider, + Parent: parent, + Subdomain: subdomain, + Region: region, + Type: tenantEntity.TypeToStr(tenantType), + Provider: provider, } } diff --git a/components/director/internal/tenantfetchersvc/provisioner_test.go b/components/director/internal/tenantfetchersvc/provisioner_test.go index 08640dcb34..e87c7c4e54 100644 --- a/components/director/internal/tenantfetchersvc/provisioner_test.go +++ b/components/director/internal/tenantfetchersvc/provisioner_test.go @@ -8,142 +8,183 @@ import ( "github.com/kyma-incubator/compass/components/director/internal/model" "github.com/kyma-incubator/compass/components/director/internal/tenantfetchersvc" "github.com/kyma-incubator/compass/components/director/internal/tenantfetchersvc/automock" - "github.com/kyma-incubator/compass/components/director/pkg/apperrors" - "github.com/kyma-incubator/compass/components/director/pkg/resource" tenantEntity "github.com/kyma-incubator/compass/components/director/pkg/tenant" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" ) -func TestProvisioner_CreateTenant(t *testing.T) { - //GIVEN - ctx := context.TODO() +const ( + autogeneratedProviderName = "autogenerated" + testProviderName = "test-provider" +) - customerTenant := model.BusinessTenantMappingInput{ +var ( + customerTenant = model.BusinessTenantMappingInput{ Name: parentTenantExtID, ExternalTenant: parentTenantExtID, Parent: "", Type: tenantEntity.TypeToStr(tenantEntity.Customer), Provider: autogeneratedProviderName, } - providedAccountTenant := model.BusinessTenantMappingInput{ + accountTenant = model.BusinessTenantMappingInput{ Name: tenantExtID, ExternalTenant: tenantExtID, Parent: parentTenantExtID, Type: tenantEntity.TypeToStr(tenantEntity.Account), Provider: testProviderName, Subdomain: tenantSubdomain, + Region: tenantRegion, } - accountTenant := model.BusinessTenantMappingInput{ + parentAccountTenant = model.BusinessTenantMappingInput{ Name: tenantExtID, ExternalTenant: tenantExtID, - Parent: parentTenantIntID, + Parent: parentTenantExtID, Type: tenantEntity.TypeToStr(tenantEntity.Account), Provider: testProviderName, + Subdomain: "", + Region: "", + } + subaccountTenant = model.BusinessTenantMappingInput{ + Name: subaccountTenantExtID, + ExternalTenant: subaccountTenantExtID, + Parent: tenantExtID, + Type: tenantEntity.TypeToStr(tenantEntity.Subaccount), + Provider: testProviderName, Subdomain: tenantSubdomain, + Region: tenantRegion, } - accountTenantWithoutParent := model.BusinessTenantMappingInput{ + accountTenantWithoutParent = model.BusinessTenantMappingInput{ Name: tenantExtID, ExternalTenant: tenantExtID, Type: tenantEntity.TypeToStr(tenantEntity.Account), Provider: testProviderName, Subdomain: tenantSubdomain, + Region: tenantRegion, + } + + requestWithAccountTenant = tenantfetchersvc.TenantProvisioningRequest{ + AccountTenantID: tenantExtID, + CustomerTenantID: parentTenantExtID, + Subdomain: tenantSubdomain, + Region: tenantRegion, + } + + requestWithAccountTenantWithoutParent = tenantfetchersvc.TenantProvisioningRequest{ + AccountTenantID: tenantExtID, + Subdomain: tenantSubdomain, + Region: tenantRegion, + } + + requestWithSubaccountTenant = tenantfetchersvc.TenantProvisioningRequest{ + SubaccountTenantID: subaccountTenantExtID, + AccountTenantID: tenantExtID, + CustomerTenantID: parentTenantExtID, + Subdomain: tenantSubdomain, + Region: tenantRegion, } +) + +func TestProvisioner_CreateTenant(t *testing.T) { + //GIVEN + ctx := context.TODO() testCases := []struct { Name string TenantSvcFn func() *automock.TenantService - Tenant model.BusinessTenantMappingInput + Request tenantfetchersvc.TenantProvisioningRequest ExpectedErrorOutput string }{ { - Name: "Succeeds when parent tenant already exists", - TenantSvcFn: func() *automock.TenantService { - tenantSvc := &automock.TenantService{} - tenantSvc.On("GetInternalTenant", ctx, customerTenant.ExternalTenant).Return(parentTenantIntID, nil).Once() - tenantSvc.On("CreateManyIfNotExists", ctx, []model.BusinessTenantMappingInput{accountTenant}).Return(nil).Once() - return tenantSvc - }, - Tenant: providedAccountTenant, - }, - { - Name: "Succeeds when parent tenant does not exist", + Name: "Succeeds to create account tenant", TenantSvcFn: func() *automock.TenantService { + expectedTenants := []model.BusinessTenantMappingInput{customerTenant, accountTenant} tenantSvc := &automock.TenantService{} - tenantSvc.On("GetInternalTenant", ctx, customerTenant.ExternalTenant).Return("", apperrors.NewNotFoundError(resource.Tenant, customerTenant.ExternalTenant)).Once() - tenantSvc.On("CreateManyIfNotExists", ctx, []model.BusinessTenantMappingInput{customerTenant}).Return(nil).Once() - tenantSvc.On("GetInternalTenant", ctx, customerTenant.ExternalTenant).Return(parentTenantIntID, nil).Once() - tenantSvc.On("CreateManyIfNotExists", ctx, []model.BusinessTenantMappingInput{accountTenant}).Return(nil).Once() + tenantSvc.On("CreateManyIfNotExists", ctx, expectedTenants).Return(nil).Once() return tenantSvc }, - Tenant: providedAccountTenant, + Request: requestWithAccountTenant, }, { - Name: "Succeeds when tenant does not have a parent", + Name: "Succeeds to create account tenant with no parent provided", TenantSvcFn: func() *automock.TenantService { + expectedTenants := []model.BusinessTenantMappingInput{accountTenantWithoutParent} tenantSvc := &automock.TenantService{} - tenantSvc.On("CreateManyIfNotExists", ctx, []model.BusinessTenantMappingInput{accountTenantWithoutParent}).Return(nil).Once() + tenantSvc.On("CreateManyIfNotExists", ctx, expectedTenants).Return(nil).Once() return tenantSvc }, - Tenant: accountTenantWithoutParent, + Request: requestWithAccountTenantWithoutParent, }, { - Name: "Succeeds when parent tenant already exists when it tries to create it", + Name: "Returns error when tenant creation fails", TenantSvcFn: func() *automock.TenantService { + expectedTenants := []model.BusinessTenantMappingInput{customerTenant, accountTenant} tenantSvc := &automock.TenantService{} - tenantSvc.On("GetInternalTenant", ctx, customerTenant.ExternalTenant).Return("", apperrors.NewNotFoundError(resource.Tenant, customerTenant.ExternalTenant)).Once() - tenantSvc.On("CreateManyIfNotExists", ctx, []model.BusinessTenantMappingInput{customerTenant}).Return(apperrors.NewNotUniqueError(resource.Tenant)).Once() - tenantSvc.On("GetInternalTenant", ctx, customerTenant.ExternalTenant).Return(parentTenantIntID, nil).Once() - tenantSvc.On("CreateManyIfNotExists", ctx, []model.BusinessTenantMappingInput{accountTenant}).Return(nil).Once() + tenantSvc.On("CreateManyIfNotExists", ctx, expectedTenants).Return(testError).Once() return tenantSvc }, - Tenant: providedAccountTenant, + Request: requestWithAccountTenant, + ExpectedErrorOutput: testError.Error(), }, { - Name: "Returns error when creating parent tenant fails", + Name: "Returns error when tenant is of type subaccount", TenantSvcFn: func() *automock.TenantService { - tenantSvc := &automock.TenantService{} - tenantSvc.On("GetInternalTenant", ctx, customerTenant.ExternalTenant).Return("", apperrors.NewNotFoundError(resource.Tenant, customerTenant.ExternalTenant)).Once() - tenantSvc.On("CreateManyIfNotExists", ctx, []model.BusinessTenantMappingInput{customerTenant}).Return(testError).Once() - return tenantSvc + return &automock.TenantService{} }, - Tenant: providedAccountTenant, - ExpectedErrorOutput: fmt.Sprintf("failed to create parent tenant with ID %s", parentTenantExtID), + Request: requestWithSubaccountTenant, + ExpectedErrorOutput: fmt.Sprintf("tenant with ID %s is of type subaccount and supports only regional provisioning", subaccountTenantExtID), }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + tenantSvc := testCase.TenantSvcFn() + defer tenantSvc.AssertExpectations(t) + + provisioner := tenantfetchersvc.NewTenantProvisioner(tenantSvc, testProviderName) + + //WHEN + err := provisioner.ProvisionTenants(ctx, testCase.Request) + + // THEN + if len(testCase.ExpectedErrorOutput) > 0 { + assert.Error(t, err) + assert.Contains(t, err.Error(), testCase.ExpectedErrorOutput) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestProvisioner_CreateRegionalTenant(t *testing.T) { + //GIVEN + ctx := context.TODO() + + testCases := []struct { + Name string + TenantSvcFn func() *automock.TenantService + Request tenantfetchersvc.TenantProvisioningRequest + ExpectedErrorOutput string + }{ { - Name: "Returns error when getting parent tenant from database fails", + Name: "Succeeds when parent account tenant already exists", TenantSvcFn: func() *automock.TenantService { + expectedTenants := []model.BusinessTenantMappingInput{customerTenant, parentAccountTenant, subaccountTenant} tenantSvc := &automock.TenantService{} - tenantSvc.On("GetInternalTenant", ctx, customerTenant.ExternalTenant).Return("", testError).Once() + tenantSvc.On("CreateManyIfNotExists", ctx, expectedTenants).Return(nil).Once() return tenantSvc }, - Tenant: providedAccountTenant, - ExpectedErrorOutput: fmt.Sprintf("failed to retrieve internal ID of parent with external ID %s", parentTenantExtID), + Request: requestWithSubaccountTenant, }, { Name: "Returns error when tenant creation fails", TenantSvcFn: func() *automock.TenantService { + expectedTenants := []model.BusinessTenantMappingInput{customerTenant, parentAccountTenant, subaccountTenant} tenantSvc := &automock.TenantService{} - tenantSvc.On("GetInternalTenant", ctx, customerTenant.ExternalTenant).Return("", apperrors.NewNotFoundError(resource.Tenant, customerTenant.ExternalTenant)).Once() - tenantSvc.On("CreateManyIfNotExists", ctx, []model.BusinessTenantMappingInput{customerTenant}).Return(apperrors.NewNotUniqueError(resource.Tenant)).Once() - tenantSvc.On("GetInternalTenant", ctx, customerTenant.ExternalTenant).Return(parentTenantIntID, nil).Once() - tenantSvc.On("CreateManyIfNotExists", ctx, []model.BusinessTenantMappingInput{accountTenant}).Return(testError).Once() - return tenantSvc - }, - Tenant: providedAccountTenant, - ExpectedErrorOutput: fmt.Sprintf(tenantCreationFailureMsgFmt, tenantExtID), - }, - { - Name: "Returns error when internal tenant ID of parent cannot be retrieved", - TenantSvcFn: func() *automock.TenantService { - tenantSvc := &automock.TenantService{} - tenantSvc.On("GetInternalTenant", mock.Anything, customerTenant.ExternalTenant).Return("", apperrors.NewNotFoundError(resource.Tenant, customerTenant.ExternalTenant)).Once() - tenantSvc.On("CreateManyIfNotExists", ctx, []model.BusinessTenantMappingInput{customerTenant}).Return(nil).Once() - tenantSvc.On("GetInternalTenant", mock.Anything, customerTenant.ExternalTenant).Return("", testError).Once() + tenantSvc.On("CreateManyIfNotExists", ctx, expectedTenants).Return(testError).Once() return tenantSvc }, - Tenant: providedAccountTenant, - ExpectedErrorOutput: fmt.Sprintf("failed to retrieve internal ID of parent with external ID %s", parentTenantExtID), + Request: requestWithSubaccountTenant, + ExpectedErrorOutput: testError.Error(), }, } @@ -152,10 +193,10 @@ func TestProvisioner_CreateTenant(t *testing.T) { tenantSvc := testCase.TenantSvcFn() defer tenantSvc.AssertExpectations(t) - provisioner := tenantfetchersvc.NewTenantProvisioner(tenantSvc) + provisioner := tenantfetchersvc.NewTenantProvisioner(tenantSvc, testProviderName) //WHEN - err := provisioner.ProvisionTenant(ctx, testCase.Tenant) + err := provisioner.ProvisionRegionalTenants(ctx, testCase.Request) // THEN if len(testCase.ExpectedErrorOutput) > 0 { diff --git a/components/director/pkg/tenant/entity.go b/components/director/pkg/tenant/entity.go index 049320ab15..0f4820955e 100644 --- a/components/director/pkg/tenant/entity.go +++ b/components/director/pkg/tenant/entity.go @@ -4,7 +4,7 @@ import ( "database/sql" ) -// Entity missing godoc +// Entity represents a Compass tenant. type Entity struct { ID string `db:"id"` Name string `db:"external_name"` @@ -20,57 +20,63 @@ type Entity struct { type Type string const ( - // Unknown missing godoc + // Unknown tenant type is used when the tenant type cannot be determined when the tenant's being created. Unknown Type = "unknown" - // Account missing godoc - Account Type = "account" - // Customer missing godoc + // Customer tenants can be parents of account tenants. Customer Type = "customer" + // Account tenant type may have a parent with type Customer. + Account Type = "account" + // Subaccount tenants must have a parent of type Account. + Subaccount Type = "subaccount" ) -// Status missing godoc +// Status is used to determine if a tenant is currently being used or not. type Status string const ( - // Active missing godoc + // Active status represents tenants, which are currently active and their resources can be operated. Active Status = "Active" - // Inactive missing godoc + // Inactive status represents tenants, whose resources cannot be operated. Inactive Status = "Inactive" ) -// EntityCollection missing godoc +// EntityCollection is a wrapper type for slice of entities. type EntityCollection []Entity -// Len missing godoc +// Len returns the current number of entities in the collection. func (a EntityCollection) Len() int { return len(a) } -// WithStatus missing godoc +// WithStatus sets the provided status to the entity. func (e Entity) WithStatus(status Status) Entity { e.Status = status return e } -// StrToType missing godoc +// StrToType returns the tenant Type value of the provided string or "Unknown" if there's no type matching the string. func StrToType(value string) Type { switch value { case string(Account): return Account case string(Customer): return Customer + case string(Subaccount): + return Subaccount default: return Unknown } } -// TypeToStr missing godoc +// TypeToStr returns the string value of the provided tenant Type. func TypeToStr(value Type) string { switch value { case Account: return string(Account) case Customer: return string(Customer) + case Subaccount: + return string(Subaccount) default: return string(Unknown) } diff --git a/components/schema-migrator/migrations/director/20210910112500_subaccount_tenants.down.sql b/components/schema-migrator/migrations/director/20210910112500_subaccount_tenants.down.sql new file mode 100644 index 0000000000..23bdac7172 --- /dev/null +++ b/components/schema-migrator/migrations/director/20210910112500_subaccount_tenants.down.sql @@ -0,0 +1,14 @@ +BEGIN; + +ALTER TYPE tenant_type RENAME TO tenant_type_old; + +CREATE TYPE tenant_type AS ENUM ( + 'unknown', + 'account', + 'customer' + ); +ALTER TABLE business_tenant_mappings ALTER COLUMN type TYPE tenant_type USING type::text::tenant_type; + +DROP TYPE tenant_type_old; + +COMMIT; diff --git a/components/schema-migrator/migrations/director/20210910112500_subaccount_tenants.up.sql b/components/schema-migrator/migrations/director/20210910112500_subaccount_tenants.up.sql new file mode 100644 index 0000000000..b1d2886fc9 --- /dev/null +++ b/components/schema-migrator/migrations/director/20210910112500_subaccount_tenants.up.sql @@ -0,0 +1,15 @@ +BEGIN; + + ALTER TYPE tenant_type RENAME TO tenant_type_old; + + CREATE TYPE tenant_type AS ENUM ( + 'unknown', + 'account', + 'customer', + 'subaccount' + ); + ALTER TABLE business_tenant_mappings ALTER COLUMN type TYPE tenant_type USING type::text::tenant_type; + + DROP TYPE tenant_type_old; + +COMMIT; diff --git a/scripts/generic_make_go.mk b/scripts/generic_make_go.mk index 8a76c5c7e2..c79b84b159 100644 --- a/scripts/generic_make_go.mk +++ b/scripts/generic_make_go.mk @@ -138,6 +138,10 @@ check-fmt-local: fmt-local: go fmt $$($(DIRS_TO_CHECK)) +format-local: imports-local fmt-local + +verify-local: test-local check-imports-local check-fmt-local errcheck-local lint-local + errcheck-local: errcheck -blank -asserts -ignorepkg '$$($(DIRS_TO_CHECK) | tr '\n' ',')' -ignoregenerated ./... diff --git a/tests/go.mod b/tests/go.mod index b6f81a0bd3..c356452edc 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -52,6 +52,7 @@ require ( github.com/tidwall/gjson v1.8.0 github.com/tidwall/match v1.0.3 // indirect github.com/tidwall/pretty v1.1.0 // indirect + github.com/tidwall/sjson v1.1.7 github.com/vektah/gqlparser/v2 v2.1.0 // indirect github.com/vrischmann/envconfig v1.3.0 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect diff --git a/tests/tenant-fetcher/tests/handler_test.go b/tests/tenant-fetcher/tests/handler_test.go index aaa9e58886..072e9f70e7 100644 --- a/tests/tenant-fetcher/tests/handler_test.go +++ b/tests/tenant-fetcher/tests/handler_test.go @@ -26,6 +26,7 @@ import ( "time" "github.com/tidwall/gjson" + "github.com/tidwall/sjson" "github.com/google/uuid" "github.com/kyma-incubator/compass/components/director/pkg/graphql" @@ -35,56 +36,59 @@ import ( ) const ( - tenantPathParamValue = "tenant" - defaultSubdomain = "default-subdomain" + tenantPathParamValue = "tenant" + regionPathParamValue = "eu-1" + defaultSubdomain = "default-subdomain" + defaultSubaccountSubdomain = "default-subaccount-subdomain" ) type Tenant struct { - TenantId string `json:"tenantId"` - CustomerId string `json:"customerId"` - Subdomain string `json:"subdomain"` + TenantID string + SubaccountID string + CustomerID string + Subdomain string } func TestOnboardingHandler(t *testing.T) { t.Run("Success with tenant and customerID", func(t *testing.T) { tenantWithCustomer := Tenant{ - TenantId: uuid.New().String(), - CustomerId: uuid.New().String(), + TenantID: uuid.New().String(), + CustomerID: uuid.New().String(), Subdomain: defaultSubdomain, } // WHEN addTenantExpectStatusCode(t, tenantWithCustomer, http.StatusOK) - tenant, err := fixtures.GetTenantByExternalID(dexGraphQLClient, tenantWithCustomer.TenantId) + tenant, err := fixtures.GetTenantByExternalID(dexGraphQLClient, tenantWithCustomer.TenantID) require.NoError(t, err) - parent, err := fixtures.GetTenantByExternalID(dexGraphQLClient, tenantWithCustomer.CustomerId) + parent, err := fixtures.GetTenantByExternalID(dexGraphQLClient, tenantWithCustomer.CustomerID) require.NoError(t, err) // THEN - assertTenant(t, tenant, tenantWithCustomer.TenantId, tenantWithCustomer.Subdomain) - assertTenant(t, parent, tenantWithCustomer.CustomerId, "") + assertTenant(t, tenant, tenantWithCustomer.TenantID, tenantWithCustomer.Subdomain) + assertTenant(t, parent, tenantWithCustomer.CustomerID, "") }) t.Run("Success with only tenant", func(t *testing.T) { tenant := Tenant{ - TenantId: uuid.New().String(), + TenantID: uuid.New().String(), Subdomain: defaultSubdomain, } addTenantExpectStatusCode(t, tenant, http.StatusOK) - tnt, err := fixtures.GetTenantByExternalID(dexGraphQLClient, tenant.TenantId) + tnt, err := fixtures.GetTenantByExternalID(dexGraphQLClient, tenant.TenantID) require.NoError(t, err) // THEN - assertTenant(t, tnt, tenant.TenantId, tenant.Subdomain) + assertTenant(t, tnt, tenant.TenantID, tenant.Subdomain) }) t.Run("Should not add already existing tenants", func(t *testing.T) { tenantWithCustomer := Tenant{ - TenantId: uuid.New().String(), - CustomerId: uuid.New().String(), + TenantID: uuid.New().String(), + CustomerID: uuid.New().String(), Subdomain: defaultSubdomain, } //GIVEN @@ -101,13 +105,13 @@ func TestOnboardingHandler(t *testing.T) { // THEN assert.Equal(t, len(oldTenantState)+2, len(tenants)) - assertTenantExists(t, tenants, tenantWithCustomer.TenantId) - assertTenantExists(t, tenants, tenantWithCustomer.CustomerId) + assertTenantExists(t, tenants, tenantWithCustomer.TenantID) + assertTenantExists(t, tenants, tenantWithCustomer.CustomerID) }) t.Run("Should fail when no tenantID is provided", func(t *testing.T) { providedTenant := Tenant{ - CustomerId: uuid.New().String(), + CustomerID: uuid.New().String(), } oldTenantState, err := fixtures.GetTenants(dexGraphQLClient) @@ -124,8 +128,8 @@ func TestOnboardingHandler(t *testing.T) { t.Run("Should fail when no subdomain is provided", func(t *testing.T) { providedTenant := Tenant{ - TenantId: uuid.New().String(), - CustomerId: uuid.New().String(), + TenantID: uuid.New().String(), + CustomerID: uuid.New().String(), } oldTenantState, err := fixtures.GetTenants(dexGraphQLClient) @@ -139,12 +143,34 @@ func TestOnboardingHandler(t *testing.T) { // THEN assert.Equal(t, len(oldTenantState), len(tenants)) }) + + t.Run("Should fail with subaccount tenant", func(t *testing.T) { + // GIVEN + parentTenant := Tenant{ + TenantID: uuid.New().String(), + Subdomain: defaultSubdomain, + } + childTenant := Tenant{ + SubaccountID: uuid.New().String(), + TenantID: parentTenant.TenantID, + Subdomain: defaultSubdomain, + } + + addTenantExpectStatusCode(t, parentTenant, http.StatusOK) + + parent, err := fixtures.GetTenantByExternalID(dexGraphQLClient, parentTenant.TenantID) + require.NoError(t, err) + assertTenant(t, parent, parentTenant.TenantID, parentTenant.Subdomain) + + // THEN + addTenantExpectStatusCode(t, childTenant, http.StatusInternalServerError) + }) } func TestDecommissioningHandler(t *testing.T) { t.Run("Success noop", func(t *testing.T) { providedTenant := Tenant{ - TenantId: uuid.New().String(), + TenantID: uuid.New().String(), Subdomain: defaultSubdomain, } @@ -163,27 +189,274 @@ func TestDecommissioningHandler(t *testing.T) { }) } +func TestRegionalOnboardingHandler(t *testing.T) { + t.Run("Regional account tenant creation", func(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // GIVEN + providedTenant := Tenant{ + TenantID: uuid.New().String(), + Subdomain: defaultSubdomain, + } + + // WHEN + addRegionalTenantExpectStatusCode(t, providedTenant, http.StatusOK) + + // THEN + tenant, err := fixtures.GetTenantByExternalID(dexGraphQLClient, providedTenant.TenantID) + require.NoError(t, err) + assertTenant(t, tenant, providedTenant.TenantID, providedTenant.Subdomain) + require.Equal(t, regionPathParamValue, tenant.Labels["region"]) + }) + }) + + t.Run("Regional subaccount tenant creation", func(t *testing.T) { + t.Run("Success when parent account tenant is pre-existing", func(t *testing.T) { + // GIVEN + parentTenant := Tenant{ + TenantID: uuid.New().String(), + Subdomain: defaultSubdomain, + } + childTenant := Tenant{ + SubaccountID: uuid.New().String(), + TenantID: parentTenant.TenantID, + Subdomain: defaultSubaccountSubdomain, + } + + addRegionalTenantExpectStatusCode(t, parentTenant, http.StatusOK) + + parent, err := fixtures.GetTenantByExternalID(dexGraphQLClient, parentTenant.TenantID) + require.NoError(t, err) + assertTenant(t, parent, parentTenant.TenantID, parentTenant.Subdomain) + require.Equal(t, regionPathParamValue, parent.Labels["region"]) + + // WHEN + addRegionalTenantExpectStatusCode(t, childTenant, http.StatusOK) + + // THEN + tenant, err := fixtures.GetTenantByExternalID(dexGraphQLClient, childTenant.SubaccountID) + require.NoError(t, err) + assertTenant(t, tenant, childTenant.SubaccountID, childTenant.Subdomain) + require.Equal(t, regionPathParamValue, tenant.Labels["region"]) + + parentTenantAfterInsert, err := fixtures.GetTenantByExternalID(dexGraphQLClient, parentTenant.TenantID) + require.NoError(t, err) + assertTenant(t, parentTenantAfterInsert, parentTenant.TenantID, parentTenant.Subdomain) + require.Equal(t, regionPathParamValue, parentTenantAfterInsert.Labels["region"]) + }) + + t.Run("Success when parent account tenant does not exist", func(t *testing.T) { + // GIVEN + providedTenant := Tenant{ + TenantID: uuid.New().String(), + CustomerID: uuid.New().String(), + SubaccountID: uuid.New().String(), + Subdomain: defaultSubaccountSubdomain, + } + + // THEN + addRegionalTenantExpectStatusCode(t, providedTenant, http.StatusOK) + + // THEN + childTenant, err := fixtures.GetTenantByExternalID(dexGraphQLClient, providedTenant.SubaccountID) + require.NoError(t, err) + assertTenant(t, childTenant, providedTenant.SubaccountID, providedTenant.Subdomain) + require.Equal(t, regionPathParamValue, childTenant.Labels["region"]) + + parentTenant, err := fixtures.GetTenantByExternalID(dexGraphQLClient, providedTenant.TenantID) + require.NoError(t, err) + assertTenant(t, parentTenant, providedTenant.TenantID, "") + require.Empty(t, parentTenant.Labels) + + customerTenant, err := fixtures.GetTenantByExternalID(dexGraphQLClient, providedTenant.CustomerID) + require.NoError(t, err) + assertTenant(t, customerTenant, providedTenant.CustomerID, "") + require.Empty(t, customerTenant.Labels) + }) + + t.Run("Should not fail when tenant already exists", func(t *testing.T) { + // GIVEN + parentTenantId := uuid.New().String() + parentTenant := Tenant{ + TenantID: parentTenantId, + Subdomain: defaultSubaccountSubdomain, + } + childTenant := Tenant{ + TenantID: parentTenantId, + SubaccountID: uuid.New().String(), + Subdomain: defaultSubaccountSubdomain, + } + oldTenantState, err := fixtures.GetTenants(dexGraphQLClient) + require.NoError(t, err) + + addTenantExpectStatusCode(t, parentTenant, http.StatusOK) + parent, err := fixtures.GetTenantByExternalID(dexGraphQLClient, parentTenant.TenantID) + require.NoError(t, err) + assertTenant(t, parent, parentTenant.TenantID, parentTenant.Subdomain) + + // WHEN + for i := 0; i < 10; i++ { + addRegionalTenantExpectStatusCode(t, childTenant, http.StatusOK) + } + + tenant, err := fixtures.GetTenantByExternalID(dexGraphQLClient, childTenant.SubaccountID) + require.NoError(t, err) + + tenants, err := fixtures.GetTenants(dexGraphQLClient) + require.NoError(t, err) + + // THEN + assertTenant(t, tenant, childTenant.SubaccountID, childTenant.Subdomain) + assert.Equal(t, len(oldTenantState)+2, len(tenants)) + }) + + t.Run("Should fail when parent tenantID is not provided", func(t *testing.T) { + // GIVEN + providedTenant := Tenant{ + CustomerID: uuid.New().String(), + SubaccountID: uuid.New().String(), + Subdomain: defaultSubaccountSubdomain, + } + oldTenantState, err := fixtures.GetTenants(dexGraphQLClient) + require.NoError(t, err) + + // WHEN + addRegionalTenantExpectStatusCode(t, providedTenant, http.StatusBadRequest) + + // THEN + tenants, err := fixtures.GetTenants(dexGraphQLClient) + require.NoError(t, err) + assert.Equal(t, len(oldTenantState), len(tenants)) + }) + + t.Run("Should fail when subdomain is not provided", func(t *testing.T) { + // GIVEN + providedTenant := Tenant{ + TenantID: uuid.New().String(), + SubaccountID: uuid.New().String(), + CustomerID: uuid.New().String(), + } + oldTenantState, err := fixtures.GetTenants(dexGraphQLClient) + require.NoError(t, err) + + // WHEN + addRegionalTenantExpectStatusCode(t, providedTenant, http.StatusBadRequest) + + // THEN + tenants, err := fixtures.GetTenants(dexGraphQLClient) + require.NoError(t, err) + assert.Equal(t, len(oldTenantState), len(tenants)) + }) + }) +} + +func TestRegionalDecommissioningHandler(t *testing.T) { + t.Run("Success noop", func(t *testing.T) { + providedTenant := Tenant{ + TenantID: uuid.New().String(), + Subdomain: defaultSubdomain, + } + + addRegionalTenantExpectStatusCode(t, providedTenant, http.StatusOK) + + oldTenantState, err := fixtures.GetTenants(dexGraphQLClient) + require.NoError(t, err) + + removeRegionalTenantExpectStatusCode(t, providedTenant, http.StatusOK) + + newTenantState, err := fixtures.GetTenants(dexGraphQLClient) + require.NoError(t, err) + + // THEN + assert.Equal(t, len(oldTenantState), len(newTenantState)) + }) +} + +func TestGetDependenciesHandler(t *testing.T) { + t.Run("Returns empty body", func(t *testing.T) { + // GIVEN + request, err := http.NewRequest(http.MethodGet, config.TenantFetcherFullDependenciesURL, nil) + require.NoError(t, err) + request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", fetchToken(t))) + + // WHEN + response, err := httpClient.Do(request) + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.StatusCode) + + responseBody, err := ioutil.ReadAll(response.Body) + require.NoError(t, err) + responseBodyJson := make(map[string]interface{}, 0) + + // THEN + err = json.Unmarshal(responseBody, &responseBodyJson) + require.NoError(t, err) + require.Empty(t, responseBodyJson) + }) +} + func addTenantExpectStatusCode(t *testing.T, providedTenant Tenant, expectedStatusCode int) { - makeTenantRequestExpectStatusCode(t, providedTenant, http.MethodPut, expectedStatusCode) + makeTenantRequestExpectStatusCode(t, providedTenant, http.MethodPut, config.TenantFetcherFullURL, expectedStatusCode) +} + +func addRegionalTenantExpectStatusCode(t *testing.T, providedTenant Tenant, expectedStatusCode int) { + makeTenantRequestExpectStatusCode(t, providedTenant, http.MethodPut, config.TenantFetcherFullRegionalURL, expectedStatusCode) } func removeTenantExpectStatusCode(t *testing.T, providedTenant Tenant, expectedStatusCode int) { - makeTenantRequestExpectStatusCode(t, providedTenant, http.MethodDelete, expectedStatusCode) + makeTenantRequestExpectStatusCode(t, providedTenant, http.MethodDelete, config.TenantFetcherFullURL, expectedStatusCode) } -func makeTenantRequestExpectStatusCode(t *testing.T, providedTenant Tenant, httpMethod string, expectedStatusCode int) { - byteTenant, err := json.Marshal(providedTenant) - require.NoError(t, err) +func removeRegionalTenantExpectStatusCode(t *testing.T, providedTenant Tenant, expectedStatusCode int) { + makeTenantRequestExpectStatusCode(t, providedTenant, http.MethodDelete, config.TenantFetcherFullRegionalURL, expectedStatusCode) +} - request, err := http.NewRequest(httpMethod, config.TenantFetcherFullURL, bytes.NewBuffer(byteTenant)) - require.NoError(t, err) - request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", fetchToken(t))) +func makeTenantRequestExpectStatusCode(t *testing.T, providedTenant Tenant, httpMethod, url string, expectedStatusCode int) { + request := createTenantRequest(t, providedTenant, httpMethod, url) + t.Log(fmt.Sprintf("Provisioning tenant with ID %s", actualTenantID(providedTenant))) response, err := httpClient.Do(request) require.NoError(t, err) require.Equal(t, expectedStatusCode, response.StatusCode) } +func actualTenantID(tenant Tenant) string { + if len(tenant.SubaccountID) > 0 { + return tenant.SubaccountID + } + + return tenant.TenantID +} + +func createTenantRequest(t *testing.T, tenant Tenant, httpMethod string, url string) *http.Request { + var ( + body = "{}" + err error + ) + + if len(tenant.TenantID) > 0 { + body, err = sjson.Set(body, config.TenantIDProperty, tenant.TenantID) + require.NoError(t, err) + } + if len(tenant.SubaccountID) > 0 { + body, err = sjson.Set(body, config.SubaccountTenantIDProperty, tenant.SubaccountID) + require.NoError(t, err) + } + if len(tenant.CustomerID) > 0 { + body, err = sjson.Set(body, config.CustomerIDProperty, tenant.CustomerID) + require.NoError(t, err) + } + if len(tenant.Subdomain) > 0 { + body, err = sjson.Set(body, config.SubdomainProperty, tenant.Subdomain) + require.NoError(t, err) + } + + request, err := http.NewRequest(httpMethod, url, bytes.NewBuffer([]byte(body))) + require.NoError(t, err) + request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", fetchToken(t))) + + return request +} + func fetchToken(t *testing.T) string { claims := map[string]interface{}{ "test": "tenant-fetcher", diff --git a/tests/tenant-fetcher/tests/main_test.go b/tests/tenant-fetcher/tests/main_test.go index b163f8b6f8..be25a6e04c 100644 --- a/tests/tenant-fetcher/tests/main_test.go +++ b/tests/tenant-fetcher/tests/main_test.go @@ -24,7 +24,10 @@ type testConfig struct { TenantFetcherURL string RootAPI string HandlerEndpoint string + RegionalHandlerEndpoint string + DependenciesEndpoint string TenantPathParam string + RegionPathParam string DbUser string DbPassword string DbHost string @@ -35,10 +38,18 @@ type testConfig struct { DbMaxOpenConnections string Tenant string SubscriptionCallbackScope string - TenantProvider string - ExternalServicesMockURL string + TenantProviderConfig + ExternalServicesMockURL string + TenantFetcherFullURL string `envconfig:"-"` + TenantFetcherFullRegionalURL string `envconfig:"-"` + TenantFetcherFullDependenciesURL string `envconfig:"-"` +} - TenantFetcherFullURL string `envconfig:"-"` +type TenantProviderConfig struct { + TenantIDProperty string `envconfig:"APP_TENANT_PROVIDER_TENANT_ID_PROPERTY"` + SubaccountTenantIDProperty string `envconfig:"APP_TENANT_PROVIDER_SUBACCOUNT_TENANT_ID_PROPERTY"` + CustomerIDProperty string `envconfig:"APP_TENANT_PROVIDER_CUSTOMER_ID_PROPERTY"` + SubdomainProperty string `envconfig:"APP_TENANT_PROVIDER_SUBDOMAIN_PROPERTY"` } var config testConfig @@ -62,6 +73,12 @@ func TestMain(m *testing.M) { endpoint := strings.Replace(config.HandlerEndpoint, fmt.Sprintf("{%s}", config.TenantPathParam), tenantPathParamValue, 1) config.TenantFetcherFullURL = config.TenantFetcherURL + config.RootAPI + endpoint + regionalEndpoint := strings.Replace(config.RegionalHandlerEndpoint, fmt.Sprintf("{%s}", config.TenantPathParam), tenantPathParamValue, 1) + regionalEndpoint = strings.Replace(regionalEndpoint, fmt.Sprintf("{%s}", config.RegionPathParam), regionPathParamValue, 1) + config.TenantFetcherFullRegionalURL = config.TenantFetcherURL + config.RootAPI + regionalEndpoint + + config.TenantFetcherFullDependenciesURL = config.TenantFetcherURL + config.RootAPI + config.DependenciesEndpoint + exitVal := m.Run() os.Exit(exitVal) }