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 }