@@ -42,7 +42,8 @@ const (
42
42
// read-write, which seems sensible for devcontainers.
43
43
coderPathInsideContainer = "/.coder-agent/coder"
44
44
45
- maxAgentNameLength = 64
45
+ maxAgentNameLength = 64
46
+ maxAttemptsToNameAgent = 5
46
47
)
47
48
48
49
// API is responsible for container-related operations in the agent.
@@ -71,17 +72,18 @@ type API struct {
71
72
ownerName string
72
73
workspaceName string
73
74
74
- mu sync.RWMutex
75
- closed bool
76
- containers codersdk.WorkspaceAgentListContainersResponse // Output from the last list operation.
77
- containersErr error // Error from the last list operation.
78
- devcontainerNames map [string ]bool // By devcontainer name.
79
- knownDevcontainers map [string ]codersdk.WorkspaceAgentDevcontainer // By workspace folder.
80
- configFileModifiedTimes map [string ]time.Time // By config file path.
81
- recreateSuccessTimes map [string ]time.Time // By workspace folder.
82
- recreateErrorTimes map [string ]time.Time // By workspace folder.
83
- injectedSubAgentProcs map [string ]subAgentProcess // By workspace folder.
84
- asyncWg sync.WaitGroup
75
+ mu sync.RWMutex
76
+ closed bool
77
+ containers codersdk.WorkspaceAgentListContainersResponse // Output from the last list operation.
78
+ containersErr error // Error from the last list operation.
79
+ devcontainerNames map [string ]bool // By devcontainer name.
80
+ knownDevcontainers map [string ]codersdk.WorkspaceAgentDevcontainer // By workspace folder.
81
+ configFileModifiedTimes map [string ]time.Time // By config file path.
82
+ recreateSuccessTimes map [string ]time.Time // By workspace folder.
83
+ recreateErrorTimes map [string ]time.Time // By workspace folder.
84
+ injectedSubAgentProcs map [string ]subAgentProcess // By workspace folder.
85
+ usingWorkspaceFolderName map [string ]bool // By workspace folder.
86
+ asyncWg sync.WaitGroup
85
87
86
88
devcontainerLogSourceIDs map [string ]uuid.UUID // By workspace folder.
87
89
}
@@ -278,6 +280,7 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
278
280
recreateErrorTimes :make (map [string ]time.Time ),
279
281
scriptLogger :func (uuid.UUID )ScriptLogger {return noopScriptLogger {} },
280
282
injectedSubAgentProcs :make (map [string ]subAgentProcess ),
283
+ usingWorkspaceFolderName :make (map [string ]bool ),
281
284
}
282
285
// The ctx and logger must be set before applying options to avoid
283
286
// nil pointer dereference.
@@ -630,7 +633,7 @@ func (api *API) processUpdatedContainersLocked(ctx context.Context, updated code
630
633
// folder's name. If it is not possible to generate a valid
631
634
// agent name based off of the folder name (i.e. no valid characters),
632
635
// we will instead fall back to using the container's friendly name.
633
- dc .Name = safeAgentName ( path . Base ( filepath . ToSlash ( dc .WorkspaceFolder )) ,dc .Container .FriendlyName )
636
+ dc .Name , api . usingWorkspaceFolderName [ dc . WorkspaceFolder ] = api . makeAgentName ( dc .WorkspaceFolder ,dc .Container .FriendlyName )
634
637
}
635
638
}
636
639
@@ -678,8 +681,10 @@ func (api *API) processUpdatedContainersLocked(ctx context.Context, updated code
678
681
var consecutiveHyphenRegex = regexp .MustCompile ("-+" )
679
682
680
683
// `safeAgentName` returns a safe agent name derived from a folder name,
681
- // falling back to the container’s friendly name if needed.
682
- func safeAgentName (name string ,friendlyName string )string {
684
+ // falling back to the container’s friendly name if needed. The second
685
+ // return value will be `true` if it succeeded and `false` if it had
686
+ // to fallback to the friendly name.
687
+ func safeAgentName (name string ,friendlyName string ) (string ,bool ) {
683
688
// Keep only ASCII letters and digits, replacing everything
684
689
// else with a hyphen.
685
690
var sb strings.Builder
@@ -701,10 +706,10 @@ func safeAgentName(name string, friendlyName string) string {
701
706
name = name [:min (len (name ),maxAgentNameLength )]
702
707
703
708
if provisioner .AgentNameRegex .Match ([]byte (name )) {
704
- return name
709
+ return name , true
705
710
}
706
711
707
- return safeFriendlyName (friendlyName )
712
+ return safeFriendlyName (friendlyName ), false
708
713
}
709
714
710
715
// safeFriendlyName returns a API safe version of the container's
@@ -719,6 +724,47 @@ func safeFriendlyName(name string) string {
719
724
return name
720
725
}
721
726
727
+ // expandedAgentName creates an agent name by including parent directories
728
+ // from the workspace folder path to avoid name collisions. Like `safeAgentName`,
729
+ // the second returned value will be true if using the workspace folder name,
730
+ // and false if it fell back to the friendly name.
731
+ func expandedAgentName (workspaceFolder string ,friendlyName string ,depth int ) (string ,bool ) {
732
+ var parts []string
733
+ for part := range strings .SplitSeq (filepath .ToSlash (workspaceFolder ),"/" ) {
734
+ if part = strings .TrimSpace (part );part != "" {
735
+ parts = append (parts ,part )
736
+ }
737
+ }
738
+ if len (parts )== 0 {
739
+ return safeFriendlyName (friendlyName ),false
740
+ }
741
+
742
+ components := parts [max (0 ,len (parts )- depth - 1 ):]
743
+ expanded := strings .Join (components ,"-" )
744
+
745
+ return safeAgentName (expanded ,friendlyName )
746
+ }
747
+
748
+ // makeAgentName attempts to create an agent name. It will first attempt to create an
749
+ // agent name based off of the workspace folder, and will eventually fallback to a
750
+ // friendly name. Like `safeAgentName`, the second returned value will be true if the
751
+ // agent name utilizes the workspace folder, and false if it falls back to the
752
+ // friendly name.
753
+ func (api * API )makeAgentName (workspaceFolder string ,friendlyName string ) (string ,bool ) {
754
+ for attempt := 0 ;attempt <= maxAttemptsToNameAgent ;attempt ++ {
755
+ agentName ,usingWorkspaceFolder := expandedAgentName (workspaceFolder ,friendlyName ,attempt )
756
+ if ! usingWorkspaceFolder {
757
+ return agentName ,false
758
+ }
759
+
760
+ if ! api .devcontainerNames [agentName ] {
761
+ return agentName ,true
762
+ }
763
+ }
764
+
765
+ return safeFriendlyName (friendlyName ),false
766
+ }
767
+
722
768
// RefreshContainers triggers an immediate update of the container list
723
769
// and waits for it to complete.
724
770
func (api * API )RefreshContainers (ctx context.Context ) (err error ) {
@@ -1234,6 +1280,7 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1234
1280
if provisioner .AgentNameRegex .Match ([]byte (name )) {
1235
1281
subAgentConfig .Name = name
1236
1282
configOutdated = true
1283
+ delete (api .usingWorkspaceFolderName ,dc .WorkspaceFolder )
1237
1284
}else {
1238
1285
logger .Warn (ctx ,"invalid name in devcontainer customization, ignoring" ,
1239
1286
slog .F ("name" ,name ),
@@ -1320,12 +1367,55 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
1320
1367
)
1321
1368
1322
1369
// Create new subagent record in the database to receive the auth token.
1370
+ // If we get a unique constraint violation, try with expanded names that
1371
+ // include parent directories to avoid collisions.
1323
1372
client := * api .subAgentClient .Load ()
1324
- newSubAgent ,err := client .Create (ctx ,subAgentConfig )
1325
- if err != nil {
1326
- return xerrors .Errorf ("create subagent failed: %w" ,err )
1373
+
1374
+ originalName := subAgentConfig .Name
1375
+
1376
+ for attempt := 1 ;attempt <= maxAttemptsToNameAgent ;attempt ++ {
1377
+ if proc .agent ,err = client .Create (ctx ,subAgentConfig );err == nil {
1378
+ if api .usingWorkspaceFolderName [dc .WorkspaceFolder ] {
1379
+ api .devcontainerNames [dc .Name ]= true
1380
+ delete (api .usingWorkspaceFolderName ,dc .WorkspaceFolder )
1381
+ }
1382
+
1383
+ break
1384
+ }
1385
+
1386
+ // NOTE(DanielleMaywood):
1387
+ // Ordinarily we'd use `errors.As` here, but it didn't appear to work. Not
1388
+ // sure if this is because of the communication protocol? Instead I've opted
1389
+ // for a slightly more janky string contains approach.
1390
+ //
1391
+ // We only care if sub agent creation has failed due to a unique constraint
1392
+ // violation on the agent name, as we can _possibly_ rectify this.
1393
+ if ! strings .Contains (err .Error (),"workspace agent name" ) {
1394
+ return xerrors .Errorf ("create subagent failed: %w" ,err )
1395
+ }
1396
+
1397
+ // If there has been a unique constraint violation but the user is *not*
1398
+ // using an auto-generated name, then we should error. This is because
1399
+ // we do not want to surprise the user with a name they did not ask for.
1400
+ if usingFolderName := api .usingWorkspaceFolderName [dc .WorkspaceFolder ];! usingFolderName {
1401
+ return xerrors .Errorf ("create subagent failed: %w" ,err )
1402
+ }
1403
+
1404
+ if attempt == maxAttemptsToNameAgent {
1405
+ return xerrors .Errorf ("create subagent failed after %d attempts: %w" ,attempt ,err )
1406
+ }
1407
+
1408
+ // We increase how much of the workspace folder is used for generating
1409
+ // the agent name. With each iteration there is greater chance of this
1410
+ // being successful.
1411
+ subAgentConfig .Name ,api .usingWorkspaceFolderName [dc .WorkspaceFolder ]= expandedAgentName (dc .WorkspaceFolder ,dc .Container .FriendlyName ,attempt )
1412
+
1413
+ logger .Debug (ctx ,"retrying subagent creation with expanded name" ,
1414
+ slog .F ("original_name" ,originalName ),
1415
+ slog .F ("expanded_name" ,subAgentConfig .Name ),
1416
+ slog .F ("attempt" ,attempt + 1 ),
1417
+ )
1327
1418
}
1328
- proc .agent = newSubAgent
1329
1419
1330
1420
logger .Info (ctx ,"created new subagent" ,slog .F ("agent_id" ,proc .agent .ID ))
1331
1421
}else {