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