github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/buildcontrol/docker_compose_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 ktypes "k8s.io/apimachinery/pkg/types" 10 ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 11 12 "github.com/tilt-dev/tilt/internal/analytics" 13 "github.com/tilt-dev/tilt/internal/controllers/core/cmdimage" 14 "github.com/tilt-dev/tilt/internal/controllers/core/dockercomposeservice" 15 "github.com/tilt-dev/tilt/internal/controllers/core/dockerimage" 16 17 "github.com/tilt-dev/tilt/internal/build" 18 "github.com/tilt-dev/tilt/internal/docker" 19 "github.com/tilt-dev/tilt/internal/store" 20 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 21 "github.com/tilt-dev/tilt/pkg/model" 22 ) 23 24 type DockerComposeBuildAndDeployer struct { 25 dr *dockerimage.Reconciler 26 cr *cmdimage.Reconciler 27 ib *build.ImageBuilder 28 dcsr *dockercomposeservice.Reconciler 29 clock build.Clock 30 ctrlClient ctrlclient.Client 31 } 32 33 var _ BuildAndDeployer = &DockerComposeBuildAndDeployer{} 34 35 func NewDockerComposeBuildAndDeployer( 36 dr *dockerimage.Reconciler, 37 cr *cmdimage.Reconciler, 38 ib *build.ImageBuilder, 39 dcsr *dockercomposeservice.Reconciler, 40 c build.Clock, 41 ctrlClient ctrlclient.Client, 42 ) *DockerComposeBuildAndDeployer { 43 return &DockerComposeBuildAndDeployer{ 44 dr: dr, 45 cr: cr, 46 ib: ib, 47 dcsr: dcsr, 48 clock: c, 49 ctrlClient: ctrlClient, 50 } 51 } 52 53 // Extract the targets we can apply -- DCBaD supports ImageTargets and DockerComposeTargets. 54 // 55 // A given Docker Compose service can be built one of two ways: 56 // - Tilt-managed: Tiltfile includes a `docker_build` or `custom_build` directive for the service's image, so Tilt 57 // will handle the image lifecycle including building/tagging and Live Update (if configured) 58 // - Docker Compose-managed: Building is delegated to Docker Compose via the `--build` flag to the `up` call; 59 // Tilt is responsible for watching file changes but does not handle the builds. 60 // 61 // It's also possible for a service to reference an image but NOT have any corresponding build (e.g. public/registry 62 // hosted images are common for infra deps like nginx). These will not have any ImageTarget. 63 func (bd *DockerComposeBuildAndDeployer) extract(specs []model.TargetSpec) (buildPlan, error) { 64 var tiltManagedImageTargets []model.ImageTarget 65 var dockerComposeImageTarget *model.ImageTarget 66 var dcTargets []model.DockerComposeTarget 67 68 for _, s := range specs { 69 switch s := s.(type) { 70 case model.ImageTarget: 71 if s.IsDockerComposeBuild() { 72 if dockerComposeImageTarget != nil { 73 return buildPlan{}, DontFallBackErrorf( 74 "Target has more than one Docker Compose managed image target") 75 } 76 dcTarget := s 77 dockerComposeImageTarget = &dcTarget 78 } else { 79 tiltManagedImageTargets = append(tiltManagedImageTargets, s) 80 } 81 case model.DockerComposeTarget: 82 dcTargets = append(dcTargets, s) 83 default: 84 // unrecognized target 85 return buildPlan{}, SilentRedirectToNextBuilderf("DockerComposeBuildAndDeployer does not support target type %T", s) 86 } 87 } 88 89 if len(dcTargets) != 1 { 90 return buildPlan{}, SilentRedirectToNextBuilderf( 91 "DockerComposeBuildAndDeployer requires exactly one dcTarget (got %d)", len(dcTargets)) 92 } 93 94 if len(tiltManagedImageTargets) != 0 && dockerComposeImageTarget != nil { 95 return buildPlan{}, DontFallBackErrorf( 96 "Docker Compose target cannot have both Tilt-managed and Docker Compose-managed image targets") 97 } 98 99 return buildPlan{ 100 dockerComposeTarget: dcTargets[0], 101 tiltManagedImageTargets: tiltManagedImageTargets, 102 dockerComposeImageTarget: dockerComposeImageTarget, 103 }, nil 104 } 105 106 func (bd *DockerComposeBuildAndDeployer) BuildAndDeploy(ctx context.Context, st store.RStore, specs []model.TargetSpec, currentState store.BuildStateSet) (res store.BuildResultSet, err error) { 107 plan, err := bd.extract(specs) 108 if err != nil { 109 return store.BuildResultSet{}, err 110 } 111 112 startTime := time.Now() 113 defer func() { 114 analytics.Get(ctx).Timer("build.docker-compose", time.Since(startTime), map[string]string{ 115 "hasError": fmt.Sprintf("%t", err != nil), 116 }) 117 }() 118 119 dcTarget := plan.dockerComposeTarget 120 dcTargetNN := types.NamespacedName{Name: dcTarget.ID().Name.String()} 121 ctx = docker.WithOrchestrator(ctx, model.OrchestratorDC) 122 123 iTargets := plan.tiltManagedImageTargets 124 q, err := NewImageTargetQueue(ctx, plan.tiltManagedImageTargets, currentState, bd.ib.CanReuseRef) 125 if err != nil { 126 return store.BuildResultSet{}, err 127 } 128 129 // base number of stages is the Tilt-managed image builds + the Docker Compose up step (which might be launching 130 // a Tilt-built image OR might build+launch a Docker Compose-managed image) 131 numStages := q.CountBuilds() + 1 132 133 hasDeleteStep := currentState.FullBuildTriggered() 134 if hasDeleteStep { 135 numStages++ 136 } 137 138 reused := q.ReusedResults() 139 hasReusedStep := len(reused) > 0 140 if hasReusedStep { 141 numStages++ 142 } 143 144 ps := build.NewPipelineState(ctx, numStages, bd.clock) 145 defer func() { ps.End(ctx, err) }() 146 147 if hasDeleteStep { 148 ps.StartPipelineStep(ctx, "Force update") 149 err = bd.dcsr.ForceDelete(ps.AttachLogger(ctx), dcTargetNN, dcTarget.Spec, "force update") 150 if err != nil { 151 return store.BuildResultSet{}, WrapDontFallBackError(err) 152 } 153 ps.EndPipelineStep(ctx) 154 } 155 156 if hasReusedStep { 157 ps.StartPipelineStep(ctx, "Loading cached images") 158 for _, result := range reused { 159 ps.Printf(ctx, "- %s", store.LocalImageRefFromBuildResult(result)) 160 } 161 ps.EndPipelineStep(ctx) 162 } 163 164 imageMapSet := make(map[ktypes.NamespacedName]*v1alpha1.ImageMap, len(plan.dockerComposeTarget.Spec.ImageMaps)) 165 for _, iTarget := range iTargets { 166 if iTarget.IsLiveUpdateOnly { 167 continue 168 } 169 170 var im v1alpha1.ImageMap 171 nn := ktypes.NamespacedName{Name: iTarget.ImageMapName()} 172 err := bd.ctrlClient.Get(ctx, nn, &im) 173 if err != nil { 174 return nil, err 175 } 176 imageMapSet[nn] = im.DeepCopy() 177 } 178 179 err = q.RunBuilds(func(target model.TargetSpec, depResults []store.ImageBuildResult) (store.ImageBuildResult, error) { 180 iTarget, ok := target.(model.ImageTarget) 181 if !ok { 182 return store.ImageBuildResult{}, fmt.Errorf("Not an image target: %T", target) 183 } 184 185 var cmd *v1alpha1.Cmd = nil 186 if iTarget.CmdImageName != "" { 187 nn := types.NamespacedName{Name: iTarget.CmdImageName} 188 cmd = &v1alpha1.Cmd{} 189 err := bd.ctrlClient.Get(ctx, nn, cmd) 190 if err != nil { 191 return store.ImageBuildResult{}, err 192 } 193 } 194 195 cluster := currentState[target.ID()].ClusterOrEmpty() 196 return bd.build(ctx, iTarget, cmd, cluster, imageMapSet, ps) 197 }) 198 199 newResults := q.NewResults().ToBuildResultSet() 200 if err != nil { 201 return newResults, err 202 } 203 204 dcManagedBuild := plan.dockerComposeImageTarget != nil 205 var stepName string 206 if dcManagedBuild { 207 stepName = "Building & deploying" 208 } else { 209 stepName = "Deploying" 210 } 211 ps.StartPipelineStep(ctx, stepName) 212 213 status := bd.dcsr.ForceApply(ctx, dcTargetNN, dcTarget.Spec, imageMapSet, dcManagedBuild) 214 ps.EndPipelineStep(ctx) 215 if status.ApplyError != "" { 216 return newResults, fmt.Errorf("%s", status.ApplyError) 217 } 218 219 dcTargetID := plan.dockerComposeTarget.ID() 220 newResults[dcTargetID] = store.NewDockerComposeDeployResult(dcTargetID, status) 221 return newResults, nil 222 } 223 224 func (bd *DockerComposeBuildAndDeployer) build( 225 ctx context.Context, 226 iTarget model.ImageTarget, 227 customBuildCmd *v1alpha1.Cmd, 228 cluster *v1alpha1.Cluster, 229 imageMaps map[types.NamespacedName]*v1alpha1.ImageMap, 230 ps *build.PipelineState) (store.ImageBuildResult, error) { 231 switch iTarget.BuildDetails.(type) { 232 case model.DockerBuild: 233 return bd.dr.ForceApply(ctx, iTarget, cluster, imageMaps, ps) 234 case model.CustomBuild: 235 return bd.cr.ForceApply(ctx, iTarget, customBuildCmd, cluster, imageMaps, ps) 236 } 237 return store.ImageBuildResult{}, fmt.Errorf("invalid image spec") 238 } 239 240 type buildPlan struct { 241 dockerComposeTarget model.DockerComposeTarget 242 243 tiltManagedImageTargets []model.ImageTarget 244 245 dockerComposeImageTarget *model.ImageTarget 246 }