github.com/coreos/mantle@v0.13.0/kola/tests/crio/crio.go (about) 1 // Copyright 2018 Red Hat, Inc. 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 crio 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "io/ioutil" 21 "path" 22 "strings" 23 "time" 24 25 "golang.org/x/crypto/ssh" 26 "golang.org/x/net/context" 27 28 "github.com/coreos/mantle/kola/cluster" 29 "github.com/coreos/mantle/kola/register" 30 "github.com/coreos/mantle/kola/tests/util" 31 "github.com/coreos/mantle/lang/worker" 32 "github.com/coreos/mantle/platform" 33 "github.com/coreos/mantle/platform/conf" 34 ) 35 36 // simplifiedCrioInfo represents the results from crio info 37 type simplifiedCrioInfo struct { 38 StorageDriver string `json:"storage_driver"` 39 StorageRoot string `json:"storage_root"` 40 CgroupDriver string `json:"cgroup_driver"` 41 } 42 43 // crioPodTemplate is a simple string template required for creating a pod in crio 44 // It takes two strings: the name (which will be expanded) and the generated image name 45 var crioPodTemplate = `{ 46 "metadata": { 47 "name": "rhcos-crio-pod-%s", 48 "namespace": "redhat.test.crio" 49 }, 50 "image": { 51 "image": "localhost/%s:latest" 52 }, 53 "args": [], 54 "readonly_rootfs": false, 55 "log_path": "", 56 "stdin": false, 57 "stdin_once": false, 58 "tty": false, 59 "linux": { 60 "resources": { 61 "memory_limit_in_bytes": 209715200, 62 "cpu_period": 10000, 63 "cpu_quota": 20000, 64 "cpu_shares": 512, 65 "oom_score_adj": 30, 66 "cpuset_cpus": "0", 67 "cpuset_mems": "0" 68 }, 69 "cgroup_parent": "Burstable-pod-123.slice", 70 "security_context": { 71 "namespace_options": { 72 "pid": 1 73 }, 74 "capabilities": { 75 "add_capabilities": [ 76 "sys_admin" 77 ] 78 } 79 } 80 } 81 }` 82 83 // crioContainerTemplate is a simple string template required for running a container 84 // It takes three strings: the name (which will be expanded), the image, and the argument to run 85 var crioContainerTemplate = `{ 86 "metadata": { 87 "name": "rhcos-crio-container-%s", 88 "attempt": 1 89 }, 90 "image": { 91 "image": "localhost/%s:latest" 92 }, 93 "command": [ 94 "%s" 95 ], 96 "args": [], 97 "working_dir": "/", 98 "envs": [ 99 { 100 "key": "PATH", 101 "value": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 102 }, 103 { 104 "key": "TERM", 105 "value": "xterm" 106 } 107 ], 108 "labels": { 109 "type": "small", 110 "batch": "no" 111 }, 112 "annotations": { 113 "daemon": "crio" 114 }, 115 "privileged": true, 116 "log_path": "", 117 "stdin": false, 118 "stdin_once": false, 119 "tty": false, 120 "linux": { 121 "resources": { 122 "cpu_period": 10000, 123 "cpu_quota": 20000, 124 "cpu_shares": 512, 125 "oom_score_adj": 30, 126 "memory_limit_in_bytes": 268435456 127 }, 128 "security_context": { 129 "readonly_rootfs": false, 130 "selinux_options": { 131 "user": "system_u", 132 "role": "system_r", 133 "type": "svirt_lxc_net_t", 134 "level": "s0:c4,c5" 135 }, 136 "capabilities": { 137 "add_capabilities": [ 138 "setuid", 139 "setgid" 140 ], 141 "drop_capabilities": [ 142 ] 143 } 144 } 145 } 146 }` 147 148 // RHCOS has the crio service disabled by default, so use Ignition to enable it 149 var enableCrioIgn = conf.Ignition(`{ 150 "ignition": { 151 "version": "2.2.0" 152 }, 153 "systemd": { 154 "units": [ 155 { 156 "enabled": true, 157 "name": "crio.service" 158 } 159 ] 160 } 161 }`) 162 163 var enableCrioIgnV3 = conf.Ignition(`{ 164 "ignition": { 165 "version": "3.0.0" 166 }, 167 "systemd": { 168 "units": [ 169 { 170 "enabled": true, 171 "name": "crio.service" 172 } 173 ] 174 } 175 }`) 176 177 // init runs when the package is imported and takes care of registering tests 178 func init() { 179 register.Register(®ister.Test{ 180 Run: crioBaseTests, 181 ClusterSize: 1, 182 Name: `crio.base`, 183 // crio pods require fetching a kubernetes pause image 184 Flags: []register.Flag{register.RequiresInternetAccess}, 185 Distros: []string{"rhcos"}, 186 UserData: enableCrioIgn, 187 UserDataV3: enableCrioIgnV3, 188 }) 189 register.Register(®ister.Test{ 190 Run: crioNetwork, 191 ClusterSize: 2, 192 Name: "crio.network", 193 Flags: []register.Flag{register.RequiresInternetAccess}, 194 Distros: []string{"rhcos"}, 195 UserData: enableCrioIgn, 196 UserDataV3: enableCrioIgnV3, 197 }) 198 } 199 200 // crioBaseTests executes multiple tests under the "base" name 201 func crioBaseTests(c cluster.TestCluster) { 202 c.Run("crio-info", testCrioInfo) 203 c.Run("networks-reliably", crioNetworksReliably) 204 } 205 206 // generateCrioConfig generates a crio pod/container configuration 207 // based on the input name and arguments returning the path to the generated configs. 208 func generateCrioConfig(podName, imageName string, command []string) (string, string, error) { 209 fileContentsPod := fmt.Sprintf(crioPodTemplate, podName, imageName) 210 211 tmpFilePod, err := ioutil.TempFile("", podName+"Pod") 212 if err != nil { 213 return "", "", err 214 } 215 defer tmpFilePod.Close() 216 if _, err = tmpFilePod.Write([]byte(fileContentsPod)); err != nil { 217 return "", "", err 218 } 219 cmd := strings.Join(command, " ") 220 fileContentsContainer := fmt.Sprintf(crioContainerTemplate, imageName, imageName, cmd) 221 222 tmpFileContainer, err := ioutil.TempFile("", imageName+"Container") 223 if err != nil { 224 return "", "", err 225 } 226 defer tmpFileContainer.Close() 227 if _, err = tmpFileContainer.Write([]byte(fileContentsContainer)); err != nil { 228 return "", "", err 229 } 230 231 return tmpFilePod.Name(), tmpFileContainer.Name(), nil 232 } 233 234 // genContainer makes a container out of binaries on the host. This function uses podman to build. 235 // The first string returned by this function is the pod config to be used with crictl runp. The second 236 // string returned is the container config to be used with crictl create/exec. They will be dropped 237 // on to all machines in the cluster as ~/$STRING_RETURNED_FROM_FUNCTION. Note that the string returned 238 // here is just the name, not the full path on the cluster machine(s). 239 func genContainer(c cluster.TestCluster, m platform.Machine, podName, imageName string, binnames []string, shellCommands []string) (string, string, error) { 240 configPathPod, configPathContainer, err := generateCrioConfig(podName, imageName, shellCommands) 241 if err != nil { 242 return "", "", err 243 } 244 if err = c.DropFile(configPathPod); err != nil { 245 return "", "", err 246 } 247 if err = c.DropFile(configPathContainer); err != nil { 248 return "", "", err 249 } 250 251 // Create the crio image used for testing, only if it doesn't exist already 252 output := c.MustSSH(m, "sudo podman images -n --format '{{.Repository}}'") 253 if !strings.Contains(string(output), "localhost/"+imageName) { 254 util.GenPodmanScratchContainer(c, m, imageName, binnames) 255 } 256 257 return path.Base(configPathPod), path.Base(configPathContainer), nil 258 } 259 260 // crioNetwork ensures that crio containers can make network connections outside of the host 261 func crioNetwork(c cluster.TestCluster) { 262 machines := c.Machines() 263 src, dest := machines[0], machines[1] 264 265 c.Log("creating ncat containers") 266 267 // Since genContainer also generates crio pod/container configs, 268 // there will be a duplicate config file on each machine. 269 // Thus we only save one set for later use. 270 crioConfigPod, crioConfigContainer, err := genContainer(c, src, "ncat", "ncat", []string{"ncat", "echo"}, []string{"ncat"}) 271 if err != nil { 272 c.Fatal(err) 273 } 274 _, _, err = genContainer(c, dest, "ncat", "ncat", []string{"ncat", "echo"}, []string{"ncat"}) 275 if err != nil { 276 c.Fatal(err) 277 } 278 279 listener := func(ctx context.Context) error { 280 podID, err := c.SSH(dest, fmt.Sprintf("sudo crictl runp %s", crioConfigPod)) 281 if err != nil { 282 return err 283 } 284 285 containerID, err := c.SSH(dest, fmt.Sprintf("sudo crictl create %s %s %s", 286 podID, crioConfigContainer, crioConfigPod)) 287 if err != nil { 288 return err 289 } 290 291 // This command will block until a message is recieved 292 output, err := c.SSH(dest, fmt.Sprintf("sudo timeout 30 crictl exec %s echo 'HELLO FROM SERVER' | timeout 20 ncat --listen 0.0.0.0 9988 || echo 'LISTENER TIMEOUT'", containerID)) 293 if err != nil { 294 return err 295 } 296 if string(output) != "HELLO FROM CLIENT" { 297 return fmt.Errorf("unexpected result from listener: %s", output) 298 } 299 300 return nil 301 } 302 303 talker := func(ctx context.Context) error { 304 // Wait until listener is ready before trying anything 305 for { 306 _, err := c.SSH(dest, "sudo netstat -tulpn|grep 9988") 307 if err == nil { 308 break // socket is ready 309 } 310 311 exit, ok := err.(*ssh.ExitError) 312 if !ok || exit.Waitmsg.ExitStatus() != 1 { // 1 is the expected exit of grep -q 313 return err 314 } 315 316 select { 317 case <-ctx.Done(): 318 return fmt.Errorf("timeout waiting for server") 319 default: 320 time.Sleep(100 * time.Millisecond) 321 } 322 } 323 podID, err := c.SSH(src, fmt.Sprintf("sudo crictl runp %s", crioConfigPod)) 324 if err != nil { 325 return err 326 } 327 328 containerID, err := c.SSH(src, fmt.Sprintf("sudo crictl create %s %s %s", 329 podID, crioConfigContainer, crioConfigPod)) 330 if err != nil { 331 return err 332 } 333 334 output, err := c.SSH(src, fmt.Sprintf("sudo crictl exec %s echo 'HELLO FROM CLIENT' | ncat %s 9988", 335 containerID, dest.PrivateIP())) 336 if err != nil { 337 return err 338 } 339 if string(output) != "HELLO FROM SERVER" { 340 return fmt.Errorf(`unexpected result from listener: "%s"`, output) 341 } 342 343 return nil 344 } 345 346 ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 347 defer cancel() 348 349 if err := worker.Parallel(ctx, listener, talker); err != nil { 350 c.Fatal(err) 351 } 352 } 353 354 // crioNetworksReliably verifies that crio containers have a reliable network 355 func crioNetworksReliably(c cluster.TestCluster) { 356 m := c.Machines()[0] 357 358 // Here we generate 10 pods, each will run a container responsible for 359 // pinging to host 360 output := "" 361 for x := 1; x <= 10; x++ { 362 // append int to name to avoid pod name collision 363 crioConfigPod, crioConfigContainer, err := genContainer( 364 c, m, fmt.Sprintf("ping%d", x), "ping", []string{"ping"}, 365 []string{"ping"}) 366 if err != nil { 367 c.Fatal(err) 368 } 369 cmdCreatePod := fmt.Sprintf("sudo crictl runp %s", crioConfigPod) 370 podID := c.MustSSH(m, cmdCreatePod) 371 containerID := c.MustSSH(m, fmt.Sprintf("sudo crictl create %s %s %s", 372 podID, crioConfigContainer, crioConfigPod)) 373 output = output + string(c.MustSSH(m, fmt.Sprintf("sudo crictl exec %s ping -i 0.2 10.88.0.1 -w 1 >/dev/null && echo PASS || echo FAIL", containerID))) 374 } 375 376 numPass := strings.Count(string(output), "PASS") 377 if numPass != 10 { 378 c.Fatalf("Expected 10 passes, but received %d passes with output: %s", numPass, output) 379 } 380 381 } 382 383 // getCrioInfo parses and returns the information crio provides via socket 384 func getCrioInfo(c cluster.TestCluster, m platform.Machine) (simplifiedCrioInfo, error) { 385 target := simplifiedCrioInfo{} 386 crioInfoJSON, err := c.SSH(m, `sudo curl -s --unix-socket /var/run/crio/crio.sock http://crio/info`) 387 if err != nil { 388 return target, fmt.Errorf("could not get info: %v", err) 389 } 390 391 err = json.Unmarshal(crioInfoJSON, &target) 392 if err != nil { 393 return target, fmt.Errorf("could not unmarshal info %q into known json: %v", string(crioInfoJSON), err) 394 } 395 return target, nil 396 } 397 398 // testCrioInfo test that crio info's output is as expected. 399 func testCrioInfo(c cluster.TestCluster) { 400 m := c.Machines()[0] 401 info, err := getCrioInfo(c, m) 402 if err != nil { 403 c.Fatal(err) 404 } 405 expectedStorageDriver := "overlay" 406 if info.StorageDriver != expectedStorageDriver { 407 c.Errorf("unexpected storage driver: %v != %v", expectedStorageDriver, info.StorageDriver) 408 } 409 expectedStorageRoot := "/var/lib/containers/storage" 410 if info.StorageRoot != expectedStorageRoot { 411 c.Errorf("unexpected storage root: %v != %v", expectedStorageRoot, info.StorageRoot) 412 } 413 expectedCgroupDriver := "systemd" 414 if info.CgroupDriver != expectedCgroupDriver { 415 c.Errorf("unexpected cgroup driver: %v != %v", expectedCgroupDriver, info.CgroupDriver) 416 } 417 418 }