package cmd import ( "fmt" "os" "path/filepath" "strings" "kforge/internal/config" "kforge/internal/generator" "github.com/spf13/cobra" ) var ( generateEnvs []string generateOutput string generateDry bool generatePRNumber string ) var generateCmd = &cobra.Command{ Use: "generate", Short: "Generate Kubernetes manifests from kforge.yml", Long: `Reads kforge.yml, resolves each requested environment, and writes flat Kubernetes manifest files to the output directory. If no --env flags are given, manifests are generated for all environments defined in kforge.yml. Use --pr-number to generate manifests for a PR preview environment. A Namespace manifest is prepended so kubectl apply creates the namespace automatically. Examples: kforge generate kforge generate --env staging kforge generate --env staging --env production kforge generate --env production --output .kube/ kforge generate --dry-run kforge generate --pr-number 42`, RunE: runGenerate, } func init() { generateCmd.Flags().StringArrayVarP(&generateEnvs, "env", "e", nil, "Environment(s) to generate (default: all)") generateCmd.Flags().StringVarP(&generateOutput, "output", "o", ".kforge-out", "Directory to write generated manifests into") generateCmd.Flags().BoolVar(&generateDry, "dry-run", false, "Print manifests to stdout instead of writing files") generateCmd.Flags().StringVar(&generatePRNumber, "pr-number", "", "PR number — synthesizes and generates a preview environment") rootCmd.AddCommand(generateCmd) } func runGenerate(cmd *cobra.Command, args []string) error { cfg, err := loadConfig() if err != nil { return err } // Preview mode: synthesize the preview environment. if generatePRNumber != "" { env, err := config.SynthesizePreviewEnvironment(cfg, generatePRNumber) if err != nil { return fmt.Errorf("synthesizing preview environment: %w", err) } return generateForPreview(cfg, &env, generatePRNumber) } envKeys := generateEnvs if len(envKeys) == 0 { envKeys = config.EnvironmentKeys(cfg) } if !generateDry { if err := os.MkdirAll(generateOutput, 0o755); err != nil { return fmt.Errorf("creating output directory: %w", err) } } for _, envKey := range envKeys { if err := generateForEnv(cfg, envKey); err != nil { return fmt.Errorf("env %q: %w", envKey, err) } } return nil } func generateForEnv(cfg *config.KforgeConfig, envKey string) error { env, err := config.ResolveEnvironment(cfg, envKey) if err != nil { return err } coreYAML, infraManifests, err := buildManifests(cfg, &env) if err != nil { return err } if generateDry { printDryRun(envKey, "core", coreYAML) for _, m := range infraManifests { printDryRun(envKey, m.Name, m.Content) } return nil } if err := os.MkdirAll(generateOutput, 0o755); err != nil { return fmt.Errorf("creating output directory: %w", err) } coreFile := filepath.Join(generateOutput, envKey+"-core.yaml") if err := os.WriteFile(coreFile, []byte(coreYAML), 0o644); err != nil { return fmt.Errorf("writing core manifest: %w", err) } fmt.Printf(" ✓ %s\n", coreFile) for _, m := range infraManifests { infraFile := filepath.Join(generateOutput, envKey+"-infra-"+m.Name+".yaml") if err := os.WriteFile(infraFile, []byte(m.Content), 0o644); err != nil { return fmt.Errorf("writing %s manifest: %w", m.Name, err) } fmt.Printf(" ✓ %s\n", infraFile) } return nil } // generateForPreview generates manifests for a synthesized preview // environment, prepending a Namespace manifest so kubectl apply // creates the namespace in a single pass. func generateForPreview(cfg *config.KforgeConfig, env *config.ResolvedEnvironment, prNumber string) error { coreYAML, infraManifests, err := buildManifests(cfg, env) if err != nil { return err } // Prepend a Namespace manifest so kubectl apply is self-contained. nsYAML := generator.Namespace(env.Namespace, map[string]string{ "managed-by": "kforge", "kforge/preview": "true", "kforge/pr": prNumber, }) coreYAML = nsYAML + generator.Separator + coreYAML envKey := "preview-" + prNumber if generateDry { printDryRun(envKey, "core", coreYAML) for _, m := range infraManifests { printDryRun(envKey, m.Name, m.Content) } return nil } if err := os.MkdirAll(generateOutput, 0o755); err != nil { return fmt.Errorf("creating output directory: %w", err) } coreFile := filepath.Join(generateOutput, envKey+"-core.yaml") if err := os.WriteFile(coreFile, []byte(coreYAML), 0o644); err != nil { return fmt.Errorf("writing core manifest: %w", err) } fmt.Printf(" ✓ %s\n", coreFile) for _, m := range infraManifests { infraFile := filepath.Join(generateOutput, envKey+"-infra-"+m.Name+".yaml") if err := os.WriteFile(infraFile, []byte(m.Content), 0o644); err != nil { return fmt.Errorf("writing %s manifest: %w", m.Name, err) } fmt.Printf(" ✓ %s\n", infraFile) } return nil } // buildManifests generates core + infra manifests for a resolved environment, // injecting infra env vars into the deployment. func buildManifests(cfg *config.KforgeConfig, env *config.ResolvedEnvironment) (string, []generator.InfraManifest, error) { coreYAML, err := generator.GenerateAll(env, cfg) if err != nil { return "", nil, fmt.Errorf("generating core manifests: %w", err) } infraManifests, err := generator.GenerateInfrastructure(env) if err != nil { return "", nil, fmt.Errorf("generating infrastructure manifests: %w", err) } var infraEnvVars []config.EnvVarConfig for _, m := range infraManifests { infraEnvVars = append(infraEnvVars, m.EnvVars...) } if len(infraEnvVars) > 0 { env.EnvVars = config.MergeEnvVars(env.EnvVars, infraEnvVars) coreYAML, err = generator.GenerateAll(env, cfg) if err != nil { return "", nil, fmt.Errorf("re-generating core manifests with infra vars: %w", err) } } return coreYAML, infraManifests, nil } func printDryRun(envKey, name, content string) { fmt.Printf("\n%s\n# --- %s / %s ---\n%s\n", generator.Separator, strings.ToUpper(envKey), name, content, ) }