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 }