145 lines
5.1 KiB
Go
145 lines
5.1 KiB
Go
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 <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 <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
|
|
}
|