github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/apis/cluster/client_fake.go (about) 1 package cluster 2 3 import ( 4 "context" 5 "errors" 6 "sync" 7 "testing" 8 "time" 9 10 "github.com/stretchr/testify/require" 11 apierrors "k8s.io/apimachinery/pkg/api/errors" 12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 "k8s.io/apimachinery/pkg/types" 14 ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 15 16 "github.com/tilt-dev/tilt/internal/controllers/apicmp" 17 "github.com/tilt-dev/tilt/internal/k8s" 18 "github.com/tilt-dev/tilt/internal/timecmp" 19 "github.com/tilt-dev/tilt/pkg/apis" 20 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 21 ) 22 23 type clientOrErr struct { 24 clientRevision 25 err error 26 } 27 28 type FakeClientProvider struct { 29 t testing.TB 30 mu sync.Mutex 31 clients map[types.NamespacedName]clientOrErr 32 ctrlClient ctrlclient.Client 33 } 34 35 var _ ClientProvider = &FakeClientProvider{} 36 37 // NewFakeClientProvider creates a client provider suitable for tests. 38 func NewFakeClientProvider(t testing.TB, ctrlClient ctrlclient.Client) *FakeClientProvider { 39 fcc := &FakeClientProvider{ 40 t: t, 41 ctrlClient: ctrlClient, 42 clients: make(map[types.NamespacedName]clientOrErr), 43 } 44 45 return fcc 46 } 47 48 func (f *FakeClientProvider) GetK8sClient(clusterKey types.NamespacedName) (k8s.Client, metav1.MicroTime, error) { 49 if clusterKey.Name == "" { 50 return nil, metav1.MicroTime{}, errors.New("cluster key cannot be empty") 51 } 52 53 f.mu.Lock() 54 defer f.mu.Unlock() 55 56 c, ok := f.clients[clusterKey] 57 if !ok { 58 return nil, metav1.MicroTime{}, NotFoundError 59 } 60 61 if c.err != nil { 62 // intentionally erase the error type 63 return nil, metav1.MicroTime{}, errors.New(c.err.Error()) 64 } 65 66 return c.client, c.connectedAt, nil 67 } 68 69 // AddK8sClient adds the client if there is currently no client/error for the cluster key. 70 func (f *FakeClientProvider) AddK8sClient(key types.NamespacedName, client k8s.Client) (bool, metav1.MicroTime) { 71 f.mu.Lock() 72 defer f.mu.Unlock() 73 74 if _, ok := f.clients[key]; !ok { 75 now := apis.NowMicro() 76 f.clients[key] = clientOrErr{clientRevision: clientRevision{client: client, connectedAt: now}} 77 return true, now 78 } 79 return false, metav1.MicroTime{} 80 } 81 82 // SetK8sClient sets a client for the cluster key, overwriting any that exists. 83 func (f *FakeClientProvider) SetK8sClient(key types.NamespacedName, client k8s.Client) metav1.MicroTime { 84 f.mu.Lock() 85 defer f.mu.Unlock() 86 87 // in apiserver, it's not feasible for a client to get updated repeatedly 88 // at sub-microsecond level speed, but this ensures things play nicely in 89 // tests by making the timestamp always move forward 90 now := metav1.NowMicro() 91 if existing, ok := f.clients[key]; ok { 92 if timecmp.BeforeOrEqual(now, existing.connectedAt) { 93 now = apis.NewMicroTime(existing.connectedAt.Add(time.Microsecond)) 94 } 95 } 96 97 f.clients[key] = clientOrErr{clientRevision: clientRevision{client: client, connectedAt: now}} 98 return now 99 } 100 101 // SetClusterError sets an error for the cluster key. 102 func (f *FakeClientProvider) SetClusterError(key types.NamespacedName, err error) { 103 f.mu.Lock() 104 defer f.mu.Unlock() 105 106 f.clients[key] = clientOrErr{err: err} 107 } 108 109 func (f *FakeClientProvider) MustK8sClient(clusterNN types.NamespacedName) *k8s.FakeK8sClient { 110 f.t.Helper() 111 kCli, _, err := f.GetK8sClient(clusterNN) 112 require.NoError(f.t, err, 113 "Maybe you forgot to call FakeClientProvider::EnsureK8sCluster?") 114 require.IsType(f.t, &k8s.FakeK8sClient{}, kCli, 115 "Only *k8s.FakeK8sClient should exist in the provider") 116 return kCli.(*k8s.FakeK8sClient) 117 } 118 119 func (f *FakeClientProvider) EnsureK8sClusterError(ctx context.Context, clusterNN types.NamespacedName, 120 clusterErr error) { 121 f.t.Helper() 122 123 f.SetClusterError(clusterNN, clusterErr) 124 125 f.upsertClusterStatus(ctx, clusterNN, 126 v1alpha1.ClusterStatus{ 127 ConnectedAt: nil, 128 Error: clusterErr.Error(), 129 }) 130 } 131 132 func (f *FakeClientProvider) EnsureDefaultK8sCluster(ctx context.Context) *k8s.FakeK8sClient { 133 kCli, _ := f.EnsureK8sCluster(ctx, types.NamespacedName{Name: "default"}) 134 return kCli 135 } 136 137 func (f *FakeClientProvider) EnsureK8sCluster( 138 ctx context.Context, 139 clusterNN types.NamespacedName, 140 ) (*k8s.FakeK8sClient, metav1.MicroTime) { 141 f.t.Helper() 142 143 kCli, rev, err := f.GetK8sClient(clusterNN) 144 if err != nil { 145 fakeCli := k8s.NewFakeK8sClient(f.t) 146 rev = f.SetK8sClient(clusterNN, fakeCli) 147 kCli = fakeCli 148 } 149 150 f.upsertClusterStatus(ctx, clusterNN, 151 v1alpha1.ClusterStatus{ 152 Arch: "amd64", 153 Version: "1.23.5", 154 ConnectedAt: &rev, 155 Connection: &v1alpha1.ClusterConnectionStatus{ 156 Kubernetes: &v1alpha1.KubernetesClusterConnectionStatus{ 157 Product: "kind", 158 }, 159 }, 160 }) 161 return kCli.(*k8s.FakeK8sClient), rev 162 } 163 164 func (f *FakeClientProvider) upsertClusterStatus(ctx context.Context, clusterNN types.NamespacedName, 165 status v1alpha1.ClusterStatus) { 166 f.t.Helper() 167 var cluster v1alpha1.Cluster 168 err := f.ctrlClient.Get(ctx, clusterNN, &cluster) 169 if apierrors.IsNotFound(err) { 170 cluster.ObjectMeta = metav1.ObjectMeta{ 171 Namespace: clusterNN.Namespace, 172 Name: clusterNN.Name, 173 } 174 require.NoError(f.t, f.ctrlClient.Create(ctx, &cluster)) 175 } 176 if !apicmp.DeepEqual(cluster.Status, status) { 177 cluster.Status = status 178 require.NoError(f.t, f.ctrlClient.Status().Update(ctx, &cluster)) 179 } 180 }