github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/cli/connect/connector.go (about) 1 package connect 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "path/filepath" 9 "regexp" 10 "slices" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/blang/semver/v4" 16 "github.com/spf13/cobra" 17 "google.golang.org/grpc" 18 "google.golang.org/grpc/codes" 19 "google.golang.org/grpc/status" 20 "google.golang.org/protobuf/types/known/emptypb" 21 22 "github.com/datawire/dlib/dlog" 23 "github.com/telepresenceio/telepresence/rpc/v2/connector" 24 "github.com/telepresenceio/telepresence/v2/pkg/authenticator/patcher" 25 "github.com/telepresenceio/telepresence/v2/pkg/client" 26 "github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon" 27 "github.com/telepresenceio/telepresence/v2/pkg/client/cli/output" 28 "github.com/telepresenceio/telepresence/v2/pkg/client/docker" 29 "github.com/telepresenceio/telepresence/v2/pkg/client/socket" 30 "github.com/telepresenceio/telepresence/v2/pkg/dos" 31 "github.com/telepresenceio/telepresence/v2/pkg/errcat" 32 "github.com/telepresenceio/telepresence/v2/pkg/filelocation" 33 "github.com/telepresenceio/telepresence/v2/pkg/ioutil" 34 "github.com/telepresenceio/telepresence/v2/pkg/proc" 35 ) 36 37 var ( 38 ErrNoUserDaemon = errors.New("telepresence user daemon is not running") 39 ErrNoRootDaemon = errors.New("telepresence root daemon is not running") 40 ) 41 42 type ConnectError struct { 43 error 44 code connector.ConnectInfo_ErrType 45 } 46 47 func (ce *ConnectError) Code() connector.ConnectInfo_ErrType { 48 return ce.code 49 } 50 51 func (ce *ConnectError) Unwrap() error { 52 return ce.error 53 } 54 55 //nolint:gochecknoglobals // extension point 56 var QuitDaemonFuncs = []func(context.Context){ 57 quitHostConnector, quitDockerDaemons, 58 } 59 60 func quitHostConnector(ctx context.Context) { 61 if conn, err := socket.Dial(ctx, socket.UserDaemonPath(ctx)); err == nil { 62 if _, err = connector.NewConnectorClient(conn).Quit(ctx, &emptypb.Empty{}); err != nil { 63 dlog.Errorf(ctx, "error when quitting user daemon: %v", err) 64 } 65 _ = socket.WaitUntilVanishes("user daemon", socket.UserDaemonPath(ctx), 5*time.Second) 66 } 67 // User daemon is responsible for killing the root daemon, but we kill it here too to cater for 68 // the fact that the user daemon might have been killed ungracefully. 69 if waitErr := socket.WaitUntilVanishes("root daemon", socket.RootDaemonPath(ctx), 5*time.Second); waitErr != nil { 70 quitRootDaemon(ctx) 71 } 72 } 73 74 func quitDockerDaemons(ctx context.Context) { 75 infos, err := daemon.LoadInfos(ctx) 76 if err != nil { 77 dlog.Error(ctx, err) 78 return 79 } 80 for _, info := range infos { 81 conn, err := DialDaemon(ctx, info) 82 if err != nil { 83 dlog.Error(ctx, err) 84 continue 85 } 86 _, _ = connector.NewConnectorClient(conn).Quit(ctx, &emptypb.Empty{}) 87 _ = conn.Close() 88 } 89 if err = daemon.WaitUntilAllVanishes(ctx, 5*time.Second); err != nil { 90 dlog.Error(ctx, err) 91 _ = daemon.DeleteAllInfos(ctx) 92 } 93 } 94 95 // DialDaemon dials the daemon appointed by the given info. 96 func DialDaemon(ctx context.Context, info *daemon.Info) (*grpc.ClientConn, error) { 97 var err error 98 var conn *grpc.ClientConn 99 if info.InDocker && !proc.RunningInContainer() { 100 // The host relies on that the daemon has exposed a port to localhost 101 conn, err = docker.ConnectDaemon(ctx, fmt.Sprintf(":%d", info.DaemonPort)) 102 } else { 103 // Try dialing the host daemon using the well known socket. 104 conn, err = socket.Dial(ctx, socket.UserDaemonPath(ctx)) 105 } 106 return conn, err 107 } 108 109 func ExistingDaemon(ctx context.Context, info *daemon.Info) (*daemon.UserClient, error) { 110 conn, err := DialDaemon(ctx, info) 111 if err != nil { 112 return nil, err 113 } 114 return newUserDaemon(conn, info.DaemonID()), nil 115 } 116 117 // Quit shuts down all daemons. 118 func Quit(ctx context.Context) { 119 stdout := output.Out(ctx) 120 ioutil.Print(stdout, "Telepresence Daemons quitting...") 121 for _, quitFunc := range QuitDaemonFuncs { 122 quitFunc(ctx) 123 } 124 ioutil.Println(stdout, "done") 125 } 126 127 // Disconnect disconnects from a session in the user daemon. 128 func Disconnect(ctx context.Context) { 129 if ud := daemon.GetUserClient(ctx); ud == nil { 130 ioutil.Println(output.Out(ctx), "Not connected") 131 } else { 132 _, err := ud.Disconnect(ctx, &emptypb.Empty{}) 133 switch { 134 case err == nil: 135 ioutil.Println(output.Out(ctx), "Disconnected") 136 case status.Code(err) == codes.Unavailable: 137 ioutil.Println(output.Out(ctx), "Not connected") 138 default: 139 ioutil.Printf(output.Err(ctx), "failed to disconnect: %v\n", err) 140 } 141 } 142 } 143 144 func RunConnect(cmd *cobra.Command, args []string) error { 145 if err := InitCommand(cmd); err != nil { 146 return err 147 } 148 if len(args) == 0 { 149 return nil 150 } 151 ctx := cmd.Context() 152 if daemon.GetSession(ctx).Started { 153 defer Disconnect(ctx) 154 } 155 return proc.Run(dos.WithStdio(ctx, cmd), nil, args[0], args[1:]...) 156 } 157 158 // DiscoverDaemon searches the daemon cache for an entry corresponding to the given name. A connection 159 // to that daemon is returned if such an entry is found. 160 func DiscoverDaemon(ctx context.Context, match *regexp.Regexp, daemonID *daemon.Identifier) (*daemon.UserClient, error) { 161 cr := daemon.GetRequest(ctx) 162 if match == nil && !cr.Implicit { 163 match = regexp.MustCompile(`\A` + regexp.QuoteMeta(daemonID.Name) + `\z`) 164 } 165 info, err := daemon.LoadMatchingInfo(ctx, match) 166 if err != nil { 167 if os.IsNotExist(err) && !cr.Docker { 168 // Try dialing the host daemon using the well known socket. 169 if conn, sockErr := socket.Dial(ctx, socket.UserDaemonPath(ctx)); sockErr == nil { 170 return newUserDaemon(conn, daemonID), nil 171 } 172 } 173 return nil, err 174 } 175 if len(cr.ExposedPorts) > 0 && !slices.Equal(info.ExposedPorts, cr.ExposedPorts) { 176 return nil, errcat.User.New("exposed ports differ. Please quit and reconnect") 177 } 178 return ExistingDaemon(ctx, info) 179 } 180 181 func launchConnectorDaemon(ctx context.Context, connectorDaemon string, required bool) (context.Context, *daemon.UserClient, error) { 182 cr := daemon.GetRequest(ctx) 183 cliInContainer := proc.RunningInContainer() 184 daemonID, err := daemon.IdentifierFromFlags(ctx, cr.Name, cr.KubeFlags, cr.KubeconfigData, cr.Docker || cliInContainer) 185 if err != nil { 186 return ctx, nil, err 187 } 188 189 // Try dialing the host daemon using the well known socket. 190 ud, err := DiscoverDaemon(ctx, cr.Use, daemonID) 191 if err == nil { 192 if ud.Containerized() && !cliInContainer { 193 ctx = docker.EnableClient(ctx) 194 cr.Docker = true 195 } 196 if ud.Containerized() == (cr.Docker || cliInContainer) { 197 return ctx, ud, nil 198 } 199 // A daemon running on the host does not fulfill a request for a containerized daemon. They can 200 // coexist though. 201 err = os.ErrNotExist 202 } 203 if !errors.Is(err, os.ErrNotExist) { 204 return ctx, nil, errcat.NoDaemonLogs.New(err) 205 } 206 if !required { 207 return ctx, nil, ErrNoUserDaemon 208 } 209 210 ioutil.Println(output.Info(ctx), "Launching Telepresence User Daemon") 211 if err = ensureAppUserCacheDirs(ctx); err != nil { 212 return ctx, nil, err 213 } 214 if err = ensureAppUserConfigDir(ctx); err != nil { 215 return ctx, nil, err 216 } 217 218 var conn *grpc.ClientConn 219 if cr.Docker && !cliInContainer { 220 // Ensure that the logfile is present before the daemon starts so that it isn't created with 221 // permissions from the docker container. 222 logDir := filelocation.AppUserLogDir(ctx) 223 logFile := filepath.Join(logDir, "connector.log") 224 if _, err := os.Stat(logFile); err != nil { 225 if !os.IsNotExist(err) { 226 return ctx, nil, err 227 } 228 fh, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY, 0o666) 229 if err != nil { 230 return ctx, nil, err 231 } 232 _ = fh.Close() 233 } 234 ctx = docker.EnableClient(ctx) 235 conn, err = docker.LaunchDaemon(ctx, daemonID) 236 } else { 237 args := []string{connectorDaemon, "connector-foreground"} 238 if cr.UserDaemonProfilingPort > 0 { 239 args = append(args, "--pprof", strconv.Itoa(int(cr.UserDaemonProfilingPort))) 240 } 241 if cliInContainer && os.Getuid() == 0 { 242 // No use having multiple daemons when running as root in docker. 243 hn, err := os.Hostname() 244 if err != nil { 245 hn = "unknown" 246 } 247 args = append(args, "--embed-network") 248 args = append(args, "--name", "docker-"+hn) 249 } 250 err = daemon.SaveInfo(ctx, 251 &daemon.Info{ 252 InDocker: cliInContainer, 253 DaemonPort: 0, 254 Name: daemonID.Name, 255 KubeContext: daemonID.KubeContext, 256 Namespace: daemonID.Namespace, 257 ExposedPorts: cr.ExposedPorts, 258 Hostname: cr.Hostname, 259 }, daemonID.InfoFileName()) 260 if err != nil { 261 return ctx, nil, err 262 } 263 defer func() { 264 if err != nil { 265 _ = daemon.DeleteInfo(ctx, daemonID.InfoFileName()) 266 } 267 }() 268 269 if err = proc.StartInBackground(false, args...); err != nil { 270 return ctx, nil, errcat.NoDaemonLogs.Newf("failed to launch the connector service: %w", err) 271 } 272 if err = socket.WaitUntilAppears("connector", socket.UserDaemonPath(ctx), 10*time.Second); err != nil { 273 return ctx, nil, errcat.NoDaemonLogs.Newf("connector service did not start: %w", err) 274 } 275 conn, err = socket.Dial(ctx, socket.UserDaemonPath(ctx)) 276 } 277 if err != nil { 278 return ctx, nil, err 279 } 280 return ctx, newUserDaemon(conn, daemonID), nil 281 } 282 283 func newUserDaemon(conn *grpc.ClientConn, daemonID *daemon.Identifier) *daemon.UserClient { 284 return &daemon.UserClient{ 285 ConnectorClient: connector.NewConnectorClient(conn), 286 Conn: conn, 287 DaemonID: daemonID, 288 } 289 } 290 291 func EnsureUserDaemon(ctx context.Context, required bool) (_ context.Context, err error) { 292 var ud *daemon.UserClient 293 defer func() { 294 if err == nil && required && !ud.Containerized() { 295 // The RootDaemon must be started if the UserDaemon was started 296 err = ensureRootDaemonRunning(ctx) 297 } 298 }() 299 300 if ud = daemon.GetUserClient(ctx); ud != nil { 301 return ctx, nil 302 } 303 if ctx, ud, err = launchConnectorDaemon(ctx, client.GetExe(ctx), required); err != nil { 304 return ctx, err 305 } 306 ctx = daemon.WithUserClient(ctx, ud) 307 return ctx, nil 308 } 309 310 func ensureDaemonVersion(ctx context.Context) error { 311 // Ensure that the already running daemon has the correct version 312 return versionCheck(ctx, client.GetExe(ctx), daemon.GetUserClient(ctx)) 313 } 314 315 func EnsureSession(ctx context.Context, useLine string, required bool) (context.Context, error) { 316 if daemon.GetSession(ctx) != nil { 317 return ctx, nil 318 } 319 s, err := connectSession(ctx, useLine, daemon.GetUserClient(ctx), daemon.GetRequest(ctx), required) 320 if err != nil { 321 return ctx, err 322 } 323 if s == nil { 324 return ctx, nil 325 } 326 327 if dns := s.Info.GetDaemonStatus().GetOutboundConfig().GetDns(); dns != nil && dns.Error != "" { 328 ioutil.Printf(output.Err(ctx), "Warning: %s\n", dns.Error) 329 } 330 return daemon.WithSession(ctx, s), nil 331 } 332 333 func connectSession(ctx context.Context, useLine string, userD *daemon.UserClient, request *daemon.Request, required bool) (*daemon.Session, error) { 334 var ci *connector.ConnectInfo 335 var err error 336 if userD.Containerized() && !proc.RunningInContainer() { 337 patcher.AnnotateConnectRequest(&request.ConnectRequest, docker.TpCache, userD.DaemonID.KubeContext) 338 } 339 session := func(ci *connector.ConnectInfo, started bool) *daemon.Session { 340 // Update the request from the connect info. 341 request.KubeFlags = ci.KubeFlags 342 request.ManagerNamespace = ci.ManagerNamespace 343 request.Name = ci.ConnectionName 344 userD.DaemonID = &daemon.Identifier{ 345 Name: ci.ConnectionName, 346 KubeContext: ci.ClusterContext, 347 Namespace: ci.Namespace, 348 Containerized: userD.Containerized(), 349 } 350 return &daemon.Session{ 351 UserClient: *userD, 352 Info: ci, 353 Started: started, 354 } 355 } 356 357 // warn if version diff between cli and manager is > 3 358 warnMngrVersion := func() error { 359 version, err := userD.TrafficManagerVersion(ctx, &emptypb.Empty{}) 360 if err != nil { 361 return err 362 } 363 364 // remove leading v from semver 365 mSemver, err := semver.Parse(strings.TrimPrefix(version.Version, "v")) 366 if err != nil { 367 return err 368 } 369 370 cliSemver := client.Semver() 371 372 var diff uint64 373 if cliSemver.Minor > mSemver.Minor { 374 diff = cliSemver.Minor - mSemver.Minor 375 } else { 376 diff = mSemver.Minor - cliSemver.Minor 377 } 378 379 maxDiff := uint64(3) 380 if diff > maxDiff { 381 ioutil.Printf(output.Info(ctx), 382 "The Traffic Manager version (%s) is more than %v minor versions diff from client version (%s), please consider upgrading.\n", 383 version.Version, maxDiff, client.Version()) 384 } 385 return nil 386 } 387 388 connectResult := func(ci *connector.ConnectInfo) (*daemon.Session, error) { 389 var msg string 390 cat := errcat.Unknown 391 switch ci.Error { 392 case connector.ConnectInfo_UNSPECIFIED: 393 ioutil.Printf(output.Info(ctx), "Connected to context %s, namespace %s (%s)\n", ci.ClusterContext, ci.Namespace, ci.ClusterServer) 394 err := warnMngrVersion() 395 if err != nil { 396 dlog.Error(ctx, err) 397 } 398 return session(ci, true), nil 399 case connector.ConnectInfo_ALREADY_CONNECTED: 400 return session(ci, false), nil 401 case connector.ConnectInfo_MUST_RESTART: 402 msg = "Cluster configuration changed, please quit telepresence and reconnect" 403 default: 404 msg = ci.ErrorText 405 if ci.ErrorCategory != 0 { 406 cat = errcat.Category(ci.ErrorCategory) 407 } 408 } 409 return nil, &ConnectError{error: cat.Newf("connector.Connect: %s", msg), code: ci.Error} 410 } 411 412 if request.Implicit { 413 // implicit calls use the current Status instead of passing flags and mapped namespaces. 414 if ci, err = userD.Status(ctx, &emptypb.Empty{}); err != nil { 415 return nil, err 416 } 417 if ci.Error != connector.ConnectInfo_DISCONNECTED { 418 return connectResult(ci) 419 } 420 if required { 421 ioutil.Printf(output.Info(ctx), 422 `Warning: You are executing the %q command without a preceding "telepresence connect", causing an implicit `+ 423 "connect to take place. The implicit connect behavior is deprecated and will be removed in a future release.\n", 424 useLine) 425 } 426 } 427 428 if !required { 429 return nil, nil 430 } 431 432 if !userD.Containerized() { 433 daemonID := userD.DaemonID 434 err = daemon.SaveInfo(ctx, 435 &daemon.Info{ 436 InDocker: false, 437 Name: daemonID.Name, 438 KubeContext: daemonID.KubeContext, 439 Namespace: daemonID.Namespace, 440 ExposedPorts: request.ExposedPorts, 441 Hostname: request.Hostname, 442 }, daemonID.InfoFileName()) 443 if err != nil { 444 return nil, errcat.NoDaemonLogs.New(err) 445 } 446 } 447 if ci, err = userD.Connect(ctx, &request.ConnectRequest); err != nil { 448 if !userD.Containerized() { 449 _ = daemon.DeleteInfo(ctx, userD.DaemonID.InfoFileName()) 450 } 451 return nil, err 452 } 453 return connectResult(ci) 454 }