gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/test/root/crictl_test.go (about)

     1  // Copyright 2018 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package root
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"net/http"
    24  	"os"
    25  	"os/exec"
    26  	"path"
    27  	"regexp"
    28  	"strconv"
    29  	"strings"
    30  	"sync"
    31  	"testing"
    32  	"time"
    33  
    34  	"gvisor.dev/gvisor/pkg/cleanup"
    35  	"gvisor.dev/gvisor/pkg/test/criutil"
    36  	"gvisor.dev/gvisor/pkg/test/dockerutil"
    37  	"gvisor.dev/gvisor/pkg/test/testutil"
    38  )
    39  
    40  // Tests for crictl have to be run as root (rather than in a user namespace)
    41  // because crictl creates named network namespaces in /var/run/netns/.
    42  
    43  // Sandbox returns a JSON config for a simple sandbox. Sandbox names must be
    44  // unique so different names should be used when running tests on the same
    45  // containerd instance.
    46  func Sandbox(name string) string {
    47  	// Sandbox is a default JSON config for a sandbox.
    48  	s := map[string]any{
    49  		"metadata": map[string]string{
    50  			"name":      name,
    51  			"namespace": "default",
    52  			"uid":       testutil.RandomID(""),
    53  		},
    54  		"linux":         map[string]string{},
    55  		"log_directory": "/tmp",
    56  	}
    57  
    58  	v, err := json.Marshal(s)
    59  	if err != nil {
    60  		// This shouldn't happen.
    61  		panic(err)
    62  	}
    63  	return string(v)
    64  }
    65  
    66  // SimpleSpec returns a JSON config for a simple container that runs the
    67  // specified command in the specified image.
    68  func SimpleSpec(name, image string, cmd []string, extra map[string]any) string {
    69  	s := map[string]any{
    70  		"metadata": map[string]string{
    71  			"name": name,
    72  		},
    73  		"image": map[string]string{
    74  			"image": testutil.ImageByName(image),
    75  		},
    76  		// Log files are not deleted after root tests are run. Log to random
    77  		// paths to ensure logs are fresh.
    78  		"log_path": fmt.Sprintf("%s.log", testutil.RandomID(name)),
    79  		"stdin":    false,
    80  		"tty":      false,
    81  	}
    82  	if len(cmd) > 0 { // Omit if empty.
    83  		s["command"] = cmd
    84  	}
    85  	for k, v := range extra {
    86  		s[k] = v // Extra settings.
    87  	}
    88  	v, err := json.Marshal(s)
    89  	if err != nil {
    90  		// This shouldn't happen.
    91  		panic(err)
    92  	}
    93  	return string(v)
    94  }
    95  
    96  // Httpd is a JSON config for an httpd container.
    97  var Httpd = SimpleSpec("httpd", "basic/httpd", nil, nil)
    98  
    99  // TestCrictlSanity refers to b/112433158.
   100  func TestCrictlSanity(t *testing.T) {
   101  	// Setup containerd and crictl.
   102  	crictl, cleanup, err := setup(t)
   103  	if err != nil {
   104  		t.Fatalf("failed to setup crictl: %v", err)
   105  	}
   106  	defer cleanup()
   107  	podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/httpd", Sandbox("default"), Httpd)
   108  	if err != nil {
   109  		t.Fatalf("start failed: %v", err)
   110  	}
   111  
   112  	// Look for the httpd page.
   113  	if err = httpGet(crictl, podID, "index.html"); err != nil {
   114  		t.Fatalf("failed to get page: %v", err)
   115  	}
   116  
   117  	// Stop everything.
   118  	if err := crictl.StopPodAndContainer(podID, contID); err != nil {
   119  		t.Fatalf("stop failed: %v", err)
   120  	}
   121  }
   122  
   123  // HttpdMountPaths is a JSON config for an httpd container with additional
   124  // mounts.
   125  var HttpdMountPaths = SimpleSpec("httpd", "basic/httpd", nil, map[string]any{
   126  	"mounts": []map[string]any{
   127  		{
   128  			"container_path": "/var/run/secrets/kubernetes.io/serviceaccount",
   129  			"host_path":      "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064/volumes/kubernetes.io~secret/default-token-2rpfx",
   130  			"readonly":       true,
   131  		},
   132  		{
   133  			"container_path": "/etc/hosts",
   134  			"host_path":      "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064/etc-hosts",
   135  			"readonly":       false,
   136  		},
   137  		{
   138  			"container_path": "/dev/termination-log",
   139  			"host_path":      "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064/containers/httpd/d1709580",
   140  			"readonly":       false,
   141  		},
   142  		{
   143  			"container_path": "/usr/local/apache2/htdocs/test",
   144  			"host_path":      "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064",
   145  			"readonly":       true,
   146  		},
   147  	},
   148  	"linux": map[string]any{},
   149  })
   150  
   151  // TestMountPaths refers to b/117635704.
   152  func TestMountPaths(t *testing.T) {
   153  	// Setup containerd and crictl.
   154  	crictl, cleanup, err := setup(t)
   155  	if err != nil {
   156  		t.Fatalf("failed to setup crictl: %v", err)
   157  	}
   158  	defer cleanup()
   159  	podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/httpd", Sandbox("default"), HttpdMountPaths)
   160  	if err != nil {
   161  		t.Fatalf("start failed: %v", err)
   162  	}
   163  
   164  	// Look for the directory available at /test.
   165  	if err = httpGet(crictl, podID, "test"); err != nil {
   166  		t.Fatalf("failed to get page: %v", err)
   167  	}
   168  
   169  	// Stop everything.
   170  	if err := crictl.StopPodAndContainer(podID, contID); err != nil {
   171  		t.Fatalf("stop failed: %v", err)
   172  	}
   173  }
   174  
   175  // TestMountPaths refers to b/118728671.
   176  func TestMountOverSymlinks(t *testing.T) {
   177  	// Setup containerd and crictl.
   178  	crictl, cleanup, err := setup(t)
   179  	if err != nil {
   180  		t.Fatalf("failed to setup crictl: %v", err)
   181  	}
   182  	defer cleanup()
   183  
   184  	spec := SimpleSpec("busybox", "basic/symlink-resolv", []string{"sleep", "1000"}, nil)
   185  	podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/symlink-resolv", Sandbox("default"), spec)
   186  	if err != nil {
   187  		t.Fatalf("start failed: %v", err)
   188  	}
   189  
   190  	out, err := crictl.Exec(contID, "readlink", "/etc/resolv.conf")
   191  	if err != nil {
   192  		t.Fatalf("readlink failed: %v, out: %s", err, out)
   193  	}
   194  	if want := "/tmp/resolv.conf"; !strings.Contains(string(out), want) {
   195  		t.Fatalf("/etc/resolv.conf is not pointing to %q: %q", want, string(out))
   196  	}
   197  
   198  	etc, err := crictl.Exec(contID, "cat", "/etc/resolv.conf")
   199  	if err != nil {
   200  		t.Fatalf("cat failed: %v, out: %s", err, etc)
   201  	}
   202  	tmp, err := crictl.Exec(contID, "cat", "/tmp/resolv.conf")
   203  	if err != nil {
   204  		t.Fatalf("cat failed: %v, out: %s", err, out)
   205  	}
   206  	if tmp != etc {
   207  		t.Fatalf("file content doesn't match:\n\t/etc/resolv.conf: %s\n\t/tmp/resolv.conf: %s", string(etc), string(tmp))
   208  	}
   209  
   210  	// Stop everything.
   211  	if err := crictl.StopPodAndContainer(podID, contID); err != nil {
   212  		t.Fatalf("stop failed: %v", err)
   213  	}
   214  }
   215  
   216  // TestHomeDir tests that the HOME environment variable is set for
   217  // Pod containers.
   218  func TestHomeDir(t *testing.T) {
   219  	// Setup containerd and crictl.
   220  	crictl, cleanup, err := setup(t)
   221  	if err != nil {
   222  		t.Fatalf("failed to setup crictl: %v", err)
   223  	}
   224  	defer cleanup()
   225  
   226  	// Note that container ID returned here is a sub-container. All Pod
   227  	// containers are sub-containers. The root container of the sandbox is the
   228  	// pause container.
   229  	t.Run("sub-container", func(t *testing.T) {
   230  		contSpec := SimpleSpec("subcontainer", "basic/busybox", []string{"sh", "-c", "echo $HOME"}, nil)
   231  		podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/busybox", Sandbox("subcont-sandbox"), contSpec)
   232  		if err != nil {
   233  			t.Fatalf("start failed: %v", err)
   234  		}
   235  
   236  		out, err := crictl.Logs(contID)
   237  		if err != nil {
   238  			t.Fatalf("failed retrieving container logs: %v, out: %s", err, out)
   239  		}
   240  		if got, want := strings.TrimSpace(string(out)), "/root"; got != want {
   241  			t.Fatalf("Home directory invalid. Got %q, Want : %q", got, want)
   242  		}
   243  
   244  		// Stop everything; note that the pod may have already stopped.
   245  		crictl.StopPodAndContainer(podID, contID)
   246  	})
   247  
   248  	// Tests that HOME is set for the exec process.
   249  	t.Run("exec", func(t *testing.T) {
   250  		contSpec := SimpleSpec("exec", "basic/busybox", []string{"sleep", "1000"}, nil)
   251  		podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/busybox", Sandbox("exec-sandbox"), contSpec)
   252  		if err != nil {
   253  			t.Fatalf("start failed: %v", err)
   254  		}
   255  
   256  		out, err := crictl.Exec(contID, "sh", "-c", "echo $HOME")
   257  		if err != nil {
   258  			t.Fatalf("failed retrieving container logs: %v, out: %s", err, out)
   259  		}
   260  		if got, want := strings.TrimSpace(string(out)), "/root"; got != want {
   261  			t.Fatalf("Home directory invalid. Got %q, Want : %q", got, want)
   262  		}
   263  
   264  		// Stop everything.
   265  		if err := crictl.StopPodAndContainer(podID, contID); err != nil {
   266  			t.Fatalf("stop failed: %v", err)
   267  		}
   268  	})
   269  }
   270  
   271  const containerdRuntime = "runsc"
   272  
   273  // containerdConfigv14 is the containerd (1.4-) configuration file that
   274  // configures the gVisor shim.
   275  //
   276  // Note that the v2 shim binary name must be containerd-shim-<runtime>-v1.
   277  const containerdConfigv14 = `
   278  disabled_plugins = ["restart"]
   279  [plugins.cri]
   280    disable_tcp_service = true
   281  [plugins.linux]
   282    shim_debug = true
   283  [plugins.cri.containerd.runtimes.` + containerdRuntime + `]
   284    runtime_type = "io.containerd.` + containerdRuntime + `.v1"
   285  [plugins.cri.containerd.runtimes.` + containerdRuntime + `.options]
   286    TypeUrl = "io.containerd.` + containerdRuntime + `.v1.options"
   287  `
   288  
   289  // containerdConfig is the containerd (1.5+) configuration file that
   290  // configures the gVisor shim.
   291  //
   292  // Note that the v2 shim binary name must be containerd-shim-<runtime>-v1.
   293  const containerdConfig = `
   294  version=2
   295  disabled_plugins = ["io.containerd.internal.v1.restart"]
   296  [plugins."io.containerd.grpc.v1.cri"]
   297    disable_tcp_service = true
   298  [plugins."io.containerd.runtime.v1.linux"]
   299    shim_debug = true
   300  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
   301    runtime_type = "io.containerd.runc.v2"
   302  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.` + containerdRuntime + `]
   303    runtime_type = "io.containerd.` + containerdRuntime + `.v1"
   304  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.` + containerdRuntime + `.options]
   305    TypeUrl = "io.containerd.` + containerdRuntime + `.v1.options"
   306  `
   307  
   308  // setup sets up before a test. Specifically it:
   309  //   - Creates directories and a socket for containerd to utilize.
   310  //   - Runs containerd and waits for it to reach a "ready" state for testing.
   311  //   - Returns a cleanup function that should be called at the end of the test.
   312  func setup(t *testing.T) (*criutil.Crictl, func(), error) {
   313  	// Create temporary containerd root and state directories, and a socket
   314  	// via which crictl and containerd communicate.
   315  	containerdRoot, err := ioutil.TempDir(testutil.TmpDir(), "containerd-root")
   316  	if err != nil {
   317  		t.Fatalf("failed to create containerd root: %v", err)
   318  	}
   319  	cu := cleanup.Make(func() { os.RemoveAll(containerdRoot) })
   320  	defer cu.Clean()
   321  	t.Logf("Using containerd root: %s", containerdRoot)
   322  
   323  	containerdState, err := ioutil.TempDir(testutil.TmpDir(), "containerd-state")
   324  	if err != nil {
   325  		t.Fatalf("failed to create containerd state: %v", err)
   326  	}
   327  	cu.Add(func() { os.RemoveAll(containerdState) })
   328  	t.Logf("Using containerd state: %s", containerdState)
   329  
   330  	sockDir, err := ioutil.TempDir(testutil.TmpDir(), "containerd-sock")
   331  	if err != nil {
   332  		t.Fatalf("failed to create containerd socket directory: %v", err)
   333  	}
   334  	cu.Add(func() { os.RemoveAll(sockDir) })
   335  	sockAddr := path.Join(sockDir, "test.sock")
   336  	t.Logf("Using containerd socket: %s", sockAddr)
   337  
   338  	// Extract the containerd version.
   339  	versionCmd := exec.Command(getContainerd(), "-v")
   340  	out, err := versionCmd.CombinedOutput()
   341  	if err != nil {
   342  		t.Fatalf("error extracting containerd version: %v (%s)", err, string(out))
   343  	}
   344  	r := regexp.MustCompile(" v([0-9]+)\\.([0-9]+)\\.([0-9+])")
   345  	vs := r.FindStringSubmatch(string(out))
   346  	if len(vs) != 4 {
   347  		t.Fatalf("error unexpected version string: %s", string(out))
   348  	}
   349  	major, err := strconv.ParseUint(vs[1], 10, 64)
   350  	if err != nil {
   351  		t.Fatalf("error parsing containerd major version: %v (%s)", err, string(out))
   352  	}
   353  	minor, err := strconv.ParseUint(vs[2], 10, 64)
   354  	if err != nil {
   355  		t.Fatalf("error parsing containerd minor version: %v (%s)", err, string(out))
   356  	}
   357  	t.Logf("Using containerd version: %d.%d", major, minor)
   358  
   359  	// Check if containerd supports shim v2.
   360  	if major < 1 || (major == 1 && minor <= 1) {
   361  		t.Skipf("skipping incompatible containerd (want at least 1.2, got %d.%d)", major, minor)
   362  	}
   363  
   364  	// We rewrite a configuration. This is based on the current docker
   365  	// configuration for the runtime under test.
   366  	runtime, err := dockerutil.RuntimePath()
   367  	if err != nil {
   368  		t.Fatalf("error discovering runtime path: %v", err)
   369  	}
   370  	t.Logf("Using runtime: %v", runtime)
   371  
   372  	// Construct a PATH that includes the runtime directory. This is
   373  	// because the shims will be installed there, and containerd may infer
   374  	// the binary name and search the PATH.
   375  	runtimeDir := path.Dir(runtime)
   376  	modifiedPath, ok := os.LookupEnv("PATH")
   377  	if ok {
   378  		modifiedPath = ":" + modifiedPath // We prepend below.
   379  	}
   380  	modifiedPath = path.Dir(getContainerd()) + modifiedPath
   381  	modifiedPath = runtimeDir + ":" + modifiedPath
   382  	t.Logf("Using PATH: %v", modifiedPath)
   383  
   384  	// Generate the configuration for the test.
   385  	config := getContainerdConfig(major, minor)
   386  	t.Logf("Using config: %s", config)
   387  	configFile, configCleanup, err := testutil.WriteTmpFile("containerd-config", config)
   388  	if err != nil {
   389  		t.Fatalf("failed to write containerd config")
   390  	}
   391  	cu.Add(configCleanup)
   392  
   393  	// Start containerd.
   394  	args := []string{
   395  		getContainerd(),
   396  		"--config", configFile,
   397  		"--log-level", "debug",
   398  		"--root", containerdRoot,
   399  		"--state", containerdState,
   400  		"--address", sockAddr,
   401  	}
   402  	t.Logf("Using args: %s", strings.Join(args, " "))
   403  	cmd := exec.Command(args[0], args[1:]...)
   404  	cmd.Env = append(os.Environ(), "PATH="+modifiedPath)
   405  
   406  	// Include output in logs.
   407  	stderrPipe, err := cmd.StderrPipe()
   408  	if err != nil {
   409  		t.Fatalf("failed to create stderr pipe: %v", err)
   410  	}
   411  	cu.Add(func() { stderrPipe.Close() })
   412  	stdoutPipe, err := cmd.StdoutPipe()
   413  	if err != nil {
   414  		t.Fatalf("failed to create stdout pipe: %v", err)
   415  	}
   416  	cu.Add(func() { stdoutPipe.Close() })
   417  	var (
   418  		wg     sync.WaitGroup
   419  		stderr bytes.Buffer
   420  		stdout bytes.Buffer
   421  	)
   422  	startupR, startupW := io.Pipe()
   423  	wg.Add(2)
   424  	go func() {
   425  		defer wg.Done()
   426  		io.Copy(io.MultiWriter(startupW, &stderr), stderrPipe)
   427  	}()
   428  	go func() {
   429  		defer wg.Done()
   430  		io.Copy(io.MultiWriter(startupW, &stdout), stdoutPipe)
   431  	}()
   432  	cu.Add(func() {
   433  		wg.Wait()
   434  		t.Logf("containerd stdout: %s", stdout.String())
   435  		t.Logf("containerd stderr: %s", stderr.String())
   436  	})
   437  
   438  	// Start the process.
   439  	if err := cmd.Start(); err != nil {
   440  		t.Fatalf("failed running containerd: %v", err)
   441  	}
   442  
   443  	// Wait for containerd to boot.
   444  	if err := testutil.WaitUntilRead(startupR, "Start streaming server", 10*time.Second); err != nil {
   445  		t.Fatalf("failed to start containerd: %v", err)
   446  	}
   447  
   448  	// Discard all subsequent data.
   449  	go io.Copy(ioutil.Discard, startupR)
   450  
   451  	// Create the crictl interface.
   452  	cc := criutil.NewCrictl(t, sockAddr)
   453  	cu.Add(cc.CleanUp)
   454  
   455  	// Kill must be the last cleanup (as it will be executed first).
   456  	cu.Add(func() {
   457  		// Best effort: ignore errors.
   458  		testutil.KillCommand(cmd)
   459  	})
   460  
   461  	return cc, cu.Release(), nil
   462  }
   463  
   464  // httpGet GETs the contents of a file served from a pod on port 80.
   465  func httpGet(crictl *criutil.Crictl, podID, filePath string) error {
   466  	// Get the IP of the httpd server.
   467  	ip, err := crictl.PodIP(podID)
   468  	if err != nil {
   469  		return fmt.Errorf("failed to get IP from pod %q: %v", podID, err)
   470  	}
   471  
   472  	// GET the page. We may be waiting for the server to start, so retry
   473  	// with a timeout.
   474  	var resp *http.Response
   475  	cb := func() error {
   476  		r, err := http.Get(fmt.Sprintf("http://%s", path.Join(ip, filePath)))
   477  		resp = r
   478  		return err
   479  	}
   480  	if err := testutil.Poll(cb, 20*time.Second); err != nil {
   481  		return err
   482  	}
   483  	defer resp.Body.Close()
   484  
   485  	if resp.StatusCode != 200 {
   486  		return fmt.Errorf("bad status returned: %d", resp.StatusCode)
   487  	}
   488  	return nil
   489  }
   490  
   491  func getContainerd() string {
   492  	// Use the local path if it exists, otherwise, use the system one.
   493  	if _, err := os.Stat("/usr/local/bin/containerd"); err == nil {
   494  		return "/usr/local/bin/containerd"
   495  	}
   496  	return "/usr/bin/containerd"
   497  }
   498  
   499  func getContainerdConfig(major, minor uint64) string {
   500  	if major == 1 && minor <= 4 {
   501  		return containerdConfigv14
   502  	}
   503  	return containerdConfig
   504  }