Stop showing persistent vs ephemeral for resources (#2333)

Signed-off-by: Spike Curtis <spike@coder.com>
This commit is contained in:
Spike Curtis
2022-06-16 11:36:11 -07:00
committed by GitHub
parent 552dad6919
commit 93b1425d85
5 changed files with 147 additions and 61 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/table"
"github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk" "github.com/coder/coder/codersdk"
) )
@ -23,12 +24,12 @@ type WorkspaceResourcesOptions struct {
// ┌────────────────────────────────────────────────────────────────────────────┐ // ┌────────────────────────────────────────────────────────────────────────────┐
// │ RESOURCE STATUS ACCESS │ // │ RESOURCE STATUS ACCESS │
// ├────────────────────────────────────────────────────────────────────────────┤ // ├────────────────────────────────────────────────────────────────────────────┤
// │ google_compute_disk.root persistent // │ google_compute_disk.root
// ├────────────────────────────────────────────────────────────────────────────┤ // ├────────────────────────────────────────────────────────────────────────────┤
// │ google_compute_instance.dev ephemeral // │ google_compute_instance.dev
// │ └─ dev (linux, amd64) ⦾ connecting [10s] coder ssh dev.dev │ // │ └─ dev (linux, amd64) ⦾ connecting [10s] coder ssh dev.dev │
// ├────────────────────────────────────────────────────────────────────────────┤ // ├────────────────────────────────────────────────────────────────────────────┤
// │ kubernetes_pod.dev ephemeral // │ kubernetes_pod.dev
// │ ├─ go (linux, amd64) ⦿ connected coder ssh dev.go │ // │ ├─ go (linux, amd64) ⦿ connected coder ssh dev.go │
// │ └─ postgres (linux, amd64) ⦾ disconnected [4s] coder ssh dev.postgres │ // │ └─ postgres (linux, amd64) ⦾ disconnected [4s] coder ssh dev.postgres │
// └────────────────────────────────────────────────────────────────────────────┘ // └────────────────────────────────────────────────────────────────────────────┘
@ -38,26 +39,16 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
return resources[i].Type < resources[j].Type 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 != codersdk.WorkspaceTransitionStop {
continue
}
addressOnStop[resource.Type+"."+resource.Name] = resource
}
// 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 := table.NewWriter()
if options.Title != "" { if options.Title != "" {
tableWriter.SetTitle(options.Title) tableWriter.SetTitle(options.Title)
} }
tableWriter.SetStyle(table.StyleLight) tableWriter.SetStyle(table.StyleLight)
tableWriter.Style().Options.SeparateColumns = false tableWriter.Style().Options.SeparateColumns = false
row := table.Row{"Resource", "Status"} row := table.Row{"Resource"}
if !options.HideAgentState {
row = append(row, "Status")
}
if !options.HideAccess { if !options.HideAccess {
row = append(row, "Access") row = append(row, "Access")
} }
@ -76,34 +67,29 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
continue continue
} }
resourceAddress := resource.Type + "." + resource.Name resourceAddress := resource.Type + "." + resource.Name
if _, shown := displayed[resourceAddress]; shown {
// The same resource can have multiple transitions.
continue
}
displayed[resourceAddress] = struct{}{}
// Sort agents by name for consistent output. // Sort agents by name for consistent output.
sort.Slice(resource.Agents, func(i, j int) bool { sort.Slice(resource.Agents, func(i, j int) bool {
return resource.Agents[i].Name < resource.Agents[j].Name return resource.Agents[i].Name < resource.Agents[j].Name
}) })
_, existsOnStop := addressOnStop[resourceAddress]
resourceState := "ephemeral"
if existsOnStop {
resourceState = "persistent"
}
// Display a line for the resource. // Display a line for the resource.
tableWriter.AppendRow(table.Row{ tableWriter.AppendRow(table.Row{
Styles.Bold.Render(resourceAddress), Styles.Bold.Render(resourceAddress),
Styles.Placeholder.Render(resourceState), "",
"", "",
}) })
// Display all agents associated with the resource. // Display all agents associated with the resource.
for index, agent := range resource.Agents { for index, agent := range resource.Agents {
sshCommand := "coder ssh " + options.WorkspaceName pipe := "├"
if totalAgents > 1 { if index == len(resource.Agents)-1 {
sshCommand += "." + agent.Name pipe = ""
} }
sshCommand = Styles.Code.Render(sshCommand) row := table.Row{
// These tree from a resource!
fmt.Sprintf("%s─ %s (%s, %s)", pipe, agent.Name, agent.OperatingSystem, agent.Architecture),
}
if !options.HideAgentState {
var agentStatus string var agentStatus string
if !options.HideAgentState { if !options.HideAgentState {
switch agent.Status { switch agent.Status {
@ -119,17 +105,14 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
agentStatus = Styles.Keyword.Render("⦿ connected") agentStatus = Styles.Keyword.Render("⦿ connected")
} }
} }
row = append(row, agentStatus)
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 { if !options.HideAccess {
sshCommand := "coder ssh " + options.WorkspaceName
if totalAgents > 1 {
sshCommand += "." + agent.Name
}
sshCommand = Styles.Code.Render(sshCommand)
row = append(row, sshCommand) row = append(row, sshCommand)
} }
tableWriter.AppendRow(row) tableWriter.AppendRow(row)

View File

@ -27,7 +27,11 @@ func TestCreate(t *testing.T) {
t.Parallel() t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: provisionCompleteWithAgent,
ProvisionDryRun: provisionCompleteWithAgent,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
args := []string{ args := []string{
@ -51,14 +55,19 @@ func TestCreate(t *testing.T) {
err := cmd.Execute() err := cmd.Execute()
assert.NoError(t, err) assert.NoError(t, err)
}() }()
matches := []string{ matches := []struct {
"Confirm create", "yes", match string
write string
}{
{match: "compute.main"},
{match: "smith (linux, i386)"},
{match: "Confirm create", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
if len(m.write) > 0 {
pty.WriteLine(m.write)
} }
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
} }
<-doneChan <-doneChan
}) })

61
cli/show_test.go Normal file
View File

@ -0,0 +1,61 @@
package cli_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/pty/ptytest"
)
func TestShow(t *testing.T) {
t.Parallel()
t.Run("Exists", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: provisionCompleteWithAgent,
ProvisionDryRun: provisionCompleteWithAgent,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
args := []string{
"show",
workspace.Name,
}
cmd, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
assert.NoError(t, err)
}()
matches := []struct {
match string
write string
}{
{match: "compute.main"},
{match: "smith (linux, i386)"},
{match: "coder ssh " + workspace.Name},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
if len(m.write) > 0 {
pty.WriteLine(m.write)
}
}
<-doneChan
})
}

View File

@ -230,7 +230,14 @@ func createValidTemplateVersion(cmd *cobra.Command, client *codersdk.Client, org
return nil, nil, err return nil, nil, err
} }
err = cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{ // Only display the resources on the start transition, to avoid listing them more than once.
var startResources []codersdk.WorkspaceResource
for _, r := range resources {
if r.Transition == codersdk.WorkspaceTransitionStart {
startResources = append(startResources, r)
}
}
err = cliui.WorkspaceResources(cmd.OutOrStdout(), startResources, cliui.WorkspaceResourcesOptions{
HideAgentState: true, HideAgentState: true,
HideAccess: true, HideAccess: true,
Title: "Template Preview", Title: "Template Preview",

View File

@ -14,6 +14,28 @@ import (
"github.com/coder/coder/pty/ptytest" "github.com/coder/coder/pty/ptytest"
) )
var provisionCompleteWithAgent = []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{
{
Type: "compute",
Name: "main",
Agents: []*proto.Agent{
{
Name: "smith",
OperatingSystem: "linux",
Architecture: "i386",
},
},
},
},
},
},
},
}
func TestTemplateCreate(t *testing.T) { func TestTemplateCreate(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("Create", func(t *testing.T) { t.Run("Create", func(t *testing.T) {
@ -22,7 +44,7 @@ func TestTemplateCreate(t *testing.T) {
coderdtest.CreateFirstUser(t, client) coderdtest.CreateFirstUser(t, client)
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
Provision: echo.ProvisionComplete, Provision: provisionCompleteWithAgent,
}) })
args := []string{ args := []string{
"templates", "templates",
@ -49,12 +71,16 @@ func TestTemplateCreate(t *testing.T) {
write string write string
}{ }{
{match: "Create and upload", write: "yes"}, {match: "Create and upload", write: "yes"},
{match: "compute.main"},
{match: "smith (linux, i386)"},
{match: "Confirm create?", write: "yes"}, {match: "Confirm create?", write: "yes"},
} }
for _, m := range matches { for _, m := range matches {
pty.ExpectMatch(m.match) pty.ExpectMatch(m.match)
if len(m.write) > 0 {
pty.WriteLine(m.write) pty.WriteLine(m.write)
} }
}
require.NoError(t, <-execDone) require.NoError(t, <-execDone)
}) })