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  }