github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/authenticator/patcher/patcher.go (about)

     1  package patcher
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  
     9  	"k8s.io/client-go/tools/clientcmd"
    10  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    11  
    12  	"github.com/datawire/dlib/dlog"
    13  	"github.com/telepresenceio/telepresence/rpc/v2/connector"
    14  	"github.com/telepresenceio/telepresence/rpc/v2/daemon"
    15  	"github.com/telepresenceio/telepresence/v2/pkg/filelocation"
    16  	"github.com/telepresenceio/telepresence/v2/pkg/ioutil"
    17  	"github.com/telepresenceio/telepresence/v2/pkg/maps"
    18  )
    19  
    20  const (
    21  	kubeConfigStubSubCommands = "kubeauth"
    22  	kubeConfigs               = "kube"
    23  )
    24  
    25  // AddressProvider is a function that returns the path to the telepresence executable and an address to a service that
    26  // implements the Authenticator gRPC.
    27  //
    28  // The function will typically start the gRPC service, and the service is therefore given
    29  // a list of files that it must listen to in order to reliably resolve requests. It is
    30  // also passed a pointer to the minified config that will be stored in a file so that it
    31  // has a chance to modify it.
    32  type (
    33  	AddressProvider func(configFiles []string) (string, string, error)
    34  	Patcher         func(*clientcmdapi.Config) error
    35  )
    36  
    37  // CreateExternalKubeConfig will load the current kubeconfig and minimize it so that it just contains the current
    38  // context. It will then check if that context contains an Exec config, and if it does, replace that config with
    39  // an Exec config that instead runs a process that will use a gRPC call to the address returned by the given
    40  // authAddressFunc.
    41  func CreateExternalKubeConfig(
    42  	ctx context.Context,
    43  	loader clientcmd.ClientConfig,
    44  	kubeContext string,
    45  	authAddressFunc AddressProvider,
    46  	patcher Patcher,
    47  ) (*clientcmdapi.Config, error) {
    48  	ns, _, err := loader.Namespace()
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  
    53  	configFiles := loader.ConfigAccess().GetLoadingPrecedence()
    54  	dlog.Debugf(ctx, "host kubeconfig = %v", configFiles)
    55  	origConfig, err := loader.RawConfig()
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  	var config clientcmdapi.Config
    60  	origConfig.DeepCopyInto(&config)
    61  
    62  	// Minify the config so that we only deal with the current context.
    63  	if kubeContext != "" {
    64  		config.CurrentContext = kubeContext
    65  	}
    66  	if err = clientcmdapi.MinifyConfig(&config); err != nil {
    67  		return nil, err
    68  	}
    69  	dlog.Debugf(ctx, "context = %q, namespace %q", config.CurrentContext, ns)
    70  
    71  	// Minify guarantees that the CurrentContext is set, but not that it has a cluster
    72  	cc := config.Contexts[config.CurrentContext]
    73  	if cc.Cluster == "" {
    74  		return nil, fmt.Errorf("current context %q has no cluster", config.CurrentContext)
    75  	}
    76  
    77  	if needsStubbedExec(&config) {
    78  		executable, addr, err := authAddressFunc(configFiles)
    79  		if err != nil {
    80  			return nil, err
    81  		}
    82  		if err = replaceAuthExecWithStub(&config, executable, addr); err != nil {
    83  			return nil, err
    84  		}
    85  	}
    86  
    87  	// Ensure that all certs are embedded instead of reachable using a path
    88  	if err = clientcmdapi.FlattenConfig(&config); err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	if patcher != nil {
    93  		if err = patcher(&config); err != nil {
    94  			return nil, err
    95  		}
    96  	}
    97  
    98  	// Store the file using its context name under the <telepresence cache>/kube directory
    99  	kubeConfigFile := ioutil.SafeName(config.CurrentContext)
   100  	kubeConfigDir := filepath.Join(filelocation.AppUserCacheDir(ctx), kubeConfigs)
   101  	if err = os.MkdirAll(kubeConfigDir, 0o700); err != nil {
   102  		return nil, err
   103  	}
   104  	if err = clientcmd.WriteToFile(config, filepath.Join(kubeConfigDir, kubeConfigFile)); err != nil {
   105  		return nil, err
   106  	}
   107  	return &config, nil
   108  }
   109  
   110  // replaceAuthExecWithStub goes through the kubeconfig and replaces all uses of the Exec auth method by
   111  // an invocation of the stub binary.
   112  func replaceAuthExecWithStub(rawConfig *clientcmdapi.Config, executable, address string) error {
   113  	for contextName, kubeContext := range rawConfig.Contexts {
   114  		// Find related Auth.
   115  		authInfo, ok := rawConfig.AuthInfos[kubeContext.AuthInfo]
   116  		if !ok {
   117  			return fmt.Errorf("auth info %s not found for context %s", kubeContext.AuthInfo, contextName)
   118  		}
   119  
   120  		// If it isn't an exec mode context, just return the default host kubeconfig.
   121  		if authInfo.Exec == nil {
   122  			continue
   123  		}
   124  
   125  		// Patch exec.
   126  		authInfo.Exec = &clientcmdapi.ExecConfig{
   127  			InteractiveMode: clientcmdapi.NeverExecInteractiveMode,
   128  			APIVersion:      authInfo.Exec.APIVersion,
   129  			Command:         executable,
   130  			Args:            []string{kubeConfigStubSubCommands, contextName, address},
   131  		}
   132  	}
   133  	return nil
   134  }
   135  
   136  // needsStubbedExec returns true if the config contains at least one user with an Exec type AuthInfo.
   137  func needsStubbedExec(rawConfig *clientcmdapi.Config) bool {
   138  	for _, kubeContext := range rawConfig.Contexts {
   139  		if authInfo, ok := rawConfig.AuthInfos[kubeContext.AuthInfo]; ok && authInfo.Exec != nil {
   140  			return true
   141  		}
   142  	}
   143  	return false
   144  }
   145  
   146  // AnnotateConnectRequest is used when the CLI connects to a containerized user-daemon. It adds a ContainerKubeFlagOverrides
   147  // to the given ConnectRequest containing the path to the modified kubeconfig file to be used in the container.
   148  func AnnotateConnectRequest(cr *connector.ConnectRequest, cacheDir, kubeContext string) {
   149  	kubeConfigFile := ioutil.SafeName(kubeContext)
   150  	if cr.ContainerKubeFlagOverrides == nil {
   151  		cr.ContainerKubeFlagOverrides = make(map[string]string)
   152  	}
   153  	// Concatenate using "/". This will be used in linux
   154  	cr.ContainerKubeFlagOverrides["kubeconfig"] = fmt.Sprintf("%s/%s/%s", cacheDir, kubeConfigs, kubeConfigFile)
   155  
   156  	// We never instruct the remote containerized daemon to modify its KUBECONFIG environment.
   157  	delete(cr.Environment, "KUBECONFIG")
   158  	delete(cr.Environment, "-KUBECONFIG")
   159  }
   160  
   161  // AnnotateOutboundInfo is used when a non-containerized user-daemon connects to the root-daemon. The KubeFlags
   162  // are modified to contain the path to the modified kubeconfig file.
   163  func AnnotateOutboundInfo(ctx context.Context, oi *daemon.OutboundInfo, kubeContext string) {
   164  	kubeConfigFile := ioutil.SafeName(kubeContext)
   165  	if oi.KubeFlags == nil {
   166  		oi.KubeFlags = make(map[string]string)
   167  	} else {
   168  		oi.KubeFlags = maps.Copy(oi.KubeFlags)
   169  	}
   170  	// Concatenate using "/". This will be used in linux
   171  	oi.KubeFlags["kubeconfig"] = fmt.Sprintf("%s/%s/%s", filelocation.AppUserCacheDir(ctx), kubeConfigs, kubeConfigFile)
   172  }