@@ -78,6 +78,7 @@ type API struct {
78
78
devcontainerNames map [string ]bool // By devcontainer name.
79
79
knownDevcontainers map [string ]codersdk.WorkspaceAgentDevcontainer // By workspace folder.
80
80
configFileModifiedTimes map [string ]time.Time // By config file path.
81
+ ignoredDevcontainers map [string ]bool // By workspace folder. Tracks three states (true, false and not checked).
81
82
recreateSuccessTimes map [string ]time.Time // By workspace folder.
82
83
recreateErrorTimes map [string ]time.Time // By workspace folder.
83
84
injectedSubAgentProcs map [string ]subAgentProcess // By workspace folder.
@@ -274,6 +275,7 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
274
275
devcontainerNames :make (map [string ]bool ),
275
276
knownDevcontainers :make (map [string ]codersdk.WorkspaceAgentDevcontainer ),
276
277
configFileModifiedTimes :make (map [string ]time.Time ),
278
+ ignoredDevcontainers :make (map [string ]bool ),
277
279
recreateSuccessTimes :make (map [string ]time.Time ),
278
280
recreateErrorTimes :make (map [string ]time.Time ),
279
281
scriptLogger :func (uuid.UUID )ScriptLogger {return noopScriptLogger {} },
@@ -750,6 +752,10 @@ func (api *API) getContainers() (codersdk.WorkspaceAgentListContainersResponse,
750
752
if len (api .knownDevcontainers )> 0 {
751
753
devcontainers = make ([]codersdk.WorkspaceAgentDevcontainer ,0 ,len (api .knownDevcontainers ))
752
754
for _ ,dc := range api .knownDevcontainers {
755
+ if api .ignoredDevcontainers [dc .WorkspaceFolder ] {
756
+ continue
757
+ }
758
+
753
759
// Include the agent if it's running (we're iterating over
754
760
// copies, so mutating is fine).
755
761
if proc := api .injectedSubAgentProcs [dc .WorkspaceFolder ];proc .agent .ID != uuid .Nil {
@@ -982,11 +988,46 @@ func (api *API) markDevcontainerDirty(configPath string, modifiedAt time.Time) {
982
988
logger .Info (api .ctx ,"marking devcontainer as dirty" )
983
989
dc .Dirty = true
984
990
}
991
+ if api .ignoredDevcontainers [dc .WorkspaceFolder ] {
992
+ logger .Debug (api .ctx ,"clearing devcontainer ignored state" )
993
+ delete (api .ignoredDevcontainers ,dc .WorkspaceFolder )// Allow re-reading config.
994
+ }
985
995
986
996
api .knownDevcontainers [dc .WorkspaceFolder ]= dc
987
997
}
988
998
}
989
999
1000
+ // markDevcontainerIgnored marks a devcontainer as ignored. Must not be called
1001
+ // with the API lock held.
1002
+ func (api * API )markDevcontainerIgnored (ctx context.Context ,dc codersdk.WorkspaceAgentDevcontainer ,proc subAgentProcess )subAgentProcess {
1003
+ logger := api .logger .With (
1004
+ slog .F ("devcontainer_id" ,dc .ID ),
1005
+ slog .F ("devcontainer_name" ,dc .Name ),
1006
+ slog .F ("workspace_folder" ,dc .WorkspaceFolder ),
1007
+ slog .F ("config_path" ,dc .ConfigPath ),
1008
+ )
1009
+
1010
+ // We only allow ignore to be set in the root customization layer to
1011
+ // prevent weird interactions with devcontainer features.
1012
+ logger .Debug (ctx ,"marking devcontainer as ignored" )
1013
+ proc .stop ()
1014
+ if proc .agent .ID != uuid .Nil {
1015
+ // If we stop the subagent, we also need to delete it.
1016
+ client := * api .subAgentClient .Load ()
1017
+ if err := client .Delete (ctx ,proc .agent .ID );err != nil {
1018
+ api .logger .Error (ctx ,"delete subagent failed" ,slog .Error (err ),slog .F ("subagent_id" ,proc .agent .ID ))
1019
+ }
1020
+ }
1021
+ // Reset agent and containerID to force config re-reading if ignore is toggled.
1022
+ proc .agent = SubAgent {}
1023
+ proc .containerID = ""
1024
+ api .mu .Lock ()
1025
+ api .ignoredDevcontainers [dc .WorkspaceFolder ]= true
1026
+ api .mu .Unlock ()
1027
+
1028
+ return proc
1029
+ }
1030
+
990
1031
// cleanupSubAgents removes subagents that are no longer managed by
991
1032
// this agent. This is usually only run at startup to ensure a clean
992
1033
// slate. This method has an internal timeout to prevent blocking
@@ -1038,6 +1079,10 @@ func (api *API) cleanupSubAgents(ctx context.Context) error {
1038
1079
// This method uses an internal timeout to prevent blocking indefinitely
1039
1080
// if something goes wrong with the injection.
1040
1081
func (api * API )maybeInjectSubAgentIntoContainerLocked (ctx context.Context ,dc codersdk.WorkspaceAgentDevcontainer ) (err error ) {
1082
+ if api .ignoredDevcontainers [dc .WorkspaceFolder ] {
1083
+ return nil
1084
+ }
1085
+
1041
1086
ctx ,cancel := context .WithTimeout (ctx ,defaultOperationTimeout )
1042
1087
defer cancel ()
1043
1088
@@ -1059,6 +1104,29 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1059
1104
maybeRecreateSubAgent := false
1060
1105
proc ,injected := api .injectedSubAgentProcs [dc .WorkspaceFolder ]
1061
1106
if injected {
1107
+ if _ ,ignoreChecked := api .ignoredDevcontainers [dc .WorkspaceFolder ];! ignoreChecked {
1108
+ // If ignore status has not yet been checked, or cleared by
1109
+ // modifications to the devcontainer.json, we must read it
1110
+ // to determine the current status. This can happen while
1111
+ // the devcontainer subagent is already running or before
1112
+ // we've had a chance to inject it.
1113
+ //
1114
+ // Note, for simplicity, we do not try to optimize to reduce
1115
+ // ReadConfig calls here.
1116
+ config ,err := api .dccli .ReadConfig (ctx ,dc .WorkspaceFolder ,dc .ConfigPath ,nil )
1117
+ if err != nil {
1118
+ return xerrors .Errorf ("read devcontainer config: %w" ,err )
1119
+ }
1120
+
1121
+ if config .Configuration .Customizations .Coder .Ignore {
1122
+ api .mu .Unlock ()
1123
+ proc = api .markDevcontainerIgnored (ctx ,dc ,proc )
1124
+ api .mu .Lock ()
1125
+ api .injectedSubAgentProcs [dc .WorkspaceFolder ]= proc
1126
+ return nil
1127
+ }
1128
+ }
1129
+
1062
1130
if proc .containerID == container .ID && proc .ctx .Err ()== nil {
1063
1131
// Same container and running, no need to reinject.
1064
1132
return nil
@@ -1077,7 +1145,8 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1077
1145
// Container ID changed or the subagent process is not running,
1078
1146
// stop the existing subagent context to replace it.
1079
1147
proc .stop ()
1080
- }else {
1148
+ }
1149
+ if proc .agent .OperatingSystem == "" {
1081
1150
// Set SubAgent defaults.
1082
1151
proc .agent .OperatingSystem = "linux" // Assuming Linux for devcontainers.
1083
1152
}
@@ -1134,48 +1203,6 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1134
1203
proc .agent .Architecture = arch
1135
1204
}
1136
1205
1137
- agentBinaryPath ,err := os .Executable ()
1138
- if err != nil {
1139
- return xerrors .Errorf ("get agent binary path: %w" ,err )
1140
- }
1141
- agentBinaryPath ,err = filepath .EvalSymlinks (agentBinaryPath )
1142
- if err != nil {
1143
- return xerrors .Errorf ("resolve agent binary path: %w" ,err )
1144
- }
1145
-
1146
- // If we scripted this as a `/bin/sh` script, we could reduce these
1147
- // steps to one instruction, speeding up the injection process.
1148
- //
1149
- // Note: We use `path` instead of `filepath` here because we are
1150
- // working with Unix-style paths inside the container.
1151
- if _ ,err := api .ccli .ExecAs (ctx ,container .ID ,"root" ,"mkdir" ,"-p" ,path .Dir (coderPathInsideContainer ));err != nil {
1152
- return xerrors .Errorf ("create agent directory in container: %w" ,err )
1153
- }
1154
-
1155
- if err := api .ccli .Copy (ctx ,container .ID ,agentBinaryPath ,coderPathInsideContainer );err != nil {
1156
- return xerrors .Errorf ("copy agent binary: %w" ,err )
1157
- }
1158
-
1159
- logger .Info (ctx ,"copied agent binary to container" )
1160
-
1161
- // Make sure the agent binary is executable so we can run it (the
1162
- // user doesn't matter since we're making it executable for all).
1163
- if _ ,err := api .ccli .ExecAs (ctx ,container .ID ,"root" ,"chmod" ,"0755" ,path .Dir (coderPathInsideContainer ),coderPathInsideContainer );err != nil {
1164
- return xerrors .Errorf ("set agent binary executable: %w" ,err )
1165
- }
1166
-
1167
- // Attempt to add CAP_NET_ADMIN to the binary to improve network
1168
- // performance (optional, allow to fail). See `bootstrap_linux.sh`.
1169
- // TODO(mafredri): Disable for now until we can figure out why this
1170
- // causes the following error on some images:
1171
- //
1172
- //Image: mcr.microsoft.com/devcontainers/base:ubuntu
1173
- // Error: /.coder-agent/coder: Operation not permitted
1174
- //
1175
- // if _, err := api.ccli.ExecAs(ctx, container.ID, "root", "setcap", "cap_net_admin+ep", coderPathInsideContainer); err != nil {
1176
- // logger.Warn(ctx, "set CAP_NET_ADMIN on agent binary failed", slog.Error(err))
1177
- // }
1178
-
1179
1206
subAgentConfig := proc .agent .CloneConfig (dc )
1180
1207
if proc .agent .ID == uuid .Nil || maybeRecreateSubAgent {
1181
1208
subAgentConfig .Architecture = arch
@@ -1192,6 +1219,7 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1192
1219
}
1193
1220
1194
1221
var (
1222
+ ignore bool
1195
1223
appsWithPossibleDuplicates []SubAgentApp
1196
1224
workspaceFolder = DevcontainerDefaultContainerWorkspaceFolder
1197
1225
)
@@ -1215,8 +1243,13 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1215
1243
return err
1216
1244
}
1217
1245
1246
+ ignore = config .Configuration .Customizations .Coder .Ignore
1218
1247
workspaceFolder = config .Workspace .WorkspaceFolder
1219
1248
1249
+ if ignore {
1250
+ return nil
1251
+ }
1252
+
1220
1253
// NOTE(DanielleMaywood):
1221
1254
// We only want to take an agent name specified in the root customization layer.
1222
1255
// This restricts the ability for a feature to specify the agent name. We may revisit
@@ -1262,6 +1295,11 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1262
1295
api .logger .Error (ctx ,"unable to read devcontainer config" ,slog .Error (err ))
1263
1296
}
1264
1297
1298
+ if ignore {
1299
+ proc = api .markDevcontainerIgnored (ctx ,dc ,proc )
1300
+ return nil
1301
+ }
1302
+
1265
1303
displayApps := make ([]codersdk.DisplayApp ,0 ,len (displayAppsMap ))
1266
1304
for app ,enabled := range displayAppsMap {
1267
1305
if enabled {
@@ -1294,6 +1332,48 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1294
1332
subAgentConfig .Directory = workspaceFolder
1295
1333
}
1296
1334
1335
+ agentBinaryPath ,err := os .Executable ()
1336
+ if err != nil {
1337
+ return xerrors .Errorf ("get agent binary path: %w" ,err )
1338
+ }
1339
+ agentBinaryPath ,err = filepath .EvalSymlinks (agentBinaryPath )
1340
+ if err != nil {
1341
+ return xerrors .Errorf ("resolve agent binary path: %w" ,err )
1342
+ }
1343
+
1344
+ // If we scripted this as a `/bin/sh` script, we could reduce these
1345
+ // steps to one instruction, speeding up the injection process.
1346
+ //
1347
+ // Note: We use `path` instead of `filepath` here because we are
1348
+ // working with Unix-style paths inside the container.
1349
+ if _ ,err := api .ccli .ExecAs (ctx ,container .ID ,"root" ,"mkdir" ,"-p" ,path .Dir (coderPathInsideContainer ));err != nil {
1350
+ return xerrors .Errorf ("create agent directory in container: %w" ,err )
1351
+ }
1352
+
1353
+ if err := api .ccli .Copy (ctx ,container .ID ,agentBinaryPath ,coderPathInsideContainer );err != nil {
1354
+ return xerrors .Errorf ("copy agent binary: %w" ,err )
1355
+ }
1356
+
1357
+ logger .Info (ctx ,"copied agent binary to container" )
1358
+
1359
+ // Make sure the agent binary is executable so we can run it (the
1360
+ // user doesn't matter since we're making it executable for all).
1361
+ if _ ,err := api .ccli .ExecAs (ctx ,container .ID ,"root" ,"chmod" ,"0755" ,path .Dir (coderPathInsideContainer ),coderPathInsideContainer );err != nil {
1362
+ return xerrors .Errorf ("set agent binary executable: %w" ,err )
1363
+ }
1364
+
1365
+ // Attempt to add CAP_NET_ADMIN to the binary to improve network
1366
+ // performance (optional, allow to fail). See `bootstrap_linux.sh`.
1367
+ // TODO(mafredri): Disable for now until we can figure out why this
1368
+ // causes the following error on some images:
1369
+ //
1370
+ //Image: mcr.microsoft.com/devcontainers/base:ubuntu
1371
+ // Error: /.coder-agent/coder: Operation not permitted
1372
+ //
1373
+ // if _, err := api.ccli.ExecAs(ctx, container.ID, "root", "setcap", "cap_net_admin+ep", coderPathInsideContainer); err != nil {
1374
+ // logger.Warn(ctx, "set CAP_NET_ADMIN on agent binary failed", slog.Error(err))
1375
+ // }
1376
+
1297
1377
deleteSubAgent := proc .agent .ID != uuid .Nil && maybeRecreateSubAgent && ! proc .agent .EqualConfig (subAgentConfig )
1298
1378
if deleteSubAgent {
1299
1379
logger .Debug (ctx ,"deleting existing subagent for recreation" ,slog .F ("agent_id" ,proc .agent .ID ))