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  }