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

     1  package tiltfile
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"go.starlark.net/syntax"
    10  
    11  	"go.starlark.net/starlark"
    12  
    13  	"github.com/tilt-dev/tilt/internal/tiltfile/starkit"
    14  	"github.com/tilt-dev/tilt/internal/tiltfile/value"
    15  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    16  	"github.com/tilt-dev/tilt/pkg/model"
    17  )
    18  
    19  const fmtRestartContainerDeprecationError = "Found `restart_container()` LiveUpdate step in resource(s): [%s]. `restart_container()`  has been deprecated for k8s resources. We recommend the restart_process extension: https://github.com/tilt-dev/tilt-extensions/tree/master/restart_process. For more information, see https://docs.tilt.dev/live_update_reference.html#restarting-your-process"
    20  
    21  func restartContainerDeprecationError(names []model.ManifestName) string {
    22  	strs := make([]string, len(names))
    23  	for i, n := range names {
    24  		strs[i] = n.String()
    25  	}
    26  	return fmt.Sprintf(fmtRestartContainerDeprecationError, strings.Join(strs, ", "))
    27  }
    28  
    29  // when adding a new type of `liveUpdateStep`, make sure that any tiltfile functions that create them also call
    30  // `s.recordLiveUpdateStep`
    31  type liveUpdateStep interface {
    32  	starlark.Value
    33  	liveUpdateStep()
    34  	declarationPos() string
    35  }
    36  
    37  type liveUpdateFallBackOnStep struct {
    38  	files    []string
    39  	position syntax.Position
    40  }
    41  
    42  var _ starlark.Value = liveUpdateFallBackOnStep{}
    43  var _ liveUpdateStep = liveUpdateFallBackOnStep{}
    44  
    45  func (l liveUpdateFallBackOnStep) String() string {
    46  	return fmt.Sprintf("fall_back_on step: %v'", l.files)
    47  }
    48  func (l liveUpdateFallBackOnStep) Type() string         { return "live_update_fall_back_on_step" }
    49  func (l liveUpdateFallBackOnStep) Freeze()              {}
    50  func (l liveUpdateFallBackOnStep) Truth() starlark.Bool { return len(l.files) > 0 }
    51  func (l liveUpdateFallBackOnStep) Hash() (uint32, error) {
    52  	t := starlark.Tuple{}
    53  	for _, path := range l.files {
    54  		t = append(t, starlark.String(path))
    55  	}
    56  	return t.Hash()
    57  }
    58  func (l liveUpdateFallBackOnStep) liveUpdateStep()        {}
    59  func (l liveUpdateFallBackOnStep) declarationPos() string { return l.position.String() }
    60  
    61  type liveUpdateSyncStep struct {
    62  	localPath, remotePath string
    63  	position              syntax.Position
    64  }
    65  
    66  var _ starlark.Value = liveUpdateSyncStep{}
    67  var _ liveUpdateStep = liveUpdateSyncStep{}
    68  
    69  func (l liveUpdateSyncStep) String() string {
    70  	return fmt.Sprintf("sync step: '%s'->'%s'", l.localPath, l.remotePath)
    71  }
    72  func (l liveUpdateSyncStep) Type() string { return "live_update_sync_step" }
    73  func (l liveUpdateSyncStep) Freeze()      {}
    74  func (l liveUpdateSyncStep) Truth() starlark.Bool {
    75  	return len(l.localPath) > 0 || len(l.remotePath) > 0
    76  }
    77  func (l liveUpdateSyncStep) Hash() (uint32, error) {
    78  	return starlark.Tuple{starlark.String(l.localPath), starlark.String(l.remotePath)}.Hash()
    79  }
    80  func (l liveUpdateSyncStep) liveUpdateStep()        {}
    81  func (l liveUpdateSyncStep) declarationPos() string { return l.position.String() }
    82  
    83  type liveUpdateRunStep struct {
    84  	command  model.Cmd
    85  	triggers []string
    86  	echoOff  bool
    87  	position syntax.Position
    88  }
    89  
    90  var _ starlark.Value = liveUpdateRunStep{}
    91  var _ liveUpdateStep = liveUpdateRunStep{}
    92  
    93  func (l liveUpdateRunStep) String() string {
    94  	s := fmt.Sprintf("run step: %s", strconv.Quote(l.command.String()))
    95  	if len(l.triggers) > 0 {
    96  		s = fmt.Sprintf("%s (triggers: %s)", s, strings.Join(l.triggers, "; "))
    97  	}
    98  	return s
    99  }
   100  
   101  func (l liveUpdateRunStep) Type() string { return "live_update_run_step" }
   102  func (l liveUpdateRunStep) Freeze()      {}
   103  func (l liveUpdateRunStep) Truth() starlark.Bool {
   104  	return starlark.Bool(!l.command.Empty())
   105  }
   106  func (l liveUpdateRunStep) Hash() (uint32, error) {
   107  	t := starlark.Tuple{starlark.String(l.command.String())}
   108  	for _, trigger := range l.triggers {
   109  		t = append(t, starlark.String(trigger))
   110  	}
   111  	return t.Hash()
   112  }
   113  func (l liveUpdateRunStep) declarationPos() string { return l.position.String() }
   114  
   115  func (l liveUpdateRunStep) liveUpdateStep() {}
   116  
   117  type liveUpdateRestartContainerStep struct {
   118  	position syntax.Position
   119  }
   120  
   121  var _ starlark.Value = liveUpdateRestartContainerStep{}
   122  var _ liveUpdateStep = liveUpdateRestartContainerStep{}
   123  
   124  func (l liveUpdateRestartContainerStep) String() string         { return "restart_container step" }
   125  func (l liveUpdateRestartContainerStep) Type() string           { return "live_update_restart_container_step" }
   126  func (l liveUpdateRestartContainerStep) Freeze()                {}
   127  func (l liveUpdateRestartContainerStep) Truth() starlark.Bool   { return true }
   128  func (l liveUpdateRestartContainerStep) Hash() (uint32, error)  { return 0, nil }
   129  func (l liveUpdateRestartContainerStep) declarationPos() string { return l.position.String() }
   130  func (l liveUpdateRestartContainerStep) liveUpdateStep()        {}
   131  
   132  func (s *tiltfileState) recordLiveUpdateStep(step liveUpdateStep) {
   133  	s.unconsumedLiveUpdateSteps[step.declarationPos()] = step
   134  }
   135  
   136  func (s *tiltfileState) liveUpdateFallBackOn(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   137  	files := value.NewLocalPathListUnpacker(thread)
   138  	if err := s.unpackArgs(fn.Name(), args, kwargs, "paths", &files); err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	ret := liveUpdateFallBackOnStep{
   143  		files:    files.Value,
   144  		position: thread.CallFrame(1).Pos,
   145  	}
   146  	s.recordLiveUpdateStep(ret)
   147  	return ret, nil
   148  }
   149  
   150  func (s *tiltfileState) liveUpdateSync(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   151  	var localPath, remotePath string
   152  	if err := s.unpackArgs(fn.Name(), args, kwargs, "local_path", &localPath, "remote_path", &remotePath); err != nil {
   153  		return nil, err
   154  	}
   155  
   156  	ret := liveUpdateSyncStep{
   157  		localPath:  starkit.AbsPath(thread, localPath),
   158  		remotePath: remotePath,
   159  		position:   thread.CallFrame(1).Pos,
   160  	}
   161  	s.recordLiveUpdateStep(ret)
   162  	return ret, nil
   163  }
   164  
   165  func (s *tiltfileState) liveUpdateRun(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   166  	var commandVal starlark.Value
   167  	var triggers starlark.Value
   168  	echoOff := false
   169  	if err := s.unpackArgs(fn.Name(), args, kwargs,
   170  		"cmd", &commandVal,
   171  		"trigger?", &triggers,
   172  		"echo_off?", &echoOff); err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	command, err := value.ValueToUnixCmd(thread, commandVal, nil, nil)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	triggersSlice := starlarkValueOrSequenceToSlice(triggers)
   182  	var triggerStrings []string
   183  	for _, t := range triggersSlice {
   184  		switch t2 := t.(type) {
   185  		case starlark.String:
   186  			triggerStrings = append(triggerStrings, string(t2))
   187  		default:
   188  			return nil, fmt.Errorf("run cmd '%s' triggers contained value '%s' of type '%s'. it may only contain strings", command, t.String(), t.Type())
   189  		}
   190  	}
   191  
   192  	ret := liveUpdateRunStep{
   193  		command:  command,
   194  		triggers: triggerStrings,
   195  		echoOff:  echoOff,
   196  		position: thread.CallFrame(1).Pos,
   197  	}
   198  	s.recordLiveUpdateStep(ret)
   199  	return ret, nil
   200  }
   201  
   202  func (s *tiltfileState) liveUpdateRestartContainer(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   203  	if err := s.unpackArgs(fn.Name(), args, kwargs); err != nil {
   204  		return nil, err
   205  	}
   206  
   207  	ret := liveUpdateRestartContainerStep{
   208  		position: thread.CallFrame(1).Pos,
   209  	}
   210  	s.recordLiveUpdateStep(ret)
   211  	return ret, nil
   212  }
   213  
   214  func (s *tiltfileState) liveUpdateFromSteps(t *starlark.Thread, maybeSteps starlark.Value) (v1alpha1.LiveUpdateSpec, error) {
   215  	var err error
   216  
   217  	basePath := starkit.AbsWorkingDir(t)
   218  	spec := v1alpha1.LiveUpdateSpec{
   219  		BasePath: basePath,
   220  	}
   221  
   222  	stepSlice := starlarkValueOrSequenceToSlice(maybeSteps)
   223  	if len(stepSlice) == 0 {
   224  		return v1alpha1.LiveUpdateSpec{}, nil
   225  	}
   226  
   227  	noMoreFallbacks := false
   228  	noMoreSyncs := false
   229  	noMoreRuns := false
   230  	for _, v := range stepSlice {
   231  		step, ok := v.(liveUpdateStep)
   232  		if !ok {
   233  			return v1alpha1.LiveUpdateSpec{}, fmt.Errorf("'steps' must be a list of live update steps - got value '%v' of type '%s'", v.String(), v.Type())
   234  		}
   235  
   236  		switch x := step.(type) {
   237  
   238  		case liveUpdateFallBackOnStep:
   239  			if noMoreFallbacks {
   240  				return v1alpha1.LiveUpdateSpec{}, fmt.Errorf("fall_back_on steps must appear at the start of the list")
   241  			}
   242  
   243  			for _, f := range x.files {
   244  				if filepath.IsAbs(f) {
   245  					f, err = filepath.Rel(basePath, f)
   246  					if err != nil {
   247  						return v1alpha1.LiveUpdateSpec{}, err
   248  					}
   249  				}
   250  				spec.StopPaths = append(spec.StopPaths, f)
   251  			}
   252  
   253  		case liveUpdateSyncStep:
   254  			if noMoreRuns {
   255  				return v1alpha1.LiveUpdateSpec{}, fmt.Errorf("restart container is only valid as the last step")
   256  			}
   257  			if noMoreSyncs {
   258  				return v1alpha1.LiveUpdateSpec{}, fmt.Errorf("all sync steps must precede all run steps")
   259  			}
   260  			noMoreFallbacks = true
   261  
   262  			localPath := x.localPath
   263  			if filepath.IsAbs(localPath) {
   264  				localPath, err = filepath.Rel(basePath, x.localPath)
   265  				if err != nil {
   266  					return v1alpha1.LiveUpdateSpec{}, err
   267  				}
   268  			}
   269  			spec.Syncs = append(spec.Syncs, v1alpha1.LiveUpdateSync{
   270  				LocalPath:     localPath,
   271  				ContainerPath: x.remotePath,
   272  			})
   273  
   274  		case liveUpdateRunStep:
   275  			if noMoreRuns {
   276  				return v1alpha1.LiveUpdateSpec{}, fmt.Errorf("restart container is only valid as the last step")
   277  			}
   278  			noMoreFallbacks = true
   279  			noMoreSyncs = true
   280  
   281  			spec.Execs = append(spec.Execs, v1alpha1.LiveUpdateExec{
   282  				Args:         x.command.Argv,
   283  				TriggerPaths: x.triggers,
   284  				EchoOff:      x.echoOff,
   285  			})
   286  
   287  		case liveUpdateRestartContainerStep:
   288  			noMoreFallbacks = true
   289  			noMoreSyncs = true
   290  			noMoreRuns = true
   291  			spec.Restart = v1alpha1.LiveUpdateRestartStrategyAlways
   292  
   293  		default:
   294  			return v1alpha1.LiveUpdateSpec{}, fmt.Errorf("%s: internal error - unknown liveUpdateStep '%v' of type '%T'", x.declarationPos(), x, x)
   295  		}
   296  
   297  		s.consumeLiveUpdateStep(step)
   298  	}
   299  
   300  	errs := (&v1alpha1.LiveUpdate{Spec: spec}).Validate(s.ctx)
   301  	if len(errs) > 0 {
   302  		return v1alpha1.LiveUpdateSpec{}, errs.ToAggregate()
   303  	}
   304  
   305  	return spec, nil
   306  }
   307  
   308  func (s *tiltfileState) consumeLiveUpdateStep(stepToConsume liveUpdateStep) {
   309  	delete(s.unconsumedLiveUpdateSteps, stepToConsume.declarationPos())
   310  }
   311  
   312  func (s *tiltfileState) checkForUnconsumedLiveUpdateSteps() error {
   313  	if len(s.unconsumedLiveUpdateSteps) > 0 {
   314  		var errorStrings []string
   315  		for _, step := range s.unconsumedLiveUpdateSteps {
   316  			errorStrings = append(errorStrings, fmt.Sprintf("%s: value '%s' of type '%s'", step.declarationPos(), step.String(), step.Type()))
   317  		}
   318  		return fmt.Errorf("found %d live_update steps that were created but not used in a live_update:\n%s",
   319  			len(s.unconsumedLiveUpdateSteps), strings.Join(errorStrings, "\n\t"))
   320  	}
   321  
   322  	return nil
   323  }