@@ -19,15 +19,19 @@ package container
1919import (
2020 "errors"
2121 "fmt"
22+ "io"
2223 "os"
2324 "path/filepath"
25+ "strconv"
2426 "strings"
27+ "syscall"
2528 "testing"
2629
2730 "github.com/opencontainers/go-digest"
2831 "gotest.tools/v3/assert"
2932
3033 "github.com/containerd/containerd/v2/defaults"
34+ "github.com/containerd/nerdctl/mod/tigron/expect"
3135 "github.com/containerd/nerdctl/mod/tigron/require"
3236 "github.com/containerd/nerdctl/mod/tigron/test"
3337
@@ -325,3 +329,184 @@ func TestCreateFromOCIArchive(t *testing.T) {
325329 base .Cmd ("create" , "--rm" , "--name" , containerName , fmt .Sprintf ("oci-archive://%s" , tarPath )).AssertOK ()
326330 base .Cmd ("start" , "--attach" , containerName ).AssertOutContains ("test-nerdctl-create-from-oci-archive" )
327331}
332+
333+ func TestUsernsMappingCreateCmd (t * testing.T ) {
334+ nerdtest .Setup ()
335+
336+ testCase := & test.Case {
337+ Require : require .All (
338+ nerdtest .AllowModifyUserns ,
339+ nerdtest .RemapIDs ,
340+ require .Not (nerdtest .Docker )),
341+ NoParallel : true ,
342+ Setup : func (data test.Data , helpers test.Helpers ) {
343+ data .Labels ().Set ("validUserns" , "nerdctltestuser" )
344+ data .Labels ().Set ("expectedHostUID" , "123456789" )
345+ data .Labels ().Set ("invalidUserns" , "invaliduser" )
346+ },
347+ SubTests : []* test.Case {
348+ {
349+ Description : "Test container create with valid Userns" ,
350+ NoParallel : true , // Changes system config so running in non parallel mode
351+ Setup : func (data test.Data , helpers test.Helpers ) {
352+ err := appendUsernsConfig (data .Labels ().Get ("validUserns" ), data .Labels ().Get ("expectedHostUID" ), helpers )
353+ assert .NilError (t , err , "Failed to append Userns config" )
354+ },
355+ Cleanup : func (data test.Data , helpers test.Helpers ) {
356+ removeUsernsConfig (t , data .Labels ().Get ("validUserns" ), helpers )
357+ helpers .Anyhow ("rm" , "-f" , data .Identifier ())
358+ },
359+ Command : func (data test.Data , helpers test.Helpers ) test.TestableCommand {
360+ helpers .Ensure ("create" , "--tty" , "--userns-remap" , data .Labels ().Get ("validUserns" ), "--name" , data .Identifier (), testutil .NginxAlpineImage )
361+ return helpers .Command ("start" , data .Identifier ())
362+ },
363+ Expected : func (data test.Data , helpers test.Helpers ) * test.Expected {
364+ return & test.Expected {
365+ ExitCode : 0 ,
366+ Output : func (stdout string , info string , t * testing.T ) {
367+ actualHostUID , err := getContainerHostUID (helpers , data .Identifier ())
368+ assert .NilError (t , err , "Failed to get container host UID" )
369+ assert .Assert (t , actualHostUID == data .Labels ().Get ("expectedHostUID" ), info )
370+ },
371+ }
372+ },
373+ },
374+ {
375+ Description : "Test container create failure with valid Userns and privileged flag" ,
376+ NoParallel : true , // Changes system config so running in non parallel mode
377+ Setup : func (data test.Data , helpers test.Helpers ) {
378+ err := appendUsernsConfig (data .Labels ().Get ("validUserns" ), data .Labels ().Get ("expectedHostUID" ), helpers )
379+ assert .NilError (t , err , "Failed to append Userns config" )
380+ },
381+ Cleanup : func (data test.Data , helpers test.Helpers ) {
382+ removeUsernsConfig (t , data .Labels ().Get ("validUserns" ), helpers )
383+ },
384+ Command : func (data test.Data , helpers test.Helpers ) test.TestableCommand {
385+ return helpers .Command ("create" , "--tty" , "--privileged" , "--userns-remap" , data .Labels ().Get ("validUserns" ), "--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+ Description : "Test container create with invalid Userns" ,
395+ NoParallel : true , // Changes system config so running in non parallel mode
396+ Cleanup : func (data test.Data , helpers test.Helpers ) {
397+ helpers .Anyhow ("rm" , "-f" , data .Identifier ())
398+ },
399+ Command : func (data test.Data , helpers test.Helpers ) test.TestableCommand {
400+ return helpers .Command ("create" , "--tty" , "--userns-remap" , data .Labels ().Get ("invalidUserns" ), "--name" , data .Identifier (), testutil .NginxAlpineImage )
401+ },
402+ Expected : func (data test.Data , helpers test.Helpers ) * test.Expected {
403+ return & test.Expected {
404+ ExitCode : 1 ,
405+ }
406+ },
407+ },
408+ },
409+ }
410+ testCase .Run (t )
411+ }
412+
413+ func getContainerHostUID (helpers test.Helpers , containerName string ) (string , error ) {
414+ result := helpers .Capture ("inspect" , "--format" , "{{.State.Pid}}" , containerName )
415+ pidStr := strings .TrimSpace (result )
416+ pid , err := strconv .Atoi (pidStr )
417+ if err != nil {
418+ return "" , fmt .Errorf ("invalid PID: %v" , err )
419+ }
420+
421+ stat , err := os .Stat (fmt .Sprintf ("/proc/%d" , pid ))
422+ if err != nil {
423+ return "" , fmt .Errorf ("failed to stat process: %v" , err )
424+ }
425+
426+ uid := int (stat .Sys ().(* syscall.Stat_t ).Uid )
427+ return strconv .Itoa (uid ), nil
428+ }
429+
430+ func appendUsernsConfig (userns string , hostUID string , helpers test.Helpers ) error {
431+ addUser (userns , hostUID , helpers )
432+ entry := fmt .Sprintf ("%s:%s:65536\n " , userns , hostUID )
433+ tempDir := helpers .T ().TempDir ()
434+ files := []string {"subuid" , "subgid" }
435+ for _ , file := range files {
436+
437+ fileBak := filepath .Join (tempDir , file )
438+ defer os .Remove (fileBak )
439+ d , err := os .Create (fileBak )
440+ if err != nil {
441+ return fmt .Errorf ("failed to create %s: %w" , fileBak , err )
442+ }
443+
444+ s , err := os .Open (filepath .Join ("/etc" , file ))
445+ if err != nil {
446+ return fmt .Errorf ("failed to open %s: %w" , file , err )
447+ }
448+ defer s .Close ()
449+
450+ _ , err = io .Copy (d , s )
451+ if err != nil {
452+ return fmt .Errorf ("failed to copy %s to %s: %w" , file , fileBak , err )
453+ }
454+
455+ f , err := os .OpenFile (fmt .Sprintf ("/etc/%s" , file ), os .O_APPEND | os .O_WRONLY , 0644 )
456+ if err != nil {
457+ return fmt .Errorf ("failed to open %s: %w" , file , err )
458+ }
459+ defer f .Close ()
460+
461+ if _ , err := f .WriteString (entry ); err != nil {
462+ return fmt .Errorf ("failed to write to %s: %w" , file , err )
463+ }
464+ }
465+ return nil
466+ }
467+
468+ func addUser (username string , hostID string , helpers test.Helpers ) {
469+ helpers .Custom ("groupadd" , "-g" , hostID , username ).Run (& test.Expected {
470+ ExitCode : 0 })
471+ helpers .Custom ("useradd" , "-u" , hostID , "-g" , hostID , "-s" , "/bin/false" , username ).Run (& test.Expected {
472+ ExitCode : 0 })
473+ }
474+
475+ func removeUsernsConfig (t * testing.T , userns string , helpers test.Helpers ) {
476+ delUser (userns , helpers )
477+ delGroup (userns , helpers )
478+ tempDir := helpers .T ().TempDir ()
479+ files := []string {"subuid" , "subgid" }
480+ for _ , file := range files {
481+ fileBak := filepath .Join (tempDir , file )
482+ s , err := os .Open (fileBak )
483+ if err != nil {
484+ t .Logf ("failed to open %s, Error: %s" , fileBak , err )
485+ continue
486+ }
487+ defer s .Close ()
488+
489+ d , err := os .Open (filepath .Join ("/etc/%s" , file ))
490+ if err != nil {
491+ t .Logf ("failed to open %s, Error: %s" , file , err )
492+ continue
493+
494+ }
495+ defer d .Close ()
496+
497+ _ , err = io .Copy (d , s )
498+ if err != nil {
499+ t .Logf ("failed to restore. Copy %s to %s failed, Error %s" , fileBak , file , err )
500+ continue
501+ }
502+
503+ }
504+ }
505+
506+ func delUser (username string , helpers test.Helpers ) {
507+ helpers .Custom ("userdel" , username ).Run (& test.Expected {ExitCode : expect .ExitCodeNoCheck })
508+ }
509+
510+ func delGroup (groupname string , helpers test.Helpers ) {
511+ helpers .Custom ("groupdel" , groupname ).Run (& test.Expected {ExitCode : expect .ExitCodeNoCheck })
512+ }
0 commit comments