github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/buildcontrol/local_target_build_and_deployer.go (about)

     1  package buildcontrol
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"time"
     7  
     8  	"k8s.io/apimachinery/pkg/types"
     9  	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
    10  
    11  	"github.com/tilt-dev/tilt/internal/analytics"
    12  	"github.com/tilt-dev/tilt/internal/build"
    13  	"github.com/tilt-dev/tilt/internal/controllers/core/cmd"
    14  	"github.com/tilt-dev/tilt/internal/store"
    15  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    16  	"github.com/tilt-dev/tilt/pkg/model"
    17  )
    18  
    19  var _ BuildAndDeployer = &LocalTargetBuildAndDeployer{}
    20  
    21  // TODO(maia): CommandRunner interface for testability
    22  type LocalTargetBuildAndDeployer struct {
    23  	clock      build.Clock
    24  	ctrlClient ctrlclient.Client
    25  	cmds       *cmd.Controller
    26  }
    27  
    28  func NewLocalTargetBuildAndDeployer(
    29  	c build.Clock,
    30  	ctrlClient ctrlclient.Client,
    31  	cmds *cmd.Controller) *LocalTargetBuildAndDeployer {
    32  	return &LocalTargetBuildAndDeployer{
    33  		clock:      c,
    34  		ctrlClient: ctrlClient,
    35  		cmds:       cmds,
    36  	}
    37  }
    38  
    39  func (bd *LocalTargetBuildAndDeployer) BuildAndDeploy(ctx context.Context, st store.RStore, specs []model.TargetSpec, stateSet store.BuildStateSet) (resultSet store.BuildResultSet, err error) {
    40  	targets := bd.extract(specs)
    41  	if len(targets) != 1 {
    42  		return store.BuildResultSet{}, SilentRedirectToNextBuilderf(
    43  			"LocalTargetBuildAndDeployer requires exactly one LocalTarget (got %d)", len(targets))
    44  	}
    45  
    46  	targ := targets[0]
    47  	if targ.UpdateCmdSpec == nil {
    48  		// Even if a LocalResource has no update command, we push it through the build-and-deploy
    49  		// pipeline so that it gets all the appropriate logs.
    50  		return bd.successfulBuildResult(targ), nil
    51  	}
    52  
    53  	startTime := time.Now()
    54  	defer func() {
    55  		analytics.Get(ctx).Timer("build.local", time.Since(startTime), map[string]string{
    56  			"hasError": fmt.Sprintf("%t", err != nil),
    57  		})
    58  	}()
    59  
    60  	var cmd v1alpha1.Cmd
    61  	err = bd.ctrlClient.Get(ctx, types.NamespacedName{Name: targ.UpdateCmdName()}, &cmd)
    62  	if err != nil {
    63  		return store.BuildResultSet{}, DontFallBackErrorf("Loading command: %v", err)
    64  	}
    65  
    66  	status, err := bd.cmds.ForceRun(ctx, &cmd)
    67  	if err != nil {
    68  		// (Never fall back from the LocalTargetBaD, none of our other BaDs can handle this target)
    69  		return store.BuildResultSet{}, DontFallBackErrorf("Command %q failed: %v",
    70  			model.ArgListToString(cmd.Spec.Args), err)
    71  	} else if status.Terminated == nil {
    72  		return store.BuildResultSet{}, DontFallBackErrorf("Command didn't terminate")
    73  	} else if status.Terminated.ExitCode != 0 {
    74  		return store.BuildResultSet{}, DontFallBackErrorf("Command %q failed: %v",
    75  			model.ArgListToString(cmd.Spec.Args), status.Terminated.Reason)
    76  	}
    77  
    78  	// HACK(maia) Suppose target A modifies file X and target B depends on file X.
    79  	//
    80  	// Consider this sequence:
    81  	//
    82  	// 1. A starts
    83  	// 2. A modifies X at time T1
    84  	// 3. A modifies X at time T2
    85  	// 4. A finishes
    86  	// 5. B starts, caused by change at T1
    87  	// 6. Tilt observes change at T2
    88  	// 7. B finishes building
    89  	// 8. B builds again, because the change at T2 was observed after the first build started.
    90  	//
    91  	// Empirically, this sleep ensures that any local file changes are processed
    92  	// before the next build starts.
    93  	//
    94  	// At the moment (2020-01-31), local_resources will not build in parallel with
    95  	// other resources by default, so this works fine.
    96  	//
    97  	// Possible approaches for a better system:
    98  	//
    99  	// - Use mtimes rather than our own internal modification tracking
   100  	//   for determining dirtiness. Here is some good discussion of this approach:
   101  	//   https://github.com/ninja-build/ninja/blob/master/src/deps_log.h#L29
   102  	//   https://apenwarr.ca/log/20181113
   103  	//   which has a lot of caveats, but you can boil it down to "using mtimes can
   104  	//   make things a lot more efficient, but be careful how you use them"
   105  	//
   106  	// - Make a "dummy" change to the file system and make sure it propagates
   107  	//   through the watch system before we start the next build (like fsync() does
   108  	//   in our watch tests).
   109  	time.Sleep(250 * time.Millisecond)
   110  
   111  	return bd.successfulBuildResult(targ), nil
   112  }
   113  
   114  // Extract the targets we can apply -- i.e. LocalTargets
   115  func (bd *LocalTargetBuildAndDeployer) extract(specs []model.TargetSpec) []model.LocalTarget {
   116  	var targs []model.LocalTarget
   117  	for _, s := range specs {
   118  		if s, ok := s.(model.LocalTarget); ok {
   119  			targs = append(targs, s)
   120  		}
   121  	}
   122  	return targs
   123  }
   124  
   125  func (bd *LocalTargetBuildAndDeployer) successfulBuildResult(t model.LocalTarget) store.BuildResultSet {
   126  	br := store.NewLocalBuildResult(t.ID())
   127  	return store.BuildResultSet{t.ID(): br}
   128  }