Files
kforge/cmd/generate.go
T
nate.lubitz 4ee1b9e13c
Publish Action Image / build (push) Successful in 1m8s
more updates
2026-06-29 15:14:55 +10:00

215 lines
6.0 KiB
Go

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,
)
}