gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/pkg/test/dockerutil/dockerutil.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 dockerutil is a collection of utility functions.
    16  package dockerutil
    17  
    18  import (
    19  	"context"
    20  	"encoding/json"
    21  	"flag"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"log"
    26  	"os"
    27  	"os/exec"
    28  	"regexp"
    29  	"strconv"
    30  	"strings"
    31  	"testing"
    32  	"time"
    33  
    34  	"gvisor.dev/gvisor/pkg/test/testutil"
    35  	"gvisor.dev/gvisor/runsc/cgroup"
    36  )
    37  
    38  var (
    39  	// runtime is the runtime to use for tests. This will be applied to all
    40  	// containers. Note that the default here ("runsc") corresponds to the
    41  	// default used by the installations.
    42  	runtime = flag.String("runtime", os.Getenv("RUNTIME"), "specify which runtime to use")
    43  
    44  	// config is the default Docker daemon configuration path.
    45  	config = flag.String("config_path", "/etc/docker/daemon.json", "configuration file for reading paths")
    46  
    47  	// The following flags are for the "pprof" profiler tool.
    48  
    49  	// pprofBaseDir allows the user to change the directory to which profiles are
    50  	// written. By default, profiles will appear under:
    51  	// /tmp/profile/RUNTIME/CONTAINER_NAME/*.pprof.
    52  	pprofBaseDir  = flag.String("pprof-dir", "/tmp/profile", "base directory in: BASEDIR/RUNTIME/CONTINER_NAME/FILENAME (e.g. /tmp/profile/runtime/mycontainer/cpu.pprof)")
    53  	pprofDuration = flag.Duration("pprof-duration", time.Hour, "profiling duration (automatically stopped at container exit)")
    54  
    55  	// The below flags enable each type of profile. Multiple profiles can be
    56  	// enabled for each run. The profile will be collected from the start.
    57  	pprofBlock = flag.Bool("pprof-block", false, "enables block profiling with runsc debug")
    58  	pprofCPU   = flag.Bool("pprof-cpu", false, "enables CPU profiling with runsc debug")
    59  	pprofHeap  = flag.Bool("pprof-heap", false, "enables heap profiling with runsc debug")
    60  	pprofMutex = flag.Bool("pprof-mutex", false, "enables mutex profiling with runsc debug")
    61  	trace      = flag.Bool("go-trace", false, "enables collecting a go trace with runsc debug")
    62  
    63  	// This matches the string "native.cgroupdriver=systemd" (including optional
    64  	// whitespace), which can be found in a docker daemon configuration file's
    65  	// exec-opts field.
    66  	useSystemdRgx = regexp.MustCompile("\\s*(native\\.cgroupdriver)\\s*=\\s*(systemd)\\s*")
    67  )
    68  
    69  // PrintDockerConfig prints the whole Docker configuration file to the log.
    70  func PrintDockerConfig() {
    71  	configBytes, err := ioutil.ReadFile(*config)
    72  	if err != nil {
    73  		log.Fatalf("Cannot read Docker config at %v: %v", *config, err)
    74  	}
    75  	log.Printf("Docker config (from %v):\n--------\n%v\n--------\n", *config, string(configBytes))
    76  }
    77  
    78  // EnsureSupportedDockerVersion checks if correct docker is installed.
    79  //
    80  // This logs directly to stderr, as it is typically called from a Main wrapper.
    81  func EnsureSupportedDockerVersion() {
    82  	cmd := exec.Command("docker", "version")
    83  	out, err := cmd.CombinedOutput()
    84  	if err != nil {
    85  		log.Fatalf("error running %q: %v", "docker version", err)
    86  	}
    87  	re := regexp.MustCompile(`Version:\s+(\d+)\.(\d+)\.\d.*`)
    88  	matches := re.FindStringSubmatch(string(out))
    89  	if len(matches) != 3 {
    90  		log.Fatalf("Invalid docker output: %s", out)
    91  	}
    92  	major, _ := strconv.Atoi(matches[1])
    93  	minor, _ := strconv.Atoi(matches[2])
    94  	if major < 17 || (major == 17 && minor < 9) {
    95  		log.Fatalf("Docker version 17.09.0 or greater is required, found: %02d.%02d", major, minor)
    96  	}
    97  }
    98  
    99  // EnsureDockerExperimentalEnabled ensures that Docker has experimental features enabled.
   100  func EnsureDockerExperimentalEnabled() {
   101  	cmd := exec.Command("docker", "version", "--format={{.Server.Experimental}}")
   102  	out, err := cmd.CombinedOutput()
   103  	if err != nil {
   104  		log.Fatalf("error running %s: %v", "docker version --format='{{.Server.Experimental}}'", err)
   105  	}
   106  	if strings.TrimSpace(string(out)) != "true" {
   107  		PrintDockerConfig()
   108  		log.Fatalf("Docker is running without experimental features enabled.")
   109  	}
   110  }
   111  
   112  // RuntimePath returns the binary path for the current runtime.
   113  func RuntimePath() (string, error) {
   114  	rs, err := runtimeMap()
   115  	if err != nil {
   116  		return "", err
   117  	}
   118  
   119  	p, ok := rs["path"].(string)
   120  	if !ok {
   121  		// The runtime does not declare a path.
   122  		return "", fmt.Errorf("runtime does not declare a path: %v", rs)
   123  	}
   124  	return p, nil
   125  }
   126  
   127  // IsGVisorRuntime returns whether the default container runtime used by
   128  // `dockerutil` is gVisor-based or not.
   129  func IsGVisorRuntime(ctx context.Context, t *testing.T) (bool, error) {
   130  	output, err := MakeContainer(ctx, t).Run(ctx, RunOpts{Image: "basic/alpine"}, "dmesg")
   131  	if err != nil {
   132  		if strings.Contains(output, "dmesg: klogctl: Operation not permitted") {
   133  			return false, nil
   134  		}
   135  		return false, fmt.Errorf("failed to run dmesg: %v (output: %q)", err, output)
   136  	}
   137  	return strings.Contains(output, "gVisor"), nil
   138  }
   139  
   140  // UsingSystemdCgroup returns true if the docker configuration has the
   141  // native.cgroupdriver=systemd option set in "exec-opts", or if the
   142  // system is using cgroupv2, in which case systemd is the default driver.
   143  func UsingSystemdCgroup() (bool, error) {
   144  	// Read the configuration data; the file must exist.
   145  	configBytes, err := ioutil.ReadFile(*config)
   146  	if err != nil {
   147  		return false, err
   148  	}
   149  	// Unmarshal the configuration.
   150  	c := make(map[string]any)
   151  	if err := json.Unmarshal(configBytes, &c); err != nil {
   152  		return false, err
   153  	}
   154  	// Decode the expected configuration.
   155  	e, ok := c["exec-opts"]
   156  	if !ok {
   157  		// No exec-opts. Default is true on cgroupv2, false otherwise.
   158  		return cgroup.IsOnlyV2(), nil
   159  	}
   160  	eos, ok := e.([]any)
   161  	if !ok {
   162  		// The exec opts are not an array.
   163  		return false, fmt.Errorf("unexpected format: %+v", eos)
   164  	}
   165  	for _, opt := range eos {
   166  		if optStr, ok := opt.(string); ok && useSystemdRgx.MatchString(optStr) {
   167  			return true, nil
   168  		}
   169  	}
   170  	return false, nil
   171  }
   172  
   173  func runtimeMap() (map[string]any, error) {
   174  	// Read the configuration data; the file must exist.
   175  	configBytes, err := ioutil.ReadFile(*config)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	// Unmarshal the configuration.
   181  	c := make(map[string]any)
   182  	if err := json.Unmarshal(configBytes, &c); err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	// Decode the expected configuration.
   187  	r, ok := c["runtimes"]
   188  	if !ok {
   189  		return nil, fmt.Errorf("no runtimes declared: %v", c)
   190  	}
   191  	rs, ok := r.(map[string]any)
   192  	if !ok {
   193  		// The runtimes are not a map.
   194  		return nil, fmt.Errorf("unexpected format: %v", rs)
   195  	}
   196  	r, ok = rs[*runtime]
   197  	if !ok {
   198  		// The expected runtime is not declared.
   199  		return nil, fmt.Errorf("runtime %q not found: %v", *runtime, rs)
   200  	}
   201  	rs, ok = r.(map[string]any)
   202  	if !ok {
   203  		// The runtime is not a map.
   204  		return nil, fmt.Errorf("unexpected format: %v", r)
   205  	}
   206  	return rs, nil
   207  }
   208  
   209  // Save exports a container image to the given Writer.
   210  //
   211  // Note that the writer should be actively consuming the output, otherwise it
   212  // is not guaranteed that the Save will make any progress and the call may
   213  // stall indefinitely.
   214  //
   215  // This is called by criutil in order to import imports.
   216  func Save(logger testutil.Logger, image string, w io.Writer) error {
   217  	cmd := testutil.Command(logger, "docker", "save", testutil.ImageByName(image))
   218  	cmd.Stdout = w // Send directly to the writer.
   219  	return cmd.Run()
   220  }
   221  
   222  // Runtime returns the value of the flag runtime.
   223  func Runtime() string {
   224  	return *runtime
   225  }