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 }