...

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

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

     1  package cliservice
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os/exec"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/google/uuid"
    12  
    13  	"edge-infra.dev/pkg/lib/fog"
    14  	"edge-infra.dev/pkg/sds/emergencyaccess/msgdata"
    15  	"edge-infra.dev/pkg/sds/emergencyaccess/remotecli"
    16  )
    17  
    18  const (
    19  	responseTopic          = "topic.dsds-ea-response"
    20  	perSessionSubscription = "sub.session.%s.dsds-ea-response"
    21  )
    22  
    23  // type uerr is a simple type that implements both error and UserError, returning
    24  // the same string for both interfaces
    25  type uerr string
    26  
    27  func (err uerr) Error() string       { return string(err) }
    28  func (err uerr) UserError() []string { return []string{string(err)} }
    29  
    30  // type uuerr is a type which implements error and UserError, it allows setting
    31  // different messages for both interfaces
    32  type uuerr struct {
    33  	error
    34  	user string
    35  }
    36  
    37  func (err uuerr) UserError() []string { return []string{err.user} }
    38  
    39  // This file defines the public APIs to connect, send to and end
    40  // a session.
    41  type remoteCLI interface {
    42  	Send(ctx context.Context, userID string, sessionID string, commandID string, request msgdata.Request, opts ...remotecli.RCLIOption) error
    43  	StartSession(ctx context.Context, sessionID string, displayChan chan<- msgdata.CommandResponse, target remotecli.Target, opts ...remotecli.RCLIOption) error
    44  	EndSession(ctx context.Context, sessionID string) error
    45  }
    46  
    47  type subscriptionCreator interface {
    48  	CreateSubscription(ctx context.Context, sessionID string, subscriptionID string, projectID string, topicID string) error
    49  	DeleteSubscription(ctx context.Context, subscriptionID string, projectID string) error
    50  }
    51  
    52  type target struct {
    53  	projectID  string
    54  	bannerID   string
    55  	storeID    string
    56  	terminalID string
    57  }
    58  
    59  func (t target) ProjectID() string  { return t.projectID }
    60  func (t target) BannerID() string   { return t.bannerID }
    61  func (t target) StoreID() string    { return t.storeID }
    62  func (t target) TerminalID() string { return t.terminalID }
    63  
    64  type CLIService struct {
    65  	dispChan   chan msgdata.CommandResponse
    66  	rcli       remoteCLI
    67  	ms         remotecli.MsgSvc
    68  	seshCtx    context.Context
    69  	seshCancel context.CancelFunc
    70  	target     remotecli.Target
    71  	sessionID  string
    72  	userID     string
    73  	idleTime   time.Time
    74  
    75  	// remotecli optionaltemplates
    76  	topicTemplate        string
    77  	subscriptionTemplate string
    78  
    79  	perSessionSubscription bool
    80  }
    81  
    82  // initilaises a cls with an rcli
    83  // takes a parent context (from main idealy)
    84  func NewCLIService(ctx context.Context, ms remotecli.MsgSvc) CLIService {
    85  	ctx = fog.IntoContext(ctx, fog.FromContext(ctx).WithName("remotecli"))
    86  	rcli := remotecli.New(ctx, ms)
    87  
    88  	return CLIService{
    89  		rcli: rcli,
    90  		ms:   ms,
    91  		// Default to creating a new subscription per session
    92  		perSessionSubscription: true,
    93  	}
    94  }
    95  
    96  // Connects to the ms. takes a context from main to generate the session context
    97  func (cls *CLIService) Connect(ctx context.Context, projectID string, bannerID string, storeID string, terminalID string) error {
    98  	cls.seshCtx, cls.seshCancel = context.WithCancel(ctx)
    99  	if projectID == "" {
   100  		return uerr("Project ID is a required field")
   101  	}
   102  	cls.target = target{projectID: projectID, bannerID: bannerID, storeID: storeID, terminalID: terminalID}
   103  
   104  	opts := []remotecli.RCLIOption{}
   105  
   106  	cls.sessionID = uuid.NewString()
   107  	cls.dispChan = make(chan msgdata.CommandResponse, 10)
   108  
   109  	os, err := cls.createSubscription(cls.seshCtx, projectID)
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  	opts = append(opts, os...)
   115  
   116  	if cls.subscriptionTemplate != "" {
   117  		opts = append(opts, remotecli.WithOptionalTemplate(cls.subscriptionTemplate))
   118  	}
   119  
   120  	err = cls.rcli.StartSession(ctx, cls.sessionID, cls.dispChan, cls.target, opts...)
   121  	if err != nil {
   122  		close(cls.dispChan)
   123  	}
   124  	cls.idleTime = time.Now()
   125  	return err
   126  }
   127  
   128  // Creates a new subscription in the given projectID with the details stored in cls.
   129  // If sussessful returns an remotecli.RCLIOption with the template set to a valid
   130  // value for the created subscription.
   131  // Does not create a subscription if [cls.DisablePerSessionSubscription] has been
   132  // called.
   133  func (cls *CLIService) createSubscription(ctx context.Context, projectID string) ([]remotecli.RCLIOption, error) {
   134  	if !cls.perSessionSubscription {
   135  		return nil, nil
   136  	}
   137  
   138  	subscriptionID := fmt.Sprintf(perSessionSubscription, cls.sessionID)
   139  
   140  	if m, ok := cls.ms.(subscriptionCreator); ok { // TODO: what if not ok?
   141  		err := m.CreateSubscription(ctx, cls.sessionID, subscriptionID, projectID, responseTopic)
   142  		if err != nil {
   143  			err := uuerr{
   144  				error: fmt.Errorf("error creating subscription: %w", err),
   145  				user:  "Error creating subscription. Consider running `rcliconfig disablePerSessionSubscription` or see https://docs.edge-infra.dev/edge/sds/remoteaccess-tools/emergency-access/#multiple-users-connected-to-the-same-store-or-terminals",
   146  			}
   147  			return nil, err
   148  		}
   149  	}
   150  
   151  	return []remotecli.RCLIOption{
   152  		remotecli.WithOptionalTemplate(subscriptionID),
   153  	}, nil
   154  }
   155  
   156  func (cls *CLIService) Send(command string) (string, error) {
   157  	if cls.userID == "" {
   158  		return "", fmt.Errorf("no user id provided")
   159  	}
   160  
   161  	commandID := uuid.NewString()
   162  
   163  	log := fog.FromContext(
   164  		cls.seshCtx,
   165  		"sessionID", cls.sessionID,
   166  		"commandID", commandID,
   167  	)
   168  	ctx := fog.IntoContext(cls.seshCtx, log)
   169  
   170  	request, err := msgdata.NewV1_0Request(command)
   171  	if err != nil {
   172  		return "", fmt.Errorf("error creating command: %w", err)
   173  	}
   174  
   175  	opts := []remotecli.RCLIOption{}
   176  	if cls.topicTemplate != "" {
   177  		opts = append(opts, remotecli.WithOptionalTemplate(cls.topicTemplate))
   178  	}
   179  	cls.idleTime = time.Now()
   180  	return commandID, cls.rcli.Send(ctx, cls.userID, cls.sessionID, commandID, request, opts...)
   181  }
   182  
   183  func (cls *CLIService) End() error {
   184  	deleteErr := cls.deleteSubscription(cls.seshCtx)
   185  	// always end the session regardless of the delete subscription error
   186  	cls.seshCancel()
   187  	endErr := cls.rcli.EndSession(cls.seshCtx, cls.sessionID)
   188  	return errors.Join(deleteErr, endErr)
   189  }
   190  
   191  // Deletes the per session subscription if required
   192  func (cls *CLIService) deleteSubscription(ctx context.Context) error {
   193  	if !cls.perSessionSubscription {
   194  		return nil
   195  	}
   196  
   197  	if m, ok := cls.ms.(subscriptionCreator); ok { // TODO: what if not ok?
   198  		subscriptionID := fmt.Sprintf(perSessionSubscription, cls.sessionID)
   199  		err := m.DeleteSubscription(ctx, subscriptionID, cls.target.ProjectID())
   200  		if err != nil {
   201  			// This error is only logged in the log file, there is no screen output
   202  			// indicating there has been an error
   203  			return fmt.Errorf("error deleting per session subscription: %w", err)
   204  		}
   205  	}
   206  
   207  	return nil
   208  }
   209  
   210  func (cls *CLIService) RetrieveIdentity(_ context.Context) error {
   211  	cmd := exec.Command("gcloud", "config", "get-value", "account")
   212  	out, err := cmd.Output()
   213  	if err != nil {
   214  		return err
   215  	}
   216  	res := strings.TrimSpace(string(out))
   217  	if res == "" {
   218  		return fmt.Errorf("no gcloud userid found")
   219  	}
   220  	cls.userID = res
   221  	return nil
   222  }
   223  
   224  // Optionally modify the topic messages are sent to
   225  func (cls *CLIService) SetTopicTemplate(topicTemplate string) {
   226  	cls.topicTemplate = topicTemplate
   227  }
   228  
   229  // Optionally modify the subscription name messages are listened to
   230  func (cls *CLIService) SetSubscriptionTemplate(subscriptionTemplate string) {
   231  	cls.subscriptionTemplate = subscriptionTemplate
   232  }
   233  
   234  // returns a read only version of the channel.
   235  // feed will be blocked when the session prompt is active.
   236  func (cls CLIService) GetDisplayChannel() <-chan msgdata.CommandResponse {
   237  	return cls.dispChan
   238  }
   239  
   240  func (cls CLIService) GetSessionContext() context.Context {
   241  	return cls.seshCtx
   242  }
   243  
   244  func (cls *CLIService) UserID() string {
   245  	return cls.userID
   246  }
   247  
   248  func (cls CLIService) IdleTime() time.Duration {
   249  	return time.Since(cls.idleTime)
   250  }
   251  
   252  // Setting which, when enabled, causes a new subscription scoped to a single
   253  // sessionID to be created on Connect, and used in place of the default
   254  // response subscription to listen to response messages.
   255  // This is the default behaviour.
   256  func (cls *CLIService) EnablePerSessionSubscription() {
   257  	cls.perSessionSubscription = true
   258  }
   259  
   260  // Disables creating a new subscription scoped to a single sessionID
   261  func (cls *CLIService) DisablePerSessionSubscription() {
   262  	cls.perSessionSubscription = false
   263  }
   264  
   265  func (cls *CLIService) Env() []string {
   266  	return nil
   267  }
   268  

View as plain text