mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Add config-ssh command (#735)
* feat: Add config-ssh command Closes #254 and #499. * Fix Windows support
This commit is contained in:
+11
-10
@@ -3,11 +3,11 @@ package cliui
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/codersdk"
|
||||
@@ -21,7 +21,7 @@ type AgentOptions struct {
|
||||
}
|
||||
|
||||
// Agent displays a spinning indicator that waits for a workspace agent to connect.
|
||||
func Agent(cmd *cobra.Command, opts AgentOptions) error {
|
||||
func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
|
||||
if opts.FetchInterval == 0 {
|
||||
opts.FetchInterval = 500 * time.Millisecond
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func Agent(cmd *cobra.Command, opts AgentOptions) error {
|
||||
opts.WarnInterval = 30 * time.Second
|
||||
}
|
||||
var resourceMutex sync.Mutex
|
||||
resource, err := opts.Fetch(cmd.Context())
|
||||
resource, err := opts.Fetch(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch: %w", err)
|
||||
}
|
||||
@@ -40,7 +40,8 @@ func Agent(cmd *cobra.Command, opts AgentOptions) error {
|
||||
opts.WarnInterval = 0
|
||||
}
|
||||
spin := spinner.New(spinner.CharSets[78], 100*time.Millisecond, spinner.WithColor("fgHiGreen"))
|
||||
spin.Writer = cmd.OutOrStdout()
|
||||
spin.Writer = writer
|
||||
spin.ForceOutput = true
|
||||
spin.Suffix = " Waiting for connection from " + Styles.Field.Render(resource.Type+"."+resource.Name) + "..."
|
||||
spin.Start()
|
||||
defer spin.Stop()
|
||||
@@ -51,7 +52,7 @@ func Agent(cmd *cobra.Command, opts AgentOptions) error {
|
||||
defer timer.Stop()
|
||||
go func() {
|
||||
select {
|
||||
case <-cmd.Context().Done():
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
@@ -63,17 +64,17 @@ func Agent(cmd *cobra.Command, opts AgentOptions) error {
|
||||
}
|
||||
// This saves the cursor position, then defers clearing from the cursor
|
||||
// position to the end of the screen.
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\033[s\r\033[2K%s\n\n", Styles.Paragraph.Render(Styles.Prompt.String()+message))
|
||||
defer fmt.Fprintf(cmd.OutOrStdout(), "\033[u\033[J")
|
||||
_, _ = fmt.Fprintf(writer, "\033[s\r\033[2K%s\n\n", Styles.Paragraph.Render(Styles.Prompt.String()+message))
|
||||
defer fmt.Fprintf(writer, "\033[u\033[J")
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-cmd.Context().Done():
|
||||
return cmd.Context().Err()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
resourceMutex.Lock()
|
||||
resource, err = opts.Fetch(cmd.Context())
|
||||
resource, err = opts.Fetch(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch: %w", err)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestAgent(t *testing.T) {
|
||||
ptty := ptytest.New(t)
|
||||
cmd := &cobra.Command{
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := cliui.Agent(cmd, cliui.AgentOptions{
|
||||
err := cliui.Agent(cmd.Context(), cmd.OutOrStdout(), cliui.AgentOptions{
|
||||
WorkspaceName: "example",
|
||||
Fetch: func(ctx context.Context) (codersdk.WorkspaceResource, error) {
|
||||
resource := codersdk.WorkspaceResource{
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package cliui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// cliMessage provides a human-readable message for CLI errors and messages.
|
||||
type cliMessage struct {
|
||||
Level string
|
||||
Style lipgloss.Style
|
||||
Header string
|
||||
Lines []string
|
||||
}
|
||||
|
||||
// String formats the CLI message for consumption by a human.
|
||||
func (m cliMessage) String() string {
|
||||
var str strings.Builder
|
||||
_, _ = fmt.Fprintf(&str, "%s\r\n",
|
||||
Styles.Bold.Render(m.Header))
|
||||
for _, line := range m.Lines {
|
||||
_, _ = fmt.Fprintf(&str, " %s %s\r\n", m.Style.Render("|"), line)
|
||||
}
|
||||
return str.String()
|
||||
}
|
||||
|
||||
// Warn writes a log to the writer provided.
|
||||
func Warn(wtr io.Writer, header string, lines ...string) {
|
||||
_, _ = fmt.Fprint(wtr, cliMessage{
|
||||
Level: "warning",
|
||||
Style: Styles.Warn,
|
||||
Header: header,
|
||||
Lines: lines,
|
||||
}.String())
|
||||
}
|
||||
+6
-1
@@ -2,6 +2,7 @@ package cliui
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -62,7 +63,11 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
|
||||
var rawMessage json.RawMessage
|
||||
err := json.NewDecoder(pipeReader).Decode(&rawMessage)
|
||||
if err == nil {
|
||||
line = string(rawMessage)
|
||||
var buf bytes.Buffer
|
||||
err = json.Compact(&buf, rawMessage)
|
||||
if err == nil {
|
||||
line = buf.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,9 +93,7 @@ func TestPrompt(t *testing.T) {
|
||||
ptty.WriteLine(`{
|
||||
"test": "wow"
|
||||
}`)
|
||||
require.Equal(t, `{
|
||||
"test": "wow"
|
||||
}`, <-doneChan)
|
||||
require.Equal(t, `{"test":"wow"}`, <-doneChan)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+12
-12
@@ -3,27 +3,27 @@ package cliui
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func WorkspaceBuild(cmd *cobra.Command, client *codersdk.Client, build uuid.UUID, before time.Time) error {
|
||||
return ProvisionerJob(cmd, ProvisionerJobOptions{
|
||||
func WorkspaceBuild(ctx context.Context, writer io.Writer, client *codersdk.Client, build uuid.UUID, before time.Time) error {
|
||||
return ProvisionerJob(ctx, writer, ProvisionerJobOptions{
|
||||
Fetch: func() (codersdk.ProvisionerJob, error) {
|
||||
build, err := client.WorkspaceBuild(cmd.Context(), build)
|
||||
build, err := client.WorkspaceBuild(ctx, build)
|
||||
return build.Job, err
|
||||
},
|
||||
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
|
||||
return client.WorkspaceBuildLogsAfter(cmd.Context(), build, before)
|
||||
return client.WorkspaceBuildLogsAfter(ctx, build, before)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -39,25 +39,25 @@ type ProvisionerJobOptions struct {
|
||||
}
|
||||
|
||||
// ProvisionerJob renders a provisioner job with interactive cancellation.
|
||||
func ProvisionerJob(cmd *cobra.Command, opts ProvisionerJobOptions) error {
|
||||
func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOptions) error {
|
||||
if opts.FetchInterval == 0 {
|
||||
opts.FetchInterval = time.Second
|
||||
}
|
||||
ctx, cancelFunc := context.WithCancel(ctx)
|
||||
defer cancelFunc()
|
||||
|
||||
var (
|
||||
currentStage = "Queued"
|
||||
currentStageStartedAt = time.Now().UTC()
|
||||
didLogBetweenStage = false
|
||||
ctx, cancelFunc = context.WithCancel(cmd.Context())
|
||||
|
||||
errChan = make(chan error, 1)
|
||||
job codersdk.ProvisionerJob
|
||||
jobMutex sync.Mutex
|
||||
)
|
||||
defer cancelFunc()
|
||||
|
||||
printStage := func() {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), Styles.Prompt.Render("⧗")+"%s\n", Styles.Field.Render(currentStage))
|
||||
_, _ = fmt.Fprintf(writer, Styles.Prompt.Render("⧗")+"%s\n", Styles.Field.Render(currentStage))
|
||||
}
|
||||
|
||||
updateStage := func(stage string, startedAt time.Time) {
|
||||
@@ -70,7 +70,7 @@ func ProvisionerJob(cmd *cobra.Command, opts ProvisionerJobOptions) error {
|
||||
if job.CompletedAt != nil && job.Status != codersdk.ProvisionerJobSucceeded {
|
||||
mark = Styles.Crossmark
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), prefix+mark.String()+Styles.Placeholder.Render(" %s [%dms]")+"\n", currentStage, startedAt.Sub(currentStageStartedAt).Milliseconds())
|
||||
_, _ = fmt.Fprintf(writer, prefix+mark.String()+Styles.Placeholder.Render(" %s [%dms]")+"\n", currentStage, startedAt.Sub(currentStageStartedAt).Milliseconds())
|
||||
}
|
||||
if stage == "" {
|
||||
return
|
||||
@@ -116,7 +116,7 @@ func ProvisionerJob(cmd *cobra.Command, opts ProvisionerJobOptions) error {
|
||||
return
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\033[2K\r\n"+Styles.FocusedPrompt.String()+Styles.Bold.Render("Gracefully canceling...")+"\n\n")
|
||||
_, _ = fmt.Fprintf(writer, "\033[2K\r\n"+Styles.FocusedPrompt.String()+Styles.Bold.Render("Gracefully canceling...")+"\n\n")
|
||||
err := opts.Cancel()
|
||||
if err != nil {
|
||||
errChan <- xerrors.Errorf("cancel: %w", err)
|
||||
@@ -183,7 +183,7 @@ func ProvisionerJob(cmd *cobra.Command, opts ProvisionerJobOptions) error {
|
||||
jobMutex.Unlock()
|
||||
continue
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", Styles.Placeholder.Render(" "), output)
|
||||
_, _ = fmt.Fprintf(writer, "%s %s\n", Styles.Placeholder.Render(" "), output)
|
||||
didLogBetweenStage = true
|
||||
jobMutex.Unlock()
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ func newProvisionerJob(t *testing.T) provisionerJobTest {
|
||||
logs := make(chan codersdk.ProvisionerJobLog, 1)
|
||||
cmd := &cobra.Command{
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cliui.ProvisionerJob(cmd, cliui.ProvisionerJobOptions{
|
||||
return cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{
|
||||
FetchInterval: time.Millisecond,
|
||||
Fetch: func() (codersdk.ProvisionerJob, error) {
|
||||
jobLock.Lock()
|
||||
|
||||
Reference in New Issue
Block a user