diff --git a/controlplane/activation_payload_test.go b/controlplane/activation_payload_test.go index efb283a..be72889 100644 --- a/controlplane/activation_payload_test.go +++ b/controlplane/activation_payload_test.go @@ -30,7 +30,7 @@ func TestBuildTenantActivationPayloadBuildsDuckLakeRuntimeFromWarehouseSecrets(t t.Fatalf("create s3 secret: %v", err) } - team := &configstore.TeamConfig{ + org := &configstore.OrgConfig{ Name: "analytics", Users: map[string]string{ "alice": "ignored", @@ -62,13 +62,13 @@ func TestBuildTenantActivationPayloadBuildsDuckLakeRuntimeFromWarehouseSecrets(t }, } - payload, err := BuildTenantActivationPayload(context.Background(), pool.clientset, pool.namespace, team) + payload, err := BuildTenantActivationPayload(context.Background(), pool.clientset, pool.namespace, org) if err != nil { t.Fatalf("BuildTenantActivationPayload: %v", err) } - if payload.TeamName != "analytics" { - t.Fatalf("expected team analytics, got %q", payload.TeamName) + if payload.OrgID != "analytics" { + t.Fatalf("expected org analytics, got %q", payload.OrgID) } if len(payload.Usernames) != 2 { t.Fatalf("expected two users, got %v", payload.Usernames) diff --git a/controlplane/admin/api.go b/controlplane/admin/api.go index 0df1dd8..2648242 100644 --- a/controlplane/admin/api.go +++ b/controlplane/admin/api.go @@ -14,12 +14,12 @@ import ( "gorm.io/gorm/clause" ) -var errWarehousePayloadNotAllowed = errors.New("warehouse payload must be updated via /teams/:name/warehouse") +var errWarehousePayloadNotAllowed = errors.New("warehouse payload must be updated via /orgs/:name/warehouse") // WorkerStatus represents a worker's current status for the API. type WorkerStatus struct { ID int `json:"id"` - Team string `json:"team"` + Org string `json:"org"` ActiveSessions int `json:"active_sessions"` Status string `json:"status"` } @@ -28,19 +28,19 @@ type WorkerStatus struct { type SessionStatus struct { PID int32 `json:"pid"` WorkerID int `json:"worker_id"` - Team string `json:"team"` + Org string `json:"org"` } // ClusterStatus aggregates cluster state for the dashboard. type ClusterStatus struct { - TotalTeams int `json:"total_teams"` - TotalWorkers int `json:"total_workers"` - TotalSessions int `json:"total_sessions"` - Teams []TeamStatus `json:"teams"` + TotalOrgs int `json:"total_orgs"` + TotalWorkers int `json:"total_workers"` + TotalSessions int `json:"total_sessions"` + Orgs []OrgStatus `json:"orgs"` } -// TeamStatus is a per-team summary. -type TeamStatus struct { +// OrgStatus is a per-org summary. +type OrgStatus struct { Name string `json:"name"` Workers int `json:"workers"` ActiveSessions int `json:"active_sessions"` @@ -48,33 +48,33 @@ type TeamStatus struct { MemoryBudget string `json:"memory_budget"` } -// TeamStackInfo provides info about a team's live state. -// Implemented by the controlplane.TeamRouter via adapter. -type TeamStackInfo interface { - // AllTeamStats returns per-team worker and session counts. - AllTeamStats() []TeamStatus - // AllWorkerStatuses returns all workers across teams. +// OrgStackInfo provides info about an org's live state. +// Implemented by the controlplane.OrgRouter via adapter. +type OrgStackInfo interface { + // AllOrgStats returns per-org worker and session counts. + AllOrgStats() []OrgStatus + // AllWorkerStatuses returns all workers across orgs. AllWorkerStatuses() []WorkerStatus - // AllSessionStatuses returns all active sessions across teams. + // AllSessionStatuses returns all active sessions across orgs. AllSessionStatuses() []SessionStatus } // RegisterAPI registers all admin REST endpoints on the given router group. -func RegisterAPI(r *gin.RouterGroup, store *configstore.ConfigStore, info TeamStackInfo) { +func RegisterAPI(r *gin.RouterGroup, store *configstore.ConfigStore, info OrgStackInfo) { registerAPIWithStore(r, newGormAPIStore(store), info) } -func registerAPIWithStore(r *gin.RouterGroup, store apiStore, info TeamStackInfo) { +func registerAPIWithStore(r *gin.RouterGroup, store apiStore, info OrgStackInfo) { h := &apiHandler{store: store, info: info} - // Teams CRUD - r.GET("/teams", h.listTeams) - r.POST("/teams", h.createTeam) - r.GET("/teams/:name", h.getTeam) - r.PUT("/teams/:name", h.updateTeam) - r.DELETE("/teams/:name", h.deleteTeam) - r.GET("/teams/:name/warehouse", h.getManagedWarehouse) - r.PUT("/teams/:name/warehouse", h.putManagedWarehouse) + // Orgs CRUD + r.GET("/orgs", h.listOrgs) + r.POST("/orgs", h.createOrg) + r.GET("/orgs/:name", h.getOrg) + r.PUT("/orgs/:name", h.updateOrg) + r.DELETE("/orgs/:name", h.deleteOrg) + r.GET("/orgs/:name/warehouse", h.getManagedWarehouse) + r.PUT("/orgs/:name/warehouse", h.putManagedWarehouse) // Users CRUD r.GET("/users", h.listUsers) @@ -104,20 +104,20 @@ func registerAPIWithStore(r *gin.RouterGroup, store apiStore, info TeamStackInfo } type apiStore interface { - ListTeams() ([]configstore.Team, error) - CreateTeam(team *configstore.Team) error - GetTeam(name string) (*configstore.Team, error) - UpdateTeam(name string, updates configstore.Team) (*configstore.Team, bool, error) - DeleteTeam(name string) (bool, error) - - ListUsers() ([]configstore.TeamUser, error) - CreateUser(user *configstore.TeamUser) error - GetUser(username string) (*configstore.TeamUser, error) - UpdateUser(username, passwordHash, teamName string) (*configstore.TeamUser, bool, error) + ListOrgs() ([]configstore.Org, error) + CreateOrg(org *configstore.Org) error + GetOrg(name string) (*configstore.Org, error) + UpdateOrg(name string, updates configstore.Org) (*configstore.Org, bool, error) + DeleteOrg(name string) (bool, error) + + ListUsers() ([]configstore.OrgUser, error) + CreateUser(user *configstore.OrgUser) error + GetUser(username string) (*configstore.OrgUser, error) + UpdateUser(username, passwordHash, orgID string) (*configstore.OrgUser, bool, error) DeleteUser(username string) (bool, error) - GetManagedWarehouse(teamName string) (*configstore.ManagedWarehouse, error) - UpsertManagedWarehouse(teamName string, warehouse *configstore.ManagedWarehouse) (*configstore.ManagedWarehouse, bool, error) + GetManagedWarehouse(orgID string) (*configstore.ManagedWarehouse, error) + UpsertManagedWarehouse(orgID string, warehouse *configstore.ManagedWarehouse) (*configstore.ManagedWarehouse, bool, error) GetGlobalConfig() (configstore.GlobalConfig, error) SaveGlobalConfig(cfg *configstore.GlobalConfig) error @@ -141,29 +141,29 @@ func (s *gormAPIStore) db() *gorm.DB { return s.store.DB() } -func (s *gormAPIStore) ListTeams() ([]configstore.Team, error) { - var teams []configstore.Team - if err := s.db().Preload("Users").Preload("Warehouse").Find(&teams).Error; err != nil { +func (s *gormAPIStore) ListOrgs() ([]configstore.Org, error) { + var orgs []configstore.Org + if err := s.db().Preload("Users").Preload("Warehouse").Find(&orgs).Error; err != nil { return nil, err } - return teams, nil + return orgs, nil } -func (s *gormAPIStore) CreateTeam(team *configstore.Team) error { - team.Warehouse = nil - return s.db().Omit("Warehouse").Create(team).Error +func (s *gormAPIStore) CreateOrg(org *configstore.Org) error { + org.Warehouse = nil + return s.db().Omit("Warehouse").Create(org).Error } -func (s *gormAPIStore) GetTeam(name string) (*configstore.Team, error) { - var team configstore.Team - if err := s.db().Preload("Users").Preload("Warehouse").First(&team, "name = ?", name).Error; err != nil { +func (s *gormAPIStore) GetOrg(name string) (*configstore.Org, error) { + var org configstore.Org + if err := s.db().Preload("Users").Preload("Warehouse").First(&org, "name = ?", name).Error; err != nil { return nil, err } - return &team, nil + return &org, nil } -func (s *gormAPIStore) UpdateTeam(name string, updates configstore.Team) (*configstore.Team, bool, error) { - result := s.db().Model(&configstore.Team{}).Where("name = ?", name).Updates(map[string]interface{}{ +func (s *gormAPIStore) UpdateOrg(name string, updates configstore.Org) (*configstore.Org, bool, error) { + result := s.db().Model(&configstore.Org{}).Where("name = ?", name).Updates(map[string]interface{}{ "max_workers": updates.MaxWorkers, "memory_budget": updates.MemoryBudget, "idle_timeout_s": updates.IdleTimeoutS, @@ -174,20 +174,20 @@ func (s *gormAPIStore) UpdateTeam(name string, updates configstore.Team) (*confi if result.RowsAffected == 0 { return nil, false, nil } - team, err := s.GetTeam(name) + org, err := s.GetOrg(name) if err != nil { return nil, true, err } - return team, true, nil + return org, true, nil } -func (s *gormAPIStore) DeleteTeam(name string) (bool, error) { +func (s *gormAPIStore) DeleteOrg(name string) (bool, error) { returnRows := int64(0) err := s.db().Transaction(func(tx *gorm.DB) error { - if err := tx.Where("team_name = ?", name).Delete(&configstore.TeamUser{}).Error; err != nil { + if err := tx.Where("org_id = ?", name).Delete(&configstore.OrgUser{}).Error; err != nil { return err } - result := tx.Where("name = ?", name).Delete(&configstore.Team{}) + result := tx.Where("name = ?", name).Delete(&configstore.Org{}) if result.Error != nil { return result.Error } @@ -200,35 +200,35 @@ func (s *gormAPIStore) DeleteTeam(name string) (bool, error) { return returnRows > 0, nil } -func (s *gormAPIStore) ListUsers() ([]configstore.TeamUser, error) { - var users []configstore.TeamUser +func (s *gormAPIStore) ListUsers() ([]configstore.OrgUser, error) { + var users []configstore.OrgUser if err := s.db().Find(&users).Error; err != nil { return nil, err } return users, nil } -func (s *gormAPIStore) CreateUser(user *configstore.TeamUser) error { +func (s *gormAPIStore) CreateUser(user *configstore.OrgUser) error { return s.db().Create(user).Error } -func (s *gormAPIStore) GetUser(username string) (*configstore.TeamUser, error) { - var user configstore.TeamUser +func (s *gormAPIStore) GetUser(username string) (*configstore.OrgUser, error) { + var user configstore.OrgUser if err := s.db().First(&user, "username = ?", username).Error; err != nil { return nil, err } return &user, nil } -func (s *gormAPIStore) UpdateUser(username, passwordHash, teamName string) (*configstore.TeamUser, bool, error) { +func (s *gormAPIStore) UpdateUser(username, passwordHash, orgID string) (*configstore.OrgUser, bool, error) { updates := map[string]interface{}{} if passwordHash != "" { updates["password"] = passwordHash } - if teamName != "" { - updates["team_name"] = teamName + if orgID != "" { + updates["org_id"] = orgID } - result := s.db().Model(&configstore.TeamUser{}).Where("username = ?", username).Updates(updates) + result := s.db().Model(&configstore.OrgUser{}).Where("username = ?", username).Updates(updates) if result.Error != nil { return nil, false, result.Error } @@ -243,39 +243,39 @@ func (s *gormAPIStore) UpdateUser(username, passwordHash, teamName string) (*con } func (s *gormAPIStore) DeleteUser(username string) (bool, error) { - result := s.db().Where("username = ?", username).Delete(&configstore.TeamUser{}) + result := s.db().Where("username = ?", username).Delete(&configstore.OrgUser{}) if result.Error != nil { return false, result.Error } return result.RowsAffected > 0, nil } -func (s *gormAPIStore) GetManagedWarehouse(teamName string) (*configstore.ManagedWarehouse, error) { +func (s *gormAPIStore) GetManagedWarehouse(orgID string) (*configstore.ManagedWarehouse, error) { var warehouse configstore.ManagedWarehouse - if err := s.db().First(&warehouse, "team_name = ?", teamName).Error; err != nil { + if err := s.db().First(&warehouse, "org_id = ?", orgID).Error; err != nil { return nil, err } return &warehouse, nil } -func (s *gormAPIStore) UpsertManagedWarehouse(teamName string, warehouse *configstore.ManagedWarehouse) (*configstore.ManagedWarehouse, bool, error) { +func (s *gormAPIStore) UpsertManagedWarehouse(orgID string, warehouse *configstore.ManagedWarehouse) (*configstore.ManagedWarehouse, bool, error) { var count int64 - if err := s.db().Model(&configstore.Team{}).Where("name = ?", teamName).Count(&count).Error; err != nil { + if err := s.db().Model(&configstore.Org{}).Where("name = ?", orgID).Count(&count).Error; err != nil { return nil, false, err } if count == 0 { return nil, false, nil } - warehouse.TeamName = teamName + warehouse.OrgID = orgID warehouse.UpdatedAt = time.Now().UTC() if err := s.db().Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "team_name"}}, + Columns: []clause.Column{{Name: "org_id"}}, DoUpdates: clause.AssignmentColumns(managedWarehouseUpsertColumns()), }).Create(warehouse).Error; err != nil { return nil, true, err } - stored, err := s.GetManagedWarehouse(teamName) + stored, err := s.GetManagedWarehouse(orgID) if err != nil { return nil, true, err } @@ -386,7 +386,7 @@ func (s *gormAPIStore) SaveQueryLogConfig(cfg *configstore.QueryLogConfig) error type apiHandler struct { store apiStore - info TeamStackInfo + info OrgStackInfo } type managedWarehouseRequest struct { @@ -447,87 +447,87 @@ func decodeStrictWarehouseRequest(c *gin.Context, dst *managedWarehouseRequest) return dec.Decode(dst) } -// --- Teams --- +// --- Orgs --- -func (h *apiHandler) listTeams(c *gin.Context) { - teams, err := h.store.ListTeams() +func (h *apiHandler) listOrgs(c *gin.Context) { + orgs, err := h.store.ListOrgs() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, teams) + c.JSON(http.StatusOK, orgs) } -func (h *apiHandler) createTeam(c *gin.Context) { - var team configstore.Team - if err := c.ShouldBindJSON(&team); err != nil { +func (h *apiHandler) createOrg(c *gin.Context) { + var org configstore.Org + if err := c.ShouldBindJSON(&org); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if err := validateTeamMutationPayload(&team); err != nil { + if err := validateOrgMutationPayload(&org); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if team.Name == "" { + if org.Name == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) return } - if err := h.store.CreateTeam(&team); err != nil { + if err := h.store.CreateOrg(&org); err != nil { c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusCreated, team) + c.JSON(http.StatusCreated, org) } -func (h *apiHandler) getTeam(c *gin.Context) { +func (h *apiHandler) getOrg(c *gin.Context) { name := c.Param("name") - team, err := h.store.GetTeam(name) + org, err := h.store.GetOrg(name) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "team not found"}) + c.JSON(http.StatusNotFound, gin.H{"error": "org not found"}) return } - c.JSON(http.StatusOK, team) + c.JSON(http.StatusOK, org) } -func (h *apiHandler) updateTeam(c *gin.Context) { +func (h *apiHandler) updateOrg(c *gin.Context) { name := c.Param("name") - var updates configstore.Team + var updates configstore.Org if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if err := validateTeamMutationPayload(&updates); err != nil { + if err := validateOrgMutationPayload(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - team, ok, err := h.store.UpdateTeam(name, updates) + org, ok, err := h.store.UpdateOrg(name, updates) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if !ok { - c.JSON(http.StatusNotFound, gin.H{"error": "team not found"}) + c.JSON(http.StatusNotFound, gin.H{"error": "org not found"}) return } - c.JSON(http.StatusOK, team) + c.JSON(http.StatusOK, org) } -func (h *apiHandler) deleteTeam(c *gin.Context) { +func (h *apiHandler) deleteOrg(c *gin.Context) { name := c.Param("name") - ok, err := h.store.DeleteTeam(name) + ok, err := h.store.DeleteOrg(name) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if !ok { - c.JSON(http.StatusNotFound, gin.H{"error": "team not found"}) + c.JSON(http.StatusNotFound, gin.H{"error": "org not found"}) return } c.JSON(http.StatusOK, gin.H{"deleted": name}) } -func validateTeamMutationPayload(team *configstore.Team) error { - if team != nil && team.Warehouse != nil { +func validateOrgMutationPayload(org *configstore.Org) error { + if org != nil && org.Warehouse != nil { return errWarehousePayloadNotAllowed } return nil @@ -547,20 +547,20 @@ func (h *apiHandler) getManagedWarehouse(c *gin.Context) { } func (h *apiHandler) putManagedWarehouse(c *gin.Context) { - teamName := c.Param("name") + orgID := c.Param("name") var req managedWarehouseRequest if err := decodeStrictWarehouseRequest(c, &req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } warehouse := req.toManagedWarehouse() - stored, ok, err := h.store.UpsertManagedWarehouse(teamName, &warehouse) + stored, ok, err := h.store.UpsertManagedWarehouse(orgID, &warehouse) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if !ok { - c.JSON(http.StatusNotFound, gin.H{"error": "team not found"}) + c.JSON(http.StatusNotFound, gin.H{"error": "org not found"}) return } c.JSON(http.StatusOK, stored) @@ -578,18 +578,18 @@ func (h *apiHandler) listUsers(c *gin.Context) { } func (h *apiHandler) createUser(c *gin.Context) { - // Use a raw struct because TeamUser.Password has json:"-" + // Use a raw struct because OrgUser.Password has json:"-" var raw struct { Username string `json:"username"` Password string `json:"password"` - TeamName string `json:"team_name"` + OrgID string `json:"org_id"` } if err := c.ShouldBindJSON(&raw); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if raw.Username == "" || raw.TeamName == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "username and team_name are required"}) + if raw.Username == "" || raw.OrgID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "username and org_id are required"}) return } if raw.Password == "" { @@ -601,10 +601,10 @@ func (h *apiHandler) createUser(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"}) return } - user := configstore.TeamUser{ + user := configstore.OrgUser{ Username: raw.Username, Password: hash, - TeamName: raw.TeamName, + OrgID: raw.OrgID, } if err := h.store.CreateUser(&user); err != nil { c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) @@ -627,7 +627,7 @@ func (h *apiHandler) updateUser(c *gin.Context) { username := c.Param("username") var raw struct { Password string `json:"password"` - TeamName string `json:"team_name"` + OrgID string `json:"org_id"` } if err := c.ShouldBindJSON(&raw); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -642,7 +642,7 @@ func (h *apiHandler) updateUser(c *gin.Context) { } passwordHash = hash } - user, ok, err := h.store.UpdateUser(username, passwordHash, raw.TeamName) + user, ok, err := h.store.UpdateUser(username, passwordHash, raw.OrgID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -790,18 +790,18 @@ func (h *apiHandler) getClusterStatus(c *gin.Context) { return } - teamStats := h.info.AllTeamStats() + orgStats := h.info.AllOrgStats() totalWorkers := 0 totalSessions := 0 - for _, ts := range teamStats { - totalWorkers += ts.Workers - totalSessions += ts.ActiveSessions + for _, os := range orgStats { + totalWorkers += os.Workers + totalSessions += os.ActiveSessions } c.JSON(http.StatusOK, ClusterStatus{ - TotalTeams: len(teamStats), + TotalOrgs: len(orgStats), TotalWorkers: totalWorkers, TotalSessions: totalSessions, - Teams: teamStats, + Orgs: orgStats, }) } diff --git a/controlplane/admin/api_postgres_test.go b/controlplane/admin/api_postgres_test.go index bec0f10..d8d4380 100644 --- a/controlplane/admin/api_postgres_test.go +++ b/controlplane/admin/api_postgres_test.go @@ -87,8 +87,8 @@ func resetConfigStoreTables(t *testing.T, db *gorm.DB) { for _, model := range []any{ &configstore.ManagedWarehouse{}, - &configstore.TeamUser{}, - &configstore.Team{}, + &configstore.OrgUser{}, + &configstore.Org{}, } { if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(model).Error; err != nil { t.Fatalf("delete %T: %v", model, err) @@ -100,13 +100,13 @@ func TestUpsertManagedWarehousePreservesCreatedAt(t *testing.T) { store := newPostgresConfigStore(t) apiStore := newGormAPIStore(store).(*gormAPIStore) - if err := store.DB().Create(&configstore.Team{Name: "analytics"}).Error; err != nil { - t.Fatalf("create team: %v", err) + if err := store.DB().Create(&configstore.Org{Name: "analytics"}).Error; err != nil { + t.Fatalf("create org: %v", err) } createdAt := time.Date(2024, time.January, 2, 3, 4, 5, 0, time.UTC) original := &configstore.ManagedWarehouse{ - TeamName: "analytics", + OrgID: "analytics", State: configstore.ManagedWarehouseStatePending, CreatedAt: createdAt, UpdatedAt: createdAt, diff --git a/controlplane/admin/api_test.go b/controlplane/admin/api_test.go index 537fe5e..64d08b2 100644 --- a/controlplane/admin/api_test.go +++ b/controlplane/admin/api_test.go @@ -18,67 +18,67 @@ import ( ) type fakeAPIStore struct { - teams map[string]*configstore.Team - users map[string]*configstore.TeamUser + orgs map[string]*configstore.Org + users map[string]*configstore.OrgUser warehouses map[string]*configstore.ManagedWarehouse } func newFakeAPIStore() *fakeAPIStore { return &fakeAPIStore{ - teams: make(map[string]*configstore.Team), - users: make(map[string]*configstore.TeamUser), + orgs: make(map[string]*configstore.Org), + users: make(map[string]*configstore.OrgUser), warehouses: make(map[string]*configstore.ManagedWarehouse), } } -func (s *fakeAPIStore) ListTeams() ([]configstore.Team, error) { - teams := make([]configstore.Team, 0, len(s.teams)) - for _, team := range s.teams { - teams = append(teams, *copyTeam(team)) +func (s *fakeAPIStore) ListOrgs() ([]configstore.Org, error) { + orgs := make([]configstore.Org, 0, len(s.orgs)) + for _, org := range s.orgs { + orgs = append(orgs, *copyOrg(org)) } - return teams, nil + return orgs, nil } -func (s *fakeAPIStore) CreateTeam(team *configstore.Team) error { - if _, ok := s.teams[team.Name]; ok { - return errors.New("duplicate team") +func (s *fakeAPIStore) CreateOrg(org *configstore.Org) error { + if _, ok := s.orgs[org.Name]; ok { + return errors.New("duplicate org") } - clone := copyTeam(team) + clone := copyOrg(org) clone.Warehouse = nil - s.teams[team.Name] = clone + s.orgs[org.Name] = clone return nil } -func (s *fakeAPIStore) GetTeam(name string) (*configstore.Team, error) { - team, ok := s.teams[name] +func (s *fakeAPIStore) GetOrg(name string) (*configstore.Org, error) { + org, ok := s.orgs[name] if !ok { return nil, gorm.ErrRecordNotFound } - return copyTeam(team), nil + return copyOrg(org), nil } -func (s *fakeAPIStore) UpdateTeam(name string, updates configstore.Team) (*configstore.Team, bool, error) { - team, ok := s.teams[name] +func (s *fakeAPIStore) UpdateOrg(name string, updates configstore.Org) (*configstore.Org, bool, error) { + org, ok := s.orgs[name] if !ok { return nil, false, nil } - team.MaxWorkers = updates.MaxWorkers - team.MemoryBudget = updates.MemoryBudget - team.IdleTimeoutS = updates.IdleTimeoutS - return copyTeam(team), true, nil + org.MaxWorkers = updates.MaxWorkers + org.MemoryBudget = updates.MemoryBudget + org.IdleTimeoutS = updates.IdleTimeoutS + return copyOrg(org), true, nil } -func (s *fakeAPIStore) DeleteTeam(name string) (bool, error) { - if _, ok := s.teams[name]; !ok { +func (s *fakeAPIStore) DeleteOrg(name string) (bool, error) { + if _, ok := s.orgs[name]; !ok { return false, nil } - delete(s.teams, name) + delete(s.orgs, name) delete(s.warehouses, name) return true, nil } -func (s *fakeAPIStore) ListUsers() ([]configstore.TeamUser, error) { - users := make([]configstore.TeamUser, 0, len(s.users)) +func (s *fakeAPIStore) ListUsers() ([]configstore.OrgUser, error) { + users := make([]configstore.OrgUser, 0, len(s.users)) for _, user := range s.users { clone := *user users = append(users, clone) @@ -86,7 +86,7 @@ func (s *fakeAPIStore) ListUsers() ([]configstore.TeamUser, error) { return users, nil } -func (s *fakeAPIStore) CreateUser(user *configstore.TeamUser) error { +func (s *fakeAPIStore) CreateUser(user *configstore.OrgUser) error { if _, ok := s.users[user.Username]; ok { return errors.New("duplicate user") } @@ -95,7 +95,7 @@ func (s *fakeAPIStore) CreateUser(user *configstore.TeamUser) error { return nil } -func (s *fakeAPIStore) GetUser(username string) (*configstore.TeamUser, error) { +func (s *fakeAPIStore) GetUser(username string) (*configstore.OrgUser, error) { user, ok := s.users[username] if !ok { return nil, gorm.ErrRecordNotFound @@ -104,7 +104,7 @@ func (s *fakeAPIStore) GetUser(username string) (*configstore.TeamUser, error) { return &clone, nil } -func (s *fakeAPIStore) UpdateUser(username, passwordHash, teamName string) (*configstore.TeamUser, bool, error) { +func (s *fakeAPIStore) UpdateUser(username, passwordHash, orgID string) (*configstore.OrgUser, bool, error) { user, ok := s.users[username] if !ok { return nil, false, nil @@ -112,8 +112,8 @@ func (s *fakeAPIStore) UpdateUser(username, passwordHash, teamName string) (*con if passwordHash != "" { user.Password = passwordHash } - if teamName != "" { - user.TeamName = teamName + if orgID != "" { + user.OrgID = orgID } clone := *user return &clone, true, nil @@ -127,23 +127,23 @@ func (s *fakeAPIStore) DeleteUser(username string) (bool, error) { return true, nil } -func (s *fakeAPIStore) GetManagedWarehouse(teamName string) (*configstore.ManagedWarehouse, error) { - warehouse, ok := s.warehouses[teamName] +func (s *fakeAPIStore) GetManagedWarehouse(orgID string) (*configstore.ManagedWarehouse, error) { + warehouse, ok := s.warehouses[orgID] if !ok { return nil, gorm.ErrRecordNotFound } return copyWarehouse(warehouse), nil } -func (s *fakeAPIStore) UpsertManagedWarehouse(teamName string, warehouse *configstore.ManagedWarehouse) (*configstore.ManagedWarehouse, bool, error) { - team, ok := s.teams[teamName] +func (s *fakeAPIStore) UpsertManagedWarehouse(orgID string, warehouse *configstore.ManagedWarehouse) (*configstore.ManagedWarehouse, bool, error) { + org, ok := s.orgs[orgID] if !ok { return nil, false, nil } clone := copyWarehouse(warehouse) - clone.TeamName = teamName - s.warehouses[teamName] = clone - team.Warehouse = copyWarehouse(clone) + clone.OrgID = orgID + s.warehouses[orgID] = clone + org.Warehouse = copyWarehouse(clone) return copyWarehouse(clone), true, nil } @@ -187,17 +187,17 @@ func copyWarehouse(warehouse *configstore.ManagedWarehouse) *configstore.Managed return &clone } -func copyTeam(team *configstore.Team) *configstore.Team { - if team == nil { +func copyOrg(org *configstore.Org) *configstore.Org { + if org == nil { return nil } - clone := *team - if team.Warehouse != nil { - clone.Warehouse = copyWarehouse(team.Warehouse) + clone := *org + if org.Warehouse != nil { + clone.Warehouse = copyWarehouse(org.Warehouse) } - if len(team.Users) > 0 { - clone.Users = make([]configstore.TeamUser, len(team.Users)) - copy(clone.Users, team.Users) + if len(org.Users) > 0 { + clone.Users = make([]configstore.OrgUser, len(org.Users)) + copy(clone.Users, org.Users) } return &clone } @@ -209,9 +209,9 @@ func newTestAPIRouter(store apiStore) *gin.Engine { return r } -func seedTeamWithWarehouse(store *fakeAPIStore, name string) { +func seedOrgWithWarehouse(store *fakeAPIStore, name string) { warehouse := &configstore.ManagedWarehouse{ - TeamName: name, + OrgID: name, WarehouseDatabase: configstore.ManagedWarehouseDatabase{ Region: "us-east-1", Endpoint: fmt.Sprintf("%s.cluster.example", name), @@ -266,19 +266,19 @@ func seedTeamWithWarehouse(store *fakeAPIStore, name string) { IdentityState: configstore.ManagedWarehouseStateReady, SecretsState: configstore.ManagedWarehouseStateReady, } - store.teams[name] = &configstore.Team{ + store.orgs[name] = &configstore.Org{ Name: name, Warehouse: copyWarehouse(warehouse), } store.warehouses[name] = warehouse } -func TestGetTeamIncludesWarehouse(t *testing.T) { +func TestGetOrgIncludesWarehouse(t *testing.T) { store := newFakeAPIStore() - seedTeamWithWarehouse(store, "analytics") + seedOrgWithWarehouse(store, "analytics") router := newTestAPIRouter(store) - req := httptest.NewRequest(http.MethodGet, "/api/v1/teams/analytics", nil) + req := httptest.NewRequest(http.MethodGet, "/api/v1/orgs/analytics", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) @@ -286,27 +286,27 @@ func TestGetTeamIncludesWarehouse(t *testing.T) { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } - var team configstore.Team - if err := json.Unmarshal(rec.Body.Bytes(), &team); err != nil { - t.Fatalf("unmarshal team: %v", err) + var org configstore.Org + if err := json.Unmarshal(rec.Body.Bytes(), &org); err != nil { + t.Fatalf("unmarshal org: %v", err) } - if team.Warehouse == nil { - t.Fatal("expected warehouse in team response") + if org.Warehouse == nil { + t.Fatal("expected warehouse in org response") } - if team.Warehouse.WarehouseDatabase.DatabaseName != "analytics_warehouse" { - t.Fatalf("expected analytics_warehouse, got %q", team.Warehouse.WarehouseDatabase.DatabaseName) + if org.Warehouse.WarehouseDatabase.DatabaseName != "analytics_warehouse" { + t.Fatalf("expected analytics_warehouse, got %q", org.Warehouse.WarehouseDatabase.DatabaseName) } - if team.Warehouse.MetadataStore.Kind != "dedicated_rds" { - t.Fatalf("expected metadata store kind dedicated_rds, got %q", team.Warehouse.MetadataStore.Kind) + if org.Warehouse.MetadataStore.Kind != "dedicated_rds" { + t.Fatalf("expected metadata store kind dedicated_rds, got %q", org.Warehouse.MetadataStore.Kind) } } -func TestListTeamsIncludesWarehouse(t *testing.T) { +func TestListOrgsIncludesWarehouse(t *testing.T) { store := newFakeAPIStore() - seedTeamWithWarehouse(store, "analytics") + seedOrgWithWarehouse(store, "analytics") router := newTestAPIRouter(store) - req := httptest.NewRequest(http.MethodGet, "/api/v1/teams", nil) + req := httptest.NewRequest(http.MethodGet, "/api/v1/orgs", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) @@ -314,24 +314,24 @@ func TestListTeamsIncludesWarehouse(t *testing.T) { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } - var teams []configstore.Team - if err := json.Unmarshal(rec.Body.Bytes(), &teams); err != nil { - t.Fatalf("unmarshal teams: %v", err) + var orgs []configstore.Org + if err := json.Unmarshal(rec.Body.Bytes(), &orgs); err != nil { + t.Fatalf("unmarshal orgs: %v", err) } - if len(teams) != 1 { - t.Fatalf("expected 1 team, got %d", len(teams)) + if len(orgs) != 1 { + t.Fatalf("expected 1 org, got %d", len(orgs)) } - if teams[0].Warehouse == nil { - t.Fatal("expected nested warehouse in team list response") + if orgs[0].Warehouse == nil { + t.Fatal("expected nested warehouse in org list response") } } func TestGetWarehouseReturnsNotFoundWhenMissing(t *testing.T) { store := newFakeAPIStore() - store.teams["analytics"] = &configstore.Team{Name: "analytics"} + store.orgs["analytics"] = &configstore.Org{Name: "analytics"} router := newTestAPIRouter(store) - req := httptest.NewRequest(http.MethodGet, "/api/v1/teams/analytics/warehouse", nil) + req := httptest.NewRequest(http.MethodGet, "/api/v1/orgs/analytics/warehouse", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) @@ -340,9 +340,9 @@ func TestGetWarehouseReturnsNotFoundWhenMissing(t *testing.T) { } } -func TestPutWarehouseUpsertsForExistingTeam(t *testing.T) { +func TestPutWarehouseUpsertsForExistingOrg(t *testing.T) { store := newFakeAPIStore() - store.teams["analytics"] = &configstore.Team{Name: "analytics"} + store.orgs["analytics"] = &configstore.Org{Name: "analytics"} router := newTestAPIRouter(store) body := []byte(`{ @@ -405,7 +405,7 @@ func TestPutWarehouseUpsertsForExistingTeam(t *testing.T) { "secrets_state": "ready" }`) - req := httptest.NewRequest(http.MethodPut, "/api/v1/teams/analytics/warehouse", bytes.NewReader(body)) + req := httptest.NewRequest(http.MethodPut, "/api/v1/orgs/analytics/warehouse", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) @@ -418,8 +418,8 @@ func TestPutWarehouseUpsertsForExistingTeam(t *testing.T) { if warehouse == nil { t.Fatal("expected stored warehouse") } - if warehouse.TeamName != "analytics" { - t.Fatalf("expected team_name analytics, got %q", warehouse.TeamName) + if warehouse.OrgID != "analytics" { + t.Fatalf("expected org_id analytics, got %q", warehouse.OrgID) } if warehouse.RuntimeConfig.Name != "analytics-runtime" { t.Fatalf("expected runtime secret analytics-runtime, got %q", warehouse.RuntimeConfig.Name) @@ -432,11 +432,11 @@ func TestPutWarehouseUpsertsForExistingTeam(t *testing.T) { } } -func TestPutWarehouseRejectsUnknownTeam(t *testing.T) { +func TestPutWarehouseRejectsUnknownOrg(t *testing.T) { store := newFakeAPIStore() router := newTestAPIRouter(store) - req := httptest.NewRequest(http.MethodPut, "/api/v1/teams/unknown/warehouse", bytes.NewReader([]byte(`{"state":"ready"}`))) + req := httptest.NewRequest(http.MethodPut, "/api/v1/orgs/unknown/warehouse", bytes.NewReader([]byte(`{"state":"ready"}`))) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) @@ -448,18 +448,18 @@ func TestPutWarehouseRejectsUnknownTeam(t *testing.T) { func TestPutWarehouseRejectsServerManagedFields(t *testing.T) { store := newFakeAPIStore() - store.teams["analytics"] = &configstore.Team{Name: "analytics"} + store.orgs["analytics"] = &configstore.Org{Name: "analytics"} router := newTestAPIRouter(store) body := []byte(`{ - "team_name": "wrong-team", + "org_id": "wrong-org", "created_at": "2026-03-18T10:00:00Z", "warehouse_database": { "database_name": "analytics_warehouse" } }`) - req := httptest.NewRequest(http.MethodPut, "/api/v1/teams/analytics/warehouse", bytes.NewReader(body)) + req := httptest.NewRequest(http.MethodPut, "/api/v1/orgs/analytics/warehouse", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) @@ -471,7 +471,7 @@ func TestPutWarehouseRejectsServerManagedFields(t *testing.T) { func TestPutWarehouseAllowsCustomProvisioningStates(t *testing.T) { store := newFakeAPIStore() - store.teams["analytics"] = &configstore.Team{Name: "analytics"} + store.orgs["analytics"] = &configstore.Org{Name: "analytics"} router := newTestAPIRouter(store) body := []byte(`{ @@ -483,7 +483,7 @@ func TestPutWarehouseAllowsCustomProvisioningStates(t *testing.T) { "secrets_state": "waiting-external-secret" }`) - req := httptest.NewRequest(http.MethodPut, "/api/v1/teams/analytics/warehouse", bytes.NewReader(body)) + req := httptest.NewRequest(http.MethodPut, "/api/v1/orgs/analytics/warehouse", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) @@ -507,7 +507,7 @@ func TestPutWarehouseAllowsCustomProvisioningStates(t *testing.T) { } } -func TestCreateTeamRejectsNestedWarehousePayload(t *testing.T) { +func TestCreateOrgRejectsNestedWarehousePayload(t *testing.T) { store := newFakeAPIStore() router := newTestAPIRouter(store) @@ -519,7 +519,7 @@ func TestCreateTeamRejectsNestedWarehousePayload(t *testing.T) { } }`) - req := httptest.NewRequest(http.MethodPost, "/api/v1/teams", bytes.NewReader(body)) + req := httptest.NewRequest(http.MethodPost, "/api/v1/orgs", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) @@ -527,14 +527,14 @@ func TestCreateTeamRejectsNestedWarehousePayload(t *testing.T) { if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusBadRequest, rec.Body.String()) } - if _, ok := store.teams["analytics"]; ok { - t.Fatal("expected team create to be rejected when warehouse payload is present") + if _, ok := store.orgs["analytics"]; ok { + t.Fatal("expected org create to be rejected when warehouse payload is present") } } -func TestUpdateTeamRejectsNestedWarehousePayload(t *testing.T) { +func TestUpdateOrgRejectsNestedWarehousePayload(t *testing.T) { store := newFakeAPIStore() - store.teams["analytics"] = &configstore.Team{Name: "analytics", MaxWorkers: 2} + store.orgs["analytics"] = &configstore.Org{Name: "analytics", MaxWorkers: 2} router := newTestAPIRouter(store) body := []byte(`{ @@ -544,7 +544,7 @@ func TestUpdateTeamRejectsNestedWarehousePayload(t *testing.T) { } }`) - req := httptest.NewRequest(http.MethodPut, "/api/v1/teams/analytics", bytes.NewReader(body)) + req := httptest.NewRequest(http.MethodPut, "/api/v1/orgs/analytics", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) @@ -552,20 +552,20 @@ func TestUpdateTeamRejectsNestedWarehousePayload(t *testing.T) { if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusBadRequest, rec.Body.String()) } - if store.teams["analytics"].MaxWorkers != 2 { - t.Fatalf("expected team update to be rejected, max_workers = %d", store.teams["analytics"].MaxWorkers) + if store.orgs["analytics"].MaxWorkers != 2 { + t.Fatalf("expected org update to be rejected, max_workers = %d", store.orgs["analytics"].MaxWorkers) } } -func TestGetTeamOmitsMinWorkers(t *testing.T) { +func TestGetOrgOmitsMinWorkers(t *testing.T) { store := newFakeAPIStore() - store.teams["analytics"] = &configstore.Team{ + store.orgs["analytics"] = &configstore.Org{ Name: "analytics", MaxWorkers: 2, } router := newTestAPIRouter(store) - req := httptest.NewRequest(http.MethodGet, "/api/v1/teams/analytics", nil) + req := httptest.NewRequest(http.MethodGet, "/api/v1/orgs/analytics", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) @@ -573,7 +573,7 @@ func TestGetTeamOmitsMinWorkers(t *testing.T) { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } if bytes.Contains(rec.Body.Bytes(), []byte(`"min_workers"`)) { - t.Fatalf("expected team response to omit min_workers, got %s", rec.Body.String()) + t.Fatalf("expected org response to omit min_workers, got %s", rec.Body.String()) } } @@ -583,8 +583,8 @@ func TestManagedWarehouseUpsertColumnsExcludeCreatedAt(t *testing.T) { if slices.Contains(columns, "created_at") { t.Fatal("expected created_at to be excluded from managed warehouse upserts") } - if slices.Contains(columns, "team_name") { - t.Fatal("expected team_name to be excluded from managed warehouse upserts") + if slices.Contains(columns, "org_id") { + t.Fatal("expected org_id to be excluded from managed warehouse upserts") } if !slices.Contains(columns, "updated_at") { t.Fatal("expected updated_at to be included in managed warehouse upserts") diff --git a/controlplane/admin/dashboard.go b/controlplane/admin/dashboard.go index 16ebecc..6984663 100644 --- a/controlplane/admin/dashboard.go +++ b/controlplane/admin/dashboard.go @@ -37,7 +37,7 @@ func APIAuthMiddleware(adminToken string) gin.HandlerFunc { // RegisterDashboard serves the admin dashboard on the Gin engine. func RegisterDashboard(r *gin.Engine, adminToken string) { r.GET("/", dashboardPageHandler("index.html", adminToken)) - r.GET("/teams", dashboardPageHandler("teams.html", adminToken)) + r.GET("/orgs", dashboardPageHandler("orgs.html", adminToken)) r.GET("/workers", dashboardPageHandler("workers.html", adminToken)) r.GET("/sessions", dashboardPageHandler("sessions.html", adminToken)) r.GET("/settings", dashboardPageHandler("settings.html", adminToken)) diff --git a/controlplane/admin/static/index.html b/controlplane/admin/static/index.html index 62668d9..1745ab5 100644 --- a/controlplane/admin/static/index.html +++ b/controlplane/admin/static/index.html @@ -16,7 +16,7 @@
-| Name | Users | Max Workers | Memory Budget |
|---|---|---|---|
| ${esc(t.name)} | ${(t.users || []).length} | diff --git a/controlplane/admin/static/teams.html b/controlplane/admin/static/orgs.html similarity index 80% rename from controlplane/admin/static/teams.html rename to controlplane/admin/static/orgs.html index f36fa3e..d114b0b 100644 --- a/controlplane/admin/static/teams.html +++ b/controlplane/admin/static/orgs.html @@ -3,7 +3,7 @@ -