github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/buildcontrol/image_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/cmdimage" 14 "github.com/tilt-dev/tilt/internal/controllers/core/dockerimage" 15 "github.com/tilt-dev/tilt/internal/controllers/core/kubernetesapply" 16 "github.com/tilt-dev/tilt/internal/store" 17 "github.com/tilt-dev/tilt/internal/store/k8sconv" 18 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 19 "github.com/tilt-dev/tilt/pkg/model" 20 ) 21 22 var _ BuildAndDeployer = &ImageBuildAndDeployer{} 23 24 type ImageBuildAndDeployer struct { 25 dr *dockerimage.Reconciler 26 cr *cmdimage.Reconciler 27 ib *build.ImageBuilder 28 analytics *analytics.TiltAnalytics 29 clock build.Clock 30 ctrlClient ctrlclient.Client 31 r *kubernetesapply.Reconciler 32 } 33 34 func NewImageBuildAndDeployer( 35 dr *dockerimage.Reconciler, 36 cr *cmdimage.Reconciler, 37 ib *build.ImageBuilder, 38 analytics *analytics.TiltAnalytics, 39 c build.Clock, 40 ctrlClient ctrlclient.Client, 41 r *kubernetesapply.Reconciler, 42 ) *ImageBuildAndDeployer { 43 return &ImageBuildAndDeployer{ 44 dr: dr, 45 cr: cr, 46 ib: ib, 47 analytics: analytics, 48 clock: c, 49 ctrlClient: ctrlClient, 50 r: r, 51 } 52 } 53 54 func (ibd *ImageBuildAndDeployer) BuildAndDeploy(ctx context.Context, st store.RStore, specs []model.TargetSpec, stateSet store.BuildStateSet) (resultSet store.BuildResultSet, err error) { 55 iTargets, kTargets := extractImageAndK8sTargets(specs) 56 if len(kTargets) != 1 { 57 return store.BuildResultSet{}, SilentRedirectToNextBuilderf("ImageBuildAndDeployer does not support these specs") 58 } 59 60 kTarget := kTargets[0] 61 kCluster := stateSet[kTarget.ID()].ClusterOrEmpty() 62 63 startTime := time.Now() 64 defer func() { 65 ibd.analytics.Timer("build.image", time.Since(startTime), map[string]string{ 66 "hasError": fmt.Sprintf("%t", err != nil), 67 }) 68 }() 69 70 q, err := NewImageTargetQueue(ctx, iTargets, stateSet, ibd.ib.CanReuseRef) 71 if err != nil { 72 return store.BuildResultSet{}, err 73 } 74 75 // each image target has two stages: one for build, and one for push 76 numStages := q.CountBuilds()*2 + 1 77 78 reused := q.ReusedResults() 79 hasReusedStep := len(reused) > 0 80 if hasReusedStep { 81 numStages++ 82 } 83 84 hasDeleteStep := stateSet.FullBuildTriggered() 85 if hasDeleteStep { 86 numStages++ 87 } 88 89 ps := build.NewPipelineState(ctx, numStages, ibd.clock) 90 defer func() { ps.End(ctx, err) }() 91 92 if hasDeleteStep { 93 ps.StartPipelineStep(ctx, "Force update") 94 err = ibd.delete(ps.AttachLogger(ctx), kTarget, kCluster) 95 if err != nil { 96 return store.BuildResultSet{}, WrapDontFallBackError(err) 97 } 98 ps.EndPipelineStep(ctx) 99 } 100 101 if hasReusedStep { 102 ps.StartPipelineStep(ctx, "Loading cached images") 103 for _, result := range reused { 104 ps.Printf(ctx, "- %s", store.LocalImageRefFromBuildResult(result)) 105 } 106 ps.EndPipelineStep(ctx) 107 } 108 109 imageMapSet := make(map[types.NamespacedName]*v1alpha1.ImageMap, len(kTarget.ImageMaps)) 110 for _, iTarget := range iTargets { 111 if iTarget.IsLiveUpdateOnly { 112 continue 113 } 114 115 var im v1alpha1.ImageMap 116 nn := types.NamespacedName{Name: iTarget.ImageMapName()} 117 err := ibd.ctrlClient.Get(ctx, nn, &im) 118 if err != nil { 119 return nil, err 120 } 121 imageMapSet[nn] = im.DeepCopy() 122 } 123 124 err = q.RunBuilds(func(target model.TargetSpec, depResults []store.ImageBuildResult) (store.ImageBuildResult, error) { 125 iTarget, ok := target.(model.ImageTarget) 126 if !ok { 127 return store.ImageBuildResult{}, fmt.Errorf("Not an image target: %T", target) 128 } 129 130 var cmd *v1alpha1.Cmd = nil 131 if iTarget.CmdImageName != "" { 132 nn := types.NamespacedName{Name: iTarget.CmdImageName} 133 cmd = &v1alpha1.Cmd{} 134 err := ibd.ctrlClient.Get(ctx, nn, cmd) 135 if err != nil { 136 return store.ImageBuildResult{}, err 137 } 138 } 139 140 cluster := stateSet[target.ID()].ClusterOrEmpty() 141 return ibd.build(ctx, iTarget, cmd, cluster, imageMapSet, ps) 142 }) 143 144 newResults := q.NewResults().ToBuildResultSet() 145 if err != nil { 146 return newResults, WrapDontFallBackError(err) 147 } 148 149 // (If we pass an empty list of refs here (as we will do if only deploying 150 // yaml), we just don't inject any image refs into the yaml, nbd. 151 k8sResult, err := ibd.deploy(ctx, st, ps, kTarget.ID(), kTarget.KubernetesApplySpec, kCluster, imageMapSet) 152 if err != nil { 153 return newResults, WrapDontFallBackError(err) 154 } 155 newResults[kTarget.ID()] = k8sResult 156 return newResults, nil 157 } 158 159 func (ibd *ImageBuildAndDeployer) build( 160 ctx context.Context, 161 iTarget model.ImageTarget, 162 customBuildCmd *v1alpha1.Cmd, 163 cluster *v1alpha1.Cluster, 164 imageMaps map[types.NamespacedName]*v1alpha1.ImageMap, 165 ps *build.PipelineState) (store.ImageBuildResult, error) { 166 switch iTarget.BuildDetails.(type) { 167 case model.DockerBuild: 168 return ibd.dr.ForceApply(ctx, iTarget, cluster, imageMaps, ps) 169 case model.CustomBuild: 170 return ibd.cr.ForceApply(ctx, iTarget, customBuildCmd, cluster, imageMaps, ps) 171 } 172 return store.ImageBuildResult{}, fmt.Errorf("invalid image spec") 173 } 174 175 // Returns: the entities deployed and the namespace of the pod with the given image name/tag. 176 func (ibd *ImageBuildAndDeployer) deploy( 177 ctx context.Context, 178 st store.RStore, 179 ps *build.PipelineState, 180 kTargetID model.TargetID, 181 spec v1alpha1.KubernetesApplySpec, 182 cluster *v1alpha1.Cluster, 183 imageMaps map[types.NamespacedName]*v1alpha1.ImageMap) (store.K8sBuildResult, error) { 184 ps.StartPipelineStep(ctx, "Deploying") 185 defer ps.EndPipelineStep(ctx) 186 187 kTargetNN := types.NamespacedName{Name: kTargetID.Name.String()} 188 status := ibd.r.ForceApply(ctx, kTargetNN, spec, cluster, imageMaps) 189 if status.Error != "" { 190 return store.K8sBuildResult{}, fmt.Errorf("%s", status.Error) 191 } 192 193 filter, err := k8sconv.NewKubernetesApplyFilter(status.ResultYAML) 194 if err != nil { 195 return store.K8sBuildResult{}, err 196 } 197 return store.NewK8sDeployResult(kTargetID, filter), nil 198 } 199 200 // Delete all the resources in the Kubernetes target, to ensure that they restart when 201 // we re-apply them. 202 func (ibd *ImageBuildAndDeployer) delete(ctx context.Context, k8sTarget model.K8sTarget, cluster *v1alpha1.Cluster) error { 203 kTargetNN := types.NamespacedName{Name: k8sTarget.ID().Name.String()} 204 return ibd.r.ForceDelete(ctx, kTargetNN, k8sTarget.KubernetesApplySpec, cluster, "force update") 205 }