gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/pkg/test/criutil/criutil.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 criutil contains utility functions for interacting with the
    16  // Container Runtime Interface (CRI), principally via the crictl command line
    17  // tool. This requires critools to be installed on the local system.
    18  package criutil
    19  
    20  import (
    21  	"encoding/json"
    22  	"fmt"
    23  	"os"
    24  	"os/exec"
    25  	"path"
    26  	"regexp"
    27  	"strconv"
    28  	"strings"
    29  	"time"
    30  
    31  	"gvisor.dev/gvisor/pkg/test/dockerutil"
    32  	"gvisor.dev/gvisor/pkg/test/testutil"
    33  )
    34  
    35  // Crictl contains information required to run the crictl utility.
    36  type Crictl struct {
    37  	logger   testutil.Logger
    38  	endpoint string
    39  	cleanup  []func()
    40  }
    41  
    42  // ResolvePath attempts to find binary paths. It may set the path to invalid,
    43  // which will cause the execution to fail with a sensible error.
    44  func ResolvePath(executable string) string {
    45  	runtime, err := dockerutil.RuntimePath()
    46  	if err == nil {
    47  		// Check first the directory of the runtime itself.
    48  		if dir := path.Dir(runtime); dir != "" && dir != "." {
    49  			guess := path.Join(dir, executable)
    50  			if fi, err := os.Stat(guess); err == nil && (fi.Mode()&0111) != 0 {
    51  				return guess
    52  			}
    53  		}
    54  	}
    55  
    56  	// Favor /usr/local/bin, if it exists.
    57  	localBin := fmt.Sprintf("/usr/local/bin/%s", executable)
    58  	if _, err := os.Stat(localBin); err == nil {
    59  		return localBin
    60  	}
    61  
    62  	// Try to find via the path.
    63  	guess, _ := exec.LookPath(executable)
    64  	if err == nil {
    65  		return guess
    66  	}
    67  
    68  	// Return a bare path; this generates a suitable error.
    69  	return executable
    70  }
    71  
    72  // NewCrictl returns a Crictl configured with a timeout and an endpoint over
    73  // which it will talk to containerd.
    74  func NewCrictl(logger testutil.Logger, endpoint string) *Crictl {
    75  	// Attempt to find the executable, but don't bother propagating the
    76  	// error at this point. The first command executed will return with a
    77  	// binary not found error.
    78  	return &Crictl{
    79  		logger:   logger,
    80  		endpoint: endpoint,
    81  	}
    82  }
    83  
    84  // CleanUp executes cleanup functions.
    85  func (cc *Crictl) CleanUp() {
    86  	for _, c := range cc.cleanup {
    87  		c()
    88  	}
    89  	cc.cleanup = nil
    90  }
    91  
    92  // RunPod creates a sandbox. It corresponds to `crictl runp`.
    93  func (cc *Crictl) RunPod(runtime, sbSpecFile string) (string, error) {
    94  	podID, err := cc.run("runp", "--runtime", runtime, sbSpecFile)
    95  	if err != nil {
    96  		return "", fmt.Errorf("runp failed: %v", err)
    97  	}
    98  	// Strip the trailing newline from crictl output.
    99  	return strings.TrimSpace(podID), nil
   100  }
   101  
   102  // Create creates a container within a sandbox. It corresponds to `crictl
   103  // create`.
   104  func (cc *Crictl) Create(podID, contSpecFile, sbSpecFile string) (string, error) {
   105  	// In version 1.16.0, crictl annoying starting attempting to pull the
   106  	// container, even if it was already available locally. We therefore
   107  	// need to parse the version and add an appropriate --no-pull argument
   108  	// since the image has already been loaded locally.
   109  	out, err := cc.run("-v")
   110  	if err != nil {
   111  		return "", err
   112  	}
   113  	r := regexp.MustCompile("crictl version ([0-9]+)\\.([0-9]+)\\.([0-9+])")
   114  	vs := r.FindStringSubmatch(out)
   115  	if len(vs) != 4 {
   116  		return "", fmt.Errorf("crictl -v had unexpected output: %s", out)
   117  	}
   118  	major, err := strconv.ParseUint(vs[1], 10, 64)
   119  	if err != nil {
   120  		return "", fmt.Errorf("crictl had invalid version: %v (%s)", err, out)
   121  	}
   122  	minor, err := strconv.ParseUint(vs[2], 10, 64)
   123  	if err != nil {
   124  		return "", fmt.Errorf("crictl had invalid version: %v (%s)", err, out)
   125  	}
   126  
   127  	args := []string{"create"}
   128  	if (major == 1 && minor >= 16) || major > 1 {
   129  		args = append(args, "--no-pull")
   130  	}
   131  	args = append(args, podID)
   132  	args = append(args, contSpecFile)
   133  	args = append(args, sbSpecFile)
   134  
   135  	podID, err = cc.run(args...)
   136  	if err != nil {
   137  		time.Sleep(10 * time.Minute) // XXX
   138  		return "", fmt.Errorf("create failed: %v", err)
   139  	}
   140  
   141  	// Strip the trailing newline from crictl output.
   142  	return strings.TrimSpace(podID), nil
   143  }
   144  
   145  // Start starts a container. It corresponds to `crictl start`.
   146  func (cc *Crictl) Start(contID string) (string, error) {
   147  	output, err := cc.run("start", contID)
   148  	if err != nil {
   149  		return "", fmt.Errorf("start failed: %v", err)
   150  	}
   151  	return output, nil
   152  }
   153  
   154  // Stop stops a container. It corresponds to `crictl stop`.
   155  func (cc *Crictl) Stop(contID string) error {
   156  	_, err := cc.run("stop", contID)
   157  	return err
   158  }
   159  
   160  // Exec execs a program inside a container. It corresponds to `crictl exec`.
   161  func (cc *Crictl) Exec(contID string, args ...string) (string, error) {
   162  	a := []string{"exec", contID}
   163  	a = append(a, args...)
   164  	output, err := cc.run(a...)
   165  	if err != nil {
   166  		return "", fmt.Errorf("exec failed: %v", err)
   167  	}
   168  	return output, nil
   169  }
   170  
   171  // Logs retrieves the container logs. It corresponds to `crictl logs`.
   172  func (cc *Crictl) Logs(contID string, args ...string) (string, error) {
   173  	a := []string{"logs", contID}
   174  	a = append(a, args...)
   175  	output, err := cc.run(a...)
   176  	if err != nil {
   177  		return "", fmt.Errorf("logs failed: %v", err)
   178  	}
   179  	return output, nil
   180  }
   181  
   182  // Rm removes a container. It corresponds to `crictl rm`.
   183  func (cc *Crictl) Rm(contID string) error {
   184  	_, err := cc.run("rm", contID)
   185  	return err
   186  }
   187  
   188  // StopPod stops a pod. It corresponds to `crictl stopp`.
   189  func (cc *Crictl) StopPod(podID string) error {
   190  	_, err := cc.run("stopp", podID)
   191  	return err
   192  }
   193  
   194  // containsConfig is a minimal copy of
   195  // https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/apis/cri/runtime/v1alpha2/api.proto
   196  // It only contains fields needed for testing.
   197  type containerConfig struct {
   198  	Status containerStatus
   199  }
   200  
   201  type containerStatus struct {
   202  	Network containerNetwork
   203  }
   204  
   205  type containerNetwork struct {
   206  	IP string
   207  }
   208  
   209  // PodIP returns a pod's IP address.
   210  func (cc *Crictl) PodIP(podID string) (string, error) {
   211  	output, err := cc.run("inspectp", podID)
   212  	if err != nil {
   213  		return "", err
   214  	}
   215  	conf := &containerConfig{}
   216  	if err := json.Unmarshal([]byte(output), conf); err != nil {
   217  		return "", fmt.Errorf("failed to unmarshal JSON: %v, %s", err, output)
   218  	}
   219  	if conf.Status.Network.IP == "" {
   220  		return "", fmt.Errorf("no IP found in config: %s", output)
   221  	}
   222  	return conf.Status.Network.IP, nil
   223  }
   224  
   225  // RmPod removes a container. It corresponds to `crictl rmp`.
   226  func (cc *Crictl) RmPod(podID string) error {
   227  	_, err := cc.run("rmp", podID)
   228  	return err
   229  }
   230  
   231  // Import imports the given container from the local Docker instance.
   232  func (cc *Crictl) Import(image string) error {
   233  	// Note that we provide a 10 minute timeout after connect because we may
   234  	// be pushing a lot of bytes in order to import the image. The connect
   235  	// timeout stays the same and is inherited from the Crictl instance.
   236  	cmd := testutil.Command(cc.logger,
   237  		ResolvePath("ctr"),
   238  		fmt.Sprintf("--connect-timeout=%s", 30*time.Second),
   239  		fmt.Sprintf("--address=%s", cc.endpoint),
   240  		"-n", "k8s.io", "images", "import", "-")
   241  	cmd.Stderr = os.Stderr // Pass through errors.
   242  
   243  	// Create a pipe and start the program.
   244  	w, err := cmd.StdinPipe()
   245  	if err != nil {
   246  		return err
   247  	}
   248  	if err := cmd.Start(); err != nil {
   249  		return err
   250  	}
   251  
   252  	// Save the image on the other end.
   253  	if err := dockerutil.Save(cc.logger, image, w); err != nil {
   254  		cmd.Wait()
   255  		return err
   256  	}
   257  
   258  	// Close our pipe reference & see if it was loaded.
   259  	if err := w.Close(); err != nil {
   260  		return w.Close()
   261  	}
   262  
   263  	return cmd.Wait()
   264  }
   265  
   266  // StartContainer pulls the given image ands starts the container in the
   267  // sandbox with the given podID.
   268  //
   269  // Note that the image will always be imported from the local docker daemon.
   270  func (cc *Crictl) StartContainer(podID, image, sbSpec, contSpec string) (string, error) {
   271  	if err := cc.Import(image); err != nil {
   272  		return "", err
   273  	}
   274  
   275  	// Write the specs to files that can be read by crictl.
   276  	sbSpecFile, cleanup, err := testutil.WriteTmpFile("sbSpec", sbSpec)
   277  	if err != nil {
   278  		return "", fmt.Errorf("failed to write sandbox spec: %v", err)
   279  	}
   280  	cc.cleanup = append(cc.cleanup, cleanup)
   281  	contSpecFile, cleanup, err := testutil.WriteTmpFile("contSpec", contSpec)
   282  	if err != nil {
   283  		return "", fmt.Errorf("failed to write container spec: %v", err)
   284  	}
   285  	cc.cleanup = append(cc.cleanup, cleanup)
   286  
   287  	return cc.startContainer(podID, image, sbSpecFile, contSpecFile)
   288  }
   289  
   290  func (cc *Crictl) startContainer(podID, image, sbSpecFile, contSpecFile string) (string, error) {
   291  	contID, err := cc.Create(podID, contSpecFile, sbSpecFile)
   292  	if err != nil {
   293  		return "", fmt.Errorf("failed to create container in pod %q: %v", podID, err)
   294  	}
   295  
   296  	if _, err := cc.Start(contID); err != nil {
   297  		return "", fmt.Errorf("failed to start container %q in pod %q: %v", contID, podID, err)
   298  	}
   299  
   300  	return contID, nil
   301  }
   302  
   303  // StopContainer stops and deletes the container with the given container ID.
   304  func (cc *Crictl) StopContainer(contID string) error {
   305  	if err := cc.Stop(contID); err != nil {
   306  		return fmt.Errorf("failed to stop container %q: %v", contID, err)
   307  	}
   308  
   309  	if err := cc.Rm(contID); err != nil {
   310  		return fmt.Errorf("failed to remove container %q: %v", contID, err)
   311  	}
   312  
   313  	return nil
   314  }
   315  
   316  // StartPodAndContainer starts a sandbox and container in that sandbox. It
   317  // returns the pod ID and container ID.
   318  func (cc *Crictl) StartPodAndContainer(runtime, image, sbSpec, contSpec string) (string, string, error) {
   319  	if err := cc.Import(image); err != nil {
   320  		return "", "", err
   321  	}
   322  
   323  	// Write the specs to files that can be read by crictl.
   324  	sbSpecFile, cleanup, err := testutil.WriteTmpFile("sbSpec", sbSpec)
   325  	if err != nil {
   326  		return "", "", fmt.Errorf("failed to write sandbox spec: %v", err)
   327  	}
   328  	cc.cleanup = append(cc.cleanup, cleanup)
   329  	contSpecFile, cleanup, err := testutil.WriteTmpFile("contSpec", contSpec)
   330  	if err != nil {
   331  		return "", "", fmt.Errorf("failed to write container spec: %v", err)
   332  	}
   333  	cc.cleanup = append(cc.cleanup, cleanup)
   334  
   335  	podID, err := cc.RunPod(runtime, sbSpecFile)
   336  	if err != nil {
   337  		return "", "", err
   338  	}
   339  
   340  	contID, err := cc.startContainer(podID, image, sbSpecFile, contSpecFile)
   341  
   342  	return podID, contID, err
   343  }
   344  
   345  // StopPodAndContainer stops a container and pod.
   346  func (cc *Crictl) StopPodAndContainer(podID, contID string) error {
   347  	if err := cc.StopContainer(contID); err != nil {
   348  		return fmt.Errorf("failed to stop container %q in pod %q: %v", contID, podID, err)
   349  	}
   350  
   351  	if err := cc.StopPod(podID); err != nil {
   352  		return fmt.Errorf("failed to stop pod %q: %v", podID, err)
   353  	}
   354  
   355  	if err := cc.RmPod(podID); err != nil {
   356  		return fmt.Errorf("failed to remove pod %q: %v", podID, err)
   357  	}
   358  
   359  	return nil
   360  }
   361  
   362  // run runs crictl with the given args.
   363  func (cc *Crictl) run(args ...string) (string, error) {
   364  	defaultArgs := []string{
   365  		ResolvePath("crictl"),
   366  		"--image-endpoint", fmt.Sprintf("unix://%s", cc.endpoint),
   367  		"--runtime-endpoint", fmt.Sprintf("unix://%s", cc.endpoint),
   368  	}
   369  	fullArgs := append(defaultArgs, args...)
   370  	out, err := testutil.Command(cc.logger, fullArgs...).CombinedOutput()
   371  	return string(out), err
   372  }