@@ -20,6 +20,7 @@ import (
20
20
"github.com/go-chi/chi/v5"
21
21
"github.com/google/uuid"
22
22
"github.com/lib/pq"
23
+ "github.com/spf13/afero"
23
24
"github.com/stretchr/testify/assert"
24
25
"github.com/stretchr/testify/require"
25
26
"go.uber.org/mock/gomock"
@@ -3189,3 +3190,234 @@ func TestWithDevcontainersNameGeneration(t *testing.T) {
3189
3190
assert .Equal (t ,"bar-project" ,response .Devcontainers [0 ].Name ,"second devcontainer should has a collision and uses the folder name with a prefix" )
3190
3191
assert .Equal (t ,"baz-project" ,response .Devcontainers [1 ].Name ,"third devcontainer should use the folder name with a prefix since it collides with the first two" )
3191
3192
}
3193
+
3194
+ func TestDevcontainerDiscovery (t * testing.T ) {
3195
+ t .Parallel ()
3196
+
3197
+ // We discover dev container projects by searching
3198
+ // for git repositories at the agent's directory,
3199
+ // and then recursively walking through these git
3200
+ // repositories to find any `.devcontainer/devcontainer.json`
3201
+ // files. These tests are to validate that behavior.
3202
+
3203
+ tests := []struct {
3204
+ name string
3205
+ agentDir string
3206
+ fs map [string ]string
3207
+ expected []codersdk.WorkspaceAgentDevcontainer
3208
+ }{
3209
+ {
3210
+ name :"GitProjectInRootDir/SingleProject" ,
3211
+ agentDir :"/home/coder" ,
3212
+ fs :map [string ]string {
3213
+ "/home/coder/.git/HEAD" :"" ,
3214
+ "/home/coder/.devcontainer/devcontainer.json" :"" ,
3215
+ },
3216
+ expected : []codersdk.WorkspaceAgentDevcontainer {
3217
+ {
3218
+ WorkspaceFolder :"/home/coder" ,
3219
+ ConfigPath :"/home/coder/.devcontainer/devcontainer.json" ,
3220
+ Status :codersdk .WorkspaceAgentDevcontainerStatusStopped ,
3221
+ },
3222
+ },
3223
+ },
3224
+ {
3225
+ name :"GitProjectInRootDir/MultipleProjects" ,
3226
+ agentDir :"/home/coder" ,
3227
+ fs :map [string ]string {
3228
+ "/home/coder/.git/HEAD" :"" ,
3229
+ "/home/coder/.devcontainer/devcontainer.json" :"" ,
3230
+ "/home/coder/site/.devcontainer/devcontainer.json" :"" ,
3231
+ },
3232
+ expected : []codersdk.WorkspaceAgentDevcontainer {
3233
+ {
3234
+ WorkspaceFolder :"/home/coder" ,
3235
+ ConfigPath :"/home/coder/.devcontainer/devcontainer.json" ,
3236
+ Status :codersdk .WorkspaceAgentDevcontainerStatusStopped ,
3237
+ },
3238
+ {
3239
+ WorkspaceFolder :"/home/coder/site" ,
3240
+ ConfigPath :"/home/coder/site/.devcontainer/devcontainer.json" ,
3241
+ Status :codersdk .WorkspaceAgentDevcontainerStatusStopped ,
3242
+ },
3243
+ },
3244
+ },
3245
+ {
3246
+ name :"GitProjectInChildDir/SingleProject" ,
3247
+ agentDir :"/home/coder" ,
3248
+ fs :map [string ]string {
3249
+ "/home/coder/coder/.git/HEAD" :"" ,
3250
+ "/home/coder/coder/.devcontainer/devcontainer.json" :"" ,
3251
+ },
3252
+ expected : []codersdk.WorkspaceAgentDevcontainer {
3253
+ {
3254
+ WorkspaceFolder :"/home/coder/coder" ,
3255
+ ConfigPath :"/home/coder/coder/.devcontainer/devcontainer.json" ,
3256
+ Status :codersdk .WorkspaceAgentDevcontainerStatusStopped ,
3257
+ },
3258
+ },
3259
+ },
3260
+ {
3261
+ name :"GitProjectInChildDir/MultipleProjects" ,
3262
+ agentDir :"/home/coder" ,
3263
+ fs :map [string ]string {
3264
+ "/home/coder/coder/.git/HEAD" :"" ,
3265
+ "/home/coder/coder/.devcontainer/devcontainer.json" :"" ,
3266
+ "/home/coder/coder/site/.devcontainer/devcontainer.json" :"" ,
3267
+ },
3268
+ expected : []codersdk.WorkspaceAgentDevcontainer {
3269
+ {
3270
+ WorkspaceFolder :"/home/coder/coder" ,
3271
+ ConfigPath :"/home/coder/coder/.devcontainer/devcontainer.json" ,
3272
+ Status :codersdk .WorkspaceAgentDevcontainerStatusStopped ,
3273
+ },
3274
+ {
3275
+ WorkspaceFolder :"/home/coder/coder/site" ,
3276
+ ConfigPath :"/home/coder/coder/site/.devcontainer/devcontainer.json" ,
3277
+ Status :codersdk .WorkspaceAgentDevcontainerStatusStopped ,
3278
+ },
3279
+ },
3280
+ },
3281
+ {
3282
+ name :"GitProjectInMultipleChildDirs/SingleProjectEach" ,
3283
+ agentDir :"/home/coder" ,
3284
+ fs :map [string ]string {
3285
+ "/home/coder/coder/.git/HEAD" :"" ,
3286
+ "/home/coder/coder/.devcontainer/devcontainer.json" :"" ,
3287
+ "/home/coder/envbuilder/.git/HEAD" :"" ,
3288
+ "/home/coder/envbuilder/.devcontainer/devcontainer.json" :"" ,
3289
+ },
3290
+ expected : []codersdk.WorkspaceAgentDevcontainer {
3291
+ {
3292
+ WorkspaceFolder :"/home/coder/coder" ,
3293
+ ConfigPath :"/home/coder/coder/.devcontainer/devcontainer.json" ,
3294
+ Status :codersdk .WorkspaceAgentDevcontainerStatusStopped ,
3295
+ },
3296
+ {
3297
+ WorkspaceFolder :"/home/coder/envbuilder" ,
3298
+ ConfigPath :"/home/coder/envbuilder/.devcontainer/devcontainer.json" ,
3299
+ Status :codersdk .WorkspaceAgentDevcontainerStatusStopped ,
3300
+ },
3301
+ },
3302
+ },
3303
+ {
3304
+ name :"GitProjectInMultipleChildDirs/MultipleProjectEach" ,
3305
+ agentDir :"/home/coder" ,
3306
+ fs :map [string ]string {
3307
+ "/home/coder/coder/.git/HEAD" :"" ,
3308
+ "/home/coder/coder/.devcontainer/devcontainer.json" :"" ,
3309
+ "/home/coder/coder/site/.devcontainer/devcontainer.json" :"" ,
3310
+ "/home/coder/envbuilder/.git/HEAD" :"" ,
3311
+ "/home/coder/envbuilder/.devcontainer/devcontainer.json" :"" ,
3312
+ "/home/coder/envbuilder/x/.devcontainer/devcontainer.json" :"" ,
3313
+ },
3314
+ expected : []codersdk.WorkspaceAgentDevcontainer {
3315
+ {
3316
+ WorkspaceFolder :"/home/coder/coder" ,
3317
+ ConfigPath :"/home/coder/coder/.devcontainer/devcontainer.json" ,
3318
+ Status :codersdk .WorkspaceAgentDevcontainerStatusStopped ,
3319
+ },
3320
+ {
3321
+ WorkspaceFolder :"/home/coder/coder/site" ,
3322
+ ConfigPath :"/home/coder/coder/site/.devcontainer/devcontainer.json" ,
3323
+ Status :codersdk .WorkspaceAgentDevcontainerStatusStopped ,
3324
+ },
3325
+ {
3326
+ WorkspaceFolder :"/home/coder/envbuilder" ,
3327
+ ConfigPath :"/home/coder/envbuilder/.devcontainer/devcontainer.json" ,
3328
+ Status :codersdk .WorkspaceAgentDevcontainerStatusStopped ,
3329
+ },
3330
+ {
3331
+ WorkspaceFolder :"/home/coder/envbuilder/x" ,
3332
+ ConfigPath :"/home/coder/envbuilder/x/.devcontainer/devcontainer.json" ,
3333
+ Status :codersdk .WorkspaceAgentDevcontainerStatusStopped ,
3334
+ },
3335
+ },
3336
+ },
3337
+ }
3338
+
3339
+ initFS := func (t * testing.T ,files map [string ]string ) afero.Fs {
3340
+ t .Helper ()
3341
+
3342
+ fs := afero .NewMemMapFs ()
3343
+ for name ,content := range files {
3344
+ err := afero .WriteFile (fs ,name , []byte (content + "\n " ),0o600 )
3345
+ require .NoError (t ,err )
3346
+ }
3347
+ return fs
3348
+ }
3349
+
3350
+ for _ ,tt := range tests {
3351
+ t .Run (tt .name ,func (t * testing.T ) {
3352
+ t .Parallel ()
3353
+
3354
+ var (
3355
+ ctx = testutil .Context (t ,testutil .WaitShort )
3356
+ logger = testutil .Logger (t )
3357
+ mClock = quartz .NewMock (t )
3358
+ tickerTrap = mClock .Trap ().TickerFunc ("updaterLoop" )
3359
+
3360
+ r = chi .NewRouter ()
3361
+ )
3362
+
3363
+ api := agentcontainers .NewAPI (logger ,
3364
+ agentcontainers .WithClock (mClock ),
3365
+ agentcontainers .WithWatcher (watcher .NewNoop ()),
3366
+ agentcontainers .WithFileSystem (initFS (t ,tt .fs )),
3367
+ agentcontainers .WithManifestInfo ("owner" ,"workspace" ,"parent-agent" ,tt .agentDir ),
3368
+ agentcontainers .WithContainerCLI (& fakeContainerCLI {}),
3369
+ agentcontainers .WithDevcontainerCLI (& fakeDevcontainerCLI {}),
3370
+ )
3371
+ api .Start ()
3372
+ defer api .Close ()
3373
+ r .Mount ("/" ,api .Routes ())
3374
+
3375
+ tickerTrap .MustWait (ctx ).MustRelease (ctx )
3376
+ tickerTrap .Close ()
3377
+
3378
+ // Wait until all projects have been discovered
3379
+ require .Eventuallyf (t ,func ()bool {
3380
+ req := httptest .NewRequest (http .MethodGet ,"/" ,nil ).WithContext (ctx )
3381
+ rec := httptest .NewRecorder ()
3382
+ r .ServeHTTP (rec ,req )
3383
+
3384
+ got := codersdk.WorkspaceAgentListContainersResponse {}
3385
+ err := json .NewDecoder (rec .Body ).Decode (& got )
3386
+ require .NoError (t ,err )
3387
+
3388
+ return len (got .Devcontainers )== len (tt .expected )
3389
+ },testutil .WaitShort ,testutil .IntervalFast ,"dev containers never found" )
3390
+
3391
+ // Now projects have been discovered, we'll allow the updater loop
3392
+ // to set the appropriate status for these containers.
3393
+ _ ,aw := mClock .AdvanceNext ()
3394
+ aw .MustWait (ctx )
3395
+
3396
+ // Now we'll fetch the list of dev containers
3397
+ req := httptest .NewRequest (http .MethodGet ,"/" ,nil ).WithContext (ctx )
3398
+ rec := httptest .NewRecorder ()
3399
+ r .ServeHTTP (rec ,req )
3400
+
3401
+ got := codersdk.WorkspaceAgentListContainersResponse {}
3402
+ err := json .NewDecoder (rec .Body ).Decode (& got )
3403
+ require .NoError (t ,err )
3404
+
3405
+ // We will set the IDs of each dev container to uuid.Nil to simplify
3406
+ // this check.
3407
+ for idx := range got .Devcontainers {
3408
+ got .Devcontainers [idx ].ID = uuid .Nil
3409
+ }
3410
+
3411
+ // Sort the expected dev containers and got dev containers by their workspace folder.
3412
+ // This helps ensure a deterministic test.
3413
+ slices .SortFunc (tt .expected ,func (a ,b codersdk.WorkspaceAgentDevcontainer )int {
3414
+ return strings .Compare (a .WorkspaceFolder ,b .WorkspaceFolder )
3415
+ })
3416
+ slices .SortFunc (got .Devcontainers ,func (a ,b codersdk.WorkspaceAgentDevcontainer )int {
3417
+ return strings .Compare (a .WorkspaceFolder ,b .WorkspaceFolder )
3418
+ })
3419
+
3420
+ require .Equal (t ,tt .expected ,got .Devcontainers )
3421
+ })
3422
+ }
3423
+ }