Skip to content

Commit 7f54628

Browse files
authored
config-ssh: always support agent name in host alias (#3036)
1 parent c9d7cbc commit 7f54628

File tree

2 files changed

+163
-7
lines changed

2 files changed

+163
-7
lines changed

cli/configssh.go

+12-7
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

+151
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+
}

0 commit comments

Comments
 (0)