...

Source file src/edge-infra.dev/pkg/sds/emergencyaccess/emulator/emulator.go

Documentation: edge-infra.dev/pkg/sds/emergencyaccess/emulator

     1  package emulator
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"path"
    10  	"path/filepath"
    11  	"time"
    12  
    13  	"github.com/go-logr/logr"
    14  
    15  	"edge-infra.dev/pkg/lib/fog"
    16  	"edge-infra.dev/pkg/sds/emergencyaccess/msgdata"
    17  	"edge-infra.dev/pkg/sds/lib/colors"
    18  )
    19  
    20  // This file holds the public methods (initialiser and run) for the emulator as well as fields
    21  // common to both the session and connect prompts
    22  // The emulator is responsible for:
    23  //   - handling user input at the command line
    24  //   - displaying output from connected sessions and errors in a user friendly manner
    25  //   - storing history and workspace for ease of use
    26  // The connect and session prompts are functional elements that are built using go-prompt and
    27  // gleisonmv/colors
    28  
    29  const (
    30  	historyFileLimit      = 5000
    31  	sessionTimeoutDefault = "300s"
    32  
    33  	connectHistoryFileName = "rcli_connect_history"
    34  	sessionHistoryFileName = "rcli_session_history"
    35  	shellHistoryFileName   = "rcli_shell_history"
    36  	workspaceFileName      = "rcli_workspace.json"
    37  
    38  	UNSET = "UNSET"
    39  )
    40  
    41  var (
    42  	ErrorRCLINoSubcmd      = errors.New("no subcommands")
    43  	ErrorRCLIUnknownSubcmd = errors.New("unrecognised subcommand")
    44  
    45  	// Array of all shared exit commands used in both connect and session prompt
    46  	exitCommands = []string{"q", "end", "exit"}
    47  )
    48  
    49  type CLIService interface {
    50  	Connect(ctx context.Context, projectID string, bannerID string, storeID string, terminalID string) error
    51  	Send(command string) (commandID string, err error)
    52  	SetTopicTemplate(topicTemplate string)
    53  	SetSubscriptionTemplate(subscriptionTemplate string)
    54  	RetrieveIdentity(context.Context) error
    55  	GetDisplayChannel() <-chan msgdata.CommandResponse
    56  	GetSessionContext() context.Context
    57  	UserID() string
    58  	IdleTime() time.Duration
    59  	EnablePerSessionSubscription()
    60  	DisablePerSessionSubscription()
    61  	End() error
    62  	// Env should return a list of environment variables, in the form name=value,
    63  	// that can be used by the CLIService to reconnect to the same environment.
    64  	// This is useful in local shell mode
    65  	Env() []string
    66  }
    67  
    68  type DarkModeService interface {
    69  	SetDarkmode(bool)
    70  	Darkmode() bool
    71  }
    72  
    73  // go-prompt will break the terminal (no characters printed) if this isnt defered on exit
    74  func handleExit() {
    75  	rawModeOff := exec.Command("/bin/stty", "-raw", "echo")
    76  	rawModeOff.Stdin = os.Stdin
    77  	_ = rawModeOff.Run()
    78  }
    79  
    80  func isPhase1() bool {
    81  	// There's probably a much better way of doing all of this
    82  	return path.Base(os.Args[0]) == "remotecliv1"
    83  }
    84  
    85  // Emulator struct to hold methods and private variables.
    86  // Only public methods should be the initialiser and Run as it is a ui driven library.
    87  type Emulator struct {
    88  	cls    CLIService
    89  	runCtx context.Context //passed from run to connect executor
    90  	log    logr.Logger
    91  
    92  	// Path to the directory which contains command history and workspace files.
    93  	// Current working directory by default.
    94  	config              Config
    95  	sessionHistory      commandHistory
    96  	connectHistory      commandHistory
    97  	shellHistory        commandHistory
    98  	workspace           *workspace
    99  	sessionPromptPaused bool
   100  
   101  	commandOptions map[string]CommandOpts
   102  
   103  	// Used to store the connection data for local shell mode
   104  	connectData connectionData
   105  }
   106  
   107  // Takes the parent context from main and initialises cliservice.
   108  func NewEmulator(ctx context.Context, cls CLIService, config Config) Emulator {
   109  	return Emulator{
   110  		cls:            cls,
   111  		log:            fog.FromContext(ctx).WithName("emulator"),
   112  		config:         config,
   113  		commandOptions: map[string]CommandOpts{},
   114  	}
   115  }
   116  
   117  func (em *Emulator) sessionPath() string   { return filepath.Join(em.config.dir, sessionHistoryFileName) }
   118  func (em *Emulator) connectPath() string   { return filepath.Join(em.config.dir, connectHistoryFileName) }
   119  func (em *Emulator) shellHistPath() string { return filepath.Join(em.config.dir, shellHistoryFileName) }
   120  func (em *Emulator) workspacePath() string { return filepath.Join(em.config.dir, workspaceFileName) }
   121  
   122  // Run starts the prompts and is blocking. Note that if the session prompt is active
   123  // the display channel will be buffered to and not printed.
   124  func (em *Emulator) Run(ctx context.Context) {
   125  	defer handleExit()
   126  	em.runCtx = ctx
   127  
   128  	// Get user details
   129  	err := em.cls.RetrieveIdentity(ctx)
   130  	if err != nil {
   131  		em.log.Error(err, "Failed to retrieve valid BSL token")
   132  		fmt.Println(colors.Text("Error authenticating user. See logs for more detail.", colors.BgRed))
   133  		return
   134  	}
   135  
   136  	// Load workspace and command history files
   137  	em.loadWorkspace(em.connectPath(), em.sessionPath(), em.workspacePath())
   138  
   139  	em.setupCompleters()
   140  
   141  	if em.workspace.DisablePerSessionSubscription {
   142  		em.cls.DisablePerSessionSubscription()
   143  	}
   144  
   145  	em.displayConnectBanner()
   146  	em.connectPrompt(em.connectHistory.history).Run()
   147  
   148  	err = em.workspace.save(em.workspacePath())
   149  	if err != nil {
   150  		em.log.Error(err, "Error saving workspace")
   151  	}
   152  }
   153  
   154  func (em *Emulator) loadWorkspace(connectFile, sessionFile, workspaceFile string) {
   155  	var err error
   156  	em.connectHistory, err = newCommandHistory(connectFile)
   157  	if err != nil {
   158  		em.log.Error(err, "Error establishing remote cli history")
   159  	}
   160  
   161  	em.sessionHistory, err = newCommandHistory(sessionFile)
   162  	if err != nil {
   163  		em.log.Error(err, "Error establishing session prompt history")
   164  	}
   165  
   166  	em.shellHistory, err = newCommandHistory(em.shellHistPath())
   167  	if err != nil {
   168  		em.log.Error(err, "Error establishing shell prompt history")
   169  	}
   170  
   171  	em.workspace, err = newWorkspace(workspaceFile)
   172  	if err != nil {
   173  		em.log.Error(err, "Error establishing workspace")
   174  	}
   175  }
   176  
   177  func getDarkmode(cls CLIService) bool {
   178  	if dm, ok := cls.(DarkModeService); ok {
   179  		return dm.Darkmode()
   180  	}
   181  	return false
   182  }
   183  
   184  // SetupCompleters is a method which sets up the appropriate completers for a
   185  // given phase binary
   186  func (em *Emulator) setupCompleters() {
   187  	if isPhase1() {
   188  		rcliconfigCompleter = append(rcliconfigCompleter, rcliConfigCompleterPhase1...)
   189  	} else {
   190  		topLevelConnectCommands = append(topLevelConnectCommands, connectCommandsCompleterPhase2...)
   191  	}
   192  }
   193  

View as plain text