Skip to content

feat: add coder connect exists hidden subcommand #17418

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 17, 2025
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
3 changes: 2 additions & 1 deletion cli/configssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ import (
"golang.org/x/exp/constraints"
"golang.org/x/xerrors"

"github.com/coder/serpent"

"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)

const (
Expand Down
47 changes: 47 additions & 0 deletions cli/connect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cli

import (
"github.com/coder/serpent"

"github.com/coder/coder/v2/codersdk/workspacesdk"
)

func (r *RootCmd) connectCmd() *serpent.Command {
cmd := &serpent.Command{
Use: "connect",
Short: "Commands related to Coder Connect (OS-level tunneled connection to workspaces).",
Handler: func(i *serpent.Invocation) error {
return i.Command.HelpHandler(i)
},
Hidden: true,
Children: []*serpent.Command{
r.existsCmd(),
},
}
return cmd
}

func (*RootCmd) existsCmd() *serpent.Command {
cmd := &serpent.Command{
Use: "exists <hostname>",
Short: "Checks if the given hostname exists via Coder Connect.",
Long: "This command is designed to be used in scripts to check if the given hostname exists via Coder " +
"Connect. It prints no output. It returns exit code 0 if it does exist and code 1 if it does not.",
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
),
Handler: func(inv *serpent.Invocation) error {
hostname := inv.Args[0]
exists, err := workspacesdk.ExistsViaCoderConnect(inv.Context(), hostname)
if err != nil {
return err
}
if !exists {
// we don't want to print any output, since this command is designed to be a check in scripts / SSH config.
return ErrSilent
}
return nil
},
}
return cmd
}
76 changes: 76 additions & 0 deletions cli/connect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cli_test

import (
"bytes"
"context"
"net"
"testing"

"github.com/stretchr/testify/require"
"tailscale.com/net/tsaddr"

"github.com/coder/serpent"

"github.com/coder/coder/v2/cli"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/coder/v2/testutil"
)

func TestConnectExists_Running(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)

var root cli.RootCmd
cmd, err := root.Command(root.AGPL())
require.NoError(t, err)

inv := (&serpent.Invocation{
Command: cmd,
Args: []string{"connect", "exists", "test.example"},
}).WithContext(withCoderConnectRunning(ctx))
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
inv.Stdout = stdout
inv.Stderr = stderr
err = inv.Run()
require.NoError(t, err)
}

func TestConnectExists_NotRunning(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)

var root cli.RootCmd
cmd, err := root.Command(root.AGPL())
require.NoError(t, err)

inv := (&serpent.Invocation{
Command: cmd,
Args: []string{"connect", "exists", "test.example"},
}).WithContext(withCoderConnectNotRunning(ctx))
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
inv.Stdout = stdout
inv.Stderr = stderr
err = inv.Run()
require.ErrorIs(t, err, cli.ErrSilent)
}

type fakeResolver struct {
shouldReturnSuccess bool
}

func (f *fakeResolver) LookupIP(_ context.Context, _, _ string) ([]net.IP, error) {
if f.shouldReturnSuccess {
return []net.IP{net.ParseIP(tsaddr.CoderServiceIPv6().String())}, nil
}
return nil, &net.DNSError{IsNotFound: true}
}

func withCoderConnectRunning(ctx context.Context) context.Context {
return workspacesdk.WithTestOnlyCoderContextResolver(ctx, &fakeResolver{shouldReturnSuccess: true})
}

func withCoderConnectNotRunning(ctx context.Context) context.Context {
return workspacesdk.WithTestOnlyCoderContextResolver(ctx, &fakeResolver{shouldReturnSuccess: false})
}
12 changes: 11 additions & 1 deletion cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@ import (

"github.com/coder/pretty"

"github.com/coder/serpent"

"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/cli/gitauth"
"github.com/coder/coder/v2/cli/telemetry"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/serpent"
)

var (
Expand All @@ -49,6 +50,10 @@ var (
workspaceCommand = map[string]string{
"workspaces": "",
}

// ErrSilent is a sentinel error that tells the command handler to just exit with a non-zero error, but not print
// anything.
ErrSilent = xerrors.New("silent error")
)

const (
Expand Down Expand Up @@ -122,6 +127,7 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command {
r.whoami(),

// Hidden
r.connectCmd(),
r.expCmd(),
r.gitssh(),
r.support(),
Expand Down Expand Up @@ -175,6 +181,10 @@ func (r *RootCmd) RunWithSubcommands(subcommands []*serpent.Command) {
//nolint:revive,gocritic
os.Exit(code)
}
if errors.Is(err, ErrSilent) {
Copy link
Member

Choose a reason for hiding this comment

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

&exitError{code: 1} could potentially be used instead, but I see other uses of it have disappeared from the code base.

//nolint:revive,gocritic
os.Exit(code)
}
f := PrettyErrorFormatter{w: os.Stderr, verbose: r.verbose}
if err != nil {
f.Format(err)
Expand Down
3 changes: 2 additions & 1 deletion cli/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import (
"sync/atomic"
"testing"

"github.com/coder/serpent"

"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down
39 changes: 30 additions & 9 deletions codersdk/workspacesdk/workspacesdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ import (

"cdr.dev/slog"

"github.com/coder/quartz"
"github.com/coder/websocket"

"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/quartz"
"github.com/coder/websocket"
)

var ErrSkipClose = xerrors.New("skip tailnet close")
Expand Down Expand Up @@ -128,19 +129,16 @@ func init() {
}
}

type resolver interface {
type Resolver interface {
LookupIP(ctx context.Context, network, host string) ([]net.IP, error)
}

type Client struct {
client *codersdk.Client

// overridden in tests
resolver resolver
}

func New(c *codersdk.Client) *Client {
return &Client{client: c, resolver: net.DefaultResolver}
return &Client{client: c}
}

// AgentConnectionInfo returns required information for establishing
Expand Down Expand Up @@ -392,6 +390,12 @@ func (c *Client) AgentReconnectingPTY(ctx context.Context, opts WorkspaceAgentRe
return websocket.NetConn(context.Background(), conn, websocket.MessageBinary), nil
}

func WithTestOnlyCoderContextResolver(ctx context.Context, r Resolver) context.Context {
return context.WithValue(ctx, dnsResolverContextKey{}, r)
}

type dnsResolverContextKey struct{}

type CoderConnectQueryOptions struct {
HostnameSuffix string
}
Expand All @@ -409,15 +413,32 @@ func (c *Client) IsCoderConnectRunning(ctx context.Context, o CoderConnectQueryO
suffix = info.HostnameSuffix
}
domainName := fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, suffix)
return ExistsViaCoderConnect(ctx, domainName)
}

func testOrDefaultResolver(ctx context.Context) Resolver {
// check the context for a non-default resolver. This is only used in testing.
resolver, ok := ctx.Value(dnsResolverContextKey{}).(Resolver)
if !ok || resolver == nil {
resolver = net.DefaultResolver
}
return resolver
}

// ExistsViaCoderConnect checks if the given hostname exists via Coder Connect. This doesn't guarantee the
// workspace is actually reachable, if, for example, its agent is unhealthy, but rather that Coder Connect knows about
// the workspace and advertises the hostname via DNS.
func ExistsViaCoderConnect(ctx context.Context, hostname string) (bool, error) {
resolver := testOrDefaultResolver(ctx)
var dnsError *net.DNSError
ips, err := c.resolver.LookupIP(ctx, "ip6", domainName)
ips, err := resolver.LookupIP(ctx, "ip6", hostname)
if xerrors.As(err, &dnsError) {
if dnsError.IsNotFound {
return false, nil
}
}
if err != nil {
return false, xerrors.Errorf("lookup DNS %s: %w", domainName, err)
return false, xerrors.Errorf("lookup DNS %s: %w", hostname, err)
}

// The returned IP addresses are probably from the Coder Connect DNS server, but there are sometimes weird captive
Expand Down
86 changes: 0 additions & 86 deletions codersdk/workspacesdk/workspacesdk_internal_test.go

This file was deleted.

Loading
Loading