github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/userd/k8s/k8s_cluster.go (about) 1 package k8s 2 3 import ( 4 "context" 5 "fmt" 6 "sort" 7 "strings" 8 "sync" 9 "time" 10 11 "github.com/blang/semver/v4" 12 "k8s.io/apimachinery/pkg/version" 13 "k8s.io/client-go/kubernetes" 14 15 "github.com/datawire/dlib/dlog" 16 "github.com/datawire/dlib/dtime" 17 "github.com/datawire/k8sapi/pkg/k8sapi" 18 rpc "github.com/telepresenceio/telepresence/rpc/v2/connector" 19 "github.com/telepresenceio/telepresence/v2/pkg/client" 20 "github.com/telepresenceio/telepresence/v2/pkg/client/k8sclient" 21 "github.com/telepresenceio/telepresence/v2/pkg/client/userd" 22 "github.com/telepresenceio/telepresence/v2/pkg/errcat" 23 ) 24 25 const ( 26 supportedKubeAPIVersion = "1.17.0" 27 defaultManagerNamespace = "ambassador" 28 ) 29 30 // Cluster is a Kubernetes cluster reference. 31 type Cluster struct { 32 *client.Kubeconfig 33 MappedNamespaces []string 34 35 // Main 36 ki kubernetes.Interface 37 38 // nsLock protects namespaceWatcherSnapshot, currentMappedNamespaces and namespaceListeners 39 nsLock sync.Mutex 40 41 // snapshot maintained by the namespaces watcher. 42 namespaceWatcherSnapshot map[string]struct{} 43 44 // Current Namespace snapshot, filtered by MappedNamespaces 45 currentMappedNamespaces map[string]bool 46 47 // Namespace listener. Notified when the currentNamespaces changes 48 namespaceListeners []userd.NamespaceListener 49 } 50 51 func (kc *Cluster) ActualNamespace(namespace string) string { 52 if namespace == "" { 53 namespace = kc.Namespace 54 } 55 if !kc.namespaceAccessible(namespace) { 56 namespace = "" 57 } 58 return namespace 59 } 60 61 // check uses a non-caching DiscoveryClientConfig to retrieve the server version. 62 func (kc *Cluster) check(c context.Context) error { 63 // The discover client is using context.TODO() so the timeout specified in our 64 // context has no effect. 65 errCh := make(chan error) 66 go func() { 67 defer close(errCh) 68 var info *version.Info 69 var err error 70 for attempts := 0; attempts < 4; attempts++ { 71 if info, err = k8sapi.GetK8sInterface(c).Discovery().ServerVersion(); err != nil { 72 if strings.Contains(err.Error(), "connection refused") { 73 dlog.Warnf(c, "Connection to connect failed, retry %d", attempts+1) 74 dtime.SleepWithContext(c, 400*time.Millisecond) 75 continue 76 } 77 } 78 break 79 } 80 if err != nil { 81 errCh <- err 82 return 83 } 84 // Validate that the kubernetes server version is supported 85 dlog.Infof(c, "Server version %s", info.GitVersion) 86 gitVer, err := semver.Parse(strings.TrimPrefix(info.GitVersion, "v")) 87 if err != nil { 88 dlog.Errorf(c, "error converting version %s to semver: %s", info.GitVersion, err) 89 } 90 supGitVer, err := semver.Parse(supportedKubeAPIVersion) 91 if err != nil { 92 dlog.Errorf(c, "error converting known version %s to semver: %s", supportedKubeAPIVersion, err) 93 } 94 if gitVer.LT(supGitVer) { 95 dlog.Errorf(c, 96 "kubernetes server versions older than %s are not supported, using %s .", 97 supportedKubeAPIVersion, info.GitVersion) 98 } 99 }() 100 101 select { 102 case <-c.Done(): 103 case err := <-errCh: 104 if err == nil { 105 return nil 106 } 107 if c.Err() == nil { 108 return fmt.Errorf("initial cluster check failed: %w", client.RunError(err)) 109 } 110 } 111 return c.Err() 112 } 113 114 // namespaceAccessible answers the question if the namespace is present and accessible 115 // to this client. 116 func (kc *Cluster) namespaceAccessible(namespace string) (exists bool) { 117 kc.nsLock.Lock() 118 ok := kc.currentMappedNamespaces[namespace] 119 kc.nsLock.Unlock() 120 return ok 121 } 122 123 func NewCluster(c context.Context, kubeFlags *client.Kubeconfig, namespaces []string) (*Cluster, error) { 124 rs := kubeFlags.RestConfig 125 cs, err := kubernetes.NewForConfig(rs) 126 if err != nil { 127 return nil, err 128 } 129 c = k8sapi.WithK8sInterface(c, cs) 130 131 ret := &Cluster{ 132 Kubeconfig: kubeFlags, 133 ki: cs, 134 } 135 136 cfg := client.GetConfig(c) 137 timedC, cancel := cfg.Timeouts().TimeoutContext(c, client.TimeoutClusterConnect) 138 defer cancel() 139 if err = ret.check(timedC); err != nil { 140 return nil, err 141 } 142 143 dlog.Infof(c, "Context: %s", ret.Context) 144 dlog.Infof(c, "Server: %s", ret.Server) 145 146 if len(namespaces) == 1 && namespaces[0] == "all" { 147 namespaces = nil 148 } 149 if len(namespaces) == 0 { 150 namespaces = cfg.Cluster().MappedNamespaces 151 } 152 if len(namespaces) == 0 { 153 if k8sclient.CanWatchNamespaces(c) { 154 ret.StartNamespaceWatcher(c) 155 } 156 } else { 157 ret.SetMappedNamespaces(c, namespaces) 158 } 159 if ret.GetManagerNamespace() == "" { 160 ret.KubeconfigExtension.Manager.Namespace, err = ret.determineTrafficManagerNamespace(c) 161 if err != nil { 162 return nil, err 163 } 164 } 165 dlog.Infof(c, "Will look for traffic manager in namespace %s", ret.GetManagerNamespace()) 166 return ret, nil 167 } 168 169 func ConnectCluster(c context.Context, cr *rpc.ConnectRequest, config *client.Kubeconfig) (*Cluster, error) { 170 mappedNamespaces := cr.MappedNamespaces 171 if len(mappedNamespaces) == 1 && mappedNamespaces[0] == "all" { 172 mappedNamespaces = nil 173 } else { 174 sort.Strings(mappedNamespaces) 175 } 176 177 cluster, err := NewCluster(c, config, mappedNamespaces) 178 if err != nil { 179 return nil, err 180 } 181 return cluster, nil 182 } 183 184 // determineTrafficManagerNamespace finds the namespace for the traffic-manager. It is determined by the following steps: 185 // 186 // 1. If a treffic-manager service is found in one of the currently accessible namespaces, return it. 187 // 2. If the client has access to the default manager namespace, then return it. 188 // 3. If the client has access to the default namespace, then return it. 189 // 4. Return an error stating that it isn't possible to determine the namespace. 190 func (kc *Cluster) determineTrafficManagerNamespace(c context.Context) (string, error) { 191 // Search for the traffic-manager in mapped namespaces 192 nss := kc.GetCurrentNamespaces(true) 193 for _, ns := range nss { 194 if _, err := k8sapi.GetService(c, "traffic-manager", ns); err == nil { 195 return ns, nil 196 } 197 } 198 199 // No existing manager was found. 200 if canGetDefaultTrafficManagerService(c) { 201 return defaultManagerNamespace, nil 202 } 203 204 // No existing traffic-manager found. Assume that it should be installed 205 // in the default namespace if it is accessible 206 if canAccessNS(c, kc.Namespace) { 207 return kc.Namespace, nil 208 } 209 return "", errcat.User.New("unable to determine the traffic-manager namespace") 210 } 211 212 // GetCurrentNamespaces returns the names of the namespaces that this client 213 // is mapping. If the forClientAccess is true, then the namespaces are restricted 214 // to those where an intercept can take place, i.e. the namespaces where this 215 // client can Watch and get services and deployments. 216 func (kc *Cluster) GetCurrentNamespaces(forClientAccess bool) []string { 217 kc.nsLock.Lock() 218 nss := make([]string, 0, len(kc.currentMappedNamespaces)) 219 if forClientAccess { 220 for ns, ok := range kc.currentMappedNamespaces { 221 if ok { 222 nss = append(nss, ns) 223 } 224 } 225 } else { 226 for ns := range kc.currentMappedNamespaces { 227 nss = append(nss, ns) 228 } 229 } 230 kc.nsLock.Unlock() 231 sort.Strings(nss) 232 return nss 233 } 234 235 func (kc *Cluster) GetClusterId(ctx context.Context) string { 236 clusterID, _ := k8sapi.GetClusterID(ctx) 237 return clusterID 238 } 239 240 func (kc *Cluster) GetManagerInstallId(ctx context.Context) string { 241 managerID, _ := k8sapi.GetNamespaceID(ctx, kc.GetManagerNamespace()) 242 return managerID 243 } 244 245 func (kc *Cluster) WithK8sInterface(c context.Context) context.Context { 246 return k8sapi.WithK8sInterface(c, kc.ki) 247 }