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

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