github.com/grahambrereton-form3/tilt@v0.10.18/internal/engine/live_update_build_and_deployer.go (about)

     1  package engine
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/opentracing/opentracing-go"
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/windmilleng/tilt/internal/analytics"
    13  
    14  	"github.com/windmilleng/tilt/internal/container"
    15  	"github.com/windmilleng/tilt/internal/containerupdate"
    16  
    17  	"github.com/windmilleng/tilt/internal/build"
    18  	"github.com/windmilleng/tilt/internal/ignore"
    19  	"github.com/windmilleng/tilt/internal/k8s"
    20  	"github.com/windmilleng/tilt/internal/store"
    21  	"github.com/windmilleng/tilt/pkg/logger"
    22  	"github.com/windmilleng/tilt/pkg/model"
    23  )
    24  
    25  var _ BuildAndDeployer = &LiveUpdateBuildAndDeployer{}
    26  
    27  type LiveUpdateBuildAndDeployer struct {
    28  	dcu     *containerupdate.DockerContainerUpdater
    29  	scu     *containerupdate.SyncletUpdater
    30  	ecu     *containerupdate.ExecUpdater
    31  	updMode UpdateMode
    32  	env     k8s.Env
    33  	runtime container.Runtime
    34  }
    35  
    36  func NewLiveUpdateBuildAndDeployer(dcu *containerupdate.DockerContainerUpdater,
    37  	scu *containerupdate.SyncletUpdater, ecu *containerupdate.ExecUpdater,
    38  	updMode UpdateMode, env k8s.Env, runtime container.Runtime) *LiveUpdateBuildAndDeployer {
    39  	return &LiveUpdateBuildAndDeployer{
    40  		dcu:     dcu,
    41  		scu:     scu,
    42  		ecu:     ecu,
    43  		updMode: updMode,
    44  		env:     env,
    45  		runtime: runtime,
    46  	}
    47  }
    48  
    49  // Info needed to perform a live update
    50  type liveUpdInfo struct {
    51  	iTarget      model.ImageTarget
    52  	state        store.BuildState
    53  	changedFiles []build.PathMapping
    54  	runs         []model.Run
    55  	hotReload    bool
    56  }
    57  
    58  func (lui liveUpdInfo) Empty() bool { return lui.iTarget.ID() == model.ImageTarget{}.ID() }
    59  
    60  func (lubad *LiveUpdateBuildAndDeployer) BuildAndDeploy(ctx context.Context, st store.RStore, specs []model.TargetSpec, stateSet store.BuildStateSet) (store.BuildResultSet, error) {
    61  	liveUpdateStateSet, err := extractImageTargetsForLiveUpdates(specs, stateSet)
    62  	if err != nil {
    63  		return store.BuildResultSet{}, err
    64  	}
    65  
    66  	containerUpdater := lubad.containerUpdaterForSpecs(specs)
    67  	liveUpdInfos := make([]liveUpdInfo, 0, len(liveUpdateStateSet))
    68  
    69  	if len(liveUpdateStateSet) == 0 {
    70  		return nil, SilentRedirectToNextBuilderf("no targets for LiveUpdate found")
    71  	}
    72  
    73  	for _, luStateTree := range liveUpdateStateSet {
    74  		luInfo, err := liveUpdateInfoForStateTree(luStateTree)
    75  		if err != nil {
    76  			return store.BuildResultSet{}, err
    77  		}
    78  
    79  		if !luInfo.Empty() {
    80  			liveUpdInfos = append(liveUpdInfos, luInfo)
    81  		}
    82  	}
    83  
    84  	var dontFallBackErr error
    85  	for _, info := range liveUpdInfos {
    86  		err = lubad.buildAndDeploy(ctx, containerUpdater, info.iTarget, info.state, info.changedFiles, info.runs, info.hotReload)
    87  		if err != nil {
    88  			if !IsDontFallBackError(err) {
    89  				// something went wrong, we want to fall back -- bail and
    90  				// let the next builder take care of it
    91  				return store.BuildResultSet{}, err
    92  			}
    93  			// if something went wrong due to USER failure (i.e. run step failed),
    94  			// run the rest of the container updates so all the containers are in
    95  			// a consistent state, then return this error, i.e. don't fall back.
    96  			dontFallBackErr = err
    97  		}
    98  	}
    99  	return createResultSet(liveUpdateStateSet, liveUpdInfos), dontFallBackErr
   100  }
   101  
   102  func (lubad *LiveUpdateBuildAndDeployer) buildAndDeploy(ctx context.Context, cu containerupdate.ContainerUpdater, iTarget model.ImageTarget, state store.BuildState, changedFiles []build.PathMapping, runs []model.Run, hotReload bool) error {
   103  	span, ctx := opentracing.StartSpanFromContext(ctx, "LiveUpdateBuildAndDeployer-buildAndDeploy")
   104  	span.SetTag("target", iTarget.ConfigurationRef.String())
   105  	defer span.Finish()
   106  
   107  	startTime := time.Now()
   108  	defer func() {
   109  		analytics.Get(ctx).Timer("build.container", time.Since(startTime), nil)
   110  	}()
   111  
   112  	l := logger.Get(ctx)
   113  	cIDStr := container.ShortStrs(store.IDsForInfos(state.RunningContainers))
   114  	l.Infof("  → Updating container(s): %s", cIDStr)
   115  
   116  	filter := ignore.CreateBuildContextFilter(iTarget)
   117  	boiledSteps, err := build.BoilRuns(runs, changedFiles)
   118  	if err != nil {
   119  		return err
   120  	}
   121  
   122  	// rm files from container
   123  	toRemove, toArchive, err := build.MissingLocalPaths(ctx, changedFiles)
   124  	if err != nil {
   125  		return errors.Wrap(err, "MissingLocalPaths")
   126  	}
   127  
   128  	if len(toRemove) > 0 {
   129  		l.Infof("Will delete %d file(s) from container(s): %s", len(toRemove), cIDStr)
   130  		for _, pm := range toRemove {
   131  			l.Infof("- '%s' (matched local path: '%s')", pm.ContainerPath, pm.LocalPath)
   132  		}
   133  	}
   134  
   135  	if len(toArchive) > 0 {
   136  		l.Infof("Will copy %d file(s) to container(s): %s", len(toArchive), cIDStr)
   137  		for _, pm := range toArchive {
   138  			l.Infof("- %s", pm.PrettyStr())
   139  		}
   140  	}
   141  
   142  	var lastUserBuildFailure error
   143  	for _, cInfo := range state.RunningContainers {
   144  		archive := build.TarArchiveForPaths(ctx, toArchive, filter)
   145  		err = cu.UpdateContainer(ctx, cInfo, archive,
   146  			build.PathMappingsToContainerPaths(toRemove), boiledSteps, hotReload)
   147  		if err != nil {
   148  			if runFail, ok := build.MaybeRunStepFailure(err); ok {
   149  				// Keep running updates -- we want all containers to have the same files on them
   150  				// even if the Runs don't succeed
   151  				lastUserBuildFailure = err
   152  				logger.Get(ctx).Infof("  → FAILED TO UPDATE CONTAINER %s: run step %q failed with with exit code: %d",
   153  					cInfo.ContainerID, runFail.Cmd.String(), runFail.ExitCode)
   154  				continue
   155  			}
   156  
   157  			// Something went wrong with this update and it's NOT the user's fault--
   158  			// likely a infrastructure error. Bail, and fall back to full build.
   159  			return err
   160  		} else {
   161  			logger.Get(ctx).Infof("  → Container %s updated!", cInfo.ContainerID.ShortStr())
   162  			if lastUserBuildFailure != nil {
   163  				// This build succeeded, but previously at least one failed due to user error.
   164  				// We may have inconsistent state--bail, and fall back to full build.
   165  				return fmt.Errorf("INCONSISTENT STATE: container %s successfully updated, "+
   166  					"but last update failed with '%v'", cInfo.ContainerID, lastUserBuildFailure)
   167  			}
   168  		}
   169  	}
   170  	if lastUserBuildFailure != nil {
   171  		return WrapDontFallBackError(lastUserBuildFailure)
   172  	}
   173  	return nil
   174  }
   175  
   176  // liveUpdateInfoForStateTree validates the state tree for LiveUpdate and returns
   177  // all the info we need to execute the update.
   178  func liveUpdateInfoForStateTree(stateTree liveUpdateStateTree) (liveUpdInfo, error) {
   179  	iTarget := stateTree.iTarget
   180  	state := stateTree.iTargetState
   181  	filesChanged := stateTree.filesChanged
   182  
   183  	var err error
   184  	var fileMappings []build.PathMapping
   185  	var runs []model.Run
   186  	var hotReload bool
   187  
   188  	if fbInfo := iTarget.AnyFastBuildInfo(); !fbInfo.Empty() {
   189  		var skipped []string
   190  		fileMappings, skipped, err = build.FilesToPathMappings(filesChanged, fbInfo.Syncs)
   191  		if err != nil {
   192  			return liveUpdInfo{}, err
   193  		}
   194  		if len(skipped) > 0 {
   195  			return liveUpdInfo{}, RedirectToNextBuilderInfof("found file(s) not matching a FastBuild sync, so "+
   196  				"performing a full build. (Files: %s)", strings.Join(skipped, ", "))
   197  		}
   198  		runs = fbInfo.Runs
   199  		hotReload = fbInfo.HotReload
   200  	} else if luInfo := iTarget.AnyLiveUpdateInfo(); !luInfo.Empty() {
   201  		var skipped []string
   202  		fileMappings, skipped, err = build.FilesToPathMappings(filesChanged, luInfo.SyncSteps())
   203  		if err != nil {
   204  			return liveUpdInfo{}, err
   205  		}
   206  		if len(skipped) > 0 {
   207  			return liveUpdInfo{}, RedirectToNextBuilderInfof("found file(s) not matching a LiveUpdate sync, so "+
   208  				"performing a full build. (Files: %s)", strings.Join(skipped, ", "))
   209  		}
   210  
   211  		// If any changed files match a FallBackOn file, fall back to next BuildAndDeployer
   212  		anyMatch, file, err := luInfo.FallBackOnFiles().AnyMatch(build.PathMappingsToLocalPaths(fileMappings))
   213  		if err != nil {
   214  			return liveUpdInfo{}, err
   215  		}
   216  		if anyMatch {
   217  			return liveUpdInfo{}, RedirectToNextBuilderInfof(
   218  				"detected change to fall_back_on file '%s'", file)
   219  		}
   220  
   221  		runs = luInfo.RunSteps()
   222  		hotReload = !luInfo.ShouldRestart()
   223  	} else {
   224  		// We should have validated this when generating the LiveUpdateStateTrees, but double check!
   225  		panic(fmt.Sprintf("found neither FastBuild nor LiveUpdate info on target %s, "+
   226  			"which should have already been validated", iTarget.ID()))
   227  	}
   228  
   229  	if len(fileMappings) == 0 {
   230  		// No files matched a sync for this image, no LiveUpdate to run
   231  		return liveUpdInfo{}, nil
   232  	}
   233  
   234  	return liveUpdInfo{
   235  		iTarget:      iTarget,
   236  		state:        state,
   237  		changedFiles: fileMappings,
   238  		runs:         runs,
   239  		hotReload:    hotReload,
   240  	}, nil
   241  }
   242  
   243  func (lubad *LiveUpdateBuildAndDeployer) containerUpdaterForSpecs(specs []model.TargetSpec) containerupdate.ContainerUpdater {
   244  	isDC := len(model.ExtractDockerComposeTargets(specs)) > 0
   245  	if isDC || lubad.updMode == UpdateModeContainer {
   246  		return lubad.dcu
   247  	}
   248  
   249  	if lubad.updMode == UpdateModeSynclet {
   250  		return lubad.scu
   251  	}
   252  
   253  	if lubad.updMode == UpdateModeKubectlExec {
   254  		return lubad.ecu
   255  	}
   256  
   257  	if shouldUseSynclet(lubad.updMode, lubad.env, lubad.runtime) {
   258  		return lubad.scu
   259  	}
   260  
   261  	if lubad.runtime == container.RuntimeDocker && lubad.env.UsesLocalDockerRegistry() {
   262  		return lubad.dcu
   263  	}
   264  
   265  	return lubad.ecu
   266  }