get.porter.sh/porter@v1.3.0/pkg/pkgmgmt/client/runner.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"get.porter.sh/porter/pkg/pkgmgmt"
    13  	"get.porter.sh/porter/pkg/portercontext"
    14  	"get.porter.sh/porter/pkg/tracing"
    15  	"go.opentelemetry.io/otel/attribute"
    16  )
    17  
    18  type Runner struct {
    19  	*portercontext.Context
    20  	// pkgDir is the absolute path to where the package is installed
    21  	pkgDir string
    22  
    23  	pkgName string
    24  	runtime bool
    25  }
    26  
    27  func NewRunner(pkgName, pkgDir string, runtime bool) *Runner {
    28  	return &Runner{
    29  		Context: portercontext.New(),
    30  		pkgName: pkgName,
    31  		pkgDir:  pkgDir,
    32  		runtime: runtime,
    33  	}
    34  }
    35  
    36  func (r *Runner) Validate() error {
    37  	if r.pkgName == "" {
    38  		return errors.New("package name to execute not specified")
    39  	}
    40  
    41  	pkgPath := r.getExecutablePath()
    42  	exists, err := r.FileSystem.Exists(pkgPath)
    43  	if err != nil {
    44  		return fmt.Errorf("failed to stat package (%s: %w)", pkgPath, err)
    45  	}
    46  	if !exists {
    47  		return fmt.Errorf("package not found (%s)", pkgPath)
    48  	}
    49  
    50  	return nil
    51  }
    52  
    53  func (r *Runner) Run(ctx context.Context, commandOpts pkgmgmt.CommandOptions) error {
    54  	ctx, span := tracing.StartSpan(ctx,
    55  		attribute.String("name", r.pkgName),
    56  		attribute.String("pkgDir", r.pkgDir),
    57  		attribute.String("file", commandOpts.File),
    58  		attribute.String("stdin", commandOpts.Input),
    59  	)
    60  	defer span.EndSpan()
    61  
    62  	pkgPath := r.getExecutablePath()
    63  	cmdArgs := strings.Split(commandOpts.Command, " ")
    64  	command := cmdArgs[0]
    65  	cmd := r.NewCommand(ctx, pkgPath, cmdArgs...)
    66  
    67  	// Pipe the output to porter and capture the error in case it fails
    68  	cmdStderr := &bytes.Buffer{}
    69  	cmd.Stdout = r.Context.Out
    70  	cmd.Stderr = io.MultiWriter(cmdStderr, r.Context.Err)
    71  
    72  	if commandOpts.PreRun != nil {
    73  		commandOpts.PreRun(command, cmd)
    74  	}
    75  
    76  	if commandOpts.File != "" {
    77  		cmd.Args = append(cmd.Args, "-f", commandOpts.File)
    78  	}
    79  
    80  	if commandOpts.Input != "" {
    81  		stdin, err := cmd.StdinPipe()
    82  		if err != nil {
    83  			return span.Error(err)
    84  		}
    85  		go func() {
    86  			defer stdin.Close()
    87  			if _, err := io.WriteString(stdin, commandOpts.Input); err != nil {
    88  				_ = span.Error(err)
    89  			}
    90  		}()
    91  	}
    92  
    93  	prettyCmd := fmt.Sprintf("%s%s", cmd.Dir, strings.Join(cmd.Args, " "))
    94  	span.SetAttributes(attribute.String("command", prettyCmd))
    95  
    96  	err := cmd.Start()
    97  	if err != nil {
    98  		return span.Error(fmt.Errorf("could not start package command %s: %w", prettyCmd, err))
    99  	}
   100  
   101  	err = cmd.Wait()
   102  	if err != nil {
   103  		// Include stderr in the error, otherwise it just includes the exit code
   104  		err = fmt.Errorf("package command failed %s\n%s", prettyCmd, cmdStderr)
   105  		// Do not flag this as an error in the logs because we often call mixins to see if they support a command
   106  		// and if they don't it's not an error, e.g. not all mixins support lint or schema
   107  		span.Debugf(err.Error())
   108  		return err
   109  	}
   110  
   111  	return nil
   112  }
   113  
   114  func (r *Runner) getExecutablePath() string {
   115  	path := filepath.Join(r.pkgDir, r.pkgName)
   116  	if r.runtime {
   117  		return filepath.Join(r.pkgDir, "runtimes", r.pkgName+"-runtime")
   118  	}
   119  	return path + pkgmgmt.FileExt
   120  }