Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: Add support for VS Code and JetBrains Gateway via SSH #956

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Improve table UI
  • Loading branch information
kylecarbs committed Apr 10, 2022
commit 6ed0bc0ed4e92aa6ebb5cb584e4ab5c2c7139c47
125 changes: 99 additions & 26 deletions cli/cliui/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,135 @@ package cliui

import (
"fmt"
"io"
"sort"
"text/tabwriter"
"strconv"

"github.com/jedib0t/go-pretty/v6/table"

"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/spf13/cobra"
"github.com/xlab/treeprint"
)

func WorkspaceResources(cmd *cobra.Command, resources []codersdk.WorkspaceResource) error {
type WorkspaceResourcesOptions struct {
WorkspaceName string
HideAgentState bool
HideAccess bool
}

// WorkspaceResources displays the connection status and tree-view of provided resources.
// ┌────────────────────────────────────────────────────────────────────────────┐
// │ RESOURCE STATE ACCESS │
// ├────────────────────────────────────────────────────────────────────────────┤
// │ google_compute_disk.root persists on stop │
// ├────────────────────────────────────────────────────────────────────────────┤
// │ google_compute_instance.dev │
// │ └─ dev (linux, amd64) ⦾ connecting [10s] coder ssh wow.dev │
// ├────────────────────────────────────────────────────────────────────────────┤
// │ kubernetes_pod.dev │
// │ ├─ go (linux, amd64) ⦿ connected coder ssh wow.go │
// │ └─ postgres (linux, amd64) ⦾ disconnected [4s] coder ssh wow.postgres │
// └────────────────────────────────────────────────────────────────────────────┘
func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource, options WorkspaceResourcesOptions) error {
// Sort resources by type for consistent output.
sort.Slice(resources, func(i, j int) bool {
return fmt.Sprintf("%s.%s", resources[i].Type, resources[i].Name) < fmt.Sprintf("%s.%s", resources[j].Type, resources[j].Name)
return resources[i].Type < resources[j].Type
})

// Address on stop indexes whether a resource still exists when in the stopped transition.
addressOnStop := map[string]codersdk.WorkspaceResource{}
for _, resource := range resources {
if resource.Transition != database.WorkspaceTransitionStop {
continue
}
addressOnStop[resource.Address] = resource
}

writer := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 4, ' ', 0)
_, _ = fmt.Fprintf(writer, "Type\tName\tGood\n")
// Displayed stores whether a resource has already been shown.
// Resources can be stored with numerous states, which we
// process prior to display.
displayed := map[string]struct{}{}

tableWriter := table.NewWriter()
tableWriter.SetStyle(table.StyleLight)
tableWriter.Style().Options.SeparateColumns = false
row := table.Row{"Resource", "State"}
if !options.HideAccess {
row = append(row, "Access")
}
tableWriter.AppendHeader(row)

totalAgents := 0
for _, resource := range resources {
totalAgents += len(resource.Agents)
}

for _, resource := range resources {
if resource.Type == "random_string" {
// Hide resources that aren't substantial to a user!
// This is an unfortunate case, and we should allow
// callers to hide resources eventually.
continue
}
_, alreadyShown := displayed[resource.Address]
if alreadyShown {
if _, shown := displayed[resource.Address]; shown {
// The same resource can have multiple transitions.
continue
}
displayed[resource.Address] = struct{}{}

tree := treeprint.NewWithRoot(resource.Type + "." + resource.Name)

// _, _ = fmt.Fprintln(cmd.OutOrStdout(), resource.Type+"."+resource.Name)
// Sort agents by name for consistent output.
sort.Slice(resource.Agents, func(i, j int) bool {
return resource.Agents[i].Name < resource.Agents[j].Name
})
_, existsOnStop := addressOnStop[resource.Address]
resourceState := ""
if existsOnStop {
// _, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+Styles.Warn.Render("~ persistent"))
} else {
// _, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+Styles.Keyword.Render("+ start")+Styles.Placeholder.Render(" (deletes on stop)"))
resourceState = Styles.Placeholder.Render("persists on rebuild")
}
// Display a line for the resource.
tableWriter.AppendRow(table.Row{
Styles.Bold.Render(resource.Type + "." + resource.Name),
resourceState,
"",
})
// Display all agents associated with the resource.
for index, agent := range resource.Agents {
sshCommand := "coder ssh " + options.WorkspaceName
if totalAgents > 1 {
sshCommand += "." + agent.Name
}
sshCommand = Styles.Code.Render(sshCommand)
var agentStatus string
if !options.HideAgentState {
switch agent.Status {
case codersdk.WorkspaceAgentConnecting:
since := database.Now().Sub(agent.CreatedAt)
agentStatus = Styles.Warn.Render("⦾ connecting") + " " +
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
case codersdk.WorkspaceAgentDisconnected:
since := database.Now().Sub(*agent.DisconnectedAt)
agentStatus = Styles.Error.Render("⦾ disconnected") + " " +
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
case codersdk.WorkspaceAgentConnected:
agentStatus = Styles.Keyword.Render("⦿ connected")
}
}

for _, agent := range resource.Agents {
tree.AddNode(agent.Name + " " + agent.OperatingSystem)
pipe := "├"
if index == len(resource.Agents)-1 {
pipe = "└"
}
row := table.Row{
// These tree from a resource!
fmt.Sprintf("%s─ %s (%s, %s)", pipe, agent.Name, agent.OperatingSystem, agent.Architecture),
agentStatus,
}
if !options.HideAccess {
row = append(row, sshCommand)
}
tableWriter.AppendRow(row)
}

fmt.Fprintln(cmd.OutOrStdout(), tree.String())

// if resource.Agent != nil {
// _, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+Styles.Fuschia.Render("▲ allows ssh"))
// }
// _, _ = fmt.Fprintln(cmd.OutOrStdout())
tableWriter.AppendSeparator()
}
return writer.Flush()
_, err := fmt.Fprintln(writer, tableWriter.Render())
return err
}
85 changes: 85 additions & 0 deletions cli/cliui/resources_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package cliui_test

import (
"testing"
"time"

"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
)

func TestWorkspaceResources(t *testing.T) {
t.Parallel()
t.Run("SingleAgentSSH", func(t *testing.T) {
t.Parallel()
ptty := ptytest.New(t)
cliui.WorkspaceResources(ptty.Output(), []codersdk.WorkspaceResource{{
Type: "google_compute_instance",
Name: "dev",
Transition: database.WorkspaceTransitionStart,
Agents: []codersdk.WorkspaceAgent{{
Name: "dev",
Status: codersdk.WorkspaceAgentConnected,
Architecture: "amd64",
OperatingSystem: "linux",
}},
}}, cliui.WorkspaceResourcesOptions{
WorkspaceName: "example",
})
ptty.ExpectMatch("coder ssh example")
})

t.Run("MultipleStates", func(t *testing.T) {
t.Parallel()
ptty := ptytest.New(t)
disconnected := database.Now().Add(-4 * time.Second)
cliui.WorkspaceResources(ptty.Output(), []codersdk.WorkspaceResource{{
Address: "disk",
Transition: database.WorkspaceTransitionStart,
Type: "google_compute_disk",
Name: "root",
}, {
Address: "disk",
Transition: database.WorkspaceTransitionStop,
Type: "google_compute_disk",
Name: "root",
}, {
Address: "another",
Transition: database.WorkspaceTransitionStart,
Type: "google_compute_instance",
Name: "dev",
Agents: []codersdk.WorkspaceAgent{{
CreatedAt: database.Now().Add(-10 * time.Second),
Status: codersdk.WorkspaceAgentConnecting,
Name: "dev",
OperatingSystem: "linux",
Architecture: "amd64",
}},
}, {
Transition: database.WorkspaceTransitionStart,
Type: "kubernetes_pod",
Name: "dev",
Agents: []codersdk.WorkspaceAgent{{
Status: codersdk.WorkspaceAgentConnected,
Name: "go",
Architecture: "amd64",
OperatingSystem: "linux",
}, {
DisconnectedAt: &disconnected,
Status: codersdk.WorkspaceAgentDisconnected,
Name: "postgres",
Architecture: "amd64",
OperatingSystem: "linux",
}},
}}, cliui.WorkspaceResourcesOptions{
WorkspaceName: "dev",
HideAgentState: false,
HideAccess: false,
})
ptty.ExpectMatch("google_compute_disk.root")
ptty.ExpectMatch("google_compute_instance.dev")
ptty.ExpectMatch("coder ssh dev.postgres")
})
}
3 changes: 2 additions & 1 deletion cli/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ func ssh() *cobra.Command {
return err
}

if isatty.IsTerminal(os.Stdout.Fd()) {
stdoutFile, valid := cmd.OutOrStdout().(*os.File)
if valid && isatty.IsTerminal(stdoutFile.Fd()) {
state, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
return err
Expand Down
25 changes: 11 additions & 14 deletions cli/ssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,31 +54,28 @@ func TestSSH(t *testing.T) {
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID)
go func() {
// Run this async so the SSH command has to wait for
// the build and agent to connect!
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
agentClient := codersdk.New(client.URL)
agentClient.SessionToken = agentToken
agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &peer.ConnOptions{
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
})
t.Cleanup(func() {
_ = agentCloser.Close()
})
}()

cmd, root := clitest.New(t, "ssh", workspace.Name)
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetErr(pty.Output())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
require.NoError(t, err)
}()
pty.ExpectMatch("Waiting")
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
agentClient := codersdk.New(client.URL)
agentClient.SessionToken = agentToken
agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &peer.ConnOptions{
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
})
t.Cleanup(func() {
_ = agentCloser.Close()
})
// Shells on Mac, Windows, and Linux all exit shells with the "exit" command.
pty.WriteLine("exit")
<-doneChan
Expand Down
5 changes: 4 additions & 1 deletion cli/templatecreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,5 +196,8 @@ func createValidTemplateVersion(cmd *cobra.Command, client *codersdk.Client, org
if err != nil {
return nil, nil, err
}
return &version, parameters, displayTemplateVersionInfo(cmd, resources)
return &version, parameters, cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{
HideAgentState: true,
HideAccess: true,
})
}
49 changes: 0 additions & 49 deletions cli/templates.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
package cli

import (
"fmt"
"sort"

"github.com/fatih/color"
"github.com/spf13/cobra"

"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
)

func templates() *cobra.Command {
Expand Down Expand Up @@ -41,45 +34,3 @@ func templates() *cobra.Command {

return cmd
}

func displayTemplateVersionInfo(cmd *cobra.Command, resources []codersdk.WorkspaceResource) error {
fmt.Printf("Previewing template!\n")

sort.Slice(resources, func(i, j int) bool {
return fmt.Sprintf("%s.%s", resources[i].Type, resources[i].Name) < fmt.Sprintf("%s.%s", resources[j].Type, resources[j].Name)
})

addressOnStop := map[string]codersdk.WorkspaceResource{}
for _, resource := range resources {
if resource.Transition != database.WorkspaceTransitionStop {
continue
}
addressOnStop[resource.Address] = resource
}

displayed := map[string]struct{}{}
for _, resource := range resources {
if resource.Type == "random_string" {
// Hide resources that aren't substantial to a user!
continue
}
_, alreadyShown := displayed[resource.Address]
if alreadyShown {
continue
}
displayed[resource.Address] = struct{}{}

_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Bold.Render("resource."+resource.Type+"."+resource.Name))
_, existsOnStop := addressOnStop[resource.Address]
if existsOnStop {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Warn.Render("~ persistent"))
} else {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Keyword.Render("+ start")+cliui.Styles.Placeholder.Render(" (deletes on stop)"))
}
if len(resource.Agents) > 0 {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Fuschia.Render("▲ allows ssh"))
}
_, _ = fmt.Fprintln(cmd.OutOrStdout())
}
return nil
}
6 changes: 5 additions & 1 deletion cli/workspacecreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@ func workspaceCreate() *cobra.Command {
if err != nil {
return err
}
err = displayTemplateVersionInfo(cmd, resources)
err = cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{
WorkspaceName: workspaceName,
// Since agent's haven't connected yet, hiding this makes more sense.
HideAgentState: true,
})
if err != nil {
return err
}
Expand Down
Loading