mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Ensure admin key deletion removes the DB record and returns a "deleted" status. Update seeder idempotency to count only active keys when deciding whether to skip or create new keys.
573 lines
16 KiB
Go
573 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// seededNamespaces stores created namespace names for later use
|
|
var seededNamespaces []string
|
|
|
|
// seededProviderGroups stores created provider groups for later use
|
|
var seededProviderGroups []ProviderGroupResponse
|
|
|
|
// seededProviders stores created providers (API keys) per group for usage samples
|
|
var seededProviders map[uint][]APIKeyResponse
|
|
|
|
// seededMasters stores created masters for later use
|
|
var seededMasters []MasterResponse
|
|
|
|
// seededKeys stores created keys per master for usage samples
|
|
var seededKeys map[uint][]KeyResponse
|
|
|
|
// matchesSeederTag checks if a string contains the current seeder tag
|
|
func (s *Seeder) matchesSeederTag(text string) bool {
|
|
return strings.Contains(text, s.seederTag)
|
|
}
|
|
|
|
// matchesSeederPrefix checks if a name matches seeder naming convention
|
|
func (s *Seeder) matchesSeederPrefix(name string) bool {
|
|
return strings.HasPrefix(name, "demo-") || strings.HasPrefix(name, "seeder-") || strings.HasSuffix(name, "-demo")
|
|
}
|
|
|
|
func filterActiveKeys(keys []KeyResponse) []KeyResponse {
|
|
if len(keys) == 0 {
|
|
return nil
|
|
}
|
|
active := make([]KeyResponse, 0, len(keys))
|
|
for _, key := range keys {
|
|
if strings.EqualFold(strings.TrimSpace(key.Status), "active") {
|
|
active = append(active, key)
|
|
}
|
|
}
|
|
return active
|
|
}
|
|
|
|
func (s *Seeder) seedNamespaces() error {
|
|
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
|
|
namespaces := gen.GenerateNamespaces(s.profile.Namespaces)
|
|
|
|
// Get existing namespaces
|
|
existing, err := s.client.ListNamespaces()
|
|
if err != nil {
|
|
if s.cfg.DryRun {
|
|
existing = []NamespaceResponse{}
|
|
fmt.Printf(" [DRY-RUN] Unable to list existing namespaces, proceeding anyway\n")
|
|
} else {
|
|
return fmt.Errorf("list namespaces: %w", err)
|
|
}
|
|
}
|
|
|
|
existingMap := make(map[string]NamespaceResponse)
|
|
for _, ns := range existing {
|
|
existingMap[ns.Name] = ns
|
|
}
|
|
|
|
seededNamespaces = make([]string, 0, len(namespaces))
|
|
|
|
for _, ns := range namespaces {
|
|
if existingNs, exists := existingMap[ns.Name]; exists {
|
|
// Check if we should reset - only reset if tag matches
|
|
if s.cfg.Reset && s.matchesSeederTag(existingNs.Description) {
|
|
if err := s.client.DeleteNamespace(existingNs.ID); err != nil {
|
|
return fmt.Errorf("delete namespace %s: %w", ns.Name, err)
|
|
}
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ✗ %s (deleted for reset)\n", ns.Name)
|
|
}
|
|
} else {
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ○ %s (exists, skipped)\n", ns.Name)
|
|
}
|
|
s.summary.Namespaces.Skipped++
|
|
seededNamespaces = append(seededNamespaces, ns.Name)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Create namespace
|
|
created, err := s.client.CreateNamespace(ns)
|
|
if err != nil {
|
|
if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() {
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ○ %s (exists, skipped)\n", ns.Name)
|
|
}
|
|
s.summary.Namespaces.Skipped++
|
|
seededNamespaces = append(seededNamespaces, ns.Name)
|
|
continue
|
|
}
|
|
return fmt.Errorf("create namespace %s: %w", ns.Name, err)
|
|
}
|
|
|
|
if s.cfg.Verbose || s.cfg.DryRun {
|
|
fmt.Printf(" ✓ %s (created)\n", ns.Name)
|
|
}
|
|
s.summary.Namespaces.Created++
|
|
if created != nil {
|
|
seededNamespaces = append(seededNamespaces, created.Name)
|
|
} else {
|
|
seededNamespaces = append(seededNamespaces, ns.Name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Seeder) seedProviderGroups() error {
|
|
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
|
|
groups := gen.GenerateProviderGroups(s.profile.ProviderGroups)
|
|
|
|
// Get existing provider groups
|
|
existing, err := s.client.ListProviderGroups()
|
|
if err != nil {
|
|
if s.cfg.DryRun {
|
|
existing = []ProviderGroupResponse{}
|
|
fmt.Printf(" [DRY-RUN] Unable to list existing provider groups, proceeding anyway\n")
|
|
} else {
|
|
return fmt.Errorf("list provider groups: %w", err)
|
|
}
|
|
}
|
|
|
|
existingMap := make(map[string]ProviderGroupResponse)
|
|
for _, g := range existing {
|
|
existingMap[g.Name] = g
|
|
}
|
|
|
|
seededProviderGroups = make([]ProviderGroupResponse, 0, len(groups))
|
|
|
|
for _, group := range groups {
|
|
if existingGroup, exists := existingMap[group.Name]; exists {
|
|
// Check if we should reset - match by seeder naming convention
|
|
// Provider groups use name suffix like "openai-demo", "anthropic-demo"
|
|
if s.cfg.Reset && s.matchesSeederPrefix(group.Name) {
|
|
if err := s.client.DeleteProviderGroup(existingGroup.ID); err != nil {
|
|
return fmt.Errorf("delete provider group %s: %w", group.Name, err)
|
|
}
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ✗ %s (deleted for reset)\n", group.Name)
|
|
}
|
|
} else {
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ○ %s (exists, skipped)\n", group.Name)
|
|
}
|
|
s.summary.ProviderGroups.Skipped++
|
|
seededProviderGroups = append(seededProviderGroups, existingGroup)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Create provider group
|
|
created, err := s.client.CreateProviderGroup(group)
|
|
if err != nil {
|
|
if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() {
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ○ %s (exists, skipped)\n", group.Name)
|
|
}
|
|
s.summary.ProviderGroups.Skipped++
|
|
continue
|
|
}
|
|
return fmt.Errorf("create provider group %s: %w", group.Name, err)
|
|
}
|
|
|
|
if s.cfg.Verbose || s.cfg.DryRun {
|
|
fmt.Printf(" ✓ %s (created)\n", group.Name)
|
|
}
|
|
s.summary.ProviderGroups.Created++
|
|
if created != nil {
|
|
seededProviderGroups = append(seededProviderGroups, *created)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Seeder) seedProviders() error {
|
|
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
|
|
seededProviders = make(map[uint][]APIKeyResponse)
|
|
|
|
// Get existing API keys for idempotency check
|
|
existingKeys, err := s.client.ListAPIKeys()
|
|
if err != nil {
|
|
if s.cfg.DryRun {
|
|
existingKeys = []APIKeyResponse{}
|
|
fmt.Printf(" [DRY-RUN] Unable to list existing API keys, proceeding anyway\n")
|
|
} else {
|
|
return fmt.Errorf("list api keys: %w", err)
|
|
}
|
|
}
|
|
|
|
// Build map of existing keys by group_id -> api_key prefix (for matching seeder-generated keys)
|
|
existingByGroup := make(map[uint]map[string]APIKeyResponse)
|
|
for _, key := range existingKeys {
|
|
if existingByGroup[key.GroupID] == nil {
|
|
existingByGroup[key.GroupID] = make(map[string]APIKeyResponse)
|
|
}
|
|
// Store by full key for exact match
|
|
existingByGroup[key.GroupID][key.APIKey] = key
|
|
}
|
|
|
|
for _, group := range seededProviderGroups {
|
|
providers := gen.GenerateProviders(group.ID, group.Name, s.profile.ProvidersPerGroup)
|
|
groupProviders := make([]APIKeyResponse, 0, len(providers))
|
|
|
|
createdCount := 0
|
|
skippedCount := 0
|
|
|
|
for _, provider := range providers {
|
|
// Check if this exact key already exists
|
|
if groupKeys, ok := existingByGroup[group.ID]; ok {
|
|
if existingKey, exists := groupKeys[provider.APIKey]; exists {
|
|
// Key already exists
|
|
if s.cfg.Reset && s.matchesSeederPrefix(group.Name) {
|
|
// Delete for reset
|
|
if err := s.client.DeleteAPIKey(existingKey.ID); err != nil {
|
|
return fmt.Errorf("delete api key for group %s: %w", group.Name, err)
|
|
}
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ✗ api key %d (deleted for reset)\n", existingKey.ID)
|
|
}
|
|
} else {
|
|
// Skip existing
|
|
s.summary.Providers.Skipped++
|
|
skippedCount++
|
|
groupProviders = append(groupProviders, existingKey)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
created, err := s.client.CreateAPIKey(provider)
|
|
if err != nil {
|
|
if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() {
|
|
s.summary.Providers.Skipped++
|
|
skippedCount++
|
|
continue
|
|
}
|
|
return fmt.Errorf("create provider for group %s: %w", group.Name, err)
|
|
}
|
|
s.summary.Providers.Created++
|
|
createdCount++
|
|
if created != nil {
|
|
groupProviders = append(groupProviders, *created)
|
|
}
|
|
}
|
|
|
|
seededProviders[group.ID] = groupProviders
|
|
|
|
if s.cfg.Verbose || s.cfg.DryRun {
|
|
fmt.Printf(" ✓ %s: %d providers created, %d skipped\n", group.Name, createdCount, skippedCount)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Seeder) seedModels() error {
|
|
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
|
|
models := gen.GenerateModels(s.profile.Models)
|
|
|
|
// Get existing models
|
|
existing, err := s.client.ListModels()
|
|
if err != nil {
|
|
if s.cfg.DryRun {
|
|
existing = []ModelResponse{}
|
|
fmt.Printf(" [DRY-RUN] Unable to list existing models, proceeding anyway\n")
|
|
} else {
|
|
return fmt.Errorf("list models: %w", err)
|
|
}
|
|
}
|
|
|
|
existingMap := make(map[string]ModelResponse)
|
|
for _, m := range existing {
|
|
existingMap[m.Name] = m
|
|
}
|
|
|
|
for _, model := range models {
|
|
if existingModel, exists := existingMap[model.Name]; exists {
|
|
// Models are shared resources - only reset if explicitly requested
|
|
// and the model name matches seeder pattern (unlikely for real models)
|
|
if s.cfg.Reset && s.matchesSeederPrefix(model.Name) {
|
|
if err := s.client.DeleteModel(existingModel.ID); err != nil {
|
|
return fmt.Errorf("delete model %s: %w", model.Name, err)
|
|
}
|
|
} else {
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ○ %s (exists, skipped)\n", model.Name)
|
|
}
|
|
s.summary.Models.Skipped++
|
|
continue
|
|
}
|
|
}
|
|
|
|
_, err := s.client.CreateModel(model)
|
|
if err != nil {
|
|
if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() {
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ○ %s (exists, skipped)\n", model.Name)
|
|
}
|
|
s.summary.Models.Skipped++
|
|
continue
|
|
}
|
|
return fmt.Errorf("create model %s: %w", model.Name, err)
|
|
}
|
|
|
|
if s.cfg.Verbose || s.cfg.DryRun {
|
|
fmt.Printf(" ✓ %s (created)\n", model.Name)
|
|
}
|
|
s.summary.Models.Created++
|
|
}
|
|
|
|
if !s.cfg.Verbose && !s.cfg.DryRun {
|
|
fmt.Printf(" ✓ %d models created\n", s.summary.Models.Created)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Seeder) seedBindings() error {
|
|
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
|
|
bindings := gen.GenerateBindings(seededNamespaces, seededProviderGroups, s.profile.Bindings)
|
|
|
|
// Get existing bindings
|
|
existing, err := s.client.ListBindings()
|
|
if err != nil {
|
|
if s.cfg.DryRun {
|
|
existing = []BindingResponse{}
|
|
fmt.Printf(" [DRY-RUN] Unable to list existing bindings, proceeding anyway\n")
|
|
} else {
|
|
return fmt.Errorf("list bindings: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create a map of existing bindings by composite key
|
|
existingMap := make(map[string]BindingResponse)
|
|
for _, b := range existing {
|
|
key := fmt.Sprintf("%s:%s:%d", b.Namespace, b.PublicModel, b.GroupID)
|
|
existingMap[key] = b
|
|
}
|
|
|
|
for _, binding := range bindings {
|
|
key := fmt.Sprintf("%s:%s:%d", binding.Namespace, binding.PublicModel, binding.GroupID)
|
|
if existingBinding, exists := existingMap[key]; exists {
|
|
// Only reset bindings in seeder namespaces
|
|
if s.cfg.Reset && s.matchesSeederPrefix(binding.Namespace) {
|
|
if err := s.client.DeleteBinding(existingBinding.ID); err != nil {
|
|
return fmt.Errorf("delete binding: %w", err)
|
|
}
|
|
} else {
|
|
s.summary.Bindings.Skipped++
|
|
continue
|
|
}
|
|
}
|
|
|
|
_, err := s.client.CreateBinding(binding)
|
|
if err != nil {
|
|
if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() {
|
|
s.summary.Bindings.Skipped++
|
|
continue
|
|
}
|
|
return fmt.Errorf("create binding: %w", err)
|
|
}
|
|
s.summary.Bindings.Created++
|
|
}
|
|
|
|
if s.cfg.Verbose || s.cfg.DryRun {
|
|
fmt.Printf(" ✓ %d bindings created\n", s.summary.Bindings.Created)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Seeder) seedMasters() error {
|
|
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
|
|
masters := gen.GenerateMasters(s.profile.Masters)
|
|
|
|
// Get existing masters
|
|
existing, err := s.client.ListMasters()
|
|
if err != nil {
|
|
if s.cfg.DryRun {
|
|
existing = []MasterResponse{}
|
|
fmt.Printf(" [DRY-RUN] Unable to list existing masters, proceeding anyway\n")
|
|
} else {
|
|
return fmt.Errorf("list masters: %w", err)
|
|
}
|
|
}
|
|
|
|
existingMap := make(map[string]MasterResponse)
|
|
for _, m := range existing {
|
|
existingMap[m.Name] = m
|
|
}
|
|
|
|
seededMasters = make([]MasterResponse, 0, len(masters))
|
|
|
|
for _, master := range masters {
|
|
if existingMaster, exists := existingMap[master.Name]; exists {
|
|
// Reset masters with seeder naming pattern
|
|
if s.cfg.Reset && s.matchesSeederPrefix(master.Name) {
|
|
if err := s.client.DeleteMaster(existingMaster.ID); err != nil {
|
|
return fmt.Errorf("delete master %s: %w", master.Name, err)
|
|
}
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ✗ %s (deleted for reset)\n", master.Name)
|
|
}
|
|
} else {
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ○ %s (exists, skipped)\n", master.Name)
|
|
}
|
|
s.summary.Masters.Skipped++
|
|
seededMasters = append(seededMasters, existingMaster)
|
|
continue
|
|
}
|
|
}
|
|
|
|
created, err := s.client.CreateMaster(master)
|
|
if err != nil {
|
|
if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() {
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ○ %s (exists, skipped)\n", master.Name)
|
|
}
|
|
s.summary.Masters.Skipped++
|
|
continue
|
|
}
|
|
return fmt.Errorf("create master %s: %w", master.Name, err)
|
|
}
|
|
|
|
if s.cfg.Verbose || s.cfg.DryRun {
|
|
fmt.Printf(" ✓ %s (created)\n", master.Name)
|
|
}
|
|
s.summary.Masters.Created++
|
|
if created != nil {
|
|
seededMasters = append(seededMasters, *created)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Seeder) seedKeys() error {
|
|
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
|
|
|
|
seededKeys = make(map[uint][]KeyResponse)
|
|
|
|
for _, master := range seededMasters {
|
|
// Get existing keys for this master for idempotency
|
|
existingKeys, err := s.client.ListKeys(master.ID)
|
|
if err != nil {
|
|
if s.cfg.DryRun {
|
|
existingKeys = []KeyResponse{}
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" [DRY-RUN] Unable to list existing keys for master %s\n", master.Name)
|
|
}
|
|
} else {
|
|
return fmt.Errorf("list keys for master %s: %w", master.Name, err)
|
|
}
|
|
}
|
|
|
|
activeKeys := filterActiveKeys(existingKeys)
|
|
|
|
// For reset mode, delete existing keys that belong to seeder masters
|
|
if s.cfg.Reset && s.matchesSeederPrefix(master.Name) && len(existingKeys) > 0 {
|
|
for _, key := range existingKeys {
|
|
if err := s.client.DeleteKey(master.ID, key.ID); err != nil {
|
|
return fmt.Errorf("delete key %d for master %s: %w", key.ID, master.Name, err)
|
|
}
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ✗ key %d (deleted for reset)\n", key.ID)
|
|
}
|
|
}
|
|
existingKeys = []KeyResponse{}
|
|
activeKeys = []KeyResponse{}
|
|
}
|
|
|
|
// Check if we already have enough keys (idempotency)
|
|
targetCount := s.profile.KeysPerMaster
|
|
if len(activeKeys) >= targetCount {
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ○ %s: %d keys (exists, skipped)\n", master.Name, len(activeKeys))
|
|
}
|
|
s.summary.Keys.Skipped += len(activeKeys)
|
|
seededKeys[master.ID] = activeKeys[:targetCount]
|
|
continue
|
|
}
|
|
|
|
// Create only the missing keys
|
|
s.summary.Keys.Skipped += len(activeKeys)
|
|
keysToCreate := targetCount - len(activeKeys)
|
|
keys := gen.GenerateKeys(keysToCreate)
|
|
|
|
masterKeys := make([]KeyResponse, 0, targetCount)
|
|
masterKeys = append(masterKeys, activeKeys...)
|
|
createdCount := 0
|
|
|
|
for _, key := range keys {
|
|
created, err := s.client.CreateKey(master.ID, key)
|
|
if err != nil {
|
|
if apiErr, ok := err.(*APIError); ok && apiErr.IsConflict() {
|
|
s.summary.Keys.Skipped++
|
|
continue
|
|
}
|
|
return fmt.Errorf("create key for master %s: %w", master.Name, err)
|
|
}
|
|
s.summary.Keys.Created++
|
|
createdCount++
|
|
if created != nil {
|
|
masterKeys = append(masterKeys, *created)
|
|
}
|
|
}
|
|
|
|
seededKeys[master.ID] = masterKeys
|
|
|
|
if s.cfg.Verbose || s.cfg.DryRun {
|
|
fmt.Printf(" ✓ %s: %d keys created, %d existing\n", master.Name, createdCount, len(existingKeys))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Seeder) seedUsageSamples() error {
|
|
gen := NewGenerator(s.rng, s.seederTag, s.cfg.Profile)
|
|
|
|
ctx := UsageSampleContext{
|
|
Masters: seededMasters,
|
|
Keys: seededKeys,
|
|
Groups: seededProviderGroups,
|
|
Providers: seededProviders,
|
|
UsageDays: s.cfg.UsageDays,
|
|
}
|
|
|
|
logs := gen.GenerateUsageSamples(ctx)
|
|
|
|
if len(logs) == 0 {
|
|
fmt.Printf(" ○ No masters or groups to generate samples for\n")
|
|
return nil
|
|
}
|
|
|
|
// Note about timestamps
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ⚠ Note: Usage samples will have current timestamp (API does not support historical timestamps)\n")
|
|
}
|
|
|
|
// Batch insert logs
|
|
batchSize := 100
|
|
for i := 0; i < len(logs); i += batchSize {
|
|
end := i + batchSize
|
|
if end > len(logs) {
|
|
end = len(logs)
|
|
}
|
|
batch := logs[i:end]
|
|
|
|
if err := s.client.CreateLogsBatch(batch); err != nil {
|
|
return fmt.Errorf("create logs batch: %w", err)
|
|
}
|
|
}
|
|
|
|
s.summary.UsageSamples = len(logs)
|
|
|
|
if s.cfg.Verbose || s.cfg.DryRun {
|
|
fmt.Printf(" ✓ %d days of data generated (%d log entries)\n", s.cfg.UsageDays, len(logs))
|
|
}
|
|
|
|
return nil
|
|
}
|