Skip to content

Commit 312277a

Browse files
Improve exec functionality (#113)
Fixes #107 This is a big change that does the following: - replaces the bespoke exec implementation with an impl inspired from the nomad cli, using the client directly - add a wander exec command - therefore tls is supported - improves general exec experience (can move cursor, does not strip ansi escape sequences, etc) - BREAKAGE: exec with wander serve breaks. Unfortunately with this impl, tea.ExecProcess is used, which is currently charmbracelet/wish#196
1 parent 82af204 commit 312277a

20 files changed

+557
-392
lines changed

cmd/exec.go

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"github.com/robinovitch61/wander/internal/tui/nomad"
6+
"github.com/spf13/cobra"
7+
"os"
8+
)
9+
10+
var (
11+
execCmd = &cobra.Command{
12+
Use: "exec",
13+
Short: "Exec into a running task",
14+
Long: `Exec into a running nomad task`,
15+
Example: `
16+
# specify job and task, assuming single allocation
17+
wander exec alright_stop --task redis echo "hi"
18+
19+
# specify allocation, assuming single task
20+
wander exec 3dca0982 echo "hi"
21+
22+
# use prefixes of jobs or allocation ids
23+
wander exec al echo "hi"
24+
wander exec 3d echo "hi"
25+
26+
# specify flags for the exec command with --
27+
wander exec alright_stop --task redis -- echo -n "hi"
28+
`,
29+
Run: execEntrypoint,
30+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return nil },
31+
}
32+
)
33+
34+
func execEntrypoint(cmd *cobra.Command, args []string) {
35+
task := cmd.Flags().Lookup("task").Value.String()
36+
client, err := getConfig(cmd, "").Client()
37+
if err != nil {
38+
fmt.Println(fmt.Errorf("could not get client: %v", err))
39+
os.Exit(1)
40+
}
41+
allocID := args[0]
42+
execArgs := args[1:]
43+
if len(execArgs) == 0 {
44+
fmt.Println("no command specified")
45+
os.Exit(1)
46+
}
47+
_, err = nomad.AllocExec(client, allocID, task, execArgs)
48+
if err != nil {
49+
fmt.Println(fmt.Errorf("could not exec into task: %v", err))
50+
os.Exit(1)
51+
}
52+
}

cmd/root.go

+4
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,11 @@ func init() {
268268
viper.BindPFlag(cliLong, serveCmd.PersistentFlags().Lookup(c.cfgFileEnvVar))
269269
}
270270

271+
// exec
272+
execCmd.PersistentFlags().StringP("task", "", "", "Sets the task to exec command in")
273+
271274
rootCmd.AddCommand(serveCmd)
275+
rootCmd.AddCommand(execCmd)
272276
}
273277

274278
func initConfig(cmd *cobra.Command, nameToArg map[string]arg) error {

cmd/util.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ func customLoggingMiddleware() wish.Middleware {
286286
}
287287
}
288288

289-
func setup(cmd *cobra.Command, overrideToken string) (app.Model, []tea.ProgramOption) {
289+
func getConfig(cmd *cobra.Command, overrideToken string) app.Config {
290290
nomadAddr := retrieveAddress(cmd)
291291
nomadToken := retrieveToken(cmd)
292292
if overrideToken != "" {
@@ -322,7 +322,7 @@ func setup(cmd *cobra.Command, overrideToken string) (app.Model, []tea.ProgramOp
322322
startFiltering := retrieveStartFiltering(cmd)
323323
filterWithContext := retrieveFilterWithContext(cmd)
324324

325-
initialModel := app.InitialModel(app.Config{
325+
return app.Config{
326326
Version: getVersion(),
327327
URL: nomadAddr,
328328
Token: nomadToken,
@@ -358,6 +358,10 @@ func setup(cmd *cobra.Command, overrideToken string) (app.Model, []tea.ProgramOp
358358
CompactTables: compactTables,
359359
StartFiltering: startFiltering,
360360
FilterWithContext: filterWithContext,
361-
})
361+
}
362+
}
363+
364+
func setup(cmd *cobra.Command, overrideToken string) (app.Model, []tea.ProgramOption) {
365+
initialModel := app.InitialModel(getConfig(cmd, overrideToken))
362366
return initialModel, []tea.ProgramOption{tea.WithAltScreen()}
363367
}

go.mod

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ require (
1313
github.com/gorilla/websocket v1.5.0
1414
github.com/hashicorp/nomad/api v0.0.0-20230619092614-e29ad68c588d
1515
github.com/itchyny/gojq v0.12.13
16+
github.com/moby/term v0.5.0
1617
github.com/olekukonko/tablewriter v0.0.5
1718
github.com/spf13/cobra v1.7.0
1819
github.com/spf13/pflag v1.0.5
1920
github.com/spf13/viper v1.16.0
21+
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
2022
)
2123

2224
require (
25+
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
2326
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
2427
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2528
github.com/caarlos0/sshmarshal v0.1.0 // indirect
@@ -54,7 +57,6 @@ require (
5457
github.com/spf13/jwalterweatherman v1.1.0 // indirect
5558
github.com/subosito/gotenv v1.4.2 // indirect
5659
golang.org/x/crypto v0.10.0 // indirect
57-
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
5860
golang.org/x/sync v0.3.0 // indirect
5961
golang.org/x/sys v0.9.0 // indirect
6062
golang.org/x/term v0.9.0 // indirect

go.sum

+7
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
3636
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
3737
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
3838
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
39+
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
40+
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
3941
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
4042
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
4143
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -73,6 +75,8 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht
7375
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
7476
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
7577
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
78+
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
79+
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
7680
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7781
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7882
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -207,6 +211,8 @@ github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJ
207211
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
208212
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
209213
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
214+
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
215+
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
210216
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
211217
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
212218
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@@ -402,6 +408,7 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w
402408
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
403409
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
404410
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
411+
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
405412
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
406413
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
407414
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

img/wander.gif

910 KB
Loading

internal/tui/components/app/app.go

+38-97
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55
"github.com/charmbracelet/bubbles/key"
66
tea "github.com/charmbracelet/bubbletea"
7-
"github.com/gorilla/websocket"
87
"github.com/hashicorp/nomad/api"
98
"github.com/itchyny/gojq"
109
"github.com/robinovitch61/wander/internal/dev"
@@ -16,7 +15,7 @@ import (
1615
"github.com/robinovitch61/wander/internal/tui/message"
1716
"github.com/robinovitch61/wander/internal/tui/nomad"
1817
"github.com/robinovitch61/wander/internal/tui/style"
19-
"os"
18+
"os/exec"
2019
"strings"
2120
"time"
2221
)
@@ -77,19 +76,15 @@ type Model struct {
7776

7877
updateID int
7978

79+
lastExecContent string
80+
8081
eventsStream nomad.EventsStream
8182
event string
8283
meta map[string]string
8384

8485
logsStream nomad.LogsStream
8586
lastLogFinished bool
8687

87-
execWebSocket *websocket.Conn
88-
execPty *os.File
89-
inPty bool
90-
webSocketConnected bool
91-
lastCommandFinished struct{ stdOut, stdErr bool }
92-
9388
width, height int
9489
initialized bool
9590
err error
@@ -110,7 +105,7 @@ func InitialModel(c Config) Model {
110105
c.LogoColor,
111106
c.URL,
112107
c.Version,
113-
nomad.GetPageKeyHelp(firstPage, false, false, false, false, false, false, nomad.StdOut, false, !c.StartAllTasksView),
108+
nomad.GetPageKeyHelp(firstPage, false, false, false, nomad.StdOut, false, !c.StartAllTasksView),
114109
)
115110
return Model{
116111
config: c,
@@ -163,10 +158,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
163158
cmds = append(cmds, m.getCurrentPageCmd())
164159
} else {
165160
m.setPageWindowSize()
166-
if m.currentPage == nomad.ExecPage {
167-
viewportHeightWithoutFooter := m.getCurrentPageModel().ViewportHeight() - 1 // hardcoded as known today, has to change if footer expands
168-
cmds = append(cmds, nomad.ResizeTty(m.execWebSocket, m.width, viewportHeightWithoutFooter))
169-
}
170161
}
171162

172163
case nomad.PageLoadedMsg:
@@ -265,43 +256,23 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
265256
m.updateID = nextUpdateID()
266257
}
267258

268-
case message.PageInputReceivedMsg:
259+
case nomad.ExecCompleteMsg:
269260
if m.currentPage == nomad.ExecPage {
270-
m.getCurrentPageModel().SetLoading(true)
271-
return m, nomad.InitiateWebSocket(m.config.URL, m.config.Token, m.alloc.ID, m.taskName, msg.Input)
261+
m.getCurrentPageModel().SetDoesNeedNewInput()
262+
m.lastExecContent = strings.TrimSpace(msg.Output)
263+
m.setPage(nomad.ExecCompletePage)
264+
cmds = append(cmds, m.getCurrentPageCmd())
272265
}
273266

274-
case nomad.ExecWebSocketConnectedMsg:
275-
m.execWebSocket = msg.WebSocketConnection
276-
m.webSocketConnected = true
277-
m.getCurrentPageModel().SetLoading(false)
278-
m.setInPty(true)
279-
viewportHeightWithoutFooter := m.getCurrentPageModel().ViewportHeight() - 1 // hardcoded as known today, has to change if footer expands
280-
cmds = append(cmds, nomad.ResizeTty(m.execWebSocket, m.width, viewportHeightWithoutFooter))
281-
cmds = append(cmds, nomad.ReadExecWebSocketNextMessage(m.execWebSocket))
282-
cmds = append(cmds, nomad.SendHeartbeatWithDelay())
283-
284-
case nomad.ExecWebSocketHeartbeatMsg:
285-
if m.currentPage == nomad.ExecPage && m.webSocketConnected {
286-
cmds = append(cmds, nomad.SendHeartbeat(m.execWebSocket))
287-
cmds = append(cmds, nomad.SendHeartbeatWithDelay())
288-
return m, tea.Batch(cmds...)
289-
}
290-
return m, nil
291-
292-
case nomad.ExecWebSocketResponseMsg:
267+
case message.PageInputReceivedMsg:
293268
if m.currentPage == nomad.ExecPage {
294-
if msg.Close {
295-
m.webSocketConnected = false
296-
m.setInPty(false)
297-
m.getCurrentPageModel().AppendToViewport([]page.Row{{Row: constants.ExecWebSocketClosed}}, true)
298-
m.getCurrentPageModel().ScrollViewportToBottom()
299-
} else {
300-
m.appendToViewport(msg.StdOut, m.lastCommandFinished.stdOut)
301-
m.appendToViewport(msg.StdErr, m.lastCommandFinished.stdErr)
302-
m.updateLastCommandFinished(msg.StdOut, msg.StdErr)
303-
cmds = append(cmds, nomad.ReadExecWebSocketNextMessage(m.execWebSocket))
304-
}
269+
c := exec.Command("wander", "exec", m.alloc.ID, "--task", m.taskName, msg.Input)
270+
stdoutProxy := &nomad.StdoutProxy{}
271+
c.Stdout = stdoutProxy
272+
m.getCurrentPageModel().SetDoesNeedNewInput()
273+
return m, tea.ExecProcess(c, func(err error) tea.Msg {
274+
return nomad.ExecCompleteMsg{Output: string(stdoutProxy.SavedOutput)}
275+
})
305276
}
306277
}
307278

@@ -328,7 +299,7 @@ func (m Model) View() string {
328299
}
329300

330301
func (m *Model) initialize() error {
331-
client, err := m.config.client()
302+
client, err := m.config.Client()
332303
if err != nil {
333304
return err
334305
}
@@ -355,9 +326,6 @@ func (m *Model) initialize() error {
355326

356327
func (m *Model) cleanupCmd() tea.Cmd {
357328
return func() tea.Msg {
358-
if m.execWebSocket != nil {
359-
nomad.CloseWebSocket(m.execWebSocket)()
360-
}
361329
return message.CleanupCompleteMsg{}
362330
}
363331
}
@@ -377,28 +345,12 @@ func (m *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd {
377345
addingQToFilter := m.currentPageFilterFocused()
378346
saving := m.currentPageViewportSaving()
379347
enteringInput := currentPageModel != nil && currentPageModel.EnteringInput()
380-
typingQLegitimately := msg.String() == "q" && (addingQToFilter || saving || enteringInput || m.inPty)
381-
ctrlCInPty := m.inPty && msg.String() == "ctrl+c"
382-
if (!ctrlCInPty && !typingQLegitimately) || m.err != nil {
348+
typingQLegitimately := msg.String() == "q" && (addingQToFilter || saving || enteringInput)
349+
if !typingQLegitimately || m.err != nil {
383350
return m.cleanupCmd()
384351
}
385352
}
386353

387-
if m.currentPage == nomad.ExecPage {
388-
var keypress string
389-
if m.inPty {
390-
if key.Matches(msg, keymap.KeyMap.Back) {
391-
m.setInPty(false)
392-
return nil
393-
} else {
394-
keypress = nomad.GetKeypress(msg)
395-
return nomad.SendWebSocketMessage(m.execWebSocket, keypress)
396-
}
397-
} else if key.Matches(msg, keymap.KeyMap.Forward) && m.webSocketConnected && !m.currentPageViewportSaving() {
398-
m.setInPty(true)
399-
}
400-
}
401-
402354
if !m.currentPageFilterFocused() && !m.currentPageViewportSaving() {
403355
switch {
404356
case key.Matches(msg, keymap.KeyMap.Compact):
@@ -436,9 +388,6 @@ func (m *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd {
436388
if !m.currentPageFilterApplied() {
437389
switch m.currentPage {
438390
case nomad.ExecPage:
439-
if !m.getCurrentPageModel().EnteringInput() {
440-
cmds = append(cmds, nomad.CloseWebSocket(m.execWebSocket))
441-
}
442391
m.getCurrentPageModel().SetDoesNeedNewInput()
443392
}
444393

@@ -618,32 +567,8 @@ func (m *Model) appendToViewport(content string, startOnNewLine bool) {
618567
m.getCurrentPageModel().ScrollViewportToBottom()
619568
}
620569

621-
// updateLastCommandFinished updates lastCommandFinished, which is necessary
622-
// because some data gets received in chunks in which a trailing \n indicates
623-
// finished content, otherwise more content is expected (e.g. the exec
624-
// websocket behaves this way when returning long content)
625-
func (m *Model) updateLastCommandFinished(stdOut, stdErr string) {
626-
m.lastCommandFinished.stdOut = false
627-
m.lastCommandFinished.stdErr = false
628-
if strings.HasSuffix(stdOut, "\n") {
629-
m.lastCommandFinished.stdOut = true
630-
}
631-
if strings.HasSuffix(stdErr, "\n") {
632-
m.lastCommandFinished.stdErr = true
633-
}
634-
}
635-
636-
func (m *Model) setInPty(inPty bool) {
637-
m.inPty = inPty
638-
m.getCurrentPageModel().SetViewportPromptVisible(inPty)
639-
if inPty {
640-
m.getCurrentPageModel().ScrollViewportToBottom()
641-
}
642-
m.updateKeyHelp()
643-
}
644-
645570
func (m *Model) updateKeyHelp() {
646-
newKeyHelp := nomad.GetPageKeyHelp(m.currentPage, m.currentPageFilterFocused(), m.currentPageFilterApplied(), m.currentPageViewportSaving(), m.getCurrentPageModel().EnteringInput(), m.inPty, m.webSocketConnected, m.logType, m.compact, m.inJobsMode)
571+
newKeyHelp := nomad.GetPageKeyHelp(m.currentPage, m.currentPageFilterFocused(), m.currentPageFilterApplied(), m.currentPageViewportSaving(), m.logType, m.compact, m.inJobsMode)
647572
m.header.SetKeyHelp(newKeyHelp)
648573
}
649574

@@ -682,7 +607,23 @@ func (m Model) getCurrentPageCmd() tea.Cmd {
682607
case nomad.JobTasksPage:
683608
return nomad.FetchTasksForJob(m.client, m.jobID, m.jobNamespace, m.config.JobTaskColumns)
684609
case nomad.ExecPage:
685-
return nomad.LoadExecPage()
610+
return func() tea.Msg {
611+
// this does no async work, just moves to request the command input
612+
return nomad.PageLoadedMsg{Page: nomad.ExecPage, TableHeader: []string{}, AllPageRows: []page.Row{}}
613+
}
614+
case nomad.ExecCompletePage:
615+
return func() tea.Msg {
616+
// this does no async work, just shows the output of the prior exec session
617+
var allPageRows []page.Row
618+
for _, row := range strings.Split(m.lastExecContent, "\n") {
619+
row = strings.ReplaceAll(row, "\r", "")
620+
if len(row) == 0 {
621+
continue
622+
}
623+
allPageRows = append(allPageRows, page.Row{Row: formatter.StripOSCommandSequences(formatter.StripANSI(row))})
624+
}
625+
return nomad.PageLoadedMsg{Page: nomad.ExecCompletePage, TableHeader: []string{"Exec Session Output"}, AllPageRows: allPageRows}
626+
}
686627
case nomad.AllocSpecPage:
687628
return nomad.FetchAllocSpec(m.client, m.alloc.ID)
688629
case nomad.LogsPage:

internal/tui/components/app/util.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func nextUpdateID() int {
1818
return updateID
1919
}
2020

21-
func (c Config) client() (*api.Client, error) {
21+
func (c Config) Client() (*api.Client, error) {
2222
config := &api.Config{
2323
Address: c.URL,
2424
SecretID: c.Token,

0 commit comments

Comments
 (0)