mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Introduce a `cmd/seeder` CLI to generate deterministic demo datasets and seed them into the Control Plane via admin endpoints, supporting reset, dry-run, profiles, and usage sample generation. Add Cobra/Viper dependencies to support the new command.
288 lines
8.1 KiB
Go
288 lines
8.1 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"math/rand"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/spf13/cobra"
|
||
"github.com/spf13/viper"
|
||
)
|
||
|
||
// Config holds seeder configuration
|
||
type Config struct {
|
||
BaseURL string
|
||
AdminToken string
|
||
Profile string
|
||
Seed int64
|
||
UsageDays int
|
||
Env string
|
||
Reset bool
|
||
DryRun bool
|
||
Force bool
|
||
Verbose bool
|
||
}
|
||
|
||
// ProfileConfig defines entity counts for a profile
|
||
type ProfileConfig struct {
|
||
Namespaces int
|
||
ProviderGroups int
|
||
ProvidersPerGroup int
|
||
Models int
|
||
Bindings int
|
||
Masters int
|
||
KeysPerMaster int
|
||
UsageDays int
|
||
}
|
||
|
||
var profiles = map[string]ProfileConfig{
|
||
"demo": {
|
||
Namespaces: 2,
|
||
ProviderGroups: 2,
|
||
ProvidersPerGroup: 2,
|
||
Models: 5,
|
||
Bindings: 4,
|
||
Masters: 2,
|
||
KeysPerMaster: 3,
|
||
UsageDays: 7,
|
||
},
|
||
"load": {
|
||
Namespaces: 5,
|
||
ProviderGroups: 10,
|
||
ProvidersPerGroup: 3,
|
||
Models: 30,
|
||
Bindings: 50,
|
||
Masters: 50,
|
||
KeysPerMaster: 2,
|
||
UsageDays: 30,
|
||
},
|
||
"dev": {
|
||
Namespaces: 3,
|
||
ProviderGroups: 5,
|
||
ProvidersPerGroup: 3,
|
||
Models: 20,
|
||
Bindings: 15,
|
||
Masters: 10,
|
||
KeysPerMaster: 2,
|
||
UsageDays: 14,
|
||
},
|
||
}
|
||
|
||
func main() {
|
||
var cfg Config
|
||
|
||
rootCmd := &cobra.Command{
|
||
Use: "seeder",
|
||
Short: "EZ-API Demo Data Seeder",
|
||
Long: `A CLI tool to seed demo data into EZ-API Control Plane.
|
||
|
||
Creates namespaces, provider groups, providers, models, bindings,
|
||
masters, API keys, and usage samples for frontend development and demos.`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
return run(cfg)
|
||
},
|
||
}
|
||
|
||
// Bind flags
|
||
rootCmd.Flags().StringVar(&cfg.BaseURL, "base-url", "http://localhost:8080", "Control Plane API base URL")
|
||
rootCmd.Flags().StringVar(&cfg.AdminToken, "admin-token", "", "Admin authentication token (required)")
|
||
rootCmd.Flags().StringVar(&cfg.Profile, "profile", "demo", "Dataset profile: demo, load, dev")
|
||
rootCmd.Flags().Int64Var(&cfg.Seed, "seed", 0, "Fixed random seed for reproducibility (0 = random)")
|
||
rootCmd.Flags().IntVar(&cfg.UsageDays, "usage-days", 0, "Time window for usage samples (0 = profile default)")
|
||
rootCmd.Flags().StringVar(&cfg.Env, "env", "dev", "Environment: dev, test, staging, production")
|
||
rootCmd.Flags().BoolVar(&cfg.Reset, "reset", false, "Remove previous seeder data before recreating")
|
||
rootCmd.Flags().BoolVar(&cfg.DryRun, "dry-run", false, "Print actions without executing write API calls")
|
||
rootCmd.Flags().BoolVar(&cfg.Force, "force", false, "Allow running against production")
|
||
rootCmd.Flags().BoolVar(&cfg.Verbose, "verbose", false, "Print detailed progress")
|
||
|
||
// Bind environment variables
|
||
viper.SetEnvPrefix("SEEDER")
|
||
viper.AutomaticEnv()
|
||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||
|
||
// Override from env if not set via flag
|
||
rootCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
|
||
if !cmd.Flags().Changed("base-url") && viper.IsSet("BASE_URL") {
|
||
cfg.BaseURL = viper.GetString("BASE_URL")
|
||
}
|
||
if !cmd.Flags().Changed("admin-token") && viper.IsSet("ADMIN_TOKEN") {
|
||
cfg.AdminToken = viper.GetString("ADMIN_TOKEN")
|
||
}
|
||
if !cmd.Flags().Changed("profile") && viper.IsSet("PROFILE") {
|
||
cfg.Profile = viper.GetString("PROFILE")
|
||
}
|
||
if !cmd.Flags().Changed("seed") && viper.IsSet("SEED") {
|
||
cfg.Seed = viper.GetInt64("SEED")
|
||
}
|
||
if !cmd.Flags().Changed("usage-days") && viper.IsSet("USAGE_DAYS") {
|
||
cfg.UsageDays = viper.GetInt("USAGE_DAYS")
|
||
}
|
||
if !cmd.Flags().Changed("env") && viper.IsSet("ENV") {
|
||
cfg.Env = viper.GetString("ENV")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
if err := rootCmd.Execute(); err != nil {
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
|
||
func run(cfg Config) error {
|
||
// Validate required fields
|
||
if cfg.AdminToken == "" {
|
||
return fmt.Errorf("admin-token is required (use --admin-token or SEEDER_ADMIN_TOKEN)")
|
||
}
|
||
|
||
// Validate profile
|
||
profile, ok := profiles[cfg.Profile]
|
||
if !ok {
|
||
return fmt.Errorf("invalid profile: %s (valid: demo, load, dev)", cfg.Profile)
|
||
}
|
||
|
||
// Environment guard
|
||
if isProduction(cfg.Env) && !cfg.Force {
|
||
return fmt.Errorf("refusing to run against production environment (env=%s). Use --force to override", cfg.Env)
|
||
}
|
||
|
||
// Initialize seed
|
||
if cfg.Seed == 0 {
|
||
cfg.Seed = time.Now().UnixNano()
|
||
}
|
||
|
||
// Use profile default for usage days if not specified
|
||
if cfg.UsageDays == 0 {
|
||
cfg.UsageDays = profile.UsageDays
|
||
}
|
||
|
||
// Create seeder tag
|
||
seederTag := fmt.Sprintf("seeder:%s:seed-%d", cfg.Profile, cfg.Seed)
|
||
|
||
// Print banner
|
||
printBanner(cfg, seederTag)
|
||
|
||
// Warning for production
|
||
if isProduction(cfg.Env) && cfg.Force {
|
||
fmt.Println("\n⚠️ WARNING: Running against PRODUCTION environment!")
|
||
fmt.Println(" Press Ctrl+C within 5 seconds to abort...")
|
||
time.Sleep(5 * time.Second)
|
||
fmt.Println(" Proceeding...")
|
||
}
|
||
|
||
// Create seeder instance
|
||
seeder := NewSeeder(cfg, profile, seederTag)
|
||
|
||
// Run seeding
|
||
return seeder.Run()
|
||
}
|
||
|
||
func isProduction(env string) bool {
|
||
env = strings.ToLower(env)
|
||
return env == "production" || env == "prod"
|
||
}
|
||
|
||
func printBanner(cfg Config, seederTag string) {
|
||
mode := "create"
|
||
if cfg.Reset {
|
||
mode = "reset + create"
|
||
}
|
||
if cfg.DryRun {
|
||
mode += " (dry-run)"
|
||
}
|
||
|
||
fmt.Println()
|
||
fmt.Println("EZ-API Demo Data Seeder")
|
||
fmt.Println("=======================")
|
||
fmt.Printf("Profile: %s\n", cfg.Profile)
|
||
fmt.Printf("Seed: %d\n", cfg.Seed)
|
||
fmt.Printf("Target: %s\n", cfg.BaseURL)
|
||
fmt.Printf("Environment: %s\n", cfg.Env)
|
||
fmt.Printf("Usage Window: %d days\n", cfg.UsageDays)
|
||
fmt.Printf("Mode: %s\n", mode)
|
||
fmt.Printf("Seeder Tag: %s\n", seederTag)
|
||
fmt.Println()
|
||
}
|
||
|
||
// Seeder handles the seeding process
|
||
type Seeder struct {
|
||
cfg Config
|
||
profile ProfileConfig
|
||
seederTag string
|
||
client *Client
|
||
rng *rand.Rand
|
||
summary Summary
|
||
}
|
||
|
||
// Summary tracks created/skipped counts
|
||
type Summary struct {
|
||
Namespaces CountPair
|
||
ProviderGroups CountPair
|
||
Providers CountPair
|
||
Models CountPair
|
||
Bindings CountPair
|
||
Masters CountPair
|
||
Keys CountPair
|
||
UsageSamples int
|
||
}
|
||
|
||
type CountPair struct {
|
||
Created int
|
||
Skipped int
|
||
}
|
||
|
||
// NewSeeder creates a new seeder instance
|
||
func NewSeeder(cfg Config, profile ProfileConfig, seederTag string) *Seeder {
|
||
return &Seeder{
|
||
cfg: cfg,
|
||
profile: profile,
|
||
seederTag: seederTag,
|
||
client: NewClient(cfg.BaseURL, cfg.AdminToken, cfg.DryRun, cfg.Verbose),
|
||
rng: rand.New(rand.NewSource(cfg.Seed)),
|
||
}
|
||
}
|
||
|
||
// Run executes the seeding process
|
||
func (s *Seeder) Run() error {
|
||
steps := []struct {
|
||
name string
|
||
fn func() error
|
||
}{
|
||
{"Creating namespaces", s.seedNamespaces},
|
||
{"Creating provider groups", s.seedProviderGroups},
|
||
{"Creating providers", s.seedProviders},
|
||
{"Creating models", s.seedModels},
|
||
{"Creating bindings", s.seedBindings},
|
||
{"Creating masters", s.seedMasters},
|
||
{"Creating API keys", s.seedKeys},
|
||
{"Generating usage samples", s.seedUsageSamples},
|
||
}
|
||
|
||
for i, step := range steps {
|
||
fmt.Printf("[%d/%d] %s...\n", i+1, len(steps), step.name)
|
||
if err := step.fn(); err != nil {
|
||
return fmt.Errorf("%s: %w", step.name, err)
|
||
}
|
||
}
|
||
|
||
s.printSummary()
|
||
return nil
|
||
}
|
||
|
||
func (s *Seeder) printSummary() {
|
||
fmt.Println("\nDone! Summary:")
|
||
fmt.Printf(" Namespaces: %d created, %d skipped\n", s.summary.Namespaces.Created, s.summary.Namespaces.Skipped)
|
||
fmt.Printf(" Provider Groups: %d created, %d skipped\n", s.summary.ProviderGroups.Created, s.summary.ProviderGroups.Skipped)
|
||
fmt.Printf(" Providers: %d created, %d skipped\n", s.summary.Providers.Created, s.summary.Providers.Skipped)
|
||
fmt.Printf(" Models: %d created, %d skipped\n", s.summary.Models.Created, s.summary.Models.Skipped)
|
||
fmt.Printf(" Bindings: %d created, %d skipped\n", s.summary.Bindings.Created, s.summary.Bindings.Skipped)
|
||
fmt.Printf(" Masters: %d created, %d skipped\n", s.summary.Masters.Created, s.summary.Masters.Skipped)
|
||
fmt.Printf(" API Keys: %d created, %d skipped\n", s.summary.Keys.Created, s.summary.Keys.Skipped)
|
||
fmt.Printf(" Usage Samples: %d entries\n", s.summary.UsageSamples)
|
||
fmt.Printf("\nSeeder tag: %s\n", s.seederTag)
|
||
fmt.Printf("\nDashboard should now show data at:\n %s/admin/dashboard\n", s.cfg.BaseURL)
|
||
}
|
||
|
||
// Seed methods are implemented in seeder.go
|