diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8ab8241..08e2093 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: - version: v1.30 + version: v1.45 args: -c .golangci.yml test: name: Test diff --git a/.golangci.yml b/.golangci.yml index 10e5e6a..aeb3e4c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -24,6 +24,9 @@ linters-settings: - octalLiteral - whyNoLint - wrapperFunc + - ioutilDeprecated + - httpNoBody + - sprintfQuotedString gocyclo: min-complexity: 15 goimports: @@ -33,8 +36,7 @@ linters-settings: gomnd: settings: mnd: - # don't include the "operation" and "assign" - checks: argument,case,condition,return + checks: case,condition,return govet: check-shadowing: true settings: diff --git a/cmd/client.go b/cmd/client.go index 440add9..70a1489 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -4,6 +4,7 @@ import ( "context" "os" + "github.com/cloudradar-monitoring/rportcli/internal/pkg/api" "github.com/cloudradar-monitoring/rportcli/internal/pkg/client" "github.com/cloudradar-monitoring/rportcli/internal/pkg/config" @@ -17,6 +18,7 @@ import ( ) func init() { + addPaginationFlags(clientsListCmd, api.ClientsLimitDefault) clientsCmd.AddCommand(clientsListCmd) clientCmd.Flags().StringP(controllers.ClientNameFlag, "n", "", "Get client by name") clientCmd.Flags().BoolP("all", "a", false, "Show client info with additional details") @@ -57,7 +59,7 @@ var clientsListCmd = &cobra.Command{ ctx, cancel := buildContext(context.Background()) defer cancel() - return clientsController.Clients(ctx) + return clientsController.Clients(ctx, params) }, } diff --git a/cmd/pagination.go b/cmd/pagination.go new file mode 100644 index 0000000..d911961 --- /dev/null +++ b/cmd/pagination.go @@ -0,0 +1,11 @@ +package cmd + +import ( + "github.com/cloudradar-monitoring/rportcli/internal/pkg/api" + "github.com/spf13/cobra" +) + +func addPaginationFlags(cmd *cobra.Command, defaultLimit int) { + cmd.Flags().IntP(api.PaginationLimit, "", defaultLimit, "Number of items to fetch") + cmd.Flags().IntP(api.PaginationOffset, "", 0, "Offset for fetch") +} diff --git a/cmd/tunnel.go b/cmd/tunnel.go index 2e693e1..b2bb82c 100644 --- a/cmd/tunnel.go +++ b/cmd/tunnel.go @@ -7,6 +7,7 @@ import ( "os/signal" "syscall" + "github.com/cloudradar-monitoring/rportcli/internal/pkg/api" "github.com/cloudradar-monitoring/rportcli/internal/pkg/rdp" "github.com/cloudradar-monitoring/rportcli/internal/pkg/client" @@ -31,6 +32,7 @@ func init() { config.DefineCommandInputs(tunnelCreateCmd, getCreateTunnelRequirements()) tunnelsCmd.AddCommand(tunnelCreateCmd) + addPaginationFlags(tunnelListCmd, api.ClientsLimitDefault) tunnelListCmd.Flags().StringP(controllers.ClientNameFlag, "n", "", "Get tunnels of a client by name") tunnelListCmd.Flags().StringP(controllers.ClientID, "c", "", "Get tunnels of a client by client id") @@ -113,10 +115,10 @@ rportcli tunnel create -l 0.0.0.0:3394 -r 22 -d bc0b705d-b5fb-4df5-84e3-82dba437 this example opens port 3394 on the rport server and forwards to port 22 of the client bc0b705d-b5fb-4df5-84e3-82dba437bbef with ssh url scheme and an IP address 10:1:2:3 allowed to access the tunnel ` - createTunnelLocalDescr = `refers to the ports of the rport server address to use for a new tunnel, e.g. '3390' or '0.0.0.0:3390'. + createTunnelLocalDescr = `refers to the ports of the rport server address to use for a new tunnel, e.g. '3390' or '0.0.0.0:3390'. If local is not specified, a random server port will be assigned automatically` - createTunnelLaunchSSHDescr = `Start the ssh client after the tunnel is established and close tunnel on ssh exit. + createTunnelLaunchSSHDescr = `Start the ssh client after the tunnel is established and close tunnel on ssh exit. Any parameter passed are append to the ssh command. i.e. -b "-l root"` ) @@ -212,7 +214,7 @@ func getCreateTunnelRequirements() []config.ParameterRequirement { }, { Field: controllers.LaunchRDP, - Description: `Start the default RDP client after the tunnel is established, e.g. -d 1 + Description: `Start the default RDP client after the tunnel is established, e.g. -d 1 Optionally pass the rdp-width and rdp-height params for RDP window size`, ShortName: "d", Type: config.BoolRequirementType, @@ -237,7 +239,6 @@ Optionally pass the rdp-width and rdp-height params for RDP window size`, Description: `username for a RDP session`, ShortName: "u", Type: config.StringRequirementType, - Validate: config.RequiredValidate, Help: "Enter a RDP user name", IsEnabled: func(providedParams *options.ParameterBag) bool { return IsRDPUserRequired }, }, @@ -285,7 +286,7 @@ func getDeleteTunnelRequirements() []config.ParameterRequirement { { Field: controllers.TunnelID, Description: "[required] tunnel id to delete", - ShortName: "t", + ShortName: "u", // t is used for timeout IsRequired: true, Validate: config.RequiredValidate, Help: "Enter a tunnel id", diff --git a/cmd/tunnelRDP_linux.go b/cmd/tunnelRDP_linux.go index 038af36..8540da9 100644 --- a/cmd/tunnelRDP_linux.go +++ b/cmd/tunnelRDP_linux.go @@ -1,3 +1,4 @@ +//go:build linux // +build linux package cmd diff --git a/go.mod b/go.mod index 80855df..f0ecfab 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dustin/go-humanize v1.0.0 github.com/fatih/color v1.10.0 - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/gorilla/websocket v1.4.2 github.com/magiconair/properties v1.8.4 github.com/mattn/go-runewidth v0.0.10 // indirect diff --git a/go.sum b/go.sum index 08b9736..7aa8c41 100644 --- a/go.sum +++ b/go.sum @@ -55,9 +55,6 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang-jwt/jwt v1.0.2 h1:Nj1npK0K5RnXGo1SxoOixRGAehIZ2326eXuca9gX9A4= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= diff --git a/internal/pkg/api/clients.go b/internal/pkg/api/clients.go index f5cdf02..486e038 100644 --- a/internal/pkg/api/clients.go +++ b/internal/pkg/api/clients.go @@ -11,14 +11,16 @@ import ( ) const ( - ClientsURL = "/api/v1/clients" + ClientsURL = "/api/v1/clients" + ClientsLimitDefault = 50 + ClientsLimitMax = 500 ) type ClientsResponse struct { Data []*models.Client } -func (rp *Rport) Clients(ctx context.Context) (cr *ClientsResponse, err error) { +func (rp *Rport) Clients(ctx context.Context, pagination Pagination) (cr *ClientsResponse, err error) { var req *http.Request u, err := url2.Parse(url.JoinURL(rp.BaseURL, ClientsURL)) if err != nil { @@ -26,6 +28,7 @@ func (rp *Rport) Clients(ctx context.Context) (cr *ClientsResponse, err error) { } q := u.Query() q.Set("fields[clients]", "id,name,timezone,tunnels,address,hostname,os_kernel,connection_state") + pagination.Apply(q) u.RawQuery = q.Encode() req, err = http.NewRequestWithContext( @@ -46,7 +49,7 @@ func (rp *Rport) Clients(ctx context.Context) (cr *ClientsResponse, err error) { func (rp *Rport) GetClients(ctx context.Context) (cls []*models.Client, err error) { var cr *ClientsResponse - cr, err = rp.Clients(ctx) + cr, err = rp.Clients(ctx, NewPaginationWithLimit(ClientsLimitMax)) if err != nil { return diff --git a/internal/pkg/api/clients_test.go b/internal/pkg/api/clients_test.go index d1a34d5..b669b26 100644 --- a/internal/pkg/api/clients_test.go +++ b/internal/pkg/api/clients_test.go @@ -83,7 +83,7 @@ func TestClientsList(t *testing.T) { authHeader := r.Header.Get("Authorization") assert.Equal(t, "Basic bG9nMTE2Njo1NjQzMjI=", authHeader) - assert.Equal(t, ClientsURL+"?fields%5Bclients%5D=id%2Cname%2Ctimezone%2Ctunnels%2Caddress%2Chostname%2Cos_kernel%2Cconnection_state", r.URL.String()) + assert.Equal(t, ClientsURL+"?fields%5Bclients%5D=id%2Cname%2Ctimezone%2Ctunnels%2Caddress%2Chostname%2Cos_kernel%2Cconnection_state&page%5Blimit%5D=500&page%5Boffset%5D=0", r.URL.String()) jsonEnc := json.NewEncoder(rw) e := jsonEnc.Encode(ClientsResponse{Data: clientsStub}) assert.NoError(t, e) @@ -98,7 +98,7 @@ func TestClientsList(t *testing.T) { }, }) - clientsResp, err := cl.Clients(context.Background()) + clientsResp, err := cl.Clients(context.Background(), NewPaginationWithLimit(ClientsLimitMax)) assert.NoError(t, err) if err != nil { return diff --git a/internal/pkg/api/pagination.go b/internal/pkg/api/pagination.go new file mode 100644 index 0000000..f655a6a --- /dev/null +++ b/internal/pkg/api/pagination.go @@ -0,0 +1,37 @@ +package api + +import ( + "net/url" + "strconv" + + options "github.com/breathbath/go_utils/v2/pkg/config" +) + +const ( + PaginationOffset = "offset" + PaginationLimit = "limit" +) + +type Pagination struct { + Limit int + Offset int +} + +func NewPaginationFromParams(params *options.ParameterBag) Pagination { + return Pagination{ + Offset: params.ReadInt(PaginationOffset, 0), + Limit: params.ReadInt(PaginationLimit, -1), + } +} + +func NewPaginationWithLimit(max int) Pagination { + return Pagination{ + Offset: 0, + Limit: max, + } +} + +func (p Pagination) Apply(q url.Values) { + q.Set("page[offset]", strconv.Itoa(p.Offset)) + q.Set("page[limit]", strconv.Itoa(p.Limit)) +} diff --git a/internal/pkg/api/pagination_test.go b/internal/pkg/api/pagination_test.go new file mode 100644 index 0000000..ba896d5 --- /dev/null +++ b/internal/pkg/api/pagination_test.go @@ -0,0 +1,22 @@ +package api + +import ( + "net/url" + "testing" + + options "github.com/breathbath/go_utils/v2/pkg/config" + "github.com/stretchr/testify/assert" +) + +func TestPaginationFromParams(t *testing.T) { + pagination := NewPaginationFromParams(options.New(options.NewMapValuesProvider(map[string]interface{}{ + "limit": 30, + "offset": 90, + }))) + + q := url.Values{} + pagination.Apply(q) + + assert.Equal(t, q.Get("page[limit]"), "30") + assert.Equal(t, q.Get("page[offset]"), "90") +} diff --git a/internal/pkg/config/load.go b/internal/pkg/config/load.go index 5dc4b99..ae78374 100644 --- a/internal/pkg/config/load.go +++ b/internal/pkg/config/load.go @@ -74,7 +74,7 @@ func (fvp *FlagValuesProvider) ToKeyValues() map[string]interface{} { func (fvp *FlagValuesProvider) Read(name string) (val interface{}, found bool) { fl := fvp.flags.Lookup(name) - if fl == nil || !fl.Changed { + if fl == nil { return nil, false } diff --git a/internal/pkg/config/load_test.go b/internal/pkg/config/load_test.go index 64b9698..47ad7e4 100644 --- a/internal/pkg/config/load_test.go +++ b/internal/pkg/config/load_test.go @@ -331,19 +331,20 @@ func TestCollectParams(t *testing.T) { func TestFlagValuesProvider(t *testing.T) { fl := &pflag.FlagSet{} - fl.StringP("somekey", "s", "", "") + fl.StringP("somekey", "s", "test-default", "") flagValuesProv := CreateFlagValuesProvider(fl) - _, found := flagValuesProv.Read("somekey") - assert.False(t, found) + val, found := flagValuesProv.Read("somekey") + assert.True(t, found) + assert.Equal(t, "test-default", val) err := fl.Parse([]string{"--somekey", "someval"}) require.NoError(t, err) - val, found2 := flagValuesProv.Read("somekey") + val2, found2 := flagValuesProv.Read("somekey") assert.True(t, found2) - assert.Equal(t, "someval", val.(string)) + assert.Equal(t, "someval", val2) actualKeyValues := flagValuesProv.ToKeyValues() assert.Equal(t, map[string]interface{}{"somekey": "someval"}, actualKeyValues) diff --git a/internal/pkg/controllers/client.go b/internal/pkg/controllers/client.go index 96fa799..59c3885 100644 --- a/internal/pkg/controllers/client.go +++ b/internal/pkg/controllers/client.go @@ -25,8 +25,8 @@ type ClientController struct { ClientRenderer ClientRenderer } -func (cc *ClientController) Clients(ctx context.Context) error { - clResp, err := cc.Rport.Clients(ctx) +func (cc *ClientController) Clients(ctx context.Context, params *options.ParameterBag) error { + clResp, err := cc.Rport.Clients(ctx, api.NewPaginationFromParams(params)) if err != nil { return err } @@ -42,7 +42,7 @@ func (cc *ClientController) Client(ctx context.Context, params *options.Paramete renderDetails := params.ReadBool("all", false) if id != "" { - clResp, err := cc.Rport.Clients(ctx) + clResp, err := cc.Rport.Clients(ctx, api.NewPaginationWithLimit(api.ClientsLimitMax)) if err != nil { return err } diff --git a/internal/pkg/controllers/client_test.go b/internal/pkg/controllers/client_test.go index 4d708b6..97c9142 100644 --- a/internal/pkg/controllers/client_test.go +++ b/internal/pkg/controllers/client_test.go @@ -82,7 +82,7 @@ func TestClientsController(t *testing.T) { ClientRenderer: &ClientRendererMock{Writer: &buf}, } - err := clController.Clients(context.Background()) + err := clController.Clients(context.Background(), options.New(nil)) assert.NoError(t, err) if err != nil { return diff --git a/internal/pkg/controllers/tunnel.go b/internal/pkg/controllers/tunnel.go index 1bc7171..7f3fdca 100644 --- a/internal/pkg/controllers/tunnel.go +++ b/internal/pkg/controllers/tunnel.go @@ -87,7 +87,7 @@ func (tc *TunnelController) Tunnels(ctx context.Context, params *options.Paramet } } else { var clResp *api.ClientsResponse - clResp, err = tc.Rport.Clients(ctx) + clResp, err = tc.Rport.Clients(ctx, api.NewPaginationFromParams(params)) if err != nil { return err } diff --git a/internal/pkg/rdp/exec_nix.go b/internal/pkg/rdp/exec_nix.go index f620c4f..45c866b 100644 --- a/internal/pkg/rdp/exec_nix.go +++ b/internal/pkg/rdp/exec_nix.go @@ -1,3 +1,4 @@ +//go:build linux // +build linux package rdp diff --git a/internal/pkg/rdp/file.go b/internal/pkg/rdp/file.go index 34b4c0d..82f4d4c 100644 --- a/internal/pkg/rdp/file.go +++ b/internal/pkg/rdp/file.go @@ -111,7 +111,7 @@ func (rfw *FileWriter) WriteRDPFile(fi models.FileInput) (filePath string, err e logrus.Debugf("will write an rdp file %s", file.Name()) - _, err = file.Write([]byte(content)) + _, err = file.WriteString(content) if err != nil { return "", err } diff --git a/internal/pkg/utils/io_nix.go b/internal/pkg/utils/io_nix.go index 4c7f328..af22bab 100644 --- a/internal/pkg/utils/io_nix.go +++ b/internal/pkg/utils/io_nix.go @@ -1,3 +1,4 @@ +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris package utils