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
24
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
31
32 type uuerr struct {
33 error
34 user string
35 }
36
37 func (err uuerr) UserError() []string { return []string{err.user} }
38
39
40
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
76 topicTemplate string
77 subscriptionTemplate string
78
79 perSessionSubscription bool
80 }
81
82
83
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
92 perSessionSubscription: true,
93 }
94 }
95
96
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
129
130
131
132
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 {
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
186 cls.seshCancel()
187 endErr := cls.rcli.EndSession(cls.seshCtx, cls.sessionID)
188 return errors.Join(deleteErr, endErr)
189 }
190
191
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 {
198 subscriptionID := fmt.Sprintf(perSessionSubscription, cls.sessionID)
199 err := m.DeleteSubscription(ctx, subscriptionID, cls.target.ProjectID())
200 if err != nil {
201
202
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
225 func (cls *CLIService) SetTopicTemplate(topicTemplate string) {
226 cls.topicTemplate = topicTemplate
227 }
228
229
230 func (cls *CLIService) SetSubscriptionTemplate(subscriptionTemplate string) {
231 cls.subscriptionTemplate = subscriptionTemplate
232 }
233
234
235
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
253
254
255
256 func (cls *CLIService) EnablePerSessionSubscription() {
257 cls.perSessionSubscription = true
258 }
259
260
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