github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/k8swatch/service_watch_test.go (about)

     1  package k8swatch
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/url"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/require"
    11  	v1 "k8s.io/api/core/v1"
    12  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  
    14  	"github.com/stretchr/testify/assert"
    15  	"k8s.io/apimachinery/pkg/types"
    16  
    17  	"github.com/tilt-dev/tilt/internal/controllers/apis/cluster"
    18  	"github.com/tilt-dev/tilt/internal/controllers/fake"
    19  	"github.com/tilt-dev/tilt/internal/k8s/testyaml"
    20  	"github.com/tilt-dev/tilt/internal/store/k8sconv"
    21  	"github.com/tilt-dev/tilt/internal/testutils"
    22  	"github.com/tilt-dev/tilt/internal/testutils/manifestbuilder"
    23  	"github.com/tilt-dev/tilt/internal/testutils/servicebuilder"
    24  	"github.com/tilt-dev/tilt/internal/testutils/tempdir"
    25  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    26  
    27  	"github.com/tilt-dev/tilt/internal/k8s"
    28  	"github.com/tilt-dev/tilt/internal/store"
    29  	"github.com/tilt-dev/tilt/pkg/model"
    30  )
    31  
    32  func TestServiceWatch(t *testing.T) {
    33  	f := newSWFixture(t)
    34  
    35  	nodePort := 9998
    36  	uid := types.UID("fake-uid")
    37  	manifest := f.addManifest("server")
    38  
    39  	s := servicebuilder.New(f.t, manifest).
    40  		WithPort(9998).
    41  		WithNodePort(int32(nodePort)).
    42  		WithIP(string(f.nip)).
    43  		WithUID(uid).
    44  		Build()
    45  	f.addDeployedService(manifest, s)
    46  	f.kClient.UpsertService(s)
    47  
    48  	require.NoError(f.t, f.sw.OnChange(f.ctx, f.store, store.LegacyChangeSummary()))
    49  
    50  	expectedSCA := ServiceChangeAction{
    51  		Service:      s,
    52  		ManifestName: manifest.Name,
    53  		URL: &url.URL{
    54  			Scheme: "http",
    55  			Host:   fmt.Sprintf("%s:%d", f.nip, nodePort),
    56  			Path:   "/",
    57  		},
    58  	}
    59  
    60  	f.assertObservedServiceChangeActions(expectedSCA)
    61  }
    62  
    63  // In many environments, we will get a Service change event
    64  // faster than the `kubectl apply` finishes. So we need to hold onto
    65  // the Service and dispatch an event when the UID returned by `kubectl apply`
    66  // shows up.
    67  func TestServiceWatchUIDDelayed(t *testing.T) {
    68  	f := newSWFixture(t)
    69  
    70  	uid := types.UID("fake-uid")
    71  	manifest := f.addManifest("server")
    72  
    73  	// the watcher won't start until it has a deployed object ref to find a namespace to watch in
    74  	// so we need to create at least one first
    75  	dummySvc := servicebuilder.New(t, manifest).WithUID("placeholder").Build()
    76  	f.kClient.UpsertService(dummySvc)
    77  	f.addDeployedService(manifest, dummySvc)
    78  
    79  	_ = f.sw.OnChange(f.ctx, f.store, store.LegacyChangeSummary())
    80  
    81  	// this service should be seen even by the watcher even though it's not yet referenced by the manifest
    82  	s := servicebuilder.New(f.t, manifest).
    83  		WithUID(uid).
    84  		Build()
    85  	f.kClient.UpsertService(s)
    86  	f.waitUntilServiceKnown(uid)
    87  
    88  	// once it's referenced by the manifest, an event should get emitted
    89  	f.addDeployedService(manifest, s)
    90  	expected := []ServiceChangeAction{
    91  		{
    92  			Service:      dummySvc,
    93  			ManifestName: manifest.Name,
    94  		},
    95  		{
    96  			Service:      s,
    97  			ManifestName: manifest.Name,
    98  		},
    99  	}
   100  	f.assertObservedServiceChangeActions(expected...)
   101  }
   102  
   103  func TestServiceWatchClusterChange(t *testing.T) {
   104  	f := newSWFixture(t)
   105  
   106  	port := int32(1234)
   107  	uid := types.UID("fake-uid")
   108  	manifest := f.addManifest("server")
   109  
   110  	s := servicebuilder.New(f.t, manifest).
   111  		WithPort(port).
   112  		WithNodePort(9998).
   113  		WithIP(string(f.nip)).
   114  		WithUID(uid).
   115  		Build()
   116  	f.addDeployedService(manifest, s)
   117  	f.kClient.UpsertService(s)
   118  
   119  	expectedSCA := ServiceChangeAction{
   120  		Service:      s,
   121  		ManifestName: manifest.Name,
   122  		URL: &url.URL{
   123  			Scheme: "http",
   124  			Host:   fmt.Sprintf("%s:%d", f.nip, port),
   125  			Path:   "/",
   126  		},
   127  	}
   128  
   129  	f.assertObservedServiceChangeActions(expectedSCA)
   130  	f.store.ClearActions()
   131  
   132  	newClusterClient := k8s.NewFakeK8sClient(t)
   133  	newSvc := s.DeepCopy()
   134  	port = 4567
   135  	newSvc.Spec.Ports[0].NodePort = 9997
   136  	newSvc.Spec.Ports[0].Port = port
   137  	newClusterClient.UpsertService(newSvc)
   138  	clusterNN := types.NamespacedName{Name: "default"}
   139  	// add the new client to
   140  	f.clients.SetK8sClient(clusterNN, newClusterClient)
   141  	_, createdAt, err := f.clients.GetK8sClient(clusterNN)
   142  	require.NoError(t, err, "Could not get cluster client hash")
   143  	state := f.store.LockMutableStateForTesting()
   144  	state.Clusters["default"].Status.ConnectedAt = createdAt.DeepCopy()
   145  	f.store.UnlockMutableState()
   146  
   147  	err = f.sw.OnChange(f.ctx, f.store, store.ChangeSummary{
   148  		Clusters: store.NewChangeSet(clusterNN),
   149  	})
   150  	require.NoError(t, err, "OnChange failed")
   151  	f.assertObservedServiceChangeActions(ServiceChangeAction{
   152  		Service:      newSvc,
   153  		ManifestName: manifest.Name,
   154  		URL: &url.URL{
   155  			Scheme: "http",
   156  			Host:   fmt.Sprintf("%s:%d", f.nip, port),
   157  			Path:   "/",
   158  		},
   159  	})
   160  }
   161  
   162  func (f *swFixture) addManifest(manifestName model.ManifestName) model.Manifest {
   163  	state := f.store.LockMutableStateForTesting()
   164  	defer f.store.UnlockMutableState()
   165  
   166  	m := manifestbuilder.New(f, manifestName).
   167  		WithK8sYAML(testyaml.SanchoYAML).
   168  		Build()
   169  	state.UpsertManifestTarget(store.NewManifestTarget(m))
   170  	return m
   171  }
   172  
   173  func (f *swFixture) addDeployedService(m model.Manifest, svc *v1.Service) {
   174  	defer func() {
   175  		require.NoError(f.t, f.sw.OnChange(f.ctx, f.store, store.LegacyChangeSummary()))
   176  	}()
   177  
   178  	state := f.store.LockMutableStateForTesting()
   179  	defer f.store.UnlockMutableState()
   180  	mState, ok := state.ManifestState(m.Name)
   181  	if !ok {
   182  		f.t.Fatalf("Unknown manifest: %s", m.Name)
   183  	}
   184  	runtimeState := mState.K8sRuntimeState()
   185  	runtimeState.ApplyFilter = &k8sconv.KubernetesApplyFilter{
   186  		DeployedRefs: k8s.ObjRefList{k8s.NewK8sEntity(svc).ToObjectReference()},
   187  	}
   188  	mState.RuntimeState = runtimeState
   189  }
   190  
   191  type swFixture struct {
   192  	*tempdir.TempDirFixture
   193  	t       *testing.T
   194  	clients *cluster.FakeClientProvider
   195  	kClient *k8s.FakeK8sClient
   196  	nip     k8s.NodeIP
   197  	sw      *ServiceWatcher
   198  	ctx     context.Context
   199  	cancel  func()
   200  	store   *store.TestingStore
   201  }
   202  
   203  func newSWFixture(t *testing.T) *swFixture {
   204  	nip := k8s.NodeIP("fakeip")
   205  
   206  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
   207  	ctx, cancel := context.WithCancel(ctx)
   208  
   209  	clients := cluster.NewFakeClientProvider(t, fake.NewFakeTiltClient())
   210  	kClient := clients.EnsureDefaultK8sCluster(ctx)
   211  	kClient.FakeNodeIP = nip
   212  
   213  	sw := NewServiceWatcher(clients, k8s.DefaultNamespace)
   214  	st := store.NewTestingStore()
   215  
   216  	state := st.LockMutableStateForTesting()
   217  	_, createdAt, err := clients.GetK8sClient(types.NamespacedName{Name: "default"})
   218  	require.NoError(t, err, "Failed to get default cluster client hash")
   219  	state.Clusters["default"] = &v1alpha1.Cluster{
   220  		ObjectMeta: metav1.ObjectMeta{
   221  			Name: "default",
   222  		},
   223  		Spec: v1alpha1.ClusterSpec{
   224  			Connection: &v1alpha1.ClusterConnection{
   225  				Kubernetes: &v1alpha1.KubernetesClusterConnection{},
   226  			},
   227  		},
   228  		Status: v1alpha1.ClusterStatus{
   229  			Arch:        "fake-arch",
   230  			ConnectedAt: createdAt.DeepCopy(),
   231  		},
   232  	}
   233  	st.UnlockMutableState()
   234  
   235  	ret := &swFixture{
   236  		TempDirFixture: tempdir.NewTempDirFixture(t),
   237  		clients:        clients,
   238  		kClient:        kClient,
   239  		sw:             sw,
   240  		nip:            nip,
   241  		ctx:            ctx,
   242  		cancel:         cancel,
   243  		t:              t,
   244  		store:          st,
   245  	}
   246  
   247  	t.Cleanup(ret.TearDown)
   248  
   249  	return ret
   250  }
   251  
   252  func (f *swFixture) TearDown() {
   253  	f.cancel()
   254  	f.store.AssertNoErrorActions(f.t)
   255  }
   256  
   257  func (f *swFixture) assertObservedServiceChangeActions(expectedSCAs ...ServiceChangeAction) {
   258  	f.t.Helper()
   259  	start := time.Now()
   260  	for time.Since(start) < time.Second {
   261  		actions := f.store.Actions()
   262  		if len(actions) == len(expectedSCAs) {
   263  			break
   264  		}
   265  	}
   266  
   267  	var observedSCAs []ServiceChangeAction
   268  	for _, a := range f.store.Actions() {
   269  		sca, ok := a.(ServiceChangeAction)
   270  		if !ok {
   271  			f.t.Fatalf("got non-%T: %v", ServiceChangeAction{}, a)
   272  		}
   273  		observedSCAs = append(observedSCAs, sca)
   274  	}
   275  	if !assert.Equal(f.t, expectedSCAs, observedSCAs) {
   276  		f.t.FailNow()
   277  	}
   278  }
   279  
   280  func (f *swFixture) waitUntilServiceKnown(uid types.UID) {
   281  	clusterNN := types.NamespacedName{Name: v1alpha1.ClusterNameDefault}
   282  	start := time.Now()
   283  	for time.Since(start) < time.Second {
   284  		f.sw.mu.Lock()
   285  		_, known := f.sw.knownServices[clusterUID{cluster: clusterNN, uid: uid}]
   286  		f.sw.mu.Unlock()
   287  		if known {
   288  			return
   289  		}
   290  
   291  		time.Sleep(10 * time.Millisecond)
   292  	}
   293  
   294  	f.t.Fatalf("timeout waiting for service with UID: %s", uid)
   295  }