diff --git a/changes/16466-transfer-hosts-to-No-team b/changes/16466-transfer-hosts-to-No-team new file mode 100644 index 0000000000..d39f7e283b --- /dev/null +++ b/changes/16466-transfer-hosts-to-No-team @@ -0,0 +1 @@ +fleetctl can now transfer hosts to No team like: fleetctl hosts transfer --team '' --hosts yourHost diff --git a/cmd/fleetctl/hosts.go b/cmd/fleetctl/hosts.go index 30916b00e7..5fae97315c 100644 --- a/cmd/fleetctl/hosts.go +++ b/cmd/fleetctl/hosts.go @@ -31,7 +31,7 @@ func transferCommand() *cli.Command { Flags: []cli.Flag{ &cli.StringFlag{ Name: teamFlagName, - Usage: "Team name hosts will be transferred to", + Usage: "Team name hosts will be transferred to. Use '' for No team", Required: true, }, &cli.StringSliceFlag{ diff --git a/cmd/fleetctl/hosts_test.go b/cmd/fleetctl/hosts_test.go index 2adaea8a5d..bfedab996f 100644 --- a/cmd/fleetctl/hosts_test.go +++ b/cmd/fleetctl/hosts_test.go @@ -64,7 +64,20 @@ func TestHostsTransferByHosts(t *testing.T) { } assert.Equal(t, "", runAppForTest(t, []string{"hosts", "transfer", "--team", "team1", "--hosts", "host1"})) - require.True(t, ds.NewActivityFuncInvoked) + assert.True(t, ds.AddHostsToTeamFuncInvoked) + assert.True(t, ds.NewActivityFuncInvoked) + + // Now, transfer out of the team. + ds.AddHostsToTeamFunc = func(ctx context.Context, teamID *uint, hostIDs []uint) error { + assert.Nil(t, teamID) + assert.Equal(t, []uint{42}, hostIDs) + return nil + } + ds.NewActivityFuncInvoked = false + ds.AddHostsToTeamFuncInvoked = false + assert.Equal(t, "", runAppForTest(t, []string{"hosts", "transfer", "--team", "", "--hosts", "host1"})) + assert.True(t, ds.AddHostsToTeamFuncInvoked) + assert.True(t, ds.NewActivityFuncInvoked) } func TestHostsTransferByLabel(t *testing.T) { @@ -121,6 +134,19 @@ func TestHostsTransferByLabel(t *testing.T) { assert.Equal(t, "", runAppForTest(t, []string{"hosts", "transfer", "--team", "team1", "--label", "label1"})) require.True(t, ds.NewActivityFuncInvoked) + assert.True(t, ds.AddHostsToTeamFuncInvoked) + + // Now, transfer out of the team. + ds.AddHostsToTeamFunc = func(ctx context.Context, teamID *uint, hostIDs []uint) error { + assert.Nil(t, teamID) + require.Equal(t, []uint{32, 12}, hostIDs) + return nil + } + ds.NewActivityFuncInvoked = false + ds.AddHostsToTeamFuncInvoked = false + assert.Equal(t, "", runAppForTest(t, []string{"hosts", "transfer", "--team", "", "--label", "label1"})) + assert.True(t, ds.AddHostsToTeamFuncInvoked) + assert.True(t, ds.NewActivityFuncInvoked) } func TestHostsTransferByStatus(t *testing.T) { diff --git a/server/service/client_hosts.go b/server/service/client_hosts.go index eb818b9149..735743b8df 100644 --- a/server/service/client_hosts.go +++ b/server/service/client_hosts.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/fleetdm/fleet/v4/server/ptr" ) // GetHosts retrieves the list of all Hosts @@ -55,23 +54,28 @@ func (c *Client) translateTransferHostsToIDs(hosts []string, label string, team translatePayloads = append(translatePayloads, translatedPayload) } - translatedPayload, err := encodeTranslatedPayload(fleet.TranslatorTypeTeam, team) - if err != nil { - return nil, 0, 0, err - } - translatePayloads = append(translatePayloads, translatedPayload) - - params := translatorRequest{List: translatePayloads} - - err = c.authenticatedRequest(¶ms, verb, path, &responseBody) - if err != nil { - return nil, 0, 0, err + if team != "" { + translatedPayload, err := encodeTranslatedPayload(fleet.TranslatorTypeTeam, team) + if err != nil { + return nil, 0, 0, err + } + translatePayloads = append(translatePayloads, translatedPayload) } var hostIDs []uint var labelID uint var teamID uint + if len(translatePayloads) == 0 { + return hostIDs, labelID, teamID, nil + } + params := translatorRequest{List: translatePayloads} + + err := c.authenticatedRequest(¶ms, verb, path, &responseBody) + if err != nil { + return nil, 0, 0, err + } + for _, payload := range responseBody.List { switch payload.Type { case fleet.TranslatorTypeLabel: @@ -99,10 +103,14 @@ func (c *Client) TransferHosts(hosts []string, label string, status, searchQuery return err } + var teamIDPtr *uint + if teamID != 0 { + teamIDPtr = &teamID + } if len(hosts) != 0 { verb, path := "POST", "/api/latest/fleet/hosts/transfer" var responseBody addHostsToTeamResponse - params := addHostsToTeamRequest{TeamID: ptr.Uint(teamID), HostIDs: hostIDs} + params := addHostsToTeamRequest{TeamID: teamIDPtr, HostIDs: hostIDs} return c.authenticatedRequest(params, verb, path, &responseBody) } @@ -113,15 +121,16 @@ func (c *Client) TransferHosts(hosts []string, label string, status, searchQuery verb, path := "POST", "/api/latest/fleet/hosts/transfer/filter" var responseBody addHostsToTeamByFilterResponse - params := addHostsToTeamByFilterRequest{TeamID: ptr.Uint(teamID), Filters: struct { - MatchQuery string `json:"query"` - Status fleet.HostStatus `json:"status"` - LabelID *uint `json:"label_id"` - }{MatchQuery: searchQuery, Status: fleet.HostStatus(status), LabelID: labelIDPtr}} + params := addHostsToTeamByFilterRequest{ + TeamID: teamIDPtr, Filters: struct { + MatchQuery string `json:"query"` + Status fleet.HostStatus `json:"status"` + LabelID *uint `json:"label_id"` + }{MatchQuery: searchQuery, Status: fleet.HostStatus(status), LabelID: labelIDPtr}} return c.authenticatedRequest(params, verb, path, &responseBody) } -// GetHosts returns a report of all hosts. +// GetHostsReport returns a report of all hosts. // // The first row holds the name of the columns and each subsequent row are // the column values for each host.