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 }