github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/cli/daemon/info.go (about)

     1  package daemon
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io/fs"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/datawire/dlib/dlog"
    15  	"github.com/telepresenceio/telepresence/v2/pkg/client/cache"
    16  	"github.com/telepresenceio/telepresence/v2/pkg/errcat"
    17  	"github.com/telepresenceio/telepresence/v2/pkg/filelocation"
    18  )
    19  
    20  type Info struct {
    21  	Options      map[string]string `json:"options,omitempty"`
    22  	InDocker     bool              `json:"in_docker,omitempty"`
    23  	Name         string            `json:"name,omitempty"`
    24  	KubeContext  string            `json:"kube_context,omitempty"`
    25  	Namespace    string            `json:"namespace,omitempty"`
    26  	DaemonPort   int               `json:"daemon_port,omitempty"`
    27  	ExposedPorts []string          `json:"exposed_ports,omitempty"`
    28  	Hostname     string            `json:"hostname,omitempty"`
    29  }
    30  
    31  func (info *Info) DaemonID() *Identifier {
    32  	id, _ := NewIdentifier(info.Name, info.KubeContext, info.Namespace, info.InDocker)
    33  	return id
    34  }
    35  
    36  const (
    37  	daemonsDirName    = "daemons"
    38  	keepAliveInterval = 5 * time.Second
    39  )
    40  
    41  func LoadInfo(ctx context.Context, file string) (*Info, error) {
    42  	var di Info
    43  	if err := cache.LoadFromUserCache(ctx, &di, filepath.Join(daemonsDirName, file)); err != nil {
    44  		return nil, err
    45  	}
    46  	return &di, nil
    47  }
    48  
    49  func SaveInfo(ctx context.Context, object *Info, file string) error {
    50  	return cache.SaveToUserCache(ctx, object, filepath.Join(daemonsDirName, file), cache.Public)
    51  }
    52  
    53  func DeleteInfo(ctx context.Context, file string) error {
    54  	return cache.DeleteFromUserCache(ctx, filepath.Join(daemonsDirName, file))
    55  }
    56  
    57  func InfoExists(ctx context.Context, file string) (bool, error) {
    58  	return cache.ExistsInCache(ctx, filepath.Join(daemonsDirName, file))
    59  }
    60  
    61  func WatchInfos(ctx context.Context, onChange func(context.Context) error, files ...string) error {
    62  	return cache.WatchUserCache(ctx, daemonsDirName, onChange, files...)
    63  }
    64  
    65  func WaitUntilAllVanishes(ctx context.Context, ttw time.Duration) error {
    66  	giveUp := time.Now().Add(ttw)
    67  	for giveUp.After(time.Now()) {
    68  		files, err := infoFiles(ctx)
    69  		if err != nil || len(files) == 0 {
    70  			return err
    71  		}
    72  		time.Sleep(250 * time.Millisecond)
    73  	}
    74  	return errors.New("timeout while waiting for daemon files to vanish")
    75  }
    76  
    77  func DeleteAllInfos(ctx context.Context) error {
    78  	files, err := infoFiles(ctx)
    79  	if err != nil {
    80  		return err
    81  	}
    82  	for _, file := range files {
    83  		_ = cache.DeleteFromUserCache(ctx, filepath.Join(daemonsDirName, file.Name()))
    84  	}
    85  	return nil
    86  }
    87  
    88  func LoadInfos(ctx context.Context) ([]*Info, error) {
    89  	files, err := infoFiles(ctx)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	DaemonInfos := make([]*Info, len(files))
    95  	for i, file := range files {
    96  		if err = cache.LoadFromUserCache(ctx, &DaemonInfos[i], filepath.Join(daemonsDirName, file.Name())); err != nil {
    97  			return nil, err
    98  		}
    99  	}
   100  	return DaemonInfos, nil
   101  }
   102  
   103  func infoFiles(ctx context.Context) ([]fs.DirEntry, error) {
   104  	files, err := os.ReadDir(filepath.Join(filelocation.AppUserCacheDir(ctx), daemonsDirName))
   105  	if err != nil {
   106  		if os.IsNotExist(err) {
   107  			err = nil
   108  		}
   109  		return nil, err
   110  	}
   111  	active := make([]fs.DirEntry, 0, len(files))
   112  	for _, file := range files {
   113  		fi, err := file.Info()
   114  		if err != nil {
   115  			return nil, err
   116  		}
   117  		age := time.Since(fi.ModTime())
   118  		if age > keepAliveInterval+600*time.Millisecond {
   119  			// File has gone stale
   120  			dlog.Debugf(ctx, "Deleting stale info %s with age = %s", file.Name(), age)
   121  			if err = cache.DeleteFromUserCache(ctx, filepath.Join(daemonsDirName, file.Name())); err != nil {
   122  				return nil, err
   123  			}
   124  		} else {
   125  			active = append(active, file)
   126  		}
   127  	}
   128  	return active, err
   129  }
   130  
   131  type InfoMatchError string
   132  
   133  func (i InfoMatchError) Error() string {
   134  	return string(i)
   135  }
   136  
   137  type MultipleDaemonsError []*Info //nolint:errname // Don't want a plural name just because the type is a slice
   138  
   139  func (m MultipleDaemonsError) Error() string {
   140  	sb := strings.Builder{}
   141  	sb.WriteString("multiple daemons are running, please select ")
   142  	l := len(m)
   143  	i := 0
   144  	if l > 2 {
   145  		sb.WriteString("one of ")
   146  		for ; i+2 < l; i++ {
   147  			sb.WriteString(m[i].DaemonID().Name)
   148  			sb.WriteString(", ")
   149  		}
   150  	} else {
   151  		sb.WriteString(m[i].DaemonID().Name)
   152  		i++
   153  	}
   154  	sb.WriteString(" or ")
   155  	sb.WriteString(m[i].DaemonID().Name)
   156  	sb.WriteString(" using the --use <match> flag")
   157  	return sb.String()
   158  }
   159  
   160  func LoadMatchingInfo(ctx context.Context, match *regexp.Regexp) (*Info, error) {
   161  	if match == nil {
   162  		infos, err := LoadInfos(ctx)
   163  		if err != nil {
   164  			return nil, err
   165  		}
   166  		switch len(infos) {
   167  		case 0:
   168  			return nil, os.ErrNotExist
   169  		case 1:
   170  			return infos[0], err
   171  		default:
   172  			return nil, MultipleDaemonsError(infos)
   173  		}
   174  	}
   175  	files, err := infoFiles(ctx)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	var found string
   180  	for _, file := range files {
   181  		name := file.Name()
   182  		if !strings.HasSuffix(name, ".json") {
   183  			continue
   184  		}
   185  		// If a match is given, then strip ".json" and apply it.
   186  		if match.MatchString(name[:len(name)-5]) {
   187  			if found != "" {
   188  				return nil, errcat.User.New(
   189  					InfoMatchError(fmt.Sprintf("the expression %q does not uniquely identify a running daemon", match.String())))
   190  			}
   191  			found = name
   192  		}
   193  	}
   194  	if found == "" {
   195  		return nil, os.ErrNotExist
   196  	}
   197  	return LoadInfo(ctx, found)
   198  }
   199  
   200  // CancelWhenRmFromCache watches for the file to be removed from the cache, then calls cancel.
   201  func CancelWhenRmFromCache(ctx context.Context, cancel context.CancelFunc, filename string) error {
   202  	return WatchInfos(ctx, func(ctx context.Context) error {
   203  		exists, err := InfoExists(ctx, filename)
   204  		if err != nil {
   205  			return err
   206  		}
   207  		if !exists {
   208  			// spec removed from cache, shut down gracefully
   209  			dlog.Infof(ctx, "daemon file %s removed from cache, shutting down gracefully", filename)
   210  			cancel()
   211  		}
   212  		return nil
   213  	}, filename)
   214  }
   215  
   216  // KeepInfoAlive updates the access and modification times of the given Info
   217  // periodically so that it never gets older than keepAliveInterval. This means that
   218  // any file with a modification time older than the current time minus two keepAliveIntervals
   219  // can be considered stale and should be removed.
   220  //
   221  // The alive poll ends and the Info is deleted when the context is cancelled.
   222  func KeepInfoAlive(ctx context.Context, file string) error {
   223  	daemonFile := filepath.Join(filelocation.AppUserCacheDir(ctx), daemonsDirName, file)
   224  	ticker := time.NewTicker(keepAliveInterval)
   225  	defer ticker.Stop()
   226  	now := time.Now()
   227  	for {
   228  		if err := os.Chtimes(daemonFile, now, now); err != nil {
   229  			if os.IsNotExist(err) {
   230  				// File is removed, so stop trying to update its timestamps
   231  				dlog.Debugf(ctx, "Daemon info %s does not exist", file)
   232  				return nil
   233  			}
   234  			return fmt.Errorf("failed to update timestamp on %s: %w", daemonFile, err)
   235  		}
   236  		select {
   237  		case <-ctx.Done():
   238  			dlog.Debugf(ctx, "Deleting daemon info %s because context was cancelled", file)
   239  			_ = DeleteInfo(ctx, file)
   240  			return nil
   241  		case now = <-ticker.C:
   242  		}
   243  	}
   244  }