github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/cli/daemon/request.go (about) 1 package daemon 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "net/netip" 9 "os" 10 "regexp" 11 "strconv" 12 "strings" 13 14 "github.com/spf13/cobra" 15 "github.com/spf13/pflag" 16 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 "k8s.io/apimachinery/pkg/util/validation" 18 "k8s.io/cli-runtime/pkg/genericclioptions" 19 "k8s.io/client-go/kubernetes" 20 "k8s.io/client-go/tools/clientcmd" 21 "k8s.io/client-go/tools/clientcmd/api" 22 23 "github.com/datawire/dlib/dlog" 24 "github.com/telepresenceio/telepresence/rpc/v2/connector" 25 "github.com/telepresenceio/telepresence/rpc/v2/daemon" 26 "github.com/telepresenceio/telepresence/v2/pkg/client/cli/global" 27 "github.com/telepresenceio/telepresence/v2/pkg/errcat" 28 "github.com/telepresenceio/telepresence/v2/pkg/slice" 29 ) 30 31 type Request struct { 32 connector.ConnectRequest 33 34 // If set, then use a containerized daemon for the connection. 35 Docker bool 36 37 // Ports exposed by a containerized daemon. Only valid when Docker == true 38 ExposedPorts []string 39 40 // Hostname used by a containerized daemon. Only valid when Docker == true 41 Hostname string 42 43 // Match expression to use when finding an existing connection by name 44 Use *regexp.Regexp 45 46 // Request is created on-demand, not by InitRequest 47 Implicit bool 48 49 kubeConfig *genericclioptions.ConfigFlags 50 UserDaemonProfilingPort uint16 51 RootDaemonProfilingPort uint16 52 53 // proxyVia holds the string version for the --proxy-via flag values. 54 proxyVia []string 55 } 56 57 type CobraRequest struct { 58 Request 59 kubeFlagSet *pflag.FlagSet 60 } 61 62 // InitRequest adds the networking flags and Kubernetes flags to the given command and 63 // returns a Request and a FlagSet with the Kubernetes flags. The FlagSet is returned 64 // here so that a map of flags that gets modified can be extracted using FlagMap once the flag 65 // parsing has completed. 66 func InitRequest(cmd *cobra.Command) *CobraRequest { 67 cr := CobraRequest{} 68 flags := cmd.Flags() 69 70 nwFlags := pflag.NewFlagSet("Telepresence networking flags", 0) 71 nwFlags.StringVar(&cr.Name, "name", "", "Optional name to use for the connection") 72 nwFlags.StringSliceVar(&cr.MappedNamespaces, 73 "mapped-namespaces", nil, ``+ 74 `Comma separated list of namespaces considered by DNS resolver and NAT for outbound connections. `+ 75 `Defaults to all namespaces`) 76 nwFlags.StringVar(&cr.ManagerNamespace, "manager-namespace", "", `The namespace where the traffic manager is to be found. `+ 77 `Overrides any other manager namespace set in config`) 78 nwFlags.StringSliceVar(&cr.AlsoProxy, 79 "also-proxy", nil, ``+ 80 `Additional comma separated list of CIDR to proxy`) 81 nwFlags.StringSliceVar(&cr.NeverProxy, 82 "never-proxy", nil, ``+ 83 `Comma separated list of CIDR to never proxy`) 84 nwFlags.StringSliceVar(&cr.proxyVia, 85 "proxy-via", nil, ``+ 86 `Locally translate cluster DNS responses matching CIDR to virtual IPs that are routed (with reverse `+ 87 `translation) via WORKLOAD. Must be in the form CIDR=WORKLOAD. CIDR can be substituted for the symblic name "service", "pods", "also", or "all".`) 88 nwFlags.StringSliceVar(&cr.AllowConflictingSubnets, 89 "allow-conflicting-subnets", nil, ``+ 90 `Comma separated list of CIDR that will be allowed to conflict with local subnets`) 91 92 // Docker flags 93 nwFlags.Bool(global.FlagDocker, false, "Start, or connect to, daemon in a docker container") 94 nwFlags.StringArrayVar(&cr.ExposedPorts, 95 "expose", nil, ``+ 96 `Port that a containerized daemon will expose. See docker run -p for more info. Can be repeated`) 97 nwFlags.StringVar(&cr.Hostname, 98 "hostname", "", ``+ 99 `Hostname used by a containerized daemon`) 100 101 flags.AddFlagSet(nwFlags) 102 103 dbgFlags := pflag.NewFlagSet("Debug and Profiling flags", 0) 104 dbgFlags.Uint16Var(&cr.UserDaemonProfilingPort, 105 "userd-profiling-port", 0, "Start a pprof server in the user daemon on this port") 106 _ = dbgFlags.MarkHidden("userd-profiling-port") 107 dbgFlags.Uint16Var(&cr.RootDaemonProfilingPort, 108 "rootd-profiling-port", 0, "Start a pprof server in the root daemon on this port") 109 _ = dbgFlags.MarkHidden("rootd-profiling-port") 110 flags.AddFlagSet(dbgFlags) 111 112 cr.kubeConfig = genericclioptions.NewConfigFlags(false) 113 cr.KubeFlags = make(map[string]string) 114 cr.kubeFlagSet = pflag.NewFlagSet("Kubernetes flags", 0) 115 cr.kubeConfig.AddFlags(cr.kubeFlagSet) 116 flags.AddFlagSet(cr.kubeFlagSet) 117 _ = cmd.RegisterFlagCompletionFunc("namespace", cr.autocompleteNamespace) 118 _ = cmd.RegisterFlagCompletionFunc("cluster", cr.autocompleteCluster) 119 return &cr 120 } 121 122 type requestKey struct{} 123 124 func (cr *CobraRequest) CommitFlags(cmd *cobra.Command) error { 125 var err error 126 cr.kubeFlagSet.VisitAll(func(flag *pflag.Flag) { 127 if flag.Changed { 128 var v string 129 if sv, ok := flag.Value.(pflag.SliceValue); ok { 130 v = slice.AsCSV(sv.GetSlice()) 131 } else { 132 v = flag.Value.String() 133 if flag.Name == "kubeconfig" && v == "-" { 134 // Read kubeconfig from stdin 135 cr.KubeconfigData, err = io.ReadAll(cmd.InOrStdin()) 136 return // kubernetes will not understand "-" 137 } 138 } 139 cr.KubeFlags[flag.Name] = v 140 } 141 }) 142 if err != nil { 143 return err 144 } 145 err = cr.setGlobalConnectFlags(cmd) 146 if err != nil { 147 return errcat.User.New(err) 148 } 149 ctx, err := cr.Commit(cmd.Context()) 150 if err != nil { 151 return err 152 } 153 cmd.SetContext(ctx) 154 return nil 155 } 156 157 func (cr *Request) Commit(ctx context.Context) (context.Context, error) { 158 cr.addKubeconfigEnv() 159 var err error 160 cr.SubnetViaWorkloads, err = parseProxyVias(cr.proxyVia) 161 if err != nil { 162 return ctx, errcat.User.New(err) 163 } 164 if len(cr.KubeconfigData) > 0 { 165 kc, err := clientcmd.Load(cr.KubeconfigData) 166 if err != nil { 167 return ctx, fmt.Errorf("unable to parse kubeconfig: %w", err) 168 } 169 if cr.KubeFlags == nil { 170 cr.KubeFlags = make(map[string]string) 171 } 172 if _, ok := cr.KubeFlags["context"]; !ok { 173 cr.KubeFlags["context"] = kc.CurrentContext 174 } 175 if _, ok := cr.KubeFlags["namespace"]; !ok { 176 if currCtx, ok := kc.Contexts[kc.CurrentContext]; ok { 177 cr.KubeFlags["namespace"] = currCtx.Namespace 178 } 179 } 180 // kubernetes will not understand "-" 181 delete(cr.KubeFlags, "kubeconfig") 182 } 183 return context.WithValue(ctx, requestKey{}, cr), nil 184 } 185 186 type prefixViaWL struct { 187 subnet netip.Prefix 188 symbolic string 189 workload string 190 } 191 192 func parseProxyVias(proxyVia []string) ([]*daemon.SubnetViaWorkload, error) { 193 l := len(proxyVia) 194 if l == 0 { 195 return nil, nil 196 } 197 pvs := make([]prefixViaWL, 0, l) 198 for _, dps := range proxyVia { 199 dp, err := parseSubnetViaWorkload(dps) 200 if err != nil { 201 return nil, err 202 } 203 lastPvs := len(pvs) - 1 204 switch dp.symbolic { 205 case "": 206 for pi := lastPvs; pi >= 0; pi-- { 207 pv := pvs[pi] 208 if pv.symbolic == "" && pv.subnet.Overlaps(dp.subnet) { 209 return nil, fmt.Errorf("CIDRs %s and %s are overlapping", pv.subnet, dp.subnet) 210 } 211 } 212 pvs = append(pvs, dp) 213 case "all": 214 for pi := lastPvs; pi >= 0; pi-- { 215 pv := pvs[pi] 216 if pv.symbolic != "" { 217 return nil, fmt.Errorf("CIDRs %s and %s are overlapping", pv.symbolic, dp.symbolic) 218 } 219 } 220 // Normalize by replacing "all" with "also", "pods", and "service" 221 for _, sym := range []string{"also", "pods", "service"} { 222 pvs = append(pvs, 223 prefixViaWL{ 224 symbolic: sym, 225 workload: dp.workload, 226 }) 227 } 228 default: 229 for pi := lastPvs; pi >= 0; pi-- { 230 pv := pvs[pi] 231 if pv.symbolic == dp.symbolic { 232 return nil, fmt.Errorf("CIDRs %s and %s are overlapping", pv.symbolic, dp.symbolic) 233 } 234 } 235 pvs = append(pvs, dp) 236 } 237 } 238 svs := make([]*daemon.SubnetViaWorkload, len(pvs)) 239 for i, pv := range pvs { 240 n := pv.symbolic 241 if n == "" { 242 n = pv.subnet.String() 243 } 244 svs[i] = &daemon.SubnetViaWorkload{ 245 Subnet: n, 246 Workload: pv.workload, 247 } 248 } 249 return svs, nil 250 } 251 252 func parseSubnetViaWorkload(dps string) (prefixViaWL, error) { 253 var pv prefixViaWL 254 eqIdx := strings.IndexByte(dps, '=') 255 if eqIdx <= 0 { 256 return pv, fmt.Errorf("--proxy-via %q is not in the format CIDR=WORKLOAD", dps) 257 } 258 lhs := dps[:eqIdx] 259 rhs := dps[eqIdx+1:] 260 if errs := validation.IsDNS1123Label(rhs); len(errs) > 0 { 261 return pv, errors.New(errs[0]) 262 } 263 if sn, err := netip.ParsePrefix(lhs); err != nil { 264 if !(lhs == "all" || lhs == "also" || lhs == "pods" || lhs == "service") { 265 return pv, err 266 } 267 pv.symbolic = lhs 268 } else { 269 pv.subnet = sn 270 } 271 pv.workload = rhs 272 return pv, nil 273 } 274 275 func (cr *Request) addKubeconfigEnv() { 276 // Certain options' default are bound to the connector daemon process; this is notably true of the kubeconfig file(s) to use, 277 // and since those files can be specified, both as a --kubeconfig flag and in the KUBECONFIG setting, and since the flag won't 278 // accept multiple path entries, we need to pass the environment setting to the connector daemon so that it can set it every 279 // time it receives a new config. 280 cr.Environment = make(map[string]string, 2) 281 addEnv := func(key string) { 282 if v, ok := os.LookupEnv(key); ok { 283 cr.Environment[key] = v 284 } else { 285 // A dash prefix in the key means "unset". 286 cr.Environment["-"+key] = "" 287 } 288 } 289 addEnv("KUBECONFIG") 290 addEnv("GOOGLE_APPLICATION_CREDENTIALS") 291 } 292 293 // setContext deals with the global --context flag and assigns it to KubeFlags because it's 294 // deliberately excluded from the original flags (to avoid conflict with the global flag). 295 func (cr *Request) setGlobalConnectFlags(cmd *cobra.Command) error { 296 if contextFlag := cmd.Flag(global.FlagContext); contextFlag != nil && contextFlag.Changed { 297 cn := contextFlag.Value.String() 298 cr.KubeFlags[global.FlagContext] = cn 299 cr.kubeConfig.Context = &cn 300 } 301 if dockerFlag := cmd.Flag(global.FlagDocker); dockerFlag != nil && dockerFlag.Changed { 302 cr.Docker, _ = strconv.ParseBool(dockerFlag.Value.String()) 303 } 304 if useFlag := cmd.Flag(global.FlagUse); useFlag != nil && useFlag.Changed { 305 var err error 306 if cr.Use, err = regexp.Compile(useFlag.Value.String()); err != nil { 307 return err 308 } 309 } 310 return nil 311 } 312 313 func GetRequest(ctx context.Context) *Request { 314 if cr, ok := ctx.Value(requestKey{}).(*Request); ok { 315 return cr 316 } 317 return nil 318 } 319 320 func WithDefaultRequest(ctx context.Context, cmd *cobra.Command) (context.Context, error) { 321 cr := NewDefaultRequest() 322 cr.Implicit = true 323 cr.kubeConfig.Context = nil // --context is global 324 325 // Handle deprecated namespace flag, but allow it in the list command. 326 if cmd.Name() != "list" { 327 if nsFlag := cmd.Flag("namespace"); nsFlag != nil && nsFlag.Changed { 328 ns := nsFlag.Value.String() 329 *cr.kubeConfig.Namespace = ns 330 cr.KubeFlags["namespace"] = ns 331 } 332 } 333 if err := cr.setGlobalConnectFlags(cmd); err != nil { 334 return ctx, err 335 } 336 return WithRequest(ctx, cr), nil 337 } 338 339 func WithRequest(ctx context.Context, cr *Request) context.Context { 340 return context.WithValue(ctx, requestKey{}, cr) 341 } 342 343 func NewDefaultRequest() *Request { 344 cr := Request{ 345 ConnectRequest: connector.ConnectRequest{ 346 KubeFlags: make(map[string]string), 347 }, 348 kubeConfig: genericclioptions.NewConfigFlags(false), 349 } 350 cr.addKubeconfigEnv() 351 return &cr 352 } 353 354 func GetKubeStartingConfig(cmd *cobra.Command) (*api.Config, error) { 355 pathOpts := clientcmd.NewDefaultPathOptions() 356 if kcFlag := cmd.Flag("kubeconfig"); kcFlag != nil && kcFlag.Changed { 357 pathOpts.ExplicitFileFlag = kcFlag.Value.String() 358 } 359 return pathOpts.GetStartingConfig() 360 } 361 362 func (cr *CobraRequest) GetAllNamespaces(cmd *cobra.Command) ([]string, error) { 363 if err := cr.CommitFlags(cmd); err != nil { 364 return nil, err 365 } 366 rs, err := cr.kubeConfig.ToRESTConfig() 367 if err != nil { 368 return nil, errcat.NoDaemonLogs.Newf("ToRESTConfig: %v", err) 369 } 370 cs, err := kubernetes.NewForConfig(rs) 371 if err != nil { 372 return nil, errcat.NoDaemonLogs.Newf("NewForConfig: %v", err) 373 } 374 nsl, err := cs.CoreV1().Namespaces().List(cmd.Context(), v1.ListOptions{}) 375 if err != nil { 376 return nil, errcat.NoDaemonLogs.Newf("Namespaces.List: %v", err) 377 } 378 itms := nsl.Items 379 nss := make([]string, len(itms)) 380 for i, itm := range itms { 381 nss[i] = itm.Name 382 } 383 return nss, nil 384 } 385 386 func (cr *CobraRequest) autocompleteNamespace(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { 387 ctx := cmd.Context() 388 nss, err := cr.GetAllNamespaces(cmd) 389 if err != nil { 390 dlog.Error(ctx, err) 391 return nil, cobra.ShellCompDirectiveError 392 } 393 394 var ctName string 395 if cp := cr.kubeConfig.Context; cp != nil { 396 ctName = *cp 397 } 398 dlog.Debugf(ctx, "namespace completion: context %q, %q", ctName, toComplete) 399 400 return nss, cobra.ShellCompDirectiveNoFileComp 401 } 402 403 func (cr *CobraRequest) autocompleteCluster(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { 404 ctx := cmd.Context() 405 config, err := cr.GetConfig(cmd) 406 if err != nil { 407 dlog.Error(ctx, err) 408 return nil, cobra.ShellCompDirectiveError 409 } 410 411 var ctName string 412 if cp := cr.kubeConfig.Context; cp != nil { 413 ctName = *cp 414 } 415 dlog.Debugf(ctx, "namespace completion: context %q, %q", ctName, toComplete) 416 417 cxl := config.Clusters 418 cs := make([]string, len(cxl)) 419 i := 0 420 for n := range cxl { 421 cs[i] = n 422 i++ 423 } 424 return cs, cobra.ShellCompDirectiveNoFileComp 425 } 426 427 func (cr *CobraRequest) GetConfig(cmd *cobra.Command) (*api.Config, error) { 428 if err := cr.CommitFlags(cmd); err != nil { 429 return nil, err 430 } 431 cfg, err := GetKubeStartingConfig(cmd) 432 if err != nil { 433 return nil, errcat.NoDaemonLogs.Newf("GetKubeStartingConfig: %v", err) 434 } 435 return cfg, nil 436 }