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 }