github.com/SagerNet/gvisor@v0.0.0-20210707092255-7731c139d75c/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  	"github.com/SagerNet/gvisor/pkg/cleanup"
    35  	"github.com/SagerNet/gvisor/pkg/test/criutil"
    36  	"github.com/SagerNet/gvisor/pkg/test/dockerutil"
    37  	"github.com/SagerNet/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]interface{}{
    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]interface{}) string {
    69  	s := map[string]interface{}{
    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]interface{}{
   126  	"mounts": []map[string]interface{}{
   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]interface{}{},
   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/resolv", []string{"sleep", "1000"}, nil)
   185  	podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/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  // Template is the containerd configuration file that configures containerd with
   274  // the gVisor shim, Note that the v2 shim binary name must be
   275  // containerd-shim-<runtime>-v1.
   276  const template = `
   277  disabled_plugins = ["restart"]
   278  [plugins.cri]
   279    disable_tcp_service = true
   280  [plugins.linux]
   281    shim_debug = true
   282  [plugins.cri.containerd.runtimes.` + containerdRuntime + `]
   283    runtime_type = "io.containerd.` + containerdRuntime + `.v1"
   284  [plugins.cri.containerd.runtimes.` + containerdRuntime + `.options]
   285    TypeUrl = "io.containerd.` + containerdRuntime + `.v1.options"
   286  `
   287  
   288  // setup sets up before a test. Specifically it:
   289  // * Creates directories and a socket for containerd to utilize.
   290  // * Runs containerd and waits for it to reach a "ready" state for testing.
   291  // * Returns a cleanup function that should be called at the end of the test.
   292  func setup(t *testing.T) (*criutil.Crictl, func(), error) {
   293  	// Create temporary containerd root and state directories, and a socket
   294  	// via which crictl and containerd communicate.
   295  	containerdRoot, err := ioutil.TempDir(testutil.TmpDir(), "containerd-root")
   296  	if err != nil {
   297  		t.Fatalf("failed to create containerd root: %v", err)
   298  	}
   299  	cu := cleanup.Make(func() { os.RemoveAll(containerdRoot) })
   300  	defer cu.Clean()
   301  	t.Logf("Using containerd root: %s", containerdRoot)
   302  
   303  	containerdState, err := ioutil.TempDir(testutil.TmpDir(), "containerd-state")
   304  	if err != nil {
   305  		t.Fatalf("failed to create containerd state: %v", err)
   306  	}
   307  	cu.Add(func() { os.RemoveAll(containerdState) })
   308  	t.Logf("Using containerd state: %s", containerdState)
   309  
   310  	sockDir, err := ioutil.TempDir(testutil.TmpDir(), "containerd-sock")
   311  	if err != nil {
   312  		t.Fatalf("failed to create containerd socket directory: %v", err)
   313  	}
   314  	cu.Add(func() { os.RemoveAll(sockDir) })
   315  	sockAddr := path.Join(sockDir, "test.sock")
   316  	t.Logf("Using containerd socket: %s", sockAddr)
   317  
   318  	// Extract the containerd version.
   319  	versionCmd := exec.Command(getContainerd(), "-v")
   320  	out, err := versionCmd.CombinedOutput()
   321  	if err != nil {
   322  		t.Fatalf("error extracting containerd version: %v (%s)", err, string(out))
   323  	}
   324  	r := regexp.MustCompile(" v([0-9]+)\\.([0-9]+)\\.([0-9+])")
   325  	vs := r.FindStringSubmatch(string(out))
   326  	if len(vs) != 4 {
   327  		t.Fatalf("error unexpected version string: %s", string(out))
   328  	}
   329  	major, err := strconv.ParseUint(vs[1], 10, 64)
   330  	if err != nil {
   331  		t.Fatalf("error parsing containerd major version: %v (%s)", err, string(out))
   332  	}
   333  	minor, err := strconv.ParseUint(vs[2], 10, 64)
   334  	if err != nil {
   335  		t.Fatalf("error parsing containerd minor version: %v (%s)", err, string(out))
   336  	}
   337  	t.Logf("Using containerd version: %d.%d", major, minor)
   338  
   339  	// Check if containerd supports shim v2.
   340  	if major < 1 || (major == 1 && minor <= 1) {
   341  		t.Skipf("skipping incompatible containerd (want at least 1.2, got %d.%d)", major, minor)
   342  	}
   343  
   344  	// We rewrite a configuration. This is based on the current docker
   345  	// configuration for the runtime under test.
   346  	runtime, err := dockerutil.RuntimePath()
   347  	if err != nil {
   348  		t.Fatalf("error discovering runtime path: %v", err)
   349  	}
   350  	t.Logf("Using runtime: %v", runtime)
   351  
   352  	// Construct a PATH that includes the runtime directory. This is
   353  	// because the shims will be installed there, and containerd may infer
   354  	// the binary name and search the PATH.
   355  	runtimeDir := path.Dir(runtime)
   356  	modifiedPath, ok := os.LookupEnv("PATH")
   357  	if ok {
   358  		modifiedPath = ":" + modifiedPath // We prepend below.
   359  	}
   360  	modifiedPath = path.Dir(getContainerd()) + modifiedPath
   361  	modifiedPath = runtimeDir + ":" + modifiedPath
   362  	t.Logf("Using PATH: %v", modifiedPath)
   363  
   364  	// Generate the configuration for the test.
   365  	t.Logf("Using config: %s", template)
   366  	configFile, configCleanup, err := testutil.WriteTmpFile("containerd-config", template)
   367  	if err != nil {
   368  		t.Fatalf("failed to write containerd config")
   369  	}
   370  	cu.Add(configCleanup)
   371  
   372  	// Start containerd.
   373  	args := []string{
   374  		getContainerd(),
   375  		"--config", configFile,
   376  		"--log-level", "debug",
   377  		"--root", containerdRoot,
   378  		"--state", containerdState,
   379  		"--address", sockAddr,
   380  	}
   381  	t.Logf("Using args: %s", strings.Join(args, " "))
   382  	cmd := exec.Command(args[0], args[1:]...)
   383  	cmd.Env = append(os.Environ(), "PATH="+modifiedPath)
   384  
   385  	// Include output in logs.
   386  	stderrPipe, err := cmd.StderrPipe()
   387  	if err != nil {
   388  		t.Fatalf("failed to create stderr pipe: %v", err)
   389  	}
   390  	cu.Add(func() { stderrPipe.Close() })
   391  	stdoutPipe, err := cmd.StdoutPipe()
   392  	if err != nil {
   393  		t.Fatalf("failed to create stdout pipe: %v", err)
   394  	}
   395  	cu.Add(func() { stdoutPipe.Close() })
   396  	var (
   397  		wg     sync.WaitGroup
   398  		stderr bytes.Buffer
   399  		stdout bytes.Buffer
   400  	)
   401  	startupR, startupW := io.Pipe()
   402  	wg.Add(2)
   403  	go func() {
   404  		defer wg.Done()
   405  		io.Copy(io.MultiWriter(startupW, &stderr), stderrPipe)
   406  	}()
   407  	go func() {
   408  		defer wg.Done()
   409  		io.Copy(io.MultiWriter(startupW, &stdout), stdoutPipe)
   410  	}()
   411  	cu.Add(func() {
   412  		wg.Wait()
   413  		t.Logf("containerd stdout: %s", stdout.String())
   414  		t.Logf("containerd stderr: %s", stderr.String())
   415  	})
   416  
   417  	// Start the process.
   418  	if err := cmd.Start(); err != nil {
   419  		t.Fatalf("failed running containerd: %v", err)
   420  	}
   421  
   422  	// Wait for containerd to boot.
   423  	if err := testutil.WaitUntilRead(startupR, "Start streaming server", 10*time.Second); err != nil {
   424  		t.Fatalf("failed to start containerd: %v", err)
   425  	}
   426  
   427  	// Discard all subsequent data.
   428  	go io.Copy(ioutil.Discard, startupR)
   429  
   430  	// Create the crictl interface.
   431  	cc := criutil.NewCrictl(t, sockAddr)
   432  	cu.Add(cc.CleanUp)
   433  
   434  	// Kill must be the last cleanup (as it will be executed first).
   435  	cu.Add(func() {
   436  		// Best effort: ignore errors.
   437  		testutil.KillCommand(cmd)
   438  	})
   439  
   440  	return cc, cu.Release(), nil
   441  }
   442  
   443  // httpGet GETs the contents of a file served from a pod on port 80.
   444  func httpGet(crictl *criutil.Crictl, podID, filePath string) error {
   445  	// Get the IP of the httpd server.
   446  	ip, err := crictl.PodIP(podID)
   447  	if err != nil {
   448  		return fmt.Errorf("failed to get IP from pod %q: %v", podID, err)
   449  	}
   450  
   451  	// GET the page. We may be waiting for the server to start, so retry
   452  	// with a timeout.
   453  	var resp *http.Response
   454  	cb := func() error {
   455  		r, err := http.Get(fmt.Sprintf("http://%s", path.Join(ip, filePath)))
   456  		resp = r
   457  		return err
   458  	}
   459  	if err := testutil.Poll(cb, 20*time.Second); err != nil {
   460  		return err
   461  	}
   462  	defer resp.Body.Close()
   463  
   464  	if resp.StatusCode != 200 {
   465  		return fmt.Errorf("bad status returned: %d", resp.StatusCode)
   466  	}
   467  	return nil
   468  }
   469  
   470  func getContainerd() string {
   471  	// Use the local path if it exists, otherwise, use the system one.
   472  	if _, err := os.Stat("/usr/local/bin/containerd"); err == nil {
   473  		return "/usr/local/bin/containerd"
   474  	}
   475  	return "/usr/bin/containerd"
   476  }