github.com/google/cadvisor@v0.49.1/integration/framework/framework.go (about)

     1  // Copyright 2014 Google Inc. All Rights Reserved.
     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 framework
    16  
    17  import (
    18  	"bytes"
    19  	"flag"
    20  	"fmt"
    21  	"os/exec"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"k8s.io/klog/v2"
    27  
    28  	"github.com/google/cadvisor/client"
    29  	v2 "github.com/google/cadvisor/client/v2"
    30  )
    31  
    32  var host = flag.String("host", "localhost", "Address of the host being tested")
    33  var port = flag.Int("port", 8080, "Port of the application on the host being tested")
    34  var sshOptions = flag.String("ssh-options", "", "Command line options for ssh")
    35  
    36  // Integration test framework.
    37  type Framework interface {
    38  	// Clean the framework state.
    39  	Cleanup()
    40  
    41  	// The testing.T used by the framework and the current test.
    42  	T() *testing.T
    43  
    44  	// Returns the hostname being tested.
    45  	Hostname() HostnameInfo
    46  
    47  	// Returns the Docker actions for the test framework.
    48  	Docker() DockerActions
    49  
    50  	// Returns the shell actions for the test framework.
    51  	Shell() ShellActions
    52  
    53  	// Returns the cAdvisor actions for the test framework.
    54  	Cadvisor() CadvisorActions
    55  }
    56  
    57  // Instantiates a Framework. Cleanup *must* be called. Class is thread-compatible.
    58  // All framework actions report fatal errors on the t specified at creation time.
    59  //
    60  // Typical use:
    61  //
    62  //	func TestFoo(t *testing.T) {
    63  //		fm := framework.New(t)
    64  //		defer fm.Cleanup()
    65  //	     ... actual test ...
    66  //	}
    67  func New(t *testing.T) Framework {
    68  	// All integration tests are large.
    69  	if testing.Short() {
    70  		t.Skip("Skipping framework test in short mode")
    71  	}
    72  
    73  	// Try to see if non-localhost hosts are GCE instances.
    74  	fm := &realFramework{
    75  		hostname: HostnameInfo{
    76  			Host: *host,
    77  			Port: *port,
    78  		},
    79  		t:        t,
    80  		cleanups: make([]func(), 0),
    81  	}
    82  	fm.shellActions = shellActions{
    83  		fm: fm,
    84  	}
    85  	fm.dockerActions = dockerActions{
    86  		fm: fm,
    87  	}
    88  
    89  	return fm
    90  }
    91  
    92  const (
    93  	Aufs         string = "aufs"
    94  	Overlay      string = "overlay"
    95  	Overlay2     string = "overlay2"
    96  	DeviceMapper string = "devicemapper"
    97  	Unknown      string = ""
    98  )
    99  
   100  type DockerActions interface {
   101  	// Run the no-op pause Docker container and return its ID.
   102  	RunPause() string
   103  
   104  	// Run the specified command in a Docker busybox container and return its ID.
   105  	RunBusybox(cmd ...string) string
   106  
   107  	// Runs a Docker container in the background. Uses the specified DockerRunArgs and command.
   108  	// Returns the ID of the new container.
   109  	//
   110  	// e.g.:
   111  	// Run(DockerRunArgs{Image: "busybox"}, "ping", "www.google.com")
   112  	//   -> docker run busybox ping www.google.com
   113  	Run(args DockerRunArgs, cmd ...string) string
   114  	RunStress(args DockerRunArgs, cmd ...string) string
   115  
   116  	Version() []string
   117  	StorageDriver() string
   118  }
   119  
   120  type ShellActions interface {
   121  	// Runs a specified command and arguments. Returns the stdout and stderr.
   122  	Run(cmd string, args ...string) (string, string)
   123  	RunStress(cmd string, args ...string) (string, string)
   124  }
   125  
   126  type CadvisorActions interface {
   127  	// Returns a cAdvisor client to the machine being tested.
   128  	Client() *client.Client
   129  	ClientV2() *v2.Client
   130  }
   131  
   132  type realFramework struct {
   133  	hostname         HostnameInfo
   134  	t                *testing.T
   135  	cadvisorClient   *client.Client
   136  	cadvisorClientV2 *v2.Client
   137  
   138  	shellActions  shellActions
   139  	dockerActions dockerActions
   140  
   141  	// Cleanup functions to call on Cleanup()
   142  	cleanups []func()
   143  }
   144  
   145  type shellActions struct {
   146  	fm *realFramework
   147  }
   148  
   149  type dockerActions struct {
   150  	fm *realFramework
   151  }
   152  
   153  type HostnameInfo struct {
   154  	Host string
   155  	Port int
   156  }
   157  
   158  // Returns: http://<host>:<port>/
   159  func (h HostnameInfo) FullHostname() string {
   160  	return fmt.Sprintf("http://%s:%d/", h.Host, h.Port)
   161  }
   162  
   163  func (f *realFramework) T() *testing.T {
   164  	return f.t
   165  }
   166  
   167  func (f *realFramework) Hostname() HostnameInfo {
   168  	return f.hostname
   169  }
   170  
   171  func (f *realFramework) Shell() ShellActions {
   172  	return f.shellActions
   173  }
   174  
   175  func (f *realFramework) Docker() DockerActions {
   176  	return f.dockerActions
   177  }
   178  
   179  func (f *realFramework) Cadvisor() CadvisorActions {
   180  	return f
   181  }
   182  
   183  // Call all cleanup functions.
   184  func (f *realFramework) Cleanup() {
   185  	for _, cleanupFunc := range f.cleanups {
   186  		cleanupFunc()
   187  	}
   188  }
   189  
   190  // Gets a client to the cAdvisor being tested.
   191  func (f *realFramework) Client() *client.Client {
   192  	if f.cadvisorClient == nil {
   193  		cadvisorClient, err := client.NewClient(f.Hostname().FullHostname())
   194  		if err != nil {
   195  			f.t.Fatalf("Failed to instantiate the cAdvisor client: %v", err)
   196  		}
   197  		f.cadvisorClient = cadvisorClient
   198  	}
   199  	return f.cadvisorClient
   200  }
   201  
   202  // Gets a v2 client to the cAdvisor being tested.
   203  func (f *realFramework) ClientV2() *v2.Client {
   204  	if f.cadvisorClientV2 == nil {
   205  		cadvisorClientV2, err := v2.NewClient(f.Hostname().FullHostname())
   206  		if err != nil {
   207  			f.t.Fatalf("Failed to instantiate the cAdvisor client: %v", err)
   208  		}
   209  		f.cadvisorClientV2 = cadvisorClientV2
   210  	}
   211  	return f.cadvisorClientV2
   212  }
   213  
   214  func (a dockerActions) RunPause() string {
   215  	return a.Run(DockerRunArgs{
   216  		Image: "registry.k8s.io/pause",
   217  	})
   218  }
   219  
   220  // Run the specified command in a Docker busybox container.
   221  func (a dockerActions) RunBusybox(cmd ...string) string {
   222  	return a.Run(DockerRunArgs{
   223  		Image: "registry.k8s.io/busybox",
   224  	}, cmd...)
   225  }
   226  
   227  type DockerRunArgs struct {
   228  	// Image to use.
   229  	Image string
   230  
   231  	// Arguments to the Docker CLI.
   232  	Args []string
   233  
   234  	InnerArgs []string
   235  }
   236  
   237  // TODO(vmarmol): Use the Docker remote API.
   238  // TODO(vmarmol): Refactor a set of "RunCommand" actions.
   239  // Runs a Docker container in the background. Uses the specified DockerRunArgs and command.
   240  //
   241  // e.g.:
   242  // RunDockerContainer(DockerRunArgs{Image: "busybox"}, "ping", "www.google.com")
   243  //
   244  //	-> docker run busybox ping www.google.com
   245  func (a dockerActions) Run(args DockerRunArgs, cmd ...string) string {
   246  	dockerCommand := append(append([]string{"docker", "run", "-d"}, args.Args...), args.Image)
   247  	dockerCommand = append(dockerCommand, cmd...)
   248  	output, _ := a.fm.Shell().Run("sudo", dockerCommand...)
   249  
   250  	// The last line is the container ID.
   251  	elements := strings.Fields(output)
   252  	containerID := elements[len(elements)-1]
   253  
   254  	a.fm.cleanups = append(a.fm.cleanups, func() {
   255  		a.fm.Shell().Run("sudo", "docker", "rm", "-f", containerID)
   256  	})
   257  	return containerID
   258  }
   259  func (a dockerActions) Version() []string {
   260  	dockerCommand := []string{"docker", "version", "-f", "'{{.Server.Version}}'"}
   261  	output, _ := a.fm.Shell().Run("sudo", dockerCommand...)
   262  	output = strings.TrimSpace(output)
   263  	ret := strings.Split(output, ".")
   264  	if len(ret) != 3 {
   265  		a.fm.T().Fatalf("invalid version %v", output)
   266  	}
   267  	return ret
   268  }
   269  
   270  func (a dockerActions) StorageDriver() string {
   271  	dockerCommand := []string{"docker", "info"}
   272  	output, _ := a.fm.Shell().Run("sudo", dockerCommand...)
   273  	if len(output) < 1 {
   274  		a.fm.T().Fatalf("failed to find docker storage driver - %v", output)
   275  	}
   276  	for _, line := range strings.Split(output, "\n") {
   277  		line = strings.TrimSpace(line)
   278  		if strings.HasPrefix(line, "Storage Driver: ") {
   279  			idx := strings.LastIndex(line, ": ") + 2
   280  			driver := line[idx:]
   281  			switch driver {
   282  			case Aufs, Overlay, Overlay2, DeviceMapper:
   283  				return driver
   284  			default:
   285  				return Unknown
   286  			}
   287  		}
   288  	}
   289  	a.fm.T().Fatalf("failed to find docker storage driver from info - %v", output)
   290  	return Unknown
   291  }
   292  
   293  func (a dockerActions) RunStress(args DockerRunArgs, cmd ...string) string {
   294  	dockerCommand := append(append(append(append([]string{"docker", "run", "-m=4M", "-d", "-t", "-i"}, args.Args...), args.Image), args.InnerArgs...), cmd...)
   295  
   296  	output, _ := a.fm.Shell().RunStress("sudo", dockerCommand...)
   297  
   298  	// The last line is the container ID.
   299  	if len(output) < 1 {
   300  		a.fm.T().Fatalf("need 1 arguments in output %v to get the name but have %v", output, len(output))
   301  	}
   302  	elements := strings.Fields(output)
   303  	containerID := elements[len(elements)-1]
   304  
   305  	a.fm.cleanups = append(a.fm.cleanups, func() {
   306  		a.fm.Shell().Run("sudo", "docker", "rm", "-f", containerID)
   307  	})
   308  	return containerID
   309  }
   310  
   311  func (a shellActions) wrapSSH(command string, args ...string) *exec.Cmd {
   312  	cmd := []string{a.fm.Hostname().Host, "--", "sh", "-c", "\"", command}
   313  	cmd = append(cmd, args...)
   314  	cmd = append(cmd, "\"")
   315  	if *sshOptions != "" {
   316  		cmd = append(strings.Split(*sshOptions, " "), cmd...)
   317  	}
   318  	return exec.Command("ssh", cmd...)
   319  }
   320  
   321  func (a shellActions) Run(command string, args ...string) (string, string) {
   322  	var cmd *exec.Cmd
   323  	if a.fm.Hostname().Host == "localhost" {
   324  		// Just run locally.
   325  		cmd = exec.Command(command, args...)
   326  	} else {
   327  		// We must SSH to the remote machine and run the command.
   328  		cmd = a.wrapSSH(command, args...)
   329  	}
   330  	var stdout bytes.Buffer
   331  	var stderr bytes.Buffer
   332  	cmd.Stdout = &stdout
   333  	cmd.Stderr = &stderr
   334  	klog.Infof("About to run - %v", cmd.Args)
   335  	err := cmd.Run()
   336  	if err != nil {
   337  		a.fm.T().Fatalf("Failed to run %q %v in %q with error: %q. Stdout: %q, Stderr: %s", command, args, a.fm.Hostname().Host, err, stdout.String(), stderr.String())
   338  		return "", ""
   339  	}
   340  	return stdout.String(), stderr.String()
   341  }
   342  
   343  func (a shellActions) RunStress(command string, args ...string) (string, string) {
   344  	var cmd *exec.Cmd
   345  	if a.fm.Hostname().Host == "localhost" {
   346  		// Just run locally.
   347  		cmd = exec.Command(command, args...)
   348  	} else {
   349  		// We must SSH to the remote machine and run the command.
   350  		cmd = a.wrapSSH(command, args...)
   351  	}
   352  	var stdout bytes.Buffer
   353  	var stderr bytes.Buffer
   354  	cmd.Stdout = &stdout
   355  	cmd.Stderr = &stderr
   356  	err := cmd.Run()
   357  	if err != nil {
   358  		a.fm.T().Logf("Ran %q %v in %q and received error: %q. Stdout: %q, Stderr: %s", command, args, a.fm.Hostname().Host, err, stdout.String(), stderr.String())
   359  		return stdout.String(), stderr.String()
   360  	}
   361  	return stdout.String(), stderr.String()
   362  }
   363  
   364  // Runs retryFunc until no error is returned. After dur time the last error is returned.
   365  // Note that the function does not timeout the execution of retryFunc when the limit is reached.
   366  func RetryForDuration(retryFunc func() error, dur time.Duration) error {
   367  	waitUntil := time.Now().Add(dur)
   368  	var err error
   369  	for time.Now().Before(waitUntil) {
   370  		err = retryFunc()
   371  		if err == nil {
   372  			return nil
   373  		}
   374  	}
   375  	return err
   376  }