Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: Improve resource preview and first-time experience#946

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
kylecarbs merged 21 commits intomainfromclihelp
Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
21 commits
Select commitHold shift + click to select a range
85745ab
Improve CLI documentation
kylecarbsApr 8, 2022
e8258ea
feat: Allow workspace resources to attach multiple agents
kylecarbsApr 8, 2022
6abc4a4
Merge branch 'updatetfprovider' into clihelp
kylecarbsApr 9, 2022
b0cb66d
Add tree view
kylecarbsApr 9, 2022
6ed0bc0
Improve table UI
kylecarbsApr 9, 2022
3ad0766
feat: Allow workspace resources to attach multiple agents
kylecarbsApr 8, 2022
9cbadef
Merge branch 'updatetfprovider' into clihelp
kylecarbsApr 10, 2022
4496f36
Rename `tunnel` to `skip-tunnel`
kylecarbsApr 10, 2022
65c19bd
Add disclaimer about editing templates
kylecarbsApr 10, 2022
c50d170
Add help to template create
kylecarbsApr 10, 2022
326f2d5
Improve workspace create flow
kylecarbsApr 10, 2022
008ba69
Add end-to-end test for config-ssh
kylecarbsApr 11, 2022
71e4544
Improve testing of config-ssh
kylecarbsApr 11, 2022
8fecb67
Fix workspace list
kylecarbsApr 11, 2022
3e31c06
Merge branch 'main' into clihelp
kylecarbsApr 11, 2022
677686b
Fix config ssh tests
kylecarbsApr 11, 2022
603bd90
Update cli/configssh.go
kylecarbsApr 11, 2022
2a6c607
Fix requested changes
kylecarbsApr 11, 2022
fec214d
Merge branch 'clihelp' of github.com:coder/coder into clihelp
kylecarbsApr 11, 2022
f397afc
Remove socat requirement
kylecarbsApr 11, 2022
683f87e
Fix resources not reading in TTY
kylecarbsApr 11, 2022
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
5 changes: 5 additions & 0 deletionsagent/agent.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,11 @@ func (a *agent) run(ctx context.Context) {

func (a *agent) handlePeerConn(ctx context.Context, conn *peer.Conn) {
go func() {
select {
case <-a.closed:
_ = conn.Close()
case <-conn.Closed():
}
<-conn.Closed()
a.connCloseWait.Done()
}()
Expand Down
91 changes: 80 additions & 11 deletionsagent/agent_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,12 @@ package agent_test

import (
"context"
"fmt"
"io"
"net"
"os/exec"
"runtime"
"strconv"
"strings"
"testing"

Expand All@@ -29,7 +34,8 @@ func TestAgent(t *testing.T) {
t.Parallel()
t.Run("SessionExec", func(t *testing.T) {
t.Parallel()
session := setupSSH(t)
session := setupSSHSession(t)

command := "echo test"
if runtime.GOOS == "windows" {
command = "cmd.exe /c echo test"
Expand All@@ -41,7 +47,7 @@ func TestAgent(t *testing.T) {

t.Run("GitSSH", func(t *testing.T) {
t.Parallel()
session :=setupSSH(t)
session :=setupSSHSession(t)
command := "sh -c 'echo $GIT_SSH_COMMAND'"
if runtime.GOOS == "windows" {
command = "cmd.exe /c echo %GIT_SSH_COMMAND%"
Expand All@@ -53,7 +59,7 @@ func TestAgent(t *testing.T) {

t.Run("SessionTTY", func(t *testing.T) {
t.Parallel()
session :=setupSSH(t)
session :=setupSSHSession(t)
prompt := "$"
command := "bash"
if runtime.GOOS == "windows" {
Expand All@@ -76,9 +82,77 @@ func TestAgent(t *testing.T) {
err = session.Wait()
require.NoError(t, err)
})

t.Run("LocalForwarding", func(t *testing.T) {
t.Parallel()
random, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
_ = random.Close()
tcpAddr, valid := random.Addr().(*net.TCPAddr)
require.True(t, valid)
randomPort := tcpAddr.Port

local, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
tcpAddr, valid = local.Addr().(*net.TCPAddr)
require.True(t, valid)
localPort := tcpAddr.Port
done := make(chan struct{})
go func() {
conn, err := local.Accept()
require.NoError(t, err)
_ = conn.Close()
close(done)
}()

err = setupSSHCommand(t, []string{"-L", fmt.Sprintf("%d:127.0.0.1:%d", randomPort, localPort)}, []string{"echo", "test"}).Start()
require.NoError(t, err)

conn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(localPort))
require.NoError(t, err)
conn.Close()
<-done
})
}

func setupSSH(t *testing.T) *ssh.Session {
func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd {
agentConn := setupAgent(t)
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
ssh, err := agentConn.SSH()
require.NoError(t, err)
go io.Copy(conn, ssh)
go io.Copy(ssh, conn)
}
}()
t.Cleanup(func() {
_ = listener.Close()
})
tcpAddr, valid := listener.Addr().(*net.TCPAddr)
require.True(t, valid)
args := append(beforeArgs,
"-o", "HostName "+tcpAddr.IP.String(),
"-o", "Port "+strconv.Itoa(tcpAddr.Port),
"-o", "StrictHostKeyChecking=no", "host")
args = append(args, afterArgs...)
return exec.Command("ssh", args...)
}

func setupSSHSession(t *testing.T) *ssh.Session {
sshClient, err := setupAgent(t).SSHClient()
require.NoError(t, err)
session, err := sshClient.NewSession()
require.NoError(t, err)
return session
}

func setupAgent(t *testing.T) *agent.Conn {
client, server := provisionersdk.TransportPipe()
closer := agent.New(func(ctx context.Context, opts *peer.ConnOptions) (*peerbroker.Listener, error) {
return peerbroker.Listen(server, nil, opts)
Expand All@@ -100,14 +174,9 @@ func setupSSH(t *testing.T) *ssh.Session {
t.Cleanup(func() {
_ = conn.Close()
})
agentClient := &agent.Conn{

return &agent.Conn{
Negotiator: api,
Conn: conn,
}
sshClient, err := agentClient.SSHClient()
require.NoError(t, err)
session, err := sshClient.NewSession()
require.NoError(t, err)

return session
}
2 changes: 2 additions & 0 deletionscli/cliui/cliui.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@ var Styles = struct {
Checkmark,
Code,
Crossmark,
Error,
Field,
Keyword,
Paragraph,
Expand All@@ -41,6 +42,7 @@ var Styles = struct {
Checkmark: defaultStyles.Checkmark,
Code: defaultStyles.Code,
Crossmark: defaultStyles.Error.Copy().SetString("✘"),
Error: defaultStyles.Error,
Field: defaultStyles.Code.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FFFFFF"}),
Keyword: defaultStyles.Keyword,
Paragraph: defaultStyles.Paragraph,
Expand Down
60 changes: 40 additions & 20 deletionscli/cliui/prompt.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/signal"
"runtime"
Expand DownExpand Up@@ -45,11 +44,11 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
var line string
var err error

inFile,valid := cmd.InOrStdin().(*os.File)
if opts.Secret &&valid && isatty.IsTerminal(inFile.Fd()) {
inFile,isInputFile := cmd.InOrStdin().(*os.File)
if opts.Secret &&isInputFile && isatty.IsTerminal(inFile.Fd()) {
line, err = speakeasy.Ask("")
} else {
if !opts.IsConfirm && runtime.GOOS == "darwin" &&valid {
if !opts.IsConfirm && runtime.GOOS == "darwin" &&isInputFile {
var restore func()
restore, err = removeLineLengthLimit(int(inFile.Fd()))
if err != nil {
Expand All@@ -66,22 +65,7 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
// This enables multiline JSON to be pasted into an input, and have
// it parse properly.
if err == nil && (strings.HasPrefix(line, "{") || strings.HasPrefix(line, "[")) {
pipeReader, pipeWriter := io.Pipe()
defer pipeWriter.Close()
defer pipeReader.Close()
go func() {
_, _ = pipeWriter.Write([]byte(line))
_, _ = reader.WriteTo(pipeWriter)
}()
var rawMessage json.RawMessage
err := json.NewDecoder(pipeReader).Decode(&rawMessage)
if err == nil {
var buf bytes.Buffer
err = json.Compact(&buf, rawMessage)
if err == nil {
line = buf.String()
}
}
line, err = promptJSON(reader, line)
}
}
if err != nil {
Expand DownExpand Up@@ -118,3 +102,39 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
return "", Canceled
}
}

func promptJSON(reader *bufio.Reader, line string) (string, error) {
var data bytes.Buffer
for {
_, _ = data.WriteString(line)
var rawMessage json.RawMessage
err := json.Unmarshal(data.Bytes(), &rawMessage)
if err != nil {
if err.Error() != "unexpected end of JSON input" {
// If a real syntax error occurs in JSON,
// we want to return that partial line to the user.
err = nil
line = data.String()
break
}

// Read line-by-line. We can't use a JSON decoder
// here because it doesn't work by newline, so
// reads will block.
line, err = reader.ReadString('\n')
if err != nil {
break
}
continue
}
// Compacting the JSON makes it easier for parsing and testing.
rawJSON := data.Bytes()
data.Reset()
err = json.Compact(&data, rawJSON)
if err != nil {
return line, xerrors.Errorf("compact json: %w", err)
}
return data.String(), nil
}
return line, nil
}
140 changes: 140 additions & 0 deletionscli/cliui/resources.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
package cliui

import (
"fmt"
"io"
"sort"
"strconv"

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

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

type WorkspaceResourcesOptions struct {
WorkspaceName string
HideAgentState bool
HideAccess bool
Title string
}

// WorkspaceResources displays the connection status and tree-view of provided resources.
// ┌────────────────────────────────────────────────────────────────────────────┐
// │ RESOURCE STATUS ACCESS │
// ├────────────────────────────────────────────────────────────────────────────┤
// │ google_compute_disk.root persistent │
// ├────────────────────────────────────────────────────────────────────────────┤
// │ google_compute_instance.dev ephemeral │
// │ └─ dev (linux, amd64) ⦾ connecting [10s] coder ssh dev.dev │
// ├────────────────────────────────────────────────────────────────────────────┤
// │ kubernetes_pod.dev ephemeral │
// │ ├─ go (linux, amd64) ⦿ connected coder ssh dev.go │
// │ └─ postgres (linux, amd64) ⦾ disconnected [4s] coder ssh dev.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 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
}
// 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()
if options.Title != "" {
tableWriter.SetTitle(options.Title)
}
tableWriter.SetStyle(table.StyleLight)
tableWriter.Style().Options.SeparateColumns = false
row := table.Row{"Resource", "Status"}
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
}
if _, shown := displayed[resource.Address]; shown {
// The same resource can have multiple transitions.
continue
}
displayed[resource.Address] = struct{}{}

// 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 := "ephemeral"
if existsOnStop {
resourceState = "persistent"
}
// Display a line for the resource.
tableWriter.AppendRow(table.Row{
Styles.Bold.Render(resource.Type + "." + resource.Name),
Styles.Placeholder.Render(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")
}
}

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)
}
tableWriter.AppendSeparator()
}
_, err := fmt.Fprintln(writer, tableWriter.Render())
return err
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp