Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@lucacome
Copy link
Owner

@lucacome lucacome commented Oct 22, 2025

Summary by CodeRabbit

  • New Features

    • Exit node management and a guided multi-step cloud node provisioning flow (prepare → create → install).
  • UI/UX Improvements

    • Replaced legacy prompts with responsive form-based selection and multi-select flows.
    • Clearer success messages (e.g., "Connected to node ... via Tailscale", "Disconnected from exit node").
  • Improvements

    • Context-aware prompts with cancellation support, consistent region/device ordering, staged install/wait flows, and clearer failure reporting.
  • Chores

    • Added a Makefile snapshot build target and updated dependency/tooling ecosystem.

@coderabbitai
Copy link

coderabbitai bot commented Oct 22, 2025

📝 Walkthrough

Walkthrough

Replaced promptui/fuzzyfinder UIs with huh-based forms, made prompts context-aware, added internal.UpdateExitNode(ctx, c, id) for exit-node management, refactored EC2 instance creation into prepare/create/install stages using SSM, updated dependencies in go.mod, and added a new tailout phony Makefile target.

Changes

Cohort / File(s) Change summary
Build
Makefile
Added a new phony target tailout that runs goreleaser build --snapshot --clean --single-target.
Module deps
go.mod
Introduced a tool directive and broad dependency churn: removed several older libraries, added/updated AWS SSM and many indirects, and migrated UI/terminal dependencies toward the Charmbracelet/huh ecosystem.
Internal utilities
internal/common.go
Replaced promptui with huh UI, made prompts context-aware (PromptYesNo(ctx, ...)), sorted region list, and added UpdateExitNode(ctx context.Context, c *tsapi.Client, id string) error to manage exit-node prefs and reachability checks.
Tailout CLI — connect
tailout/connect.go
Switched interactive node selection to huh-based list/form, validate missing node name explicitly, and perform exit-node changes via internal.UpdateExitNode; updated messages and error propagation.
Tailout CLI — disconnect
tailout/disconnect.go
Replaced local prefs editing with API-based tsapi.Client flow and call into internal.UpdateExitNode (with empty target) to disconnect; adjusted error messages and success text.
Tailout CLI — create
tailout/create.go
Refactored instance creation into staged functions (prepareInstance, createInstance, installTailScale), added ErrUserAborted, used SSM SendCommand for post-launch installation, improved wait/tagging logic and error handling.
Tailout CLI — stop
tailout/stop.go
Replaced fuzzyfinder multi-select with huh-based multi-select form, integrated context-aware confirmation, handled zero-selection early, and updated error propagation and messages.
Tailout CLI — init
tailout/init.go
Updated calls to internal.PromptYesNo to pass context.Context, making prompts cancelable/timeout-aware.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User
  participant CLI as tailout CLI
  participant HUH as huh UI
  participant API as tsapi.Client
  Note right of CLI #f6f8ff: Exit-node update (context-aware)
  User->>CLI: choose connect / disconnect
  CLI->>HUH: present node selection / confirm
  HUH-->>CLI: selected node or cancel
  CLI->>API: UpdateExitNode(ctx, nodeID)
  API-->>CLI: success / error
  CLI-->>User: "Connected to node..." / "Disconnected from exit node."
Loading
sequenceDiagram
  autonumber
  participant User
  participant CLI as tailout Create
  participant HUH as huh UI
  participant AWS as EC2/SSM
  Note right of CLI #f6f8ff: Staged instance creation (prepare → create → install)
  User->>CLI: trigger create (prompts)
  CLI->>HUH: AMI and params selection
  HUH-->>CLI: confirmed inputs
  CLI->>AWS: RunInstances (createInstance)
  AWS-->>CLI: instanceId / metadata
  CLI->>AWS: Waiter & Tag instance
  CLI->>AWS: SSM SendCommand (installTailScale)
  AWS-->>CLI: command result
  CLI-->>User: node join info / errors
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

"I hopped through prompts and cloud by night,
swapped forms for huh and made things bright.
I staged the launch, sent SSM to play,
set exit nodes, then hopped away.
Tailout hums — a carrot-built delight. 🐇"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Replace promptui with huh" accurately represents the primary change throughout the pull request. The changeset demonstrates extensive replacement of the promptui library with huh across multiple files: go.mod removes promptui and related UI dependencies while adding the Charmbracelet huh ecosystem, and internal/common.go, tailout/connect.go, and tailout/stop.go all explicitly replace promptui/fuzzyfinder-based UI flows with huh-based implementations. While the PR includes supporting changes such as refactoring in create.go and API-based modifications in disconnect.go, these appear to be secondary improvements that complement and enable the main UI library migration. The title is clear, specific, and captures the core intent of this enhancement.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/huh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added enhancement New feature or request dependencies labels Oct 22, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/common.go (1)

111-116: Fix recency check; current logic is wrong.

device.LastSeen.Minute() returns the minute within the hour (0–59), not “minutes ago”. This admits stale nodes and excludes fresh ones around hour boundaries.

Apply this diff:

-				if time.Duration(device.LastSeen.Minute()) < 10*time.Minute {
+				if time.Since(device.LastSeen) < 10*time.Minute {
 					tailoutDevices = append(tailoutDevices, device)
 				}
🧹 Nitpick comments (7)
go.mod (2)

88-88: Use the canonical YAML module path.

go.yaml.in/yaml/v3 exists, but the canonical and widely used path is gopkg.in/yaml.v3. Prefer the canonical path to avoid tooling surprises.

Apply this diff:

-	go.yaml.in/yaml/v3 v3.0.4 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect

Reference: official import path is gopkg.in/yaml.v3. (github.com)


15-15: Optional: avoid long‑lived pseudo‑versions for tools/UI libs.

If stability matters, consider pinning github.com/charmbracelet/huh/spinner to a tagged release once available to reduce churn from pseudo‑versions. This is optional.

internal/common.go (2)

53-55: Remove duplicate sorting.

GetRegions already sorts regionNames. The extra sort in SelectRegion is redundant.

Apply this diff:

-	sort.Slice(regionNames, func(i, j int) bool {
-		return regionNames[i] < regionNames[j]
-	})

Also applies to: 36-39


69-77: Handle cancellation robustly; avoid string‑matching “^C”.

Compare with context.Canceled (or ctx.Err()) instead of matching error text.

Apply this diff:

-		select {
-		case <-ctx.Done():
-			return "", ctx.Err()
-		default:
-			if err.Error() == "^C" {
-				return "", context.Canceled
-			}
-			return "", fmt.Errorf("failed to select region: %w", err)
-		}
+		if errors.Is(err, context.Canceled) || ctx.Err() != nil {
+			return "", context.Canceled
+		}
+		return "", fmt.Errorf("failed to select region: %w", err)
tailout/connect.go (1)

49-51: Optional: auto‑select when only one node is available.

If exactly one active node exists and interactive mode is enabled, select it without prompting for a smoother UX.

tailout/create.go (2)

291-293: Avoid returning (nil, nil) — an anti-pattern in Go.

When the user declines instance creation, the function returns (nil, nil). This requires the caller to check both the return value and error, which is error-prone. Instead, return a sentinel error or a custom error type to clearly indicate user cancellation.

Define a sentinel error at the package level:

var ErrUserCancelled = errors.New("user cancelled operation")

Then apply this diff:

 	if !result {
-		return nil, nil
+		return nil, ErrUserCancelled
 	}

Update the caller (lines 102-109) to check for this error:

 	runInput, errPrep := prepareInstance(ctx, cfg, aws.Bool(dryRun))
 	if errPrep != nil {
+		if errors.Is(errPrep, ErrUserCancelled) {
+			fmt.Println("Instance creation cancelled.")
+			return nil
+		}
 		return fmt.Errorf("failed to prepare instance: %w", errPrep)
 	}
-	if runInput == nil {
-		fmt.Println("Instance creation aborted.")
-		return nil
-	}

363-370: Use a more descriptive variable name.

The variable instance1 is confusing. Consider renaming it to describedInstance or runningInstance to clarify that it's the instance retrieved from the DescribeInstances call.

Apply this diff:

-	instance1 := reservation.Instances[0]
-	if instance1.PublicIpAddress == nil {
+	describedInstance := reservation.Instances[0]
+	if describedInstance.PublicIpAddress == nil {
 		return instance, errors.New("no public IP address found")
 	}
 
 	instance.Name = nodeName
 	instance.InstanceID = *createdInstance.InstanceId
-	instance.IP = *instance1.PublicIpAddress
+	instance.IP = *describedInstance.PublicIpAddress
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f539858 and ffae618.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • internal/assets/spinner.svg is excluded by !**/*.svg
📒 Files selected for processing (8)
  • Makefile (1 hunks)
  • go.mod (5 hunks)
  • internal/common.go (3 hunks)
  • tailout/connect.go (2 hunks)
  • tailout/create.go (7 hunks)
  • tailout/disconnect.go (1 hunks)
  • tailout/init.go (1 hunks)
  • tailout/stop.go (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
tailout/connect.go (1)
internal/common.go (1)
  • UpdateExitNode (123-186)
tailout/stop.go (1)
internal/common.go (1)
  • PromptYesNo (84-101)
tailout/init.go (1)
internal/common.go (1)
  • PromptYesNo (84-101)
tailout/create.go (2)
tailout/config/config.go (1)
  • Config (13-21)
internal/common.go (1)
  • PromptYesNo (84-101)
tailout/disconnect.go (3)
tailout/app.go (1)
  • App (7-9)
tailout/config/config.go (1)
  • Config (13-21)
internal/common.go (1)
  • UpdateExitNode (123-186)
🪛 checkmake (0.2.2)
Makefile

[warning] 7-7: Missing required phony target "all"

(minphony)


[warning] 7-7: Missing required phony target "clean"

(minphony)


[warning] 7-7: Missing required phony target "test"

(minphony)

🔇 Additional comments (15)
Makefile (1)

7-9: New tailout build target looks good.

The target is properly declared as .PHONY and the goreleaser flags (--snapshot, --clean, --single-target) are appropriate for building a local development snapshot.

go.mod (1)

5-5: Tool directive looks good; ensure CI uses Go ≥ 1.24 (you’re on 1.25.3).

The tool directive is supported starting with Go 1.24, and with go 1.25.3 you’re covered. Just confirm CI/build images aren’t on older toolchains that would choke on tool. (tip.golang.org)

tailout/disconnect.go (1)

13-21: LGTM: consistent API‑based disconnect.

Clean switch to the shared UpdateExitNode flow; messages and errors read well.

Also applies to: 23-29

tailout/init.go (1)

108-108: LGTM: prompt is now context‑aware.

All PromptYesNo call sites across the codebase (tailout/stop.go, tailout/create.go, tailout/init.go) properly pass ctx as the first parameter, confirming consistent context propagation.

tailout/stop.go (4)

14-14: LGTM! Clean migration to huh.

The import addition supports the new multi-select UI implementation.


48-83: LGTM! Well-structured multi-select implementation.

The huh-based selection flow is clean and handles edge cases properly:

  • Meaningful labels with hostname and IP address
  • Proper error handling for selection failures
  • Early return on zero selections
  • Correct mapping from indices to nodes

98-101: LGTM! Good defensive check.

The early return for empty nodesToStop prevents unnecessary processing and provides clear user feedback.


109-109: LGTM! Correct context propagation.

The updated call aligns with the new context-aware signature of internal.PromptYesNo.

tailout/create.go (7)

17-22: LGTM! Necessary imports for new functionality.

The new imports support SSM-based Tailscale installation, dry-run error handling, and spinner UI feedback.


97-97: LGTM! Good addition of retry configuration.

The retry configuration improves resilience for AWS API calls, especially helpful with spot instance operations.


102-175: LGTM! Well-structured staged provisioning flow.

The refactored approach provides:

  • Clear separation of concerns (prepare, create, install)
  • Better UX with spinner feedback
  • Proper error handling at each stage
  • Verification that the node joined the tailnet

178-182: LGTM! Clean data structure.

The instance struct provides a clear way to pass instance details between the staged functions.


303-309: LGTM! Proper dry-run error handling.

The dry-run detection using Smithy's error type is correct, and returning a zero-valued instance is appropriate since the caller checks for empty InstanceID.


337-343: LGTM! Proper use of AWS SDK waiter.

The instance status waiter with a 5-minute timeout is appropriate for EC2 instance startup, and the spinner title update provides good UX feedback.


375-421: LGTM! Clean SSM-based installation.

The SSM approach is robust and provides:

  • Non-interactive remote installation
  • Proper wait for command completion with timeout
  • Status checking with detailed error output

Note: This relies on the SSM agent being pre-installed on Amazon Linux 2023 AMIs, which is the default.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
tailout/connect.go (2)

54-60: Avoid panic on empty addresses; add defensive check.

device.Addresses[0] will panic if the slice is empty. Add a guard similar to the pattern used in tailout/stop.go lines 51-57.

Apply this diff:

 		// Create options for huh select
 		options := make([]huh.Option[int], len(tailoutDevices))
 		for i, device := range tailoutDevices {
-			// Display hostname and IP address
-			label := fmt.Sprintf("%s (%s)", device.Hostname, device.Addresses[0])
+			addr := "no IP"
+			if len(device.Addresses) > 0 {
+				addr = device.Addresses[0]
+			}
+			label := fmt.Sprintf("%s (%s)", device.Hostname, addr)
 			options[i] = huh.NewOption(label, i)
 		}

88-88: Avoid panic on empty addresses in success message.

Same issue as lines 54-60: deviceToConnectTo.Addresses[0] can panic if Addresses is empty.

Apply this diff:

+	addr := "no IP"
+	if len(deviceToConnectTo.Addresses) > 0 {
+		addr = deviceToConnectTo.Addresses[0]
+	}
-	fmt.Printf("Connected to node %s (%s) via Tailscale.\n", deviceToConnectTo.Hostname, deviceToConnectTo.Addresses[0])
+	fmt.Printf("Connected to node %s (%s) via Tailscale.\n", deviceToConnectTo.Hostname, addr)
tailout/create.go (2)

243-243: Use the calculated durationMinutes variable instead of hardcoded value.

The shutdown time is hardcoded as 10 minutes, but the durationMinutes variable (calculated from the user-provided shutdown config at lines 84-87) is never used.

Apply this diff:

-sudo echo "sudo shutdown" | at now + ` + strconv.Itoa(10) + ` minutes`
+sudo echo "sudo shutdown" | at now + ` + strconv.Itoa(durationMinutes) + ` minutes`

Note: You'll need to pass durationMinutes as a parameter to prepareInstance to access it here.


290-290: Display the actual shutdown duration instead of "TODO".

The user-facing output shows "TODO" for the auto shutdown time. Pass durationMinutes as a parameter to prepareInstance to display the actual value.

Apply this diff:

 - Auto shutdown after: %s
-	`, *identity.Account, imageID, imageName, imageOwner, imageArchitecture, types.InstanceTypeT3aMicro, cfg.Region, "TODO")
+	`, *identity.Account, imageID, imageName, imageOwner, imageArchitecture, types.InstanceTypeT3aMicro, cfg.Region, strconv.Itoa(durationMinutes) + " minutes")

And update the function signature:

-func prepareInstance(ctx context.Context, cfg aws.Config, dryRun *bool) (instance *ec2.RunInstancesInput, err error) {
+func prepareInstance(ctx context.Context, cfg aws.Config, dryRun *bool, durationMinutes int) (instance *ec2.RunInstancesInput, err error) {

Then update the call at line 104:

-	runInput, errPrep := prepareInstance(ctx, cfg, aws.Bool(dryRun))
+	runInput, errPrep := prepareInstance(ctx, cfg, aws.Bool(dryRun), durationMinutes)
🧹 Nitpick comments (2)
internal/common.go (2)

53-55: Remove redundant sort.

GetRegions already sorts the region names at lines 36-38, so this sort is unnecessary.

Apply this diff:

-	sort.Slice(regionNames, func(i, j int) bool {
-		return regionNames[i] < regionNames[j]
-	})
-
 	var selectedRegion string

90-93: Add context cancellation check for consistency.

For consistency with SelectRegion (lines 69-71), check for context cancellation explicitly.

Apply this diff:

 	err := form.RunWithContext(ctx)
 	if err != nil {
+		if ctxErr := ctx.Err(); ctxErr != nil {
+			return false, fmt.Errorf("prompt canceled: %w", ctxErr)
+		}
 		return false, fmt.Errorf("failed to prompt for yes/no: %w", err)
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffae618 and 1002d63.

📒 Files selected for processing (4)
  • internal/common.go (3 hunks)
  • tailout/connect.go (2 hunks)
  • tailout/create.go (7 hunks)
  • tailout/stop.go (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tailout/create.go (2)
tailout/config/config.go (1)
  • Config (13-21)
internal/common.go (1)
  • PromptYesNo (79-96)
tailout/connect.go (1)
internal/common.go (1)
  • UpdateExitNode (118-181)
tailout/stop.go (1)
internal/common.go (1)
  • PromptYesNo (79-96)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: binary
🔇 Additional comments (2)
tailout/stop.go (1)

48-83: Excellent defensive pattern for handling addresses.

The guard at lines 51-54 properly prevents panics when Addresses is empty. This is the correct pattern that should be replicated in tailout/connect.go (see my comments there).

tailout/create.go (1)

292-299: LGTM! Correctly integrated with updated PromptYesNo API.

The context is properly passed to internal.PromptYesNo, and the user abort case returns the custom error for clear error handling.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
tailout/create.go (3)

138-139: Clarify or remove the dry run comment.

The comment "// If dry run, exit here?" is unclear because dry run is already handled within createInstance at lines 310-313. If additional dry run logic is needed here, implement it; otherwise, remove the comment.

Apply this diff if no additional logic is needed:

-	// If dry run, exit here?
-

282-290: Consider clarifying the shutdown duration unit in the display.

The display shows shutdownDuration but doesn't explicitly state the unit. Consider adding " minutes" to line 288 for clarity.

Apply this diff:

-- Auto shutdown after: %s
+- Auto shutdown after: %s minutes

344-346: Consider increasing the instance status wait timeout.

The 5-minute timeout for InstanceStatusOkWaiter might be insufficient for spot instances, which can take longer to reach running status, especially during high demand periods. Consider increasing to 10 minutes.

Apply this diff:

-	}, time.Minute*5)
+	}, time.Minute*10)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1002d63 and 1768788.

📒 Files selected for processing (2)
  • internal/common.go (3 hunks)
  • tailout/create.go (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tailout/create.go (2)
tailout/config/config.go (1)
  • Config (13-21)
internal/common.go (1)
  • PromptYesNo (75-95)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: binary
🔇 Additional comments (12)
internal/common.go (3)

36-38: LGTM: Alphabetical sorting improves UX.

The alphabetical sorting of region names enhances user experience by making regions easier to locate.


43-72: LGTM: Context-aware region selection with proper cancellation handling.

The migration to huh is implemented correctly with proper context cancellation handling throughout.


74-95: LGTM: Context-aware confirmation prompt.

The function correctly implements context-aware confirmation using huh with proper cancellation handling.

tailout/create.go (9)

27-27: LGTM: Error sentinel for user-initiated aborts.

The ErrUserAborted sentinel is a good practice for distinguishing user-initiated cancellations from actual errors.


99-99: LGTM: Retry configuration improves resilience.

Adding retry configuration with 5 max attempts and standard mode improves the resilience of AWS SDK operations.


104-115: LGTM: Proper handling of ErrUserAborted and nil returns.

The handling of ErrUserAborted and nil returns from prepareInstance correctly distinguishes user-initiated aborts from errors.


216-216: LGTM: Explicit nil returns as suggested in past review.

The function now explicitly returns nil on error paths instead of relying on uninitialized named return variables, improving code clarity.

Also applies to: 220-220, 279-279


243-243: LGTM: Shutdown duration parameter is now used correctly.

The shutdownDuration parameter is correctly used in the user data script, addressing the past review comment about using the calculated duration instead of a hardcoded value.


255-266: LGTM: Tagging resources at creation time.

Adding the App tag in TagSpecifications ensures the instance is properly tagged from creation, which is a best practice for resource management.


309-314: LGTM: Proper dry run handling.

The dry run operation is correctly detected and handled, with an appropriate message and early return.


184-188: LGTM: Clean data structure for instance details.

The instance struct provides a clean way to pass instance details between the staged provisioning functions.


381-427: LGTM: SSM-based installation with proper error handling.

The SSM-based TailScale installation is well-structured with proper waiting and status validation. The error handling correctly includes stderr output on failure.

However, the 5-minute timeout at line 409 might be insufficient for instances with slow network connections or during package manager updates. Consider increasing to 10 minutes:

-	}, 5*time.Minute)
+	}, 10*time.Minute)

You can verify typical SSM command execution times in your environment by checking CloudWatch Logs for SSM Run Command executions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
tailout/connect.go (1)

93-98: Previous issue resolved: defensive address handling in success message.

Lines 94-97 apply the same safe pattern as the interactive flow, checking array length before accessing deviceToConnectTo.Addresses[0]. This completes the mitigation of the panic risk flagged in earlier reviews.

Optional: The address fallback logic appears twice (lines 58-61 and here). Extracting it into a helper function like getDeviceAddress(device tsapi.Device) string would reduce duplication, but given the minimal size (3 lines), this is not essential.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1768788 and b24c29f.

📒 Files selected for processing (1)
  • tailout/connect.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tailout/connect.go (1)
internal/common.go (1)
  • UpdateExitNode (117-180)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: binary
🔇 Additional comments (4)
tailout/connect.go (4)

10-10: LGTM! Clean transition to huh.

The import of huh replaces the deprecated promptui dependency, aligning with the PR objectives.


44-48: Previous issue resolved: NodeID now correctly extracted.

The bug flagged in earlier reviews (passing hostname to UpdateExitNode) has been fixed. Line 48 now correctly assigns deviceToConnectTo.NodeID to nodeConnect, ensuring the function receives a NodeID rather than a hostname.


54-83: Previous issue resolved: defensive address handling added.

The panic risk flagged in earlier reviews has been mitigated. Lines 58-61 now check len(device.Addresses) > 0 before accessing the first element, providing a safe fallback ("no IP") when the array is empty. The huh-based selection flow is well-implemented and context-aware.


88-91: LGTM! Correct NodeID usage and error handling.

The call to internal.UpdateExitNode correctly passes nodeConnect, which now contains the NodeID (set on lines 48 or 83). Error handling provides helpful context.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
internal/common.go (1)

170-176: Make post-update verification robust when ExitNodeStatus is nil.

If the node isn’t in the current netmap, ExitNodeStatus can be nil even though the policy was applied; confirm via prefs before failing. This was raised earlier and remains unaddressed.

- status, err = localClient.Status(ctx)
+ status, err = localClient.Status(ctx)
  if err != nil {
    return fmt.Errorf("failed to get tailscale status: %w", err)
  }
- if id != "" && (status.ExitNodeStatus == nil || status.ExitNodeStatus.ID != tailcfg.StableNodeID(id)) {
-   return errors.New("failed to set the exit node")
- }
+ // Re-fetch prefs to validate policy application.
+ postPrefs, err := localClient.GetPrefs(ctx)
+ if err != nil {
+   return fmt.Errorf("failed to get prefs after update: %w", err)
+ }
+ if id != "" {
+   if status.ExitNodeStatus != nil {
+     if status.ExitNodeStatus.ID != tailcfg.StableNodeID(id) {
+       return fmt.Errorf("exit node mismatch: want %s, got %s", tailcfg.StableNodeID(id), status.ExitNodeStatus.ID)
+     }
+   } else if postPrefs.ExitNodeID != tailcfg.StableNodeID(id) {
+     return errors.New("exit node ID not applied")
+   }
+ } else {
+   if postPrefs.ExitNodeID != "" || status.ExitNodeStatus != nil {
+     return errors.New("failed to clear the exit node")
+   }
+ }
🧹 Nitpick comments (7)
internal/common.go (7)

36-38: Use sort.Strings for simpler intent.

Directly sort the slice of strings.

- sort.Slice(regionNames, func(i, j int) bool {
-   return regionNames[i] < regionNames[j]
- })
+ sort.Strings(regionNames)

43-44: Fix exported doc comment style.

Make the comment start with the function name to satisfy linters.

-// Function that uses huh to return an AWS region fetched from the aws sdk.
+// SelectRegion uses huh to return an AWS region fetched via the AWS SDK.

74-76: LGTM: context-aware yes/no prompt.

Minor: mirror doc comment style like above for linters.

-// Function that uses huh to return a boolean value.
+// PromptYesNo uses huh to return a boolean value.

Also applies to: 86-95


125-127: Prefer ipn.Running constant; include actual state in error.

Avoid string literal comparison.

- if status.BackendState != "Running" {
-   return errors.New("tailscale is not running")
- }
+ if status.BackendState != ipn.Running {
+   return fmt.Errorf("tailscale backend not running (state=%s)", status.BackendState)
+ }

150-166: Minimize MaskedPrefs to avoid stomping unrelated Prefs.

Update only the field you intend to change.

- prefs, err := localClient.GetPrefs(ctx)
- if err != nil {
-   return fmt.Errorf("failed to get prefs: %w", err)
- }
-
- if id != "" {
-   fmt.Printf("Setting exit node to %s...\n", id)
-   prefs.ExitNodeID = tailcfg.StableNodeID(id)
- } else {
-   fmt.Println("Clearing exit node...")
-   prefs.ClearExitNode()
- }
- _, err = localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
-   Prefs:         *prefs,
-   ExitNodeIDSet: true,
- })
+ if id != "" {
+   fmt.Printf("Setting exit node to %s...\n", id)
+   _, err = localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
+     Prefs:         ipn.Prefs{ExitNodeID: tailcfg.StableNodeID(id)},
+     ExitNodeIDSet: true,
+   })
+ } else {
+   fmt.Println("Clearing exit node...")
+   _, err = localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
+     // zero value clears ExitNodeID when ExitNodeIDSet is true
+     Prefs:         ipn.Prefs{},
+     ExitNodeIDSet: true,
+   })
+ }
  if err != nil {
    return fmt.Errorf("failed to set/unset exit node: %w", err)
  }

145-147: Avoid printing from internal packages.

Return status strings or accept an io.Writer/logger so CLI decides how to display.

- fmt.Printf("Currently connected to exit node: %s\n", currentExitNodeName)
+ // return this info to caller for display

Option: change signature to accept an io.Writer or a logger interface.

Also applies to: 155-161


178-180: Reachability check is good; consider downgraded severity.

If policy was applied but node is offline, consider returning a typed error so callers can choose between warn/fail.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b24c29f and 8a3065b.

📒 Files selected for processing (1)
  • internal/common.go (3 hunks)
🔇 Additional comments (2)
internal/common.go (2)

53-61: LGTM: huh-based region select with context support.


129-148: Nil client guard is unnecessary—all call sites pass non-nil values.

Both UpdateExitNode call sites construct the tsapi.Client using struct literals (&tsapi.Client{...}), which always produce non-nil pointers in Go. In disconnect.go, the client is created at lines 17-20 and passed directly at line 23. In connect.go, the client is created at lines 23-27 and passed at line 88. No error handling paths result in nil clients being passed. The function parameter c *tsapi.Client is a required contract fulfilled by all identified call sites.

Likely an incorrect or invalid review comment.

@lucacome lucacome merged commit 9cd3777 into main Oct 23, 2025
14 of 18 checks passed
@lucacome lucacome deleted the feat/huh branch October 23, 2025 00:24
@coderabbitai coderabbitai bot mentioned this pull request Oct 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant