Skip to content

Commit 0f2394a

Browse files
committed
Merge branch 'main' into optional-external-auth-frontend
2 parents 1dc0874 + 475c365 commit 0f2394a

File tree

177 files changed

+7760
-2033
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

177 files changed

+7760
-2033
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ coderd/apidoc/swagger.json linguist-generated=true
66
coderd/database/dump.sql linguist-generated=true
77
peerbroker/proto/*.go linguist-generated=true
88
provisionerd/proto/*.go linguist-generated=true
9+
provisionerd/proto/version.go linguist-generated=false
910
provisionersdk/proto/*.go linguist-generated=true
1011
*.tfplan.json linguist-generated=true
1112
*.tfstate.json linguist-generated=true

.github/workflows/dogfood.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ jobs:
109109
- name: "Push template"
110110
if: github.ref == 'refs/heads/main'
111111
run: |
112-
./coder templates push $CODER_TEMPLATE_NAME --directory $CODER_TEMPLATE_DIR --yes --name=$CODER_TEMPLATE_VERSION --message="$CODER_TEMPLATE_MESSAGE" --variable jfrog_url=${{ secrets.JFROG_URL }}
112+
./coder templates push $CODER_TEMPLATE_NAME --directory $CODER_TEMPLATE_DIR --yes --name=$CODER_TEMPLATE_VERSION --message="$CODER_TEMPLATE_MESSAGE"
113113
env:
114114
# Consumed by Coder CLI
115115
CODER_URL: https://dev.coder.com

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"Signup",
115115
"slogtest",
116116
"sourcemapped",
117+
"spinbutton",
117118
"Srcs",
118119
"stdbuf",
119120
"stretchr",

agent/agent.go

Lines changed: 115 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type Options struct {
6666
Filesystem afero.Fs
6767
LogDir string
6868
TempDir string
69+
ScriptDataDir string
6970
ExchangeToken func(ctx context.Context) (string, error)
7071
Client Client
7172
ReconnectingPTYTimeout time.Duration
@@ -112,9 +113,19 @@ func New(options Options) Agent {
112113
if options.LogDir == "" {
113114
if options.TempDir != os.TempDir() {
114115
options.Logger.Debug(context.Background(), "log dir not set, using temp dir", slog.F("temp_dir", options.TempDir))
116+
} else {
117+
options.Logger.Debug(context.Background(), "using log dir", slog.F("log_dir", options.LogDir))
115118
}
116119
options.LogDir = options.TempDir
117120
}
121+
if options.ScriptDataDir == "" {
122+
if options.TempDir != os.TempDir() {
123+
options.Logger.Debug(context.Background(), "script data dir not set, using temp dir", slog.F("temp_dir", options.TempDir))
124+
} else {
125+
options.Logger.Debug(context.Background(), "using script data dir", slog.F("script_data_dir", options.ScriptDataDir))
126+
}
127+
options.ScriptDataDir = options.TempDir
128+
}
118129
if options.ExchangeToken == nil {
119130
options.ExchangeToken = func(ctx context.Context) (string, error) {
120131
return "", nil
@@ -146,12 +157,13 @@ func New(options Options) Agent {
146157
logger: options.Logger,
147158
closeCancel: cancelFunc,
148159
closed: make(chan struct{}),
149-
envVars: options.EnvironmentVariables,
160+
environmentVariables: options.EnvironmentVariables,
150161
client: options.Client,
151162
exchangeToken: options.ExchangeToken,
152163
filesystem: options.Filesystem,
153164
logDir: options.LogDir,
154165
tempDir: options.TempDir,
166+
scriptDataDir: options.ScriptDataDir,
155167
lifecycleUpdate: make(chan struct{}, 1),
156168
lifecycleReported: make(chan codersdk.WorkspaceAgentLifecycle, 1),
157169
lifecycleStates: []agentsdk.PostLifecycleRequest{{State: codersdk.WorkspaceAgentLifecycleCreated}},
@@ -169,6 +181,8 @@ func New(options Options) Agent {
169181
prometheusRegistry: prometheusRegistry,
170182
metrics: newAgentMetrics(prometheusRegistry),
171183
}
184+
a.serviceBanner.Store(new(codersdk.ServiceBannerConfig))
185+
a.sessionToken.Store(new(string))
172186
a.init(ctx)
173187
return a
174188
}
@@ -181,6 +195,7 @@ type agent struct {
181195
filesystem afero.Fs
182196
logDir string
183197
tempDir string
198+
scriptDataDir string
184199
// ignorePorts tells the api handler which ports to ignore when
185200
// listing all listening ports. This is helpful to hide ports that
186201
// are used by the agent, that the user does not care about.
@@ -196,7 +211,7 @@ type agent struct {
196211
closeMutex sync.Mutex
197212
closed chan struct{}
198213

199-
envVars map[string]string
214+
environmentVariables map[string]string
200215

201216
manifest atomic.Pointer[agentsdk.Manifest] // manifest is atomic because values can change after reconnection.
202217
reportMetadataInterval time.Duration
@@ -235,21 +250,24 @@ func (a *agent) TailnetConn() *tailnet.Conn {
235250
}
236251

237252
func (a *agent) init(ctx context.Context) {
238-
sshSrv, err := agentssh.NewServer(ctx, a.logger.Named("ssh-server"), a.prometheusRegistry, a.filesystem, a.sshMaxTimeout, "")
253+
sshSrv, err := agentssh.NewServer(ctx, a.logger.Named("ssh-server"), a.prometheusRegistry, a.filesystem, &agentssh.Config{
254+
MaxTimeout: a.sshMaxTimeout,
255+
MOTDFile: func() string { return a.manifest.Load().MOTDFile },
256+
ServiceBanner: func() *codersdk.ServiceBannerConfig { return a.serviceBanner.Load() },
257+
UpdateEnv: a.updateCommandEnv,
258+
WorkingDirectory: func() string { return a.manifest.Load().Directory },
259+
})
239260
if err != nil {
240261
panic(err)
241262
}
242-
sshSrv.Env = a.envVars
243-
sshSrv.AgentToken = func() string { return *a.sessionToken.Load() }
244-
sshSrv.Manifest = &a.manifest
245-
sshSrv.ServiceBanner = &a.serviceBanner
246263
a.sshServer = sshSrv
247264
a.scriptRunner = agentscripts.New(agentscripts.Options{
248-
LogDir: a.logDir,
249-
Logger: a.logger,
250-
SSHServer: sshSrv,
251-
Filesystem: a.filesystem,
252-
PatchLogs: a.client.PatchLogs,
265+
LogDir: a.logDir,
266+
DataDirBase: a.scriptDataDir,
267+
Logger: a.logger,
268+
SSHServer: sshSrv,
269+
Filesystem: a.filesystem,
270+
PatchLogs: a.client.PatchLogs,
253271
})
254272
// Register runner metrics. If the prom registry is nil, the metrics
255273
// will not report anywhere.
@@ -879,6 +897,90 @@ func (a *agent) run(ctx context.Context) error {
879897
return eg.Wait()
880898
}
881899

900+
// updateCommandEnv updates the provided command environment with the
901+
// following set of environment variables:
902+
// - Predefined workspace environment variables
903+
// - Environment variables currently set (overriding predefined)
904+
// - Environment variables passed via the agent manifest (overriding predefined and current)
905+
// - Agent-level environment variables (overriding all)
906+
func (a *agent) updateCommandEnv(current []string) (updated []string, err error) {
907+
manifest := a.manifest.Load()
908+
if manifest == nil {
909+
return nil, xerrors.Errorf("no manifest")
910+
}
911+
912+
executablePath, err := os.Executable()
913+
if err != nil {
914+
return nil, xerrors.Errorf("getting os executable: %w", err)
915+
}
916+
unixExecutablePath := strings.ReplaceAll(executablePath, "\\", "/")
917+
918+
// Define environment variables that should be set for all commands,
919+
// and then merge them with the current environment.
920+
envs := map[string]string{
921+
// Set env vars indicating we're inside a Coder workspace.
922+
"CODER": "true",
923+
"CODER_WORKSPACE_NAME": manifest.WorkspaceName,
924+
"CODER_WORKSPACE_AGENT_NAME": manifest.AgentName,
925+
926+
// Specific Coder subcommands require the agent token exposed!
927+
"CODER_AGENT_TOKEN": *a.sessionToken.Load(),
928+
929+
// Git on Windows resolves with UNIX-style paths.
930+
// If using backslashes, it's unable to find the executable.
931+
"GIT_SSH_COMMAND": fmt.Sprintf("%s gitssh --", unixExecutablePath),
932+
// Hide Coder message on code-server's "Getting Started" page
933+
"CS_DISABLE_GETTING_STARTED_OVERRIDE": "true",
934+
}
935+
936+
// This adds the ports dialog to code-server that enables
937+
// proxying a port dynamically.
938+
// If this is empty string, do not set anything. Code-server auto defaults
939+
// using its basepath to construct a path based port proxy.
940+
if manifest.VSCodePortProxyURI != "" {
941+
envs["VSCODE_PROXY_URI"] = manifest.VSCodePortProxyURI
942+
}
943+
944+
// Allow any of the current env to override what we defined above.
945+
for _, env := range current {
946+
parts := strings.SplitN(env, "=", 2)
947+
if len(parts) != 2 {
948+
continue
949+
}
950+
if _, ok := envs[parts[0]]; !ok {
951+
envs[parts[0]] = parts[1]
952+
}
953+
}
954+
955+
// Load environment variables passed via the agent manifest.
956+
// These override all variables we manually specify.
957+
for k, v := range manifest.EnvironmentVariables {
958+
// Expanding environment variables allows for customization
959+
// of the $PATH, among other variables. Customers can prepend
960+
// or append to the $PATH, so allowing expand is required!
961+
envs[k] = os.ExpandEnv(v)
962+
}
963+
964+
// Agent-level environment variables should take over all. This is
965+
// used for setting agent-specific variables like CODER_AGENT_TOKEN
966+
// and GIT_ASKPASS.
967+
for k, v := range a.environmentVariables {
968+
envs[k] = v
969+
}
970+
971+
// Prepend the agent script bin directory to the PATH
972+
// (this is where Coder modules place their binaries).
973+
if _, ok := envs["PATH"]; !ok {
974+
envs["PATH"] = os.Getenv("PATH")
975+
}
976+
envs["PATH"] = fmt.Sprintf("%s%c%s", a.scriptRunner.ScriptBinDir(), filepath.ListSeparator, envs["PATH"])
977+
978+
for k, v := range envs {
979+
updated = append(updated, fmt.Sprintf("%s=%s", k, v))
980+
}
981+
return updated, nil
982+
}
983+
882984
func (a *agent) wireguardAddresses(agentID uuid.UUID) []netip.Prefix {
883985
if len(a.addresses) == 0 {
884986
return []netip.Prefix{
@@ -1314,7 +1416,7 @@ func (a *agent) manageProcessPriorityLoop(ctx context.Context) {
13141416
}
13151417
}()
13161418

1317-
if val := a.envVars[EnvProcPrioMgmt]; val == "" || runtime.GOOS != "linux" {
1419+
if val := a.environmentVariables[EnvProcPrioMgmt]; val == "" || runtime.GOOS != "linux" {
13181420
a.logger.Debug(ctx, "process priority not enabled, agent will not manage process niceness/oom_score_adj ",
13191421
slog.F("env_var", EnvProcPrioMgmt),
13201422
slog.F("value", val),

agent/agent_test.go

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"bytes"
66
"context"
77
"encoding/json"
8+
"errors"
89
"fmt"
910
"io"
1011
"math/rand"
@@ -281,6 +282,91 @@ func TestAgent_SessionExec(t *testing.T) {
281282
require.Equal(t, "test", strings.TrimSpace(string(output)))
282283
}
283284

285+
//nolint:tparallel // Sub tests need to run sequentially.
286+
func TestAgent_Session_EnvironmentVariables(t *testing.T) {
287+
t.Parallel()
288+
289+
tmpdir := t.TempDir()
290+
291+
// Defined by the coder script runner, hardcoded here since we don't
292+
// have a reference to it.
293+
scriptBinDir := filepath.Join(tmpdir, "coder-script-data", "bin")
294+
295+
manifest := agentsdk.Manifest{
296+
EnvironmentVariables: map[string]string{
297+
"MY_MANIFEST": "true",
298+
"MY_OVERRIDE": "false",
299+
"MY_SESSION_MANIFEST": "false",
300+
},
301+
}
302+
banner := codersdk.ServiceBannerConfig{}
303+
session := setupSSHSession(t, manifest, banner, nil, func(_ *agenttest.Client, opts *agent.Options) {
304+
opts.ScriptDataDir = tmpdir
305+
opts.EnvironmentVariables["MY_OVERRIDE"] = "true"
306+
})
307+
308+
err := session.Setenv("MY_SESSION_MANIFEST", "true")
309+
require.NoError(t, err)
310+
err = session.Setenv("MY_SESSION", "true")
311+
require.NoError(t, err)
312+
313+
command := "sh"
314+
echoEnv := func(t *testing.T, w io.Writer, env string) {
315+
if runtime.GOOS == "windows" {
316+
_, err := fmt.Fprintf(w, "echo %%%s%%\r\n", env)
317+
require.NoError(t, err)
318+
} else {
319+
_, err := fmt.Fprintf(w, "echo $%s\n", env)
320+
require.NoError(t, err)
321+
}
322+
}
323+
if runtime.GOOS == "windows" {
324+
command = "cmd.exe"
325+
}
326+
stdin, err := session.StdinPipe()
327+
require.NoError(t, err)
328+
defer stdin.Close()
329+
stdout, err := session.StdoutPipe()
330+
require.NoError(t, err)
331+
332+
err = session.Start(command)
333+
require.NoError(t, err)
334+
335+
// Context is fine here since we're not doing a parallel subtest.
336+
ctx := testutil.Context(t, testutil.WaitLong)
337+
go func() {
338+
<-ctx.Done()
339+
_ = session.Close()
340+
}()
341+
342+
s := bufio.NewScanner(stdout)
343+
344+
//nolint:paralleltest // These tests need to run sequentially.
345+
for k, partialV := range map[string]string{
346+
"CODER": "true", // From the agent.
347+
"MY_MANIFEST": "true", // From the manifest.
348+
"MY_OVERRIDE": "true", // From the agent environment variables option, overrides manifest.
349+
"MY_SESSION_MANIFEST": "false", // From the manifest, overrides session env.
350+
"MY_SESSION": "true", // From the session.
351+
"PATH": scriptBinDir + string(filepath.ListSeparator),
352+
} {
353+
t.Run(k, func(t *testing.T) {
354+
echoEnv(t, stdin, k)
355+
// Windows is unreliable, so keep scanning until we find a match.
356+
for s.Scan() {
357+
got := strings.TrimSpace(s.Text())
358+
t.Logf("%s=%s", k, got)
359+
if strings.Contains(got, partialV) {
360+
break
361+
}
362+
}
363+
if err := s.Err(); !errors.Is(err, io.EOF) {
364+
require.NoError(t, err)
365+
}
366+
})
367+
}
368+
}
369+
284370
func TestAgent_GitSSH(t *testing.T) {
285371
t.Parallel()
286372
session := setupSSHSession(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil)
@@ -1991,15 +2077,17 @@ func setupSSHSession(
19912077
manifest agentsdk.Manifest,
19922078
serviceBanner codersdk.ServiceBannerConfig,
19932079
prepareFS func(fs afero.Fs),
2080+
opts ...func(*agenttest.Client, *agent.Options),
19942081
) *ssh.Session {
19952082
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
19962083
defer cancel()
1997-
//nolint:dogsled
1998-
conn, _, _, fs, _ := setupAgent(t, manifest, 0, func(c *agenttest.Client, _ *agent.Options) {
2084+
opts = append(opts, func(c *agenttest.Client, o *agent.Options) {
19992085
c.SetServiceBannerFunc(func() (codersdk.ServiceBannerConfig, error) {
20002086
return serviceBanner, nil
20012087
})
20022088
})
2089+
//nolint:dogsled
2090+
conn, _, _, fs, _ := setupAgent(t, manifest, 0, opts...)
20032091
if prepareFS != nil {
20042092
prepareFS(fs)
20052093
}
@@ -2057,6 +2145,7 @@ func setupAgent(t *testing.T, metadata agentsdk.Manifest, ptyTimeout time.Durati
20572145
Filesystem: fs,
20582146
Logger: logger.Named("agent"),
20592147
ReconnectingPTYTimeout: ptyTimeout,
2148+
EnvironmentVariables: map[string]string{},
20602149
}
20612150

20622151
for _, opt := range opts {

0 commit comments

Comments
 (0)