github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/userd/k8s/outbound.go (about)

     1  package k8s
     2  
     3  import (
     4  	"context"
     5  	"math"
     6  	"sort"
     7  	"time"
     8  
     9  	auth "k8s.io/api/authorization/v1"
    10  	core "k8s.io/api/core/v1"
    11  	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
    12  	"k8s.io/apimachinery/pkg/watch"
    13  
    14  	"github.com/datawire/dlib/dlog"
    15  	"github.com/datawire/k8sapi/pkg/k8sapi"
    16  	"github.com/telepresenceio/telepresence/v2/pkg/client/k8sclient"
    17  	"github.com/telepresenceio/telepresence/v2/pkg/client/userd"
    18  	"github.com/telepresenceio/telepresence/v2/pkg/slice"
    19  )
    20  
    21  // StartNamespaceWatcher runs a Kubernetes Watcher that provide information about the cluster's namespaces'.
    22  // The function waits for the first snapshot to arrive before returning.
    23  func (kc *Cluster) StartNamespaceWatcher(ctx context.Context) {
    24  	kc.namespaceWatcherSnapshot = make(map[string]struct{})
    25  	nsSynced := make(chan struct{})
    26  	go func() {
    27  		api := kc.ki.CoreV1()
    28  		for ctx.Err() == nil {
    29  			w, err := api.Namespaces().Watch(ctx, meta.ListOptions{})
    30  			if err != nil {
    31  				dlog.Errorf(ctx, "unable to create service watcher: %v", err)
    32  				return
    33  			}
    34  			kc.namespacesEventHandler(ctx, w.ResultChan(), nsSynced)
    35  		}
    36  	}()
    37  	select {
    38  	case <-ctx.Done():
    39  	case <-nsSynced:
    40  	}
    41  }
    42  
    43  func (kc *Cluster) namespacesEventHandler(ctx context.Context, evCh <-chan watch.Event, nsSynced chan struct{}) {
    44  	// The delay timer will initially sleep forever. It's reset to a very short
    45  	// delay when the file is modified.
    46  	delay := time.AfterFunc(time.Duration(math.MaxInt64), func() {
    47  		kc.refreshNamespaces(ctx)
    48  		select {
    49  		case <-nsSynced:
    50  		default:
    51  			close(nsSynced)
    52  		}
    53  	})
    54  	defer delay.Stop()
    55  
    56  	for {
    57  		select {
    58  		case <-ctx.Done():
    59  			return
    60  		case event, ok := <-evCh:
    61  			if !ok {
    62  				return // restart watcher
    63  			}
    64  			ns, ok := event.Object.(*core.Namespace)
    65  			if !ok {
    66  				continue
    67  			}
    68  			kc.nsLock.Lock()
    69  			switch event.Type {
    70  			case watch.Deleted:
    71  				delete(kc.namespaceWatcherSnapshot, ns.Name)
    72  			case watch.Added, watch.Modified:
    73  				kc.namespaceWatcherSnapshot[ns.Name] = struct{}{}
    74  			}
    75  			kc.nsLock.Unlock()
    76  
    77  			// We consider the watcher synced after 10 ms of inactivity. It's not a big deal
    78  			// if more namespaces arrive after that.
    79  			delay.Reset(10 * time.Millisecond)
    80  		}
    81  	}
    82  }
    83  
    84  // canGetDefaultTrafficManagerService answers the question if this client has the RBAC permissions
    85  // necessary to get the traffic-manager in the default namespace.
    86  func canGetDefaultTrafficManagerService(ctx context.Context) bool {
    87  	ok, err := k8sclient.CanI(ctx, &auth.ResourceAttributes{
    88  		Verb:      "get",
    89  		Resource:  "services",
    90  		Name:      "traffic-manager",
    91  		Namespace: defaultManagerNamespace,
    92  	})
    93  	return err == nil && ok
    94  }
    95  
    96  // canAccessNS answers the question if this client has the RBAC permissions
    97  // necessary to list and intercept workloads the namespace.
    98  func canAccessNS(ctx context.Context, namespace string) bool {
    99  	authHandler := k8sapi.GetK8sInterface(ctx).AuthorizationV1().SelfSubjectRulesReviews()
   100  	review := auth.SelfSubjectRulesReview{Spec: auth.SelfSubjectRulesReviewSpec{Namespace: namespace}}
   101  	rr, err := authHandler.Create(ctx, &review, meta.CreateOptions{})
   102  	if err != nil {
   103  		dlog.Errorf(ctx, `unable to do "can-i --list" on namespace %s`, namespace)
   104  	}
   105  	if rr.Status.Incomplete {
   106  		// Incomplete is most commonly encountered when an authorizer, such as an external authorizer, doesn't support rules evaluation.
   107  		// When this happens, we must default to using standard can-i semantics and only check deployments (checking every single
   108  		// resource here takes a long time, so this is a best-effort).
   109  		ok, err := k8sclient.CanI(ctx, &auth.ResourceAttributes{
   110  			Namespace: namespace,
   111  			Verb:      "get",
   112  			Resource:  "deployments",
   113  			Group:     "apps",
   114  		})
   115  		return err == nil && ok
   116  	}
   117  	ras := []*auth.ResourceAttributes{
   118  		{
   119  			Resource: "services",
   120  			Verb:     "list",
   121  		},
   122  		{
   123  			Resource: "services",
   124  			Verb:     "watch",
   125  		},
   126  	}
   127  	for _, r := range []string{"deployments", "replicasets", "statefulsets"} {
   128  		for _, v := range []string{"get", "watch", "list"} {
   129  			ras = append(ras, &auth.ResourceAttributes{
   130  				Group:    "apps",
   131  				Resource: r,
   132  				Verb:     v,
   133  			})
   134  		}
   135  	}
   136  
   137  	sliceMatch := func(vs []string, s string) bool {
   138  		return slice.Contains(vs, "*") || slice.Contains(vs, s)
   139  	}
   140  	// canDo will just compare the group, verb, and resource property. We know that the namespace is correct, and
   141  	// we don't care about names or sub-resources.
   142  	canDo := func(ra *auth.ResourceAttributes) bool {
   143  		for _, rule := range rr.Status.ResourceRules {
   144  			if sliceMatch(rule.APIGroups, ra.Group) && sliceMatch(rule.Verbs, ra.Verb) && sliceMatch(rule.Resources, ra.Resource) {
   145  				return true
   146  			}
   147  		}
   148  		return false
   149  	}
   150  	for _, ra := range ras {
   151  		if !canDo(ra) {
   152  			dlog.Errorf(ctx, `client can't do %s %s/%s in namespace %s`, ra.Verb, ra.Group, ra.Resource, namespace)
   153  			return false
   154  		}
   155  	}
   156  	return true
   157  }
   158  
   159  func sortedStringSlicesEqual(as, bs []string) bool {
   160  	if len(as) != len(bs) {
   161  		return false
   162  	}
   163  	for i, a := range as {
   164  		if a != bs[i] {
   165  			return false
   166  		}
   167  	}
   168  	return true
   169  }
   170  
   171  func (kc *Cluster) SetMappedNamespaces(c context.Context, namespaces []string) bool {
   172  	sort.Strings(namespaces)
   173  	if !sortedStringSlicesEqual(namespaces, kc.MappedNamespaces) {
   174  		kc.MappedNamespaces = namespaces
   175  		kc.refreshNamespaces(c)
   176  		return true
   177  	}
   178  	return false
   179  }
   180  
   181  func (kc *Cluster) AddNamespaceListener(c context.Context, nsListener userd.NamespaceListener) {
   182  	kc.nsLock.Lock()
   183  	kc.namespaceListeners = append(kc.namespaceListeners, nsListener)
   184  	kc.nsLock.Unlock()
   185  	nsListener(c)
   186  }
   187  
   188  func (kc *Cluster) refreshNamespaces(c context.Context) {
   189  	kc.nsLock.Lock()
   190  	defer kc.nsLock.Unlock()
   191  	var nss []string
   192  	if kc.namespaceWatcherSnapshot == nil {
   193  		// No permission to watch namespaces. Use the mapped-namespaces instead.
   194  		nss = kc.MappedNamespaces
   195  		if len(nss) == 0 {
   196  			// No mapped namespaces exists. Fallback to what's defined in the kube-context (will be "default" if none was defined).
   197  			nss = []string{kc.Namespace}
   198  		}
   199  	} else {
   200  		nss = make([]string, len(kc.namespaceWatcherSnapshot))
   201  		i := 0
   202  		for ns := range kc.namespaceWatcherSnapshot {
   203  			nss[i] = ns
   204  			i++
   205  		}
   206  	}
   207  	namespaces := make(map[string]bool, len(nss))
   208  	for _, ns := range nss {
   209  		if kc.shouldBeWatched(ns) {
   210  			accessOk, ok := kc.currentMappedNamespaces[ns]
   211  			if !ok {
   212  				accessOk = canAccessNS(c, ns)
   213  			}
   214  			namespaces[ns] = accessOk
   215  		}
   216  	}
   217  	equal := len(namespaces) == len(kc.currentMappedNamespaces)
   218  	if equal {
   219  		for k, ov := range kc.currentMappedNamespaces {
   220  			if nv, ok := namespaces[k]; !ok || nv != ov {
   221  				equal = false
   222  				break
   223  			}
   224  		}
   225  	}
   226  	if equal {
   227  		return
   228  	}
   229  	kc.currentMappedNamespaces = namespaces
   230  	for _, nsListener := range kc.namespaceListeners {
   231  		func() {
   232  			kc.nsLock.Unlock()
   233  			defer kc.nsLock.Lock()
   234  			nsListener(c)
   235  		}()
   236  	}
   237  }
   238  
   239  func (kc *Cluster) shouldBeWatched(namespace string) bool {
   240  	if len(kc.MappedNamespaces) == 0 {
   241  		return true
   242  	}
   243  	for _, n := range kc.MappedNamespaces {
   244  		if n == namespace {
   245  			return true
   246  		}
   247  	}
   248  	return false
   249  }