Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions cmd/obol/tunnel_domain.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,11 +56,16 @@ func tunnelCommand(cfg *config.Config) *cli.Command {
Required: true,
},
tunnelTransportProtocolFlag(),
&cli.BoolFlag{
Name: "overwrite-dns",
Usage: "Replace any existing A/AAAA/CNAME at the hostname (forwards --overwrite-dns to cloudflared)",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
return tunnel.Login(cfg, getUI(cmd), tunnel.LoginOptions{
Hostname: cmd.String("hostname"),
TransportProtocol: cmd.String("transport-protocol"),
OverwriteDNS: cmd.Bool("overwrite-dns"),
})
},
},
Expand DownExpand Up@@ -247,13 +252,15 @@ func tunnelSetupFlags() []cli.Flag {
&cli.BoolFlag{Name: "auto-renew", Usage: "Enable domain auto-renew when registering a domain"},
&cli.StringFlag{Name: "privacy-mode", Usage: "WHOIS privacy mode for registration", Value: "redaction"},
&cli.BoolFlag{Name: "yes", Aliases: []string{"y"}, Usage: "Confirm billable domain registration without prompting"},
&cli.BoolFlag{Name: "overwrite-dns", Usage: "Replace any existing A/AAAA/CNAME at the hostname (forwards --overwrite-dns to cloudflared in local-managed mode)"},
&cli.StringFlag{Name: "from-json", Usage: "Read setup options from JSON file (or - for stdin)"},
}
}

func setupOptionsFromCommand(cmd *cli.Command, u interface {
Input(string, string) (string, error)
}) (tunnel.SetupOptions, error) {
},
) (tunnel.SetupOptions, error) {
if jsonPath := cmd.String("from-json"); jsonPath != "" {
var opts tunnel.SetupOptions
data, err := readJSONInput(jsonPath)
Expand DownExpand Up@@ -287,12 +294,14 @@ func setupOptionsFromCommand(cmd *cli.Command, u interface {
AutoRenew: cmd.Bool("auto-renew"),
PrivacyMode: cmd.String("privacy-mode"),
ConfirmCharge: cmd.Bool("yes"),
OverwriteDNS: cmd.Bool("overwrite-dns"),
}, nil
}

func domainSearchOptionsFromCommand(cmd *cli.Command, u interface {
Input(string, string) (string, error)
}) (tunnel.DomainSearchOptions, error) {
},
) (tunnel.DomainSearchOptions, error) {
if jsonPath := cmd.String("from-json"); jsonPath != "" {
var opts tunnel.DomainSearchOptions
data, err := readJSONInput(jsonPath)
Expand DownExpand Up@@ -373,7 +382,8 @@ func printDomainSuggestions(u interface {
Bold(string)
Print(string)
Detail(string, string)
}, result *tunnel.DomainSearchResult) {
}, result *tunnel.DomainSearchResult,
) {
u.Blank()
u.Bold("Domain Suggestions")
for _, domain := range result.Domains {
Expand All@@ -394,7 +404,8 @@ func printDomainChecks(u interface {
Bold(string)
Print(string)
Detail(string, string)
}, result *tunnel.DomainCheckResult) {
}, result *tunnel.DomainCheckResult,
) {
u.Blank()
u.Bold("Domain Availability")
for _, domain := range result.Domains {
Expand All@@ -416,7 +427,8 @@ func printDomainRegistration(u interface {
Print(string)
Detail(string, string)
Successf(string, ...any)
}, result *tunnel.DomainRegisterResult) {
}, result *tunnel.DomainRegisterResult,
) {
u.Blank()
u.Successf("Domain registration submitted for %s", result.Availability.Name)
u.Detail("Price", tunnelSummaryPrice(result.Availability))
Expand Down
13 changes: 12 additions & 1 deletion internal/tunnel/domain_setup.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,13 @@ type SetupOptions struct {
AutoRenew bool
PrivacyMode string
ConfirmCharge bool

// OverwriteDNS forwards --overwrite-dns to the underlying
// `cloudflared tunnel route dns` invocation in local-managed mode, so a
// prior CNAME at the hostname is replaced instead of failing the wizard.
// Has no effect in remote-managed mode (the Cloudflare API path performs
// its own upsert).
OverwriteDNS bool
}

type SetupResult struct {
Expand DownExpand Up@@ -455,7 +462,11 @@ func Setup(cfg *config.Config, u *ui.UI, opts SetupOptions) (*SetupResult, error
}

if management == tunnelManagementLocal {
if err := Login(cfg, u, LoginOptions{Hostname: hostname, TransportProtocol: opts.TransportProtocol}); err != nil {
if err := Login(cfg, u, LoginOptions{
Hostname: hostname,
TransportProtocol: opts.TransportProtocol,
OverwriteDNS: opts.OverwriteDNS,
}); err != nil {
return nil, err
}
return &SetupResult{
Expand Down
29 changes: 27 additions & 2 deletions internal/tunnel/login.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,13 @@ import (
type LoginOptions struct {
Hostname string
TransportProtocol string

// OverwriteDNS passes --overwrite-dns to `cloudflared tunnel route dns`.
// Without it, cloudflared refuses to replace an existing A/AAAA/CNAME
// record at the hostname, so re-running the wizard after a prior attempt
// fails with "An A, AAAA, or CNAME record with that host already exists"
// (Cloudflare API error 1003).
OverwriteDNS bool
}

// Login provisions a locally-managed tunnel using `cloudflared tunnel login` (browser auth),
Expand DownExpand Up@@ -101,9 +108,14 @@ func Login(cfg *config.Config, u *ui.UI, opts LoginOptions) error {

u.Infof("Creating DNS route for %s...", hostname)

routeOut, err := exec.Command(cloudflaredPath, "tunnel", "route", "dns", tunnelName, hostname).CombinedOutput()
routeArgs := routeDNSArgs(tunnelName, hostname, opts.OverwriteDNS)
routeOut, err := exec.Command(cloudflaredPath, routeArgs...).CombinedOutput()
if err != nil {
return fmt.Errorf("cloudflared tunnel route dns failed: %w\n%s", err, strings.TrimSpace(string(routeOut)))
hint := ""
if !opts.OverwriteDNS && strings.Contains(string(routeOut), "record with that host already exists") {
hint = "\nhint: a record for this hostname already exists. Re-run with --overwrite-dns to replace it."
}
return fmt.Errorf("cloudflared tunnel route dns failed: %w\n%s%s", err, strings.TrimSpace(string(routeOut)), hint)
}

if err := applyLocalManagedK8sResources(cfg, u, kubeconfigPath, hostname, tunnelID, cert, cred); err != nil {
Expand DownExpand Up@@ -160,6 +172,19 @@ func Login(cfg *config.Config, u *ui.UI, opts LoginOptions) error {
return nil
}

// routeDNSArgs builds the cloudflared argument vector for the
// `tunnel route dns` subcommand. When overwrite is true, --overwrite-dns is
// inserted between `dns` and the tunnel/hostname so cloudflared replaces an
// existing A/AAAA/CNAME record at the hostname instead of failing with API
// error 1003.
func routeDNSArgs(tunnelName, hostname string, overwrite bool) []string {
args := []string{"tunnel", "route", "dns"}
if overwrite {
args = append(args, "--overwrite-dns")
}
return append(args, tunnelName, hostname)
}

func defaultCloudflaredDir() string {
home, err := os.UserHomeDir()
if err != nil {
Expand Down
40 changes: 40 additions & 0 deletions internal/tunnel/login_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
package tunnel

import (
"reflect"
"testing"
)

func TestRouteDNSArgs(t *testing.T) {
tests := []struct {
name string
tunnelName string
hostname string
overwrite bool
want []string
}{
{
name: "default (no overwrite)",
tunnelName: "obol-stack-foo",
hostname: "inference.example.com",
overwrite: false,
want: []string{"tunnel", "route", "dns", "obol-stack-foo", "inference.example.com"},
},
{
name: "overwrite-dns inserted before tunnel/hostname",
tunnelName: "obol-stack-foo",
hostname: "inference.example.com",
overwrite: true,
want: []string{"tunnel", "route", "dns", "--overwrite-dns", "obol-stack-foo", "inference.example.com"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := routeDNSArgs(tc.tunnelName, tc.hostname, tc.overwrite)
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("routeDNSArgs(%q, %q, %v) = %v; want %v",
tc.tunnelName, tc.hostname, tc.overwrite, got, tc.want)
}
})
}
}
Loading