Skip to content

Commit 3bec160

Browse files
committed
Merge branch 'main' into abhineetjain/template-versions
2 parents 047766f + 7f54628 commit 3bec160

File tree

25 files changed

+414
-97
lines changed

25 files changed

+414
-97
lines changed

cli/configssh.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,23 @@ func sshFetchWorkspaceConfigs(ctx context.Context, client *codersdk.Client) ([]s
8989
}
9090

9191
wc := sshWorkspaceConfig{Name: workspace.Name}
92+
var agents []codersdk.WorkspaceAgent
9293
for _, resource := range resources {
9394
if resource.Transition != codersdk.WorkspaceTransitionStart {
9495
continue
9596
}
96-
for _, agent := range resource.Agents {
97-
hostname := workspace.Name
98-
if len(resource.Agents) > 1 {
99-
hostname += "." + agent.Name
100-
}
101-
wc.Hosts = append(wc.Hosts, hostname)
102-
}
97+
agents = append(agents, resource.Agents...)
98+
}
99+
100+
// handle both WORKSPACE and WORKSPACE.AGENT syntax
101+
if len(agents) == 1 {
102+
wc.Hosts = append(wc.Hosts, workspace.Name)
103+
}
104+
for _, agent := range agents {
105+
hostname := workspace.Name + "." + agent.Name
106+
wc.Hosts = append(wc.Hosts, hostname)
103107
}
108+
104109
workspaceConfigs[i] = wc
105110

106111
return nil

cli/configssh_test.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package cli_test
22

33
import (
4+
"bufio"
5+
"bytes"
46
"context"
57
"fmt"
68
"io"
@@ -692,3 +694,152 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
692694
})
693695
}
694696
}
697+
698+
func TestConfigSSH_Hostnames(t *testing.T) {
699+
t.Parallel()
700+
701+
type resourceSpec struct {
702+
name string
703+
agents []string
704+
}
705+
tests := []struct {
706+
name string
707+
resources []resourceSpec
708+
expected []string
709+
}{
710+
{
711+
name: "one resource with one agent",
712+
resources: []resourceSpec{
713+
{name: "foo", agents: []string{"agent1"}},
714+
},
715+
expected: []string{"coder.@", "coder.@.agent1"},
716+
},
717+
{
718+
name: "one resource with two agents",
719+
resources: []resourceSpec{
720+
{name: "foo", agents: []string{"agent1", "agent2"}},
721+
},
722+
expected: []string{"coder.@.agent1", "coder.@.agent2"},
723+
},
724+
{
725+
name: "two resources with one agent",
726+
resources: []resourceSpec{
727+
{name: "foo", agents: []string{"agent1"}},
728+
{name: "bar"},
729+
},
730+
expected: []string{"coder.@", "coder.@.agent1"},
731+
},
732+
{
733+
name: "two resources with two agents",
734+
resources: []resourceSpec{
735+
{name: "foo", agents: []string{"agent1"}},
736+
{name: "bar", agents: []string{"agent2"}},
737+
},
738+
expected: []string{"coder.@.agent1", "coder.@.agent2"},
739+
},
740+
}
741+
742+
for _, tt := range tests {
743+
tt := tt
744+
t.Run(tt.name, func(t *testing.T) {
745+
t.Parallel()
746+
747+
var resources []*proto.Resource
748+
for _, resourceSpec := range tt.resources {
749+
resource := &proto.Resource{
750+
Name: resourceSpec.name,
751+
Type: "aws_instance",
752+
}
753+
for _, agentName := range resourceSpec.agents {
754+
resource.Agents = append(resource.Agents, &proto.Agent{
755+
Id: uuid.NewString(),
756+
Name: agentName,
757+
})
758+
}
759+
resources = append(resources, resource)
760+
}
761+
762+
provisionResponse := []*proto.Provision_Response{{
763+
Type: &proto.Provision_Response_Complete{
764+
Complete: &proto.Provision_Complete{
765+
Resources: resources,
766+
},
767+
},
768+
}}
769+
770+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
771+
user := coderdtest.CreateFirstUser(t, client)
772+
// authToken := uuid.NewString()
773+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
774+
Parse: echo.ParseComplete,
775+
ProvisionDryRun: provisionResponse,
776+
Provision: provisionResponse,
777+
})
778+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
779+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
780+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
781+
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
782+
783+
sshConfigFile, _ := sshConfigFileNames(t)
784+
785+
cmd, root := clitest.New(t, "config-ssh", "--ssh-config-file", sshConfigFile)
786+
clitest.SetupConfig(t, client, root)
787+
doneChan := make(chan struct{})
788+
pty := ptytest.New(t)
789+
cmd.SetIn(pty.Input())
790+
cmd.SetOut(pty.Output())
791+
go func() {
792+
defer close(doneChan)
793+
err := cmd.Execute()
794+
assert.NoError(t, err)
795+
}()
796+
797+
matches := []struct {
798+
match, write string
799+
}{
800+
{match: "Continue?", write: "yes"},
801+
}
802+
for _, m := range matches {
803+
pty.ExpectMatch(m.match)
804+
pty.WriteLine(m.write)
805+
}
806+
807+
<-doneChan
808+
809+
var expectedHosts []string
810+
for _, hostnamePattern := range tt.expected {
811+
hostname := strings.ReplaceAll(hostnamePattern, "@", workspace.Name)
812+
expectedHosts = append(expectedHosts, hostname)
813+
}
814+
815+
hosts := sshConfigFileParseHosts(t, sshConfigFile)
816+
require.ElementsMatch(t, expectedHosts, hosts)
817+
})
818+
}
819+
}
820+
821+
// sshConfigFileParseHosts reads a file in the format of .ssh/config and extracts
822+
// the hostnames that are listed in "Host" directives.
823+
func sshConfigFileParseHosts(t *testing.T, name string) []string {
824+
t.Helper()
825+
b, err := os.ReadFile(name)
826+
require.NoError(t, err)
827+
828+
var result []string
829+
lineScanner := bufio.NewScanner(bytes.NewBuffer(b))
830+
for lineScanner.Scan() {
831+
line := lineScanner.Text()
832+
line = strings.TrimSpace(line)
833+
834+
tokenScanner := bufio.NewScanner(bytes.NewBufferString(line))
835+
tokenScanner.Split(bufio.ScanWords)
836+
ok := tokenScanner.Scan()
837+
if ok && tokenScanner.Text() == "Host" {
838+
for tokenScanner.Scan() {
839+
result = append(result, tokenScanner.Text())
840+
}
841+
}
842+
}
843+
844+
return result
845+
}

cli/root.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,11 @@ func checkVersions(cmd *cobra.Command, client *codersdk.Client) error {
455455
clientVersion := buildinfo.Version()
456456

457457
info, err := client.BuildInfo(cmd.Context())
458+
// Avoid printing errors that are connection-related.
459+
if codersdk.IsConnectionErr(err) {
460+
return nil
461+
}
462+
458463
if err != nil {
459464
return xerrors.Errorf("build info: %w", err)
460465
}

coderd/coderdtest/coderdtest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ func NewProvisionerDaemon(t *testing.T, coderAPI *coderd.API) io.Closer {
230230
Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug),
231231
PollInterval: 10 * time.Millisecond,
232232
UpdateInterval: 25 * time.Millisecond,
233-
ForceCancelInterval: 25 * time.Millisecond,
233+
ForceCancelInterval: time.Second,
234234
Provisioners: provisionerd.Provisioners{
235235
string(database.ProvisionerTypeEcho): proto.NewDRPCProvisionerClient(provisionersdk.Conn(echoClient)),
236236
},

coderd/templateversions_test.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ func TestPatchCancelTemplateVersion(t *testing.T) {
126126
require.ErrorAs(t, err, &apiErr)
127127
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
128128
})
129-
t.Run("Success", func(t *testing.T) {
129+
// TODO(Cian): until we are able to test cancellation properly, validating
130+
// Running -> Canceling is the best we can do for now.
131+
t.Run("Canceling", func(t *testing.T) {
130132
t.Parallel()
131133
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
132134
user := coderdtest.CreateFirstUser(t, client)
@@ -150,8 +152,11 @@ func TestPatchCancelTemplateVersion(t *testing.T) {
150152
require.Eventually(t, func() bool {
151153
var err error
152154
version, err = client.TemplateVersion(context.Background(), version.ID)
153-
require.NoError(t, err)
154-
return version.Job.Status == codersdk.ProvisionerJobCanceled
155+
return assert.NoError(t, err) &&
156+
// The job will never actually cancel successfully because it will never send a
157+
// provision complete response.
158+
assert.Empty(t, version.Job.Error) &&
159+
version.Job.Status == codersdk.ProvisionerJobCanceling
155160
}, 5*time.Second, 25*time.Millisecond)
156161
})
157162
}

coderd/workspacebuilds_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/google/uuid"
12+
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
1314

1415
"github.com/coder/coder/coderd/coderdtest"
@@ -228,8 +229,11 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
228229
require.Eventually(t, func() bool {
229230
var err error
230231
build, err = client.WorkspaceBuild(context.Background(), build.ID)
231-
require.NoError(t, err)
232-
return build.Job.Status == codersdk.ProvisionerJobCanceled
232+
return assert.NoError(t, err) &&
233+
// The job will never actually cancel successfully because it will never send a
234+
// provision complete response.
235+
assert.Empty(t, build.Job.Error) &&
236+
build.Job.Status == codersdk.ProvisionerJobCanceling
233237
}, 5*time.Second, 25*time.Millisecond)
234238
}
235239

codersdk/error.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package codersdk
22

3+
import (
4+
"net"
5+
6+
"golang.org/x/xerrors"
7+
)
8+
39
// Response represents a generic HTTP response.
410
type Response struct {
511
// Message is an actionable message that depicts actions the request took.
@@ -25,3 +31,16 @@ type ValidationError struct {
2531
Field string `json:"field" validate:"required"`
2632
Detail string `json:"detail" validate:"required"`
2733
}
34+
35+
// IsConnectionErr is a convenience function for checking if the source of an
36+
// error is due to a 'connection refused', 'no such host', etc.
37+
func IsConnectionErr(err error) bool {
38+
var (
39+
// E.g. no such host
40+
dnsErr *net.DNSError
41+
// Eg. connection refused
42+
opErr *net.OpError
43+
)
44+
45+
return xerrors.As(err, &dnsErr) || xerrors.As(err, &opErr)
46+
}

codersdk/error_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package codersdk_test
2+
3+
import (
4+
"net"
5+
"os"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
"golang.org/x/xerrors"
10+
11+
"github.com/coder/coder/codersdk"
12+
)
13+
14+
func TestIsConnectionErr(t *testing.T) {
15+
t.Parallel()
16+
17+
type tc = struct {
18+
name string
19+
err error
20+
expectedResult bool
21+
}
22+
23+
cases := []tc{
24+
{
25+
// E.g. "no such host"
26+
name: "DNSError",
27+
err: &net.DNSError{
28+
Err: "no such host",
29+
Name: "foofoo",
30+
Server: "1.1.1.1:53",
31+
IsTimeout: false,
32+
IsTemporary: false,
33+
IsNotFound: true,
34+
},
35+
expectedResult: true,
36+
},
37+
{
38+
// E.g. "connection refused"
39+
name: "OpErr",
40+
err: &net.OpError{
41+
Op: "dial",
42+
Net: "tcp",
43+
Source: nil,
44+
Addr: nil,
45+
Err: &os.SyscallError{},
46+
},
47+
expectedResult: true,
48+
},
49+
{
50+
name: "OpaqueError",
51+
err: xerrors.Errorf("I'm opaque!"),
52+
expectedResult: false,
53+
},
54+
}
55+
56+
for _, c := range cases {
57+
c := c
58+
59+
t.Run(c.name, func(t *testing.T) {
60+
t.Parallel()
61+
62+
require.Equal(t, c.expectedResult, codersdk.IsConnectionErr(c.err))
63+
})
64+
}
65+
}

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,15 @@ require (
116116
golang.org/x/net v0.0.0-20220630215102-69896b714898
117117
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2
118118
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
119-
golang.org/x/sys v0.0.0-20220624220833-87e55d714810
119+
golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d
120120
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
121121
golang.org/x/text v0.3.7
122122
golang.org/x/tools v0.1.11
123123
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f
124124
golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478
125125
golang.zx2c4.com/wireguard/tun/netstack v0.0.0-00010101000000-000000000000
126126
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b
127-
google.golang.org/api v0.86.0
127+
google.golang.org/api v0.88.0
128128
google.golang.org/protobuf v1.28.0
129129
gopkg.in/natefinch/lumberjack.v2 v2.0.0
130130
gopkg.in/yaml.v3 v3.0.1
@@ -217,7 +217,7 @@ require (
217217
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
218218
github.com/muesli/reflow v0.3.0 // indirect
219219
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
220-
github.com/nhatthm/otelsql v0.3.4
220+
github.com/nhatthm/otelsql v0.4.0
221221
github.com/niklasfasching/go-org v1.6.5 // indirect
222222
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
223223
github.com/opencontainers/go-digest v1.0.0 // indirect
@@ -269,7 +269,7 @@ require (
269269
go.opentelemetry.io/otel v1.8.0
270270
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.8.0 // indirect
271271
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.7.0
272-
go.opentelemetry.io/otel/metric v0.30.0 // indirect
272+
go.opentelemetry.io/otel/metric v0.31.0 // indirect
273273
go.opentelemetry.io/otel/sdk v1.8.0
274274
go.opentelemetry.io/otel/trace v1.8.0
275275
go.opentelemetry.io/proto/otlp v0.18.0 // indirect

0 commit comments

Comments
 (0)