github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/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 := mt.State.MutableBuildStatus(kTarget.ID()) 65 assert.Equal(t, "Initial Build", 66 mt.NextBuildReason().String()) 67 68 status.PendingDependencyChanges[iTargetID] = time.Now() 69 assert.Equal(t, "Initial Build", 70 mt.NextBuildReason().String()) 71 72 mt.State.AddCompletedBuild(model.BuildRecord{StartTime: time.Now(), FinishTime: time.Now()}) 73 assert.Equal(t, "Dependency Updated", 74 mt.NextBuildReason().String()) 75 76 status.PendingFileChanges["a.txt"] = time.Now() 77 assert.Equal(t, "Changed Files | Dependency Updated", 78 mt.NextBuildReason().String()) 79 } 80 81 func TestManifestTargetEndpoints(t *testing.T) { 82 cases := []endpointsCase{ 83 { 84 name: "port forward", 85 expected: []model.Link{ 86 model.MustNewLink("http://localhost:8000/", "foobar"), 87 model.MustNewLink("http://localhost:7000/", ""), 88 }, 89 portFwds: []model.PortForward{ 90 {LocalPort: 8000, ContainerPort: 5000, Name: "foobar"}, 91 {LocalPort: 7000, ContainerPort: 5001}, 92 }, 93 }, 94 { 95 name: "port forward with host", 96 expected: []model.Link{ 97 model.MustNewLink("http://host1:8000/", "foobar"), 98 model.MustNewLink("http://host2:7000/", ""), 99 }, 100 portFwds: []model.PortForward{ 101 {LocalPort: 8000, ContainerPort: 5000, Host: "host1", Name: "foobar"}, 102 {LocalPort: 7000, ContainerPort: 5001, Host: "host2"}, 103 }, 104 }, 105 { 106 name: "port forward with path", 107 expected: []model.Link{ 108 model.MustNewLink("http://localhost:8000/stuff", "foobar"), 109 }, 110 portFwds: []model.PortForward{ 111 model.MustPortForward(8000, 5000, "", "foobar", "stuff"), 112 }, 113 }, 114 { 115 name: "port forward with path trims leading slash", 116 expected: []model.Link{ 117 model.MustNewLink("http://localhost:8000/v1/ui", "UI"), 118 }, 119 portFwds: []model.PortForward{ 120 model.MustPortForward(8000, 0, "", "UI", "/v1/ui"), 121 }, 122 }, 123 { 124 name: "port forward with path and host", 125 expected: []model.Link{ 126 model.MustNewLink("http://host1:8000/apple", "foobar"), 127 model.MustNewLink("http://host2:7000/banana", ""), 128 }, 129 portFwds: []model.PortForward{ 130 model.MustPortForward(8000, 5000, "host1", "foobar", "apple"), 131 model.MustPortForward(7000, 5001, "host2", "", "/banana"), 132 }, 133 }, 134 { 135 name: "port forward and links", 136 expected: []model.Link{ 137 model.MustNewLink("www.zombo.com", "zombo"), 138 model.MustNewLink("http://apple.edu", "apple"), 139 model.MustNewLink("http://localhost:8000/", "foobar"), 140 }, 141 portFwds: []model.PortForward{ 142 {LocalPort: 8000, Name: "foobar"}, 143 }, 144 k8sResLinks: []model.Link{ 145 model.MustNewLink("www.zombo.com", "zombo"), 146 model.MustNewLink("http://apple.edu", "apple"), 147 }, 148 }, 149 { 150 name: "local resource links", 151 expected: []model.Link{ 152 model.MustNewLink("www.apple.edu", "apple"), 153 model.MustNewLink("www.zombo.com", "zombo"), 154 }, 155 localResLinks: []model.Link{ 156 model.MustNewLink("www.apple.edu", "apple"), 157 model.MustNewLink("www.zombo.com", "zombo"), 158 }, 159 }, 160 { 161 name: "docker compose ports", 162 expected: []model.Link{ 163 model.MustNewLink("http://localhost:8000/", ""), 164 model.MustNewLink("http://localhost:7000/", ""), 165 }, 166 dcPublishedPorts: []int{8000, 7000}, 167 }, 168 { 169 name: "docker compose ports and links", 170 expected: []model.Link{ 171 model.MustNewLink("http://localhost:8000/", ""), 172 model.MustNewLink("http://localhost:7000/", ""), 173 model.MustNewLink("www.apple.edu", "apple"), 174 model.MustNewLink("www.zombo.com", "zombo"), 175 }, 176 dcPublishedPorts: []int{8000, 7000}, 177 dcResLinks: []model.Link{ 178 model.MustNewLink("www.apple.edu", "apple"), 179 model.MustNewLink("www.zombo.com", "zombo"), 180 }, 181 }, 182 { 183 name: "docker compose ports with inferLinks=false", 184 dcPublishedPorts: []int{8000, 7000}, 185 dcDoNotInferLinks: true, 186 }, 187 { 188 name: "docker compose ports and links with inferLinks=false", 189 expected: []model.Link{ 190 model.MustNewLink("www.apple.edu", "apple"), 191 model.MustNewLink("www.zombo.com", "zombo"), 192 }, 193 dcPublishedPorts: []int{8000, 7000}, 194 dcResLinks: []model.Link{ 195 model.MustNewLink("www.apple.edu", "apple"), 196 model.MustNewLink("www.zombo.com", "zombo"), 197 }, 198 dcDoNotInferLinks: true, 199 }, 200 { 201 name: "docker compose dynamic ports", 202 expected: []model.Link{ 203 model.MustNewLink("http://localhost:8000/", ""), 204 }, 205 dcPortBindings: []v1alpha1.DockerPortBinding{ 206 { 207 ContainerPort: 8080, 208 HostIP: "0.0.0.0", 209 HostPort: 8000, 210 }, 211 { 212 ContainerPort: 8080, 213 HostIP: "::", 214 HostPort: 8000, 215 }, 216 }, 217 }, 218 { 219 name: "load balancers", 220 expected: []model.Link{ 221 model.MustNewLink("a", ""), model.MustNewLink("b", ""), model.MustNewLink("c", ""), model.MustNewLink("d", ""), 222 model.MustNewLink("w", ""), model.MustNewLink("x", ""), model.MustNewLink("y", ""), model.MustNewLink("z", ""), 223 }, 224 // this is where we have some room for non-determinism, so maximize the chance of something going wrong 225 lbURLs: []string{"z", "y", "x", "w", "d", "c", "b", "a"}, 226 }, 227 { 228 name: "load balancers and links", 229 expected: []model.Link{ 230 model.MustNewLink("www.zombo.com", "zombo"), 231 model.MustNewLink("www.apple.edu", ""), 232 model.MustNewLink("www.banana.com", ""), 233 }, 234 lbURLs: []string{"www.banana.com", "www.apple.edu"}, 235 k8sResLinks: []model.Link{ 236 model.MustNewLink("www.zombo.com", "zombo"), 237 }, 238 }, 239 { 240 name: "port forwards supercede LBs", 241 expected: []model.Link{ 242 model.MustNewLink("http://localhost:7000/", ""), 243 }, 244 portFwds: []model.PortForward{ 245 {LocalPort: 7000, ContainerPort: 5001}, 246 }, 247 lbURLs: []string{"www.zombo.com"}, 248 }, 249 } 250 251 for _, c := range cases { 252 t.Run(c.name, func(t *testing.T) { 253 c.validate() 254 m := model.Manifest{Name: "foo"} 255 256 if len(c.portFwds) > 0 || len(c.k8sResLinks) > 0 { 257 var forwards []v1alpha1.Forward 258 for _, pf := range c.portFwds { 259 forwards = append(forwards, v1alpha1.Forward{ 260 LocalPort: int32(pf.LocalPort), 261 ContainerPort: int32(pf.ContainerPort), 262 Host: pf.Host, 263 Name: pf.Name, 264 Path: pf.PathForAppend(), 265 }) 266 } 267 268 m = m.WithDeployTarget(model.K8sTarget{ 269 KubernetesApplySpec: v1alpha1.KubernetesApplySpec{ 270 PortForwardTemplateSpec: &v1alpha1.PortForwardTemplateSpec{ 271 Forwards: forwards, 272 }, 273 }, 274 Links: c.k8sResLinks, 275 }) 276 } else if len(c.localResLinks) > 0 { 277 m = m.WithDeployTarget(model.LocalTarget{Links: c.localResLinks}) 278 } 279 280 isDC := len(c.dcPublishedPorts) > 0 || len(c.dcResLinks) > 0 281 282 if isDC { 283 dockerDeployTarget := model.DockerComposeTarget{} 284 285 if len(c.dcPublishedPorts) > 0 { 286 dockerDeployTarget = dockerDeployTarget.WithPublishedPorts(c.dcPublishedPorts) 287 } 288 289 if len(c.dcResLinks) > 0 { 290 dockerDeployTarget.Links = c.dcResLinks 291 } 292 293 if c.dcDoNotInferLinks { 294 dockerDeployTarget = dockerDeployTarget.WithInferLinks(false) 295 } 296 297 m = m.WithDeployTarget(dockerDeployTarget) 298 } 299 300 if len(c.dcPortBindings) > 0 && !m.IsDC() { 301 m = m.WithDeployTarget(model.DockerComposeTarget{}) 302 } 303 304 mt := newManifestTargetWithLoadBalancerURLs(m, c.lbURLs) 305 if len(c.dcPortBindings) > 0 { 306 dcState := mt.State.DCRuntimeState() 307 dcState.Ports = c.dcPortBindings 308 mt.State.RuntimeState = dcState 309 } 310 actual := ManifestTargetEndpoints(mt) 311 assertLinks(t, c.expected, actual) 312 }) 313 } 314 } 315 316 func newManifestTargetWithLoadBalancerURLs(m model.Manifest, urls []string) *ManifestTarget { 317 mt := NewManifestTarget(m) 318 if len(urls) == 0 { 319 return mt 320 } 321 322 lbs := make(map[k8s.ServiceName]*url.URL) 323 for i, s := range urls { 324 u, err := url.Parse(s) 325 if err != nil { 326 panic(fmt.Sprintf("error parsing url %q for dummy load balancers: %v", 327 s, err)) 328 } 329 name := k8s.ServiceName(fmt.Sprintf("svc#%d", i)) 330 lbs[name] = u 331 } 332 k8sState := NewK8sRuntimeState(m) 333 k8sState.LBs = lbs 334 mt.State.RuntimeState = k8sState 335 336 if !mt.Manifest.IsK8s() { 337 // k8s state implies a k8s deploy target; if this manifest doesn't have one, 338 // add a dummy one 339 mt.Manifest = mt.Manifest.WithDeployTarget(model.K8sTarget{}) 340 } 341 342 return mt 343 } 344 345 // assert.Equal on a URL is ugly and hard to read; where it's helpful, compare URLs as strings 346 func assertLinks(t *testing.T, expected, actual []model.Link) { 347 require.Len(t, actual, len(expected), "expected %d links but got %d", len(expected), len(actual)) 348 expectedStrs := model.LinksToURLStrings(expected) 349 actualStrs := model.LinksToURLStrings(actual) 350 // compare the URLs as strings for readability 351 if assert.Equal(t, expectedStrs, actualStrs, "url string comparison") { 352 // and if those match, compare everything else 353 assert.Equal(t, expected, actual) 354 } 355 } 356 357 func k8sManifest(t testing.TB, name model.ManifestName, yaml string) model.Manifest { 358 t.Helper() 359 kt, err := k8s.NewTargetForYAML(name.TargetName(), yaml, nil) 360 require.NoError(t, err, "Failed to create Kubernetes deploy target") 361 return model.Manifest{Name: name}.WithDeployTarget(kt) 362 }