mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Trim whitespace in provider model lists, format provider names as `group#keyID` to match DP logs, and skip existing API keys during seeding (deleting on reset) to keep runs idempotent and summaries accurate
556 lines
16 KiB
Go
556 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 (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)
|
|
}
|
|
}
|
|
|
|
// 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{}
|
|
}
|
|
|
|
// Check if we already have enough keys (idempotency)
|
|
targetCount := s.profile.KeysPerMaster
|
|
if len(existingKeys) >= targetCount {
|
|
if s.cfg.Verbose {
|
|
fmt.Printf(" ○ %s: %d keys (exists, skipped)\n", master.Name, len(existingKeys))
|
|
}
|
|
s.summary.Keys.Skipped += targetCount
|
|
seededKeys[master.ID] = existingKeys[:targetCount]
|
|
continue
|
|
}
|
|
|
|
// Create only the missing keys
|
|
keysToCreate := targetCount - len(existingKeys)
|
|
keys := gen.GenerateKeys(keysToCreate)
|
|
|
|
masterKeys := make([]KeyResponse, 0, targetCount)
|
|
masterKeys = append(masterKeys, existingKeys...)
|
|
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
|
|
}
|