github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/build/image_builder.go (about) 1 package build 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 8 "github.com/distribution/reference" 9 "k8s.io/apimachinery/pkg/types" 10 11 "github.com/tilt-dev/clusterid" 12 "github.com/tilt-dev/tilt/internal/container" 13 "github.com/tilt-dev/tilt/internal/ignore" 14 "github.com/tilt-dev/tilt/internal/k8s" 15 "github.com/tilt-dev/tilt/pkg/apis" 16 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 17 "github.com/tilt-dev/tilt/pkg/model" 18 ) 19 20 type ImageBuilder struct { 21 db *DockerBuilder 22 custb *CustomBuilder 23 kl KINDLoader 24 } 25 26 func NewImageBuilder(db *DockerBuilder, custb *CustomBuilder, kl KINDLoader) *ImageBuilder { 27 return &ImageBuilder{ 28 db: db, 29 custb: custb, 30 kl: kl, 31 } 32 } 33 34 func (ib *ImageBuilder) CanReuseRef(ctx context.Context, iTarget model.ImageTarget, ref reference.NamedTagged) (bool, error) { 35 switch iTarget.BuildDetails.(type) { 36 case model.DockerBuild: 37 return ib.db.ImageExists(ctx, ref) 38 case model.CustomBuild: 39 // Custom build doesn't have a good way to check if the ref still exists in the image 40 // store, so just assume we can. 41 return true, nil 42 } 43 return false, fmt.Errorf("image %q has no valid buildDetails (neither "+ 44 "DockerBuild nor CustomBuild)", iTarget.ImageMapSpec.Selector) 45 } 46 47 // Build the image, and push it if necessary. 48 // 49 // Note that this function can return partial results on an error. 50 // 51 // The error is simply the "main" build failure reason. 52 func (ib *ImageBuilder) Build(ctx context.Context, 53 iTarget model.ImageTarget, 54 customBuildCmd *v1alpha1.Cmd, 55 cluster *v1alpha1.Cluster, 56 imageMaps map[types.NamespacedName]*v1alpha1.ImageMap, 57 ps *PipelineState) (container.TaggedRefs, []v1alpha1.DockerImageStageStatus, error) { 58 refs, stages, err := ib.buildOnly(ctx, iTarget, customBuildCmd, cluster, imageMaps, ps) 59 if err != nil { 60 return refs, stages, err 61 } 62 63 pushStage := ib.push(ctx, refs, ps, iTarget, cluster) 64 if pushStage != nil { 65 stages = append(stages, *pushStage) 66 } 67 68 if pushStage != nil && pushStage.Error != "" { 69 err = errors.New(pushStage.Error) 70 } 71 72 return refs, stages, err 73 } 74 75 // Build the image, but don't do any push. 76 func (ib *ImageBuilder) buildOnly(ctx context.Context, 77 iTarget model.ImageTarget, 78 customBuildCmd *v1alpha1.Cmd, 79 cluster *v1alpha1.Cluster, 80 imageMaps map[types.NamespacedName]*v1alpha1.ImageMap, 81 ps *PipelineState, 82 ) (container.TaggedRefs, []v1alpha1.DockerImageStageStatus, error) { 83 refs, err := iTarget.Refs(cluster) 84 if err != nil { 85 return container.TaggedRefs{}, nil, err 86 } 87 88 userFacingRefName := container.FamiliarString(refs.ConfigurationRef) 89 90 switch bd := iTarget.BuildDetails.(type) { 91 case model.DockerBuild: 92 ps.StartPipelineStep(ctx, "Building Dockerfile: [%s]", userFacingRefName) 93 defer ps.EndPipelineStep(ctx) 94 95 filter := ignore.CreateBuildContextFilter(bd.DockerImageSpec.ContextIgnores) 96 return ib.db.BuildImage(ctx, ps, refs, bd.DockerImageSpec, 97 cluster, 98 imageMaps, 99 filter) 100 101 case model.CustomBuild: 102 ps.StartPipelineStep(ctx, "Building Custom Build: [%s]", userFacingRefName) 103 defer ps.EndPipelineStep(ctx) 104 refs, err := ib.custb.Build(ctx, refs, bd.CmdImageSpec, customBuildCmd, imageMaps) 105 return refs, nil, err 106 } 107 108 // Theoretically this should never trip b/c we `validate` the manifest beforehand...? 109 // If we get here, something is very wrong. 110 return container.TaggedRefs{}, nil, fmt.Errorf("image %q has no valid buildDetails (neither "+ 111 "DockerBuild nor CustomBuild)", refs.ConfigurationRef) 112 } 113 114 // Push the image if the cluster requires it. 115 func (ib *ImageBuilder) push(ctx context.Context, refs container.TaggedRefs, ps *PipelineState, iTarget model.ImageTarget, cluster *v1alpha1.Cluster) *v1alpha1.DockerImageStageStatus { 116 // Skip the push phase entirely if we're on Docker Compose. 117 isDC := cluster != nil && 118 cluster.Spec.Connection != nil && 119 cluster.Spec.Connection.Docker != nil 120 if isDC { 121 return nil 122 } 123 124 // On Kubernetes, we count each push() as a stage, and need to print why 125 // we're skipping if we don't need to push. 126 ps.StartPipelineStep(ctx, "Pushing %s", container.FamiliarString(refs.LocalRef)) 127 defer ps.EndPipelineStep(ctx) 128 129 cbSkip := false 130 if iTarget.IsCustomBuild() { 131 cbSkip = iTarget.CustomBuildInfo().SkipsPush() 132 } 133 134 if cbSkip { 135 ps.Printf(ctx, "Skipping push: custom_build() configured to handle push itself") 136 return nil 137 } 138 139 // We can also skip the push of the image if it isn't used 140 // in any k8s resources! (e.g., it's consumed by another image). 141 if iTarget.ClusterNeeds() != v1alpha1.ClusterImageNeedsPush { 142 ps.Printf(ctx, "Skipping push: base image does not need deploy") 143 return nil 144 } 145 146 if ib.db.WillBuildToKubeContext(k8s.KubeContext(k8sConnStatus(cluster).Context)) { 147 ps.Printf(ctx, "Skipping push: building on cluster's container runtime") 148 return nil 149 } 150 151 startTime := apis.NowMicro() 152 var err error 153 if ib.shouldUseKINDLoad(refs, cluster) { 154 ps.Printf(ctx, "Loading image to KIND") 155 err := ib.kl.LoadToKIND(ps.AttachLogger(ctx), cluster, refs.LocalRef) 156 endTime := apis.NowMicro() 157 stage := &v1alpha1.DockerImageStageStatus{ 158 Name: "kind load", 159 StartedAt: &startTime, 160 FinishedAt: &endTime, 161 } 162 if err != nil { 163 stage.Error = fmt.Sprintf("Error loading image to KIND: %v", err) 164 } 165 return stage 166 } 167 168 ps.Printf(ctx, "Pushing with Docker client") 169 err = ib.db.PushImage(ps.AttachLogger(ctx), refs.LocalRef) 170 171 endTime := apis.NowMicro() 172 stage := &v1alpha1.DockerImageStageStatus{ 173 Name: "docker push", 174 StartedAt: &startTime, 175 FinishedAt: &endTime, 176 } 177 if err != nil { 178 stage.Error = fmt.Sprintf("docker push: %v", err) 179 } 180 return stage 181 } 182 183 func (ib *ImageBuilder) shouldUseKINDLoad(refs container.TaggedRefs, cluster *v1alpha1.Cluster) bool { 184 isKIND := k8sConnStatus(cluster).Product == string(clusterid.ProductKIND) 185 if !isKIND { 186 return false 187 } 188 189 // if we're using KIND and the image has a separate ref by which it's referred to 190 // in the cluster, that implies that we have a local registry in place, and should 191 // push to that instead of using KIND load. 192 if refs.LocalRef.String() != refs.ClusterRef.String() { 193 return false 194 } 195 196 hasRegistry := cluster.Status.Registry != nil && cluster.Status.Registry.Host != "" 197 if hasRegistry { 198 return false 199 } 200 201 return true 202 } 203 204 func k8sConnStatus(cluster *v1alpha1.Cluster) *v1alpha1.KubernetesClusterConnectionStatus { 205 if cluster != nil && 206 cluster.Status.Connection != nil && 207 cluster.Status.Connection.Kubernetes != nil { 208 return cluster.Status.Connection.Kubernetes 209 } 210 return &v1alpha1.KubernetesClusterConnectionStatus{} 211 }