@@ -83,6 +83,7 @@ type API struct {
83
83
recreateErrorTimes map [string ]time.Time // By workspace folder.
84
84
injectedSubAgentProcs map [string ]subAgentProcess // By workspace folder.
85
85
usingWorkspaceFolderName map [string ]bool // By workspace folder.
86
+ ignoredDevcontainers map [string ]bool // By workspace folder. Tracks three states (true, false and not checked).
86
87
asyncWg sync.WaitGroup
87
88
88
89
devcontainerLogSourceIDs map [string ]uuid.UUID // By workspace folder.
@@ -276,6 +277,7 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
276
277
devcontainerNames :make (map [string ]bool ),
277
278
knownDevcontainers :make (map [string ]codersdk.WorkspaceAgentDevcontainer ),
278
279
configFileModifiedTimes :make (map [string ]time.Time ),
280
+ ignoredDevcontainers :make (map [string ]bool ),
279
281
recreateSuccessTimes :make (map [string ]time.Time ),
280
282
recreateErrorTimes :make (map [string ]time.Time ),
281
283
scriptLogger :func (uuid.UUID )ScriptLogger {return noopScriptLogger {} },
@@ -804,6 +806,10 @@ func (api *API) getContainers() (codersdk.WorkspaceAgentListContainersResponse,
804
806
if len (api .knownDevcontainers )> 0 {
805
807
devcontainers = make ([]codersdk.WorkspaceAgentDevcontainer ,0 ,len (api .knownDevcontainers ))
806
808
for _ ,dc := range api .knownDevcontainers {
809
+ if api .ignoredDevcontainers [dc .WorkspaceFolder ] {
810
+ continue
811
+ }
812
+
807
813
// Include the agent if it's running (we're iterating over
808
814
// copies, so mutating is fine).
809
815
if proc := api .injectedSubAgentProcs [dc .WorkspaceFolder ];proc .agent .ID != uuid .Nil {
@@ -1036,6 +1042,10 @@ func (api *API) markDevcontainerDirty(configPath string, modifiedAt time.Time) {
1036
1042
logger .Info (api .ctx ,"marking devcontainer as dirty" )
1037
1043
dc .Dirty = true
1038
1044
}
1045
+ if _ ,ok := api .ignoredDevcontainers [dc .WorkspaceFolder ];ok {
1046
+ logger .Debug (api .ctx ,"clearing devcontainer ignored state" )
1047
+ delete (api .ignoredDevcontainers ,dc .WorkspaceFolder )// Allow re-reading config.
1048
+ }
1039
1049
1040
1050
api .knownDevcontainers [dc .WorkspaceFolder ]= dc
1041
1051
}
@@ -1092,6 +1102,10 @@ func (api *API) cleanupSubAgents(ctx context.Context) error {
1092
1102
// This method uses an internal timeout to prevent blocking indefinitely
1093
1103
// if something goes wrong with the injection.
1094
1104
func (api * API )maybeInjectSubAgentIntoContainerLocked (ctx context.Context ,dc codersdk.WorkspaceAgentDevcontainer ) (err error ) {
1105
+ if api .ignoredDevcontainers [dc .WorkspaceFolder ] {
1106
+ return nil
1107
+ }
1108
+
1095
1109
ctx ,cancel := context .WithTimeout (ctx ,defaultOperationTimeout )
1096
1110
defer cancel ()
1097
1111
@@ -1113,6 +1127,42 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1113
1127
maybeRecreateSubAgent := false
1114
1128
proc ,injected := api .injectedSubAgentProcs [dc .WorkspaceFolder ]
1115
1129
if injected {
1130
+ if _ ,ignoreChecked := api .ignoredDevcontainers [dc .WorkspaceFolder ];! ignoreChecked {
1131
+ // If ignore status has not yet been checked, or cleared by
1132
+ // modifications to the devcontainer.json, we must read it
1133
+ // to determine the current status. This can happen while
1134
+ // the devcontainer subagent is already running or before
1135
+ // we've had a chance to inject it.
1136
+ //
1137
+ // Note, for simplicity, we do not try to optimize to reduce
1138
+ // ReadConfig calls here.
1139
+ config ,err := api .dccli .ReadConfig (ctx ,dc .WorkspaceFolder ,dc .ConfigPath ,nil )
1140
+ if err != nil {
1141
+ return xerrors .Errorf ("read devcontainer config: %w" ,err )
1142
+ }
1143
+
1144
+ dcIgnored := config .Configuration .Customizations .Coder .Ignore
1145
+ if dcIgnored {
1146
+ proc .stop ()
1147
+ if proc .agent .ID != uuid .Nil {
1148
+ // Unlock while doing the delete operation.
1149
+ api .mu .Unlock ()
1150
+ client := * api .subAgentClient .Load ()
1151
+ if err := client .Delete (ctx ,proc .agent .ID );err != nil {
1152
+ api .mu .Lock ()
1153
+ return xerrors .Errorf ("delete subagent: %w" ,err )
1154
+ }
1155
+ api .mu .Lock ()
1156
+ }
1157
+ // Reset agent and containerID to force config re-reading if ignore is toggled.
1158
+ proc .agent = SubAgent {}
1159
+ proc .containerID = ""
1160
+ api .injectedSubAgentProcs [dc .WorkspaceFolder ]= proc
1161
+ api .ignoredDevcontainers [dc .WorkspaceFolder ]= dcIgnored
1162
+ return nil
1163
+ }
1164
+ }
1165
+
1116
1166
if proc .containerID == container .ID && proc .ctx .Err ()== nil {
1117
1167
// Same container and running, no need to reinject.
1118
1168
return nil
@@ -1131,7 +1181,8 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1131
1181
// Container ID changed or the subagent process is not running,
1132
1182
// stop the existing subagent context to replace it.
1133
1183
proc .stop ()
1134
- }else {
1184
+ }
1185
+ if proc .agent .OperatingSystem == "" {
1135
1186
// Set SubAgent defaults.
1136
1187
proc .agent .OperatingSystem = "linux" // Assuming Linux for devcontainers.
1137
1188
}
@@ -1150,7 +1201,11 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1150
1201
ranSubAgent := false
1151
1202
1152
1203
// Clean up if injection fails.
1204
+ var dcIgnored ,setDCIgnored bool
1153
1205
defer func () {
1206
+ if setDCIgnored {
1207
+ api .ignoredDevcontainers [dc .WorkspaceFolder ]= dcIgnored
1208
+ }
1154
1209
if ! ranSubAgent {
1155
1210
proc .stop ()
1156
1211
if ! api .closed {
@@ -1188,48 +1243,6 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1188
1243
proc .agent .Architecture = arch
1189
1244
}
1190
1245
1191
- agentBinaryPath ,err := os .Executable ()
1192
- if err != nil {
1193
- return xerrors .Errorf ("get agent binary path: %w" ,err )
1194
- }
1195
- agentBinaryPath ,err = filepath .EvalSymlinks (agentBinaryPath )
1196
- if err != nil {
1197
- return xerrors .Errorf ("resolve agent binary path: %w" ,err )
1198
- }
1199
-
1200
- // If we scripted this as a `/bin/sh` script, we could reduce these
1201
- // steps to one instruction, speeding up the injection process.
1202
- //
1203
- // Note: We use `path` instead of `filepath` here because we are
1204
- // working with Unix-style paths inside the container.
1205
- if _ ,err := api .ccli .ExecAs (ctx ,container .ID ,"root" ,"mkdir" ,"-p" ,path .Dir (coderPathInsideContainer ));err != nil {
1206
- return xerrors .Errorf ("create agent directory in container: %w" ,err )
1207
- }
1208
-
1209
- if err := api .ccli .Copy (ctx ,container .ID ,agentBinaryPath ,coderPathInsideContainer );err != nil {
1210
- return xerrors .Errorf ("copy agent binary: %w" ,err )
1211
- }
1212
-
1213
- logger .Info (ctx ,"copied agent binary to container" )
1214
-
1215
- // Make sure the agent binary is executable so we can run it (the
1216
- // user doesn't matter since we're making it executable for all).
1217
- if _ ,err := api .ccli .ExecAs (ctx ,container .ID ,"root" ,"chmod" ,"0755" ,path .Dir (coderPathInsideContainer ),coderPathInsideContainer );err != nil {
1218
- return xerrors .Errorf ("set agent binary executable: %w" ,err )
1219
- }
1220
-
1221
- // Attempt to add CAP_NET_ADMIN to the binary to improve network
1222
- // performance (optional, allow to fail). See `bootstrap_linux.sh`.
1223
- // TODO(mafredri): Disable for now until we can figure out why this
1224
- // causes the following error on some images:
1225
- //
1226
- //Image: mcr.microsoft.com/devcontainers/base:ubuntu
1227
- // Error: /.coder-agent/coder: Operation not permitted
1228
- //
1229
- // if _, err := api.ccli.ExecAs(ctx, container.ID, "root", "setcap", "cap_net_admin+ep", coderPathInsideContainer); err != nil {
1230
- // logger.Warn(ctx, "set CAP_NET_ADMIN on agent binary failed", slog.Error(err))
1231
- // }
1232
-
1233
1246
subAgentConfig := proc .agent .CloneConfig (dc )
1234
1247
if proc .agent .ID == uuid .Nil || maybeRecreateSubAgent {
1235
1248
subAgentConfig .Architecture = arch
@@ -1269,6 +1282,13 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1269
1282
return err
1270
1283
}
1271
1284
1285
+ // We only allow ignore to be set in the root customization layer to
1286
+ // prevent weird interactions with devcontainer features.
1287
+ dcIgnored ,setDCIgnored = config .Configuration .Customizations .Coder .Ignore ,true
1288
+ if dcIgnored {
1289
+ return nil
1290
+ }
1291
+
1272
1292
workspaceFolder = config .Workspace .WorkspaceFolder
1273
1293
1274
1294
// NOTE(DanielleMaywood):
@@ -1317,6 +1337,22 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1317
1337
api .logger .Error (ctx ,"unable to read devcontainer config" ,slog .Error (err ))
1318
1338
}
1319
1339
1340
+ if dcIgnored {
1341
+ proc .stop ()
1342
+ if proc .agent .ID != uuid .Nil {
1343
+ // If we stop the subagent, we also need to delete it.
1344
+ client := * api .subAgentClient .Load ()
1345
+ if err := client .Delete (ctx ,proc .agent .ID );err != nil {
1346
+ return xerrors .Errorf ("delete subagent: %w" ,err )
1347
+ }
1348
+ }
1349
+ // Reset agent and containerID to force config re-reading if
1350
+ // ignore is toggled.
1351
+ proc .agent = SubAgent {}
1352
+ proc .containerID = ""
1353
+ return nil
1354
+ }
1355
+
1320
1356
displayApps := make ([]codersdk.DisplayApp ,0 ,len (displayAppsMap ))
1321
1357
for app ,enabled := range displayAppsMap {
1322
1358
if enabled {
@@ -1349,6 +1385,48 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1349
1385
subAgentConfig .Directory = workspaceFolder
1350
1386
}
1351
1387
1388
+ agentBinaryPath ,err := os .Executable ()
1389
+ if err != nil {
1390
+ return xerrors .Errorf ("get agent binary path: %w" ,err )
1391
+ }
1392
+ agentBinaryPath ,err = filepath .EvalSymlinks (agentBinaryPath )
1393
+ if err != nil {
1394
+ return xerrors .Errorf ("resolve agent binary path: %w" ,err )
1395
+ }
1396
+
1397
+ // If we scripted this as a `/bin/sh` script, we could reduce these
1398
+ // steps to one instruction, speeding up the injection process.
1399
+ //
1400
+ // Note: We use `path` instead of `filepath` here because we are
1401
+ // working with Unix-style paths inside the container.
1402
+ if _ ,err := api .ccli .ExecAs (ctx ,container .ID ,"root" ,"mkdir" ,"-p" ,path .Dir (coderPathInsideContainer ));err != nil {
1403
+ return xerrors .Errorf ("create agent directory in container: %w" ,err )
1404
+ }
1405
+
1406
+ if err := api .ccli .Copy (ctx ,container .ID ,agentBinaryPath ,coderPathInsideContainer );err != nil {
1407
+ return xerrors .Errorf ("copy agent binary: %w" ,err )
1408
+ }
1409
+
1410
+ logger .Info (ctx ,"copied agent binary to container" )
1411
+
1412
+ // Make sure the agent binary is executable so we can run it (the
1413
+ // user doesn't matter since we're making it executable for all).
1414
+ if _ ,err := api .ccli .ExecAs (ctx ,container .ID ,"root" ,"chmod" ,"0755" ,path .Dir (coderPathInsideContainer ),coderPathInsideContainer );err != nil {
1415
+ return xerrors .Errorf ("set agent binary executable: %w" ,err )
1416
+ }
1417
+
1418
+ // Attempt to add CAP_NET_ADMIN to the binary to improve network
1419
+ // performance (optional, allow to fail). See `bootstrap_linux.sh`.
1420
+ // TODO(mafredri): Disable for now until we can figure out why this
1421
+ // causes the following error on some images:
1422
+ //
1423
+ //Image: mcr.microsoft.com/devcontainers/base:ubuntu
1424
+ // Error: /.coder-agent/coder: Operation not permitted
1425
+ //
1426
+ // if _, err := api.ccli.ExecAs(ctx, container.ID, "root", "setcap", "cap_net_admin+ep", coderPathInsideContainer); err != nil {
1427
+ // logger.Warn(ctx, "set CAP_NET_ADMIN on agent binary failed", slog.Error(err))
1428
+ // }
1429
+
1352
1430
deleteSubAgent := proc .agent .ID != uuid .Nil && maybeRecreateSubAgent && ! proc .agent .EqualConfig (subAgentConfig )
1353
1431
if deleteSubAgent {
1354
1432
logger .Debug (ctx ,"deleting existing subagent for recreation" ,slog .F ("agent_id" ,proc .agent .ID ))