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  }