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

     1  package cli
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"errors"
     7  	"fmt"
     8  	"log"
     9  	"net/url"
    10  	"os"
    11  	"strconv"
    12  	"time"
    13  
    14  	"github.com/mattn/go-isatty"
    15  	"github.com/spf13/cobra"
    16  
    17  	"github.com/tilt-dev/tilt/internal/analytics"
    18  	"github.com/tilt-dev/tilt/internal/controllers"
    19  	engineanalytics "github.com/tilt-dev/tilt/internal/engine/analytics"
    20  	"github.com/tilt-dev/tilt/internal/hud/prompt"
    21  	"github.com/tilt-dev/tilt/internal/store"
    22  	"github.com/tilt-dev/tilt/internal/store/liveupdates"
    23  	"github.com/tilt-dev/tilt/pkg/assets"
    24  	"github.com/tilt-dev/tilt/pkg/logger"
    25  	"github.com/tilt-dev/tilt/pkg/model"
    26  	"github.com/tilt-dev/tilt/web"
    27  )
    28  
    29  var webModeFlag model.WebMode = model.DefaultWebMode
    30  
    31  const DefaultWebDevPort = 46764
    32  
    33  var updateModeFlag string = string(liveupdates.UpdateModeAuto)
    34  var webDevPort = 0
    35  var logActionsFlag bool = false
    36  
    37  var userExitError = errors.New("user requested Tilt exit")
    38  
    39  //go:embed Tiltfile.starter
    40  var starterTiltfile []byte
    41  
    42  type upCmd struct {
    43  	fileName             string
    44  	outputSnapshotOnExit string
    45  
    46  	legacy bool
    47  	stream bool
    48  }
    49  
    50  func (c *upCmd) name() model.TiltSubcommand { return "up" }
    51  
    52  func (c *upCmd) register() *cobra.Command {
    53  	cmd := &cobra.Command{
    54  		Use:                   "up [<tilt flags>] [-- <Tiltfile args>]",
    55  		DisableFlagsInUseLine: true,
    56  		Short:                 "Start Tilt with the given Tiltfile args",
    57  		Long: `
    58  Starts Tilt and runs services defined in the Tiltfile.
    59  
    60  There are two types of args:
    61  1) Tilt flags, listed below, which are handled entirely by Tilt.
    62  2) Tiltfile args, which can be anything, and are potentially accessed by config.parse in your Tiltfile.
    63  
    64  By default:
    65  1) Tiltfile args are interpreted as the list of services to start, e.g. tilt up frontend backend.
    66  2) Running with no Tiltfile args starts all services defined in the Tiltfile
    67  
    68  This default behavior does not apply if the Tiltfile uses config.parse or config.set_enabled_resources.
    69  In that case, see https://docs.tilt.dev/tiltfile_config.html and/or comments in your Tiltfile
    70  
    71  When you exit Tilt (using Ctrl+C), Kubernetes resources and Docker Compose resources continue running;
    72  you can use tilt down (https://docs.tilt.dev/cli/tilt_down.html) to delete these resources. Any long-running
    73  local resources--i.e. those using serve_cmd--are terminated when you exit Tilt.
    74  `,
    75  	}
    76  
    77  	cmd.Flags().StringVar(&updateModeFlag, "update-mode", string(liveupdates.UpdateModeAuto),
    78  		fmt.Sprintf("Control the strategy Tilt uses for updating instances. Possible values: %v", liveupdates.AllUpdateModes))
    79  	cmd.Flags().BoolVar(&c.legacy, "legacy", false, "If true, tilt will open in legacy terminal mode.")
    80  	cmd.Flags().BoolVar(&c.stream, "stream", false, "If true, tilt will stream logs in the terminal.")
    81  	cmd.Flags().BoolVar(&logActionsFlag, "logactions", false, "log all actions and state changes")
    82  	addStartServerFlags(cmd)
    83  	addDevServerFlags(cmd)
    84  	addTiltfileFlag(cmd, &c.fileName)
    85  	addKubeContextFlag(cmd)
    86  	addNamespaceFlag(cmd)
    87  	cmd.Flags().Lookup("logactions").Hidden = true
    88  	cmd.Flags().StringVar(&c.outputSnapshotOnExit, "output-snapshot-on-exit", "", "If specified, Tilt will dump a snapshot of its state to the specified path when it exits")
    89  
    90  	return cmd
    91  }
    92  
    93  func (c *upCmd) initialTermMode(isTerminal bool) store.TerminalMode {
    94  	if !isTerminal {
    95  		return store.TerminalModeStream
    96  	}
    97  
    98  	if c.legacy {
    99  		return store.TerminalModeHUD
   100  	}
   101  
   102  	if c.stream {
   103  		return store.TerminalModeStream
   104  	}
   105  
   106  	return store.TerminalModePrompt
   107  }
   108  
   109  func (c *upCmd) run(ctx context.Context, args []string) error {
   110  	ctx, cancel := context.WithCancel(ctx)
   111  	defer cancel()
   112  
   113  	a := analytics.Get(ctx)
   114  	defer a.Flush(time.Second)
   115  
   116  	log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime))
   117  	isTTY := isatty.IsTerminal(os.Stdout.Fd())
   118  	termMode := c.initialTermMode(isTTY)
   119  
   120  	cmdUpTags := engineanalytics.CmdTags(map[string]string{
   121  		"update_mode": updateModeFlag, // before 7/8/20 this was just called "mode"
   122  		"term_mode":   strconv.Itoa(int(termMode)),
   123  	})
   124  
   125  	generateTiltfileResult, err := maybeGenerateTiltfile(c.fileName)
   126  	// N.B. report the command before handling the error; result enum is always valid
   127  	cmdUpTags["generate_tiltfile.result"] = string(generateTiltfileResult)
   128  	a.Incr("cmd.up", cmdUpTags.AsMap())
   129  	if err == userExitError {
   130  		return nil
   131  	} else if err != nil {
   132  		return err
   133  	}
   134  
   135  	deferred := logger.NewDeferredLogger(ctx)
   136  	ctx = redirectLogs(ctx, deferred)
   137  
   138  	webHost := provideWebHost()
   139  	webURL, _ := provideWebURL(webHost, provideWebPort())
   140  	startLine := prompt.StartStatusLine(webURL, webHost)
   141  	log.Print(startLine)
   142  	log.Print(buildStamp())
   143  
   144  	if ok, reason := analytics.IsAnalyticsDisabledFromEnv(); ok {
   145  		log.Printf("Tilt analytics disabled: %s", reason)
   146  	}
   147  
   148  	cmdUpDeps, err := wireCmdUp(ctx, a, cmdUpTags, "up")
   149  	if err != nil {
   150  		deferred.SetOutput(deferred.Original())
   151  		return err
   152  	}
   153  
   154  	upper := cmdUpDeps.Upper
   155  	if termMode == store.TerminalModePrompt {
   156  		// Any logs that showed up during initialization, make sure they're
   157  		// in the prompt.
   158  		cmdUpDeps.Prompt.SetInitOutput(deferred.CopyBuffered(logger.InfoLvl))
   159  	}
   160  
   161  	l := store.NewLogActionLogger(ctx, upper.Dispatch)
   162  	deferred.SetOutput(l)
   163  	ctx = redirectLogs(ctx, l)
   164  	if c.outputSnapshotOnExit != "" {
   165  		defer cmdUpDeps.Snapshotter.WriteSnapshot(ctx, c.outputSnapshotOnExit)
   166  	}
   167  
   168  	err = upper.Start(ctx, args, cmdUpDeps.TiltBuild,
   169  		c.fileName, termMode, a.UserOpt(), cmdUpDeps.Token, string(cmdUpDeps.CloudAddress))
   170  	if err != context.Canceled {
   171  		return err
   172  	} else {
   173  		return nil
   174  	}
   175  }
   176  
   177  func redirectLogs(ctx context.Context, l logger.Logger) context.Context {
   178  	ctx = logger.WithLogger(ctx, l)
   179  	log.SetOutput(l.Writer(logger.InfoLvl))
   180  	controllers.MaybeSetKlogOutput(l.Writer(logger.InfoLvl))
   181  	return ctx
   182  }
   183  
   184  func provideUpdateModeFlag() liveupdates.UpdateModeFlag {
   185  	return liveupdates.UpdateModeFlag(updateModeFlag)
   186  }
   187  
   188  func provideLogActions() store.LogActionsFlag {
   189  	return store.LogActionsFlag(logActionsFlag)
   190  }
   191  
   192  func provideWebMode(b model.TiltBuild) (model.WebMode, error) {
   193  	switch webModeFlag {
   194  	case model.LocalWebMode,
   195  		model.ProdWebMode,
   196  		model.EmbeddedWebMode,
   197  		model.PrecompiledWebMode:
   198  		return webModeFlag, nil
   199  	case model.DefaultWebMode:
   200  		// Set prod web mode from an environment variable. Useful for
   201  		// running integration tests against dev tilt.
   202  		webMode := os.Getenv("TILT_WEB_MODE")
   203  		if webMode == "prod" {
   204  			return model.ProdWebMode, nil
   205  		}
   206  
   207  		if b.Dev {
   208  			return model.LocalWebMode, nil
   209  		} else {
   210  			return model.ProdWebMode, nil
   211  		}
   212  	}
   213  	return "", model.UnrecognizedWebModeError(string(webModeFlag))
   214  }
   215  
   216  func provideWebHost() model.WebHost {
   217  	return model.WebHost(webHostFlag)
   218  }
   219  
   220  func provideWebPort() model.WebPort {
   221  	return model.WebPort(webPortFlag)
   222  }
   223  
   224  func provideWebURL(webHost model.WebHost, webPort model.WebPort) (model.WebURL, error) {
   225  	if webPort == 0 {
   226  		return model.WebURL{}, nil
   227  	}
   228  
   229  	if webHost == "0.0.0.0" {
   230  		// 0.0.0.0 means "listen on all hosts"
   231  		// For UI displays, we use 127.0.0.1 (loopback)
   232  		webHost = "127.0.0.1"
   233  	}
   234  
   235  	u, err := url.Parse(fmt.Sprintf("http://%s:%d/", webHost, webPort))
   236  	if err != nil {
   237  		return model.WebURL{}, err
   238  	}
   239  	return model.WebURL(*u), nil
   240  }
   241  
   242  func targetMode(mode model.WebMode, embeddedAvailable bool) (model.WebMode, error) {
   243  	if (mode == model.EmbeddedWebMode || mode == model.PrecompiledWebMode) && !embeddedAvailable {
   244  		return mode, fmt.Errorf(
   245  			("requested %s mode, but JS/CSS files are not available.\n" +
   246  				"Please report this: https://github.com/tilt-dev/tilt/issues"), string(mode))
   247  	}
   248  	if mode.IsProd() {
   249  		// defaults to embedded, reporting an error if embedded not available.
   250  		if !embeddedAvailable {
   251  			return mode, fmt.Errorf(
   252  				("running in prod mode, but JS/CSS files are not available.\n" +
   253  					"Please report this: https://github.com/tilt-dev/tilt/issues"))
   254  		} else if mode == model.ProdWebMode {
   255  			mode = model.EmbeddedWebMode
   256  		}
   257  	} else { // precompiled when available and by request, otherwise local
   258  		if mode != model.PrecompiledWebMode {
   259  			mode = model.LocalWebMode
   260  		}
   261  	}
   262  	return mode, nil
   263  }
   264  
   265  func provideAssetServer(mode model.WebMode, version model.WebVersion) (assets.Server, error) {
   266  	s, ok := assets.GetEmbeddedServer()
   267  	m, err := targetMode(mode, ok)
   268  
   269  	if err != nil {
   270  		return nil, err
   271  	}
   272  
   273  	switch m {
   274  	case model.EmbeddedWebMode, model.PrecompiledWebMode:
   275  		return s, nil
   276  	case model.LocalWebMode:
   277  		path, err := web.StaticPath()
   278  		if err != nil {
   279  			return nil, err
   280  		}
   281  		pkgDir := assets.PackageDir(path)
   282  		return assets.NewDevServer(pkgDir, model.WebDevPort(webDevPort))
   283  	}
   284  	return nil, model.UnrecognizedWebModeError(string(mode))
   285  }