github.com/tilt-dev/tilt@v0.36.0/internal/store/engine_state_test.go (about) 1 package store 2 3 import ( 4 "fmt" 5 "net/url" 6 "testing" 7 "time" 8 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 12 "github.com/tilt-dev/tilt/internal/container" 13 "github.com/tilt-dev/tilt/internal/k8s" 14 "github.com/tilt-dev/tilt/internal/k8s/testyaml" 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 endpointsCase struct { 21 name string 22 expected []model.Link 23 24 // k8s resource fields 25 portFwds []model.PortForward 26 lbURLs []string 27 28 dcPublishedPorts []int 29 dcPortBindings []v1alpha1.DockerPortBinding 30 31 k8sResLinks []model.Link 32 localResLinks []model.Link 33 dcResLinks []model.Link 34 dcDoNotInferLinks bool 35 } 36 37 func (c endpointsCase) validate() { 38 if len(c.portFwds) > 0 || len(c.lbURLs) > 0 || len(c.k8sResLinks) > 0 { 39 if len(c.dcPublishedPorts) > 0 || len(c.localResLinks) > 0 { 40 // portForwards and LoadBalancerURLs are exclusively the province 41 // of k8s resources, so you should never see them paired with 42 // test settings that imply a. a DC resource or b. a local resource 43 panic("test case implies impossible resource") 44 } 45 } 46 } 47 48 func TestMostRecentPod(t *testing.T) { 49 podA := v1alpha1.Pod{Name: "pod-a", CreatedAt: apis.Now()} 50 podB := v1alpha1.Pod{Name: "pod-b", CreatedAt: apis.NewTime(time.Now().Add(time.Minute))} 51 podC := v1alpha1.Pod{Name: "pod-c", CreatedAt: apis.NewTime(time.Now().Add(-time.Minute))} 52 m := model.Manifest{Name: "fe"} 53 podSet := NewK8sRuntimeStateWithPods(m, podA, podB, podC) 54 assert.Equal(t, "pod-b", podSet.MostRecentPod().Name) 55 } 56 57 func TestNextBuildReason(t *testing.T) { 58 m := k8sManifest(t, model.UnresourcedYAMLManifestName, testyaml.SanchoYAML) 59 60 kTarget := m.K8sTarget() 61 mt := NewManifestTarget(m) 62 63 iTargetID := model.ImageID(container.MustParseSelector("sancho")) 64 status, ok := mt.State.BuildStatus(kTarget.ID()) 65 require.True(t, ok) 66 assert.Equal(t, "Initial Build", 67 mt.NextBuildReason().String()) 68 69 status.DependencyChanges[iTargetID] = time.Now() 70 assert.Equal(t, "Initial Build", 71 mt.NextBuildReason().String()) 72 73 mt.State.AddCompletedBuild(model.BuildRecord{StartTime: time.Now(), FinishTime: time.Now()}) 74 assert.Equal(t, "Dependency Updated", 75 mt.NextBuildReason().String()) 76 77 status.FileChanges["a.txt"] = time.Now() 78 assert.Equal(t, "Changed Files | Dependency Updated", 79 mt.NextBuildReason().String()) 80 } 81 82 func TestBuildStatusGC(t *testing.T) { 83 start := time.Now() 84 bs := newBuildStatus() 85 assert.False(t, bs.HasPendingFileChanges()) 86 assert.False(t, bs.HasPendingDependencyChanges()) 87 88 bs.FileChanges["a.txt"] = start 89 bs.FileChanges["b.txt"] = start 90 bs.DependencyChanges[model.ImageID(container.MustParseSelector("sancho"))] = start 91 92 assert.True(t, bs.HasPendingFileChanges()) 93 assert.True(t, bs.HasPendingDependencyChanges()) 94 assert.Equal(t, []string{"a.txt", "b.txt"}, bs.PendingFileChangesSorted()) 95 96 bs.ConsumeChangesBefore(start.Add(time.Second)) 97 assert.False(t, bs.HasPendingFileChanges()) 98 assert.False(t, bs.HasPendingDependencyChanges()) 99 assert.Equal(t, 2, len(bs.FileChanges)) 100 assert.Equal(t, 1, len(bs.DependencyChanges)) 101 assert.Equal(t, []string(nil), bs.PendingFileChangesSorted()) 102 103 bs.FileChanges["a.txt"] = start.Add(2 * time.Second) 104 assert.True(t, bs.HasPendingFileChanges()) 105 106 bs.ConsumeChangesBefore(start.Add(time.Hour)) 107 assert.False(t, bs.HasPendingFileChanges()) 108 assert.False(t, bs.HasPendingDependencyChanges()) 109 110 // GC should remove sufficiently old changes from the map 111 // since they'll just slow things down. 112 assert.Equal(t, 0, len(bs.FileChanges)) 113 assert.Equal(t, 0, len(bs.DependencyChanges)) 114 } 115 116 func TestManifestTargetEndpoints(t *testing.T) { 117 cases := []endpointsCase{ 118 { 119 name: "port forward", 120 expected: []model.Link{ 121 model.MustNewLink("http://localhost:8000/", "foobar"), 122 model.MustNewLink("http://localhost:7000/", ""), 123 }, 124 portFwds: []model.PortForward{ 125 {LocalPort: 8000, ContainerPort: 5000, Name: "foobar"}, 126 {LocalPort: 7000, ContainerPort: 5001}, 127 }, 128 }, 129 { 130 name: "port forward with host", 131 expected: []model.Link{ 132 model.MustNewLink("http://host1:8000/", "foobar"), 133 model.MustNewLink("http://host2:7000/", ""), 134 }, 135 portFwds: []model.PortForward{ 136 {LocalPort: 8000, ContainerPort: 5000, Host: "host1", Name: "foobar"}, 137 {LocalPort: 7000, ContainerPort: 5001, Host: "host2"}, 138 }, 139 }, 140 { 141 name: "port forward with path", 142 expected: []model.Link{ 143 model.MustNewLink("http://localhost:8000/stuff", "foobar"), 144 }, 145 portFwds: []model.PortForward{ 146 model.MustPortForward(8000, 5000, "", "foobar", "stuff"), 147 }, 148 }, 149 { 150 name: "port forward with path trims leading slash", 151 expected: []model.Link{ 152 model.MustNewLink("http://localhost:8000/v1/ui", "UI"), 153 }, 154 portFwds: []model.PortForward{ 155 model.MustPortForward(8000, 0, "", "UI", "/v1/ui"), 156 }, 157 }, 158 { 159 name: "port forward with path and host", 160 expected: []model.Link{ 161 model.MustNewLink("http://host1:8000/apple", "foobar"), 162 model.MustNewLink("http://host2:7000/banana", ""), 163 }, 164 portFwds: []model.PortForward{ 165 model.MustPortForward(8000, 5000, "host1", "foobar", "apple"), 166 model.MustPortForward(7000, 5001, "host2", "", "/banana"), 167 }, 168 }, 169 { 170 name: "port forward and links", 171 expected: []model.Link{ 172 model.MustNewLink("www.zombo.com", "zombo"), 173 model.MustNewLink("http://apple.edu", "apple"), 174 model.MustNewLink("http://localhost:8000/", "foobar"), 175 }, 176 portFwds: []model.PortForward{ 177 {LocalPort: 8000, Name: "foobar"}, 178 }, 179 k8sResLinks: []model.Link{ 180 model.MustNewLink("www.zombo.com", "zombo"), 181 model.MustNewLink("http://apple.edu", "apple"), 182 }, 183 }, 184 { 185 name: "local resource links", 186 expected: []model.Link{ 187 model.MustNewLink("www.apple.edu", "apple"), 188 model.MustNewLink("www.zombo.com", "zombo"), 189 }, 190 localResLinks: []model.Link{ 191 model.MustNewLink("www.apple.edu", "apple"), 192 model.MustNewLink("www.zombo.com", "zombo"), 193 }, 194 }, 195 { 196 name: "docker compose ports", 197 expected: []model.Link{ 198 model.MustNewLink("http://localhost:8000/", ""), 199 model.MustNewLink("http://localhost:7000/", ""), 200 }, 201 dcPublishedPorts: []int{8000, 7000}, 202 }, 203 { 204 name: "docker compose ports and links", 205 expected: []model.Link{ 206 model.MustNewLink("http://localhost:8000/", ""), 207 model.MustNewLink("http://localhost:7000/", ""), 208 model.MustNewLink("www.apple.edu", "apple"), 209 model.MustNewLink("www.zombo.com", "zombo"), 210 }, 211 dcPublishedPorts: []int{8000, 7000}, 212 dcResLinks: []model.Link{ 213 model.MustNewLink("www.apple.edu", "apple"), 214 model.MustNewLink("www.zombo.com", "zombo"), 215 }, 216 }, 217 { 218 name: "docker compose ports with inferLinks=false", 219 dcPublishedPorts: []int{8000, 7000}, 220 dcDoNotInferLinks: true, 221 }, 222 { 223 name: "docker compose ports and links with inferLinks=false", 224 expected: []model.Link{ 225 model.MustNewLink("www.apple.edu", "apple"), 226 model.MustNewLink("www.zombo.com", "zombo"), 227 }, 228 dcPublishedPorts: []int{8000, 7000}, 229 dcResLinks: []model.Link{ 230 model.MustNewLink("www.apple.edu", "apple"), 231 model.MustNewLink("www.zombo.com", "zombo"), 232 }, 233 dcDoNotInferLinks: true, 234 }, 235 { 236 name: "docker compose dynamic ports", 237 expected: []model.Link{ 238 model.MustNewLink("http://localhost:8000/", ""), 239 }, 240 dcPortBindings: []v1alpha1.DockerPortBinding{ 241 { 242 ContainerPort: 8080, 243 HostIP: "0.0.0.0", 244 HostPort: 8000, 245 }, 246 { 247 ContainerPort: 8080, 248 HostIP: "::", 249 HostPort: 8000, 250 }, 251 }, 252 }, 253 { 254 name: "load balancers", 255 expected: []model.Link{ 256 model.MustNewLink("a", ""), model.MustNewLink("b", ""), model.MustNewLink("c", ""), model.MustNewLink("d", ""), 257 model.MustNewLink("w", ""), model.MustNewLink("x", ""), model.MustNewLink("y", ""), model.MustNewLink("z", ""), 258 }, 259 // this is where we have some room for non-determinism, so maximize the chance of something going wrong 260 lbURLs: []string{"z", "y", "x", "w", "d", "c", "b", "a"}, 261 }, 262 { 263 name: "load balancers and links", 264 expected: []model.Link{ 265 model.MustNewLink("www.zombo.com", "zombo"), 266 model.MustNewLink("www.apple.edu", ""), 267 model.MustNewLink("www.banana.com", ""), 268 }, 269 lbURLs: []string{"www.banana.com", "www.apple.edu"}, 270 k8sResLinks: []model.Link{ 271 model.MustNewLink("www.zombo.com", "zombo"), 272 }, 273 }, 274 { 275 name: "port forwards supercede LBs", 276 expected: []model.Link{ 277 model.MustNewLink("http://localhost:7000/", ""), 278 }, 279 portFwds: []model.PortForward{ 280 {LocalPort: 7000, ContainerPort: 5001}, 281 }, 282 lbURLs: []string{"www.zombo.com"}, 283 }, 284 } 285 286 for _, c := range cases { 287 t.Run(c.name, func(t *testing.T) { 288 c.validate() 289 m := model.Manifest{Name: "foo"} 290 291 if len(c.portFwds) > 0 || len(c.k8sResLinks) > 0 { 292 var forwards []v1alpha1.Forward 293 for _, pf := range c.portFwds { 294 forwards = append(forwards, v1alpha1.Forward{ 295 LocalPort: int32(pf.LocalPort), 296 ContainerPort: int32(pf.ContainerPort), 297 Host: pf.Host, 298 Name: pf.Name, 299 Path: pf.PathForAppend(), 300 }) 301 } 302 303 m = m.WithDeployTarget(model.K8sTarget{ 304 KubernetesApplySpec: v1alpha1.KubernetesApplySpec{ 305 PortForwardTemplateSpec: &v1alpha1.PortForwardTemplateSpec{ 306 Forwards: forwards, 307 }, 308 }, 309 Links: c.k8sResLinks, 310 }) 311 } else if len(c.localResLinks) > 0 { 312 m = m.WithDeployTarget(model.LocalTarget{Links: c.localResLinks}) 313 } 314 315 isDC := len(c.dcPublishedPorts) > 0 || len(c.dcResLinks) > 0 316 317 if isDC { 318 dockerDeployTarget := model.DockerComposeTarget{} 319 320 if len(c.dcPublishedPorts) > 0 { 321 dockerDeployTarget = dockerDeployTarget.WithPublishedPorts(c.dcPublishedPorts) 322 } 323 324 if len(c.dcResLinks) > 0 { 325 dockerDeployTarget.Links = c.dcResLinks 326 } 327 328 if c.dcDoNotInferLinks { 329 dockerDeployTarget = dockerDeployTarget.WithInferLinks(false) 330 } 331 332 m = m.WithDeployTarget(dockerDeployTarget) 333 } 334 335 if len(c.dcPortBindings) > 0 && !m.IsDC() { 336 m = m.WithDeployTarget(model.DockerComposeTarget{}) 337 } 338 339 mt := newManifestTargetWithLoadBalancerURLs(m, c.lbURLs) 340 if len(c.dcPortBindings) > 0 { 341 dcState := mt.State.DCRuntimeState() 342 dcState.Ports = c.dcPortBindings 343 mt.State.RuntimeState = dcState 344 } 345 actual := ManifestTargetEndpoints(mt) 346 assertLinks(t, c.expected, actual) 347 }) 348 } 349 } 350 351 func newManifestTargetWithLoadBalancerURLs(m model.Manifest, urls []string) *ManifestTarget { 352 mt := NewManifestTarget(m) 353 if len(urls) == 0 { 354 return mt 355 } 356 357 lbs := make(map[k8s.ServiceName]*url.URL) 358 for i, s := range urls { 359 u, err := url.Parse(s) 360 if err != nil { 361 panic(fmt.Sprintf("error parsing url %q for dummy load balancers: %v", 362 s, err)) 363 } 364 name := k8s.ServiceName(fmt.Sprintf("svc#%d", i)) 365 lbs[name] = u 366 } 367 k8sState := NewK8sRuntimeState(m) 368 k8sState.LBs = lbs 369 mt.State.RuntimeState = k8sState 370 371 if !mt.Manifest.IsK8s() { 372 // k8s state implies a k8s deploy target; if this manifest doesn't have one, 373 // add a dummy one 374 mt.Manifest = mt.Manifest.WithDeployTarget(model.K8sTarget{}) 375 } 376 377 return mt 378 } 379 380 // assert.Equal on a URL is ugly and hard to read; where it's helpful, compare URLs as strings 381 func assertLinks(t *testing.T, expected, actual []model.Link) { 382 require.Len(t, actual, len(expected), "expected %d links but got %d", len(expected), len(actual)) 383 expectedStrs := model.LinksToURLStrings(expected) 384 actualStrs := model.LinksToURLStrings(actual) 385 // compare the URLs as strings for readability 386 if assert.Equal(t, expectedStrs, actualStrs, "url string comparison") { 387 // and if those match, compare everything else 388 assert.Equal(t, expected, actual) 389 } 390 } 391 392 func k8sManifest(t testing.TB, name model.ManifestName, yaml string) model.Manifest { 393 t.Helper() 394 kt, err := k8s.NewTargetForYAML(name.TargetName(), yaml, nil) 395 require.NoError(t, err, "Failed to create Kubernetes deploy target") 396 return model.Manifest{Name: name}.WithDeployTarget(kt) 397 }