mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(seeder): add control plane data seeder
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.
This commit is contained in:
287
cmd/seeder/main.go
Normal file
287
cmd/seeder/main.go
Normal file
@@ -0,0 +1,287 @@
|
||||
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
|
||||
Reference in New Issue
Block a user