package cmd import ( "context" "errors" "fmt" "strings" "github.com/Pingu-Studio/MaidnCLI/internal/bootstrap" "github.com/Pingu-Studio/MaidnCLI/internal/cloudflare" "github.com/Pingu-Studio/MaidnCLI/internal/config" "github.com/spf13/cobra" ) var cloudflareTunnelConfigPath string var cloudflareTunnelHostname string var cloudflareTunnelService string var cloudflareTunnelZoneID string var loadCloudflareTunnelConfig = config.Load var readCloudflareTunnelSecrets = bootstrap.ReadOperationalSecrets var writeCloudflareTunnelSecrets = bootstrap.WriteOperationalSecrets var newCloudflareRouteClient = func(token string) cloudflare.API { return cloudflare.NewClient(token) } var initializeCloudflareTunnelOpenBao = bootstrap.InitializeOpenBao var cloudflareTunnelCmd = &cobra.Command{ Use: "cloudflare-tunnel", Short: "Manage CLI-created Cloudflare Tunnel routes.", } var cloudflareTunnelRouteCmd = &cobra.Command{Use: "route"} var cloudflareTunnelRouteAddCmd = &cobra.Command{ Use: "add", Short: "Add a proxied public route to the managed tunnel.", RunE: func(cmd *cobra.Command, _ []string) error { return runCloudflareTunnelRoute(cmd, true) }, } var cloudflareTunnelRouteRemoveCmd = &cobra.Command{ Use: "remove", Short: "Remove a proxied public route from the managed tunnel.", RunE: func(cmd *cobra.Command, _ []string) error { return runCloudflareTunnelRoute(cmd, false) }, } func init() { rootCmd.AddCommand(cloudflareTunnelCmd) cloudflareTunnelCmd.AddCommand(cloudflareTunnelRouteCmd) cloudflareTunnelRouteCmd.AddCommand(cloudflareTunnelRouteAddCmd, cloudflareTunnelRouteRemoveCmd) cloudflareTunnelCmd.PersistentFlags().StringVar(&cloudflareTunnelConfigPath, "config", "", "Path to bootstrap config YAML") for _, command := range []*cobra.Command{cloudflareTunnelRouteAddCmd, cloudflareTunnelRouteRemoveCmd} { command.Flags().StringVar(&cloudflareTunnelHostname, "hostname", "", "Public DNS hostname") command.Flags().StringVar(&cloudflareTunnelService, "service", "", "Upstream http or https service URL") command.Flags().StringVar(&cloudflareTunnelZoneID, "zone-id", "", "Cloudflare zone ID") _ = command.MarkFlagRequired("hostname") _ = command.MarkFlagRequired("service") _ = command.MarkFlagRequired("zone-id") } } func runCloudflareTunnelRoute(cmd *cobra.Command, add bool) error { if cloudflareTunnelConfigPath == "" { return errors.New("--config is required") } if strings.TrimSpace(cloudflareTunnelZoneID) == "" { return errors.New("--zone-id is required") } route, err := cloudflare.NewRoute(cloudflareTunnelHostname, cloudflareTunnelService) if err != nil { return err } cfg, err := loadCloudflareTunnelConfig(cloudflareTunnelConfigPath) if err != nil { return err } secrets, err := readCloudflareTunnelSecrets(cfg.SOPS.OperationalSecretsPath, cfg.SOPS.AgeKeyPath) if err != nil { return fmt.Errorf("read encrypted Cloudflare operational state: %w", err) } token := strings.TrimSpace(secrets["platform/cloudflare"]["api-token"]) if token == "" { return errors.New("encrypted operational secrets requires platform/cloudflare.api-token") } stored, present, err := cloudflare.ParseStoredTunnel(secrets["platform/cloudflare-tunnel"]) if err != nil { return err } if !present { return errors.New("Cloudflare tunnel credentials and config are not generated; run bootstrap reconcile first") } ctx := context.Background() if cmd != nil && cmd.Context() != nil { ctx = cmd.Context() } client := newCloudflareRouteClient(token) if add { changed, err := stored.Config.AddRoute(route) if err != nil { return err } if err := client.EnsureCNAME(ctx, strings.TrimSpace(cloudflareTunnelZoneID), route.Hostname, stored.Credentials.TunnelID); err != nil { return fmt.Errorf("ensure Cloudflare route DNS record: %w", err) } if !changed { return nil } values, err := stored.Values() if err != nil { return err } secrets["platform/cloudflare-tunnel"] = values if err := writeCloudflareTunnelSecrets(cfg.SOPS.OperationalSecretsPath, cfg.SOPS.AgeKeyPath, secrets); err != nil { return errors.New("save encrypted Cloudflare tunnel route") } if err := initializeCloudflareTunnelOpenBao(cfg); err != nil { return errors.New("Cloudflare tunnel route was saved but OpenBao could not be seeded; rerun bootstrap --config --initialize-openbao") } return nil } changed, err := stored.Config.RemoveRoute(route) if err != nil { return err } if changed { values, err := stored.Values() if err != nil { return err } secrets["platform/cloudflare-tunnel"] = values if err := writeCloudflareTunnelSecrets(cfg.SOPS.OperationalSecretsPath, cfg.SOPS.AgeKeyPath, secrets); err != nil { return errors.New("save encrypted Cloudflare tunnel route") } if err := initializeCloudflareTunnelOpenBao(cfg); err != nil { return errors.New("Cloudflare tunnel route was saved but OpenBao could not be seeded; rerun bootstrap --config --initialize-openbao") } } if err := client.DeleteCNAME(ctx, strings.TrimSpace(cloudflareTunnelZoneID), route.Hostname, stored.Credentials.TunnelID); err != nil { return fmt.Errorf("delete Cloudflare route DNS record: %w", err) } return nil }