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(&register.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(&register.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  }