github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/cluster/reconciler_test.go (about) 1 package cluster 2 3 import ( 4 "errors" 5 "os" 6 "testing" 7 "time" 8 9 "github.com/google/go-cmp/cmp" 10 "github.com/jonboulle/clockwork" 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 v1 "k8s.io/api/core/v1" 14 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 "k8s.io/apimachinery/pkg/types" 16 17 "github.com/tilt-dev/tilt/internal/hud/server" 18 "github.com/tilt-dev/tilt/internal/testutils/tempdir" 19 "github.com/tilt-dev/tilt/internal/xdg" 20 "github.com/tilt-dev/wmclient/pkg/analytics" 21 22 "github.com/tilt-dev/tilt/internal/controllers/apicmp" 23 "github.com/tilt-dev/tilt/internal/controllers/fake" 24 "github.com/tilt-dev/tilt/internal/controllers/indexer" 25 "github.com/tilt-dev/tilt/internal/docker" 26 "github.com/tilt-dev/tilt/internal/k8s" 27 "github.com/tilt-dev/tilt/internal/timecmp" 28 "github.com/tilt-dev/tilt/pkg/apis" 29 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 30 ) 31 32 func TestKubernetesError(t *testing.T) { 33 f := newFixture(t) 34 cluster := &v1alpha1.Cluster{ 35 ObjectMeta: metav1.ObjectMeta{Name: "default"}, 36 Spec: v1alpha1.ClusterSpec{ 37 Connection: &v1alpha1.ClusterConnection{ 38 Kubernetes: &v1alpha1.KubernetesClusterConnection{}, 39 }, 40 }, 41 } 42 nn := apis.Key(cluster) 43 44 // Create a fake client factory that always returns an error 45 origClientFactory := f.r.k8sClientFactory 46 f.r.k8sClientFactory = FakeKubernetesClientOrError(nil, errors.New("fake error")) 47 f.Create(cluster) 48 49 assert.Equal(t, "", cluster.Status.Error) 50 f.MustGet(nn, cluster) 51 assert.Equal(t, 52 "Tilt encountered an error connecting to your Kubernetes cluster:\n\tfake error\nYou will need to restart Tilt after resolving the issue.", 53 cluster.Status.Error) 54 assert.Nil(t, cluster.Status.ConnectedAt, "ConnectedAt should be empty") 55 56 // replace the working client factory but ensure that it's not invoked 57 // we should be in a steady state until the retry/backoff window elapses 58 f.r.k8sClientFactory = origClientFactory 59 f.assertSteadyState(cluster) 60 61 // advance the clock such that we should retry, but ensure that no retry 62 // is attempted because the cluster refresh feature flag annotation is 63 // not set 64 f.clock.Advance(time.Minute) 65 f.assertSteadyState(cluster) 66 67 // add the cluster refresh feature flag and verify that it gets refreshed 68 // and creates a new client without errors 69 if cluster.Annotations == nil { 70 cluster.Annotations = make(map[string]string) 71 } 72 cluster.Annotations["features.tilt.dev/cluster-refresh"] = "true" 73 f.Update(cluster) 74 75 f.MustGet(nn, cluster) 76 require.Empty(t, cluster.Status.Error, "No error should be present on cluster") 77 if assert.NotNil(t, cluster.Status.ConnectedAt, "ConnectedAt should be populated") { 78 assert.NotZero(t, cluster.Status.ConnectedAt.Time, "ConnectedAt should not be zero time") 79 } 80 } 81 82 func TestKubernetesDelete(t *testing.T) { 83 f := newFixture(t) 84 cluster := &v1alpha1.Cluster{ 85 ObjectMeta: metav1.ObjectMeta{Name: "default"}, 86 Spec: v1alpha1.ClusterSpec{ 87 Connection: &v1alpha1.ClusterConnection{ 88 Kubernetes: &v1alpha1.KubernetesClusterConnection{}, 89 }, 90 }, 91 } 92 nn := apis.Key(cluster) 93 94 f.Create(cluster) 95 _, ok := f.r.connManager.load(nn) 96 require.True(t, ok, "Connection was not present in connection manager") 97 98 f.Delete(cluster) 99 _, ok = f.r.connManager.load(nn) 100 require.False(t, ok, "Connection was not removed from connection manager") 101 } 102 103 func TestKubernetesArch(t *testing.T) { 104 f := newFixture(t) 105 cluster := &v1alpha1.Cluster{ 106 ObjectMeta: metav1.ObjectMeta{Name: "default"}, 107 Spec: v1alpha1.ClusterSpec{ 108 Connection: &v1alpha1.ClusterConnection{ 109 Kubernetes: &v1alpha1.KubernetesClusterConnection{}, 110 }, 111 }, 112 } 113 114 // Inject a Node into the fake client so that the arch can be determined. 115 nn := types.NamespacedName{Name: "default"} 116 f.k8sClient.Inject(k8s.K8sEntity{ 117 Obj: &v1.Node{ 118 TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Node"}, 119 ObjectMeta: metav1.ObjectMeta{ 120 Name: "node-1", 121 UID: "a", 122 Labels: map[string]string{ 123 "kubernetes.io/arch": "amd64", 124 }, 125 }, 126 }, 127 }) 128 129 f.Create(cluster) 130 f.MustGet(nn, cluster) 131 assert.Equal(t, "amd64", cluster.Status.Arch) 132 133 f.assertSteadyState(cluster) 134 135 connectEvt := analytics.CountEvent{ 136 Name: "api.cluster.connect", 137 Tags: map[string]string{ 138 "type": "kubernetes", 139 "arch": "amd64", 140 "status": "connected", 141 }, 142 N: 1, 143 } 144 assert.ElementsMatch(t, []analytics.CountEvent{connectEvt}, f.ma.Counts) 145 } 146 147 func TestKubernetesConnStatus(t *testing.T) { 148 f := newFixture(t) 149 cluster := &v1alpha1.Cluster{ 150 ObjectMeta: metav1.ObjectMeta{Name: "default"}, 151 Spec: v1alpha1.ClusterSpec{ 152 Connection: &v1alpha1.ClusterConnection{ 153 Kubernetes: &v1alpha1.KubernetesClusterConnection{}, 154 }, 155 }, 156 } 157 158 nn := types.NamespacedName{Name: "default"} 159 f.Create(cluster) 160 f.MustGet(nn, cluster) 161 162 configPath := cluster.Status.Connection.Kubernetes.ConfigPath 163 require.NotEqual(t, configPath, "") 164 165 expected := &v1alpha1.ClusterConnectionStatus{ 166 Kubernetes: &v1alpha1.KubernetesClusterConnectionStatus{ 167 Context: "default", 168 Namespace: "default", 169 Cluster: "default", 170 Product: "unknown", 171 ConfigPath: configPath, 172 }, 173 } 174 assert.Equal(t, expected, cluster.Status.Connection) 175 176 contents, err := os.ReadFile(configPath) 177 require.NoError(t, err) 178 assert.Equal(t, `apiVersion: v1 179 clusters: 180 - cluster: 181 server: "" 182 name: default 183 contexts: 184 - context: 185 cluster: default 186 namespace: default 187 user: "" 188 name: default 189 current-context: default 190 kind: Config 191 preferences: {} 192 users: null 193 `, string(contents)) 194 } 195 196 func TestKubernetesMonitor(t *testing.T) { 197 f := newFixture(t) 198 cluster := &v1alpha1.Cluster{ 199 ObjectMeta: metav1.ObjectMeta{Name: "default"}, 200 Spec: v1alpha1.ClusterSpec{ 201 Connection: &v1alpha1.ClusterConnection{ 202 Kubernetes: &v1alpha1.KubernetesClusterConnection{}, 203 }, 204 }, 205 } 206 nn := apis.Key(cluster) 207 208 f.Create(cluster) 209 f.MustGet(nn, cluster) 210 connectedAt := *cluster.Status.ConnectedAt 211 f.assertSteadyState(cluster) 212 213 f.k8sClient.ClusterHealthError = errors.New("fake cluster health error") 214 f.clock.Advance(time.Minute) 215 <-f.requeues 216 217 f.MustGet(nn, cluster) 218 assert.Equal(t, "fake cluster health error", cluster.Status.Error) 219 timecmp.RequireTimeEqual(t, connectedAt, cluster.Status.ConnectedAt) 220 } 221 222 func TestDockerError(t *testing.T) { 223 f := newFixture(t) 224 cluster := &v1alpha1.Cluster{ 225 ObjectMeta: metav1.ObjectMeta{Name: "default"}, 226 Spec: v1alpha1.ClusterSpec{ 227 Connection: &v1alpha1.ClusterConnection{ 228 Docker: &v1alpha1.DockerClusterConnection{}, 229 }, 230 }, 231 } 232 nn := apis.Key(cluster) 233 234 f.r.dockerClientFactory = FakeDockerClientOrError(nil, errors.New("fake docker error")) 235 236 f.Create(cluster) 237 f.MustGet(nn, cluster) 238 assert.Equal(t, "fake docker error", cluster.Status.Error) 239 assert.Nil(t, cluster.Status.ConnectedAt, "ConnectedAt should not be populated") 240 assert.Empty(t, cluster.Status.Arch, "no arch should be present") 241 } 242 243 func TestDockerArch(t *testing.T) { 244 f := newFixture(t) 245 cluster := &v1alpha1.Cluster{ 246 ObjectMeta: metav1.ObjectMeta{Name: "default"}, 247 Spec: v1alpha1.ClusterSpec{ 248 Connection: &v1alpha1.ClusterConnection{ 249 Docker: &v1alpha1.DockerClusterConnection{}, 250 }, 251 }, 252 } 253 254 nn := types.NamespacedName{Name: "default"} 255 f.Create(cluster) 256 f.MustGet(nn, cluster) 257 assert.Equal(t, "amd64", cluster.Status.Arch) 258 if assert.NotNil(t, cluster.Status.ConnectedAt, "ConnectedAt should be populated") { 259 assert.NotZero(t, cluster.Status.ConnectedAt.Time, "ConnectedAt should not be zero") 260 } 261 } 262 263 type fixture struct { 264 *fake.ControllerFixture 265 r *Reconciler 266 ma *analytics.MemoryAnalytics 267 clock clockwork.FakeClock 268 k8sClient *k8s.FakeK8sClient 269 dockerClient *docker.FakeClient 270 requeues <-chan indexer.RequeueForTestResult 271 } 272 273 func newFixture(t *testing.T) *fixture { 274 cfb := fake.NewControllerFixtureBuilder(t) 275 clock := clockwork.NewFakeClock() 276 tmpf := tempdir.NewTempDirFixture(t) 277 278 k8sClient := k8s.NewFakeK8sClient(t) 279 dockerClient := docker.NewFakeClient() 280 base := xdg.FakeBase{Dir: tmpf.Path()} 281 282 r := NewReconciler(cfb.Context(), 283 cfb.Client, 284 cfb.Store, 285 clock, 286 NewConnectionManager(), 287 docker.LocalEnv{}, 288 FakeDockerClientOrError(dockerClient, nil), 289 FakeKubernetesClientOrError(k8sClient, nil), 290 server.NewWebsocketList(), 291 base, 292 "tilt-default") 293 requeueChan := make(chan indexer.RequeueForTestResult, 1) 294 return &fixture{ 295 ControllerFixture: cfb.WithRequeuer(r.requeuer).WithRequeuerResultChan(requeueChan).Build(r), 296 r: r, 297 ma: cfb.Analytics(), 298 clock: clock, 299 k8sClient: k8sClient, 300 dockerClient: dockerClient, 301 requeues: requeueChan, 302 } 303 } 304 305 func (f *fixture) assertSteadyState(o *v1alpha1.Cluster) { 306 f.T().Helper() 307 f.MustReconcile(types.NamespacedName{Name: o.Name}) 308 var o2 v1alpha1.Cluster 309 f.MustGet(types.NamespacedName{Name: o.Name}, &o2) 310 assert.True(f.T(), apicmp.DeepEqual(o, &o2), 311 "Cluster object should have been in steady state but changed: %s", 312 cmp.Diff(o, &o2)) 313 }