Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions action/common/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,29 @@ import (
)

func ReadArgs(in io.Reader) error {
os.Args = []string{""}
return ReadArgsFromScanner(bufio.NewScanner(in))
}

scanner := bufio.NewScanner(in)
func ReadArgsFromScanner(scanner *bufio.Scanner) error {
os.Args = []string{""}

var lines []string

for scanner.Scan() {
line := strings.Trim(scanner.Text(), "\r\n ")
if line[len(line)-1] == '\\' {
line = line[:len(line)-1]
lines = append(lines, line)
current := strings.Trim(scanner.Text(), "\r\n ")
if strings.HasSuffix(current, "\\") {
current = strings.TrimSuffix(current, "\\")
lines = append(lines, current)
} else {
lines = append(lines, line)
lines = append(lines, current)
break
}
}
if err := scanner.Err(); err != nil {
return err
}
if len(lines) == 0 {
return io.EOF
}

argsStr := strings.Join(lines, " ")
rawArgs := strings.Split(argsStr, "'")
Expand Down
26 changes: 22 additions & 4 deletions action/common/args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
package common

import (
"bufio"
"bytes"
"io"
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -134,13 +137,28 @@ func TestReadArgs(t *testing.T) {
continue
}
assert.Nil(t, ReadArgs(&stdin))
assert.Equal(t, len(os.Args), len(testCase.args))
for i := 0; i < len(os.Args); i++ {
assert.Equal(t, os.Args[i], testCase.args[i])
}
assert.Equal(t, testCase.args, os.Args[1:])
}
}

func TestReadArgsPreservesFollowingCommands(t *testing.T) {
scanner := bufio.NewScanner(strings.NewReader("transaction --help\nquit\n"))

assert.NoError(t, ReadArgsFromScanner(scanner))
assert.Equal(t, []string{"transaction", "--help"}, os.Args[1:])

assert.NoError(t, ReadArgsFromScanner(scanner))
assert.Equal(t, []string{"quit"}, os.Args[1:])
}

func TestReadArgsHandlesEmptyLineAndEOF(t *testing.T) {
stdin := strings.NewReader("\n")

assert.NoError(t, ReadArgs(stdin))
assert.Equal(t, []string{}, os.Args[1:])
assert.ErrorIs(t, ReadArgs(stdin), io.EOF)
}

func TestParseDictArg(t *testing.T) {
for _, testCase := range dictArgTestCases {
if !testCase.valid {
Expand Down
43 changes: 43 additions & 0 deletions action/common/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package common

import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

func ResetLocalFlags(cmd *cobra.Command) {
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
_ = cmd.Flags().Set(flag.Name, flag.DefValue)
flag.Changed = false
})
}

func ResetLocalFlagsOnParseErrorAndHelp(cmd *cobra.Command) {
cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
ResetLocalFlags(cmd)
return err
})

helpFunc := cmd.HelpFunc()
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
defer ResetLocalFlags(cmd)
helpFunc(cmd, args)
})
}
37 changes: 37 additions & 0 deletions action/diagnose/diagnose.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package diagnose

import (
"github.com/seata/seata-ctl/action/common"
"github.com/spf13/cobra"
)

func init() {
DiagnoseCmd.AddCommand(RunCmd)
DiagnoseCmd.SetUsageTemplate(common.GetUsageTmpl("diagnose"))
DiagnoseCmd.SetHelpTemplate(common.GetHelpTmpl())
}

var DiagnoseCmd = &cobra.Command{
Use: "diagnose",
Short: "Run read-only diagnostics",
Run: func(cmd *cobra.Command, _ []string) {
_ = cmd.Help()
},
}
88 changes: 88 additions & 0 deletions action/diagnose/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package diagnose

import (
"fmt"
"strings"
"time"

"github.com/seata/seata-ctl/action/common"
"github.com/seata/seata-ctl/seata"
"github.com/spf13/cobra"
)

var RunCmd = newRunCommand()

func newRunCommand() *cobra.Command {
var (
output string
checkDB bool
dbAddress string
tcpTimeout time.Duration
pageNum int
pageSize int
lockPageNum int
lockPageSize int
)

cmd := &cobra.Command{
Use: "run",
Short: "Run diagnostics against the current Seata server",
RunE: func(cmd *cobra.Command, _ []string) error {
defer common.ResetLocalFlags(cmd)

output = strings.ToLower(output)
if _, err := seata.NormalizeOutput(output); err != nil {
return err
}

report, err := seata.RunDiagnostics(seata.DiagnoseOptions{
CheckDB: checkDB,
DBAddress: dbAddress,
TCPTimeout: tcpTimeout,
PageNum: pageNum,
PageSize: pageSize,
LockPageNum: lockPageNum,
LockPageSize: lockPageSize,
})
if err != nil {
return err
}

result, err := seata.FormatDiagnoseReport(report, output)
if err != nil {
return err
}
fmt.Println(result)
return nil
},
}
common.ResetLocalFlagsOnParseErrorAndHelp(cmd)
cmd.SetUsageTemplate(common.GetUsageTmpl("diagnose run"))
cmd.SetHelpTemplate(common.GetHelpTmpl())
cmd.Flags().BoolVar(&checkDB, "check-db", false, "Check database connectivity")
cmd.Flags().StringVar(&dbAddress, "db-address", "", "Database address in host:port form")
cmd.Flags().DurationVar(&tcpTimeout, "tcp-timeout", 3*time.Second, "TCP timeout")
cmd.Flags().IntVar(&pageNum, "page-num", 1, "Transaction page number")
cmd.Flags().IntVar(&pageSize, "page-size", 1, "Transaction page size")
cmd.Flags().IntVar(&lockPageNum, "lock-page-num", 1, "Lock page number")
cmd.Flags().IntVar(&lockPageSize, "lock-page-size", 1, "Lock page size")
cmd.Flags().StringVar(&output, "output", seata.OutputTable, "Output format: table, json, yaml")
return cmd
}
122 changes: 122 additions & 0 deletions action/diagnose/run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package diagnose

import (
"bytes"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strconv"
"strings"
"testing"

"github.com/seata/seata-ctl/seata"
"github.com/spf13/cobra"
)

func TestRunCommandBuildsReport(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case seata.LoginURL:
fmt.Fprint(w, `{"code":"200","message":"success","data":"test-token","success":true}`)
case seata.HealthCheckURL:
if got := r.Header.Get("authorization"); got != "test-token" {
t.Fatalf("authorization = %q, want test-token", got)
}
fmt.Fprint(w, `{"code":"200","message":"success","success":true,"data":[{"type":"nacos","address":"127.0.0.1:7091","status":"ok"}]}`)
case seata.GlobalSessionQueryURL:
if got := r.Header.Get("authorization"); got != "test-token" {
t.Fatalf("authorization = %q, want test-token", got)
}
fmt.Fprint(w, `{"code":"200","message":"success","success":true,"pageSize":1,"pageNum":1,"total":1,"pages":1,"data":[{"xid":"xid-1","transactionId":"1001","status":1,"applicationId":"order-service","transactionServiceGroup":"default_tx_group","transactionName":"createOrder","timeout":60000,"beginTime":1710000000000}]}`)
case seata.GlobalLockQueryURL:
if got := r.Header.Get("authorization"); got != "test-token" {
t.Fatalf("authorization = %q, want test-token", got)
}
fmt.Fprint(w, `{"code":"200","message":"success","success":true,"pageSize":1,"pageNum":1,"total":1,"pages":1,"data":[{"xid":"xid-1","transactionId":"1001","branchId":"2001","resourceId":"jdbc:mysql://127.0.0.1:3306/seata","tableName":"account","pk":"1","rowKey":"1","vgroup":"default_tx_group"}]}`)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
loginToServer(t, server)

cmd := newRunCommand()
output, err := executeCommand(t, cmd, "--output", "json")
if err != nil {
t.Fatalf("execute command: %v", err)
}
if !strings.Contains(output, `"stage": "status"`) || !strings.Contains(output, `"stage": "transaction query"`) || !strings.Contains(output, `"stage": "global lock query"`) {
t.Fatalf("output = %s, want JSON diagnose report", output)
}
}

func loginToServer(t *testing.T, server *httptest.Server) {
t.Helper()
parsedURL, err := url.Parse(server.URL)
if err != nil {
t.Fatalf("parse server url: %v", err)
}
host, portStr, err := net.SplitHostPort(parsedURL.Host)
if err != nil {
t.Fatalf("split host port: %v", err)
}
port, err := strconv.Atoi(portStr)
if err != nil {
t.Fatalf("parse port: %v", err)
}
auth := seata.GetAuth()
auth.ServerIP = host
auth.ServerPort = port
auth.Username = "seata"
auth.Password = "seata"
if err = auth.Login(); err != nil {
t.Fatalf("login to test server: %v", err)
}
}

func executeCommand(t *testing.T, cmd *cobra.Command, args ...string) (string, error) {
t.Helper()
cmd.SetArgs(args)
cmd.SilenceUsage = true
cmd.SilenceErrors = true

var output bytes.Buffer
cmd.SetOut(&output)
cmd.SetErr(&output)

oldStdout := os.Stdout
reader, writer, err := os.Pipe()
if err != nil {
t.Fatalf("create stdout pipe: %v", err)
}
os.Stdout = writer
execErr := cmd.Execute()
_ = writer.Close()
os.Stdout = oldStdout
if _, err = io.Copy(&output, reader); err != nil {
t.Fatalf("read stdout: %v", err)
}
_ = reader.Close()
return output.String(), execErr
}
Loading
Loading