feat: Allow workspace resources to attach multiple agents (#942)

This enables a "kubernetes_pod" to attach multiple agents that
could be for multiple services. Each agent is required to have
a unique name, so SSH syntax is:

`coder ssh <workspace>.<agent>`

A resource can have zero agents too, they aren't required.
This commit is contained in:
Kyle Carberry
2022-04-11 16:06:15 -05:00
committed by GitHub
parent 2835bb45e5
commit 19b4323512
47 changed files with 1550 additions and 1040 deletions
+8 -8
View File
@@ -15,7 +15,7 @@ import (
type AgentOptions struct {
WorkspaceName string
Fetch func(context.Context) (codersdk.WorkspaceResource, error)
Fetch func(context.Context) (codersdk.WorkspaceAgent, error)
FetchInterval time.Duration
WarnInterval time.Duration
}
@@ -29,20 +29,20 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
opts.WarnInterval = 30 * time.Second
}
var resourceMutex sync.Mutex
resource, err := opts.Fetch(ctx)
agent, err := opts.Fetch(ctx)
if err != nil {
return xerrors.Errorf("fetch: %w", err)
}
if resource.Agent.Status == codersdk.WorkspaceAgentConnected {
if agent.Status == codersdk.WorkspaceAgentConnected {
return nil
}
if resource.Agent.Status == codersdk.WorkspaceAgentDisconnected {
if agent.Status == codersdk.WorkspaceAgentDisconnected {
opts.WarnInterval = 0
}
spin := spinner.New(spinner.CharSets[78], 100*time.Millisecond, spinner.WithColor("fgHiGreen"))
spin.Writer = writer
spin.ForceOutput = true
spin.Suffix = " Waiting for connection from " + Styles.Field.Render(resource.Type+"."+resource.Name) + "..."
spin.Suffix = " Waiting for connection from " + Styles.Field.Render(agent.Name) + "..."
spin.Start()
defer spin.Stop()
@@ -59,7 +59,7 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
resourceMutex.Lock()
defer resourceMutex.Unlock()
message := "Don't panic, your workspace is booting up!"
if resource.Agent.Status == codersdk.WorkspaceAgentDisconnected {
if agent.Status == codersdk.WorkspaceAgentDisconnected {
message = "The workspace agent lost connection! Wait for it to reconnect or run: " + Styles.Code.Render("coder workspaces rebuild "+opts.WorkspaceName)
}
// This saves the cursor position, then defers clearing from the cursor
@@ -74,11 +74,11 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
case <-ticker.C:
}
resourceMutex.Lock()
resource, err = opts.Fetch(ctx)
agent, err = opts.Fetch(ctx)
if err != nil {
return xerrors.Errorf("fetch: %w", err)
}
if resource.Agent.Status != codersdk.WorkspaceAgentConnected {
if agent.Status != codersdk.WorkspaceAgentConnected {
resourceMutex.Unlock()
continue
}
+5 -7
View File
@@ -22,16 +22,14 @@ func TestAgent(t *testing.T) {
RunE: func(cmd *cobra.Command, args []string) error {
err := cliui.Agent(cmd.Context(), cmd.OutOrStdout(), cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(ctx context.Context) (codersdk.WorkspaceResource, error) {
resource := codersdk.WorkspaceResource{
Agent: &codersdk.WorkspaceAgent{
Status: codersdk.WorkspaceAgentDisconnected,
},
Fetch: func(ctx context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
Status: codersdk.WorkspaceAgentDisconnected,
}
if disconnected.Load() {
resource.Agent.Status = codersdk.WorkspaceAgentConnected
agent.Status = codersdk.WorkspaceAgentConnected
}
return resource, nil
return agent, nil
},
FetchInterval: time.Millisecond,
WarnInterval: 10 * time.Millisecond,
+18 -16
View File
@@ -15,6 +15,7 @@ import (
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
)
@@ -70,29 +71,30 @@ func configSSH() *cobra.Command {
for _, workspace := range workspaces {
workspace := workspace
errGroup.Go(func() error {
resources, err := client.WorkspaceResourcesByBuild(cmd.Context(), workspace.LatestBuild.ID)
resources, err := client.TemplateVersionResources(cmd.Context(), workspace.LatestBuild.TemplateVersionID)
if err != nil {
return err
}
resourcesWithAgents := make([]codersdk.WorkspaceResource, 0)
for _, resource := range resources {
if resource.Agent == nil {
if resource.Transition != database.WorkspaceTransitionStart {
continue
}
resourcesWithAgents = append(resourcesWithAgents, resource)
for _, agent := range resource.Agents {
sshConfigContentMutex.Lock()
hostname := workspace.Name
if len(resource.Agents) > 1 {
hostname += "." + agent.Name
}
sshConfigContent += strings.Join([]string{
"Host coder." + hostname,
"\tHostName coder." + hostname,
fmt.Sprintf("\tProxyCommand %q ssh --stdio %s", binPath, hostname),
"\tConnectTimeout=0",
"\tStrictHostKeyChecking=no",
}, "\n") + "\n"
sshConfigContentMutex.Unlock()
}
}
sshConfigContentMutex.Lock()
defer sshConfigContentMutex.Unlock()
if len(resourcesWithAgents) == 1 {
sshConfigContent += strings.Join([]string{
"Host coder." + workspace.Name,
"\tHostName coder." + workspace.Name,
fmt.Sprintf("\tProxyCommand %q ssh --stdio %s", binPath, workspace.Name),
"\tConnectTimeout=0",
"\tStrictHostKeyChecking=no",
}, "\n") + "\n"
}
return nil
})
}
+3 -3
View File
@@ -49,11 +49,11 @@ func TestGitSSH(t *testing.T) {
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
Agent: &proto.Agent{
Agents: []*proto.Agent{{
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
},
}},
}},
},
},
@@ -81,7 +81,7 @@ func TestGitSSH(t *testing.T) {
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].ID, nil, nil)
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil, nil)
require.NoError(t, err)
defer dialer.Close()
_, err = dialer.Ping()
+24 -28
View File
@@ -7,6 +7,7 @@ import (
"os"
"time"
"github.com/google/uuid"
"github.com/mattn/go-isatty"
"github.com/pion/webrtc/v3"
"github.com/spf13/cobra"
@@ -25,7 +26,7 @@ func ssh() *cobra.Command {
stdio bool
)
cmd := &cobra.Command{
Use: "ssh <workspace> [resource]",
Use: "ssh <workspace> [agent]",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
@@ -57,50 +58,45 @@ func ssh() *cobra.Command {
return err
}
resourceByAddress := make(map[string]codersdk.WorkspaceResource)
agents := make([]codersdk.WorkspaceAgent, 0)
for _, resource := range resources {
if resource.Agent == nil {
continue
}
resourceByAddress[resource.Address] = resource
agents = append(agents, resource.Agents...)
}
var resourceAddress string
if len(agents) == 0 {
return xerrors.New("workspace has no agents")
}
var agent codersdk.WorkspaceAgent
if len(args) >= 2 {
resourceAddress = args[1]
} else {
// No resource name was provided!
if len(resourceByAddress) > 1 {
// List available resources to connect into?
return xerrors.Errorf("multiple agents")
}
for _, resource := range resourceByAddress {
resourceAddress = resource.Address
for _, otherAgent := range agents {
if otherAgent.Name != args[1] {
continue
}
agent = otherAgent
break
}
}
resource, exists := resourceByAddress[resourceAddress]
if !exists {
resourceKeys := make([]string, 0)
for resourceKey := range resourceByAddress {
resourceKeys = append(resourceKeys, resourceKey)
if agent.ID == uuid.Nil {
return xerrors.Errorf("agent not found by name %q", args[1])
}
return xerrors.Errorf("no sshable agent with address %q: %+v", resourceAddress, resourceKeys)
}
if agent.ID == uuid.Nil {
if len(agents) > 1 {
return xerrors.New("you must specify the name of an agent")
}
agent = agents[0]
}
// OpenSSH passes stderr directly to the calling TTY.
// This is required in "stdio" mode so a connecting indicator can be displayed.
err = cliui.Agent(cmd.Context(), cmd.ErrOrStderr(), cliui.AgentOptions{
WorkspaceName: workspace.Name,
Fetch: func(ctx context.Context) (codersdk.WorkspaceResource, error) {
return client.WorkspaceResource(ctx, resource.ID)
Fetch: func(ctx context.Context) (codersdk.WorkspaceAgent, error) {
return client.WorkspaceAgent(ctx, agent.ID)
},
})
if err != nil {
return xerrors.Errorf("await agent: %w", err)
}
conn, err := client.DialWorkspaceAgent(cmd.Context(), resource.ID, []webrtc.ICEServer{{
conn, err := client.DialWorkspaceAgent(cmd.Context(), agent.ID, []webrtc.ICEServer{{
URLs: []string{"stun:stun.l.google.com:19302"},
}}, nil)
if err != nil {
+4 -4
View File
@@ -40,12 +40,12 @@ func TestSSH(t *testing.T) {
Resources: []*proto.Resource{{
Name: "dev",
Type: "google_compute_instance",
Agent: &proto.Agent{
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Auth: &proto.Agent_Token{
Token: agentToken,
},
},
}},
}},
},
},
@@ -98,12 +98,12 @@ func TestSSH(t *testing.T) {
Resources: []*proto.Resource{{
Name: "dev",
Type: "google_compute_instance",
Agent: &proto.Agent{
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Auth: &proto.Agent_Token{
Token: agentToken,
},
},
}},
}},
},
},
+1 -1
View File
@@ -74,7 +74,7 @@ func displayTemplateVersionInfo(cmd *cobra.Command, resources []codersdk.Workspa
} else {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Keyword.Render("+ start")+cliui.Styles.Placeholder.Render(" (deletes on stop)"))
}
if resource.Agent != nil {
if len(resource.Agents) > 0 {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Fuschia.Render("▲ allows ssh"))
}
_, _ = fmt.Fprintln(cmd.OutOrStdout())
+6 -6
View File
@@ -32,11 +32,11 @@ func TestWorkspaceAgent(t *testing.T) {
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
Agent: &proto.Agent{
Agents: []*proto.Agent{{
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
},
}},
}},
},
},
@@ -61,7 +61,7 @@ func TestWorkspaceAgent(t *testing.T) {
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].ID, nil, nil)
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil, nil)
require.NoError(t, err)
defer dialer.Close()
_, err = dialer.Ping()
@@ -86,11 +86,11 @@ func TestWorkspaceAgent(t *testing.T) {
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
Agent: &proto.Agent{
Agents: []*proto.Agent{{
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
},
}},
}},
},
},
@@ -115,7 +115,7 @@ func TestWorkspaceAgent(t *testing.T) {
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].ID, nil, nil)
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil, nil)
require.NoError(t, err)
defer dialer.Close()
_, err = dialer.Ping()
-23
View File
@@ -1,36 +1,13 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coder/coder/codersdk"
)
func workspaceShow() *cobra.Command {
return &cobra.Command{
Use: "show",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
if err != nil {
return err
}
resources, err := client.WorkspaceResourcesByBuild(cmd.Context(), workspace.LatestBuild.ID)
if err != nil {
return err
}
for _, resource := range resources {
if resource.Agent == nil {
continue
}
_, _ = fmt.Printf("Agent: %+v\n", resource.Agent)
}
return nil
},
}