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

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

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy