github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/pkg/model/manifest.go (about) 1 package model 2 3 import ( 4 "fmt" 5 "net/url" 6 "strings" 7 8 "github.com/distribution/reference" 9 "github.com/google/go-cmp/cmp" 10 "github.com/google/go-cmp/cmp/cmpopts" 11 "github.com/pkg/errors" 12 "k8s.io/apimachinery/pkg/api/validation/path" 13 "k8s.io/apimachinery/pkg/labels" 14 15 "github.com/tilt-dev/tilt/internal/container" 16 "github.com/tilt-dev/tilt/internal/sliceutils" 17 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 18 ) 19 20 // TODO(nick): We should probably get rid of ManifestName completely and just use TargetName everywhere. 21 type ManifestName string 22 type ManifestNameSet map[ManifestName]bool 23 24 func ManifestNames(names []string) []ManifestName { 25 mNames := make([]ManifestName, len(names)) 26 for i, name := range names { 27 mNames[i] = ManifestName(name) 28 } 29 return mNames 30 } 31 32 const MainTiltfileManifestName = ManifestName("(Tiltfile)") 33 34 func (m ManifestName) String() string { return string(m) } 35 func (m ManifestName) TargetName() TargetName { return TargetName(m) } 36 func (m ManifestName) TargetID() TargetID { 37 return TargetID{ 38 Type: TargetTypeManifest, 39 Name: m.TargetName(), 40 } 41 } 42 43 // NOTE: If you modify Manifest, make sure to modify `equalForBuildInvalidation` appropriately 44 type Manifest struct { 45 // Properties for all manifests. 46 Name ManifestName 47 48 // Info needed to build an image. (This struct contains details of DockerBuild, CustomBuild... etc.) 49 ImageTargets []ImageTarget 50 51 // Info needed to deploy. Can be k8s yaml, docker compose, etc. 52 DeployTarget TargetSpec 53 54 // How updates are triggered: 55 // - automatically, when we detect a change 56 // - manually, only when the user tells us to 57 TriggerMode TriggerMode 58 59 // The resource in this manifest will not be built until all of its dependencies have been 60 // ready at least once. 61 ResourceDependencies []ManifestName 62 63 SourceTiltfile ManifestName 64 65 Labels map[string]string 66 } 67 68 func (m Manifest) ID() TargetID { 69 return TargetID{ 70 Type: TargetTypeManifest, 71 Name: m.Name.TargetName(), 72 } 73 } 74 75 func (m Manifest) DependencyIDs() []TargetID { 76 result := []TargetID{} 77 for _, iTarget := range m.ImageTargets { 78 result = append(result, iTarget.ID()) 79 } 80 if !m.DeployTarget.ID().Empty() { 81 result = append(result, m.DeployTarget.ID()) 82 } 83 return result 84 } 85 86 // A map from each target id to the target IDs that depend on it. 87 func (m Manifest) ReverseDependencyIDs() map[TargetID][]TargetID { 88 result := make(map[TargetID][]TargetID) 89 for _, iTarget := range m.ImageTargets { 90 for _, depID := range iTarget.DependencyIDs() { 91 result[depID] = append(result[depID], iTarget.ID()) 92 } 93 } 94 if !m.DeployTarget.ID().Empty() { 95 for _, depID := range m.DeployTarget.DependencyIDs() { 96 result[depID] = append(result[depID], m.DeployTarget.ID()) 97 } 98 } 99 return result 100 } 101 102 func (m Manifest) WithImageTarget(iTarget ImageTarget) Manifest { 103 m.ImageTargets = []ImageTarget{iTarget} 104 return m 105 } 106 107 func (m Manifest) WithImageTargets(iTargets []ImageTarget) Manifest { 108 m.ImageTargets = append([]ImageTarget{}, iTargets...) 109 return m 110 } 111 112 func (m Manifest) ImageTargetAt(i int) ImageTarget { 113 if i < len(m.ImageTargets) { 114 return m.ImageTargets[i] 115 } 116 return ImageTarget{} 117 } 118 119 func (m Manifest) ImageTargetWithID(id TargetID) ImageTarget { 120 for _, target := range m.ImageTargets { 121 if target.ID() == id { 122 return target 123 } 124 } 125 return ImageTarget{} 126 } 127 128 func (m Manifest) LocalTarget() LocalTarget { 129 ret, _ := m.DeployTarget.(LocalTarget) 130 return ret 131 } 132 133 func (m Manifest) IsLocal() bool { 134 _, ok := m.DeployTarget.(LocalTarget) 135 return ok 136 } 137 138 func (m Manifest) DockerComposeTarget() DockerComposeTarget { 139 ret, _ := m.DeployTarget.(DockerComposeTarget) 140 return ret 141 } 142 143 func (m Manifest) IsDC() bool { 144 _, ok := m.DeployTarget.(DockerComposeTarget) 145 return ok 146 } 147 148 func (m Manifest) K8sTarget() K8sTarget { 149 ret, _ := m.DeployTarget.(K8sTarget) 150 return ret 151 } 152 153 func (m Manifest) IsK8s() bool { 154 _, ok := m.DeployTarget.(K8sTarget) 155 return ok 156 } 157 158 func (m Manifest) PodReadinessMode() PodReadinessMode { 159 if k8sTarget, ok := m.DeployTarget.(K8sTarget); ok { 160 return k8sTarget.PodReadinessMode 161 } 162 return PodReadinessNone 163 } 164 165 func (m Manifest) WithDeployTarget(t TargetSpec) Manifest { 166 switch typedTarget := t.(type) { 167 case K8sTarget: 168 typedTarget.Name = m.Name.TargetName() 169 t = typedTarget 170 case DockerComposeTarget: 171 typedTarget.Name = m.Name.TargetName() 172 t = typedTarget 173 } 174 m.DeployTarget = t 175 return m 176 } 177 178 func (m Manifest) WithTriggerMode(mode TriggerMode) Manifest { 179 m.TriggerMode = mode 180 return m 181 } 182 183 func (m Manifest) TargetIDSet() map[TargetID]bool { 184 result := make(map[TargetID]bool) 185 specs := m.TargetSpecs() 186 for _, spec := range specs { 187 result[spec.ID()] = true 188 } 189 return result 190 } 191 192 func (m Manifest) TargetSpecs() []TargetSpec { 193 result := []TargetSpec{} 194 for _, t := range m.ImageTargets { 195 result = append(result, t) 196 } 197 if m.DeployTarget != nil { 198 result = append(result, m.DeployTarget) 199 } 200 return result 201 } 202 203 func (m Manifest) IsImageDeployed(iTarget ImageTarget) bool { 204 id := iTarget.ID() 205 for _, depID := range m.DeployTarget.DependencyIDs() { 206 if depID == id { 207 return true 208 } 209 } 210 return false 211 } 212 213 func (m Manifest) LocalPaths() []string { 214 switch di := m.DeployTarget.(type) { 215 case LocalTarget: 216 return di.Dependencies() 217 case ImageTarget, K8sTarget, DockerComposeTarget: 218 // fall through to paths for image targets, below 219 } 220 paths := []string{} 221 for _, iTarget := range m.ImageTargets { 222 paths = append(paths, iTarget.LocalPaths()...) 223 } 224 return sliceutils.DedupedAndSorted(paths) 225 } 226 227 func (m Manifest) WithLabels(labels map[string]string) Manifest { 228 m.Labels = make(map[string]string) 229 for k, v := range labels { 230 m.Labels[k] = v 231 } 232 return m 233 } 234 235 func (m Manifest) Validate() error { 236 if m.Name == "" { 237 return fmt.Errorf("[validate] manifest missing name: %+v", m) 238 } 239 240 if errs := path.ValidatePathSegmentName(m.Name.String(), false); len(errs) != 0 { 241 return fmt.Errorf("invalid value %q: %v", m.Name.String(), errs[0]) 242 } 243 244 for _, iTarget := range m.ImageTargets { 245 err := iTarget.Validate() 246 if err != nil { 247 return err 248 } 249 } 250 251 if m.DeployTarget != nil { 252 err := m.DeployTarget.Validate() 253 if err != nil { 254 return err 255 } 256 } 257 258 return nil 259 } 260 261 func (m *Manifest) ClusterName() string { 262 if m.IsDC() { 263 return v1alpha1.ClusterNameDocker 264 } 265 if m.IsK8s() { 266 return v1alpha1.ClusterNameDefault 267 } 268 return "" 269 } 270 271 // Infer image properties for each image. 272 func (m *Manifest) InferImageProperties() error { 273 var deployImageIDs []TargetID 274 if m.DeployTarget != nil { 275 deployImageIDs = m.DeployTarget.DependencyIDs() 276 } 277 deployImageIDSet := make(map[TargetID]bool, len(deployImageIDs)) 278 for _, depID := range deployImageIDs { 279 deployImageIDSet[depID] = true 280 } 281 282 for i, iTarget := range m.ImageTargets { 283 // An image only needs to be pushed if it's used in-cluster. 284 clusterNeeds := v1alpha1.ClusterImageNeedsBase 285 if deployImageIDSet[iTarget.ID()] { 286 clusterNeeds = v1alpha1.ClusterImageNeedsPush 287 } 288 289 iTarget, err := iTarget.inferImageProperties(clusterNeeds, m.ClusterName()) 290 if err != nil { 291 return fmt.Errorf("manifest %s: %v", m.Name, err) 292 } 293 294 m.ImageTargets[i] = iTarget 295 } 296 return nil 297 } 298 299 // Assemble selectors that point to other API objects created by this manifest. 300 func (m *Manifest) InferLiveUpdateSelectors() error { 301 dag, err := NewTargetGraph(m.TargetSpecs()) 302 if err != nil { 303 return err 304 } 305 306 for i, iTarget := range m.ImageTargets { 307 luSpec := iTarget.LiveUpdateSpec 308 luName := iTarget.LiveUpdateName 309 if luName == "" || (len(luSpec.Syncs) == 0 && len(luSpec.Execs) == 0) { 310 continue 311 } 312 313 if m.IsK8s() { 314 kSelector := luSpec.Selector.Kubernetes 315 if kSelector == nil { 316 kSelector = &v1alpha1.LiveUpdateKubernetesSelector{} 317 luSpec.Selector.Kubernetes = kSelector 318 } 319 320 if kSelector.ApplyName == "" { 321 kSelector.ApplyName = m.Name.String() 322 } 323 if kSelector.DiscoveryName == "" { 324 kSelector.DiscoveryName = m.Name.String() 325 } 326 327 // infer a selector from the ImageTarget if a container name 328 // selector was not specified (currently, this is always the case 329 // except in some k8s_custom_deploy configurations) 330 if kSelector.ContainerName == "" { 331 if iTarget.IsLiveUpdateOnly { 332 // use the selector (image name) as-is; Tilt isn't building 333 // this image, so no image name rewriting will occur 334 kSelector.Image = iTarget.Selector 335 } else { 336 // refer to the ImageMap so that the LU reconciler can find 337 // the true image name after any registry rewriting 338 kSelector.ImageMapName = iTarget.ImageMapName() 339 } 340 } 341 } 342 343 if m.IsDC() { 344 dcSelector := luSpec.Selector.DockerCompose 345 if dcSelector == nil { 346 dcSelector = &v1alpha1.LiveUpdateDockerComposeSelector{} 347 luSpec.Selector.DockerCompose = dcSelector 348 } 349 350 if dcSelector.Service == "" { 351 dcSelector.Service = m.Name.String() 352 } 353 } 354 355 luSpec.Sources = nil 356 err := dag.VisitTree(iTarget, func(dep TargetSpec) error { 357 // Relies on the idea that ImageTargets creates 358 // FileWatches and ImageMaps related to the ImageTarget ID. 359 id := dep.ID() 360 fw := id.String() 361 362 // LiveUpdateOnly targets do NOT have an associated image map 363 var imageMap string 364 if depImg, ok := dep.(ImageTarget); ok && !depImg.IsLiveUpdateOnly { 365 imageMap = id.Name.String() 366 } 367 368 luSpec.Sources = append(luSpec.Sources, v1alpha1.LiveUpdateSource{ 369 FileWatch: fw, 370 ImageMap: imageMap, 371 }) 372 return nil 373 }) 374 if err != nil { 375 return err 376 } 377 378 iTarget.LiveUpdateSpec = luSpec 379 m.ImageTargets[i] = iTarget 380 } 381 return nil 382 } 383 384 // Set DisableSource for any pieces of the manifest that are disable-able but not yet in the API 385 func (m Manifest) WithDisableSource(disableSource *v1alpha1.DisableSource) Manifest { 386 if lt, ok := m.DeployTarget.(LocalTarget); ok { 387 lt.ServeCmdDisableSource = disableSource 388 m.DeployTarget = lt 389 } 390 return m 391 } 392 393 // ChangesInvalidateBuild checks whether the changes from old => new manifest 394 // invalidate our build of the old one; i.e. if we're replacing `old` with `new`, 395 // should we perform a full rebuild? 396 func ChangesInvalidateBuild(old, new Manifest) bool { 397 dockerEq, k8sEq, dcEq, localEq := old.fieldGroupsEqualForBuildInvalidation(new) 398 399 return !dockerEq || !k8sEq || !dcEq || !localEq 400 } 401 402 // Compare all fields that might invalidate a build 403 func (m1 Manifest) fieldGroupsEqualForBuildInvalidation(m2 Manifest) (dockerEq, k8sEq, dcEq, localEq bool) { 404 dockerEq = equalForBuildInvalidation(m1.ImageTargets, m2.ImageTargets) 405 406 dc1 := m1.DockerComposeTarget() 407 dc2 := m2.DockerComposeTarget() 408 dcEq = equalForBuildInvalidation(dc1, dc2) 409 410 k8s1 := m1.K8sTarget() 411 k8s2 := m2.K8sTarget() 412 k8sEq = equalForBuildInvalidation(k8s1, k8s2) 413 414 lt1 := m1.LocalTarget() 415 lt2 := m2.LocalTarget() 416 localEq = equalForBuildInvalidation(lt1, lt2) 417 418 return dockerEq, dcEq, k8sEq, localEq 419 } 420 421 func (m Manifest) ManifestName() ManifestName { 422 return m.Name 423 } 424 425 func LocalRefSelectorsForManifests(manifests []Manifest, clusters map[string]*v1alpha1.Cluster) []container.RefSelector { 426 var res []container.RefSelector 427 for _, m := range manifests { 428 cluster := clusters[m.ClusterName()] 429 for _, iTarg := range m.ImageTargets { 430 refs, err := iTarg.Refs(cluster) 431 if err != nil { 432 // silently ignore any invalid image references because this 433 // logic is only used for Docker pruning, and we can't prune 434 // something invalid anyway 435 continue 436 } 437 sel := container.NameSelector(refs.LocalRef()) 438 res = append(res, sel) 439 } 440 } 441 return res 442 } 443 444 var _ TargetSpec = Manifest{} 445 446 // Self-contained spec for syncing files from local to a container. 447 // 448 // Unlike v1alpha1.LiveUpdateSync, all fields of this object must be absolute 449 // paths. 450 type Sync struct { 451 LocalPath string 452 ContainerPath string 453 } 454 455 // Self-contained spec for running in a container. 456 // 457 // Unlike v1alpha1.LiveUpdateExec, all fields of this object must be absolute 458 // paths. 459 type Run struct { 460 // Required. The command to run. 461 Cmd Cmd 462 // Optional. If not specified, this command runs on every change. 463 // If specified, we only run the Cmd if the changed file matches a trigger. 464 Triggers PathSet 465 } 466 467 func (r Run) WithTriggers(paths []string, baseDir string) Run { 468 if len(paths) > 0 { 469 r.Triggers = PathSet{ 470 Paths: paths, 471 BaseDirectory: baseDir, 472 } 473 } else { 474 r.Triggers = PathSet{} 475 } 476 return r 477 } 478 479 type PortForward struct { 480 // The port to connect to inside the deployed container. 481 // If 0, we will connect to the first containerPort. 482 ContainerPort int 483 484 // The port to expose on the current machine. 485 LocalPort int 486 487 // Optional host to bind to on the current machine (localhost by default) 488 Host string 489 490 // Optional name of the port forward; if given, used as text of the URL 491 // displayed in the web UI (e.g. <a href="localhost:8888">Debugger</a>) 492 Name string 493 494 // Optional path at the port forward that we link to in UIs 495 // (useful if e.g. nothing lives at "/" and devs will always 496 // want "localhost:xxxx/v1/app") 497 // (Private with getter/setter b/c may be nil.) 498 path *url.URL 499 } 500 501 func (pf PortForward) PathForAppend() string { 502 if pf.path == nil { 503 return "" 504 } 505 return strings.TrimPrefix(pf.path.String(), "/") 506 } 507 508 func (pf PortForward) WithPath(p *url.URL) PortForward { 509 pf.path = p 510 return pf 511 } 512 513 func MustPortForward(local int, container int, host string, name string, path string) PortForward { 514 var parsedPath *url.URL 515 var err error 516 if path != "" { 517 parsedPath, err = url.Parse(path) 518 if err != nil { 519 panic(err) 520 } 521 } 522 return PortForward{ 523 ContainerPort: container, 524 LocalPort: local, 525 Host: host, 526 Name: name, 527 path: parsedPath, 528 } 529 } 530 531 // A link associated with resource; may represent a port forward, an endpoint 532 // derived from a Service/Ingress/etc., or a URL manually associated with a 533 // resource via the Tiltfile 534 type Link struct { 535 URL *url.URL 536 537 // Optional name of the link; if given, used as text of the URL 538 // displayed in the web UI (e.g. <a href="localhost:8888">Debugger</a>) 539 Name string 540 } 541 542 func (li Link) URLString() string { return li.URL.String() } 543 544 func NewLink(urlStr string, name string) (Link, error) { 545 u, err := url.Parse(urlStr) 546 if err != nil { 547 return Link{}, errors.Wrapf(err, "parsing URL %q", urlStr) 548 } 549 return Link{ 550 URL: u, 551 Name: name, 552 }, nil 553 } 554 555 func MustNewLink(urlStr string, name string) Link { 556 li, err := NewLink(urlStr, name) 557 if err != nil { 558 panic(err) 559 } 560 return li 561 } 562 563 // ByURL implements sort.Interface based on the URL field. 564 type ByURL []Link 565 566 func (lns ByURL) Len() int { return len(lns) } 567 func (lns ByURL) Less(i, j int) bool { return lns[i].URLString() < lns[j].URLString() } 568 func (lns ByURL) Swap(i, j int) { lns[i], lns[j] = lns[j], lns[i] } 569 570 func PortForwardToLink(pf v1alpha1.Forward) Link { 571 host := pf.Host 572 if host == "" { 573 host = "localhost" 574 } 575 u := fmt.Sprintf("http://%s:%d/%s", host, pf.LocalPort, strings.TrimPrefix(pf.Path, "/")) 576 577 // We panic on error here because we provide the URL format ourselves, 578 // so if it's bad, something is very wrong. 579 return MustNewLink(u, pf.Name) 580 } 581 582 func LinksToURLStrings(lns []Link) []string { 583 res := make([]string, len(lns)) 584 for i, ln := range lns { 585 res[i] = ln.URLString() 586 } 587 return res 588 } 589 590 var imageTargetAllowUnexported = cmp.AllowUnexported(ImageTarget{}) 591 var dcTargetAllowUnexported = cmp.AllowUnexported(DockerComposeTarget{}) 592 var labelRequirementAllowUnexported = cmp.AllowUnexported(labels.Requirement{}) 593 var k8sTargetAllowUnexported = cmp.AllowUnexported(K8sTarget{}) 594 var localTargetAllowUnexported = cmp.AllowUnexported(LocalTarget{}) 595 var selectorAllowUnexported = cmp.AllowUnexported(container.RefSelector{}) 596 var refSetAllowUnexported = cmp.AllowUnexported(container.RefSet{}) 597 var portForwardPathAllowUnexported = cmp.AllowUnexported(PortForward{}) 598 var ignoreCustomBuildDepsField = cmpopts.IgnoreFields(CustomBuild{}, "Deps") 599 var ignoreLocalTargetDepsField = cmpopts.IgnoreFields(LocalTarget{}, "Deps") 600 var ignoreDockerBuildCacheFrom = cmpopts.IgnoreFields(DockerBuild{}, "CacheFrom") 601 var ignoreLabels = cmpopts.IgnoreFields(Manifest{}, "Labels") 602 var ignoreDockerComposeProject = cmpopts.IgnoreFields(v1alpha1.DockerComposeServiceSpec{}, "Project") 603 var ignoreRegistryFields = cmpopts.IgnoreFields(v1alpha1.RegistryHosting{}, "HostFromClusterNetwork", "Help") 604 605 // ignoreLinks ignores user-defined links for the purpose of build invalidation 606 // 607 // This is done both because they don't actually invalidate the build AND because url.URL is not directly comparable 608 // in all cases (e.g. a URL with a user@ value will result in url.URL->User being populated which has unexported fields). 609 var ignoreLinks = cmpopts.IgnoreTypes(Link{}) 610 611 var dockerRefEqual = cmp.Comparer(func(a, b reference.Named) bool { 612 aNil := a == nil 613 bNil := b == nil 614 if aNil && bNil { 615 return true 616 } 617 618 if aNil != bNil { 619 return false 620 } 621 622 return a.String() == b.String() 623 }) 624 625 // Determine whether interfaces x and y are equal, excluding fields that don't invalidate a build. 626 func equalForBuildInvalidation(x, y interface{}) bool { 627 return cmp.Equal(x, y, 628 cmpopts.EquateEmpty(), 629 imageTargetAllowUnexported, 630 dcTargetAllowUnexported, 631 labelRequirementAllowUnexported, 632 k8sTargetAllowUnexported, 633 localTargetAllowUnexported, 634 selectorAllowUnexported, 635 refSetAllowUnexported, 636 portForwardPathAllowUnexported, 637 dockerRefEqual, 638 639 // deps changes don't invalidate a build, so don't compare fields used only for deps 640 ignoreCustomBuildDepsField, 641 ignoreLocalTargetDepsField, 642 643 // DockerBuild.CacheFrom doesn't invalidate a build (b/c it affects HOW we build but 644 // shouldn't affect the result of the build), so don't compare these fields 645 ignoreDockerBuildCacheFrom, 646 647 // user-added labels don't invalidate a build 648 ignoreLabels, 649 650 // user-added links don't invalidate a build 651 ignoreLinks, 652 653 // We don't want a change to the DockerCompose Project to invalidate 654 // all individual services. We track the service-specific YAML with 655 // a separate ServiceYAML field. 656 ignoreDockerComposeProject, 657 658 // the RegistryHosting spec includes informational fields (Help) as 659 // well as some unused by Tilt (HostFromClusterNetwork) 660 ignoreRegistryFields, 661 ) 662 }