github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/cli/tiltfile_result.go (about)

     1  package cli
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"regexp"
     8  	"time"
     9  
    10  	"github.com/pkg/errors"
    11  	"github.com/spf13/cobra"
    12  	"k8s.io/cli-runtime/pkg/genericclioptions"
    13  
    14  	"github.com/tilt-dev/tilt/pkg/logger"
    15  
    16  	"github.com/tilt-dev/tilt/internal/analytics"
    17  	ctrltiltfile "github.com/tilt-dev/tilt/internal/controllers/apis/tiltfile"
    18  	"github.com/tilt-dev/tilt/internal/tiltfile"
    19  	"github.com/tilt-dev/tilt/pkg/model"
    20  )
    21  
    22  var tupleRE = regexp.MustCompile(`,\)$`)
    23  
    24  // arbitrary non-1 value chosen to allow callers to distinguish between
    25  // Tilt errors and Tiltfile errors
    26  const TiltfileErrExitCode = 5
    27  
    28  type tiltfileResultCmd struct {
    29  	streams genericclioptions.IOStreams
    30  	exit    func(code int)
    31  
    32  	fileName string
    33  
    34  	// for Builtin Timings mode
    35  	builtinTimings bool
    36  	durThreshold   time.Duration
    37  }
    38  
    39  var _ tiltCmd = &tiltfileResultCmd{}
    40  
    41  type cmdTiltfileResultDeps struct {
    42  	tfl tiltfile.TiltfileLoader
    43  }
    44  
    45  func newTiltfileResultDeps(tfl tiltfile.TiltfileLoader) cmdTiltfileResultDeps {
    46  	return cmdTiltfileResultDeps{
    47  		tfl: tfl,
    48  	}
    49  }
    50  
    51  func newTiltfileResultCmd(streams genericclioptions.IOStreams) *tiltfileResultCmd {
    52  	return &tiltfileResultCmd{
    53  		streams: streams,
    54  		exit:    os.Exit,
    55  	}
    56  }
    57  
    58  func (c *tiltfileResultCmd) name() model.TiltSubcommand { return "tiltfile-result" }
    59  
    60  func (c *tiltfileResultCmd) register() *cobra.Command {
    61  	cmd := &cobra.Command{
    62  		Use:   "tiltfile-result",
    63  		Short: "Exec the Tiltfile and print data about execution",
    64  		Long: `Exec the Tiltfile and print data about execution.
    65  
    66  By default, prints Tiltfile execution results as JSON (note: the API is unstable and may change); can also print timings of Tiltfile Builtin calls.
    67  
    68  Exit code 0: successful Tiltfile evaluation (data printed to stdout)
    69  Exit code 1: some failure in setup, printing results, etc. (any logs printed to stderr)
    70  Exit code 5: error when evaluating the Tiltfile, such as syntax error, illegal Tiltfile operation, etc. (any logs printed to stderr)
    71  
    72  Run with -v | --verbose to print Tiltfile execution logs on stderr, regardless of whether there was an error.`,
    73  	}
    74  
    75  	addTiltfileFlag(cmd, &c.fileName)
    76  	addKubeContextFlag(cmd)
    77  	cmd.Flags().BoolVarP(&c.builtinTimings, "builtin-timings", "b", false, "If true, print timing data for Tiltfile builtin calls instead of Tiltfile result JSON")
    78  	cmd.Flags().DurationVar(&c.durThreshold, "dur-threshold", 0, "Only compatible with Builtin Timings mode. Should be a Go duration string. If passed, only print information about builtin calls lasting this duration and longer.")
    79  
    80  	return cmd
    81  }
    82  
    83  func (c *tiltfileResultCmd) run(ctx context.Context, args []string) error {
    84  	// HACK(maia): we're overloading the -v|--verbose flags here, which isn't ideal,
    85  	// but eh, it's fast. Might be cleaner to do --logs=true or something.
    86  	logLvl := logger.Get(ctx).Level()
    87  	showTiltfileLogs := logLvl.ShouldDisplay(logger.VerboseLvl)
    88  
    89  	if !showTiltfileLogs {
    90  		// defer Tiltfile output -- only print on error
    91  		l := logger.NewDeferredLogger(ctx)
    92  		ctx = logger.WithLogger(ctx, l)
    93  	} else {
    94  		// send all logs to stderr so stdout has only structured output
    95  		ctx = logger.WithLogger(ctx, logger.NewLogger(logLvl, c.streams.ErrOut))
    96  	}
    97  
    98  	deps, err := wireTiltfileResult(ctx, analytics.Get(ctx), "alpha tiltfile-result")
    99  	if err != nil {
   100  		c.maybePrintDeferredLogsToStderr(ctx, showTiltfileLogs)
   101  		return errors.Wrap(err, "wiring dependencies")
   102  	}
   103  
   104  	start := time.Now()
   105  	tlr := deps.tfl.Load(ctx, ctrltiltfile.MainTiltfile(c.fileName, args), nil)
   106  	tflDur := time.Since(start)
   107  	if tlr.Error != nil {
   108  		c.maybePrintDeferredLogsToStderr(ctx, showTiltfileLogs)
   109  
   110  		// Some errors won't JSONify properly by default, so just print it
   111  		// to STDERR and use the exit code to indicate that it's an error
   112  		// from Tiltfile parsing.
   113  		fmt.Fprintln(c.streams.ErrOut, tlr.Error)
   114  		c.exit(TiltfileErrExitCode)
   115  		return nil
   116  	}
   117  
   118  	// Instead of printing result JSON, print Builtin Timings instead
   119  	if c.builtinTimings {
   120  		if len(tlr.BuiltinCalls) == 0 {
   121  			return fmt.Errorf("executed Tiltfile, but recorded no Builtin calls")
   122  		}
   123  		for _, call := range tlr.BuiltinCalls {
   124  			if call.Dur < c.durThreshold {
   125  				continue
   126  			}
   127  			argsStr := tupleRE.ReplaceAllString(fmt.Sprintf("%v", call.Args), ")") // clean up tuple stringification
   128  			fmt.Fprintf(c.streams.Out, "- %s%s took %s\n", call.Name, argsStr, call.Dur)
   129  		}
   130  		fmt.Fprintf(c.streams.Out, "Tiltfile execution took %s\n", tflDur.String())
   131  		return nil
   132  	}
   133  
   134  	err = encodeJSON(c.streams.Out, tlr)
   135  	if err != nil {
   136  		c.maybePrintDeferredLogsToStderr(ctx, showTiltfileLogs)
   137  		return errors.Wrap(err, "encoding JSON")
   138  	}
   139  	return nil
   140  }
   141  
   142  func (c *tiltfileResultCmd) maybePrintDeferredLogsToStderr(ctx context.Context, showTiltfileLogs bool) {
   143  	if showTiltfileLogs {
   144  		// We've already printed the logs elsewhere, do nothing
   145  		return
   146  	}
   147  	l, ok := logger.Get(ctx).(*logger.DeferredLogger)
   148  	if !ok {
   149  		panic(fmt.Sprintf("expected logger of type DeferredLogger, got: %T", logger.Get(ctx)))
   150  	}
   151  	stderrLogger := logger.NewLogger(l.Level(), c.streams.ErrOut)
   152  	l.SetOutput(stderrLogger)
   153  }