github.com/ehazlett/containerd@v0.2.5/runtime/container.go (about)

     1  package runtime
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"syscall"
    12  	"time"
    13  
    14  	"github.com/Sirupsen/logrus"
    15  	"github.com/docker/containerd/specs"
    16  	ocs "github.com/opencontainers/runtime-spec/specs-go"
    17  	"golang.org/x/sys/unix"
    18  )
    19  
    20  // Container defines the operations allowed on a container
    21  type Container interface {
    22  	// ID returns the container ID
    23  	ID() string
    24  	// Path returns the path to the bundle
    25  	Path() string
    26  	// Start starts the init process of the container
    27  	Start(checkpointPath string, s Stdio) (Process, error)
    28  	// Exec starts another process in an existing container
    29  	Exec(string, specs.ProcessSpec, Stdio) (Process, error)
    30  	// Delete removes the container's state and any resources
    31  	Delete() error
    32  	// Processes returns all the containers processes that have been added
    33  	Processes() ([]Process, error)
    34  	// State returns the containers runtime state
    35  	State() State
    36  	// Resume resumes a paused container
    37  	Resume() error
    38  	// Pause pauses a running container
    39  	Pause() error
    40  	// RemoveProcess removes the specified process from the container
    41  	RemoveProcess(string) error
    42  	// Checkpoints returns all the checkpoints for a container
    43  	Checkpoints(checkpointDir string) ([]Checkpoint, error)
    44  	// Checkpoint creates a new checkpoint
    45  	Checkpoint(checkpoint Checkpoint, checkpointDir string) error
    46  	// DeleteCheckpoint deletes the checkpoint for the provided name
    47  	DeleteCheckpoint(name string, checkpointDir string) error
    48  	// Labels are user provided labels for the container
    49  	Labels() []string
    50  	// Pids returns all pids inside the container
    51  	Pids() ([]int, error)
    52  	// Stats returns realtime container stats and resource information
    53  	Stats() (*Stat, error)
    54  	// Name or path of the OCI compliant runtime used to execute the container
    55  	Runtime() string
    56  	// OOM signals the channel if the container received an OOM notification
    57  	OOM() (OOM, error)
    58  	// UpdateResource updates the containers resources to new values
    59  	UpdateResources(*Resource) error
    60  
    61  	// Status return the current status of the container.
    62  	Status() (State, error)
    63  }
    64  
    65  // OOM wraps a container OOM.
    66  type OOM interface {
    67  	io.Closer
    68  	FD() int
    69  	ContainerID() string
    70  	Flush()
    71  	Removed() bool
    72  }
    73  
    74  // Stdio holds the path to the 3 pipes used for the standard ios.
    75  type Stdio struct {
    76  	Stdin  string
    77  	Stdout string
    78  	Stderr string
    79  }
    80  
    81  // NewStdio wraps the given standard io path into an Stdio struct.
    82  // If a given parameter is the empty string, it is replaced by "/dev/null"
    83  func NewStdio(stdin, stdout, stderr string) Stdio {
    84  	for _, s := range []*string{
    85  		&stdin, &stdout, &stderr,
    86  	} {
    87  		if *s == "" {
    88  			*s = "/dev/null"
    89  		}
    90  	}
    91  	return Stdio{
    92  		Stdin:  stdin,
    93  		Stdout: stdout,
    94  		Stderr: stderr,
    95  	}
    96  }
    97  
    98  // ContainerOpts keeps the options passed at container creation
    99  type ContainerOpts struct {
   100  	Root        string
   101  	ID          string
   102  	Bundle      string
   103  	Runtime     string
   104  	RuntimeArgs []string
   105  	Shim        string
   106  	Labels      []string
   107  	NoPivotRoot bool
   108  	Timeout     time.Duration
   109  }
   110  
   111  // New returns a new container
   112  func New(opts ContainerOpts) (Container, error) {
   113  	c := &container{
   114  		root:        opts.Root,
   115  		id:          opts.ID,
   116  		bundle:      opts.Bundle,
   117  		labels:      opts.Labels,
   118  		processes:   make(map[string]*process),
   119  		runtime:     opts.Runtime,
   120  		runtimeArgs: opts.RuntimeArgs,
   121  		shim:        opts.Shim,
   122  		noPivotRoot: opts.NoPivotRoot,
   123  		timeout:     opts.Timeout,
   124  	}
   125  	if err := os.Mkdir(filepath.Join(c.root, c.id), 0755); err != nil {
   126  		return nil, err
   127  	}
   128  	f, err := os.Create(filepath.Join(c.root, c.id, StateFile))
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	defer f.Close()
   133  	if err := json.NewEncoder(f).Encode(state{
   134  		Bundle:      c.bundle,
   135  		Labels:      c.labels,
   136  		Runtime:     c.runtime,
   137  		RuntimeArgs: c.runtimeArgs,
   138  		Shim:        c.shim,
   139  		NoPivotRoot: opts.NoPivotRoot,
   140  	}); err != nil {
   141  		return nil, err
   142  	}
   143  	return c, nil
   144  }
   145  
   146  // Load return a new container from the matchin state file on disk.
   147  func Load(root, id, shimName string, timeout time.Duration) (Container, error) {
   148  	var s state
   149  	f, err := os.Open(filepath.Join(root, id, StateFile))
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	defer f.Close()
   154  	if err := json.NewDecoder(f).Decode(&s); err != nil {
   155  		return nil, err
   156  	}
   157  	c := &container{
   158  		root:        root,
   159  		id:          id,
   160  		bundle:      s.Bundle,
   161  		labels:      s.Labels,
   162  		runtime:     s.Runtime,
   163  		runtimeArgs: s.RuntimeArgs,
   164  		shim:        s.Shim,
   165  		noPivotRoot: s.NoPivotRoot,
   166  		processes:   make(map[string]*process),
   167  		timeout:     timeout,
   168  	}
   169  
   170  	if c.shim == "" {
   171  		c.shim = shimName
   172  	}
   173  
   174  	dirs, err := ioutil.ReadDir(filepath.Join(root, id))
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  	for _, d := range dirs {
   179  		if !d.IsDir() {
   180  			continue
   181  		}
   182  		pid := d.Name()
   183  		s, err := readProcessState(filepath.Join(root, id, pid))
   184  		if err != nil {
   185  			return nil, err
   186  		}
   187  		p, err := loadProcess(filepath.Join(root, id, pid), pid, c, s)
   188  		if err != nil {
   189  			logrus.WithField("id", id).WithField("pid", pid).Debug("containerd: error loading process %s", err)
   190  			continue
   191  		}
   192  		c.processes[pid] = p
   193  	}
   194  	return c, nil
   195  }
   196  
   197  func readProcessState(dir string) (*ProcessState, error) {
   198  	f, err := os.Open(filepath.Join(dir, "process.json"))
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  	defer f.Close()
   203  	var s ProcessState
   204  	if err := json.NewDecoder(f).Decode(&s); err != nil {
   205  		return nil, err
   206  	}
   207  	return &s, nil
   208  }
   209  
   210  type container struct {
   211  	// path to store runtime state information
   212  	root        string
   213  	id          string
   214  	bundle      string
   215  	runtime     string
   216  	runtimeArgs []string
   217  	shim        string
   218  	processes   map[string]*process
   219  	labels      []string
   220  	oomFds      []int
   221  	noPivotRoot bool
   222  	timeout     time.Duration
   223  }
   224  
   225  func (c *container) ID() string {
   226  	return c.id
   227  }
   228  
   229  func (c *container) Path() string {
   230  	return c.bundle
   231  }
   232  
   233  func (c *container) Labels() []string {
   234  	return c.labels
   235  }
   236  
   237  func (c *container) readSpec() (*specs.Spec, error) {
   238  	var spec specs.Spec
   239  	f, err := os.Open(filepath.Join(c.bundle, "config.json"))
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	defer f.Close()
   244  	if err := json.NewDecoder(f).Decode(&spec); err != nil {
   245  		return nil, err
   246  	}
   247  	return &spec, nil
   248  }
   249  
   250  func (c *container) Delete() error {
   251  	err := os.RemoveAll(filepath.Join(c.root, c.id))
   252  
   253  	args := c.runtimeArgs
   254  	args = append(args, "delete", c.id)
   255  	if b, derr := exec.Command(c.runtime, args...).CombinedOutput(); err != nil {
   256  		err = fmt.Errorf("%s: %q", derr, string(b))
   257  	} else if len(b) > 0 {
   258  		logrus.Debugf("%v %v: %q", c.runtime, args, string(b))
   259  	}
   260  	return err
   261  }
   262  
   263  func (c *container) Processes() ([]Process, error) {
   264  	out := []Process{}
   265  	for _, p := range c.processes {
   266  		out = append(out, p)
   267  	}
   268  	return out, nil
   269  }
   270  
   271  func (c *container) RemoveProcess(pid string) error {
   272  	delete(c.processes, pid)
   273  	return os.RemoveAll(filepath.Join(c.root, c.id, pid))
   274  }
   275  
   276  func (c *container) State() State {
   277  	proc := c.processes["init"]
   278  	if proc == nil {
   279  		return Stopped
   280  	}
   281  	return proc.State()
   282  }
   283  
   284  func (c *container) Runtime() string {
   285  	return c.runtime
   286  }
   287  
   288  func (c *container) Pause() error {
   289  	args := c.runtimeArgs
   290  	args = append(args, "pause", c.id)
   291  	b, err := exec.Command(c.runtime, args...).CombinedOutput()
   292  	if err != nil {
   293  		return fmt.Errorf("%s: %q", err.Error(), string(b))
   294  	}
   295  	return nil
   296  }
   297  
   298  func (c *container) Resume() error {
   299  	args := c.runtimeArgs
   300  	args = append(args, "resume", c.id)
   301  	b, err := exec.Command(c.runtime, args...).CombinedOutput()
   302  	if err != nil {
   303  		return fmt.Errorf("%s: %q", err.Error(), string(b))
   304  	}
   305  	return nil
   306  }
   307  
   308  func (c *container) Checkpoints(checkpointDir string) ([]Checkpoint, error) {
   309  	if checkpointDir == "" {
   310  		checkpointDir = filepath.Join(c.bundle, "checkpoints")
   311  	}
   312  
   313  	dirs, err := ioutil.ReadDir(checkpointDir)
   314  	if err != nil {
   315  		return nil, err
   316  	}
   317  	var out []Checkpoint
   318  	for _, d := range dirs {
   319  		if !d.IsDir() {
   320  			continue
   321  		}
   322  		path := filepath.Join(checkpointDir, d.Name(), "config.json")
   323  		data, err := ioutil.ReadFile(path)
   324  		if err != nil {
   325  			return nil, err
   326  		}
   327  		var cpt Checkpoint
   328  		if err := json.Unmarshal(data, &cpt); err != nil {
   329  			return nil, err
   330  		}
   331  		out = append(out, cpt)
   332  	}
   333  	return out, nil
   334  }
   335  
   336  func (c *container) Checkpoint(cpt Checkpoint, checkpointDir string) error {
   337  	if checkpointDir == "" {
   338  		checkpointDir = filepath.Join(c.bundle, "checkpoints")
   339  	}
   340  
   341  	if err := os.MkdirAll(checkpointDir, 0755); err != nil {
   342  		return err
   343  	}
   344  
   345  	path := filepath.Join(checkpointDir, cpt.Name)
   346  	if err := os.Mkdir(path, 0755); err != nil {
   347  		return err
   348  	}
   349  	f, err := os.Create(filepath.Join(path, "config.json"))
   350  	if err != nil {
   351  		return err
   352  	}
   353  	cpt.Created = time.Now()
   354  	err = json.NewEncoder(f).Encode(cpt)
   355  	f.Close()
   356  	if err != nil {
   357  		return err
   358  	}
   359  	args := []string{
   360  		"checkpoint",
   361  		"--image-path", path,
   362  		"--work-path", filepath.Join(path, "criu.work"),
   363  	}
   364  	add := func(flags ...string) {
   365  		args = append(args, flags...)
   366  	}
   367  	add(c.runtimeArgs...)
   368  	if !cpt.Exit {
   369  		add("--leave-running")
   370  	}
   371  	if cpt.Shell {
   372  		add("--shell-job")
   373  	}
   374  	if cpt.TCP {
   375  		add("--tcp-established")
   376  	}
   377  	if cpt.UnixSockets {
   378  		add("--ext-unix-sk")
   379  	}
   380  	for _, ns := range cpt.EmptyNS {
   381  		add("--empty-ns", ns)
   382  	}
   383  	add(c.id)
   384  	out, err := exec.Command(c.runtime, args...).CombinedOutput()
   385  	if err != nil {
   386  		return fmt.Errorf("%s: %q", err.Error(), string(out))
   387  	}
   388  	return err
   389  }
   390  
   391  func (c *container) DeleteCheckpoint(name string, checkpointDir string) error {
   392  	if checkpointDir == "" {
   393  		checkpointDir = filepath.Join(c.bundle, "checkpoints")
   394  	}
   395  	return os.RemoveAll(filepath.Join(checkpointDir, name))
   396  }
   397  
   398  func (c *container) Start(checkpointPath string, s Stdio) (Process, error) {
   399  	processRoot := filepath.Join(c.root, c.id, InitProcessID)
   400  	if err := os.Mkdir(processRoot, 0755); err != nil {
   401  		return nil, err
   402  	}
   403  	cmd := exec.Command(c.shim,
   404  		c.id, c.bundle, c.runtime,
   405  	)
   406  	cmd.Dir = processRoot
   407  	cmd.SysProcAttr = &syscall.SysProcAttr{
   408  		Setpgid: true,
   409  	}
   410  	spec, err := c.readSpec()
   411  	if err != nil {
   412  		return nil, err
   413  	}
   414  	config := &processConfig{
   415  		checkpoint:  checkpointPath,
   416  		root:        processRoot,
   417  		id:          InitProcessID,
   418  		c:           c,
   419  		stdio:       s,
   420  		spec:        spec,
   421  		processSpec: specs.ProcessSpec(spec.Process),
   422  	}
   423  	p, err := newProcess(config)
   424  	if err != nil {
   425  		return nil, err
   426  	}
   427  	if err := c.createCmd(InitProcessID, cmd, p); err != nil {
   428  		return nil, err
   429  	}
   430  	return p, nil
   431  }
   432  
   433  func (c *container) Exec(pid string, pspec specs.ProcessSpec, s Stdio) (pp Process, err error) {
   434  	processRoot := filepath.Join(c.root, c.id, pid)
   435  	if err := os.Mkdir(processRoot, 0755); err != nil {
   436  		return nil, err
   437  	}
   438  	defer func() {
   439  		if err != nil {
   440  			c.RemoveProcess(pid)
   441  		}
   442  	}()
   443  	cmd := exec.Command(c.shim,
   444  		c.id, c.bundle, c.runtime,
   445  	)
   446  	cmd.Dir = processRoot
   447  	cmd.SysProcAttr = &syscall.SysProcAttr{
   448  		Setpgid: true,
   449  	}
   450  	spec, err := c.readSpec()
   451  	if err != nil {
   452  		return nil, err
   453  	}
   454  	config := &processConfig{
   455  		exec:        true,
   456  		id:          pid,
   457  		root:        processRoot,
   458  		c:           c,
   459  		processSpec: pspec,
   460  		spec:        spec,
   461  		stdio:       s,
   462  	}
   463  	p, err := newProcess(config)
   464  	if err != nil {
   465  		return nil, err
   466  	}
   467  	if err := c.createCmd(pid, cmd, p); err != nil {
   468  		return nil, err
   469  	}
   470  	return p, nil
   471  }
   472  
   473  func (c *container) createCmd(pid string, cmd *exec.Cmd, p *process) error {
   474  	p.cmd = cmd
   475  	if err := cmd.Start(); err != nil {
   476  		close(p.cmdDoneCh)
   477  		if exErr, ok := err.(*exec.Error); ok {
   478  			if exErr.Err == exec.ErrNotFound || exErr.Err == os.ErrNotExist {
   479  				return fmt.Errorf("%s not installed on system", c.shim)
   480  			}
   481  		}
   482  		return err
   483  	}
   484  	// We need the pid file to have been written to run
   485  	defer func() {
   486  		go func() {
   487  			err := p.cmd.Wait()
   488  			if err == nil {
   489  				p.cmdSuccess = true
   490  			}
   491  
   492  			if same, err := p.isSameProcess(); same && p.pid > 0 {
   493  				// The process changed its PR_SET_PDEATHSIG, so force
   494  				// kill it
   495  				logrus.Infof("containerd: %s:%s (pid %v) has become an orphan, killing it", p.container.id, p.id, p.pid)
   496  				err = unix.Kill(p.pid, syscall.SIGKILL)
   497  				if err != nil && err != syscall.ESRCH {
   498  					logrus.Errorf("containerd: unable to SIGKILL %s:%s (pid %v): %v", p.container.id, p.id, p.pid, err)
   499  				} else {
   500  					for {
   501  						err = unix.Kill(p.pid, 0)
   502  						if err != nil {
   503  							break
   504  						}
   505  						time.Sleep(5 * time.Millisecond)
   506  					}
   507  				}
   508  			}
   509  			close(p.cmdDoneCh)
   510  		}()
   511  	}()
   512  	if err := c.waitForCreate(p, cmd); err != nil {
   513  		return err
   514  	}
   515  	c.processes[pid] = p
   516  	return nil
   517  }
   518  
   519  func hostIDFromMap(id uint32, mp []ocs.IDMapping) int {
   520  	for _, m := range mp {
   521  		if (id >= m.ContainerID) && (id <= (m.ContainerID + m.Size - 1)) {
   522  			return int(m.HostID + (id - m.ContainerID))
   523  		}
   524  	}
   525  	return 0
   526  }
   527  
   528  func (c *container) Pids() ([]int, error) {
   529  	args := c.runtimeArgs
   530  	args = append(args, "ps", "--format=json", c.id)
   531  	out, err := exec.Command(c.runtime, args...).CombinedOutput()
   532  	if err != nil {
   533  		return nil, fmt.Errorf("%s: %q", err.Error(), out)
   534  	}
   535  	var pids []int
   536  	if err := json.Unmarshal(out, &pids); err != nil {
   537  		return nil, err
   538  	}
   539  	return pids, nil
   540  }
   541  
   542  func (c *container) Stats() (*Stat, error) {
   543  	now := time.Now()
   544  	args := c.runtimeArgs
   545  	args = append(args, "events", "--stats", c.id)
   546  	out, err := exec.Command(c.runtime, args...).CombinedOutput()
   547  	if err != nil {
   548  		return nil, fmt.Errorf("%s: %q", err.Error(), out)
   549  	}
   550  	s := struct {
   551  		Data *Stat `json:"data"`
   552  	}{}
   553  	if err := json.Unmarshal(out, &s); err != nil {
   554  		return nil, err
   555  	}
   556  	s.Data.Timestamp = now
   557  	return s.Data, nil
   558  }
   559  
   560  // Status implements the runtime Container interface.
   561  func (c *container) Status() (State, error) {
   562  	args := c.runtimeArgs
   563  	args = append(args, "state", c.id)
   564  
   565  	out, err := exec.Command(c.runtime, args...).CombinedOutput()
   566  	if err != nil {
   567  		return "", fmt.Errorf("%s: %q", err.Error(), out)
   568  	}
   569  
   570  	// We only require the runtime json output to have a top level Status field.
   571  	var s struct {
   572  		Status State `json:"status"`
   573  	}
   574  	if err := json.Unmarshal(out, &s); err != nil {
   575  		return "", err
   576  	}
   577  	return s.Status, nil
   578  }
   579  
   580  func (c *container) writeEventFD(root string, cfd, efd int) error {
   581  	f, err := os.OpenFile(filepath.Join(root, "cgroup.event_control"), os.O_WRONLY, 0)
   582  	if err != nil {
   583  		return err
   584  	}
   585  	defer f.Close()
   586  	_, err = f.WriteString(fmt.Sprintf("%d %d", efd, cfd))
   587  	return err
   588  }
   589  
   590  type waitArgs struct {
   591  	pid int
   592  	err error
   593  }
   594  
   595  func (c *container) waitForCreate(p *process, cmd *exec.Cmd) error {
   596  	wc := make(chan error, 1)
   597  	go func() {
   598  		for {
   599  			if _, err := p.getPidFromFile(); err != nil {
   600  				if os.IsNotExist(err) || err == errInvalidPidInt {
   601  					alive, err := isAlive(cmd)
   602  					if err != nil {
   603  						wc <- err
   604  						return
   605  					}
   606  					if !alive {
   607  						// runc could have failed to run the container so lets get the error
   608  						// out of the logs or the shim could have encountered an error
   609  						messages, err := readLogMessages(filepath.Join(p.root, "shim-log.json"))
   610  						if err != nil {
   611  							wc <- err
   612  							return
   613  						}
   614  						for _, m := range messages {
   615  							if m.Level == "error" {
   616  								wc <- fmt.Errorf("shim error: %v", m.Msg)
   617  								return
   618  							}
   619  						}
   620  						// no errors reported back from shim, check for runc/runtime errors
   621  						messages, err = readLogMessages(filepath.Join(p.root, "log.json"))
   622  						if err != nil {
   623  							if os.IsNotExist(err) {
   624  								err = ErrContainerNotStarted
   625  							}
   626  							wc <- err
   627  							return
   628  						}
   629  						for _, m := range messages {
   630  							if m.Level == "error" {
   631  								wc <- fmt.Errorf("oci runtime error: %v", m.Msg)
   632  								return
   633  							}
   634  						}
   635  						wc <- ErrContainerNotStarted
   636  						return
   637  					}
   638  					time.Sleep(15 * time.Millisecond)
   639  					continue
   640  				}
   641  				wc <- err
   642  				return
   643  			}
   644  			// the pid file was read successfully
   645  			wc <- nil
   646  			return
   647  		}
   648  	}()
   649  	select {
   650  	case err := <-wc:
   651  		if err != nil {
   652  			return err
   653  		}
   654  		err = p.saveStartTime()
   655  		if err != nil {
   656  			logrus.Warnf("containerd: unable to save %s:%s starttime: %v", p.container.id, p.id, err)
   657  		}
   658  		return nil
   659  	case <-time.After(c.timeout):
   660  		cmd.Process.Kill()
   661  		cmd.Wait()
   662  		return ErrContainerStartTimeout
   663  	}
   664  }
   665  
   666  // isAlive checks if the shim that launched the container is still alive
   667  func isAlive(cmd *exec.Cmd) (bool, error) {
   668  	if _, err := syscall.Wait4(cmd.Process.Pid, nil, syscall.WNOHANG, nil); err == nil {
   669  		return true, nil
   670  	}
   671  	if err := syscall.Kill(cmd.Process.Pid, 0); err != nil {
   672  		if err == syscall.ESRCH {
   673  			return false, nil
   674  		}
   675  		return false, err
   676  	}
   677  	return true, nil
   678  }
   679  
   680  type oom struct {
   681  	id      string
   682  	root    string
   683  	control *os.File
   684  	eventfd int
   685  }
   686  
   687  func (o *oom) ContainerID() string {
   688  	return o.id
   689  }
   690  
   691  func (o *oom) FD() int {
   692  	return o.eventfd
   693  }
   694  
   695  func (o *oom) Flush() {
   696  	buf := make([]byte, 8)
   697  	syscall.Read(o.eventfd, buf)
   698  }
   699  
   700  func (o *oom) Removed() bool {
   701  	_, err := os.Lstat(filepath.Join(o.root, "cgroup.event_control"))
   702  	return os.IsNotExist(err)
   703  }
   704  
   705  func (o *oom) Close() error {
   706  	err := syscall.Close(o.eventfd)
   707  	if cerr := o.control.Close(); err == nil {
   708  		err = cerr
   709  	}
   710  	return err
   711  }
   712  
   713  type message struct {
   714  	Level string `json:"level"`
   715  	Msg   string `json:"msg"`
   716  }
   717  
   718  func readLogMessages(path string) ([]message, error) {
   719  	var out []message
   720  	f, err := os.Open(path)
   721  	if err != nil {
   722  		return nil, err
   723  	}
   724  	defer f.Close()
   725  	dec := json.NewDecoder(f)
   726  	for {
   727  		var m message
   728  		if err := dec.Decode(&m); err != nil {
   729  			if err == io.EOF {
   730  				break
   731  			}
   732  			return nil, err
   733  		}
   734  		out = append(out, m)
   735  	}
   736  	return out, nil
   737  }