github.com/grahambrereton-form3/tilt@v0.10.18/internal/engine/image_build_and_deployer.go (about) 1 package engine 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "os/exec" 8 "time" 9 10 "github.com/docker/distribution/reference" 11 "github.com/opentracing/opentracing-go" 12 "github.com/pkg/errors" 13 v1 "k8s.io/api/core/v1" 14 "k8s.io/apimachinery/pkg/types" 15 16 "github.com/windmilleng/tilt/internal/analytics" 17 "github.com/windmilleng/tilt/internal/build" 18 "github.com/windmilleng/tilt/internal/container" 19 "github.com/windmilleng/tilt/internal/dockerfile" 20 "github.com/windmilleng/tilt/internal/k8s" 21 "github.com/windmilleng/tilt/internal/store" 22 "github.com/windmilleng/tilt/internal/synclet/sidecar" 23 "github.com/windmilleng/tilt/pkg/logger" 24 "github.com/windmilleng/tilt/pkg/model" 25 ) 26 27 var _ BuildAndDeployer = &ImageBuildAndDeployer{} 28 29 type KINDPusher interface { 30 PushToKIND(ctx context.Context, ref reference.NamedTagged, w io.Writer) error 31 } 32 33 type cmdKINDPusher struct { 34 clusterName k8s.ClusterName 35 } 36 37 func (p *cmdKINDPusher) PushToKIND(ctx context.Context, ref reference.NamedTagged, w io.Writer) error { 38 cmd := exec.CommandContext(ctx, "kind", "load", "docker-image", ref.String(), "--name", string(p.clusterName)) 39 cmd.Stdout = w 40 cmd.Stderr = w 41 42 return cmd.Run() 43 } 44 45 func NewKINDPusher(clusterName k8s.ClusterName) KINDPusher { 46 return &cmdKINDPusher{ 47 clusterName: clusterName, 48 } 49 } 50 51 type ImageBuildAndDeployer struct { 52 ib build.ImageBuilder 53 icb *imageAndCacheBuilder 54 k8sClient k8s.Client 55 env k8s.Env 56 runtime container.Runtime 57 analytics *analytics.TiltAnalytics 58 injectSynclet bool 59 clock build.Clock 60 kp KINDPusher 61 syncletContainer sidecar.SyncletContainer 62 } 63 64 func NewImageBuildAndDeployer( 65 b build.ImageBuilder, 66 cacheBuilder build.CacheBuilder, 67 customBuilder build.CustomBuilder, 68 k8sClient k8s.Client, 69 env k8s.Env, 70 analytics *analytics.TiltAnalytics, 71 updMode UpdateMode, 72 c build.Clock, 73 runtime container.Runtime, 74 kp KINDPusher, 75 syncletContainer sidecar.SyncletContainer, 76 ) *ImageBuildAndDeployer { 77 return &ImageBuildAndDeployer{ 78 ib: b, 79 icb: NewImageAndCacheBuilder(b, cacheBuilder, customBuilder, updMode), 80 k8sClient: k8sClient, 81 env: env, 82 analytics: analytics, 83 clock: c, 84 runtime: runtime, 85 kp: kp, 86 syncletContainer: syncletContainer, 87 } 88 } 89 90 // Turn on synclet injection. Should be called before any builds. 91 func (ibd *ImageBuildAndDeployer) SetInjectSynclet(inject bool) { 92 ibd.injectSynclet = inject 93 } 94 95 func (ibd *ImageBuildAndDeployer) BuildAndDeploy(ctx context.Context, st store.RStore, specs []model.TargetSpec, stateSet store.BuildStateSet) (resultSet store.BuildResultSet, err error) { 96 iTargets, kTargets := extractImageAndK8sTargets(specs) 97 if len(kTargets) != 1 { 98 return store.BuildResultSet{}, SilentRedirectToNextBuilderf("ImageBuildAndDeployer does not support these specs") 99 } 100 101 kTarget := kTargets[0] 102 span, ctx := opentracing.StartSpanFromContext(ctx, "daemon-ImageBuildAndDeployer-BuildAndDeploy") 103 span.SetTag("target", kTarget.Name) 104 defer span.Finish() 105 106 startTime := time.Now() 107 defer func() { 108 incremental := "0" 109 for _, state := range stateSet { 110 if state.HasImage() { 111 incremental = "1" 112 } 113 } 114 tags := map[string]string{"incremental": incremental} 115 ibd.analytics.Timer("build.image", time.Since(startTime), tags) 116 }() 117 118 q, err := NewImageTargetQueue(ctx, iTargets, stateSet, ibd.ib.ImageExists) 119 if err != nil { 120 return store.BuildResultSet{}, err 121 } 122 123 // each image target has two stages: one for build, and one for push 124 numStages := q.CountDirty()*2 + 1 125 126 ps := build.NewPipelineState(ctx, numStages, ibd.clock) 127 defer func() { ps.End(ctx, err) }() 128 129 var anyInPlaceBuild bool 130 131 iTargetMap := model.ImageTargetsByID(iTargets) 132 err = q.RunBuilds(func(target model.TargetSpec, state store.BuildState, depResults []store.BuildResult) (store.BuildResult, error) { 133 iTarget, ok := target.(model.ImageTarget) 134 if !ok { 135 return nil, fmt.Errorf("Not an image target: %T", target) 136 } 137 138 iTarget, err := injectImageDependencies(iTarget, iTargetMap, depResults) 139 if err != nil { 140 return nil, err 141 } 142 143 ref, err := ibd.icb.Build(ctx, iTarget, state, ps) 144 if err != nil { 145 return nil, err 146 } 147 148 ref, err = ibd.push(ctx, ref, ps, iTarget, kTarget) 149 if err != nil { 150 return nil, err 151 } 152 153 anyInPlaceBuild = anyInPlaceBuild || 154 !iTarget.AnyFastBuildInfo().Empty() || !iTarget.AnyLiveUpdateInfo().Empty() 155 return store.NewImageBuildResult(iTarget.ID(), ref), nil 156 }) 157 if err != nil { 158 return store.BuildResultSet{}, err 159 } 160 161 // (If we pass an empty list of refs here (as we will do if only deploying 162 // yaml), we just don't inject any image refs into the yaml, nbd. 163 return ibd.deploy(ctx, st, ps, iTargetMap, kTarget, q.results, anyInPlaceBuild) 164 } 165 166 func (ibd *ImageBuildAndDeployer) push(ctx context.Context, ref reference.NamedTagged, ps *build.PipelineState, iTarget model.ImageTarget, kTarget model.K8sTarget) (reference.NamedTagged, error) { 167 ps.StartPipelineStep(ctx, "Pushing %s", reference.FamiliarString(ref)) 168 defer ps.EndPipelineStep(ctx) 169 170 cbSkip := false 171 if iTarget.IsCustomBuild() { 172 cbSkip = iTarget.CustomBuildInfo().DisablePush 173 } 174 175 // We can also skip the push of the image if it isn't used 176 // in any k8s resources! (e.g., it's consumed by another image). 177 if ibd.canAlwaysSkipPush() || !isImageDeployedToK8s(iTarget, kTarget) || cbSkip { 178 ps.Printf(ctx, "Skipping push") 179 return ref, nil 180 } 181 182 var err error 183 if ibd.env == k8s.EnvKIND { 184 ps.Printf(ctx, "Pushing to KIND") 185 err := ibd.kp.PushToKIND(ctx, ref, ps.Writer(ctx)) 186 if err != nil { 187 return nil, fmt.Errorf("Error pushing to KIND: %v", err) 188 } 189 } else { 190 ps.Printf(ctx, "Pushing with Docker client") 191 writer := ps.Writer(ctx) 192 ctx = logger.WithLogger(ctx, logger.NewLogger(logger.InfoLvl, writer)) 193 ref, err = ibd.ib.PushImage(ctx, ref, writer) 194 if err != nil { 195 return nil, err 196 } 197 } 198 199 return ref, nil 200 } 201 202 // Returns: the entities deployed and the namespace of the pod with the given image name/tag. 203 func (ibd *ImageBuildAndDeployer) deploy(ctx context.Context, st store.RStore, ps *build.PipelineState, 204 iTargetMap map[model.TargetID]model.ImageTarget, kTarget model.K8sTarget, results store.BuildResultSet, needsSynclet bool) (store.BuildResultSet, error) { 205 ps.StartPipelineStep(ctx, "Deploying") 206 defer ps.EndPipelineStep(ctx) 207 208 ps.StartBuildStep(ctx, "Injecting images into Kubernetes YAML") 209 210 newK8sEntities, err := ibd.createEntitiesToDeploy(ctx, iTargetMap, kTarget, results, needsSynclet) 211 if err != nil { 212 return nil, err 213 } 214 215 ctx, l := ibd.indentLogger(ctx) 216 217 l.Infof("Applying via kubectl:") 218 for _, displayName := range kTarget.DisplayNames { 219 l.Infof(" %s", displayName) 220 } 221 222 deployed, err := ibd.k8sClient.Upsert(ctx, newK8sEntities) 223 if err != nil { 224 return nil, err 225 } 226 227 // TODO(nick): Do something with this result 228 uids := []types.UID{} 229 for _, entity := range deployed { 230 uid := entity.UID() 231 if uid == "" { 232 return nil, fmt.Errorf("Entity not deployed correctly: %v", entity) 233 } 234 uids = append(uids, entity.UID()) 235 } 236 results[kTarget.ID()] = store.NewK8sDeployResult(kTarget.ID(), uids, deployed) 237 238 return results, nil 239 } 240 241 func (ibd *ImageBuildAndDeployer) indentLogger(ctx context.Context) (context.Context, logger.Logger) { 242 l := logger.Get(ctx) 243 writer := logger.NewPrefixedWriter(logger.Blue(l).Sprint(" │ "), l.Writer(logger.InfoLvl)) 244 l = logger.NewLogger(logger.InfoLvl, writer) 245 return logger.WithLogger(ctx, l), l 246 } 247 248 func (ibd *ImageBuildAndDeployer) createEntitiesToDeploy(ctx context.Context, 249 iTargetMap map[model.TargetID]model.ImageTarget, k8sTarget model.K8sTarget, 250 results store.BuildResultSet, needsSynclet bool) ([]k8s.K8sEntity, error) { 251 newK8sEntities := []k8s.K8sEntity{} 252 253 // TODO(nick): The parsed YAML should probably be a part of the model? 254 // It doesn't make much sense to re-parse it and inject labels on every deploy. 255 entities, err := k8s.ParseYAMLFromString(k8sTarget.YAML) 256 if err != nil { 257 return nil, err 258 } 259 260 depIDs := k8sTarget.DependencyIDs() 261 injectedDepIDs := map[model.TargetID]bool{} 262 for _, e := range entities { 263 injectedSynclet := false 264 e, err = k8s.InjectLabels(e, []model.LabelPair{ 265 k8s.TiltManagedByLabel(), 266 }) 267 if err != nil { 268 return nil, errors.Wrap(err, "deploy") 269 } 270 271 // For development, image pull policy should never be set to "Always", 272 // even if it might make sense to use "Always" in prod. People who 273 // set "Always" for development are shooting their own feet. 274 e, err = k8s.InjectImagePullPolicy(e, v1.PullIfNotPresent) 275 if err != nil { 276 return nil, err 277 } 278 279 // StatefulSet pods should be managed in parallel. See: 280 // https://github.com/windmilleng/tilt/issues/1962 281 e = k8s.InjectParallelPodManagementPolicy(e) 282 283 // When working with a local k8s cluster, we set the pull policy to Never, 284 // to ensure that k8s fails hard if the image is missing from docker. 285 policy := v1.PullIfNotPresent 286 if ibd.canAlwaysSkipPush() { 287 policy = v1.PullNever 288 } 289 290 for _, depID := range depIDs { 291 ref := store.ImageFromBuildResult(results[depID]) 292 if ref == nil { 293 return nil, fmt.Errorf("Internal error: missing image build result for dependency ID: %s", depID) 294 } 295 296 iTarget := iTargetMap[depID] 297 selector := iTarget.ConfigurationRef 298 matchInEnvVars := iTarget.MatchInEnvVars 299 300 var replaced bool 301 e, replaced, err = k8s.InjectImageDigest(e, selector, ref, matchInEnvVars, policy) 302 if err != nil { 303 return nil, err 304 } 305 if replaced { 306 injectedDepIDs[depID] = true 307 308 if !iTarget.OverrideCmd.Empty() { 309 e, err = k8s.InjectCommand(e, ref, iTarget.OverrideCmd) 310 if err != nil { 311 return nil, err 312 } 313 } 314 315 if ibd.injectSynclet && needsSynclet && !injectedSynclet { 316 injectedRefSelector := container.NewRefSelector(ref).WithExactMatch() 317 318 var sidecarInjected bool 319 e, sidecarInjected, err = sidecar.InjectSyncletSidecar(e, injectedRefSelector, ibd.syncletContainer) 320 if err != nil { 321 return nil, err 322 } 323 if !sidecarInjected { 324 return nil, fmt.Errorf("Could not inject synclet: %v", e) 325 } 326 injectedSynclet = true 327 } 328 } 329 } 330 newK8sEntities = append(newK8sEntities, e) 331 } 332 333 for _, depID := range depIDs { 334 if !injectedDepIDs[depID] { 335 return nil, fmt.Errorf("Docker image missing from yaml: %s", depID) 336 } 337 } 338 339 return newK8sEntities, nil 340 } 341 342 // If we're using docker-for-desktop as our k8s backend, 343 // we don't need to push to the central registry. 344 // The k8s will use the image already available 345 // in the local docker daemon. 346 func (ibd *ImageBuildAndDeployer) canAlwaysSkipPush() bool { 347 return ibd.env.UsesLocalDockerRegistry() && ibd.runtime == container.RuntimeDocker 348 } 349 350 // Create a new ImageTarget with the dockerfiles rewritten 351 // with the injected images. 352 func injectImageDependencies(iTarget model.ImageTarget, iTargetMap map[model.TargetID]model.ImageTarget, deps []store.BuildResult) (model.ImageTarget, error) { 353 if len(deps) == 0 { 354 return iTarget, nil 355 } 356 357 df := dockerfile.Dockerfile("") 358 switch bd := iTarget.BuildDetails.(type) { 359 case model.DockerBuild: 360 df = dockerfile.Dockerfile(bd.Dockerfile) 361 case model.FastBuild: 362 df = dockerfile.Dockerfile(bd.BaseDockerfile) 363 default: 364 return model.ImageTarget{}, fmt.Errorf("image %q has no valid buildDetails", iTarget.ConfigurationRef) 365 } 366 367 ast, err := dockerfile.ParseAST(df) 368 if err != nil { 369 return model.ImageTarget{}, errors.Wrap(err, "injectImageDependencies") 370 } 371 372 for _, dep := range deps { 373 image := store.ImageFromBuildResult(dep) 374 if image == nil { 375 return model.ImageTarget{}, fmt.Errorf("Internal error: image is nil") 376 } 377 id := dep.TargetID() 378 modified, err := ast.InjectImageDigest(iTargetMap[id].ConfigurationRef, image) 379 if err != nil { 380 return model.ImageTarget{}, errors.Wrap(err, "injectImageDependencies") 381 } else if !modified { 382 return model.ImageTarget{}, fmt.Errorf("Could not inject image %q into Dockerfile of image %q", image, iTarget.ConfigurationRef) 383 } 384 } 385 386 newDf, err := ast.Print() 387 if err != nil { 388 return model.ImageTarget{}, errors.Wrap(err, "injectImageDependencies") 389 } 390 391 switch bd := iTarget.BuildDetails.(type) { 392 case model.DockerBuild: 393 bd.Dockerfile = newDf.String() 394 iTarget = iTarget.WithBuildDetails(bd) 395 case model.FastBuild: 396 bd.BaseDockerfile = newDf.String() 397 iTarget = iTarget.WithBuildDetails(bd) 398 } 399 400 return iTarget, nil 401 }