github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/docker/kubeauth/cmd.go (about)

     1  package kubeauth
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/fsnotify/fsnotify"
    14  	"github.com/spf13/cobra"
    15  	"google.golang.org/grpc"
    16  	"k8s.io/cli-runtime/pkg/genericclioptions"
    17  
    18  	"github.com/datawire/dlib/dgroup"
    19  	"github.com/datawire/dlib/dhttp"
    20  	"github.com/datawire/dlib/dlog"
    21  	authGrpc "github.com/telepresenceio/telepresence/v2/pkg/authenticator/grpc"
    22  	"github.com/telepresenceio/telepresence/v2/pkg/client"
    23  	"github.com/telepresenceio/telepresence/v2/pkg/client/logging"
    24  	"github.com/telepresenceio/telepresence/v2/pkg/errcat"
    25  )
    26  
    27  const (
    28  	CommandName       = "kubeauth-foreground"
    29  	PortFileDir       = "kubeauth"
    30  	PortFileStaleTime = 3 * time.Second
    31  )
    32  
    33  type authService struct {
    34  	portFile    string
    35  	kubeFlags   *genericclioptions.ConfigFlags
    36  	cancel      context.CancelFunc
    37  	configFiles []string
    38  }
    39  
    40  type PortFile struct {
    41  	Port       int    `json:"port"`
    42  	Kubeconfig string `json:"kubeconfig"`
    43  }
    44  
    45  func Command() *cobra.Command {
    46  	as := authService{kubeFlags: genericclioptions.NewConfigFlags(false)}
    47  	c := &cobra.Command{
    48  		Use:    CommandName,
    49  		Short:  "Launch Telepresence Kubernetes authenticator",
    50  		Args:   cobra.NoArgs,
    51  		Hidden: true,
    52  		RunE:   as.run,
    53  	}
    54  	flags := c.Flags()
    55  	flags.StringVar(&as.portFile, "portfile", "", "File where server existence is announced.")
    56  	as.kubeFlags.AddFlags(flags)
    57  	return c
    58  }
    59  
    60  func (as *authService) run(cmd *cobra.Command, _ []string) error {
    61  	ctx := cmd.Context()
    62  	cfg, err := client.LoadConfig(ctx)
    63  	if err != nil {
    64  		return err
    65  	}
    66  	ctx = client.WithConfig(ctx, cfg)
    67  
    68  	if as.portFile == "" {
    69  		return errcat.User.New("missing required flag --portfile")
    70  	}
    71  	grpcListener, err := net.Listen("tcp", ":0")
    72  	if err != nil {
    73  		return errcat.NoDaemonLogs.Newf("unable to open a port on localhost: %w", err)
    74  	}
    75  
    76  	ctx, err = logging.InitContext(ctx, "kubeauth", logging.RotateNever, false)
    77  	if err != nil {
    78  		return err
    79  	}
    80  	addr := grpcListener.Addr().(*net.TCPAddr)
    81  	dlog.Infof(ctx, "kubeauth listening on address %s", addr)
    82  
    83  	config := as.kubeFlags.ToRawKubeConfigLoader()
    84  	as.configFiles = config.ConfigAccess().GetLoadingPrecedence()
    85  	p := PortFile{
    86  		Port:       addr.Port,
    87  		Kubeconfig: strings.Join(as.configFiles, string(filepath.ListSeparator)),
    88  	}
    89  	pb, err := json.Marshal(&p)
    90  	if err != nil {
    91  		return err
    92  	}
    93  	if err = os.WriteFile(as.portFile, pb, 0o644); err != nil {
    94  		return err
    95  	}
    96  
    97  	ctx, as.cancel = context.WithCancel(ctx)
    98  	g := dgroup.NewGroup(ctx, dgroup.GroupConfig{})
    99  	g.Go("portfile-alive", as.keepPortFileAlive)
   100  	g.Go("portfile-watcher", as.watchFiles)
   101  	g.Go("grpc-server", func(ctx context.Context) error {
   102  		grpcHandler := grpc.NewServer()
   103  		authGrpc.RegisterAuthenticatorServer(grpcHandler, config)
   104  		sc := &dhttp.ServerConfig{Handler: grpcHandler}
   105  		return sc.Serve(ctx, grpcListener)
   106  	})
   107  	return g.Wait()
   108  }
   109  
   110  func (as *authService) keepPortFileAlive(ctx context.Context) error {
   111  	ticker := time.NewTicker(PortFileStaleTime)
   112  	defer func() {
   113  		ticker.Stop()
   114  		_ = os.Remove(as.portFile)
   115  		dlog.Debugf(ctx, "kubeauth removed %s", as.portFile)
   116  	}()
   117  	now := time.Now()
   118  	for {
   119  		if err := os.Chtimes(as.portFile, now, now); err != nil {
   120  			if !os.IsNotExist(err) {
   121  				return fmt.Errorf("failed to update timestamp on %s: %v", as.portFile, err)
   122  			}
   123  			// File is removed, so stop trying to update its timestamps and die
   124  			dlog.Info(ctx, "kubeauth exiting")
   125  			as.cancel()
   126  			return nil
   127  		}
   128  		select {
   129  		case <-ctx.Done():
   130  			return nil
   131  		case now = <-ticker.C:
   132  		}
   133  	}
   134  }
   135  
   136  func (as *authService) watchFiles(ctx context.Context) error {
   137  	// If any of the files that the current kubeconfig uses change, then we die
   138  	files := as.configFiles
   139  
   140  	// If the portFile changes, then we die
   141  	files = append(files, as.portFile)
   142  
   143  	dirs := make(map[string]struct{})
   144  	for _, file := range files {
   145  		dir := filepath.Dir(file)
   146  		dirs[dir] = struct{}{}
   147  	}
   148  
   149  	watcher, err := fsnotify.NewWatcher()
   150  	if err != nil {
   151  		return err
   152  	}
   153  	defer watcher.Close()
   154  
   155  	isOfInterest := func(s string, files []string) bool {
   156  		for _, file := range files {
   157  			if s == file {
   158  				return true
   159  			}
   160  		}
   161  		return false
   162  	}
   163  	for dir := range dirs {
   164  		// Can't watch things that don't exist. We want to know if files in there change though.
   165  		if err := os.MkdirAll(dir, 0o755); err != nil {
   166  			return err
   167  		}
   168  		if err = watcher.Add(dir); err != nil {
   169  			return err
   170  		}
   171  	}
   172  	for {
   173  		select {
   174  		case <-ctx.Done():
   175  			return nil
   176  		case err = <-watcher.Errors:
   177  			dlog.Error(ctx, err)
   178  		case event := <-watcher.Events:
   179  			if event.Op&(fsnotify.Remove|fsnotify.Write|fsnotify.Create) != 0 && isOfInterest(event.Name, files) {
   180  				dlog.Infof(ctx, "Terminated due to %s in %s", event.Op, event.Name)
   181  				as.cancel()
   182  			}
   183  		}
   184  	}
   185  }