Support command status for commands and host for results in fleetctl (#37133)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #36870 

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually
This commit is contained in:
Magnus Jensen
2025-12-12 19:26:07 -04:00
committed by GitHub
parent 1618874800
commit 7b8cb785f0
5 changed files with 136 additions and 10 deletions
+7
View File
@@ -71,3 +71,10 @@ func byMDMCommandRequestType() cli.Flag {
Usage: "Filter MDM commands by type.",
}
}
func withMDMCommandStatusFilter() cli.Flag {
return &cli.StringFlag{
Name: "command_status",
Usage: "Filter MDM commands by command status in a comma-separated list. Valid values are 'pending', 'ran', and 'failed'. ",
}
}
+21 -5
View File
@@ -1473,6 +1473,7 @@ func getMDMCommandResultsCommand() *cli.Command {
Usage: "Filter MDM commands by ID.",
Required: true,
},
byHostIdentifier(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
@@ -1485,10 +1486,10 @@ func getMDMCommandResultsCommand() *cli.Command {
return err
}
res, err := client.MDMGetCommandResults(c.String("id"))
res, err := client.MDMGetCommandResults(c.String("id"), c.String("host"))
if err != nil {
var nfe service.NotFoundErr
if errors.As(err, &nfe) {
if errors.As(err, &nfe) && c.String("host") == "" {
return errors.New("The command doesn't exist. Please provide a valid command ID. To see a list of commands that were run, run `fleetctl get mdm-commands`.")
}
@@ -1569,6 +1570,7 @@ func getMDMCommandsCommand() *cli.Command {
debugFlag(),
byHostIdentifier(),
byMDMCommandRequestType(),
withMDMCommandStatusFilter(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
@@ -1581,10 +1583,18 @@ func getMDMCommandsCommand() *cli.Command {
return err
}
commandStatuses := []fleet.MDMCommandStatusFilter{}
if c.IsSet("command_status") {
for val := range strings.SplitSeq(c.String("command_status"), ",") {
commandStatuses = append(commandStatuses, fleet.MDMCommandStatusFilter(val))
}
}
opts := fleet.MDMCommandListOptions{
Filters: fleet.MDMCommandFilters{
HostIdentifier: c.String("host"),
RequestType: c.String("type"),
HostIdentifier: c.String("host"),
RequestType: c.String("type"),
CommandStatuses: commandStatuses,
},
}
@@ -1595,7 +1605,13 @@ func getMDMCommandsCommand() *cli.Command {
}
return err
}
if len(results) == 0 && opts.Filters.HostIdentifier == "" && opts.Filters.RequestType == "" {
if len(results) == 0 {
if opts.Filters.HostIdentifier != "" {
log(c, "No MDM commands have been run on this host.\n")
return nil
}
log(c, "You haven't run any MDM commands. Run MDM commands with the `fleetctl mdm run-command` command.\n")
return nil
}
+95 -1
View File
@@ -2593,6 +2593,18 @@ func TestGetMDMCommandResults(t *testing.T) {
{ID: 2, UUID: uuids[1], Hostname: "host2"},
}, nil
}
ds.GetHostMDMIdentifiersFunc = func(ctx context.Context, identifer string, teamFilter fleet.TeamFilter) ([]*fleet.HostMDMIdentifiers, error) {
return []*fleet.HostMDMIdentifiers{
{
UUID: "device1",
HardwareSerial: "C02XXXXXXX1",
Hostname: "host1",
ID: 1,
TeamID: ptr.Uint(1),
Platform: "darwin",
},
}, nil
}
ds.GetMDMAppleCommandResultsFunc = func(ctx context.Context, commandUUID string, hostUUID string) ([]*fleet.MDMCommandResult, error) {
switch commandUUID {
case "empty-cmd":
@@ -3000,6 +3012,66 @@ RESULTS:
ds.GetMDMWindowsCommandResultsFuncInvoked = false
require.False(t, ds.GetMDMAppleCommandResultsFuncInvoked)
})
t.Run("host specific results", func(t *testing.T) {
expectedOutput := strings.TrimSpace(`
ID:
valid-cmd
TIME:
2023-04-04T15:29:00Z
TYPE:
test
STATUS:
Acknowledged
HOSTNAME:
host1
PAYLOAD:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Command</key>
<dict>
<key>ManagedOnly</key>
<false/>
<key>RequestType</key>
<string>ProfileList</string>
</dict>
<key>CommandUUID</key>
<string>0001_ProfileList</string>
</dict>
</plist>
RESULTS:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CommandUUID</key>
<string>6d7cb698-8d93-45a3-b544-71aef37d42e8</string>
<key>Status</key>
<string>Acknowledged</string>
<key>UDID</key>
<string>419D46EC-06E6-557C-AD52-601BA0667730</string>
</dict>
</plist>`)
platform = "darwin"
buf, err := RunAppNoChecks([]string{"get", "mdm-command-results", "--id", "valid-cmd", "--host", "device1"})
require.NoError(t, err)
require.Contains(t, buf.String(), expectedOutput)
require.True(t, ds.GetMDMCommandPlatformFuncInvoked)
ds.GetMDMCommandPlatformFuncInvoked = false
require.False(t, ds.GetMDMWindowsCommandResultsFuncInvoked)
require.True(t, ds.GetMDMAppleCommandResultsFuncInvoked)
ds.GetMDMAppleCommandResultsFuncInvoked = false
})
}
func TestGetMDMCommands(t *testing.T) {
@@ -3008,6 +3080,14 @@ func TestGetMDMCommands(t *testing.T) {
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
}
ds.HostLiteByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.HostLite, error) {
fmt.Println("Called", identifier)
if identifier == "foo" || identifier == "h1" {
return &fleet.HostLite{ID: 1, UUID: "h1", Hostname: "host1"}, nil
}
return nil, errors.New(fleet.HostIdentiferNotFound)
}
var empty bool
var listErr error
var noHostErr error
@@ -3029,7 +3109,7 @@ func TestGetMDMCommands(t *testing.T) {
if expectRequestType {
require.NotEmpty(t, listOpts.Filters.RequestType)
}
fmt.Println("Returning commands")
return []*fleet.MDMCommand{
{
HostUUID: "h1",
@@ -3123,6 +3203,20 @@ The list of 3 most recent commands:
_, err = RunAppNoChecks([]string{"get", "mdm-commands", "--host", "foo"})
require.Error(t, err)
require.ErrorContains(t, err, fleet.HostIdentiferNotFound)
// Empty results when using host identifier
listErr = nil
empty = true
expectIdentifier = true
noHostErr = nil
buf, err = RunAppNoChecks([]string{"get", "mdm-commands", "--host", "foo"})
require.NoError(t, err)
require.Contains(t, buf.String(), "No MDM commands have been run on this host.")
// Command status no host
_, err = RunAppNoChecks([]string{"get", "mdm-commands", "--command_status", "ran"})
require.Error(t, err)
require.ErrorContains(t, err, `"host_identifier" must be specified when filtering by "command_status"`)
}
func TestUserIsObserver(t *testing.T) {
+13 -3
View File
@@ -283,7 +283,7 @@ func (c *Client) deleteMacOSSetupAssistant(teamID *uint) error {
func (c *Client) MDMListCommands(opts fleet.MDMCommandListOptions) ([]*fleet.MDMCommand, error) {
const defaultCommandsPerPage = 20
verb, path := http.MethodGet, "/api/latest/fleet/mdm/commands"
verb, path := http.MethodGet, "/api/latest/fleet/commands"
query := url.Values{}
query.Set("per_page", fmt.Sprint(defaultCommandsPerPage))
@@ -292,6 +292,15 @@ func (c *Client) MDMListCommands(opts fleet.MDMCommandListOptions) ([]*fleet.MDM
query.Set("host_identifier", opts.Filters.HostIdentifier)
query.Set("request_type", opts.Filters.RequestType)
var statuses []string
if len(opts.Filters.CommandStatuses) > 0 {
statuses = make([]string, 0, len(opts.Filters.CommandStatuses))
for _, s := range opts.Filters.CommandStatuses {
statuses = append(statuses, string(s))
}
}
query.Set("command_status", strings.Join(statuses, ","))
var responseBody listMDMCommandsResponse
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query.Encode())
if err != nil {
@@ -301,11 +310,12 @@ func (c *Client) MDMListCommands(opts fleet.MDMCommandListOptions) ([]*fleet.MDM
return responseBody.Results, nil
}
func (c *Client) MDMGetCommandResults(commandUUID string) ([]*fleet.MDMCommandResult, error) {
verb, path := http.MethodGet, "/api/latest/fleet/mdm/commandresults"
func (c *Client) MDMGetCommandResults(commandUUID, hostIdentifier string) ([]*fleet.MDMCommandResult, error) {
verb, path := http.MethodGet, "/api/latest/fleet/commands/results"
query := url.Values{}
query.Set("command_uuid", commandUUID)
query.Set("host_identifier", hostIdentifier)
var responseBody getMDMCommandResultsResponse
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query.Encode())
-1
View File
@@ -1060,7 +1060,6 @@ func (svc *Service) ListMDMCommands(ctx context.Context, opts *fleet.MDMCommandL
return nil, nil, fleet.NewInvalidArgumentError("Invalid Host", fleet.HostIdentiferNotFound).WithStatus(http.StatusNotFound)
}
}
return results, total, nil
}