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  }