github.com/tilt-dev/tilt@v0.36.0/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(clusterImageNeeds func(TargetID) v1alpha1.ClusterImageNeeds) 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 iTarget, err := iTarget.inferImageProperties( 284 clusterImageNeeds(iTarget.ID()), m.ClusterName()) 285 if err != nil { 286 return fmt.Errorf("manifest %s: %v", m.Name, err) 287 } 288 289 m.ImageTargets[i] = iTarget 290 } 291 return nil 292 } 293 294 // Assemble selectors that point to other API objects created by this manifest. 295 func (m *Manifest) InferLiveUpdateSelectors() error { 296 dag, err := NewTargetGraph(m.TargetSpecs()) 297 if err != nil { 298 return err 299 } 300 301 for i, iTarget := range m.ImageTargets { 302 luSpec := iTarget.LiveUpdateSpec 303 luName := iTarget.LiveUpdateName 304 if luName == "" || (len(luSpec.Syncs) == 0 && len(luSpec.Execs) == 0) { 305 continue 306 } 307 308 if m.IsK8s() { 309 kSelector := luSpec.Selector.Kubernetes 310 if kSelector == nil { 311 kSelector = &v1alpha1.LiveUpdateKubernetesSelector{} 312 luSpec.Selector.Kubernetes = kSelector 313 } 314 315 if kSelector.ApplyName == "" { 316 kSelector.ApplyName = m.Name.String() 317 } 318 if kSelector.DiscoveryName == "" { 319 kSelector.DiscoveryName = m.Name.String() 320 } 321 322 // infer a selector from the ImageTarget if a container name 323 // selector was not specified (currently, this is always the case 324 // except in some k8s_custom_deploy configurations) 325 if kSelector.ContainerName == "" { 326 if iTarget.IsLiveUpdateOnly { 327 // use the selector (image name) as-is; Tilt isn't building 328 // this image, so no image name rewriting will occur 329 kSelector.Image = iTarget.Selector 330 } else { 331 // refer to the ImageMap so that the LU reconciler can find 332 // the true image name after any registry rewriting 333 kSelector.ImageMapName = iTarget.ImageMapName() 334 } 335 } 336 } 337 338 if m.IsDC() { 339 dcSelector := luSpec.Selector.DockerCompose 340 if dcSelector == nil { 341 dcSelector = &v1alpha1.LiveUpdateDockerComposeSelector{} 342 luSpec.Selector.DockerCompose = dcSelector 343 } 344 345 if dcSelector.Service == "" { 346 dcSelector.Service = m.Name.String() 347 } 348 } 349 350 luSpec.Sources = nil 351 err := dag.VisitTree(iTarget, func(dep TargetSpec) error { 352 // Relies on the idea that ImageTargets creates 353 // FileWatches and ImageMaps related to the ImageTarget ID. 354 id := dep.ID() 355 fw := id.String() 356 357 // LiveUpdateOnly targets do NOT have an associated image map 358 var imageMap string 359 if depImg, ok := dep.(ImageTarget); ok && !depImg.IsLiveUpdateOnly { 360 imageMap = id.Name.String() 361 } 362 363 luSpec.Sources = append(luSpec.Sources, v1alpha1.LiveUpdateSource{ 364 FileWatch: fw, 365 ImageMap: imageMap, 366 }) 367 return nil 368 }) 369 if err != nil { 370 return err 371 } 372 373 iTarget.LiveUpdateSpec = luSpec 374 m.ImageTargets[i] = iTarget 375 } 376 return nil 377 } 378 379 // Set DisableSource for any pieces of the manifest that are disable-able but not yet in the API 380 func (m Manifest) WithDisableSource(disableSource *v1alpha1.DisableSource) Manifest { 381 if lt, ok := m.DeployTarget.(LocalTarget); ok { 382 lt.ServeCmdDisableSource = disableSource 383 m.DeployTarget = lt 384 } 385 return m 386 } 387 388 // ChangesInvalidateBuild checks whether the changes from old => new manifest 389 // invalidate our build of the old one; i.e. if we're replacing `old` with `new`, 390 // should we perform a full rebuild? 391 func ChangesInvalidateBuild(old, new Manifest) bool { 392 dockerEq, k8sEq, dcEq, localEq := old.fieldGroupsEqualForBuildInvalidation(new) 393 394 return !dockerEq || !k8sEq || !dcEq || !localEq 395 } 396 397 // Compare all fields that might invalidate a build 398 func (m1 Manifest) fieldGroupsEqualForBuildInvalidation(m2 Manifest) (dockerEq, k8sEq, dcEq, localEq bool) { 399 dockerEq = equalForBuildInvalidation(m1.ImageTargets, m2.ImageTargets) 400 401 dc1 := m1.DockerComposeTarget() 402 dc2 := m2.DockerComposeTarget() 403 dcEq = equalForBuildInvalidation(dc1, dc2) 404 405 k8s1 := m1.K8sTarget() 406 k8s2 := m2.K8sTarget() 407 k8sEq = equalForBuildInvalidation(k8s1, k8s2) 408 409 lt1 := m1.LocalTarget() 410 lt2 := m2.LocalTarget() 411 localEq = equalForBuildInvalidation(lt1, lt2) 412 413 return dockerEq, dcEq, k8sEq, localEq 414 } 415 416 func (m Manifest) ManifestName() ManifestName { 417 return m.Name 418 } 419 420 func LocalRefSelectorsForManifests(manifests []Manifest, clusters map[string]*v1alpha1.Cluster) []container.RefSelector { 421 var res []container.RefSelector 422 for _, m := range manifests { 423 cluster := clusters[m.ClusterName()] 424 for _, iTarg := range m.ImageTargets { 425 refs, err := iTarg.Refs(cluster) 426 if err != nil { 427 // silently ignore any invalid image references because this 428 // logic is only used for Docker pruning, and we can't prune 429 // something invalid anyway 430 continue 431 } 432 sel := container.NameSelector(refs.LocalRef()) 433 res = append(res, sel) 434 } 435 } 436 return res 437 } 438 439 var _ TargetSpec = Manifest{} 440 441 // Self-contained spec for syncing files from local to a container. 442 // 443 // Unlike v1alpha1.LiveUpdateSync, all fields of this object must be absolute 444 // paths. 445 type Sync struct { 446 LocalPath string 447 ContainerPath string 448 } 449 450 // Self-contained spec for running in a container. 451 // 452 // Unlike v1alpha1.LiveUpdateExec, all fields of this object must be absolute 453 // paths. 454 type Run struct { 455 // Required. The command to run. 456 Cmd Cmd 457 // Optional. If not specified, this command runs on every change. 458 // If specified, we only run the Cmd if the changed file matches a trigger. 459 Triggers PathSet 460 } 461 462 func (r Run) WithTriggers(paths []string, baseDir string) Run { 463 if len(paths) > 0 { 464 r.Triggers = PathSet{ 465 Paths: paths, 466 BaseDirectory: baseDir, 467 } 468 } else { 469 r.Triggers = PathSet{} 470 } 471 return r 472 } 473 474 type PortForward struct { 475 // The port to connect to inside the deployed container. 476 // If 0, we will connect to the first containerPort. 477 ContainerPort int 478 479 // The port to expose on the current machine. 480 LocalPort int 481 482 // Optional host to bind to on the current machine (localhost by default) 483 Host string 484 485 // Optional name of the port forward; if given, used as text of the URL 486 // displayed in the web UI (e.g. <a href="localhost:8888">Debugger</a>) 487 Name string 488 489 // Optional path at the port forward that we link to in UIs 490 // (useful if e.g. nothing lives at "/" and devs will always 491 // want "localhost:xxxx/v1/app") 492 // (Private with getter/setter b/c may be nil.) 493 path *url.URL 494 } 495 496 func (pf PortForward) PathForAppend() string { 497 if pf.path == nil { 498 return "" 499 } 500 return strings.TrimPrefix(pf.path.String(), "/") 501 } 502 503 func (pf PortForward) WithPath(p *url.URL) PortForward { 504 pf.path = p 505 return pf 506 } 507 508 func MustPortForward(local int, container int, host string, name string, path string) PortForward { 509 var parsedPath *url.URL 510 var err error 511 if path != "" { 512 parsedPath, err = url.Parse(path) 513 if err != nil { 514 panic(err) 515 } 516 } 517 return PortForward{ 518 ContainerPort: container, 519 LocalPort: local, 520 Host: host, 521 Name: name, 522 path: parsedPath, 523 } 524 } 525 526 // A link associated with resource; may represent a port forward, an endpoint 527 // derived from a Service/Ingress/etc., or a URL manually associated with a 528 // resource via the Tiltfile 529 type Link struct { 530 URL *url.URL 531 532 // Optional name of the link; if given, used as text of the URL 533 // displayed in the web UI (e.g. <a href="localhost:8888">Debugger</a>) 534 Name string 535 } 536 537 func (li Link) URLString() string { return li.URL.String() } 538 539 func NewLink(urlStr string, name string) (Link, error) { 540 u, err := url.Parse(urlStr) 541 if err != nil { 542 return Link{}, errors.Wrapf(err, "parsing URL %q", urlStr) 543 } 544 return Link{ 545 URL: u, 546 Name: name, 547 }, nil 548 } 549 550 func MustNewLink(urlStr string, name string) Link { 551 li, err := NewLink(urlStr, name) 552 if err != nil { 553 panic(err) 554 } 555 return li 556 } 557 558 // ByURL implements sort.Interface based on the URL field. 559 type ByURL []Link 560 561 func (lns ByURL) Len() int { return len(lns) } 562 func (lns ByURL) Less(i, j int) bool { return lns[i].URLString() < lns[j].URLString() } 563 func (lns ByURL) Swap(i, j int) { lns[i], lns[j] = lns[j], lns[i] } 564 565 func PortForwardToLink(pf v1alpha1.Forward) Link { 566 host := pf.Host 567 if host == "" { 568 host = "localhost" 569 } 570 u := fmt.Sprintf("http://%s:%d/%s", host, pf.LocalPort, strings.TrimPrefix(pf.Path, "/")) 571 572 // We panic on error here because we provide the URL format ourselves, 573 // so if it's bad, something is very wrong. 574 return MustNewLink(u, pf.Name) 575 } 576 577 func LinksToURLStrings(lns []Link) []string { 578 res := make([]string, len(lns)) 579 for i, ln := range lns { 580 res[i] = ln.URLString() 581 } 582 return res 583 } 584 585 var imageTargetAllowUnexported = cmp.AllowUnexported(ImageTarget{}) 586 var dcTargetAllowUnexported = cmp.AllowUnexported(DockerComposeTarget{}) 587 var labelRequirementAllowUnexported = cmp.AllowUnexported(labels.Requirement{}) 588 var k8sTargetAllowUnexported = cmp.AllowUnexported(K8sTarget{}) 589 var localTargetAllowUnexported = cmp.AllowUnexported(LocalTarget{}) 590 var selectorAllowUnexported = cmp.AllowUnexported(container.RefSelector{}) 591 var refSetAllowUnexported = cmp.AllowUnexported(container.RefSet{}) 592 var portForwardPathAllowUnexported = cmp.AllowUnexported(PortForward{}) 593 var ignoreCustomBuildDepsField = cmpopts.IgnoreFields(CustomBuild{}, "Deps") 594 var ignoreLocalTargetDepsField = cmpopts.IgnoreFields(LocalTarget{}, "Deps") 595 var ignoreDockerBuildCacheFrom = cmpopts.IgnoreFields(DockerBuild{}, "CacheFrom") 596 var ignoreLabels = cmpopts.IgnoreFields(Manifest{}, "Labels") 597 var ignoreDockerComposeProject = cmpopts.IgnoreFields(v1alpha1.DockerComposeServiceSpec{}, "Project") 598 var ignoreRegistryFields = cmpopts.IgnoreFields(v1alpha1.RegistryHosting{}, "HostFromClusterNetwork", "Help") 599 600 // ignoreLinks ignores user-defined links for the purpose of build invalidation 601 // 602 // This is done both because they don't actually invalidate the build AND because url.URL is not directly comparable 603 // in all cases (e.g. a URL with a user@ value will result in url.URL->User being populated which has unexported fields). 604 var ignoreLinks = cmpopts.IgnoreTypes(Link{}) 605 606 var dockerRefEqual = cmp.Comparer(func(a, b reference.Named) bool { 607 aNil := a == nil 608 bNil := b == nil 609 if aNil && bNil { 610 return true 611 } 612 613 if aNil != bNil { 614 return false 615 } 616 617 return a.String() == b.String() 618 }) 619 620 // Determine whether interfaces x and y are equal, excluding fields that don't invalidate a build. 621 func equalForBuildInvalidation(x, y interface{}) bool { 622 return cmp.Equal(x, y, 623 cmpopts.EquateEmpty(), 624 imageTargetAllowUnexported, 625 dcTargetAllowUnexported, 626 labelRequirementAllowUnexported, 627 k8sTargetAllowUnexported, 628 localTargetAllowUnexported, 629 selectorAllowUnexported, 630 refSetAllowUnexported, 631 portForwardPathAllowUnexported, 632 dockerRefEqual, 633 634 // deps changes don't invalidate a build, so don't compare fields used only for deps 635 ignoreCustomBuildDepsField, 636 ignoreLocalTargetDepsField, 637 638 // DockerBuild.CacheFrom doesn't invalidate a build (b/c it affects HOW we build but 639 // shouldn't affect the result of the build), so don't compare these fields 640 ignoreDockerBuildCacheFrom, 641 642 // user-added labels don't invalidate a build 643 ignoreLabels, 644 645 // user-added links don't invalidate a build 646 ignoreLinks, 647 648 // We don't want a change to the DockerCompose Project to invalidate 649 // all individual services. We track the service-specific YAML with 650 // a separate ServiceYAML field. 651 ignoreDockerComposeProject, 652 653 // the RegistryHosting spec includes informational fields (Help) as 654 // well as some unused by Tilt (HostFromClusterNetwork) 655 ignoreRegistryFields, 656 ) 657 } 658 659 // Infer image properties for each image in the manifest set. 660 func InferImageProperties(manifests []Manifest) error { 661 deployImageIDSet := make(map[TargetID]bool, len(manifests)) 662 for _, m := range manifests { 663 if m.DeployTarget != nil { 664 for _, depID := range m.DeployTarget.DependencyIDs() { 665 deployImageIDSet[depID] = true 666 } 667 } 668 } 669 670 // An image only needs to be pushed if it's used in-cluster. 671 // If it needs to be pushed for one manifest, it needs to be pushed for all. 672 // The caching system will make sure it's not pushed multiple times. 673 clusterImageNeeds := func(id TargetID) v1alpha1.ClusterImageNeeds { 674 if deployImageIDSet[id] { 675 return v1alpha1.ClusterImageNeedsPush 676 } 677 return v1alpha1.ClusterImageNeedsBase 678 } 679 680 for _, m := range manifests { 681 if err := m.inferImageProperties(clusterImageNeeds); err != nil { 682 return err 683 } 684 } 685 return nil 686 }