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: restart stopped workspaces on ssh command#11050

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
Emyrk merged 10 commits intomainfromstevenmasley/ssh_autostart
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes fromall commits
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
28 changes: 24 additions & 4 deletionscli/configssh.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ import (
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"

"github.com/cli/safeexec"
Expand DownExpand Up@@ -46,9 +47,10 @@ const (
// sshConfigOptions represents options that can be stored and read
// from the coder config in ~/.ssh/coder.
type sshConfigOptions struct {
waitEnum string
userHostPrefix string
sshOptions []string
waitEnum string
userHostPrefix string
sshOptions []string
disableAutostart bool
}

// addOptions expects options in the form of "option=value" or "option value".
Expand DownExpand Up@@ -106,7 +108,7 @@ func (o sshConfigOptions) equal(other sshConfigOptions) bool {
if !slices.Equal(opt1, opt2) {
return false
}
return o.waitEnum == other.waitEnum && o.userHostPrefix == other.userHostPrefix
return o.waitEnum == other.waitEnum && o.userHostPrefix == other.userHostPrefix && o.disableAutostart == other.disableAutostart
}

func (o sshConfigOptions) asList() (list []string) {
Expand All@@ -116,6 +118,9 @@ func (o sshConfigOptions) asList() (list []string) {
if o.userHostPrefix != "" {
list = append(list, fmt.Sprintf("ssh-host-prefix: %s", o.userHostPrefix))
}
if o.disableAutostart {
list = append(list, fmt.Sprintf("disable-autostart: %v", o.disableAutostart))
}
for _, opt := range o.sshOptions {
list = append(list, fmt.Sprintf("ssh-option: %s", opt))
}
Expand DownExpand Up@@ -392,6 +397,9 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
if sshConfigOpts.waitEnum != "auto" {
flags += " --wait=" + sshConfigOpts.waitEnum
}
if sshConfigOpts.disableAutostart {
flags += " --disable-autostart=true"
}
defaultOptions = append(defaultOptions, fmt.Sprintf(
"ProxyCommand %s --global-config %s ssh --stdio%s %s",
escapedCoderBinary, escapedGlobalConfig, flags, workspaceHostname,
Expand DownExpand Up@@ -566,6 +574,13 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
Default: "auto",
Value: clibase.EnumOf(&sshConfigOpts.waitEnum, "yes", "no", "auto"),
},
{
Flag: "disable-autostart",
Description: "Disable starting the workspace automatically when connecting via SSH.",
Env: "CODER_CONFIGSSH_DISABLE_AUTOSTART",
Value: clibase.BoolOf(&sshConfigOpts.disableAutostart),
Default: "false",
},
{
Flag: "force-unix-filepaths",
Env: "CODER_CONFIGSSH_UNIX_FILEPATHS",
Expand DownExpand Up@@ -602,6 +617,9 @@ func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOption
if o.userHostPrefix != "" {
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "ssh-host-prefix", o.userHostPrefix)
}
if o.disableAutostart {
_, _ = fmt.Fprintf(&ow, "# :%s=%v\n", "disable-autostart", o.disableAutostart)
}
for _, opt := range o.sshOptions {
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "ssh-option", opt)
}
Expand DownExpand Up@@ -634,6 +652,8 @@ func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) {
o.userHostPrefix = parts[1]
case "ssh-option":
o.sshOptions = append(o.sshOptions, parts[1])
case "disable-autostart":
o.disableAutostart, _ = strconv.ParseBool(parts[1])
default:
// Unknown option, ignore.
}
Expand Down
1 change: 1 addition & 0 deletionscli/ping.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ func (r *RootCmd) ping() *clibase.Cmd {
workspaceName := inv.Args[0]
_, workspaceAgent, err := getWorkspaceAndAgent(
ctx, inv, client,
false, // Do not autostart for a ping.
codersdk.Me, workspaceName,
)
if err != nil {
Expand Down
8 changes: 5 additions & 3 deletionscli/portforward.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,9 @@ import (

func (r *RootCmd) portForward() *clibase.Cmd {
var (
tcpForwards []string // <port>:<port>
udpForwards []string // <port>:<port>
tcpForwards []string // <port>:<port>
udpForwards []string // <port>:<port>
disableAutostart bool
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
Expand DownExpand Up@@ -76,7 +77,7 @@ func (r *RootCmd) portForward() *clibase.Cmd {
return xerrors.New("no port-forwards requested")
}

workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, codersdk.Me, inv.Args[0])
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client,!disableAutostart,codersdk.Me, inv.Args[0])
if err != nil {
return err
}
Expand DownExpand Up@@ -180,6 +181,7 @@ func (r *RootCmd) portForward() *clibase.Cmd {
Description: "Forward UDP port(s) from the workspace to the local machine. The UDP connection has TCP-like semantics to support stateful UDP protocols.",
Value: clibase.StringArrayOf(&udpForwards),
},
sshDisableAutostartOption(clibase.BoolOf(&disableAutostart)),
}

return cmd
Expand Down
2 changes: 1 addition & 1 deletioncli/speedtest.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ func (r *RootCmd) speedtest() *clibase.Cmd {
ctx, cancel := context.WithCancel(inv.Context())
defer cancel()

_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, codersdk.Me, inv.Args[0])
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client,false,codersdk.Me, inv.Args[0])
if err != nil {
return err
}
Expand Down
70 changes: 55 additions & 15 deletionscli/ssh.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import (
"sync"
"time"

"github.com/coder/retry"
"github.com/gen2brain/beeep"
"github.com/gofrs/flock"
"github.com/google/uuid"
Expand All@@ -34,7 +35,6 @@ import (
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/retry"
)

var (
Expand All@@ -44,15 +44,16 @@ var (

func (r *RootCmd) ssh() *clibase.Cmd {
var (
stdio bool
forwardAgent bool
forwardGPG bool
identityAgent string
wsPollInterval time.Duration
waitEnum string
noWait bool
logDirPath string
remoteForward string
stdio bool
forwardAgent bool
forwardGPG bool
identityAgent string
wsPollInterval time.Duration
waitEnum string
noWait bool
logDirPath string
remoteForward string
disableAutostart bool
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
Expand DownExpand Up@@ -143,7 +144,7 @@ func (r *RootCmd) ssh() *clibase.Cmd {
}
}

workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, codersdk.Me, inv.Args[0])
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client,!disableAutostart,codersdk.Me, inv.Args[0])
if err != nil {
return err
}
Expand DownExpand Up@@ -459,6 +460,7 @@ func (r *RootCmd) ssh() *clibase.Cmd {
FlagShorthand: "R",
Value: clibase.StringOf(&remoteForward),
},
sshDisableAutostartOption(clibase.BoolOf(&disableAutostart)),
}
return cmd
}
Expand DownExpand Up@@ -530,9 +532,9 @@ startWatchLoop:
}

// getWorkspaceAgent returns the workspace and agent selected using either the
// `<workspace>[.<agent>]` syntax via `in` or picks a random workspace and agent
//if `shuffle` is true.
func getWorkspaceAndAgent(ctx context.Context, inv *clibase.Invocation, client *codersdk.Client, userID string, in string) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
// `<workspace>[.<agent>]` syntax via `in`.
//If autoStart is true, the workspace will be started if it is not already running.
func getWorkspaceAndAgent(ctx context.Context, inv *clibase.Invocation, client *codersdk.Client,autostart bool,userID string, in string) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
var (
workspace codersdk.Workspace
workspaceParts = strings.Split(in, ".")
Expand All@@ -545,7 +547,35 @@ func getWorkspaceAndAgent(ctx context.Context, inv *clibase.Invocation, client *
}

if workspace.LatestBuild.Transition != codersdk.WorkspaceTransitionStart {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.New("workspace must be in start transition to ssh")
if !autostart {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.New("workspace must be in start transition to ssh")
}
// Autostart the workspace for the user.
// For some failure modes, return a better message.
if workspace.LatestBuild.Transition == codersdk.WorkspaceTransitionDelete {
// Any sort of deleting status, we should reject with a nicer error.
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.Errorf("workspace %q is deleted", workspace.Name)
}
if workspace.LatestBuild.Job.Status == codersdk.ProvisionerJobFailed {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{},
xerrors.Errorf("workspace %q is in failed state, unable to autostart the workspace", workspace.Name)
}
// The workspace needs to be stopped before we can start it.
// It cannot be in any pending or failed state.
if workspace.LatestBuild.Status != codersdk.WorkspaceStatusStopped {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

A case that I would personally like for to work is that I can ssh into a stopping workspace and have it wait until the stop is complete and then either issue the start or wait for a start to complete.

No action necessary for this PR, though. Just writing this down as a food for thought.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Things could be improved to handle more cases, but that felt a bit overkill atm.

return codersdk.Workspace{}, codersdk.WorkspaceAgent{},
xerrors.Errorf("workspace must be in start transition to ssh, was unable to autostart as the last build job is %q, expected %q",
workspace.LatestBuild.Status,
codersdk.WorkspaceStatusStopped,
)
}
// startWorkspace based on the last build parameters.
_, _ = fmt.Fprintf(inv.Stderr, "Workspace was stopped, starting workspace to allow connecting to %q...\n", workspace.Name)
build, err := startWorkspace(inv, client, workspace, workspaceParameterFlags{}, WorkspaceStart)
if err != nil {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.Errorf("unable to start workspace: %w", err)
}
workspace.LatestBuild = build
}
if workspace.LatestBuild.Job.CompletedAt == nil {
err := cliui.WorkspaceBuild(ctx, inv.Stderr, client, workspace.LatestBuild.ID)
Expand DownExpand Up@@ -915,3 +945,13 @@ func (c *rawSSHCopier) Close() error {
}
return err
}

func sshDisableAutostartOption(src *clibase.Bool) clibase.Option {
return clibase.Option{
Flag: "disable-autostart",
Description: "Disable starting the workspace automatically when connecting via SSH.",
Env: "CODER_SSH_DISABLE_AUTOSTART",
Value: src,
Default: "false",
}
}
45 changes: 45 additions & 0 deletionscli/ssh_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ import (
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
Expand All@@ -38,7 +39,9 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/pty"
"github.com/coder/coder/v2/pty/ptytest"
Expand DownExpand Up@@ -86,6 +89,48 @@ func TestSSH(t *testing.T) {
pty.WriteLine("exit")
<-cmdDone
})
t.Run("StartStoppedWorkspace", func(t *testing.T) {
t.Parallel()

authToken := uuid.NewString()
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, ownerClient)
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, owner.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// Stop the workspace
workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)

// SSH to the workspace which should autostart it
inv, root := clitest.New(t, "ssh", workspace.Name)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

cmdDone := tGo(t, func() {
err := inv.WithContext(ctx).Run()
assert.NoError(t, err)
})

// When the agent connects, the workspace was started, and we should
// have access to the shell.
_ = agenttest.New(t, client.URL, authToken)
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)

// Shells on Mac, Windows, and Linux all exit shells with the "exit" command.
pty.WriteLine("exit")
<-cmdDone
})
t.Run("ShowTroubleshootingURLAfterTimeout", func(t *testing.T) {
t.Parallel()

Expand Down
3 changes: 3 additions & 0 deletionscli/testdata/coder_config-ssh_--help.golden
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,9 @@ OPTIONS:
ProxyCommand. By default, the binary invoking this command ('config
ssh') is used.

--disable-autostart bool, $CODER_CONFIGSSH_DISABLE_AUTOSTART (default: false)
Disable starting the workspace automatically when connecting via SSH.

-n, --dry-run bool, $CODER_SSH_DRY_RUN
Perform a trial run with no changes made, showing a diff at the end.

Expand Down
3 changes: 3 additions & 0 deletionscli/testdata/coder_port-forward_--help.golden
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@ USAGE:
$ coder port-forward <workspace> --tcp 1.2.3.4:8080:8080

OPTIONS:
--disable-autostart bool, $CODER_SSH_DISABLE_AUTOSTART (default: false)
Disable starting the workspace automatically when connecting via SSH.

-p, --tcp string-array, $CODER_PORT_FORWARD_TCP
Forward TCP port(s) from the workspace to the local machine.

Expand Down
3 changes: 3 additions & 0 deletionscli/testdata/coder_ssh_--help.golden
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,9 @@ USAGE:
Start a shell into a workspace

OPTIONS:
--disable-autostart bool, $CODER_SSH_DISABLE_AUTOSTART (default: false)
Disable starting the workspace automatically when connecting via SSH.

-A, --forward-agent bool, $CODER_SSH_FORWARD_AGENT
Specifies whether to forward the SSH agent specified in
$SSH_AUTH_SOCK.
Expand Down
10 changes: 10 additions & 0 deletionsdocs/cli/config-ssh.md
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

10 changes: 10 additions & 0 deletionsdocs/cli/port-forward.md
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

Loading

[8]ページ先頭

©2009-2025 Movatter.jp