Merge pull request #198 from tongjingran/list

add command goc list
This commit is contained in:
Li Yiyang 2021-07-21 16:32:21 +08:00 committed by GitHub
commit 8e7ef51597
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 276 additions and 2 deletions

30
cmd/list.go Normal file
View File

@ -0,0 +1,30 @@
package cmd
import (
"github.com/qiniu/goc/v2/pkg/client"
"github.com/qiniu/goc/v2/pkg/config"
"github.com/spf13/cobra"
)
var listCmd = &cobra.Command{
Use: "list",
Short: "Lists all the registered services",
Long: "Lists all the registered services",
Example: `
goc list [flags]
`,
Run: list,
}
var listWide bool
func init() {
listCmd.Flags().StringVar(&config.GocConfig.Host, "host", "127.0.0.1:7777", "specify the host of the goc server")
listCmd.Flags().BoolVar(&listWide, "wide", false, "list all services with more information (such as pid)")
rootCmd.AddCommand(listCmd)
}
func list(cmd *cobra.Command, args []string) {
client.NewWorker("http://" + config.GocConfig.Host).ListAgents(listWide)
}

3
go.mod
View File

@ -9,12 +9,15 @@ require (
github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/olekukonko/tablewriter v0.0.5
github.com/spf13/cobra v1.1.3 github.com/spf13/cobra v1.1.3
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
github.com/tongjingran/copy v1.4.2 github.com/tongjingran/copy v1.4.2
go.uber.org/zap v1.17.0 go.uber.org/zap v1.17.0
golang.org/x/mod v0.4.2 golang.org/x/mod v0.4.2
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 // indirect golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 // indirect
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d
golang.org/x/tools v0.1.3 golang.org/x/tools v0.1.3
k8s.io/kubectl v0.21.2 k8s.io/kubectl v0.21.2
k8s.io/test-infra v0.0.0-20210618100605-34aa2f2aa75b k8s.io/test-infra v0.0.0-20210618100605-34aa2f2aa75b

4
go.sum
View File

@ -853,6 +853,8 @@ github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v0.0.0-20160514122348-38ee283dabf1/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v0.0.0-20160514122348-38ee283dabf1/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@ -932,6 +934,8 @@ github.com/octago/sflags v0.2.0/go.mod h1:G0bjdxh4qPRycF74a2B8pU36iTp9QHGx0w0dFZ
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=

147
pkg/client/client.go Normal file
View File

@ -0,0 +1,147 @@
package client
import (
"encoding/json"
"fmt"
"golang.org/x/term"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"github.com/olekukonko/tablewriter"
"github.com/qiniu/goc/v2/pkg/log"
)
// Action provides methods to contact with the covered agent under test
type Action interface {
ListAgents(bool)
}
const (
// CoverAgentsListAPI list all the registered agents
CoverAgentsListAPI = "/v2/rpcagents"
)
type client struct {
Host string
client *http.Client
}
// gocListAgents response of the list request
type gocListAgents struct {
Items []gocCoveredAgent `json:"items"`
}
// gocCoveredAgent represents a covered client
type gocCoveredAgent struct {
Id string `json:"id"`
RemoteIP string `json:"remoteip"`
Hostname string `json:"hostname"`
CmdLine string `json:"cmdline"`
Pid string `json:"pid"`
}
// NewWorker creates a worker to contact with host
func NewWorker(host string) Action {
_, err := url.ParseRequestURI(host)
if err != nil {
log.Fatalf("parse url %s failed, err: %v", host, err)
}
return &client{
Host: host,
client: http.DefaultClient,
}
}
func (c *client) ListAgents(wide bool) {
u := fmt.Sprintf("%s%s", c.Host, CoverAgentsListAPI)
_, body, err := c.do("GET", u, "", nil)
if err != nil && isNetworkError(err) {
_, body, err = c.do("GET", u, "", nil)
}
if err != nil {
err = fmt.Errorf("goc list failed: %v", err)
log.Fatalf(err.Error())
}
agents := gocListAgents{}
err = json.Unmarshal(body, &agents)
if err != nil {
err = fmt.Errorf("goc list failed: json unmarshal failed: %v", err)
log.Fatalf(err.Error())
}
table := tablewriter.NewWriter(os.Stdout)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetTablePadding(" ") // pad with 3 blank spaces
table.SetNoWhiteSpace(true)
table.SetReflowDuringAutoWrap(false)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAutoWrapText(false)
if wide {
table.SetHeader([]string{"ID", "REMOTEIP", "HOSTNAME", "PID", "CMD"})
table.SetColumnAlignment([]int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
} else {
table.SetHeader([]string{"ID", "REMOTEIP", "CMD"})
table.SetColumnAlignment([]int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
}
for _, agent := range agents.Items {
if wide {
table.Append([]string{agent.Id, agent.RemoteIP, agent.Hostname, agent.Pid, agent.CmdLine})
} else {
preLen := len(agent.Id) + len(agent.RemoteIP) + 9
table.Append([]string{agent.Id, agent.RemoteIP, getSimpleCmdLine(preLen, agent.CmdLine)})
}
}
table.Render()
return
}
// getSimpleCmdLine
func getSimpleCmdLine(preLen int, cmdLine string) string {
pathLen := len(cmdLine)
width, _, err := term.GetSize(int(os.Stdin.Fd()))
if err != nil || width <= preLen+16 {
width = 16 + preLen // show at least 16 words of the command
}
if pathLen > width-preLen {
return cmdLine[:width-preLen]
}
return cmdLine
}
func (c *client) do(method, url, contentType string, body io.Reader) (*http.Response, []byte, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, nil, err
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
res, err := c.client.Do(req)
if err != nil {
return nil, nil, err
}
defer res.Body.Close()
responseBody, err := ioutil.ReadAll(res.Body)
if err != nil {
return res, nil, err
}
return res, responseBody, nil
}
func isNetworkError(err error) bool {
if err == io.EOF {
return true
}
_, ok := err.(net.Error)
return ok
}

89
pkg/client/client_test.go Normal file
View File

@ -0,0 +1,89 @@
package client
import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func captureStdout(f func()) string {
r, w, _ := os.Pipe()
stdout := os.Stdout
os.Stdout = w
defer func() {
os.Stdout = stdout
}()
f()
w.Close()
var buf bytes.Buffer
io.Copy(&buf, r)
return buf.String()
}
func TestClientListAgents(t *testing.T) {
mockAgents := `{"items": [{"id": "testID", "remoteip": "1.1.1.1", "hostname": "testHost", "cmdline": "./testCmd -f testArgs", "pid": "0"}]}`
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(mockAgents))
}))
defer mockServer.Close()
c := NewWorker(mockServer.URL)
testCases := map[string]struct {
input bool
expected string
}{
"simple list": {
false,
`ID REMOTEIP CMD
testID 1.1.1.1 ./testCmd -f tes
`,
},
"wide list": {
true,
`ID REMOTEIP HOSTNAME PID CMD
testID 1.1.1.1 testHost 0 ./testCmd -f testArgs
`,
},
}
for name, tt := range testCases {
t.Run(name, func(t *testing.T) {
f := func() { c.ListAgents(tt.input) }
output := captureStdout(f)
fmt.Println(output)
assert.Equal(t, output, tt.expected)
})
}
}
func TestClientDo(t *testing.T) {
c := &client{
client: http.DefaultClient,
}
_, _, err := c.do(" ", "http://127.0.0.1:7777", "", nil) // a invalid method
assert.Contains(t, err.Error(), "invalid method")
}
func TestGetSimpleSvcName(t *testing.T) {
testCases := map[string]struct {
input string
expected string
}{
"short path": {"1234567890abc.go", "1234567890abc.go"},
"long path": {"1234567890abcdef.go", "1234567890abcdef"},
}
for name, tt := range testCases {
t.Run(name, func(t *testing.T) {
assert.Equal(t, getSimpleCmdLine(0, tt.input), tt.expected)
})
}
}

View File

@ -204,7 +204,7 @@ func getRegisterInfo() (*processInfo, error) {
pid := os.Getpid() pid := os.Getpid()
cmdline := os.Args[0] cmdline := strings.Join(os.Args, " ")
return &processInfo{ return &processInfo{
hostname: hostname, hostname: hostname,

View File

@ -6,6 +6,7 @@ import (
"os" "os"
"log" "log"
"strconv" "strconv"
"strings"
"net/url" "net/url"
"{{.GlobalCoverVarImportPath}}/websocket" "{{.GlobalCoverVarImportPath}}/websocket"
@ -115,7 +116,7 @@ func getRegisterInfo() (*processInfo, error) {
pid := os.Getpid() pid := os.Getpid()
cmdline := os.Args[0] cmdline := strings.Join(os.Args, " ")
return &processInfo{ return &processInfo{
hostname: hostname, hostname: hostname,