github.com/vmware/govmomi@v0.51.0/simulator/container.go (about)

     1  // © Broadcom. All Rights Reserved.
     2  // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package simulator
     6  
     7  import (
     8  	"archive/tar"
     9  	"bufio"
    10  	"bytes"
    11  	"context"
    12  	"encoding/json"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"log"
    17  	"net"
    18  	"os"
    19  	"os/exec"
    20  	"path"
    21  	"regexp"
    22  	"strings"
    23  	"sync"
    24  	"time"
    25  )
    26  
    27  var (
    28  	shell      = "/bin/sh"
    29  	eventWatch eventWatcher
    30  )
    31  
    32  const (
    33  	deleteWithContainer = "lifecycle=container"
    34  	createdByVcsim      = "createdBy=vcsim"
    35  )
    36  
    37  func init() {
    38  	if sh, err := exec.LookPath("bash"); err != nil {
    39  		shell = sh
    40  	}
    41  }
    42  
    43  type eventWatcher struct {
    44  	sync.Mutex
    45  
    46  	stdin   io.WriteCloser
    47  	stdout  io.ReadCloser
    48  	process *os.Process
    49  
    50  	// watches is a map of container IDs to container objects
    51  	watches map[string]*container
    52  }
    53  
    54  // container provides methods to manage a container within a simulator VM lifecycle.
    55  type container struct {
    56  	sync.Mutex
    57  
    58  	id   string
    59  	name string
    60  
    61  	cancelWatch context.CancelFunc
    62  	changes     chan struct{}
    63  }
    64  
    65  type networkSettings struct {
    66  	Gateway     string
    67  	IPAddress   string
    68  	IPPrefixLen int
    69  	MacAddress  string
    70  }
    71  
    72  type containerDetails struct {
    73  	Config struct {
    74  		Hostname   string
    75  		Domainname string
    76  		DNS        []string `json:"dns"`
    77  	}
    78  	State struct {
    79  		Running bool
    80  		Paused  bool
    81  	}
    82  	NetworkSettings struct {
    83  		networkSettings
    84  		Networks map[string]networkSettings
    85  	}
    86  }
    87  
    88  type unknownContainer error
    89  type uninitializedContainer error
    90  
    91  var sanitizeNameRx = regexp.MustCompile(`[\(\)\s]`)
    92  
    93  func sanitizeName(name string) string {
    94  	return sanitizeNameRx.ReplaceAllString(name, "-")
    95  }
    96  
    97  func constructContainerName(name, uid string) string {
    98  	return fmt.Sprintf("vcsim-%s-%s", sanitizeName(name), uid)
    99  }
   100  
   101  func constructVolumeName(containerName, uid, volumeName string) string {
   102  	return constructContainerName(containerName, uid) + "--" + sanitizeName(volumeName)
   103  }
   104  
   105  func prefixToMask(prefix int) string {
   106  	mask := net.CIDRMask(prefix, 32)
   107  	return fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3])
   108  }
   109  
   110  type tarEntry struct {
   111  	header  *tar.Header
   112  	content []byte
   113  }
   114  
   115  // From https://docs.docker.com/engine/reference/commandline/cp/ :
   116  // > It is not possible to copy certain system files such as resources under /proc, /sys, /dev, tmpfs, and mounts created by the user in the container.
   117  // > However, you can still copy such files by manually running tar in docker exec.
   118  func copyToGuest(id string, dest string, length int64, reader io.Reader) error {
   119  	cmd := exec.Command("docker", "exec", "-i", id, "tar", "Cxf", path.Dir(dest), "-")
   120  	cmd.Stderr = os.Stderr
   121  	stdin, err := cmd.StdinPipe()
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	err = cmd.Start()
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	tw := tar.NewWriter(stdin)
   132  	_ = tw.WriteHeader(&tar.Header{
   133  		Name:    path.Base(dest),
   134  		Size:    length,
   135  		Mode:    0444,
   136  		ModTime: time.Now(),
   137  	})
   138  
   139  	_, err = io.Copy(tw, reader)
   140  
   141  	twErr := tw.Close()
   142  	stdinErr := stdin.Close()
   143  
   144  	waitErr := cmd.Wait()
   145  
   146  	if err != nil || twErr != nil || stdinErr != nil || waitErr != nil {
   147  		return fmt.Errorf("copy: {%s}, tw: {%s}, stdin: {%s}, wait: {%s}", err, twErr, stdinErr, waitErr)
   148  	}
   149  
   150  	return nil
   151  }
   152  
   153  func copyFromGuest(id string, src string, sink func(int64, io.Reader) error) error {
   154  	cmd := exec.Command("docker", "exec", id, "tar", "Ccf", path.Dir(src), "-", path.Base(src))
   155  	cmd.Stderr = os.Stderr
   156  	stdout, err := cmd.StdoutPipe()
   157  	if err != nil {
   158  		return err
   159  	}
   160  	if err = cmd.Start(); err != nil {
   161  		return err
   162  	}
   163  
   164  	tr := tar.NewReader(stdout)
   165  	header, err := tr.Next()
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	err = sink(header.Size, tr)
   171  	waitErr := cmd.Wait()
   172  
   173  	if err != nil || waitErr != nil {
   174  		return fmt.Errorf("err: {%s}, wait: {%s}", err, waitErr)
   175  	}
   176  
   177  	return nil
   178  }
   179  
   180  // createVolume creates a volume populated with the provided files
   181  // If the header.Size is omitted or set to zero, then len(content+1) is used.
   182  // Docker appears to treat this volume create command as idempotent so long as it's identical
   183  // to an existing volume, so we can use this both for creating volumes inline in container create (for labelling) and
   184  // for population after.
   185  // returns:
   186  //
   187  //	uid - string
   188  //	err - error or nil
   189  func createVolume(volumeName string, labels []string, files []tarEntry) (string, error) {
   190  	image := os.Getenv("VCSIM_BUSYBOX")
   191  	if image == "" {
   192  		image = "busybox"
   193  	}
   194  
   195  	name := sanitizeName(volumeName)
   196  	uid := ""
   197  
   198  	// label the volume if specified - this requires the volume be created before use
   199  	if len(labels) > 0 {
   200  		run := []string{"volume", "create"}
   201  		for i := range labels {
   202  			run = append(run, "--label", labels[i])
   203  		}
   204  		run = append(run, name)
   205  		cmd := exec.Command("docker", run...)
   206  		out, err := cmd.Output()
   207  		if err != nil {
   208  			return "", err
   209  		}
   210  		uid = strings.TrimSpace(string(out))
   211  
   212  		if name == "" {
   213  			name = uid
   214  		}
   215  	}
   216  
   217  	run := []string{"run", "--rm", "-i"}
   218  	run = append(run, "-v", name+":/"+name)
   219  	run = append(run, image, "tar", "-C", "/"+name, "-xf", "-")
   220  	cmd := exec.Command("docker", run...)
   221  	stdin, err := cmd.StdinPipe()
   222  	if err != nil {
   223  		return uid, err
   224  	}
   225  
   226  	err = cmd.Start()
   227  	if err != nil {
   228  		return uid, err
   229  	}
   230  
   231  	tw := tar.NewWriter(stdin)
   232  
   233  	for _, file := range files {
   234  		header := file.header
   235  
   236  		if header.Size == 0 && len(file.content) > 0 {
   237  			header.Size = int64(len(file.content))
   238  		}
   239  
   240  		if header.ModTime.IsZero() {
   241  			header.ModTime = time.Now()
   242  		}
   243  
   244  		if header.Mode == 0 {
   245  			header.Mode = 0444
   246  		}
   247  
   248  		tarErr := tw.WriteHeader(header)
   249  		if tarErr == nil {
   250  			_, tarErr = tw.Write(file.content)
   251  		}
   252  	}
   253  
   254  	err = nil
   255  	twErr := tw.Close()
   256  	stdinErr := stdin.Close()
   257  	if twErr != nil || stdinErr != nil {
   258  		err = fmt.Errorf("tw: {%s}, stdin: {%s}", twErr, stdinErr)
   259  	}
   260  
   261  	if waitErr := cmd.Wait(); waitErr != nil {
   262  		stderr := ""
   263  		if xerr, ok := waitErr.(*exec.ExitError); ok {
   264  			stderr = string(xerr.Stderr)
   265  		}
   266  		log.Printf("%s %s: %s %s", name, cmd.Args, waitErr, stderr)
   267  
   268  		err = fmt.Errorf("%s, wait: {%s}", err, waitErr)
   269  		return uid, err
   270  	}
   271  
   272  	return uid, err
   273  }
   274  
   275  func getBridge(bridgeName string) (string, error) {
   276  	// {"CreatedAt":"2023-07-11 19:22:25.45027052 +0000 UTC","Driver":"bridge","ID":"fe52c7502c5d","IPv6":"false","Internal":"false","Labels":"goodbye=,hello=","Name":"testnet","Scope":"local"}
   277  	// podman has distinctly different fields at v4.4.1 so commented out fields that don't match. We only actually care about ID
   278  	type bridgeNet struct {
   279  		// CreatedAt string
   280  		Driver string
   281  		ID     string
   282  		// IPv6      string
   283  		// Internal  string
   284  		// Labels    string
   285  		Name string
   286  		// Scope     string
   287  	}
   288  
   289  	// if the underlay bridge already exists, return that
   290  	// we don't check for a specific label or similar so that it's possible to use a bridge created by other frameworks for composite testing
   291  	var bridge bridgeNet
   292  	cmd := exec.Command("docker", "network", "ls", "--format={{json .}}", "-f", fmt.Sprintf("name=%s$", bridgeName))
   293  	out, err := cmd.Output()
   294  	if err != nil {
   295  		log.Printf("vcsim %s: %s, %s", cmd.Args, err, out)
   296  		return "", err
   297  	}
   298  
   299  	// unfortunately docker returns an empty string not an empty json doc and podman returns '[]'
   300  	// podman also returns an array of matches even when there's only one, so we normalize.
   301  	str := strings.TrimSpace(string(out))
   302  	str = strings.TrimPrefix(str, "[")
   303  	str = strings.TrimSuffix(str, "]")
   304  	if len(str) == 0 {
   305  		return "", nil
   306  	}
   307  
   308  	err = json.Unmarshal([]byte(str), &bridge)
   309  	if err != nil {
   310  		log.Printf("vcsim %s: %s, %s", cmd.Args, err, str)
   311  		return "", err
   312  	}
   313  
   314  	return bridge.ID, nil
   315  }
   316  
   317  // createBridge creates a bridge network if one does not already exist
   318  // returns:
   319  //
   320  //	uid - string
   321  //	err - error or nil
   322  func createBridge(bridgeName string, labels ...string) (string, error) {
   323  
   324  	id, err := getBridge(bridgeName)
   325  	if err != nil {
   326  		return "", err
   327  	}
   328  
   329  	if id != "" {
   330  		return id, nil
   331  	}
   332  
   333  	run := []string{"network", "create", "--label", createdByVcsim}
   334  	for i := range labels {
   335  		run = append(run, "--label", labels[i])
   336  	}
   337  	run = append(run, bridgeName)
   338  
   339  	cmd := exec.Command("docker", run...)
   340  	out, err := cmd.Output()
   341  	if err != nil {
   342  		log.Printf("vcsim %s: %s: %s", cmd.Args, out, err)
   343  		return "", err
   344  	}
   345  
   346  	// docker returns the ID regardless of whether you supply a name when creating the network, however
   347  	// podman returns the pretty name, so we have to normalize
   348  	id, err = getBridge(bridgeName)
   349  	if err != nil {
   350  		return "", err
   351  	}
   352  
   353  	return id, nil
   354  }
   355  
   356  // create
   357  //   - name - pretty name, eg. vm name
   358  //   - id - uuid or similar - this is merged into container name rather than dictating containerID
   359  //   - networks - set of bridges to connect the container to
   360  //   - volumes - colon separated tuple of volume name to mount path. Passed directly to docker via -v so mount options can be postfixed.
   361  //   - env - array of environment vairables in name=value form
   362  //   - optsAndImage - pass-though options and must include at least the container image to use, including tag if necessary
   363  //   - args - the command+args to pass to the container
   364  func create(ctx *Context, name string, id string, networks []string, volumes []string, ports []string, env []string, image string, args []string) (*container, error) {
   365  	if len(image) == 0 {
   366  		return nil, errors.New("cannot create container backing without an image")
   367  	}
   368  
   369  	var c container
   370  	c.name = constructContainerName(name, id)
   371  	c.changes = make(chan struct{})
   372  
   373  	for i := range volumes {
   374  		// we'll pre-create anonymous volumes, simply for labelling consistency
   375  		volName := strings.Split(volumes[i], ":")
   376  		createVolume(volName[0], []string{deleteWithContainer, "container=" + c.name}, nil)
   377  	}
   378  
   379  	// assemble env
   380  	var dockerNet []string
   381  	var dockerVol []string
   382  	var dockerPort []string
   383  	var dockerEnv []string
   384  
   385  	for i := range env {
   386  		dockerEnv = append(dockerEnv, "--env", env[i])
   387  	}
   388  
   389  	for i := range volumes {
   390  		dockerVol = append(dockerVol, "-v", volumes[i])
   391  	}
   392  
   393  	for i := range ports {
   394  		dockerPort = append(dockerPort, "-p", ports[i])
   395  	}
   396  
   397  	for i := range networks {
   398  		dockerNet = append(dockerNet, "--network", networks[i])
   399  	}
   400  
   401  	run := []string{"docker", "create", "--name", c.name}
   402  	run = append(run, dockerNet...)
   403  	run = append(run, dockerVol...)
   404  	run = append(run, dockerPort...)
   405  	run = append(run, dockerEnv...)
   406  	run = append(run, image)
   407  	run = append(run, args...)
   408  
   409  	// this combines all the run options into a single string that's passed to /bin/bash -c as the single argument to force bash parsing.
   410  	// TODO: make this configurable behaviour so users also have the option of not escaping everything for bash
   411  	cmd := exec.Command(shell, "-c", strings.Join(run, " "))
   412  	out, err := cmd.Output()
   413  	if err != nil {
   414  		stderr := ""
   415  		if xerr, ok := err.(*exec.ExitError); ok {
   416  			stderr = string(xerr.Stderr)
   417  		}
   418  		log.Printf("%s %s: %s %s", name, cmd.Args, err, stderr)
   419  
   420  		return nil, err
   421  	}
   422  
   423  	c.id = strings.TrimSpace(string(out))
   424  
   425  	return &c, nil
   426  }
   427  
   428  // createVolume takes the specified files and writes them into a volume named for the container.
   429  func (c *container) createVolume(name string, labels []string, files []tarEntry) (string, error) {
   430  	return createVolume(c.name+"--"+name, append(labels, "container="+c.name), files)
   431  }
   432  
   433  // inspect retrieves and parses container properties into directly usable struct
   434  // returns:
   435  //
   436  //	out - the stdout of the command
   437  //	detail - basic struct populated with container details
   438  //	err:
   439  //		* if c.id is empty, or docker returns "No such object", will return an uninitializedContainer error
   440  //		* err from either execution or parsing of json output
   441  func (c *container) inspect() (out []byte, detail containerDetails, err error) {
   442  	c.Lock()
   443  	id := c.id
   444  	c.Unlock()
   445  
   446  	if id == "" {
   447  		err = uninitializedContainer(errors.New("inspect of uninitialized container"))
   448  		return
   449  	}
   450  
   451  	var details []containerDetails
   452  
   453  	cmd := exec.Command("docker", "inspect", c.id)
   454  	out, err = cmd.Output()
   455  	if eErr, ok := err.(*exec.ExitError); ok {
   456  		if strings.Contains(string(eErr.Stderr), "No such object") {
   457  			err = uninitializedContainer(errors.New("inspect of uninitialized container"))
   458  		}
   459  	}
   460  
   461  	if err != nil {
   462  		return
   463  	}
   464  
   465  	if err = json.NewDecoder(bytes.NewReader(out)).Decode(&details); err != nil {
   466  		return
   467  	}
   468  
   469  	if len(details) != 1 {
   470  		err = fmt.Errorf("multiple containers (%d) match ID: %s", len(details), c.id)
   471  		return
   472  	}
   473  
   474  	detail = details[0]
   475  
   476  	// DNS setting
   477  	f, oerr := os.Open("/etc/docker/daemon.json")
   478  	if oerr != nil {
   479  		return
   480  	}
   481  	err = json.NewDecoder(f).Decode(&detail.Config)
   482  	_ = f.Close()
   483  
   484  	return
   485  }
   486  
   487  // start
   488  //   - if the container already exists, start it or unpause it.
   489  func (c *container) start(ctx *Context) error {
   490  	c.Lock()
   491  	id := c.id
   492  	c.Unlock()
   493  
   494  	if id == "" {
   495  		return uninitializedContainer(errors.New("start of uninitialized container"))
   496  	}
   497  
   498  	start := "start"
   499  	_, detail, err := c.inspect()
   500  	if err != nil {
   501  		return err
   502  	}
   503  
   504  	if detail.State.Paused {
   505  		start = "unpause"
   506  	}
   507  
   508  	cmd := exec.Command("docker", start, c.id)
   509  	err = cmd.Run()
   510  	if err != nil {
   511  		log.Printf("%s %s: %s", c.name, cmd.Args, err)
   512  	}
   513  
   514  	return err
   515  }
   516  
   517  // pause the container (if any) for the given vm.
   518  func (c *container) pause(ctx *Context) error {
   519  	c.Lock()
   520  	id := c.id
   521  	c.Unlock()
   522  
   523  	if id == "" {
   524  		return uninitializedContainer(errors.New("pause of uninitialized container"))
   525  	}
   526  
   527  	cmd := exec.Command("docker", "pause", c.id)
   528  	err := cmd.Run()
   529  	if err != nil {
   530  		log.Printf("%s %s: %s", c.name, cmd.Args, err)
   531  	}
   532  
   533  	return err
   534  }
   535  
   536  // restart the container (if any) for the given vm.
   537  func (c *container) restart(ctx *Context) error {
   538  	c.Lock()
   539  	id := c.id
   540  	c.Unlock()
   541  
   542  	if id == "" {
   543  		return uninitializedContainer(errors.New("restart of uninitialized container"))
   544  	}
   545  
   546  	cmd := exec.Command("docker", "restart", c.id)
   547  	err := cmd.Run()
   548  	if err != nil {
   549  		log.Printf("%s %s: %s", c.name, cmd.Args, err)
   550  	}
   551  
   552  	return err
   553  }
   554  
   555  // stop the container (if any) for the given vm.
   556  func (c *container) stop(ctx *Context) error {
   557  	c.Lock()
   558  	id := c.id
   559  	c.Unlock()
   560  
   561  	if id == "" {
   562  		return uninitializedContainer(errors.New("stop of uninitialized container"))
   563  	}
   564  
   565  	cmd := exec.Command("docker", "stop", c.id)
   566  	err := cmd.Run()
   567  	if err != nil {
   568  		log.Printf("%s %s: %s", c.name, cmd.Args, err)
   569  	}
   570  
   571  	return err
   572  }
   573  
   574  // exec invokes the specified command, with executable being the first of the args, in the specified container
   575  // returns
   576  //
   577  //	 string - combined stdout and stderr from command
   578  //	 err
   579  //			* uninitializedContainer error - if c.id is empty
   580  //		   	* err from cmd execution
   581  func (c *container) exec(ctx *Context, args []string) (string, error) {
   582  	c.Lock()
   583  	id := c.id
   584  	c.Unlock()
   585  
   586  	if id == "" {
   587  		return "", uninitializedContainer(errors.New("exec into uninitialized container"))
   588  	}
   589  
   590  	args = append([]string{"exec", c.id}, args...)
   591  	cmd := exec.Command("docker", args...)
   592  	res, err := cmd.CombinedOutput()
   593  	if err != nil {
   594  		log.Printf("%s: %s (%s)", c.name, cmd.Args, string(res))
   595  		return "", err
   596  	}
   597  
   598  	return strings.TrimSpace(string(res)), nil
   599  }
   600  
   601  // remove the container (if any) for the given vm. Considers removal of an uninitialized container success.
   602  // Also removes volumes and networks that indicate they are lifecycle coupled with this container.
   603  // returns:
   604  //
   605  //	err - joined err from deletion of container and any volumes or networks that have coupled lifecycle
   606  func (c *container) remove(ctx *Context) error {
   607  	c.Lock()
   608  	defer c.Unlock()
   609  
   610  	if c.id == "" {
   611  		// consider absence success
   612  		return nil
   613  	}
   614  
   615  	cmd := exec.Command("docker", "rm", "-v", "-f", c.id)
   616  	err := cmd.Run()
   617  	if err != nil {
   618  		log.Printf("%s %s: %s", c.name, cmd.Args, err)
   619  		return err
   620  	}
   621  
   622  	cmd = exec.Command("docker", "volume", "ls", "-q", "--filter", "label=container="+c.name, "--filter", "label="+deleteWithContainer)
   623  	volumesToReap, lsverr := cmd.Output()
   624  	if lsverr != nil {
   625  		log.Printf("%s %s: %s", c.name, cmd.Args, lsverr)
   626  	}
   627  	log.Printf("%s volumes: %s", c.name, volumesToReap)
   628  
   629  	var rmverr error
   630  	if len(volumesToReap) > 0 {
   631  		run := []string{"volume", "rm", "-f"}
   632  		run = append(run, strings.Split(string(volumesToReap), "\n")...)
   633  		cmd = exec.Command("docker", run...)
   634  		out, rmverr := cmd.Output()
   635  		if rmverr != nil {
   636  			log.Printf("%s %s: %s, %s", c.name, cmd.Args, rmverr, out)
   637  		}
   638  	}
   639  
   640  	cmd = exec.Command("docker", "network", "ls", "-q", "--filter", "label=container="+c.name, "--filter", "label="+deleteWithContainer)
   641  	networksToReap, lsnerr := cmd.Output()
   642  	if lsnerr != nil {
   643  		log.Printf("%s %s: %s", c.name, cmd.Args, lsnerr)
   644  	}
   645  
   646  	var rmnerr error
   647  	if len(networksToReap) > 0 {
   648  		run := []string{"network", "rm", "-f"}
   649  		run = append(run, strings.Split(string(volumesToReap), "\n")...)
   650  		cmd = exec.Command("docker", run...)
   651  		rmnerr = cmd.Run()
   652  		if rmnerr != nil {
   653  			log.Printf("%s %s: %s", c.name, cmd.Args, rmnerr)
   654  		}
   655  	}
   656  
   657  	if err != nil || lsverr != nil || rmverr != nil || lsnerr != nil || rmnerr != nil {
   658  		return fmt.Errorf("err: {%s}, lsverr: {%s}, rmverr: {%s}, lsnerr:{%s}, rmerr: {%s}", err, lsverr, rmverr, lsnerr, rmnerr)
   659  	}
   660  
   661  	if c.cancelWatch != nil {
   662  		c.cancelWatch()
   663  		eventWatch.ignore(c)
   664  	}
   665  	c.id = ""
   666  	return nil
   667  }
   668  
   669  // updated is a simple trigger allowing a caller to indicate that something has likely changed about the container
   670  // and interested parties should re-inspect as needed.
   671  func (c *container) updated() {
   672  	consolidationWindow := 250 * time.Millisecond
   673  	if d, err := time.ParseDuration(os.Getenv("VCSIM_EVENT_CONSOLIDATION_WINDOW")); err == nil {
   674  		consolidationWindow = d
   675  	}
   676  
   677  	select {
   678  	case c.changes <- struct{}{}:
   679  		time.Sleep(consolidationWindow)
   680  		// as this is only a hint to avoid waiting for the full inspect interval, we don't care about accumulating
   681  		// multiple triggers. We do pause to allow large numbers of sequential updates to consolidate
   682  	default:
   683  	}
   684  }
   685  
   686  // watchContainer monitors the underlying container and updates
   687  // properties based on the container status. This occurs until either
   688  // the container or the VM is removed.
   689  // returns:
   690  //
   691  //	err - uninitializedContainer error - if c.id is empty
   692  func (c *container) watchContainer(ctx *Context, updateFn func(*containerDetails, *container) error) error {
   693  	c.Lock()
   694  	defer c.Unlock()
   695  
   696  	if c.id == "" {
   697  		return uninitializedContainer(errors.New("Attempt to watch uninitialized container"))
   698  	}
   699  
   700  	eventWatch.watch(c)
   701  
   702  	cancelCtx, cancelFunc := context.WithCancel(ctx)
   703  	c.cancelWatch = cancelFunc
   704  
   705  	// Update the VM from the container at regular intervals until the done
   706  	// channel is closed.
   707  	go func() {
   708  		inspectInterval := 10 * time.Second
   709  		if d, err := time.ParseDuration(os.Getenv("VCSIM_INSPECT_INTERVAL")); err == nil {
   710  			inspectInterval = d
   711  		}
   712  		ticker := time.NewTicker(inspectInterval)
   713  
   714  		update := func() {
   715  			_, details, err := c.inspect()
   716  			var rmErr error
   717  			var removing bool
   718  			if _, ok := err.(uninitializedContainer); ok {
   719  				removing = true
   720  				rmErr = c.remove(ctx)
   721  			}
   722  
   723  			updateErr := updateFn(&details, c)
   724  			// if we don't succeed we want to re-try
   725  			if removing && rmErr == nil && updateErr == nil {
   726  				ticker.Stop()
   727  				return
   728  			}
   729  			if updateErr != nil {
   730  				log.Printf("vcsim container watch: %s %s", c.id, updateErr)
   731  			}
   732  		}
   733  
   734  		for {
   735  			select {
   736  			case <-c.changes:
   737  				update()
   738  			case <-ticker.C:
   739  				update()
   740  			case <-cancelCtx.Done():
   741  				return
   742  			}
   743  		}
   744  	}()
   745  
   746  	return nil
   747  }
   748  
   749  func (w *eventWatcher) watch(c *container) {
   750  	w.Lock()
   751  	defer w.Unlock()
   752  
   753  	if w.watches == nil {
   754  		w.watches = make(map[string]*container)
   755  	}
   756  
   757  	w.watches[c.id] = c
   758  
   759  	if w.stdin == nil {
   760  		cmd := exec.Command("docker", "events", "--format", "'{{.ID}}'", "--filter", "Type=container")
   761  		w.stdout, _ = cmd.StdoutPipe()
   762  		w.stdin, _ = cmd.StdinPipe()
   763  		err := cmd.Start()
   764  		if err != nil {
   765  			log.Printf("docker event watcher: %s %s", cmd.Args, err)
   766  			w.stdin = nil
   767  			w.stdout = nil
   768  			w.process = nil
   769  
   770  			return
   771  		}
   772  
   773  		w.process = cmd.Process
   774  
   775  		go w.monitor()
   776  	}
   777  }
   778  
   779  func (w *eventWatcher) ignore(c *container) {
   780  	w.Lock()
   781  
   782  	delete(w.watches, c.id)
   783  
   784  	if len(w.watches) == 0 && w.stdin != nil {
   785  		w.stop()
   786  	}
   787  
   788  	w.Unlock()
   789  }
   790  
   791  func (w *eventWatcher) monitor() {
   792  	w.Lock()
   793  	watches := len(w.watches)
   794  	w.Unlock()
   795  
   796  	if watches == 0 {
   797  		return
   798  	}
   799  
   800  	scanner := bufio.NewScanner(w.stdout)
   801  	for scanner.Scan() {
   802  		id := strings.TrimSpace(scanner.Text())
   803  
   804  		w.Lock()
   805  		container := w.watches[id]
   806  		w.Unlock()
   807  
   808  		if container != nil {
   809  			// this is called in a routine to allow an event consolidation window
   810  			go container.updated()
   811  		}
   812  	}
   813  }
   814  
   815  func (w *eventWatcher) stop() {
   816  	if w.stdin != nil {
   817  		w.stdin.Close()
   818  		w.stdin = nil
   819  	}
   820  	if w.stdout != nil {
   821  		w.stdout.Close()
   822  		w.stdout = nil
   823  	}
   824  	w.process.Kill()
   825  }