package cmd import ( "fmt" "github.com/Pingu-Studio/MaidnCLI/internal/bootstrap" "github.com/Pingu-Studio/MaidnCLI/internal/config" "github.com/Pingu-Studio/MaidnCLI/internal/forgejo" "github.com/Pingu-Studio/MaidnCLI/internal/openbao" "github.com/Pingu-Studio/MaidnCLI/internal/ui" "github.com/spf13/cobra" ) var bootstrapConfigPath string var bootstrapOutputPath string var bootstrapWorkspaceDir string var bootstrapMode string var bootstrapYes bool var bootstrapPromptDemocraticCSI bool var bootstrapPromptOperationalSecrets bool var bootstrapInitializeOpenBaoRecovery bool var bootstrapInitializeOpenBao bool var bootstrapCreateForgejoRegistryToken bool var bootstrapRegisterWebhook bool var bootstrapRotateWebhookAuthorization bool var bootstrapMergeBootstrapPR bool var bootstrapManageNetworkBridges bool var bootstrapEnableDelivery bool var bootstrapDestroyDemocraticCSIStorage bool var upsertOperationalSecret = bootstrap.UpsertOperationalSecret var bootstrapCmd = &cobra.Command{ Use: "bootstrap", Short: "Bootstrap Talos and Flux from config or an interactive wizard.", RunE: runBootstrap, } func init() { rootCmd.AddCommand(bootstrapCmd) bootstrapCmd.Flags().StringVar(&bootstrapConfigPath, "config", "", "Path to bootstrap config YAML") bootstrapCmd.Flags().StringVar(&bootstrapOutputPath, "out", "maidn-bootstrap.yaml", "Path to save generated config") bootstrapCmd.Flags().StringVar(&bootstrapWorkspaceDir, "workspace-dir", "", "Override workspace directory for this bootstrap run") bootstrapCmd.Flags().StringVar(&bootstrapMode, "mode", string(bootstrap.Reconcile), "Lifecycle mode: reconcile or rebuild") bootstrapCmd.Flags().BoolVar(&bootstrapYes, "yes", false, "Confirm destructive rebuild") bootstrapCmd.Flags().BoolVar(&bootstrapPromptDemocraticCSI, "prompt-democratic-csi", false, "Prompt for and save Democratic CSI settings in --config") bootstrapCmd.Flags().BoolVar(&bootstrapPromptOperationalSecrets, "prompt-operational-secrets", false, "Prompt for and encrypt operational secrets for --config") bootstrapCmd.Flags().BoolVar(&bootstrapInitializeOpenBaoRecovery, "initialize-openbao-recovery", false, "Create and save a separate OpenBao recovery age identity for --config") bootstrapCmd.Flags().BoolVar(&bootstrapInitializeOpenBao, "initialize-openbao", false, "Initialize OpenBao and seed encrypted operational secrets for --config") bootstrapCmd.Flags().BoolVar(&bootstrapCreateForgejoRegistryToken, "create-forgejo-registry-token", false, "Create a least-privilege Forgejo package registry token and seed it through OpenBao") bootstrapCmd.Flags().BoolVar(&bootstrapRegisterWebhook, "register-webhook", false, "Seed OpenBao secrets and register the Forgejo webhook") bootstrapCmd.Flags().BoolVar(&bootstrapRotateWebhookAuthorization, "rotate-webhook-authorization", false, "Replace the Forgejo webhook authorization and reconcile it through OpenBao") bootstrapCmd.Flags().BoolVar(&bootstrapMergeBootstrapPR, "merge-bootstrap-pr", false, "Merge the generated Flux repository migration PR before bootstrapping") bootstrapCmd.Flags().BoolVar(&bootstrapManageNetworkBridges, "manage-network-bridges", false, "Persist Terraform management for existing Talos network bridges") bootstrapCmd.Flags().BoolVar(&bootstrapEnableDelivery, "enable-delivery", false, "Resolve delivery defaults and reconcile the configured app delivery source") bootstrapCmd.Flags().BoolVar(&bootstrapDestroyDemocraticCSIStorage, "destroy-democratic-csi-storage", false, "Delete only TrueNAS datasets under this cluster's configured Democratic CSI parent during rebuild") } func runBootstrap(cmd *cobra.Command, args []string) error { var cfg config.Config var err error if bootstrapCreateForgejoRegistryToken { if bootstrapConfigPath == "" { return fmt.Errorf("--create-forgejo-registry-token requires --config") } cfg, err = config.Load(bootstrapConfigPath) if err != nil { return err } cfg, err = config.ResolveDelivery(cfg) if err != nil { return err } if err := bootstrap.EnsureTemplateRevisions(cfg); err != nil { return err } return createForgejoRegistryToken(cfg) } if bootstrapRotateWebhookAuthorization { if bootstrapConfigPath == "" { return fmt.Errorf("--rotate-webhook-authorization requires --config") } cfg, err = config.Load(bootstrapConfigPath) if err != nil { return err } cfg, err = config.ResolveDelivery(cfg) if err != nil { return err } authorization, err := bootstrap.NewWebhookAuthorization() if err != nil { return fmt.Errorf("generate Forgejo webhook authorization: %w", err) } if err := bootstrap.UpsertOperationalSecret(cfg.SOPS.OperationalSecretsPath, cfg.SOPS.AgeKeyPath, "cicd/forgejo-webhook", "authorization", authorization); err != nil { return fmt.Errorf("save Forgejo webhook authorization: %w", err) } return bootstrap.Runner{Config: cfg, RegisterWebhook: true}.Run() } if bootstrapInitializeOpenBao { if bootstrapConfigPath == "" { return fmt.Errorf("--initialize-openbao requires --config") } cfg, err = config.Load(bootstrapConfigPath) if err != nil { return err } if err := bootstrap.EnsureTemplateRevisions(cfg); err != nil { return err } return bootstrap.InitializeOpenBao(cfg) } if bootstrapMergeBootstrapPR { if bootstrapConfigPath == "" { return fmt.Errorf("--merge-bootstrap-pr requires --config") } cfg, err = config.Load(bootstrapConfigPath) if err != nil { return err } if err := bootstrap.EnsureTemplateRevisions(cfg); err != nil { return err } manager := forgejo.NewRepoManager(cfg.Git.BaseURL, cfg.Git.Token, cfg.Git.Owner, cfg.Git.Username, "", "", cfg.Flux.Branch, "") for _, repository := range []string{cfg.Flux.ManifestsRepo, cfg.Flux.RepoName} { open, err := manager.HasOpenPullRequest(repository, "maidn/bootstrap-"+cfg.ClusterID) if err != nil { return err } if open { if err := manager.MergePullRequest(repository, "maidn/bootstrap-"+cfg.ClusterID); err != nil { return err } } } } if bootstrapConfigPath != "" { if bootstrapPromptDemocraticCSI || bootstrapPromptOperationalSecrets || bootstrapInitializeOpenBaoRecovery || bootstrapManageNetworkBridges { cfg, err = config.LoadRaw(bootstrapConfigPath) if err == nil { if bootstrapPromptDemocraticCSI { cfg = ui.PromptDemocraticCSI(cfg) } if bootstrapManageNetworkBridges { cfg.Talos.Cluster.ManageNetworkBridges = true } cfg, err = config.Resolve(cfg) } if err == nil && bootstrapPromptOperationalSecrets { cfg, err = config.ResolveDelivery(cfg) } if err == nil && bootstrapPromptOperationalSecrets { var secrets map[string]map[string]string secrets, err = ui.PromptOperationalSecrets(cfg) if err == nil { var authorization string authorization, err = bootstrap.NewWebhookAuthorization() if err == nil { secrets["cicd/forgejo-webhook"] = map[string]string{"authorization": authorization} err = bootstrap.WriteOperationalSecrets(cfg.SOPS.OperationalSecretsPath, cfg.SOPS.AgeKeyPath, secrets) } } } if err == nil && bootstrapInitializeOpenBaoRecovery { var recipient string recipient, err = openbao.EnsureRecoveryIdentity(cfg.SOPS.RecoveryIdentityPath) if err == nil { cfg.SOPS.RecoveryRecipient = recipient } } if err == nil { err = config.Save(bootstrapConfigPath, cfg) } } else { cfg, err = config.Load(bootstrapConfigPath) } } else { cfg, err = ui.RunBootstrapWizard(config.Config{}) if err == nil { cfg, err = config.Resolve(cfg) } if err == nil { err = config.Save(bootstrapOutputPath, cfg) if err == nil { fmt.Printf("[INFO] Saved config to %s\n", bootstrapOutputPath) } } } if err != nil { return err } if bootstrapWorkspaceDir != "" { cfg.WorkspaceDir = bootstrapWorkspaceDir } if bootstrapRegisterWebhook { cfg, err = config.ResolveDelivery(cfg) if err != nil { return err } if err := seedForgejoOperationalCredentials(cfg); err != nil { return err } } runner := bootstrap.Runner{Config: cfg, Mode: bootstrap.Mode(bootstrapMode), ConfirmRebuild: bootstrapYes, RegisterWebhook: bootstrapRegisterWebhook, EnableDelivery: bootstrapEnableDelivery, DestroyDemocraticCSIStorage: bootstrapDestroyDemocraticCSIStorage} return runner.Run() } func seedForgejoOperationalCredentials(cfg config.Config) error { if err := upsertOperationalSecret(cfg.SOPS.OperationalSecretsPath, cfg.SOPS.AgeKeyPath, "cicd/forgejo", "username", cfg.Git.Username); err != nil { return fmt.Errorf("save Forgejo username for webhook registration: %w", err) } if err := upsertOperationalSecret(cfg.SOPS.OperationalSecretsPath, cfg.SOPS.AgeKeyPath, "cicd/forgejo", "token", cfg.Git.Token); err != nil { return fmt.Errorf("save Forgejo token for webhook registration: %w", err) } return nil } func createForgejoRegistryToken(cfg config.Config) error { if _, err := bootstrap.ReadOperationalSecrets(cfg.SOPS.OperationalSecretsPath, cfg.SOPS.AgeKeyPath); err != nil { return err } password, otp, name, err := ui.PromptForgejoRegistryToken() if err != nil { return err } token, err := forgejo.CreateRegistryToken(cfg.Git.BaseURL, cfg.Git.Username, password, otp, name) if err != nil { return fmt.Errorf("create Forgejo registry token: %w", err) } dockerConfig, err := bootstrap.ForgejoRegistryDockerConfig(cfg.Delivery.ImageRepository, cfg.Git.Username, token) if err != nil { return err } if err := bootstrap.UpsertOperationalSecret(cfg.SOPS.OperationalSecretsPath, cfg.SOPS.AgeKeyPath, "cicd/forgejo-registry", "dockerconfigjson", dockerConfig); err != nil { return fmt.Errorf("Forgejo registry token was created but could not be saved; revoke the new token in Forgejo and retry: %w", err) } if err := bootstrap.InitializeOpenBao(cfg); err != nil { return fmt.Errorf("Forgejo registry token was saved to encrypted operational secrets but OpenBao seeding failed; rerun bootstrap --config %q --initialize-openbao: %w", bootstrapConfigPath, err) } return nil }