github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/libcontainer/cgroups/systemd/freeze_test.go (about)

     1  package systemd
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"os"
     7  	"os/exec"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/opencontainers/runc/libcontainer/cgroups"
    12  	"github.com/opencontainers/runc/libcontainer/configs"
    13  	"golang.org/x/sys/unix"
    14  )
    15  
    16  func TestFreezeBeforeSet(t *testing.T) {
    17  	requireV1(t)
    18  
    19  	testCases := []struct {
    20  		desc string
    21  		// Test input.
    22  		cg        *configs.Cgroup
    23  		preFreeze bool
    24  		// Expected values.
    25  		// Before unit creation (Apply).
    26  		freeze0, thaw0 bool
    27  		// After unit creation.
    28  		freeze1, thaw1 bool
    29  	}{
    30  		{
    31  			// A slice with SkipDevices.
    32  			desc: "slice,skip-devices",
    33  			cg: &configs.Cgroup{
    34  				Name:   "system-runc_test_freeze_1.slice",
    35  				Parent: "system.slice",
    36  				Resources: &configs.Resources{
    37  					SkipDevices: true,
    38  				},
    39  			},
    40  			// Expected.
    41  			freeze0: false,
    42  			thaw0:   false,
    43  			freeze1: false,
    44  			thaw1:   false,
    45  		},
    46  		{
    47  			// A scope with SkipDevices. Not a realistic scenario with runc
    48  			// (as container can't have SkipDevices == true), but possible
    49  			// for a standalone cgroup manager.
    50  			desc: "scope,skip-devices",
    51  			cg: &configs.Cgroup{
    52  				ScopePrefix: "test",
    53  				Name:        "testFreeze2",
    54  				Parent:      "system.slice",
    55  				Resources: &configs.Resources{
    56  					SkipDevices: true,
    57  				},
    58  			},
    59  			// Expected.
    60  			freeze0: false,
    61  			thaw0:   false,
    62  			freeze1: false,
    63  			thaw1:   false,
    64  		},
    65  		{
    66  			// A slice that is about to be frozen in Set.
    67  			desc: "slice,will-freeze",
    68  			cg: &configs.Cgroup{
    69  				Name:   "system-runc_test_freeze_3.slice",
    70  				Parent: "system.slice",
    71  				Resources: &configs.Resources{
    72  					Freezer: configs.Frozen,
    73  				},
    74  			},
    75  			// Expected.
    76  			freeze0: true,
    77  			thaw0:   false,
    78  			freeze1: true,
    79  			thaw1:   false,
    80  		},
    81  		{
    82  			// A pre-frozen slice that should stay frozen.
    83  			desc: "slice,pre-frozen,will-freeze",
    84  			cg: &configs.Cgroup{
    85  				Name:   "system-runc_test_freeze_4.slice",
    86  				Parent: "system.slice",
    87  				Resources: &configs.Resources{
    88  					Freezer: configs.Frozen,
    89  				},
    90  			},
    91  			preFreeze: true,
    92  			// Expected.
    93  			freeze0: true, // not actually frozen yet.
    94  			thaw0:   false,
    95  			freeze1: false,
    96  			thaw1:   false,
    97  		},
    98  		{
    99  			// A pre-frozen scope with skip devices set.
   100  			desc: "scope,pre-frozen,skip-devices",
   101  			cg: &configs.Cgroup{
   102  				ScopePrefix: "test",
   103  				Name:        "testFreeze5",
   104  				Parent:      "system.slice",
   105  				Resources: &configs.Resources{
   106  					SkipDevices: true,
   107  				},
   108  			},
   109  			preFreeze: true,
   110  			// Expected.
   111  			freeze0: false,
   112  			thaw0:   false,
   113  			freeze1: false,
   114  			thaw1:   false,
   115  		},
   116  		{
   117  			// A pre-frozen scope which will be thawed.
   118  			desc: "scope,pre-frozen",
   119  			cg: &configs.Cgroup{
   120  				ScopePrefix: "test",
   121  				Name:        "testFreeze6",
   122  				Parent:      "system.slice",
   123  				Resources:   &configs.Resources{},
   124  			},
   125  			preFreeze: true,
   126  			// Expected.
   127  			freeze0: true, // not actually frozen yet.
   128  			thaw0:   true,
   129  			freeze1: false,
   130  			thaw1:   false,
   131  		},
   132  	}
   133  
   134  	for _, tc := range testCases {
   135  		tc := tc
   136  		t.Run(tc.desc, func(t *testing.T) {
   137  			m, err := NewLegacyManager(tc.cg, nil)
   138  			if err != nil {
   139  				t.Fatal(err)
   140  			}
   141  			defer m.Destroy() //nolint:errcheck
   142  
   143  			// Checks for a non-existent unit.
   144  			freeze, thaw, err := m.freezeBeforeSet(getUnitName(tc.cg), tc.cg.Resources)
   145  			if err != nil {
   146  				t.Fatal(err)
   147  			}
   148  			if freeze != tc.freeze0 || thaw != tc.thaw0 {
   149  				t.Errorf("before Apply (non-existent unit): expected freeze: %v, thaw: %v, got freeze: %v, thaw: %v",
   150  					tc.freeze0, tc.thaw0, freeze, thaw)
   151  			}
   152  
   153  			// Create systemd unit.
   154  			pid := -1
   155  			if strings.HasSuffix(getUnitName(tc.cg), ".scope") {
   156  				// Scopes require a process inside.
   157  				cmd := exec.Command("bash", "-c", "sleep 1m")
   158  				if err := cmd.Start(); err != nil {
   159  					t.Fatal(err)
   160  				}
   161  				pid = cmd.Process.Pid
   162  				// Make sure to not leave a zombie.
   163  				defer func() {
   164  					// These may fail, we don't care.
   165  					_ = cmd.Process.Kill()
   166  					_ = cmd.Wait()
   167  				}()
   168  			}
   169  			if err := m.Apply(pid); err != nil {
   170  				t.Fatal(err)
   171  			}
   172  			if tc.preFreeze {
   173  				if err := m.Freeze(configs.Frozen); err != nil {
   174  					t.Error(err)
   175  					return // no more checks
   176  				}
   177  			}
   178  			freeze, thaw, err = m.freezeBeforeSet(getUnitName(tc.cg), tc.cg.Resources)
   179  			if err != nil {
   180  				t.Error(err)
   181  				return // no more checks
   182  			}
   183  			if freeze != tc.freeze1 || thaw != tc.thaw1 {
   184  				t.Errorf("expected freeze: %v, thaw: %v, got freeze: %v, thaw: %v",
   185  					tc.freeze1, tc.thaw1, freeze, thaw)
   186  			}
   187  			// Destroy() timeouts on a frozen container, so we need to thaw it.
   188  			if tc.preFreeze {
   189  				if err := m.Freeze(configs.Thawed); err != nil {
   190  					t.Error(err)
   191  				}
   192  			}
   193  			// Destroy() does not kill processes in cgroup, so we should.
   194  			if pid != -1 {
   195  				if err = unix.Kill(pid, unix.SIGKILL); err != nil {
   196  					t.Errorf("unable to kill pid %d: %s", pid, err)
   197  				}
   198  			}
   199  			// Not really needed, but may help catch some bugs.
   200  			if err := m.Destroy(); err != nil {
   201  				t.Errorf("destroy: %s", err)
   202  			}
   203  		})
   204  	}
   205  }
   206  
   207  // requireV1 skips the test unless a set of requirements (cgroup v1,
   208  // systemd, root) is met.
   209  func requireV1(t *testing.T) {
   210  	t.Helper()
   211  	if cgroups.IsCgroup2UnifiedMode() {
   212  		t.Skip("Test requires cgroup v1.")
   213  	}
   214  	if !IsRunningSystemd() {
   215  		t.Skip("Test requires systemd.")
   216  	}
   217  	if os.Geteuid() != 0 {
   218  		t.Skip("Test requires root.")
   219  	}
   220  }
   221  
   222  func TestFreezePodCgroup(t *testing.T) {
   223  	if !IsRunningSystemd() {
   224  		t.Skip("Test requires systemd.")
   225  	}
   226  	if os.Geteuid() != 0 {
   227  		t.Skip("Test requires root.")
   228  	}
   229  
   230  	podConfig := &configs.Cgroup{
   231  		Parent: "system.slice",
   232  		Name:   "system-runc_test_pod.slice",
   233  		Resources: &configs.Resources{
   234  			SkipDevices: true,
   235  			Freezer:     configs.Frozen,
   236  		},
   237  	}
   238  	// Create a "pod" cgroup (a systemd slice to hold containers),
   239  	// which is frozen initially.
   240  	pm := newManager(t, podConfig)
   241  	if err := pm.Apply(-1); err != nil {
   242  		t.Fatal(err)
   243  	}
   244  
   245  	if err := pm.Set(podConfig.Resources); err != nil {
   246  		t.Fatal(err)
   247  	}
   248  
   249  	// Check the pod is frozen.
   250  	pf, err := pm.GetFreezerState()
   251  	if err != nil {
   252  		t.Fatal(err)
   253  	}
   254  	if pf != configs.Frozen {
   255  		t.Fatalf("expected pod to be frozen, got %v", pf)
   256  	}
   257  
   258  	// Create a "container" within the "pod" cgroup.
   259  	// This is not a real container, just a process in the cgroup.
   260  	containerConfig := &configs.Cgroup{
   261  		Parent:      "system-runc_test_pod.slice",
   262  		ScopePrefix: "test",
   263  		Name:        "inner-container",
   264  		Resources:   &configs.Resources{},
   265  	}
   266  
   267  	cmd := exec.Command("bash", "-c", "while read; do echo $REPLY; done")
   268  	cmd.Env = append(os.Environ(), "LANG=C")
   269  
   270  	// Setup stdin.
   271  	stdinR, stdinW, err := os.Pipe()
   272  	if err != nil {
   273  		t.Fatal(err)
   274  	}
   275  	cmd.Stdin = stdinR
   276  
   277  	// Setup stdout.
   278  	stdoutR, stdoutW, err := os.Pipe()
   279  	if err != nil {
   280  		t.Fatal(err)
   281  	}
   282  	cmd.Stdout = stdoutW
   283  	rdr := bufio.NewReader(stdoutR)
   284  
   285  	// Setup stderr.
   286  	var stderr bytes.Buffer
   287  	cmd.Stderr = &stderr
   288  
   289  	err = cmd.Start()
   290  	stdinR.Close()
   291  	stdoutW.Close()
   292  	defer func() {
   293  		_ = stdinW.Close()
   294  		_ = stdoutR.Close()
   295  	}()
   296  	if err != nil {
   297  		t.Fatal(err)
   298  	}
   299  	// Make sure to not leave a zombie.
   300  	defer func() {
   301  		// These may fail, we don't care.
   302  		_ = cmd.Process.Kill()
   303  		_ = cmd.Wait()
   304  	}()
   305  
   306  	// Put the process into a cgroup.
   307  	cm := newManager(t, containerConfig)
   308  
   309  	if err := cm.Apply(cmd.Process.Pid); err != nil {
   310  		t.Fatal(err)
   311  	}
   312  	if err := cm.Set(containerConfig.Resources); err != nil {
   313  		t.Fatal(err)
   314  	}
   315  	// Check that we put the "container" into the "pod" cgroup.
   316  	if !strings.HasPrefix(cm.Path("freezer"), pm.Path("freezer")) {
   317  		t.Fatalf("expected container cgroup path %q to be under pod cgroup path %q",
   318  			cm.Path("freezer"), pm.Path("freezer"))
   319  	}
   320  	// Check the container is not reported as frozen despite the frozen parent.
   321  	cf, err := cm.GetFreezerState()
   322  	if err != nil {
   323  		t.Fatal(err)
   324  	}
   325  	if cf != configs.Thawed {
   326  		t.Fatalf("expected container to be thawed, got %v", cf)
   327  	}
   328  
   329  	// Unfreeze the pod.
   330  	if err := pm.Freeze(configs.Thawed); err != nil {
   331  		t.Fatal(err)
   332  	}
   333  
   334  	cf, err = cm.GetFreezerState()
   335  	if err != nil {
   336  		t.Fatal(err)
   337  	}
   338  	if cf != configs.Thawed {
   339  		t.Fatalf("expected container to be thawed, got %v", cf)
   340  	}
   341  
   342  	// Check the "container" works.
   343  	marker := "one two\n"
   344  	_, err = stdinW.WriteString(marker)
   345  	if err != nil {
   346  		t.Fatal(err)
   347  	}
   348  	reply, err := rdr.ReadString('\n')
   349  	if err != nil {
   350  		t.Fatalf("reading from container: %v", err)
   351  	}
   352  	if reply != marker {
   353  		t.Fatalf("expected %q, got %q", marker, reply)
   354  	}
   355  }