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 }