@@ -19,13 +19,18 @@ package container
19
19
import (
20
20
"errors"
21
21
"fmt"
22
+ "io"
22
23
"os"
24
+ "os/exec"
23
25
"path/filepath"
26
+ "strconv"
24
27
"strings"
28
+ "syscall"
25
29
"testing"
26
30
27
31
"github.com/opencontainers/go-digest"
28
32
"gotest.tools/v3/assert"
33
+ "gotest.tools/v3/icmd"
29
34
30
35
"github.com/containerd/containerd/v2/defaults"
31
36
"github.com/containerd/nerdctl/mod/tigron/require"
@@ -325,3 +330,210 @@ func TestCreateFromOCIArchive(t *testing.T) {
325
330
base .Cmd ("create" , "--rm" , "--name" , containerName , fmt .Sprintf ("oci-archive://%s" , tarPath )).AssertOK ()
326
331
base .Cmd ("start" , "--attach" , containerName ).AssertOutContains ("test-nerdctl-create-from-oci-archive" )
327
332
}
333
+
334
+ func TestUsernsMappingCreateCmd (t * testing.T ) {
335
+ nerdtest .Setup ()
336
+
337
+ testCase := & test.Case {
338
+ Require : require .All (
339
+ nerdtest .AllowModifyUserns ,
340
+ require .Not (nerdtest .ContainerdV1 ),
341
+ require .Not (nerdtest .Docker )),
342
+ SubTests : []* test.Case {
343
+ {
344
+ Description : "Test container start with valid Userns" ,
345
+ NoParallel : true , // Changes system config so running in non parallel mode
346
+ Setup : func (data test.Data , helpers test.Helpers ) {
347
+ data .Set ("validUserns" , "nerdctltestuser" )
348
+ data .Set ("expectedHostUID" , "123456789" )
349
+ // need to be compiled with containerd version >2.0.2 to support multi uidmap and gidmap.
350
+ if err := appendUsernsConfig (data .Get ("validUserns" ), data .Get ("expectedHostUID" )); err != nil {
351
+ t .Fatalf ("Failed to append Userns config: %v" , err )
352
+ }
353
+ },
354
+ Cleanup : func (data test.Data , helpers test.Helpers ) {
355
+ removeUsernsConfig (t , data .Get ("validUserns" ), data .Get ("expectedHostUID" ))
356
+ helpers .Anyhow ("rm" , "-f" , data .Identifier ())
357
+ },
358
+ Command : func (data test.Data , helpers test.Helpers ) test.TestableCommand {
359
+ helpers .Ensure ("create" , "--tty" , "--userns" , data .Get ("validUserns" ), "--name" , data .Identifier (), testutil .NginxAlpineImage )
360
+ return helpers .Command ("start" , data .Identifier ())
361
+ },
362
+ Expected : func (data test.Data , helpers test.Helpers ) * test.Expected {
363
+ return & test.Expected {
364
+ ExitCode : 0 ,
365
+ Output : func (stdout string , info string , t * testing.T ) {
366
+ actualHostUID , err := getContainerHostUID (helpers , data .Identifier ())
367
+ if err != nil {
368
+ t .Fatalf ("Failed to get container host UID: %v" , err )
369
+ }
370
+ assert .Assert (t , actualHostUID == data .Get ("expectedHostUID" ), info )
371
+ },
372
+ }
373
+ },
374
+ },
375
+ {
376
+ Description : "Test container start with invalid Userns" ,
377
+ NoParallel : true , // Changes system config so running in non parallel mode
378
+ Setup : func (data test.Data , helpers test.Helpers ) {
379
+ data .Set ("invalidUserns" , "invaliduser" )
380
+ },
381
+ Cleanup : func (data test.Data , helpers test.Helpers ) {
382
+ helpers .Anyhow ("rm" , "-f" , data .Identifier ())
383
+ },
384
+ Command : func (data test.Data , helpers test.Helpers ) test.TestableCommand {
385
+ return helpers .Command ("create" , "--tty" , "--userns" , data .Get ("invalidUserns" ), "--name" , data .Identifier (), testutil .NginxAlpineImage )
386
+ },
387
+ Expected : func (data test.Data , helpers test.Helpers ) * test.Expected {
388
+ return & test.Expected {
389
+ ExitCode : 1 ,
390
+ }
391
+ },
392
+ },
393
+ },
394
+ }
395
+ testCase .Run (t )
396
+ }
397
+
398
+ func runUsernsContainer (t * testing.T , name , Userns , image , cmd string ) * icmd.Result {
399
+ base := testutil .NewBase (t )
400
+ removeContainerArgs := []string {
401
+ "rm" , "-f" , name ,
402
+ }
403
+ base .Cmd (removeContainerArgs ... ).Run ()
404
+
405
+ args := []string {
406
+ "run" , "-d" , "--userns" , Userns , "--name" , name , image , "sh" , "-c" , cmd ,
407
+ }
408
+ return base .Cmd (args ... ).Run ()
409
+ }
410
+
411
+ func getContainerHostUID (helpers test.Helpers , containerName string ) (string , error ) {
412
+ result := helpers .Capture ("inspect" , "--format" , "{{.State.Pid}}" , containerName )
413
+ pidStr := strings .TrimSpace (result )
414
+ pid , err := strconv .Atoi (pidStr )
415
+ if err != nil {
416
+ return "" , fmt .Errorf ("invalid PID: %v" , err )
417
+ }
418
+
419
+ stat , err := os .Stat (fmt .Sprintf ("/proc/%d" , pid ))
420
+ if err != nil {
421
+ return "" , fmt .Errorf ("failed to stat process: %v" , err )
422
+ }
423
+
424
+ uid := int (stat .Sys ().(* syscall.Stat_t ).Uid )
425
+ return strconv .Itoa (uid ), nil
426
+ }
427
+
428
+ func appendUsernsConfig (Userns string , hostUid string ) error {
429
+ if err := addUser (Userns , hostUid ); err != nil {
430
+ return fmt .Errorf ("failed to add user %s: %w" , Userns , err )
431
+ }
432
+
433
+ entry := fmt .Sprintf ("%s:%s:65536\n " , Userns , hostUid )
434
+
435
+ tempDir := os .TempDir ()
436
+
437
+ files := []string {"subuid" , "subgid" }
438
+ for _ , file := range files {
439
+
440
+ fileBak := fmt .Sprintf ("%s/%s.bak" , tempDir , file )
441
+ defer os .Remove (fileBak )
442
+ d , err := os .Create (fileBak )
443
+ if err != nil {
444
+ return fmt .Errorf ("failed to create %s: %w" , fileBak , err )
445
+ }
446
+
447
+ s , err := os .Open (fmt .Sprintf ("/etc/%s" , file ))
448
+ if err != nil {
449
+ return fmt .Errorf ("failed to open %s: %w" , file , err )
450
+ }
451
+ defer s .Close ()
452
+
453
+ _ , err = io .Copy (d , s )
454
+ if err != nil {
455
+ return fmt .Errorf ("failed to copy %s to %s: %w" , file , fileBak , err )
456
+ }
457
+
458
+ f , err := os .OpenFile (fmt .Sprintf ("/etc/%s" , file ), os .O_APPEND | os .O_WRONLY , 0644 )
459
+ if err != nil {
460
+ return fmt .Errorf ("failed to open %s: %w" , file , err )
461
+ }
462
+ defer f .Close ()
463
+
464
+ if _ , err := f .WriteString (entry ); err != nil {
465
+ return fmt .Errorf ("failed to write to %s: %w" , file , err )
466
+ }
467
+ }
468
+ return nil
469
+ }
470
+
471
+ func addUser (username string , hostId string ) error {
472
+ cmd := exec .Command ("groupadd" , "-g" , hostId , username )
473
+ output , err := cmd .CombinedOutput ()
474
+ if err != nil {
475
+ return fmt .Errorf ("groupadd failed: %s, %w" , string (output ), err )
476
+ }
477
+ cmd = exec .Command ("useradd" , "-u" , hostId , "-g" , hostId , "-s" , "/bin/false" , username )
478
+ output , err = cmd .CombinedOutput ()
479
+ if err != nil {
480
+ return fmt .Errorf ("useradd failed: %s, %w" , string (output ), err )
481
+ }
482
+ return nil
483
+ }
484
+
485
+ func removeUsernsConfig (t * testing.T , Userns string , hostUid string ) {
486
+
487
+ if err := delUser (Userns ); err != nil {
488
+ t .Logf ("failed to del user %s, Error: %s" , Userns , err )
489
+ }
490
+
491
+ if err := delGroup (Userns ); err != nil {
492
+ t .Logf ("failed to del group %s, Error: %s" , Userns , err )
493
+ }
494
+
495
+ tempDir := os .TempDir ()
496
+ files := []string {"subuid" , "subgid" }
497
+ for _ , file := range files {
498
+ fileBak := fmt .Sprintf ("%s/%s.bak" , tempDir , file )
499
+ s , err := os .Open (fileBak )
500
+ if err != nil {
501
+ t .Logf ("failed to open %s, Error: %s" , fileBak , err )
502
+ continue
503
+ }
504
+ defer s .Close ()
505
+
506
+ d , err := os .Open (fmt .Sprintf ("/etc/%s" , file ))
507
+ if err != nil {
508
+ t .Logf ("failed to open %s, Error: %s" , file , err )
509
+ continue
510
+
511
+ }
512
+ defer d .Close ()
513
+
514
+ _ , err = io .Copy (d , s )
515
+ if err != nil {
516
+ t .Logf ("failed to restore. Copy %s to %s failed, Error %s" , fileBak , file , err )
517
+ continue
518
+ }
519
+
520
+ }
521
+ }
522
+
523
+ func delUser (username string ) error {
524
+ cmd := exec .Command ("sudo" , "userdel" , username )
525
+ output , err := cmd .CombinedOutput ()
526
+ if err != nil {
527
+ return fmt .Errorf ("userdel failed: %s, %w" , string (output ), err )
528
+ }
529
+ return nil
530
+ }
531
+
532
+ func delGroup (groupname string ) error {
533
+ cmd := exec .Command ("sudo" , "groupdel" , groupname )
534
+ output , err := cmd .CombinedOutput ()
535
+ if err != nil {
536
+ return fmt .Errorf ("groupdel failed: %s, %w" , string (output ), err )
537
+ }
538
+ return nil
539
+ }
0 commit comments