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  }