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  }