Skip to content

Commit

Permalink
Issue 1359 fleetctl team transfer (#1413)
Browse files Browse the repository at this point in the history
* wip

* Add delete user command and translator

* Add host transfer command

* Add changes file

* Undo bad refactor

* Fix copypaste error

* Implement with interfaces instead of assertions

* Ad documentation and simplify implementation further

* Update docs/1-Using-Fleet/3-REST-API.md

Co-authored-by: Zach Wasserman <[email protected]>

Co-authored-by: Zach Wasserman <[email protected]>
  • Loading branch information
chiiph and zwass authored Jul 21, 2021
1 parent 567f43d commit 484c615
Show file tree
Hide file tree
Showing 17 changed files with 703 additions and 3 deletions.
1 change: 1 addition & 0 deletions changes/issue-1359-fleetctl-host-transfer
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Add host transfer capabilities to fleetctl. Fixes issue 1359.
1 change: 1 addition & 0 deletions changes/issue-1360-fleetctl-user-delete
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Add user delete capabilities to fleetctl. Fixes issue 1360
1 change: 1 addition & 0 deletions cmd/fleetctl/fleetctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func createApp(reader io.Reader, writer io.Writer, exitErrHandler cli.ExitErrHan
debugCommand(),
previewCommand(),
eefleetctl.UpdatesCommand(),
hostsCommand(),
}
return app
}
82 changes: 82 additions & 0 deletions cmd/fleetctl/hosts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)

const (
hostsFlagName = "hosts"
labelFlagName = "label"
statusFlagName = "status"
searchQueryFlagName = "search_query"
)

func hostsCommand() *cli.Command {
return &cli.Command{
Name: "hosts",
Usage: "Manage Fleet hosts",
Subcommands: []*cli.Command{
transferCommand(),
},
}
}

func transferCommand() *cli.Command {
return &cli.Command{
Name: "transfer",
Usage: "Transfer one or more hosts to a team",
UsageText: `This command will gather the set of hosts specified and transfer them to the team.`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: teamFlagName,
Usage: "Team name hosts will be transferred to",
Required: true,
},
&cli.StringSliceFlag{
Name: hostsFlagName,
Usage: "Comma separated hostnames to transfer",
},
&cli.StringFlag{
Name: labelFlagName,
Usage: "Label name to transfer",
},
&cli.StringFlag{
Name: statusFlagName,
Usage: "Status to use when filtering hosts",
},
&cli.StringFlag{
Name: searchQueryFlagName,
Usage: "A search query that returns matching hostnames to be transferred",
},
configFlag(),
contextFlag(),
yamlFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
if err != nil {
return err
}

team := c.String(teamFlagName)
hosts := c.StringSlice(hostsFlagName)
label := c.String(labelFlagName)
status := c.String(statusFlagName)
searchQuery := c.String(searchQueryFlagName)

if hosts != nil {
if label != "" || searchQuery != "" || status != "" {
return errors.New("--hosts cannot be used along side any other flag")
}
} else {
if label == "" && searchQuery == "" && status == "" {
return errors.New("You need to define either --hosts, or one or more of --label, --status, --search_query")
}
}

return client.TransferHosts(hosts, label, status, searchQuery, team)
},
}
}
153 changes: 153 additions & 0 deletions cmd/fleetctl/hosts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package main

import (
"testing"

"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestHostTransferFlagChecks(t *testing.T) {
server, _ := runServerWithMockedDS(t)
defer server.Close()

runAppCheckErr(t,
[]string{"hosts", "transfer", "--team", "team1", "--hosts", "host1", "--label", "AAA"},
"--hosts cannot be used along side any other flag",
)
runAppCheckErr(t,
[]string{"hosts", "transfer", "--team", "team1"},
"You need to define either --hosts, or one or more of --label, --status, --search_query",
)
}

func TestHostsTransferByHosts(t *testing.T) {
server, ds := runServerWithMockedDS(t)
defer server.Close()

ds.HostByIdentifierFunc = func(identifier string) (*fleet.Host, error) {
require.Equal(t, "host1", identifier)
return &fleet.Host{ID: 42}, nil
}

ds.TeamByNameFunc = func(name string) (*fleet.Team, error) {
require.Equal(t, "team1", name)
return &fleet.Team{ID: 99, Name: "team1"}, nil
}

ds.AddHostsToTeamFunc = func(teamID *uint, hostIDs []uint) error {
require.NotNil(t, teamID)
require.Equal(t, uint(99), *teamID)
require.Equal(t, []uint{42}, hostIDs)
return nil
}

assert.Equal(t, "", runAppForTest(t, []string{"hosts", "transfer", "--team", "team1", "--hosts", "host1"}))
}

func TestHostsTransferByLabel(t *testing.T) {
server, ds := runServerWithMockedDS(t)
defer server.Close()

ds.HostByIdentifierFunc = func(identifier string) (*fleet.Host, error) {
require.Equal(t, "host1", identifier)
return &fleet.Host{ID: 42}, nil
}

ds.TeamByNameFunc = func(name string) (*fleet.Team, error) {
require.Equal(t, "team1", name)
return &fleet.Team{ID: 99, Name: "team1"}, nil
}

ds.LabelIDsByNameFunc = func(labels []string) ([]uint, error) {
require.Equal(t, []string{"label1"}, labels)
return []uint{uint(11)}, nil
}

ds.ListHostsInLabelFunc = func(filter fleet.TeamFilter, lid uint, opt fleet.HostListOptions) ([]*fleet.Host, error) {
require.Equal(t, fleet.HostStatus(""), opt.StatusFilter)
require.Equal(t, uint(11), lid)
return []*fleet.Host{{ID: 32}, {ID: 12}}, nil
}

ds.AddHostsToTeamFunc = func(teamID *uint, hostIDs []uint) error {
require.NotNil(t, teamID)
require.Equal(t, uint(99), *teamID)
require.Equal(t, []uint{32, 12}, hostIDs)
return nil
}

assert.Equal(t, "", runAppForTest(t, []string{"hosts", "transfer", "--team", "team1", "--label", "label1"}))
}

func TestHostsTransferByStatus(t *testing.T) {
server, ds := runServerWithMockedDS(t)
defer server.Close()

ds.HostByIdentifierFunc = func(identifier string) (*fleet.Host, error) {
require.Equal(t, "host1", identifier)
return &fleet.Host{ID: 42}, nil
}

ds.TeamByNameFunc = func(name string) (*fleet.Team, error) {
require.Equal(t, "team1", name)
return &fleet.Team{ID: 99, Name: "team1"}, nil
}

ds.LabelIDsByNameFunc = func(labels []string) ([]uint, error) {
require.Equal(t, []string{"label1"}, labels)
return []uint{uint(11)}, nil
}

ds.ListHostsFunc = func(filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
require.Equal(t, fleet.StatusOnline, opt.StatusFilter)
return []*fleet.Host{{ID: 32}, {ID: 12}}, nil
}

ds.AddHostsToTeamFunc = func(teamID *uint, hostIDs []uint) error {
require.NotNil(t, teamID)
require.Equal(t, uint(99), *teamID)
require.Equal(t, []uint{32, 12}, hostIDs)
return nil
}

assert.Equal(t, "", runAppForTest(t,
[]string{"hosts", "transfer", "--team", "team1", "--status", "online"}))
}

func TestHostsTransferByStatusAndSearchQuery(t *testing.T) {
server, ds := runServerWithMockedDS(t)
defer server.Close()

ds.HostByIdentifierFunc = func(identifier string) (*fleet.Host, error) {
require.Equal(t, "host1", identifier)
return &fleet.Host{ID: 42}, nil
}

ds.TeamByNameFunc = func(name string) (*fleet.Team, error) {
require.Equal(t, "team1", name)
return &fleet.Team{ID: 99, Name: "team1"}, nil
}

ds.LabelIDsByNameFunc = func(labels []string) ([]uint, error) {
require.Equal(t, []string{"label1"}, labels)
return []uint{uint(11)}, nil
}

ds.ListHostsFunc = func(filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
require.Equal(t, fleet.StatusOnline, opt.StatusFilter)
require.Equal(t, "somequery", opt.MatchQuery)
return []*fleet.Host{{ID: 32}, {ID: 12}}, nil
}

ds.AddHostsToTeamFunc = func(teamID *uint, hostIDs []uint) error {
require.NotNil(t, teamID)
require.Equal(t, uint(99), *teamID)
require.Equal(t, []uint{32, 12}, hostIDs)
return nil
}

assert.Equal(t, "", runAppForTest(t,
[]string{"hosts", "transfer", "--team", "team1", "--status", "online", "--search_query", "somequery"}))
}
17 changes: 14 additions & 3 deletions cmd/fleetctl/testing_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,25 @@ func runServerWithMockedDS(t *testing.T, opts ...service.TestServerOpts) (*httpt
}

func runAppForTest(t *testing.T, args []string) string {
w, exitErr, err := runAppNoChecks(args)
require.Nil(t, err)
require.Nil(t, exitErr)
return w.String()
}

func runAppCheckErr(t *testing.T, args []string, errorMsg string) string {
w, _, err := runAppNoChecks(args)
require.Equal(t, errorMsg, err.Error())
return w.String()
}

func runAppNoChecks(args []string) (*bytes.Buffer, error, error) {
w := new(bytes.Buffer)
r, _, _ := os.Pipe()
var exitErr error
app := createApp(r, w, func(context *cli.Context, err error) {
exitErr = err
})
err := app.Run(append([]string{""}, args...))
require.Nil(t, err)
require.Nil(t, exitErr)
return w.String()
return w, exitErr, err
}
29 changes: 29 additions & 0 deletions cmd/fleetctl/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func userCommand() *cli.Command {
Usage: "Manage Fleet users",
Subcommands: []*cli.Command{
createUserCommand(),
deleteUserCommand(),
},
}
}
Expand Down Expand Up @@ -171,3 +172,31 @@ func createUserCommand() *cli.Command {
},
}
}

func deleteUserCommand() *cli.Command {
return &cli.Command{
Name: "delete",
Usage: "Delete a user",
UsageText: `This command will delete a user specified by their email in Fleet.`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: emailFlagName,
Usage: "Email for user (required)",
Required: true,
},
configFlag(),
contextFlag(),
yamlFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
if err != nil {
return err
}

email := c.String(emailFlagName)
return client.DeleteUser(email)
},
}
}
31 changes: 31 additions & 0 deletions cmd/fleetctl/users_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import (
"testing"

"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/assert"
)

func TestUserDelete(t *testing.T) {
server, ds := runServerWithMockedDS(t)
defer server.Close()

ds.UserByEmailFunc = func(email string) (*fleet.User, error) {
return &fleet.User{
ID: 42,
Name: "test1",
Email: "[email protected]",
}, nil
}

deletedUser := uint(0)

ds.DeleteUserFunc = func(id uint) error {
deletedUser = id
return nil
}

assert.Equal(t, "", runAppForTest(t, []string{"user", "delete", "--email", "[email protected]"}))
assert.Equal(t, uint(42), deletedUser)
}
Loading

0 comments on commit 484c615

Please sign in to comment.