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 }