8000 feat: enable agent connection reports by default, remove flag (cherry-pick #16778) by matifali · Pull Request #16809 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

feat: enable agent connection reports by default, remove flag (cherry-pick #16778) #16809

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

Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
172e523
feat(agent): wire up agentssh server to allow exec into container (#1…
johnstcn Feb 26, 2025
38c0e8a
fix(agent/agentssh): ensure RSA key generation always produces valid …
ThomasK33 Feb 26, 2025
c5a265f
feat(cli): add experimental rpty command (#16700)
johnstcn Feb 26, 2025
a2cc1b8
fix: display premium banner on audit page when license inactive (#16713)
mtojek Feb 26, 2025
2971957
ci: also restart tagged provisioner deployment (#16716)
johnstcn Feb 26, 2025
f1b357d
feat: support session audit log (#16703)
BrunoQuaresma Feb 26, 2025
b94d2cb
fix(coderd): handle deletes and links for new agent/app audit resourc…
mafredri Feb 26, 2025
7c035a4
fix: remove provisioners from deployment sidebar (#16717)
BrunoQuaresma Feb 26, 2025
7cd6e9c
fix: return provisioners in desc order and add limit to cli (#16720)
mafredri Feb 26, 2025
5295902
docs: clarified prometheus integration behavior (#16724)
michaelvp411 Feb 26, 2025
1cb864b
fix: allow viewOrgRoles for custom roles page (#16722)
jaaydenh Feb 26, 2025
81ef9e9
docs: document new feature stages (#16719)
EdwardAngert Feb 26, 2025
2aa749a
chore(cli): fix test flake caused by agent connect race (#16725)
johnstcn Feb 26, 2025
6b69635
chore: warn user without permissions to view org members (#16721)
jaaydenh Feb 26, 2025
5cdc13b
docs: fix broken links in feature-stages (#16727)
EdwardAngert Feb 26, 2025
b3d6755
docs: copy edit early access section in feature-stages doc (#16730)
EdwardAngert Feb 27, 2025
95363c9
fix(enterprise/coderd): remove useless provisioner daemon id from req…
johnstcn Feb 27, 2025
6dd51f9
chore: test metricscache on postgres (#16711)
DanielleMaywood Feb 27, 2025
4ba5a8a
feat(agent): add connection reporting for SSH and reconnecting PTY (#…
mafredri Feb 27, 2025
cccdf1e
feat: implement WorkspaceCreationBan org role (#16686)
Emyrk Feb 27, 2025
464fccd
chore: create collapsible summary component (#16705)
jaaydenh Feb 27, 2025
bf5b002
fix: add org role read permissions to site wide template admins and a…
jaaydenh Feb 27, 2025
91a4a98
chore: add an unassign action for roles (#16728)
aslilac Feb 27, 2025
0ea0601
fix: handle undefined job while updating build progress (#16732)
mtojek Feb 27, 2025
7e33902
chore: use org-scoped roles for organization groups and members e2e t…
aslilac Feb 27, 2025
b23e05b
fix(vpn): fail early if wintun.dll is not present (#16707)
deansheather Feb 28, 2025
3997eee
chore: update tailscale (#16737)
deansheather Feb 28, 2025
64fec8b
feat: include winres metadata in Windows binaries (#16706)
deansheather Feb 28, 2025
ec44f06
feat(cli): allow SSH command to connect to running container (#16726)
johnstcn Feb 28, 2025
6889ad2
fix(agent/agentcontainers): remove empty warning if no containers exi…
johnstcn Feb 28, 2025
e27953d
fix(site): add a beta badge for presets (#16751)
SasSwart Feb 28, 2025
930816f
fix: locate Terraform entrypoint file (#16753)
mtojek Feb 28, 2025
4216e28
fix: editor: fallback to default entrypoint (#16757)
mtojek Feb 28, 2025
fc2815c
docs: fix anchor and repo links (#16555)
guspan-tanadi Mar 2, 2025
ca23abe
feat(provisioner): add support for workspace_owner_rbac_roles (#16407)
nxf5025 Mar 2, 2025
d0e2060
feat(agent): add second SSH listener on port 22 (#16627)
ThomasK33 Mar 3, 2025
c074f77
feat: add notifications inbox db (#16599)
defelmnq Mar 3, 2025
a5842e5
docs: document default GitHub OAuth2 configuration and device flow (#…
hugodutka Mar 3, 2025
9c5d496
docs: suggest disabling the default GitHub OAuth2 provider on k8s (#1…
hugodutka Mar 3, 2025
0f4f6bd
docs: describe default sign up behavior with GitHub (#16765)
hugodutka Mar 3, 2025
88f0131
fix: use dbtime in dbmem query to fix flake (#16773)
ethanndickson Mar 3, 2025
04c3396
refactor: replace `golang.org/x/exp/slices` with `slices` (#16772)
Juneezee Mar 3, 2025
ca23abc
chore(cli): fix test flake in TestSSH_Container/NotFound (#16771)
johnstcn Mar 3, 2025
7dc05cc
feat: enable agent connection reports by default, remove flag
mafredri Mar 3, 2025
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
Next Next commit
feat(agent): wire up agentssh server to allow exec into container (#1…
…6638)

Builds on top of #16623 and wires up
the ReconnectingPTY server. This does nothing to wire up the web
terminal yet but the added test demonstrates the functionality working.

Other changes:
* Refactors and moves the `SystemEnvInfo` interface to the
`agent/usershell` package to address follow-up from
#16623 (comment)
* Marks `usershellinfo.Get` as deprecated. Consumers should use the
`EnvInfoer` interface instead.

---------

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
Co-authored-by: Danny Kopping <danny@coder.com>
  • Loading branch information
3 people authored Feb 26, 2025
commit 172e52317cd053dcdffc2b7d445a1d390ebbe53b
9 changes: 9 additions & 0 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ type Options struct {
BlockFileTransfer bool
Execer agentexec.Execer
ContainerLister agentcontainers.Lister

ExperimentalContainersEnabled bool
}

type Client interface {
Expand Down Expand Up @@ -188,6 +190,8 @@ func New(options Options) Agent {
metrics: newAgentMetrics(prometheusRegistry),
execer: options.Execer,
lister: options.ContainerLister,

experimentalDevcontainersEnabled: options.ExperimentalContainersEnabled,
}
// Initially, we have a closed channel, reflecting the fact that we are not initially connected.
// Each time we connect we replace the channel (while holding the closeMutex) with a new one
Expand Down Expand Up @@ -258,6 +262,8 @@ type agent struct {
metrics *agentMetrics
execer agentexec.Execer
lister agentcontainers.Lister

experimentalDevcontainersEnabled bool
}

func (a *agent) TailnetConn() *tailnet.Conn {
Expand Down Expand Up @@ -297,6 +303,9 @@ func (a *agent) init() {
a.sshServer,
a.metrics.connectionsTotal, a.metrics.reconnectingPTYErrors,
a.reconnectingPTYTimeout,
func(s *reconnectingpty.Server) {
s.ExperimentalContainersEnabled = a.experimentalDevcontainersEnabled
},
)
go a.runLoop()
}
Expand Down
78 changes: 75 additions & 3 deletions agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,28 @@ import (
"testing"
"time"

"go.uber.org/goleak"
"tailscale.com/net/speedtest"
"tailscale.com/tailcfg"

"github.com/bramvdbogaerde/go-scp"
"github.com/google/uuid"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/pion/udp"
"github.com/pkg/sftp"
"github.com/prometheus/client_golang/prometheus"
promgo "github.com/prometheus/client_model/go"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"golang.org/x/crypto/ssh"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"tailscale.com/net/speedtest"
"tailscale.com/tailcfg"

"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"

"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/agent/agenttest"
Expand Down Expand Up @@ -1761,6 +1765,74 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
}
}

// This tests end-to-end functionality of connecting to a running container
// and executing a command. It creates a real Docker container and runs a
// command. As such, it does not run by default in CI.
// You can run it manually as follows:
//
// CODER_TEST_USE_DOCKER=1 go test -count=1 ./agent -run TestAgent_ReconnectingPTYContainer
func TestAgent_ReconnectingPTYContainer(t *testing.T) {
t.Parallel()
if os.Getenv("CODER_TEST_USE_DOCKER") != "1" {
t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test")
}

ctx := testutil.Context(t, testutil.WaitLong)

pool, err := dockertest.NewPool("")
require.NoError(t, err, "Could not connect to docker")
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: "busybox",
Tag: "latest",
Cmd: []string{"sleep", "infnity"},
}, func(config *docker.HostConfig) {
config.AutoRemove = true
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
})
require.NoError(t, err, "Could not start container")
t.Cleanup(func() {
err := pool.Purge(ct)
require.NoError(t, err, "Could not stop container")
})
// Wait for container to start
require.Eventually(t, func() bool {
ct, ok := pool.ContainerByName(ct.Container.Name)
return ok && ct.Container.State.Running
}, testutil.WaitShort, testutil.IntervalSlow, "Container did not start in time")

// nolint: dogsled
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
o.ExperimentalContainersEnabled = true
})
ac, err := conn.ReconnectingPTY(ctx, uuid.New(), 80, 80, "/bin/sh", func(arp *workspacesdk.AgentReconnectingPTYInit) {
arp.Container = ct.Container.ID
})
require.NoError(t, err, "failed to create ReconnectingPTY")
defer ac.Close()
tr := testutil.NewTerminalReader(t, ac)

require.NoError(t, tr.ReadUntil(ctx, func(line string) bool {
return strings.Contains(line, "#") || strings.Contains(line, "$")
}), "find prompt")

require.NoError(t, json.NewEncoder(ac).Encode(workspacesdk.ReconnectingPTYRequest{
Data: "hostname\r",
}), "write hostname")
require.NoError(t, tr.ReadUntil(ctx, func(line string) bool {
return strings.Contains(line, "hostname")
}), "find hostname command")

require.NoError(t, tr.ReadUntil(ctx, func(line string) bool {
return strings.Contains(line, ct.Container.Config.Hostname)
}), "find hostname output")
require.NoError(t, json.NewEncoder(ac).Encode(workspacesdk.ReconnectingPTYRequest{
Data: "exit\r",
}), "write exit command")

// Wait for the connection to close.
require.ErrorIs(t, tr.ReadUntil(ctx, nil), io.EOF)
}

func TestAgent_Dial(t *testing.T) {
t.Parallel()

Expand Down
20 changes: 4 additions & 16 deletions agent/agentcontainers/containers_dockercli.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"os/user"
"slices"
"sort"
Expand All @@ -15,6 +14,7 @@ import (
"time"

"github.com/coder/coder/v2/agent/agentexec"
"github.com/coder/coder/v2/agent/usershell"
"github.com/coder/coder/v2/codersdk"

"golang.org/x/exp/maps"
Expand All @@ -37,6 +37,7 @@ func NewDocker(execer agentexec.Execer) Lister {
// DockerEnvInfoer is an implementation of agentssh.EnvInfoer that returns
// information about a container.
type DockerEnvInfoer struct {
usershell.SystemEnvInfo
container string
user *user.User
userShell string
Expand Down Expand Up @@ -122,26 +123,13 @@ func EnvInfo(ctx context.Context, execer agentexec.Execer, container, containerU
return &dei, nil
}

func (dei *DockerEnvInfoer) CurrentUser() (*user.User, error) {
func (dei *DockerEnvInfoer) User() (*user.User, error) {
// Clone the user so that the caller can't modify it
u := *dei.user
return &u, nil
}

func (*DockerEnvInfoer) Environ() []string {
// Return a clone of the environment so that the caller can't modify it
return os.Environ()
}

func (*DockerEnvInfoer) UserHomeDir() (string, error) {
// We default the working directory of the command to the user's home
// directory. Since this came from inside the container, we cannot guarantee
// that this exists on the host. Return the "real" home directory of the user
// instead.
return os.UserHomeDir()
}

func (dei *DockerEnvInfoer) UserShell(string) (string, error) {
func (dei *DockerEnvInfoer) Shell(string) (string, error) {
return dei.userShell, nil
}

Expand Down
6 changes: 3 additions & 3 deletions agent/agentcontainers/containers_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -502,15 +502,15 @@ func TestDockerEnvInfoer(t *testing.T) {
dei, err := EnvInfo(ctx, agentexec.DefaultExecer, ct.Container.ID, tt.containerUser)
require.NoError(t, err, "Expected no error from DockerEnvInfo()")

u, err := dei.CurrentUser()
u, err := dei.User()
require.NoError(t, err, "Expected no error from CurrentUser()")
require.Equal(t, tt.expectedUsername, u.Username, "Expected username to match")

hd, err := dei.UserHomeDir()
hd, err := dei.HomeDir()
require.NoError(t, err, "Expected no error from UserHomeDir()")
require.NotEmpty(t, hd, "Expected user homedir to be non-empty")

sh, err := dei.UserShell(tt.containerUser)
sh, err := dei.Shell(tt.containerUser)
require.NoError(t, err, "Expected no error from UserShell()")
require.Equal(t, tt.expectedUserShell, sh, "Expected user shell to match")

Expand Down
66 changes: 19 additions & 47 deletions agent/agentssh/agentssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,63 +698,24 @@ func (s *Server) sftpHandler(logger slog.Logger, session ssh.Session) {
_ = session.Exit(1)
}

// EnvInfoer encapsulates external information required by CreateCommand.
type EnvInfoer interface {
// CurrentUser returns the current user.
CurrentUser() (*user.User, error)
// Environ returns the environment variables of the current process.
Environ() []string
// UserHomeDir returns the home directory of the current user.
UserHomeDir() (string, error)
// UserShell returns the shell of the given user.
UserShell(username string) (string, error)
}

type systemEnvInfoer struct{}

var defaultEnvInfoer EnvInfoer = &systemEnvInfoer{}

// DefaultEnvInfoer returns a default implementation of
// EnvInfoer. This reads information using the default Go
// implementations.
func DefaultEnvInfoer() EnvInfoer {
return defaultEnvInfoer
}

func (systemEnvInfoer) CurrentUser() (*user.User, error) {
return user.Current()
}

func (systemEnvInfoer) Environ() []string {
return os.Environ()
}

func (systemEnvInfoer) UserHomeDir() (string, error) {
return userHomeDir()
}

func (systemEnvInfoer) UserShell(username string) (string, error) {
return usershell.Get(username)
}

// CreateCommand processes raw command input with OpenSSH-like behavior.
// If the script provided is empty, it will default to the users shell.
// This injects environment variables specified by the user at launch too.
// The final argument is an interface that allows the caller to provide
// alternative implementations for the dependencies of CreateCommand.
// This is useful when creating a command to be run in a separate environment
// (for example, a Docker container). Pass in nil to use the default.
func (s *Server) CreateCommand(ctx context.Context, script string, env []string, deps EnvInfoer) (*pty.Cmd, error) {
if deps == nil {
deps = DefaultEnvInfoer()
func (s *Server) CreateCommand(ctx context.Context, script string, env []string, ei usershell.EnvInfoer) (*pty.Cmd, error) {
if ei == nil {
ei = &usershell.SystemEnvInfo{}
}
currentUser, err := deps.CurrentUser()
currentUser, err := ei.User()
if err != nil {
return nil, xerrors.Errorf("get current user: %w", err)
}
username := currentUser.Username

shell, err := deps.UserShell(username)
shell, err := ei.Shell(username)
if err != nil {
return nil, xerrors.Errorf("get user shell: %w", err)
}
Expand Down Expand Up @@ -802,21 +763,32 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string,
}
}

cmd := s.Execer.PTYCommandContext(ctx, name, args...)
// Modify command prior to execution. This will usually be a no-op, but not
// always. For example, to run a command in a Docker container, we need to
// modify the command to be `docker exec -it <container> <command>`.
modifiedName, modifiedArgs := ei.ModifyCommand(name, args...)
// Log if the command was modified.
if modifiedName != name && slices.Compare(modifiedArgs, args) != 0 {
s.logger.Debug(ctx, "modified command",
slog.F("before", append([]string{name}, args...)),
slog.F("after", append([]string{modifiedName}, modifiedArgs...)),
)
}
cmd := s.Execer.PTYCommandContext(ctx, modifiedName, modifiedArgs...)
cmd.Dir = s.config.WorkingDirectory()

// If the metadata directory doesn't exist, we run the command
// in the users home directory.
_, err = os.Stat(cmd.Dir)
if cmd.Dir == "" || err != nil {
// Default to user home if a directory is not set.
homedir, err := deps.UserHomeDir()
homedir, err := ei.HomeDir()
if err != nil {
return nil, xerrors.Errorf("get home dir: %w", err)
}
cmd.Dir = homedir
}
cmd.Env = append(deps.Environ(), env...)
cmd.Env = append(ei.Environ(), env...)
cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", username))

// Set SSH connection environment variables (these are also set by OpenSSH
Expand Down
10 changes: 7 additions & 3 deletions agent/agentssh/agentssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,22 +124,26 @@ type fakeEnvInfoer struct {
UserShellFn func(string) (string, error)
}

func (f *fakeEnvInfoer) CurrentUser() (u *user.User, err error) {
func (f *fakeEnvInfoer) User() (u *user.User, err error) {
return f.CurrentUserFn()
}

func (f *fakeEnvInfoer) Environ() []string {
return f.EnvironFn()
}

func (f *fakeEnvInfoer) UserHomeDir() (string, error) {
func (f *fakeEnvInfoer) HomeDir() (string, error) {
return f.UserHomeDirFn()
}

func (f *fakeEnvInfoer) UserShell(u string) (string, error) {
func (f *fakeEnvInfoer) Shell(u string) (string, error) {
return f.UserShellFn(u)
}

func (*fakeEnvInfoer) ModifyCommand(cmd string, args ...string) (string, []string) {
return cmd, args
}

func TestNewServer_CloseActiveConnections(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading
0