Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitdc5fab3

Browse files
committed
feat: add coder connect exists hidden subcommand
1 parent0bc49ff commitdc5fab3

File tree

8 files changed

+242
-98
lines changed

8 files changed

+242
-98
lines changed

‎cli/configssh.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import (
2222
"golang.org/x/exp/constraints"
2323
"golang.org/x/xerrors"
2424

25+
"github.com/coder/serpent"
26+
2527
"github.com/coder/coder/v2/cli/cliui"
2628
"github.com/coder/coder/v2/codersdk"
27-
"github.com/coder/serpent"
2829
)
2930

3031
const (

‎cli/connect.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package cli
2+
3+
import (
4+
"github.com/coder/serpent"
5+
6+
"github.com/coder/coder/v2/codersdk/workspacesdk"
7+
)
8+
9+
func (r*RootCmd)connectCmd()*serpent.Command {
10+
cmd:=&serpent.Command{
11+
Use:"connect",
12+
Short:"Commands related to Coder Connect (OS-level tunneled connection to workspaces).",
13+
Handler:func(i*serpent.Invocation)error {
14+
returni.Command.HelpHandler(i)
15+
},
16+
Hidden:true,
17+
Children: []*serpent.Command{
18+
r.existsCmd(),
19+
},
20+
}
21+
returncmd
22+
}
23+
24+
func (*RootCmd)existsCmd()*serpent.Command {
25+
cmd:=&serpent.Command{
26+
Use:"exists <hostname>",
27+
Short:"Checks if the given hostname exists via Coder Connect.",
28+
Long:"This command is designed to be used in scripts to check if the given hostname exists via Coder "+
29+
"Connect. It prints no output. It returns exit code 0 if it does exist and code 1 if it does not.",
30+
Middleware:serpent.Chain(
31+
serpent.RequireNArgs(1),
32+
),
33+
Handler:func(inv*serpent.Invocation)error {
34+
hostname:=inv.Args[0]
35+
exists,err:=workspacesdk.ExistsViaCoderConnect(inv.Context(),hostname)
36+
iferr!=nil {
37+
returnerr
38+
}
39+
if!exists {
40+
// we don't want to print any output, since this command is designed to be a check in scripts / SSH config.
41+
returnErrSilent
42+
}
43+
returnnil
44+
},
45+
}
46+
returncmd
47+
}

‎cli/connect_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"net"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
"tailscale.com/net/tsaddr"
11+
12+
"github.com/coder/serpent"
13+
14+
"github.com/coder/coder/v2/cli"
15+
"github.com/coder/coder/v2/codersdk/workspacesdk"
16+
"github.com/coder/coder/v2/testutil"
17+
)
18+
19+
funcTestConnectExists_Running(t*testing.T) {
20+
t.Parallel()
21+
ctx:=testutil.Context(t,testutil.WaitShort)
22+
23+
varroot cli.RootCmd
24+
cmd,err:=root.Command(root.AGPL())
25+
require.NoError(t,err)
26+
27+
inv:= (&serpent.Invocation{
28+
Command:cmd,
29+
Args: []string{"connect","exists","test.example"},
30+
}).WithContext(withCoderConnectRunning(ctx))
31+
stdout:=new(bytes.Buffer)
32+
stderr:=new(bytes.Buffer)
33+
inv.Stdout=stdout
34+
inv.Stderr=stderr
35+
err=inv.Run()
36+
require.NoError(t,err)
37+
}
38+
39+
funcTestConnectExists_NotRunning(t*testing.T) {
40+
t.Parallel()
41+
ctx:=testutil.Context(t,testutil.WaitShort)
42+
43+
varroot cli.RootCmd
44+
cmd,err:=root.Command(root.AGPL())
45+
require.NoError(t,err)
46+
47+
inv:= (&serpent.Invocation{
48+
Command:cmd,
49+
Args: []string{"connect","exists","test.example"},
50+
}).WithContext(withCoderConnectNotRunning(ctx))
51+
stdout:=new(bytes.Buffer)
52+
stderr:=new(bytes.Buffer)
53+
inv.Stdout=stdout
54+
inv.Stderr=stderr
55+
err=inv.Run()
56+
require.ErrorIs(t,err,cli.ErrSilent)
57+
}
58+
59+
typefakeResolverstruct {
60+
shouldReturnSuccessbool
61+
}
62+
63+
func (f*fakeResolver)LookupIP(_ context.Context,_,_string) ([]net.IP,error) {
64+
iff.shouldReturnSuccess {
65+
return []net.IP{net.ParseIP(tsaddr.CoderServiceIPv6().String())},nil
66+
}
67+
returnnil,&net.DNSError{IsNotFound:true}
68+
}
69+
70+
funcwithCoderConnectRunning(ctx context.Context) context.Context {
71+
returnworkspacesdk.WithTestOnlyCoderContextResolver(ctx,&fakeResolver{shouldReturnSuccess:true})
72+
}
73+
74+
funcwithCoderConnectNotRunning(ctx context.Context) context.Context {
75+
returnworkspacesdk.WithTestOnlyCoderContextResolver(ctx,&fakeResolver{shouldReturnSuccess:false})
76+
}

‎cli/root.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,15 @@ import (
3131

3232
"github.com/coder/pretty"
3333

34+
"github.com/coder/serpent"
35+
3436
"github.com/coder/coder/v2/buildinfo"
3537
"github.com/coder/coder/v2/cli/cliui"
3638
"github.com/coder/coder/v2/cli/config"
3739
"github.com/coder/coder/v2/cli/gitauth"
3840
"github.com/coder/coder/v2/cli/telemetry"
3941
"github.com/coder/coder/v2/codersdk"
4042
"github.com/coder/coder/v2/codersdk/agentsdk"
41-
"github.com/coder/serpent"
4243
)
4344

4445
var (
@@ -49,6 +50,10 @@ var (
4950
workspaceCommand=map[string]string{
5051
"workspaces":"",
5152
}
53+
54+
// ErrSilent is a sentinel error that tells the command handler to just exit with a non-zero error, but not print
55+
// anything.
56+
ErrSilent=xerrors.New("silent error")
5257
)
5358

5459
const (
@@ -122,6 +127,7 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command {
122127
r.whoami(),
123128

124129
// Hidden
130+
r.connectCmd(),
125131
r.expCmd(),
126132
r.gitssh(),
127133
r.support(),
@@ -175,6 +181,10 @@ func (r *RootCmd) RunWithSubcommands(subcommands []*serpent.Command) {
175181
//nolint:revive,gocritic
176182
os.Exit(code)
177183
}
184+
iferrors.Is(err,ErrSilent) {
185+
//nolint:revive,gocritic
186+
os.Exit(code)
187+
}
178188
f:=PrettyErrorFormatter{w:os.Stderr,verbose:r.verbose}
179189
iferr!=nil {
180190
f.Format(err)

‎cli/root_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import (
1010
"sync/atomic"
1111
"testing"
1212

13+
"github.com/coder/serpent"
14+
1315
"github.com/coder/coder/v2/coderd"
1416
"github.com/coder/coder/v2/coderd/coderdtest"
1517
"github.com/coder/coder/v2/codersdk"
1618
"github.com/coder/coder/v2/pty/ptytest"
1719
"github.com/coder/coder/v2/testutil"
18-
"github.com/coder/serpent"
1920

2021
"github.com/stretchr/testify/assert"
2122
"github.com/stretchr/testify/require"

‎codersdk/workspacesdk/workspacesdk.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ import (
2020

2121
"cdr.dev/slog"
2222

23+
"github.com/coder/quartz"
24+
"github.com/coder/websocket"
25+
2326
"github.com/coder/coder/v2/codersdk"
2427
"github.com/coder/coder/v2/tailnet"
2528
"github.com/coder/coder/v2/tailnet/proto"
26-
"github.com/coder/quartz"
27-
"github.com/coder/websocket"
2829
)
2930

3031
varErrSkipClose=xerrors.New("skip tailnet close")
@@ -128,19 +129,16 @@ func init() {
128129
}
129130
}
130131

131-
typeresolverinterface {
132+
typeResolverinterface {
132133
LookupIP(ctx context.Context,network,hoststring) ([]net.IP,error)
133134
}
134135

135136
typeClientstruct {
136137
client*codersdk.Client
137-
138-
// overridden in tests
139-
resolverresolver
140138
}
141139

142140
funcNew(c*codersdk.Client)*Client {
143-
return&Client{client:c,resolver:net.DefaultResolver}
141+
return&Client{client:c}
144142
}
145143

146144
// AgentConnectionInfo returns required information for establishing
@@ -392,6 +390,12 @@ func (c *Client) AgentReconnectingPTY(ctx context.Context, opts WorkspaceAgentRe
392390
returnwebsocket.NetConn(context.Background(),conn,websocket.MessageBinary),nil
393391
}
394392

393+
funcWithTestOnlyCoderContextResolver(ctx context.Context,rResolver) context.Context {
394+
returncontext.WithValue(ctx,dnsResolverContextKey{},r)
395+
}
396+
397+
typednsResolverContextKeystruct{}
398+
395399
typeCoderConnectQueryOptionsstruct {
396400
HostnameSuffixstring
397401
}
@@ -409,15 +413,32 @@ func (c *Client) IsCoderConnectRunning(ctx context.Context, o CoderConnectQueryO
409413
suffix=info.HostnameSuffix
410414
}
411415
domainName:=fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString,suffix)
416+
returnExistsViaCoderConnect(ctx,domainName)
417+
}
418+
419+
functestOrDefaultResolver(ctx context.Context)Resolver {
420+
// check the context for a non-default resolver. This is only used in testing.
421+
resolver,ok:=ctx.Value(dnsResolverContextKey{}).(Resolver)
422+
if!ok||resolver==nil {
423+
resolver=net.DefaultResolver
424+
}
425+
returnresolver
426+
}
427+
428+
// ExistsViaCoderConnect checks if the given hostname exists via Coder Connect. This doesn't guarantee the
429+
// workspace is actually reachable, if, for example, its agent is unhealthy, but rather that Coder Connect knows about
430+
// the workspace and advertises the hostname via DNS.
431+
funcExistsViaCoderConnect(ctx context.Context,hostnamestring) (bool,error) {
432+
resolver:=testOrDefaultResolver(ctx)
412433
vardnsError*net.DNSError
413-
ips,err:=c.resolver.LookupIP(ctx,"ip6",domainName)
434+
ips,err:=resolver.LookupIP(ctx,"ip6",hostname)
414435
ifxerrors.As(err,&dnsError) {
415436
ifdnsError.IsNotFound {
416437
returnfalse,nil
417438
}
418439
}
419440
iferr!=nil {
420-
returnfalse,xerrors.Errorf("lookup DNS %s: %w",domainName,err)
441+
returnfalse,xerrors.Errorf("lookup DNS %s: %w",hostname,err)
421442
}
422443

423444
// The returned IP addresses are probably from the Coder Connect DNS server, but there are sometimes weird captive

‎codersdk/workspacesdk/workspacesdk_internal_test.go

Lines changed: 0 additions & 86 deletions
This file was deleted.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp