github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/apis/cluster/client.go (about)

     1  package cluster
     2  
     3  import (
     4  	"errors"
     5  	"sync"
     6  
     7  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     8  	"k8s.io/apimachinery/pkg/types"
     9  
    10  	"github.com/tilt-dev/tilt/internal/k8s"
    11  	"github.com/tilt-dev/tilt/internal/timecmp"
    12  	"github.com/tilt-dev/tilt/pkg/apis"
    13  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    14  )
    15  
    16  // NotFoundError indicates there is no cluster client for the given key.
    17  var NotFoundError = errors.New("cluster client does not exist")
    18  
    19  // StaleClientError indicates that the Cluster object is out-of-date.
    20  //
    21  // This is indicative of a bug in the caller!
    22  //
    23  // If calls to ClientManager::Refresh are not made properly, StaleClientError
    24  // can be returned by ClientManager::GetK8sClient. This is to prevent
    25  // unintentional misuse resulting in a stale client being used while
    26  // also ensuring that callers handle connection changes uniformly.
    27  //
    28  // See the expected usage/flow on ClientManager.
    29  var StaleClientError = errors.New("cluster revision is stale")
    30  
    31  // ClientProvider provides client instances to the ClientManager.
    32  //
    33  // All clients MUST be goroutine-safe.
    34  type ClientProvider interface {
    35  	// GetK8sClient returns the Kubernetes client for the cluster or an error for unknown clusters, connections
    36  	// in a transient error state, or if the connection is of a different type (i.e. Docker Compose).
    37  	//
    38  	// In addition to the client, the timestamp at which the client was created is returned so callers can track
    39  	// when the client instance has changed.
    40  	GetK8sClient(clusterKey types.NamespacedName) (k8s.Client, metav1.MicroTime, error)
    41  }
    42  
    43  type clusterRef struct {
    44  	objKey     types.NamespacedName
    45  	clusterKey types.NamespacedName
    46  }
    47  
    48  type clientRevision struct {
    49  	connectedAt metav1.MicroTime
    50  	client      k8s.Client
    51  }
    52  
    53  // ClientManager is a convenience wrapper over ClientProvider which simplifies
    54  // handling client changes.
    55  //
    56  // On reconcile, the controller should:
    57  //
    58  //	(1) Fetch the Cluster object referenced by the type its reconciling.
    59  //	(2) Call ClientManager::Refresh to determine if the client for the cluster
    60  //		has changed. If true, all state associated with the old cluster should
    61  //		be cleared.
    62  //	(3) As needed, call ClientManager::GetK8sClient to get a client instance.
    63  type ClientManager struct {
    64  	mu       sync.Mutex
    65  	provider ClientProvider
    66  
    67  	revisions map[clusterRef]metav1.MicroTime
    68  }
    69  
    70  func NewClientManager(clientProvider ClientProvider) *ClientManager {
    71  	return &ClientManager{
    72  		provider:  clientProvider,
    73  		revisions: make(map[clusterRef]metav1.MicroTime),
    74  	}
    75  }
    76  
    77  // GetK8sClient returns the client associated with the Cluster object.
    78  //
    79  // If no client is known for the Cluster object, NotFoundError is returned.
    80  // If the Cluster object's config hash in the status does not match the known client, StaleClientError is returned.
    81  func (c *ClientManager) GetK8sClient(obj apis.KeyableObject, cluster *v1alpha1.Cluster) (k8s.Client, error) {
    82  	c.mu.Lock()
    83  	defer c.mu.Unlock()
    84  
    85  	return c.getK8sClient(obj, cluster)
    86  }
    87  
    88  // Refresh checks to see if there is an updated client for the Cluster object.
    89  //
    90  // If it returns true, any state associated with this Cluster should be reset
    91  // and rebuilt using a new client retrieved via a subsequent call to GetK8sClient.
    92  func (c *ClientManager) Refresh(obj apis.KeyableObject, cluster *v1alpha1.Cluster) bool {
    93  	c.mu.Lock()
    94  	defer c.mu.Unlock()
    95  
    96  	clusterRef := clusterRef{clusterKey: apis.Key(cluster), objKey: apis.Key(obj)}
    97  	objRevision, knownForObj := c.revisions[clusterRef]
    98  
    99  	if knownForObj && timecmp.Equal(cluster.Status.ConnectedAt, objRevision) {
   100  		// regardless of if there's a new client, state from the perspective
   101  		// of this caller is in sync because the client its tracking matches
   102  		// the cluster object it passed in, so having it potentially reset state
   103  		// (assuming an updated client exists) will be more harmful than helpful
   104  		// as it won't be able to fetch it
   105  		return false
   106  	}
   107  
   108  	_, err := c.getK8sClient(obj, cluster)
   109  	if err != nil {
   110  		delete(c.revisions, clusterRef)
   111  		return knownForObj
   112  	}
   113  
   114  	return false
   115  }
   116  
   117  func (c *ClientManager) getK8sClient(obj apis.KeyableObject, cluster *v1alpha1.Cluster) (k8s.Client, error) {
   118  	if cluster == nil {
   119  		return nil, NotFoundError
   120  	}
   121  
   122  	clusterNN := apis.Key(cluster)
   123  	cli, revision, err := c.provider.GetK8sClient(clusterNN)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	if !timecmp.Equal(cluster.Status.ConnectedAt, revision) {
   129  		// the client does not match the cluster object that was passed in
   130  		return nil, StaleClientError
   131  	}
   132  
   133  	clusterRef := clusterRef{objKey: apis.Key(obj), clusterKey: clusterNN}
   134  	if objRevision, ok := c.revisions[clusterRef]; ok {
   135  		if !timecmp.Equal(revision, objRevision) {
   136  			// client previously had an old version of client and need to call
   137  			// refresh to clear out their old state
   138  			return nil, StaleClientError
   139  		}
   140  	} else {
   141  		// first time this object has fetched this client, so track the version
   142  		// we gave it so we can detect when it becomes stale
   143  		c.revisions[clusterRef] = revision
   144  	}
   145  
   146  	return cli, nil
   147  }