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