@@ -19,7 +19,6 @@ package container
19
19
import (
20
20
"encoding/json"
21
21
"errors"
22
- "fmt"
23
22
"strings"
24
23
"testing"
25
24
"time"
@@ -32,11 +31,16 @@ import (
32
31
"github.com/containerd/nerdctl/mod/tigron/tig"
33
32
34
33
"github.com/containerd/nerdctl/v2/pkg/healthcheck"
34
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
35
35
"github.com/containerd/nerdctl/v2/pkg/testutil"
36
36
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
37
37
)
38
38
39
39
func TestContainerHealthCheckBasic (t * testing.T ) {
40
+ if rootlessutil .IsRootless () {
41
+ t .Skip ("healthcheck tests are skipped in rootless environment" )
42
+ }
43
+
40
44
testCase := nerdtest .Setup ()
41
45
42
46
// Docker CLI does not provide a standalone healthcheck command.
@@ -134,6 +138,10 @@ func TestContainerHealthCheckBasic(t *testing.T) {
134
138
}
135
139
136
140
func TestContainerHealthCheckAdvance (t * testing.T ) {
141
+ if rootlessutil .IsRootless () {
142
+ t .Skip ("healthcheck tests are skipped in rootless environment" )
143
+ }
144
+
137
145
testCase := nerdtest .Setup ()
138
146
139
147
// Docker CLI does not provide a standalone healthcheck command.
@@ -391,43 +399,6 @@ func TestContainerHealthCheckAdvance(t *testing.T) {
391
399
}
392
400
},
393
401
},
394
- {
395
- Description : "Healthcheck emits large output repeatedly" ,
396
- Setup : func (data test.Data , helpers test.Helpers ) {
397
- helpers .Ensure ("run" , "-d" , "--name" , data .Identifier (),
398
- "--health-cmd" , "yes X | head -c 60000" ,
399
- "--health-interval" , "1s" , "--health-timeout" , "2s" ,
400
- testutil .CommonImage , "sleep" , nerdtest .Infinity )
401
- nerdtest .EnsureContainerStarted (helpers , data .Identifier ())
402
- },
403
- Cleanup : func (data test.Data , helpers test.Helpers ) {
404
- helpers .Anyhow ("rm" , "-f" , data .Identifier ())
405
- },
406
- Command : func (data test.Data , helpers test.Helpers ) test.TestableCommand {
407
- for i := 0 ; i < 3 ; i ++ {
408
- helpers .Ensure ("container" , "healthcheck" , data .Identifier ())
409
- time .Sleep (2 * time .Second )
410
- }
411
- return helpers .Command ("inspect" , data .Identifier ())
412
- },
413
- Expected : func (data test.Data , helpers test.Helpers ) * test.Expected {
414
- return & test.Expected {
415
- ExitCode : 0 ,
416
- Output : expect .All (func (_ string , t tig.T ) {
417
- inspect := nerdtest .InspectContainer (helpers , data .Identifier ())
418
- h := inspect .State .Health
419
- debug , _ := json .MarshalIndent (h , "" , " " )
420
- t .Log (string (debug ))
421
- assert .Assert (t , h != nil , "expected health state" )
422
- assert .Equal (t , h .Status , healthcheck .Healthy )
423
- assert .Assert (t , len (h .Log ) >= 3 , "expected at least 3 health log entries" )
424
- for _ , log := range h .Log {
425
- assert .Assert (t , len (log .Output ) >= 1024 , fmt .Sprintf ("each output should be >= 1024 bytes, was: %s" , log .Output ))
426
- }
427
- }),
428
- }
429
- },
430
- },
431
402
{
432
403
Description : "Health log in inspect keeps only the latest 5 entries" ,
433
404
Setup : func (data test.Data , helpers test.Helpers ) {
@@ -602,3 +573,209 @@ func TestContainerHealthCheckAdvance(t *testing.T) {
602
573
603
574
testCase .Run (t )
604
575
}
576
+
577
+ func TestHealthCheck_SystemdIntegration_Basic (t * testing.T ) {
578
+ testCase := nerdtest .Setup ()
579
+ testCase .Require = require .Not (nerdtest .Docker )
580
+
581
+ testCase .SubTests = []* test.Case {
582
+ {
583
+ Description : "Basic healthy container with systemd-triggered healthcheck" ,
584
+ Setup : func (data test.Data , helpers test.Helpers ) {
585
+ helpers .Ensure ("run" , "-d" , "--name" , data .Identifier (),
586
+ "--health-cmd" , "echo healthy" ,
587
+ "--health-interval" , "2s" ,
588
+ testutil .CommonImage , "sleep" , "30" )
589
+ nerdtest .EnsureContainerStarted (helpers , data .Identifier ())
590
+ // Wait for a healthcheck to execute
591
+ time .Sleep (2 * time .Second )
592
+ },
593
+ Cleanup : func (data test.Data , helpers test.Helpers ) {
594
+ // Ensure proper cleanup of systemd units
595
+ helpers .Anyhow ("stop" , data .Identifier ())
596
+ time .Sleep (500 * time .Millisecond ) // Allow systemd cleanup
597
+ helpers .Anyhow ("rm" , "-f" , data .Identifier ())
598
+ },
599
+ Expected : func (data test.Data , helpers test.Helpers ) * test.Expected {
600
+ return & test.Expected {
601
+ ExitCode : 0 ,
602
+ Output : expect .All (func (stdout string , t tig.T ) {
603
+ inspect := nerdtest .InspectContainer (helpers , data .Identifier ())
604
+ h := inspect .State .Health
605
+ assert .Assert (t , h != nil , "expected health state to be present" )
606
+ assert .Equal (t , h .Status , "healthy" )
607
+ assert .Assert (t , len (h .Log ) > 0 , "expected at least one health check log entry" )
608
+ }),
609
+ }
610
+ },
611
+ },
612
+ {
613
+ Description : "Kill stops healthcheck execution" ,
614
+ Setup : func (data test.Data , helpers test.Helpers ) {
615
+ helpers .Ensure ("run" , "-d" , "--name" , data .Identifier (),
616
+ "--health-cmd" , "echo healthy" ,
617
+ "--health-interval" , "1s" ,
618
+ testutil .CommonImage , "sleep" , "30" )
619
+ nerdtest .EnsureContainerStarted (helpers , data .Identifier ())
620
+ time .Sleep (2 * time .Second ) // Wait for at least one health check to execute
621
+ helpers .Ensure ("kill" , data .Identifier ()) // Kill the container
622
+ time .Sleep (3 * time .Second ) // Wait to allow any potential extra healthchecks (shouldn't happen)
623
+ },
624
+ Cleanup : func (data test.Data , helpers test.Helpers ) {
625
+ // Container is already killed, just remove it
626
+ helpers .Anyhow ("rm" , "-f" , data .Identifier ())
627
+ },
628
+ Expected : func (data test.Data , helpers test.Helpers ) * test.Expected {
629
+ return & test.Expected {
630
+ ExitCode : 0 ,
631
+ Output : expect .All (func (stdout string , t tig.T ) {
632
+ inspect := nerdtest .InspectContainer (helpers , data .Identifier ())
633
+ h := inspect .State .Health
634
+ assert .Assert (t , h != nil , "expected health state to be present" )
635
+ assert .Assert (t , len (h .Log ) > 0 , "expected at least one health check log entry" )
636
+
637
+ // Get container FinishedAt timestamp
638
+ containerEnd , err := time .Parse (time .RFC3339Nano , inspect .State .FinishedAt )
639
+ assert .NilError (t , err , "parsing container FinishedAt" )
640
+
641
+ // Assert all healthcheck log start times are before container finished
642
+ for _ , entry := range h .Log {
643
+ assert .NilError (t , err , "parsing healthcheck Start time" )
644
+ assert .Assert (t , entry .Start .Before (containerEnd ), "healthcheck ran after container was killed" )
645
+ }
646
+ }),
647
+ }
648
+ },
649
+ },
650
+ }
651
+ testCase .Run (t )
652
+ }
653
+
654
+ func TestHealthCheck_SystemdIntegration_Advanced (t * testing.T ) {
655
+ if rootlessutil .IsRootless () {
656
+ t .Skip ("systemd healthcheck tests are skipped in rootless environment" )
657
+ }
658
+ testCase := nerdtest .Setup ()
659
+ testCase .Require = require .Not (nerdtest .Docker )
660
+
661
+ testCase .SubTests = []* test.Case {
662
+ {
663
+ // Tests that CreateTimer() successfully creates systemd timer units and
664
+ // RemoveTransientHealthCheckFiles() properly cleans up units when container stops.
665
+ Description : "Systemd timer unit creation and cleanup" ,
666
+ Setup : func (data test.Data , helpers test.Helpers ) {
667
+ helpers .Ensure ("run" , "-d" , "--name" , data .Identifier (),
668
+ "--health-cmd" , "echo healthy" ,
669
+ "--health-interval" , "1s" ,
670
+ testutil .CommonImage , "sleep" , "30" )
671
+ nerdtest .EnsureContainerStarted (helpers , data .Identifier ())
672
+ // Wait longer for systemd timer creation and first healthcheck execution
673
+ time .Sleep (3 * time .Second )
674
+ },
675
+ Cleanup : func (data test.Data , helpers test.Helpers ) {
676
+ helpers .Anyhow ("rm" , "-f" , data .Identifier ())
677
+ },
678
+ Command : func (data test.Data , helpers test.Helpers ) test.TestableCommand {
679
+ return helpers .Command ("inspect" , data .Identifier ())
680
+ },
681
+ Expected : func (data test.Data , helpers test.Helpers ) * test.Expected {
682
+ return & test.Expected {
683
+ ExitCode : 0 ,
684
+ Output : expect .All (func (stdout string , t tig.T ) {
685
+ // Get container ID and check systemd timer
686
+ containerInspect := nerdtest .InspectContainer (helpers , data .Identifier ())
687
+ containerID := containerInspect .ID
688
+
689
+ // Check systemd timer
690
+ result := helpers .Custom ("systemctl" , "list-timers" , "--all" , "--no-pager" )
691
+ result .Run (& test.Expected {
692
+ ExitCode : expect .ExitCodeNoCheck ,
693
+ Output : func (stdout string , _ tig.T ) {
694
+ // Verify that a timer exists for this specific container
695
+ assert .Assert (t , strings .Contains (stdout , containerID ),
696
+ "expected to find nerdctl healthcheck timer containing container ID: %s" , containerID )
697
+ },
698
+ })
699
+ // Stop container and verify cleanup
700
+ helpers .Ensure ("stop" , data .Identifier ())
701
+ time .Sleep (500 * time .Millisecond ) // Allow cleanup to complete
702
+
703
+ // Check that timer is gone
704
+ result = helpers .Custom ("systemctl" , "list-timers" , "--all" , "--no-pager" )
705
+ result .Run (& test.Expected {
706
+ ExitCode : expect .ExitCodeNoCheck ,
707
+ Output : func (stdout string , _ tig.T ) {
708
+ assert .Assert (t , ! strings .Contains (stdout , containerID ),
709
+ "expected nerdctl healthcheck timer for container ID %s to be removed after container stop" , containerID )
710
+
711
+ },
712
+ })
713
+ }),
714
+ }
715
+ },
716
+ },
717
+ {
718
+ Description : "Container restart recreates systemd timer" ,
719
+ Setup : func (data test.Data , helpers test.Helpers ) {
720
+ helpers .Ensure ("run" , "-d" , "--name" , data .Identifier (),
721
+ "--health-cmd" , "echo restart-test" ,
722
+ "--health-interval" , "2s" ,
723
+ testutil .CommonImage , "sleep" , "60" )
724
+ nerdtest .EnsureContainerStarted (helpers , data .Identifier ())
725
+ time .Sleep (3 * time .Second ) // Wait for initial timer creation
726
+ },
727
+ Cleanup : func (data test.Data , helpers test.Helpers ) {
728
+ helpers .Anyhow ("rm" , "-f" , data .Identifier ())
729
+ },
730
+ Command : func (data test.Data , helpers test.Helpers ) test.TestableCommand {
731
+ // Get container ID for verification
732
+ containerInspect := nerdtest .InspectContainer (helpers , data .Identifier ())
733
+ containerID := containerInspect .ID
734
+
735
+ // Step 1: Verify timer exists initially
736
+ result := helpers .Custom ("systemctl" , "list-timers" , "--all" , "--no-pager" )
737
+ result .Run (& test.Expected {
738
+ ExitCode : expect .ExitCodeNoCheck ,
739
+ Output : func (stdout string , t tig.T ) {
740
+ assert .Assert (t , strings .Contains (stdout , containerID ),
741
+ "expected timer for container %s to exist initially" , containerID )
742
+ },
743
+ })
744
+
745
+ // Step 2: Stop container
746
+ helpers .Ensure ("stop" , data .Identifier ())
747
+ time .Sleep (1 * time .Second ) // Allow cleanup
748
+
749
+ // Step 3: Verify timer is removed after stop
750
+ result = helpers .Custom ("systemctl" , "list-timers" , "--all" , "--no-pager" )
751
+ result .Run (& test.Expected {
752
+ ExitCode : expect .ExitCodeNoCheck ,
753
+ Output : func (stdout string , t tig.T ) {
754
+ assert .Assert (t , ! strings .Contains (stdout , containerID ),
755
+ "expected timer for container %s to be removed after stop" , containerID )
756
+ },
757
+ })
758
+
759
+ // Step 4: Restart container
760
+ helpers .Ensure ("start" , data .Identifier ())
761
+ nerdtest .EnsureContainerStarted (helpers , data .Identifier ())
762
+ time .Sleep (3 * time .Second ) // Wait for timer recreation
763
+
764
+ // Step 5: Verify timer is recreated after restart - this is our final verification
765
+ return helpers .Custom ("systemctl" , "list-timers" , "--all" , "--no-pager" )
766
+ },
767
+ Expected : func (data test.Data , helpers test.Helpers ) * test.Expected {
768
+ return & test.Expected {
769
+ ExitCode : expect .ExitCodeNoCheck ,
770
+ Output : func (stdout string , t tig.T ) {
771
+ containerInspect := nerdtest .InspectContainer (helpers , data .Identifier ())
772
+ containerID := containerInspect .ID
773
+ assert .Assert (t , strings .Contains (stdout , containerID ),
774
+ "expected timer for container %s to be recreated after restart" , containerID )
775
+ },
776
+ }
777
+ },
778
+ },
779
+ }
780
+ testCase .Run (t )
781
+ }
0 commit comments