github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/pkg/model/image_target.go (about) 1 package model 2 3 import ( 4 "fmt" 5 6 "github.com/google/go-cmp/cmp" 7 8 "github.com/tilt-dev/tilt/internal/container" 9 "github.com/tilt-dev/tilt/internal/sliceutils" 10 "github.com/tilt-dev/tilt/pkg/apis" 11 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 12 ) 13 14 type ImageTarget struct { 15 // An apiserver-driven data model for injecting the image into other resources. 16 v1alpha1.ImageMapSpec 17 18 // An apiserver-driven data model for live-updating containers. 19 LiveUpdateName string 20 LiveUpdateSpec v1alpha1.LiveUpdateSpec 21 LiveUpdateReconciler bool 22 23 // An apiserver-driven data model for using docker to build images. 24 DockerImageName string 25 26 // Name for both the CmdImage and the Cmd it manages. 27 CmdImageName string 28 29 BuildDetails BuildDetails 30 31 // In a live-update-only image, we don't inject the image into the Kubernetes 32 // deploy, we only live-update to the deployed object. See this issue: 33 // 34 // https://github.com/tilt-dev/tilt/issues/4577 35 // 36 // This is a hacky way to model this right now until we 37 // firm up how images work in the apiserver. 38 IsLiveUpdateOnly bool 39 40 FileWatchIgnores []v1alpha1.IgnoreDef 41 } 42 43 var _ TargetSpec = ImageTarget{} 44 45 func MustNewImageTarget(ref container.RefSelector) ImageTarget { 46 return ImageTarget{}.MustWithRef(ref) 47 } 48 49 func ImageID(ref container.RefSelector) TargetID { 50 name := TargetName("") 51 if !ref.Empty() { 52 name = TargetName(apis.SanitizeName(container.FamiliarString(ref))) 53 } 54 return TargetID{ 55 Type: TargetTypeImage, 56 Name: name, 57 } 58 } 59 60 func (i ImageTarget) ImageMapName() string { 61 return i.ID().Name.String() 62 } 63 64 func (i ImageTarget) GetFileWatchIgnores() []v1alpha1.IgnoreDef { 65 return i.FileWatchIgnores 66 } 67 68 func (i ImageTarget) WithFileWatchIgnores(ignores []v1alpha1.IgnoreDef) ImageTarget { 69 i.FileWatchIgnores = ignores 70 return i 71 } 72 73 // Modified both FileWatchIgnores and ContextIgnores. Useful in tests where they're the same. 74 func (i ImageTarget) WithIgnores(ignores []v1alpha1.IgnoreDef) ImageTarget { 75 i.FileWatchIgnores = ignores 76 switch bd := i.BuildDetails.(type) { 77 case DockerBuild: 78 bd.DockerImageSpec.ContextIgnores = ignores 79 i.BuildDetails = bd 80 } 81 return i 82 } 83 84 func (i ImageTarget) MustWithRef(ref container.RefSelector) ImageTarget { 85 i.ImageMapSpec.Selector = ref.RefFamiliarString() 86 i.ImageMapSpec.MatchExact = ref.MatchExact() 87 return i 88 } 89 90 func (i ImageTarget) WithLiveUpdateSpec(name string, luSpec v1alpha1.LiveUpdateSpec) ImageTarget { 91 if luSpec.Selector.Kubernetes == nil { 92 luSpec.Selector.Kubernetes = i.LiveUpdateSpec.Selector.Kubernetes 93 } 94 i.LiveUpdateName = name 95 i.LiveUpdateSpec = luSpec 96 return i 97 } 98 99 func (i ImageTarget) ID() TargetID { 100 return TargetID{ 101 Type: TargetTypeImage, 102 Name: TargetName(apis.SanitizeName(i.ImageMapSpec.Selector)), 103 } 104 } 105 106 func (i ImageTarget) DependencyIDs() []TargetID { 107 deps := i.ImageMapDeps() 108 result := make([]TargetID, 0, len(deps)) 109 for _, im := range deps { 110 result = append(result, TargetID{ 111 Type: TargetTypeImage, 112 Name: TargetName(im), 113 }) 114 } 115 return result 116 } 117 118 func (i ImageTarget) ImageMapDeps() []string { 119 switch bd := i.BuildDetails.(type) { 120 case DockerBuild: 121 return bd.ImageMaps 122 case CustomBuild: 123 return bd.ImageMaps 124 } 125 return nil 126 } 127 128 func (i ImageTarget) WithImageMapDeps(names []string) ImageTarget { 129 switch bd := i.BuildDetails.(type) { 130 case DockerBuild: 131 bd.ImageMaps = sliceutils.Dedupe(names) 132 i.BuildDetails = bd 133 case CustomBuild: 134 bd.ImageMaps = sliceutils.Dedupe(names) 135 i.BuildDetails = bd 136 default: 137 if len(names) > 0 { 138 panic(fmt.Sprintf("image does not support image deps: %v", i.ID())) 139 } 140 } 141 return i 142 } 143 144 func (i ImageTarget) Validate() error { 145 if i.ImageMapSpec.Selector == "" { 146 return fmt.Errorf("[Validate] Image target missing image ref: %+v", i.BuildDetails) 147 } 148 149 selector, err := container.SelectorFromImageMap(i.ImageMapSpec) 150 if err != nil { 151 return fmt.Errorf("[Validate]: %v", err) 152 } 153 154 refs, err := container.NewRefSet(selector, nil) 155 if err != nil { 156 return fmt.Errorf("[Validate]: %v", err) 157 } 158 159 if err := refs.Validate(); err != nil { 160 return fmt.Errorf("[Validate] Image %q refset failed validation: %v", i.ImageMapSpec.Selector, err) 161 } 162 163 switch bd := i.BuildDetails.(type) { 164 case DockerBuild: 165 if bd.Context == "" { 166 return fmt.Errorf("[Validate] Image %q missing build path", i.ImageMapSpec.Selector) 167 } 168 case CustomBuild: 169 if !i.IsLiveUpdateOnly && len(bd.Args) == 0 { 170 return fmt.Errorf( 171 "[Validate] CustomBuild command must not be empty", 172 ) 173 } 174 case DockerComposeBuild: 175 if bd.Service == "" { 176 return fmt.Errorf("[Validate] DockerComposeBuild missing service name") 177 } 178 default: 179 return fmt.Errorf( 180 "[Validate] Image %q has unsupported %T build details", i.ImageMapSpec.Selector, bd) 181 } 182 183 return nil 184 } 185 186 type BuildDetails interface { 187 buildDetails() 188 } 189 190 func (i ImageTarget) DockerBuildInfo() DockerBuild { 191 ret, _ := i.BuildDetails.(DockerBuild) 192 return ret 193 } 194 195 func (i ImageTarget) IsDockerBuild() bool { 196 _, ok := i.BuildDetails.(DockerBuild) 197 return ok 198 } 199 200 func (i ImageTarget) CustomBuildInfo() CustomBuild { 201 ret, _ := i.BuildDetails.(CustomBuild) 202 return ret 203 } 204 205 func (i ImageTarget) IsCustomBuild() bool { 206 _, ok := i.BuildDetails.(CustomBuild) 207 return ok 208 } 209 210 func (i ImageTarget) DockerComposeBuildInfo() DockerComposeBuild { 211 ret, _ := i.BuildDetails.(DockerComposeBuild) 212 return ret 213 } 214 215 func (i ImageTarget) IsDockerComposeBuild() bool { 216 _, ok := i.BuildDetails.(DockerComposeBuild) 217 return ok 218 } 219 220 func (i ImageTarget) WithDockerImage(spec v1alpha1.DockerImageSpec) ImageTarget { 221 return i.WithBuildDetails(DockerBuild{DockerImageSpec: spec}) 222 } 223 224 func (i ImageTarget) WithBuildDetails(details BuildDetails) ImageTarget { 225 i.BuildDetails = details 226 227 cb, ok := details.(CustomBuild) 228 isEmptyLiveUpdateSpec := len(i.LiveUpdateSpec.Syncs) == 0 && len(i.LiveUpdateSpec.Execs) == 0 229 if ok && cmp.Equal(cb.Args, ToHostCmd(":").Argv) && !isEmptyLiveUpdateSpec { 230 // NOTE(nick): This is a hack for the file_sync_only extension 231 // until we come up with a real API for specifying live update 232 // without an image build. 233 i.IsLiveUpdateOnly = true 234 } 235 return i 236 } 237 238 func (i ImageTarget) WithOverrideCommand(cmd Cmd) ImageTarget { 239 i.ImageMapSpec.OverrideCommand = &v1alpha1.ImageMapOverrideCommand{ 240 Command: cmd.Argv, 241 } 242 return i 243 } 244 245 func (i ImageTarget) LocalPaths() []string { 246 switch bd := i.BuildDetails.(type) { 247 case DockerBuild: 248 return []string{bd.Context} 249 case CustomBuild: 250 return append([]string(nil), bd.Deps...) 251 case DockerComposeBuild: 252 return []string{bd.Context} 253 } 254 return nil 255 } 256 257 func (i ImageTarget) ClusterNeeds() v1alpha1.ClusterImageNeeds { 258 switch bd := i.BuildDetails.(type) { 259 case DockerBuild: 260 return bd.DockerImageSpec.ClusterNeeds 261 case CustomBuild: 262 return bd.CmdImageSpec.ClusterNeeds 263 } 264 return v1alpha1.ClusterImageNeedsBase 265 } 266 267 // TODO(nick): This method should be deleted. We should just de-dupe and sort LocalPaths once 268 // when we create it, rather than have a duplicate method that does the "right" thing. 269 func (i ImageTarget) Dependencies() []string { 270 return sliceutils.DedupedAndSorted(i.LocalPaths()) 271 } 272 273 func (i ImageTarget) Refs(cluster *v1alpha1.Cluster) (container.RefSet, error) { 274 refs, err := container.RefSetFromImageMap(i.ImageMapSpec, cluster) 275 if err != nil { 276 return container.RefSet{}, err 277 } 278 279 // I (Nick) am deeply unhappy with the parameters of CustomBuild. They're not 280 // well-specified, and often interact in weird and unpredictable ways. This 281 // function is a good example. 282 // 283 // custom_build(tag) means "My custom_build script already has a tag that it 284 // wants to use". In practice, it becomes the "You can't tell me what to do" 285 // flag. 286 // 287 // custom_build(skips_local_docker) means "My custom_build script doesn't use 288 // Docker for storage, so you shouldn't expect to find the image there." In 289 // practice, it becomes the "You can't touch my outputs" flag. 290 // 291 // When used together, you have a script that takes no inputs and doesn't let Tilt 292 // fix the outputs. So people use custom_build(tag=x, skips_local_docker=True) to 293 // enable all sorts of off-road experimental image-building flows that need better 294 // primitives. 295 // 296 // For now, when we detect this case, we strip off registry information, since 297 // the script isn't going to use it anyway. This is tightly coupled with 298 // CustomBuilder, which already has similar logic for handling these two cases 299 // together. 300 customBuild, ok := i.BuildDetails.(CustomBuild) 301 if ok && customBuild.OutputMode == v1alpha1.CmdImageOutputRemote && customBuild.OutputTag != "" { 302 refs = refs.WithoutRegistry() 303 } 304 _, ok = i.BuildDetails.(DockerComposeBuild) 305 if ok { 306 refs = refs.WithoutRegistry() 307 } 308 309 return refs, nil 310 } 311 312 // inferImageProperties sets properties on the underlying image spec. 313 // 314 // This should eventually go away but helps bridge some of the Tiltfile/engine 315 // semantics with the apiserver models for now. 316 func (i ImageTarget) inferImageProperties(clusterNeeds v1alpha1.ClusterImageNeeds, clusterName string) (ImageTarget, error) { 317 db, ok := i.BuildDetails.(DockerBuild) 318 if ok { 319 db.DockerImageSpec.Ref = i.ImageMapSpec.Selector 320 db.DockerImageSpec.ClusterNeeds = clusterNeeds 321 db.DockerImageSpec.Cluster = clusterName 322 i.BuildDetails = db 323 } 324 325 cb, ok := i.BuildDetails.(CustomBuild) 326 if ok { 327 cb.CmdImageSpec.Ref = i.ImageMapSpec.Selector 328 cb.CmdImageSpec.ClusterNeeds = clusterNeeds 329 cb.CmdImageSpec.Cluster = clusterName 330 i.BuildDetails = cb 331 } 332 333 return i, nil 334 } 335 336 func ImageTargetsByID(iTargets []ImageTarget) map[TargetID]ImageTarget { 337 result := make(map[TargetID]ImageTarget, len(iTargets)) 338 for _, target := range iTargets { 339 result[target.ID()] = target 340 } 341 return result 342 } 343 344 type DockerBuild struct { 345 v1alpha1.DockerImageSpec 346 } 347 348 func (DockerBuild) buildDetails() {} 349 350 type CustomBuild struct { 351 v1alpha1.CmdImageSpec 352 353 // Deps is a list of file paths that are dependencies of this command. 354 // 355 // TODO(nick): This creates a FileWatch. We should add a RestartOn field 356 // to CmdImageSpec that points to the FileWatch. 357 Deps []string 358 } 359 360 func (CustomBuild) buildDetails() {} 361 362 func (cb CustomBuild) WithTag(t string) CustomBuild { 363 cb.CmdImageSpec.OutputTag = t 364 return cb 365 } 366 367 func (cb CustomBuild) SkipsPush() bool { 368 return cb.OutputMode == v1alpha1.CmdImageOutputLocalDockerAndRemote || 369 cb.OutputMode == v1alpha1.CmdImageOutputRemote 370 } 371 372 type DockerComposeBuild struct { 373 // Service is the name of the Docker Compose service as defined in docker-compose.yaml. 374 Service string 375 376 // Context is the build context absolute path. 377 Context string 378 } 379 380 func (d DockerComposeBuild) buildDetails() { 381 }