github.com/opencontainers/runtime-tools@v0.9.0/validation/util/test.go (about)

     1  package util
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"runtime"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/mndrix/tap-go"
    15  	"github.com/mrunalp/fileutils"
    16  	rspec "github.com/opencontainers/runtime-spec/specs-go"
    17  	"github.com/opencontainers/runtime-tools/generate"
    18  	"github.com/opencontainers/runtime-tools/specerror"
    19  	"github.com/satori/go.uuid"
    20  )
    21  
    22  var (
    23  	// RuntimeCommand is the default runtime command.
    24  	RuntimeCommand = "runc"
    25  )
    26  
    27  // LifecycleAction defines the phases will be called.
    28  type LifecycleAction int
    29  
    30  const (
    31  	// LifecycleActionNone does nothing
    32  	LifecycleActionNone = 0
    33  	// LifecycleActionCreate creates a container
    34  	LifecycleActionCreate = 1 << iota
    35  	// LifecycleActionStart starts a container
    36  	LifecycleActionStart
    37  	// LifecycleActionDelete deletes a container
    38  	LifecycleActionDelete
    39  )
    40  
    41  // LifecycleStatus follows https://github.com/opencontainers/runtime-spec/blob/master/runtime.md#state
    42  type LifecycleStatus int
    43  
    44  const (
    45  	// LifecycleStatusCreating "creating"
    46  	LifecycleStatusCreating = 1 << iota
    47  	// LifecycleStatusCreated "created"
    48  	LifecycleStatusCreated
    49  	// LifecycleStatusRunning "running"
    50  	LifecycleStatusRunning
    51  	// LifecycleStatusStopped "stopped"
    52  	LifecycleStatusStopped
    53  )
    54  
    55  var lifecycleStatusMap = map[string]LifecycleStatus{
    56  	"creating": LifecycleStatusCreating,
    57  	"created":  LifecycleStatusCreated,
    58  	"running":  LifecycleStatusRunning,
    59  	"stopped":  LifecycleStatusStopped,
    60  }
    61  
    62  // LifecycleConfig includes
    63  // 1. Config to set the 'config.json'
    64  // 2. BundleDir to set the bundle directory
    65  // 3. Actions to define the default running lifecycles
    66  // 4. Four phases for user to add his/her own operations
    67  type LifecycleConfig struct {
    68  	Config     *generate.Generator
    69  	BundleDir  string
    70  	Actions    LifecycleAction
    71  	PreCreate  func(runtime *Runtime) error
    72  	PostCreate func(runtime *Runtime) error
    73  	PreDelete  func(runtime *Runtime) error
    74  	PostDelete func(runtime *Runtime) error
    75  }
    76  
    77  // PreFunc initializes the test environment after preparing the bundle
    78  // but before creating the container.
    79  type PreFunc func(string) error
    80  
    81  // AfterFunc validate container's outside environment after created
    82  type AfterFunc func(config *rspec.Spec, t *tap.T, state *rspec.State) error
    83  
    84  func init() {
    85  	runtimeInEnv := os.Getenv("RUNTIME")
    86  	if runtimeInEnv != "" {
    87  		RuntimeCommand = runtimeInEnv
    88  	}
    89  }
    90  
    91  // Fatal prints a warning to stderr and exits.
    92  func Fatal(err error) {
    93  	fmt.Fprintf(os.Stderr, "%+v\n", err)
    94  	os.Exit(1)
    95  }
    96  
    97  // Skip skips a full TAP suite.
    98  func Skip(message string, diagnostic interface{}) {
    99  	t := tap.New()
   100  	t.Header(1)
   101  	t.Skip(1, message)
   102  	if diagnostic != nil {
   103  		t.YAML(diagnostic)
   104  	}
   105  }
   106  
   107  // SpecErrorOK generates TAP output indicating whether a spec code test passed or failed.
   108  func SpecErrorOK(t *tap.T, expected bool, specErr error, detailedErr error) {
   109  	t.Ok(expected, specErr.(*specerror.Error).Err.Err.Error())
   110  	diagnostic := map[string]string{
   111  		"reference": specErr.(*specerror.Error).Err.Reference,
   112  	}
   113  
   114  	if detailedErr != nil {
   115  		diagnostic["error"] = detailedErr.Error()
   116  		if e, ok := detailedErr.(*exec.ExitError); ok {
   117  			if len(e.Stderr) > 0 {
   118  				diagnostic["stderr"] = string(e.Stderr)
   119  			}
   120  		}
   121  	}
   122  	t.YAML(diagnostic)
   123  }
   124  
   125  // PrepareBundle creates a test bundle in a temporary directory.
   126  func PrepareBundle() (string, error) {
   127  	bundleDir, err := ioutil.TempDir("", "ocitest")
   128  	if err != nil {
   129  		return "", err
   130  	}
   131  
   132  	// Untar the root fs
   133  	untarCmd := exec.Command("tar", "-xf", fmt.Sprintf("rootfs-%s.tar.gz", runtime.GOARCH), "-C", bundleDir)
   134  	output, err := untarCmd.CombinedOutput()
   135  	if err != nil {
   136  		os.Stderr.Write(output)
   137  		os.RemoveAll(bundleDir)
   138  		return "", err
   139  	}
   140  
   141  	return bundleDir, nil
   142  }
   143  
   144  // GetDefaultGenerator creates a default configuration generator.
   145  func GetDefaultGenerator() (*generate.Generator, error) {
   146  	g, err := generate.New(runtime.GOOS)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	g.SetRootPath(".")
   151  	g.SetProcessArgs([]string{"/runtimetest", "--path=/"})
   152  	return &g, err
   153  }
   154  
   155  // WaitingForStatus waits an expected runtime status, return error if
   156  // 1. fail to query the status
   157  // 2. timeout
   158  func WaitingForStatus(r Runtime, status LifecycleStatus, retryTimeout time.Duration, pollInterval time.Duration) error {
   159  	for start := time.Now(); time.Since(start) < retryTimeout; time.Sleep(pollInterval) {
   160  		state, err := r.State()
   161  		if err != nil {
   162  			return err
   163  		}
   164  		if v, ok := lifecycleStatusMap[state.Status]; ok {
   165  			if status&v != 0 {
   166  				return nil
   167  			}
   168  		} else {
   169  			// In spec, it says 'Additional values MAY be defined by the runtime'.
   170  			continue
   171  		}
   172  	}
   173  
   174  	return errors.New("timeout in waiting for the container status")
   175  }
   176  
   177  var runtimeInsideValidateCalled bool
   178  
   179  // RuntimeInsideValidate runs runtimetest inside a container.
   180  func RuntimeInsideValidate(g *generate.Generator, t *tap.T, f PreFunc) (err error) {
   181  	bundleDir, err := PrepareBundle()
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	if f != nil {
   187  		if err := f(bundleDir); err != nil {
   188  			return err
   189  		}
   190  	}
   191  
   192  	r, err := NewRuntime(RuntimeCommand, bundleDir)
   193  	if err != nil {
   194  		os.RemoveAll(bundleDir)
   195  		return err
   196  	}
   197  	defer r.Clean(true, true)
   198  	err = r.SetConfig(g)
   199  	if err != nil {
   200  		return err
   201  	}
   202  	err = fileutils.CopyFile("runtimetest", filepath.Join(r.BundleDir, "runtimetest"))
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	r.SetID(uuid.NewV4().String())
   208  	err = r.Create()
   209  	if err != nil {
   210  		os.Stderr.WriteString("failed to create the container\n")
   211  		if e, ok := err.(*exec.ExitError); ok && len(e.Stderr) > 0 {
   212  			os.Stderr.Write(e.Stderr)
   213  		}
   214  		return err
   215  	}
   216  
   217  	err = r.Start()
   218  	if err != nil {
   219  		os.Stderr.WriteString("failed to start the container\n")
   220  		if e, ok := err.(*exec.ExitError); ok && len(e.Stderr) > 0 {
   221  			os.Stderr.Write(e.Stderr)
   222  		}
   223  		return err
   224  	}
   225  
   226  	err = WaitingForStatus(r, LifecycleStatusStopped, 10*time.Second, 1*time.Second)
   227  	if err != nil {
   228  		return err
   229  	}
   230  
   231  	stdout, stderr, err := r.ReadStandardStreams()
   232  	if err != nil {
   233  		if len(stderr) == 0 {
   234  			stderr = stdout
   235  		}
   236  		os.Stderr.WriteString("failed to read standard streams\n")
   237  		os.Stderr.Write(stderr)
   238  		return err
   239  	}
   240  
   241  	// Write stdout in the outter TAP
   242  	if t != nil {
   243  		diagnostic := map[string]string{
   244  			"stdout": string(stdout),
   245  			"stderr": string(stderr),
   246  		}
   247  		if err != nil {
   248  			diagnostic["error"] = fmt.Sprintf("%v", err)
   249  		}
   250  		t.YAML(diagnostic)
   251  		t.Ok(err == nil && !strings.Contains(string(stdout), "not ok"), g.Config.Annotations["TestName"])
   252  	} else {
   253  		if runtimeInsideValidateCalled {
   254  			Fatal(errors.New("RuntimeInsideValidate called several times in the same test without passing TAP"))
   255  		}
   256  		runtimeInsideValidateCalled = true
   257  		os.Stdout.Write(stdout)
   258  	}
   259  	return nil
   260  }
   261  
   262  // RuntimeOutsideValidate validate runtime outside a container.
   263  func RuntimeOutsideValidate(g *generate.Generator, t *tap.T, f AfterFunc) error {
   264  	bundleDir, err := PrepareBundle()
   265  	if err != nil {
   266  		return err
   267  	}
   268  
   269  	r, err := NewRuntime(RuntimeCommand, bundleDir)
   270  	if err != nil {
   271  		os.RemoveAll(bundleDir)
   272  		return err
   273  	}
   274  	defer r.Clean(true, true)
   275  	err = r.SetConfig(g)
   276  	if err != nil {
   277  		return err
   278  	}
   279  	err = fileutils.CopyFile("runtimetest", filepath.Join(r.BundleDir, "runtimetest"))
   280  	if err != nil {
   281  		return err
   282  	}
   283  
   284  	r.SetID(uuid.NewV4().String())
   285  	err = r.Create()
   286  	if err != nil {
   287  		os.Stderr.WriteString("failed to create the container\n")
   288  		if e, ok := err.(*exec.ExitError); ok && len(e.Stderr) > 0 {
   289  			os.Stderr.Write(e.Stderr)
   290  		}
   291  		return err
   292  	}
   293  
   294  	if f != nil {
   295  		state, err := r.State()
   296  		if err != nil {
   297  			return err
   298  		}
   299  		if err := f(g.Spec(), t, &state); err != nil {
   300  			return err
   301  		}
   302  	}
   303  	return nil
   304  }
   305  
   306  // RuntimeLifecycleValidate validates runtime lifecycle.
   307  func RuntimeLifecycleValidate(config LifecycleConfig) error {
   308  	var bundleDir string
   309  	var err error
   310  
   311  	if config.BundleDir == "" {
   312  		bundleDir, err = PrepareBundle()
   313  		if err != nil {
   314  			return err
   315  		}
   316  		defer os.RemoveAll(bundleDir)
   317  	} else {
   318  		bundleDir = config.BundleDir
   319  	}
   320  
   321  	r, err := NewRuntime(RuntimeCommand, bundleDir)
   322  	if err != nil {
   323  		return err
   324  	}
   325  
   326  	if config.Config != nil {
   327  		if err := r.SetConfig(config.Config); err != nil {
   328  			return err
   329  		}
   330  	}
   331  
   332  	if config.PreCreate != nil {
   333  		if err := config.PreCreate(&r); err != nil {
   334  			return err
   335  		}
   336  	}
   337  
   338  	if config.Actions&LifecycleActionCreate != 0 {
   339  		err := r.Create()
   340  		if err != nil {
   341  			os.Stderr.WriteString("failed to create the container\n")
   342  			if e, ok := err.(*exec.ExitError); ok && len(e.Stderr) > 0 {
   343  				os.Stderr.Write(e.Stderr)
   344  			}
   345  			return err
   346  		}
   347  		if config.Actions&LifecycleActionDelete != 0 {
   348  			defer func() {
   349  				// runtime error or the container is already deleted
   350  				if _, err := r.State(); err != nil {
   351  					return
   352  				}
   353  				r.Kill("KILL")
   354  				err := WaitingForStatus(r, LifecycleStatusStopped, time.Second*10, time.Second*1)
   355  				if err == nil {
   356  					r.Delete()
   357  				} else {
   358  					os.Stderr.WriteString("failed to delete the container\n")
   359  					os.Stderr.WriteString(err.Error())
   360  				}
   361  			}()
   362  		}
   363  	}
   364  
   365  	if config.PostCreate != nil {
   366  		if err := config.PostCreate(&r); err != nil {
   367  			return err
   368  		}
   369  	}
   370  
   371  	if config.Actions&LifecycleActionStart != 0 {
   372  		err := r.Start()
   373  		if err != nil {
   374  			os.Stderr.WriteString("failed to start the container\n")
   375  			if e, ok := err.(*exec.ExitError); ok && len(e.Stderr) > 0 {
   376  				os.Stderr.Write(e.Stderr)
   377  			}
   378  			return err
   379  		}
   380  	}
   381  
   382  	if config.PreDelete != nil {
   383  		if err := config.PreDelete(&r); err != nil {
   384  			return err
   385  		}
   386  	}
   387  
   388  	if config.Actions&LifecycleActionDelete != 0 {
   389  		err := r.Delete()
   390  		if err != nil {
   391  			os.Stderr.WriteString("failed to delete the container\n")
   392  			if e, ok := err.(*exec.ExitError); ok && len(e.Stderr) > 0 {
   393  				os.Stderr.Write(e.Stderr)
   394  			}
   395  			return err
   396  		}
   397  	}
   398  
   399  	if config.PostDelete != nil {
   400  		if err := config.PostDelete(&r); err != nil {
   401  			return err
   402  		}
   403  	}
   404  	return nil
   405  }