...

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

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

     1  package emulator
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/c-bata/go-prompt"
     9  	"github.com/google/shlex"
    10  
    11  	"edge-infra.dev/pkg/sds/lib/colors"
    12  )
    13  
    14  // This file contains all private fields and methods for the connect prompt.
    15  // The connect prompt is responsible for:
    16  //  - handling and passing connection parameters
    17  //  - launching the session prompt on a successful connection
    18  //  - reporting on errors from the connection
    19  // Connect generally uses the blue color scheme for its display methods i.e colors.FgBlue, colors.BgBlue,
    20  // colors.FgBlueBold and red for error.
    21  
    22  // used for returning parsed input from parseconnectinput
    23  type connectionData struct {
    24  	projectID  string
    25  	bannerID   string
    26  	storeID    string
    27  	terminalID string
    28  }
    29  
    30  var topLevelConnectCommands = []prompt.Suggest{
    31  	{Text: "connect", Description: "Connect to a new session. See help for further info."},
    32  	{Text: "rcliconfig", Description: "Sets rcliconfig settings."},
    33  	{Text: "help", Description: "Display help"},
    34  }
    35  
    36  var connectCommandsCompleterPhase2 = []prompt.Suggest{
    37  	{Text: "shell", Description: "Enter local shell mode to interact with remotecli exec output"},
    38  }
    39  
    40  var rcliconfigCompleter = []prompt.Suggest{
    41  	{Text: "bannerID", Description: "Set default banner ID for workspace."},
    42  	{Text: "storeID", Description: "Set default store ID for workspace."},
    43  }
    44  
    45  var rcliConfigCompleterPhase1 = []prompt.Suggest{
    46  	{Text: "projectID", Description: "Set default project ID for workspace."},
    47  	{Text: "enablePerSessionSubscription", Description: "Enables Creating a new Subscription per Session"},
    48  	{Text: "disablePerSessionSubscription", Description: "Disables Creating a new Subscription per Session"},
    49  }
    50  
    51  func connectPromptCompleter(in prompt.Document) []prompt.Suggest {
    52  	commands, err := shlex.Split(in.TextBeforeCursor())
    53  	if err != nil {
    54  		// This will occur when there is an unclosed quote on the prompt
    55  		return []prompt.Suggest{}
    56  	}
    57  
    58  	// Total number of words in the list on prompt
    59  	lenCommands := len(commands)
    60  	// Index of word currently being typed in the list, shlex.Split does not
    61  	// append empty string to list when space before cursor, so must add 1 in
    62  	// this case
    63  	wordidx := lenCommands - 1
    64  	if in.GetWordBeforeCursor() == "" {
    65  		wordidx = lenCommands
    66  	}
    67  
    68  	// Top level command to be run
    69  	command := ""
    70  	if lenCommands > 0 {
    71  		command = commands[0]
    72  	}
    73  
    74  	if wordidx < 1 {
    75  		return prompt.FilterHasPrefix(topLevelConnectCommands, command, true)
    76  	}
    77  
    78  	if command == "rcliconfig" && wordidx < 2 {
    79  		command2 := ""
    80  		if len(commands) > 1 {
    81  			command2 = commands[1]
    82  		}
    83  		return prompt.FilterHasPrefix(rcliconfigCompleter, command2, true)
    84  	}
    85  
    86  	if command != "connect" {
    87  		return []prompt.Suggest{}
    88  	}
    89  
    90  	if wordidx > 4 {
    91  		return []prompt.Suggest{}
    92  	}
    93  
    94  	return []prompt.Suggest{}
    95  }
    96  
    97  // connect prompt generator
    98  func (em Emulator) connectPrompt(optionHistory []string) *prompt.Prompt {
    99  	return prompt.New(em.connectPromptExecutor, connectPromptCompleter,
   100  		prompt.OptionHistory(optionHistory),
   101  		prompt.OptionSetExitCheckerOnInput(exitChecker),
   102  		prompt.OptionPrefix("\r> "), prompt.OptionPrefixTextColor(prompt.Yellow),
   103  		prompt.OptionPreviewSuggestionTextColor(prompt.Blue),
   104  		prompt.OptionSelectedSuggestionBGColor(prompt.LightGray),
   105  		prompt.OptionSuggestionBGColor(prompt.DarkGray),
   106  		prompt.OptionAddKeyBind(commonKeyBindings...),
   107  	)
   108  }
   109  
   110  // handles input for the connection prompt.
   111  func (em *Emulator) connectPromptExecutor(in string) {
   112  	if in == "" {
   113  		return
   114  	}
   115  	if in == "help" {
   116  		fmt.Println(connectHelp)
   117  		return
   118  	}
   119  	for _, item := range exitCommands {
   120  		if in == item {
   121  			em.connectHistory.close()
   122  			em.sessionHistory.close()
   123  			em.shellHistory.close()
   124  			return
   125  		}
   126  	}
   127  
   128  	err := em.connectHistory.updateHistory(in, historyFileLimit)
   129  	if err != nil {
   130  		em.log.Error(err, "Error updating connect history file")
   131  	}
   132  	if strings.HasPrefix(in, "dark ") {
   133  		// remove "dark " from the input string, enable darkmode option in `cliServiceItfc`, and allow standard `connect` handler to process the remaining command string
   134  		in = in[5:]
   135  		// enable darkmode
   136  		dm, ok := em.cls.(DarkModeService)
   137  		if !ok {
   138  			fmt.Println(colors.Text("Error Dark mode not supported.", colors.BgRed))
   139  			return
   140  		}
   141  		fmt.Println(colors.BufferedText("darkmode enabled", colors.BgBrightBlack))
   142  		dm.SetDarkmode(true)
   143  		// disable so the next call is not set to dark
   144  		defer dm.SetDarkmode(false)
   145  	}
   146  	if strings.HasPrefix(in, "connect") {
   147  		connectData, err := em.parseConnectInput(in)
   148  		if err != nil {
   149  			fmt.Println(colors.Text("error whilst parsing connection query: %q. Type help to see more info.", colors.FgRed, err))
   150  			return
   151  		}
   152  		err = em.cls.Connect(em.runCtx, connectData.projectID, connectData.bannerID, connectData.storeID, connectData.terminalID)
   153  		if err != nil {
   154  			fmt.Println(colors.Text("Error whilst setting up connection:", colors.BgRed))
   155  			em.displayError(em.log, err)
   156  			return
   157  		}
   158  		em.startSession()
   159  		fmt.Println(colors.Text("Exited session", colors.FgGreenBold))
   160  		// display the connnect prompt banner
   161  		em.displayConnectBanner()
   162  		return
   163  	}
   164  
   165  	if strings.HasPrefix(in, "shell") {
   166  		connectData, err := em.parseConnectInput(in)
   167  		if err != nil {
   168  			fmt.Println(colors.Text("error whilst parsing connection query: %q. Type help to see more info.", colors.FgRed, err))
   169  			return
   170  		}
   171  
   172  		err = em.startShell(connectData)
   173  		if err != nil {
   174  			fmt.Println(colors.Text("Error during local shell mode initialisation:", colors.BgRed))
   175  			em.displayError(em.log, err)
   176  			return
   177  		}
   178  
   179  		fmt.Println(colors.Text("Exited shell mode", colors.FgGreenBold))
   180  		em.displayConnectBanner()
   181  		return
   182  	}
   183  
   184  	if strings.HasPrefix(in, "rcliconfig") {
   185  		err := em.rcliconfig(in)
   186  		if err != nil {
   187  			em.log.Error(err, "Error processing rcliconfig command")
   188  		}
   189  		return
   190  	}
   191  
   192  	fmt.Println(colors.Text("Unknown command: %s", colors.FgRed, in))
   193  }
   194  
   195  // parses connect command and replaces missing fields with data from em.workspace if
   196  // these exist
   197  func (em *Emulator) parseConnectInput(str string) (connectionData, error) {
   198  	vals, err := shlex.Split(str)
   199  	if err != nil {
   200  		return connectionData{}, err
   201  	}
   202  	flags := []string{"bannerID", "storeID", "terminalID"}
   203  	switch len(vals) {
   204  	case 2:
   205  		projectID := getProjectID(em.workspace.ProjectID)
   206  		fmt.Println(colors.Text("Using bannerID, and storeID from workspace", colors.FgBlue))
   207  		for idx, item := range []string{em.workspace.BannerID, em.workspace.StoreID} {
   208  			if item == "" || item == UNSET {
   209  				return connectionData{}, fmt.Errorf("%s in workspace was not set", flags[idx])
   210  			}
   211  		}
   212  		return connectionData{
   213  			projectID:  projectID,
   214  			bannerID:   em.workspace.BannerID,
   215  			storeID:    em.workspace.StoreID,
   216  			terminalID: vals[1]}, nil
   217  	case 3:
   218  		projectID := getProjectID(em.workspace.ProjectID)
   219  		fmt.Println(colors.Text("Using bannerID from workspace", colors.FgBlue))
   220  		for idx, item := range []string{em.workspace.BannerID} {
   221  			if item == "" || item == UNSET {
   222  				return connectionData{}, fmt.Errorf("%s in workspace was not set", flags[idx])
   223  			}
   224  		}
   225  		return connectionData{
   226  			projectID:  projectID,
   227  			bannerID:   em.workspace.BannerID,
   228  			storeID:    vals[1],
   229  			terminalID: vals[2],
   230  		}, nil
   231  	case 4:
   232  		projectID := getProjectID(em.workspace.ProjectID)
   233  
   234  		return connectionData{
   235  			projectID:  projectID,
   236  			bannerID:   vals[1],
   237  			storeID:    vals[2],
   238  			terminalID: vals[3],
   239  		}, nil
   240  	case 5:
   241  		return connectionData{
   242  			projectID:  vals[1],
   243  			bannerID:   vals[2],
   244  			storeID:    vals[3],
   245  			terminalID: vals[4],
   246  		}, nil
   247  	}
   248  	return connectionData{}, fmt.Errorf("wrong number of values given")
   249  }
   250  
   251  func getProjectID(projectID string) string {
   252  	if projectID != "" && projectID != UNSET {
   253  		fmt.Println(colors.Text("Using projectID from workspace", colors.FgBlue))
   254  	}
   255  	if projectID == UNSET {
   256  		// cliServiceItfc Connect expects an empty string for projectID if it
   257  		// is not set rather than the const UNSET
   258  		projectID = ""
   259  	}
   260  	return projectID
   261  }
   262  
   263  func exitChecker(in string, breakline bool) bool {
   264  	if breakline {
   265  		for _, item := range exitCommands {
   266  			if in == item {
   267  				return true
   268  			}
   269  		}
   270  	}
   271  
   272  	return false
   273  }
   274  
   275  // Function deals with all configuration options available via the rcliconfig
   276  // command at the command prompt
   277  func (em *Emulator) rcliconfig(in string) error {
   278  	// TODO pass in io.writer and test?
   279  	values, err := shlex.Split(in)
   280  	if err != nil {
   281  		fmt.Println(colors.Text("Error processing command, see log for further details", colors.FgRed))
   282  		return err
   283  	}
   284  
   285  	if len(values) < 2 {
   286  		fmt.Println(colors.Text("Please provide a subcommand", colors.FgRed))
   287  		return ErrorRCLINoSubcmd
   288  	}
   289  	return em.rcliConfigSwitch(values)
   290  }
   291  
   292  func (em *Emulator) rcliConfigSwitch(values []string) error {
   293  	switch values[1] {
   294  	case "topicTemplate":
   295  		err := checkrcliConfValues(values)
   296  		if err != nil {
   297  			return err
   298  		}
   299  		em.cls.SetTopicTemplate(values[2])
   300  	case "subscriptionTemplate":
   301  		err := checkrcliConfValues(values)
   302  		if err != nil {
   303  			return err
   304  		}
   305  		em.cls.SetSubscriptionTemplate(values[2])
   306  	case "projectID":
   307  		err := checkrcliConfValues(values)
   308  		if err != nil {
   309  			return err
   310  		}
   311  		em.workspace.ProjectID = values[2]
   312  		em.displayWorkspace()
   313  	case "bannerID":
   314  		err := checkrcliConfValues(values)
   315  		if err != nil {
   316  			return err
   317  		}
   318  		em.workspace.BannerID = values[2]
   319  		em.displayWorkspace()
   320  	case "storeID":
   321  		err := checkrcliConfValues(values)
   322  		if err != nil {
   323  			return err
   324  		}
   325  		em.workspace.StoreID = values[2]
   326  		em.displayWorkspace()
   327  	case "enablePerSessionSubscription":
   328  		em.workspace.DisablePerSessionSubscription = false
   329  		em.cls.EnablePerSessionSubscription()
   330  	case "disablePerSessionSubscription":
   331  		em.workspace.DisablePerSessionSubscription = true
   332  		em.cls.DisablePerSessionSubscription()
   333  	default:
   334  		fmt.Println(colors.Text("Unknown subcommand: %s", colors.FgRed, values[1]))
   335  		return ErrorRCLIUnknownSubcmd
   336  	}
   337  	return nil
   338  }
   339  
   340  func checkrcliConfValues(values []string) error {
   341  	if len(values) != 3 {
   342  		fmt.Println(colors.Text("Unexpected number of arguments for %s subcommand", colors.FgRed, values[1]))
   343  		return errors.New("Unexpected number of arguments for subcommand " + values[1])
   344  	}
   345  	fmt.Println(colors.Text("Setting "+values[1]+" to "+values[2], colors.FgBlue))
   346  	return nil
   347  }
   348  
   349  func (em Emulator) displayConnectBanner() {
   350  	fmt.Println(colors.Text("Remote CLI: 'Ctrl-D', 'end', 'exit' or 'q' to exit. ", colors.BgBlue))
   351  	em.displayUser()
   352  	em.displayWorkspace()
   353  }
   354  
   355  // prints workspace
   356  func (em Emulator) displayWorkspace() {
   357  	fmt.Print(colors.Text("Workspace: ", colors.FgBlueBold))
   358  	fmt.Print(colors.Text("%s", colors.FgBlue, em.workspace.string()))
   359  }
   360  
   361  func (em Emulator) displayUser() {
   362  	fmt.Print(colors.Text("User: ", colors.FgBlueBold))
   363  	fmt.Print(colors.Text("%s\n", colors.FgBlue, em.cls.UserID()))
   364  }
   365  

View as plain text