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  }