diff --git a/controller/tenant.go b/controller/tenant.go index 34c75b1..6661ae0 100644 --- a/controller/tenant.go +++ b/controller/tenant.go @@ -83,12 +83,22 @@ func (c *TenantController) Setup(ctx *app.SetupTenantContext) error { return jsonapi.JSONErrorResponse(ctx, errors.NewInternalError(ctx, err)) } + nsBaseName, err := tenant.ConstructNsBaseName(c.tenantService, env.RetrieveUserName(user.OpenShiftUsername)) + if err != nil { + log.Error(ctx, map[string]interface{}{ + "err": err, + "os_username": user.OpenShiftUsername, + }, "unable to construct namespace base name") + return jsonapi.JSONErrorResponse(ctx, errors.NewInternalError(ctx, err)) + } + // create openshift config openshiftConfig := openshift.NewConfig(c.config, user.UserData, cluster.User, cluster.Token, cluster.APIURL) tenant := &tenant.Tenant{ ID: ttoken.Subject(), Email: ttoken.Email(), OSUsername: user.OpenShiftUsername, + NsBaseName: nsBaseName, } err = c.tenantService.CreateTenant(tenant) if err != nil { @@ -104,8 +114,9 @@ func (c *TenantController) Setup(ctx *app.SetupTenantContext) error { err = openshift.RawInitTenant( ctx, openshiftConfig, - InitTenant(ctx, openshiftConfig.MasterURL, c.tenantService, t, user.OpenShiftUsername), + InitTenant(ctx, openshiftConfig.MasterURL, c.tenantService, t), user.OpenShiftUsername, + nsBaseName, user.OpenShiftUserToken) if err != nil { @@ -156,7 +167,9 @@ func (c *TenantController) Update(ctx *app.UpdateTenantContext) error { // update tenant config tenant.OSUsername = user.OpenShiftUsername - + if tenant.NsBaseName == "" { + tenant.NsBaseName = env.RetrieveUserName(user.OpenShiftUsername) + } if err = c.tenantService.SaveTenant(tenant); err != nil { log.Error(ctx, map[string]interface{}{ "err": err, @@ -170,8 +183,9 @@ func (c *TenantController) Update(ctx *app.UpdateTenantContext) error { err = openshift.RawUpdateTenant( ctx, openshiftConfig, - InitTenant(ctx, openshiftConfig.MasterURL, c.tenantService, t, user.OpenShiftUsername), - user.OpenShiftUsername) + InitTenant(ctx, openshiftConfig.MasterURL, c.tenantService, t), + user.OpenShiftUsername, + tenant.NsBaseName) if err != nil { sentry.LogError(ctx, map[string]interface{}{ @@ -198,6 +212,11 @@ func (c *TenantController) Clean(ctx *app.CleanTenantContext) error { return jsonapi.JSONErrorResponse(ctx, err) } + tenant, err := c.tenantService.GetTenant(ttoken.Subject()) + if err != nil { + return jsonapi.JSONErrorResponse(ctx, errors.NewNotFoundError("tenants", ttoken.Subject().String())) + } + // restrict deprovision from cluster to internal users only removeFromCluster := false if user.UserData.FeatureLevel != nil && *user.UserData.FeatureLevel == "internal" { @@ -216,7 +235,12 @@ func (c *TenantController) Clean(ctx *app.CleanTenantContext) error { // create openshift config openshiftConfig := openshift.NewConfig(c.config, user.UserData, cluster.User, cluster.Token, cluster.APIURL) - err = openshift.CleanTenant(ctx, openshiftConfig, user.OpenShiftUsername, removeFromCluster) + nsBaseName := tenant.NsBaseName + if nsBaseName == "" { + nsBaseName = env.RetrieveUserName(user.OpenShiftUsername) + } + + err = openshift.CleanTenant(ctx, openshiftConfig, user.OpenShiftUsername, nsBaseName, removeFromCluster) if err != nil { return jsonapi.JSONErrorResponse(ctx, err) } @@ -253,10 +277,9 @@ func (c *TenantController) Show(ctx *app.ShowTenantContext) error { } // InitTenant is a Callback that assumes a new tenant is being created -func InitTenant(ctx context.Context, masterURL string, service tenant.Service, currentTenant *tenant.Tenant, openshiftUsername string) openshift.Callback { +func InitTenant(ctx context.Context, masterURL string, service tenant.Service, currentTenant *tenant.Tenant) openshift.Callback { var maxResourceQuotaStatusCheck int32 = 50 // technically a global retry count across all ResourceQuota on all Tenant Namespaces var currentResourceQuotaStatusCheck int32 // default is 0 - username := env.RetrieveUserName(openshiftUsername) return func(statusCode int, method string, request, response map[interface{}]interface{}) (string, map[interface{}]interface{}) { log.Info(ctx, map[string]interface{}{ "status": statusCode, @@ -290,7 +313,7 @@ func InitTenant(ctx context.Context, masterURL string, service tenant.Service, c Name: name, State: "created", Version: env.GetLabelVersion(request), - Type: tenant.GetNamespaceType(name, username), + Type: tenant.GetNamespaceType(name, currentTenant.NsBaseName), MasterURL: masterURL, }) @@ -305,7 +328,7 @@ func InitTenant(ctx context.Context, masterURL string, service tenant.Service, c Name: name, State: "created", Version: env.GetLabelVersion(request), - Type: tenant.GetNamespaceType(name, username), + Type: tenant.GetNamespaceType(name, currentTenant.NsBaseName), MasterURL: masterURL, }) } else if env.GetKind(request) == env.ValKindResourceQuota { diff --git a/environment/template.go b/environment/template.go index be67d98..1da1d09 100644 --- a/environment/template.go +++ b/environment/template.go @@ -152,13 +152,11 @@ func (t *Template) ReplaceVars(variables map[string]string) (string, error) { })), nil } -func CollectVars(user, masterUser string, config *configuration.Data) map[string]string { - userName := RetrieveUserName(user) - +func CollectVars(osUsername, nsBaseName, masterUser string, config *configuration.Data) map[string]string { vars := map[string]string{ - varUserName: userName, - varProjectUser: user, - varProjectRequestingUser: user, + varUserName: nsBaseName, + varProjectUser: osUsername, + varProjectRequestingUser: osUsername, varProjectAdminUser: masterUser, } diff --git a/environment/template_test.go b/environment/template_test.go index 5b118d8..b7c3e49 100644 --- a/environment/template_test.go +++ b/environment/template_test.go @@ -158,7 +158,7 @@ func TestSort(t *testing.T) { defer reset() template := environment.Template{Content: sortTemplate} - objects, err := template.Process(environment.CollectVars("developer", "master", data)) + objects, err := template.Process(environment.CollectVars("developer", "developer", "master", data)) // when sort.Sort(environment.ByKind(objects)) @@ -180,7 +180,7 @@ func TestParseNamespace(t *testing.T) { template := environment.Template{Content: parseNamespaceTemplate} // when - objects, err := template.Process(environment.CollectVars("developer", "master", data)) + objects, err := template.Process(environment.CollectVars("developer", "developer", "master", data)) // then require.NoError(t, err) diff --git a/migration/migration.go b/migration/migration.go index 2d302d2..007f60a 100644 --- a/migration/migration.go +++ b/migration/migration.go @@ -106,6 +106,7 @@ func getMigrations() migrations { m = append(m, steps{executeSQLFile("003-profiles.sql")}) m = append(m, steps{executeSQLFile("004-index-tenants-search.sql")}) m = append(m, steps{executeSQLFile("005-add-username-column-to-tenant.sql")}) + m = append(m, steps{executeSQLFile("006-add-ns-base-name-column-to-tenant.sql")}) // Version N // diff --git a/migration/sql-files/006-add-ns-base-name-column-to-tenant.sql b/migration/sql-files/006-add-ns-base-name-column-to-tenant.sql new file mode 100644 index 0000000..2f79df4 --- /dev/null +++ b/migration/sql-files/006-add-ns-base-name-column-to-tenant.sql @@ -0,0 +1,3 @@ +ALTER TABLE tenants ADD COLUMN ns_base_name TEXT; +CREATE INDEX idx_ns_base_name ON tenants USING btree (ns_base_name); +UPDATE tenants SET ns_base_name = (SELECT name FROM namespaces n WHERE tenants.id = n.tenant_id AND n.type = 'user'); diff --git a/openshift/apply_test.go b/openshift/apply_test.go index e8167d9..4f2751d 100644 --- a/openshift/apply_test.go +++ b/openshift/apply_test.go @@ -140,7 +140,7 @@ func TestDeleteAndGet(t *testing.T) { func prepareObjectsAndOpts(t *testing.T, content string, config *configuration.Data) (environment.Objects, openshift.ApplyOptions) { template := environment.Template{Content: content} - objects, err := template.Process(environment.CollectVars("aslak", "master", config)) + objects, err := template.Process(environment.CollectVars("aslak", "aslak", "master", config)) require.NoError(t, err) opts := openshift.ApplyOptions{ Config: openshift.Config{ diff --git a/openshift/clean_tenant.go b/openshift/clean_tenant.go index 1ad27d2..ea441b3 100644 --- a/openshift/clean_tenant.go +++ b/openshift/clean_tenant.go @@ -10,8 +10,8 @@ import ( ) // CleanTenant clean or remove -func CleanTenant(ctx context.Context, config Config, username string, remove bool) error { - templs, err := LoadProcessedTemplates(ctx, config, username) +func CleanTenant(ctx context.Context, config Config, osUsername, nsBaseName string, remove bool) error { + templs, err := LoadProcessedTemplates(ctx, config, osUsername, nsBaseName) if err != nil { return err } diff --git a/openshift/init_tenant.go b/openshift/init_tenant.go index 0dda02f..9f18725 100644 --- a/openshift/init_tenant.go +++ b/openshift/init_tenant.go @@ -12,8 +12,8 @@ import ( "gopkg.in/yaml.v2" ) -func RawInitTenant(ctx context.Context, config Config, callback Callback, openshiftUsername, usertoken string) error { - templs, err := LoadProcessedTemplates(ctx, config, openshiftUsername) +func RawInitTenant(ctx context.Context, config Config, callback Callback, openshiftUsername, nsBaseName, usertoken string) error { + templs, err := LoadProcessedTemplates(ctx, config, openshiftUsername, nsBaseName) if err != nil { return err } @@ -26,9 +26,8 @@ func RawInitTenant(ctx context.Context, config Config, callback Callback, opensh userOpts := ApplyOptions{Config: config.WithToken(usertoken), Callback: callback} var wg sync.WaitGroup wg.Add(len(mapped)) - username := env.RetrieveUserName(openshiftUsername) for key, val := range mapped { - namespaceType := tenant.GetNamespaceType(key, username) + namespaceType := tenant.GetNamespaceType(key, nsBaseName) if namespaceType == tenant.TypeUser { go func(namespace string, objects env.Objects, opts, userOpts ApplyOptions) { defer wg.Done() @@ -88,8 +87,8 @@ func RawInitTenant(ctx context.Context, config Config, callback Callback, opensh return nil } -func RawUpdateTenant(ctx context.Context, config Config, callback Callback, username string) error { - templs, err := LoadProcessedTemplates(ctx, config, username) +func RawUpdateTenant(ctx context.Context, config Config, callback Callback, osUsername, nsBaseName string) error { + templs, err := LoadProcessedTemplates(ctx, config, osUsername, nsBaseName) if err != nil { return err } diff --git a/openshift/init_tenant_test.go b/openshift/init_tenant_test.go index 755b688..32fa6cc 100644 --- a/openshift/init_tenant_test.go +++ b/openshift/init_tenant_test.go @@ -37,7 +37,7 @@ func TestNumberOfCallsToCluster(t *testing.T) { objectsInTemplates := tmplObjects(t, data) // when - err := openshift.RawInitTenant(context.Background(), config, emptyCallback, "developer", "12345") + err := openshift.RawInitTenant(context.Background(), config, emptyCallback, "developer", "developer", "12345") // then require.NoError(t, err) diff --git a/openshift/process_template.go b/openshift/process_template.go index 407507c..dd7b2a2 100644 --- a/openshift/process_template.go +++ b/openshift/process_template.go @@ -43,10 +43,10 @@ func IsNotOfKind(kinds ...string) FilterFunc { } } -func LoadProcessedTemplates(ctx context.Context, config Config, username string) (environment.Objects, error) { +func LoadProcessedTemplates(ctx context.Context, config Config, osUsername, nsBaseName string) (environment.Objects, error) { envService := environment.NewService(config.TemplatesRepo, config.TemplatesRepoBlob, config.TemplatesRepoDir) - vars := environment.CollectVars(username, config.MasterUser, config.OriginalConfig) + vars := environment.CollectVars(osUsername, nsBaseName, config.MasterUser, config.OriginalConfig) var objs environment.Objects for _, envType := range environment.DefaultEnvTypes { diff --git a/openshift/process_template_test.go b/openshift/process_template_test.go index 97d9cfe..4ae3bd0 100644 --- a/openshift/process_template_test.go +++ b/openshift/process_template_test.go @@ -104,7 +104,7 @@ func TestPresenceOfTemplateObjects(t *testing.T) { func tmplObjects(t *testing.T, data *configuration.Data) environment.Objects { config := openshift.Config{OriginalConfig: data, MasterUser: "master"} - templs, err := openshift.LoadProcessedTemplates(context.Background(), config, "developer") + templs, err := openshift.LoadProcessedTemplates(context.Background(), config, "developer", "developer") assert.NoError(t, err) return templs } diff --git a/tenant/repository.go b/tenant/repository.go index b2b4672..70e81ea 100644 --- a/tenant/repository.go +++ b/tenant/repository.go @@ -3,6 +3,7 @@ package tenant import ( "fmt" + "github.com/fabric8-services/fabric8-tenant/environment" "github.com/fabric8-services/fabric8-wit/errors" "github.com/jinzhu/gorm" errs "github.com/pkg/errors" @@ -20,6 +21,8 @@ type Service interface { SaveTenant(tenant *Tenant) error SaveNamespace(namespace *Namespace) error DeleteAll(tenantID uuid.UUID) error + NamespaceExists(nsName string) (bool, error) + ExistsWithNsBaseName(nsBaseName string) (bool, error) } func NewDBService(db *gorm.DB) Service { @@ -39,6 +42,18 @@ func (s DBService) Exists(tenantID uuid.UUID) bool { return true } +func (s DBService) ExistsWithNsBaseName(nsBaseName string) (bool, error) { + var t Tenant + err := s.db.Table(t.TableName()).Where("ns_base_name = ?", nsBaseName).Find(&t).Error + if err != nil { + if gorm.ErrRecordNotFound == err { + return false, nil + } + return false, err + } + return true, nil +} + func (s DBService) GetTenant(tenantID uuid.UUID) (*Tenant, error) { var t Tenant err := s.db.Table(t.TableName()).Where("id = ?", tenantID).Find(&t).Error @@ -51,6 +66,18 @@ func (s DBService) GetTenant(tenantID uuid.UUID) (*Tenant, error) { return &t, nil } +func (s DBService) NamespaceExists(nsName string) (bool, error) { + var ns Namespace + err := s.db.Table(Namespace{}.TableName()).Where("name = ?", nsName).Find(&ns).Error + if err != nil { + if gorm.ErrRecordNotFound == err { + return false, nil + } + return false, err + } + return true, nil +} + func (s DBService) LookupTenantByClusterAndNamespace(masterURL, namespace string) (*Tenant, error) { // select t.id from tenant t, namespaces n where t.id = n.tenant_id and n.master_url = ? and n.name = ? query := fmt.Sprintf("select t.* from %[1]s t, %[2]s n where t.id = n.tenant_id and n.master_url = ? and n.name = ?", Tenant{}.TableName(), Namespace{}.TableName()) @@ -145,3 +172,37 @@ func (s NilService) LookupTenantByClusterAndNamespace(masterURL, namespace strin func (s NilService) DeleteAll(tenantID uuid.UUID) error { return nil } + +func ConstructNsBaseName(repo Service, username string) (string, error) { + return constructNsBaseName(repo, username, 1) +} + +func constructNsBaseName(repo Service, username string, number int) (string, error) { + nsBaseName := username + if number > 1 { + nsBaseName += fmt.Sprintf("%d", number) + } + exists, err := repo.ExistsWithNsBaseName(nsBaseName) + if err != nil { + return "", errs.Wrapf(err, "getting already existing tenants with the NsBaseName %s failed: ", nsBaseName) + } + if exists { + number++ + return constructNsBaseName(repo, username, number) + } + for _, nsType := range environment.DefaultEnvTypes { + nsName := nsBaseName + if nsType != "user" { + nsName += "-" + nsType + } + exists, err := repo.NamespaceExists(nsName) + if err != nil { + return "", errs.Wrapf(err, "getting already existing namespaces with the name %s failed: ", nsName) + } + if exists { + number++ + return constructNsBaseName(repo, username, number) + } + } + return nsBaseName, nil +} diff --git a/tenant/repository_test.go b/tenant/repository_test.go index e8f5fd0..e3b7834 100644 --- a/tenant/repository_test.go +++ b/tenant/repository_test.go @@ -3,12 +3,15 @@ package tenant_test import ( "testing" + "fmt" "github.com/fabric8-services/fabric8-tenant/tenant" + "github.com/fabric8-services/fabric8-tenant/test" "github.com/fabric8-services/fabric8-tenant/test/gormsupport" "github.com/fabric8-services/fabric8-tenant/test/resource" tf "github.com/fabric8-services/fabric8-tenant/test/testfixture" "github.com/fabric8-services/fabric8-wit/errors" - uuid "github.com/satori/go.uuid" + "github.com/jinzhu/gorm" + "github.com/satori/go.uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -172,3 +175,107 @@ func (s *TenantServiceTestSuite) TestDelete() { require.Len(t, ns2, 5) }) } + +func (s *TenantServiceTestSuite) TestNsBaseNameConstruction() { + + s.T().Run("is first tenant", func(t *testing.T) { + // given + svc := tenant.NewDBService(s.DB) + // when + nsBaseName, err := tenant.ConstructNsBaseName(svc, "johny") + // then + assert.NoError(t, err) + assert.Equal(t, "johny", nsBaseName) + }) + + s.T().Run("is second tenant with the same name", func(t *testing.T) { + // given + tf.NewTestFixture(t, s.DB, tf.Namespaces(1, func(fxt *tf.TestFixture, idx int) error { + fxt.Namespaces[idx].Name = "johny-che" + return nil + })) + svc := tenant.NewDBService(s.DB) + // when + nsBaseName, err := tenant.ConstructNsBaseName(svc, "johny") + // then + assert.NoError(t, err) + assert.Equal(t, "johny2", nsBaseName) + }) + + s.T().Run("is tenth tenant with the same name", func(t *testing.T) { + // given + tf.NewTestFixture(t, s.DB, tf.Tenants(8, func(fxt *tf.TestFixture, idx int) error { + nsBaseName := fmt.Sprintf("johny%d", idx+2) + fxt.Tenants[idx].NsBaseName = nsBaseName + return nil + }), tf.Namespaces(1, func(fxt *tf.TestFixture, idx int) error { + fxt.Namespaces[idx] = &tenant.Namespace{Name: "johny"} + return nil + })) + svc := tenant.NewDBService(s.DB) + // when + nsBaseName, err := tenant.ConstructNsBaseName(svc, "johny") + // then + assert.NoError(t, err) + assert.Equal(t, "johny10", nsBaseName) + }) + + s.T().Run("repo returns a failure while getting tenants", func(t *testing.T) { + // given + svc := serviceWithFailures{ + Service: tenant.NewDBService(s.DB), + errsToReturn: &[]error{gorm.ErrInvalidSQL}, + } + // when + _, err := tenant.ConstructNsBaseName(svc, "failingJohny") + // then + test.AssertError(t, err, + test.HasMessageContaining("getting already existing tenants with the NsBaseName failingJohny failed"), + test.IsOfType(gorm.ErrInvalidSQL)) + }) + + s.T().Run("repo returns a failure while getting namespaces", func(t *testing.T) { + // given + tf.NewTestFixture(t, s.DB, tf.Tenants(1, func(fxt *tf.TestFixture, idx int) error { + fxt.Tenants[idx].NsBaseName = "failingJohny" + return nil + })) + svc := &serviceWithFailures{ + Service: tenant.NewDBService(s.DB), + errsToReturn: &[]error{nil, nil, gorm.ErrInvalidSQL}, + } + // when + _, err := tenant.ConstructNsBaseName(svc, "failingJohny") + // then + test.AssertError(t, err, + test.HasMessageContaining("getting already existing namespaces with the name failingJohny2-che failed"), + test.IsOfType(gorm.ErrInvalidSQL)) + }) +} + +type serviceWithFailures struct { + tenant.Service + errsToReturn *[]error +} + +func (s serviceWithFailures) ExistsWithNsBaseName(nsUsername string) (bool, error) { + if len(*s.errsToReturn) > 0 { + errToReturn := (*s.errsToReturn)[0] + *s.errsToReturn = (*s.errsToReturn)[1:] + if errToReturn != nil { + return false, errToReturn + } + } + return s.Service.ExistsWithNsBaseName(nsUsername) +} + +func (s serviceWithFailures) NamespaceExists(nsName string) (bool, error) { + if len(*s.errsToReturn) > 0 { + errToReturn := (*s.errsToReturn)[0] + *s.errsToReturn = (*s.errsToReturn)[1:] + if errToReturn != nil { + return false, errToReturn + } + } + return s.Service.NamespaceExists(nsName) +} diff --git a/tenant/tenant.go b/tenant/tenant.go index 0952cd2..0903eee 100644 --- a/tenant/tenant.go +++ b/tenant/tenant.go @@ -6,7 +6,7 @@ import ( "strings" "time" - uuid "github.com/satori/go.uuid" + "github.com/satori/go.uuid" ) // NamespaceType describes which type of namespace this is @@ -55,6 +55,7 @@ type Tenant struct { Email string Profile string OSUsername string + NsBaseName string } // TableName overrides the table name settings in Gorm to force a specific table name @@ -84,8 +85,8 @@ func (m Namespace) TableName() string { } // GetNamespaceType attempts to extract the namespace type based on namespace name -func GetNamespaceType(name, username string) NamespaceType { - if name == username { +func GetNamespaceType(name, nsBaseName string) NamespaceType { + if name == nsBaseName { return TypeUser } if strings.HasSuffix(name, "-jenkins") {