Remove aggregate host counts from GET /hosts responses (#7510)

This commit is contained in:
Martin Angers 2022-09-06 10:34:06 -04:00 committed by GitHub
parent d98a53f945
commit aa0102d6b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 230 additions and 140 deletions

View File

@ -0,0 +1 @@
* Removed the `hosts_count` field from the `mobile_device_management_solution` and `munki_issue` top-level objects in the `GET /hosts` API endpoint response, as it was a pre-computed count that is updated at intervals, not a live count, and it could be confusing when it did not match the number of hosts returned.

View File

@ -1743,9 +1743,9 @@ If `additional_info_filters` is not specified, no `additional` information will
If `software_id` is specified, an additional top-level key `"software"` is returned with the software object corresponding to the `software_id`. See [List all software](#list-all-software) response payload for details about this object. If `software_id` is specified, an additional top-level key `"software"` is returned with the software object corresponding to the `software_id`. See [List all software](#list-all-software) response payload for details about this object.
If `mdm_id` is specified, an additional top-level key `"mobile_device_management_solution"` is returned with the aggregated statistics corresponding to the `mdm_id` (and, if provided, `team_id`). See [Get aggregated host's mobile device management (MDM) and Munki information](#get-aggregated-hosts-mobile-device-management-mdm-and-munki-information) response payload for details about this object. Note that the statistics are for the corresponding `mdm_id` (and `team_id` if provided) only, and do not take into account other potential filters such as `mdm_enrollment_status`. If `mdm_id` is specified, an additional top-level key `"mobile_device_management_solution"` is returned with the information corresponding to the `mdm_id`.
If `munki_issue_id` is specified, an additional top-level key `"munki_issue"` is returned with the aggregated statistics corresponding to the `munki_issue_id` (and, if provided, `team_id`). See [Get aggregated host's mobile device management (MDM) and Munki information](#get-aggregated-hosts-mobile-device-management-mdm-and-munki-information) response payload for details about this object. If `munki_issue_id` is specified, an additional top-level key `"munki_issue"` is returned with the information corresponding to the `munki_issue_id`.
#### Example #### Example
@ -1826,6 +1826,32 @@ If `munki_issue_id` is specified, an additional top-level key `"munki_issue"` is
> Note: the response above assumes a [GeoIP database is configured](https://fleetdm.com/docs/deploying/configuration#geo-ip), otherwise the `geolocation` object won't be included. > Note: the response above assumes a [GeoIP database is configured](https://fleetdm.com/docs/deploying/configuration#geo-ip), otherwise the `geolocation` object won't be included.
Response payload with the `mdm_id` filter provided:
```json
{
"hosts": [...],
"mobile_device_management_solution": {
"server_url": "http://some.url/mdm",
"name": "MDM Vendor Name",
"id": 999
}
}
```
Response payload with the `munki_issue_id` filter provided:
```json
{
"hosts": [...],
"munki_issue": {
"id": 1,
"name": "Could not retrieve managed install primary manifest",
"type": "error"
}
}
```
### Count hosts ### Count hosts
`GET /api/v1/fleet/hosts/count` `GET /api/v1/fleet/hosts/count`

View File

@ -1670,7 +1670,7 @@ func (ds *Datastore) getOrInsertMDMSolution(ctx context.Context, serverURL strin
return ds.optimisticGetOrInsert(ctx, readStmt, insStmt) return ds.optimisticGetOrInsert(ctx, readStmt, insStmt)
} }
func (ds *Datastore) GetMunkiVersion(ctx context.Context, hostID uint) (string, error) { func (ds *Datastore) GetHostMunkiVersion(ctx context.Context, hostID uint) (string, error) {
var version string var version string
err := sqlx.GetContext(ctx, ds.reader, &version, `SELECT version FROM host_munki_info WHERE deleted_at is NULL AND host_id = ?`, hostID) err := sqlx.GetContext(ctx, ds.reader, &version, `SELECT version FROM host_munki_info WHERE deleted_at is NULL AND host_id = ?`, hostID)
if err != nil { if err != nil {
@ -1683,7 +1683,7 @@ func (ds *Datastore) GetMunkiVersion(ctx context.Context, hostID uint) (string,
return version, nil return version, nil
} }
func (ds *Datastore) GetMDM(ctx context.Context, hostID uint) (*fleet.HostMDM, error) { func (ds *Datastore) GetHostMDM(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
var hmdm fleet.HostMDM var hmdm fleet.HostMDM
err := sqlx.GetContext(ctx, ds.reader, &hmdm, ` err := sqlx.GetContext(ctx, ds.reader, &hmdm, `
SELECT SELECT
@ -1703,7 +1703,26 @@ func (ds *Datastore) GetMDM(ctx context.Context, hostID uint) (*fleet.HostMDM, e
return &hmdm, nil return &hmdm, nil
} }
func (ds *Datastore) GetMunkiIssues(ctx context.Context, hostID uint) ([]*fleet.HostMunkiIssue, error) { func (ds *Datastore) GetMDMSolution(ctx context.Context, mdmID uint) (*fleet.MDMSolution, error) {
var solution fleet.MDMSolution
err := sqlx.GetContext(ctx, ds.reader, &solution, `
SELECT
id,
name,
server_url
FROM
mobile_device_management_solutions
WHERE id = ?`, mdmID)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("MDMSolution").WithID(mdmID))
}
return nil, ctxerr.Wrapf(ctx, err, "select mobile_device_management_solutions for id %d", mdmID)
}
return &solution, nil
}
func (ds *Datastore) GetHostMunkiIssues(ctx context.Context, hostID uint) ([]*fleet.HostMunkiIssue, error) {
var issues []*fleet.HostMunkiIssue var issues []*fleet.HostMunkiIssue
err := sqlx.SelectContext(ctx, ds.reader, &issues, ` err := sqlx.SelectContext(ctx, ds.reader, &issues, `
SELECT SELECT
@ -1725,6 +1744,25 @@ func (ds *Datastore) GetMunkiIssues(ctx context.Context, hostID uint) ([]*fleet.
return issues, nil return issues, nil
} }
func (ds *Datastore) GetMunkiIssue(ctx context.Context, munkiIssueID uint) (*fleet.MunkiIssue, error) {
var issue fleet.MunkiIssue
err := sqlx.GetContext(ctx, ds.reader, &issue, `
SELECT
id,
name,
issue_type
FROM
munki_issues
WHERE id = ?`, munkiIssueID)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("MunkiIssue").WithID(munkiIssueID))
}
return nil, ctxerr.Wrapf(ctx, err, "select munki_issues for id %d", munkiIssueID)
}
return &issue, nil
}
func (ds *Datastore) AggregatedMunkiVersion(ctx context.Context, teamID *uint) ([]fleet.AggregatedMunkiVersion, time.Time, error) { func (ds *Datastore) AggregatedMunkiVersion(ctx context.Context, teamID *uint) ([]fleet.AggregatedMunkiVersion, time.Time, error) {
id := uint(0) id := uint(0)

View File

@ -948,7 +948,7 @@ func testHostsListMunkiIssueID(t *testing.T, ds *Datastore) {
err = ds.SetOrUpdateMunkiInfo(ctx, hostIDs[0], "1.0.0", []string{strings.Repeat("Z", maxMunkiIssueNameLen)}, []string{strings.Repeat("💞", maxMunkiIssueNameLen)}) err = ds.SetOrUpdateMunkiInfo(ctx, hostIDs[0], "1.0.0", []string{strings.Repeat("Z", maxMunkiIssueNameLen)}, []string{strings.Repeat("💞", maxMunkiIssueNameLen)})
require.NoError(t, err) require.NoError(t, err)
issues, err := ds.GetMunkiIssues(ctx, hostIDs[0]) issues, err := ds.GetHostMunkiIssues(ctx, hostIDs[0])
require.NoError(t, err) require.NoError(t, err)
require.Len(t, issues, 2) require.Len(t, issues, 2)
names := []string{issues[0].Name, issues[1].Name} names := []string{issues[0].Name, issues[1].Name}
@ -961,7 +961,7 @@ func testHostsListMunkiIssueID(t *testing.T, ds *Datastore) {
err = ds.SetOrUpdateMunkiInfo(ctx, hostIDs[0], "1.0.0", []string{strings.Repeat("A", maxMunkiIssueNameLen+1)}, []string{strings.Repeat("☺", maxMunkiIssueNameLen+1)}) err = ds.SetOrUpdateMunkiInfo(ctx, hostIDs[0], "1.0.0", []string{strings.Repeat("A", maxMunkiIssueNameLen+1)}, []string{strings.Repeat("☺", maxMunkiIssueNameLen+1)})
require.NoError(t, err) require.NoError(t, err)
issues, err = ds.GetMunkiIssues(ctx, hostIDs[0]) issues, err = ds.GetHostMunkiIssues(ctx, hostIDs[0])
require.NoError(t, err) require.NoError(t, err)
require.Len(t, issues, 2) require.Len(t, issues, 2)
names = []string{issues[0].Name, issues[1].Name} names = []string{issues[0].Name, issues[1].Name}
@ -3795,26 +3795,40 @@ func assertHostDeviceMapping(t *testing.T, got, want []*fleet.HostDeviceMapping)
} }
func testHostMDMAndMunki(t *testing.T, ds *Datastore) { func testHostMDMAndMunki(t *testing.T, ds *Datastore) {
_, err := ds.GetMunkiVersion(context.Background(), 123) _, err := ds.GetHostMunkiVersion(context.Background(), 123)
require.True(t, fleet.IsNotFound(err)) require.True(t, fleet.IsNotFound(err))
require.NoError(t, ds.SetOrUpdateMunkiInfo(context.Background(), 123, "1.2.3", nil, nil)) require.NoError(t, ds.SetOrUpdateMunkiInfo(context.Background(), 123, "1.2.3", nil, nil))
require.NoError(t, ds.SetOrUpdateMunkiInfo(context.Background(), 999, "9.0", nil, nil)) require.NoError(t, ds.SetOrUpdateMunkiInfo(context.Background(), 999, "9.0", nil, nil))
require.NoError(t, ds.SetOrUpdateMunkiInfo(context.Background(), 123, "1.3.0", []string{"a", "b"}, []string{"c"})) require.NoError(t, ds.SetOrUpdateMunkiInfo(context.Background(), 123, "1.3.0", []string{"a", "b"}, []string{"c"}))
version, err := ds.GetMunkiVersion(context.Background(), 123) version, err := ds.GetHostMunkiVersion(context.Background(), 123)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "1.3.0", version) require.Equal(t, "1.3.0", version)
issues, err := ds.GetMunkiIssues(context.Background(), 123) issues, err := ds.GetHostMunkiIssues(context.Background(), 123)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, issues, 3) require.Len(t, issues, 3)
var aMunkiIssueID uint
for _, iss := range issues { for _, iss := range issues {
assert.NotZero(t, iss.MunkiIssueID) assert.NotZero(t, iss.MunkiIssueID)
if iss.Name == "a" {
aMunkiIssueID = iss.MunkiIssueID
}
assert.False(t, iss.HostIssueCreatedAt.IsZero()) assert.False(t, iss.HostIssueCreatedAt.IsZero())
} }
// get a Munki Issue
miss, err := ds.GetMunkiIssue(context.Background(), aMunkiIssueID)
require.NoError(t, err)
require.Equal(t, "a", miss.Name)
// get an invalid munki issue
_, err = ds.GetMunkiIssue(context.Background(), aMunkiIssueID+1000)
require.Error(t, err)
require.ErrorIs(t, err, sql.ErrNoRows)
// ignore IDs and timestamps in slice comparison // ignore IDs and timestamps in slice comparison
issues[0].MunkiIssueID, issues[0].HostIssueCreatedAt = 0, time.Time{} issues[0].MunkiIssueID, issues[0].HostIssueCreatedAt = 0, time.Time{}
issues[1].MunkiIssueID, issues[1].HostIssueCreatedAt = 0, time.Time{} issues[1].MunkiIssueID, issues[1].HostIssueCreatedAt = 0, time.Time{}
@ -3825,29 +3839,29 @@ func testHostMDMAndMunki(t *testing.T, ds *Datastore) {
{Name: "c", IssueType: "warning"}, {Name: "c", IssueType: "warning"},
}, issues) }, issues)
version, err = ds.GetMunkiVersion(context.Background(), 999) version, err = ds.GetHostMunkiVersion(context.Background(), 999)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "9.0", version) require.Equal(t, "9.0", version)
issues, err = ds.GetMunkiIssues(context.Background(), 999) issues, err = ds.GetHostMunkiIssues(context.Background(), 999)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, issues, 0) require.Len(t, issues, 0)
// simulate uninstall // simulate uninstall
require.NoError(t, ds.SetOrUpdateMunkiInfo(context.Background(), 123, "", nil, nil)) require.NoError(t, ds.SetOrUpdateMunkiInfo(context.Background(), 123, "", nil, nil))
_, err = ds.GetMunkiVersion(context.Background(), 123) _, err = ds.GetHostMunkiVersion(context.Background(), 123)
require.True(t, fleet.IsNotFound(err)) require.True(t, fleet.IsNotFound(err))
issues, err = ds.GetMunkiIssues(context.Background(), 123) issues, err = ds.GetHostMunkiIssues(context.Background(), 123)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, issues, 0) require.Len(t, issues, 0)
_, err = ds.GetMDM(context.Background(), 432) _, err = ds.GetHostMDM(context.Background(), 432)
require.True(t, fleet.IsNotFound(err), err) require.True(t, fleet.IsNotFound(err), err)
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 432, true, "url", false)) require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 432, true, "url", false))
hmdm, err := ds.GetMDM(context.Background(), 432) hmdm, err := ds.GetHostMDM(context.Background(), 432)
require.NoError(t, err) require.NoError(t, err)
assert.True(t, hmdm.Enrolled) assert.True(t, hmdm.Enrolled)
assert.Equal(t, "url", hmdm.ServerURL) assert.Equal(t, "url", hmdm.ServerURL)
@ -3860,7 +3874,7 @@ func testHostMDMAndMunki(t *testing.T, ds *Datastore) {
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 455, true, "https://kandji.io", true)) // kandji mdm name require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 455, true, "https://kandji.io", true)) // kandji mdm name
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 432, false, "url3", true)) require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 432, false, "url3", true))
hmdm, err = ds.GetMDM(context.Background(), 432) hmdm, err = ds.GetHostMDM(context.Background(), 432)
require.NoError(t, err) require.NoError(t, err)
assert.False(t, hmdm.Enrolled) assert.False(t, hmdm.Enrolled)
assert.Equal(t, "url3", hmdm.ServerURL) assert.Equal(t, "url3", hmdm.ServerURL)
@ -3870,7 +3884,7 @@ func testHostMDMAndMunki(t *testing.T, ds *Datastore) {
assert.NotEqual(t, urlMDMID, *hmdm.MDMID) assert.NotEqual(t, urlMDMID, *hmdm.MDMID)
assert.Equal(t, fleet.UnknownMDMName, hmdm.Name) assert.Equal(t, fleet.UnknownMDMName, hmdm.Name)
hmdm, err = ds.GetMDM(context.Background(), 455) hmdm, err = ds.GetHostMDM(context.Background(), 455)
require.NoError(t, err) require.NoError(t, err)
assert.True(t, hmdm.Enrolled) assert.True(t, hmdm.Enrolled)
assert.Equal(t, "https://kandji.io", hmdm.ServerURL) assert.Equal(t, "https://kandji.io", hmdm.ServerURL)
@ -3880,10 +3894,21 @@ func testHostMDMAndMunki(t *testing.T, ds *Datastore) {
kandjiID1 := *hmdm.MDMID kandjiID1 := *hmdm.MDMID
assert.Equal(t, fleet.WellKnownMDMKandji, hmdm.Name) assert.Equal(t, fleet.WellKnownMDMKandji, hmdm.Name)
// get mdm solution
mdmSol, err := ds.GetMDMSolution(context.Background(), kandjiID1)
require.NoError(t, err)
require.Equal(t, "https://kandji.io", mdmSol.ServerURL)
require.Equal(t, fleet.WellKnownMDMKandji, mdmSol.Name)
// get unknown mdm solution
_, err = ds.GetMDMSolution(context.Background(), kandjiID1+1000)
require.Error(t, err)
require.ErrorIs(t, err, sql.ErrNoRows)
// switch to simplemdm in an update // switch to simplemdm in an update
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 455, true, "https://simplemdm.com", false)) // now simplemdm name require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 455, true, "https://simplemdm.com", false)) // now simplemdm name
hmdm, err = ds.GetMDM(context.Background(), 455) hmdm, err = ds.GetHostMDM(context.Background(), 455)
require.NoError(t, err) require.NoError(t, err)
assert.True(t, hmdm.Enrolled) assert.True(t, hmdm.Enrolled)
assert.Equal(t, "https://simplemdm.com", hmdm.ServerURL) assert.Equal(t, "https://simplemdm.com", hmdm.ServerURL)
@ -3895,7 +3920,7 @@ func testHostMDMAndMunki(t *testing.T, ds *Datastore) {
// switch back to "url" // switch back to "url"
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 455, false, "url", false)) require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 455, false, "url", false))
hmdm, err = ds.GetMDM(context.Background(), 455) hmdm, err = ds.GetHostMDM(context.Background(), 455)
require.NoError(t, err) require.NoError(t, err)
assert.False(t, hmdm.Enrolled) assert.False(t, hmdm.Enrolled)
assert.Equal(t, "url", hmdm.ServerURL) assert.Equal(t, "url", hmdm.ServerURL)
@ -3908,7 +3933,7 @@ func testHostMDMAndMunki(t *testing.T, ds *Datastore) {
// even though this is another Kandji, the URL is different. // even though this is another Kandji, the URL is different.
require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 455, true, "https://kandji.io/2", false)) require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), 455, true, "https://kandji.io/2", false))
hmdm, err = ds.GetMDM(context.Background(), 455) hmdm, err = ds.GetHostMDM(context.Background(), 455)
require.NoError(t, err) require.NoError(t, err)
assert.True(t, hmdm.Enrolled) assert.True(t, hmdm.Enrolled)
assert.Equal(t, "https://kandji.io/2", hmdm.ServerURL) assert.Equal(t, "https://kandji.io/2", hmdm.ServerURL)
@ -3961,7 +3986,7 @@ func testMunkiIssuesBatchSize(t *testing.T, ds *Datastore) {
// try those errors/warning with some hosts // try those errors/warning with some hosts
require.NoError(t, ds.SetOrUpdateMunkiInfo(context.Background(), 123, "1.2.3", []string{"a", "b"}, []string{"C"})) require.NoError(t, ds.SetOrUpdateMunkiInfo(context.Background(), 123, "1.2.3", []string{"a", "b"}, []string{"C"}))
issues, err := ds.GetMunkiIssues(ctx, 123) issues, err := ds.GetHostMunkiIssues(ctx, 123)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, issues, 3) require.Len(t, issues, 3)
for _, iss := range issues { for _, iss := range issues {
@ -3969,7 +3994,7 @@ func testMunkiIssuesBatchSize(t *testing.T, ds *Datastore) {
} }
require.NoError(t, ds.SetOrUpdateMunkiInfo(context.Background(), 123, "1.2.3", []string{"c", "z"}, []string{"D", "E", "Z"})) require.NoError(t, ds.SetOrUpdateMunkiInfo(context.Background(), 123, "1.2.3", []string{"c", "z"}, []string{"D", "E", "Z"}))
issues, err = ds.GetMunkiIssues(ctx, 123) issues, err = ds.GetHostMunkiIssues(ctx, 123)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, issues, 5) require.Len(t, issues, 5)
for _, iss := range issues { for _, iss := range issues {
@ -4054,18 +4079,24 @@ func testAggregatedHostMDMAndMunki(t *testing.T, ds *Datastore) {
issues[2].ID = 0 issues[2].ID = 0
assert.ElementsMatch(t, issues, []fleet.AggregatedMunkiIssue{ assert.ElementsMatch(t, issues, []fleet.AggregatedMunkiIssue{
{ {
Name: "a", MunkiIssue: fleet.MunkiIssue{
IssueType: "error", Name: "a",
IssueType: "error",
},
HostsCount: 2, HostsCount: 2,
}, },
{ {
Name: "b", MunkiIssue: fleet.MunkiIssue{
IssueType: "error", Name: "b",
IssueType: "error",
},
HostsCount: 1, HostsCount: 1,
}, },
{ {
Name: "c", MunkiIssue: fleet.MunkiIssue{
IssueType: "warning", Name: "c",
IssueType: "warning",
},
HostsCount: 2, HostsCount: 2,
}, },
}) })
@ -4157,13 +4188,17 @@ func testAggregatedHostMDMAndMunki(t *testing.T, ds *Datastore) {
issues[1].ID = 0 issues[1].ID = 0
assert.ElementsMatch(t, issues, []fleet.AggregatedMunkiIssue{ assert.ElementsMatch(t, issues, []fleet.AggregatedMunkiIssue{
{ {
Name: "d", MunkiIssue: fleet.MunkiIssue{
IssueType: "error", Name: "d",
IssueType: "error",
},
HostsCount: 2, HostsCount: 2,
}, },
{ {
Name: "f", MunkiIssue: fleet.MunkiIssue{
IssueType: "warning", Name: "f",
IssueType: "warning",
},
HostsCount: 1, HostsCount: 1,
}, },
}) })

View File

@ -240,9 +240,9 @@ type Datastore interface {
// ListPoliciesForHost lists the policies that a host will check and whether they are passing // ListPoliciesForHost lists the policies that a host will check and whether they are passing
ListPoliciesForHost(ctx context.Context, host *Host) ([]*HostPolicy, error) ListPoliciesForHost(ctx context.Context, host *Host) ([]*HostPolicy, error)
GetMunkiVersion(ctx context.Context, hostID uint) (string, error) GetHostMunkiVersion(ctx context.Context, hostID uint) (string, error)
GetMunkiIssues(ctx context.Context, hostID uint) ([]*HostMunkiIssue, error) GetHostMunkiIssues(ctx context.Context, hostID uint) ([]*HostMunkiIssue, error)
GetMDM(ctx context.Context, hostID uint) (*HostMDM, error) GetHostMDM(ctx context.Context, hostID uint) (*HostMDM, error)
AggregatedMunkiVersion(ctx context.Context, teamID *uint) ([]AggregatedMunkiVersion, time.Time, error) AggregatedMunkiVersion(ctx context.Context, teamID *uint) ([]AggregatedMunkiVersion, time.Time, error)
AggregatedMunkiIssues(ctx context.Context, teamID *uint) ([]AggregatedMunkiIssue, time.Time, error) AggregatedMunkiIssues(ctx context.Context, teamID *uint) ([]AggregatedMunkiIssue, time.Time, error)
@ -250,6 +250,9 @@ type Datastore interface {
AggregatedMDMSolutions(ctx context.Context, teamID *uint) ([]AggregatedMDMSolutions, time.Time, error) AggregatedMDMSolutions(ctx context.Context, teamID *uint) ([]AggregatedMDMSolutions, time.Time, error)
GenerateAggregatedMunkiAndMDM(ctx context.Context) error GenerateAggregatedMunkiAndMDM(ctx context.Context) error
GetMunkiIssue(ctx context.Context, munkiIssueID uint) (*MunkiIssue, error)
GetMDMSolution(ctx context.Context, mdmID uint) (*MDMSolution, error)
OSVersions(ctx context.Context, teamID *uint, platform *string, name *string, version *string) (*OSVersions, error) OSVersions(ctx context.Context, teamID *uint, platform *string, name *string, version *string) (*OSVersions, error)
UpdateOSVersions(ctx context.Context) error UpdateOSVersions(ctx context.Context) error

View File

@ -422,11 +422,17 @@ type AggregatedMunkiVersion struct {
HostsCount int `json:"hosts_count" db:"hosts_count"` HostsCount int `json:"hosts_count" db:"hosts_count"`
} }
// MunkiIssue represents a single munki issue, as returned by the list hosts
// endpoint when a muniki issue ID is provided as filter.
type MunkiIssue struct {
ID uint `json:"id" db:"id"`
Name string `json:"name" db:"name"`
IssueType string `json:"type" db:"issue_type"`
}
type AggregatedMunkiIssue struct { type AggregatedMunkiIssue struct {
ID uint `json:"id" db:"id"` MunkiIssue
Name string `json:"name" db:"name"` HostsCount int `json:"hosts_count" db:"hosts_count"`
IssueType string `json:"type" db:"issue_type"`
HostsCount int `json:"hosts_count" db:"hosts_count"`
} }
type AggregatedMDMStatus struct { type AggregatedMDMStatus struct {
@ -436,11 +442,17 @@ type AggregatedMDMStatus struct {
HostsCount int `json:"hosts_count" db:"hosts_count"` HostsCount int `json:"hosts_count" db:"hosts_count"`
} }
// MDMSolution represents a single MDM solution, as returned by the list hosts
// endpoint when an MDM Solution ID is provided as filter.
type MDMSolution struct {
ID uint `json:"id" db:"id"`
Name string `json:"name" db:"name"`
ServerURL string `json:"server_url" db:"server_url"`
}
type AggregatedMDMSolutions struct { type AggregatedMDMSolutions struct {
ID uint `json:"id,omitempty" db:"id"` MDMSolution
Name string `json:"name,omitempty" db:"name"` HostsCount int `json:"hosts_count" db:"hosts_count"`
HostsCount int `json:"hosts_count" db:"hosts_count"`
ServerURL string `json:"server_url" db:"server_url"`
} }
type AggregatedMacadminsData struct { type AggregatedMacadminsData struct {

View File

@ -298,8 +298,8 @@ type Service interface {
MacadminsData(ctx context.Context, id uint) (*MacadminsData, error) MacadminsData(ctx context.Context, id uint) (*MacadminsData, error)
AggregatedMacadminsData(ctx context.Context, teamID *uint) (*AggregatedMacadminsData, error) AggregatedMacadminsData(ctx context.Context, teamID *uint) (*AggregatedMacadminsData, error)
AggregatedMDMSolutions(ctx context.Context, teamID *uint, mdmID uint) (*AggregatedMDMSolutions, error) GetMDMSolution(ctx context.Context, mdmID uint) (*MDMSolution, error)
AggregatedMunkiIssue(ctx context.Context, teamID *uint, munkiIssueID uint) (*AggregatedMunkiIssue, error) GetMunkiIssue(ctx context.Context, munkiIssueID uint) (*MunkiIssue, error)
// OSVersions returns a list of operating systems and associated host counts, which may be // OSVersions returns a list of operating systems and associated host counts, which may be
// filtered using the following optional criteria: team id, platform, or name and version. // filtered using the following optional criteria: team id, platform, or name and version.

View File

@ -189,11 +189,11 @@ type SetOrUpdateDeviceAuthTokenFunc func(ctx context.Context, hostID uint, authT
type ListPoliciesForHostFunc func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) type ListPoliciesForHostFunc func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error)
type GetMunkiVersionFunc func(ctx context.Context, hostID uint) (string, error) type GetHostMunkiVersionFunc func(ctx context.Context, hostID uint) (string, error)
type GetMunkiIssuesFunc func(ctx context.Context, hostID uint) ([]*fleet.HostMunkiIssue, error) type GetHostMunkiIssuesFunc func(ctx context.Context, hostID uint) ([]*fleet.HostMunkiIssue, error)
type GetMDMFunc func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) type GetHostMDMFunc func(ctx context.Context, hostID uint) (*fleet.HostMDM, error)
type AggregatedMunkiVersionFunc func(ctx context.Context, teamID *uint) ([]fleet.AggregatedMunkiVersion, time.Time, error) type AggregatedMunkiVersionFunc func(ctx context.Context, teamID *uint) ([]fleet.AggregatedMunkiVersion, time.Time, error)
@ -205,6 +205,10 @@ type AggregatedMDMSolutionsFunc func(ctx context.Context, teamID *uint) ([]fleet
type GenerateAggregatedMunkiAndMDMFunc func(ctx context.Context) error type GenerateAggregatedMunkiAndMDMFunc func(ctx context.Context) error
type GetMunkiIssueFunc func(ctx context.Context, munkiIssueID uint) (*fleet.MunkiIssue, error)
type GetMDMSolutionFunc func(ctx context.Context, mdmID uint) (*fleet.MDMSolution, error)
type OSVersionsFunc func(ctx context.Context, teamID *uint, platform *string, name *string, version *string) (*fleet.OSVersions, error) type OSVersionsFunc func(ctx context.Context, teamID *uint, platform *string, name *string, version *string) (*fleet.OSVersions, error)
type UpdateOSVersionsFunc func(ctx context.Context) error type UpdateOSVersionsFunc func(ctx context.Context) error
@ -708,14 +712,14 @@ type DataStore struct {
ListPoliciesForHostFunc ListPoliciesForHostFunc ListPoliciesForHostFunc ListPoliciesForHostFunc
ListPoliciesForHostFuncInvoked bool ListPoliciesForHostFuncInvoked bool
GetMunkiVersionFunc GetMunkiVersionFunc GetHostMunkiVersionFunc GetHostMunkiVersionFunc
GetMunkiVersionFuncInvoked bool GetHostMunkiVersionFuncInvoked bool
GetMunkiIssuesFunc GetMunkiIssuesFunc GetHostMunkiIssuesFunc GetHostMunkiIssuesFunc
GetMunkiIssuesFuncInvoked bool GetHostMunkiIssuesFuncInvoked bool
GetMDMFunc GetMDMFunc GetHostMDMFunc GetHostMDMFunc
GetMDMFuncInvoked bool GetHostMDMFuncInvoked bool
AggregatedMunkiVersionFunc AggregatedMunkiVersionFunc AggregatedMunkiVersionFunc AggregatedMunkiVersionFunc
AggregatedMunkiVersionFuncInvoked bool AggregatedMunkiVersionFuncInvoked bool
@ -732,6 +736,12 @@ type DataStore struct {
GenerateAggregatedMunkiAndMDMFunc GenerateAggregatedMunkiAndMDMFunc GenerateAggregatedMunkiAndMDMFunc GenerateAggregatedMunkiAndMDMFunc
GenerateAggregatedMunkiAndMDMFuncInvoked bool GenerateAggregatedMunkiAndMDMFuncInvoked bool
GetMunkiIssueFunc GetMunkiIssueFunc
GetMunkiIssueFuncInvoked bool
GetMDMSolutionFunc GetMDMSolutionFunc
GetMDMSolutionFuncInvoked bool
OSVersionsFunc OSVersionsFunc OSVersionsFunc OSVersionsFunc
OSVersionsFuncInvoked bool OSVersionsFuncInvoked bool
@ -1530,19 +1540,19 @@ func (s *DataStore) ListPoliciesForHost(ctx context.Context, host *fleet.Host) (
return s.ListPoliciesForHostFunc(ctx, host) return s.ListPoliciesForHostFunc(ctx, host)
} }
func (s *DataStore) GetMunkiVersion(ctx context.Context, hostID uint) (string, error) { func (s *DataStore) GetHostMunkiVersion(ctx context.Context, hostID uint) (string, error) {
s.GetMunkiVersionFuncInvoked = true s.GetHostMunkiVersionFuncInvoked = true
return s.GetMunkiVersionFunc(ctx, hostID) return s.GetHostMunkiVersionFunc(ctx, hostID)
} }
func (s *DataStore) GetMunkiIssues(ctx context.Context, hostID uint) ([]*fleet.HostMunkiIssue, error) { func (s *DataStore) GetHostMunkiIssues(ctx context.Context, hostID uint) ([]*fleet.HostMunkiIssue, error) {
s.GetMunkiIssuesFuncInvoked = true s.GetHostMunkiIssuesFuncInvoked = true
return s.GetMunkiIssuesFunc(ctx, hostID) return s.GetHostMunkiIssuesFunc(ctx, hostID)
} }
func (s *DataStore) GetMDM(ctx context.Context, hostID uint) (*fleet.HostMDM, error) { func (s *DataStore) GetHostMDM(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
s.GetMDMFuncInvoked = true s.GetHostMDMFuncInvoked = true
return s.GetMDMFunc(ctx, hostID) return s.GetHostMDMFunc(ctx, hostID)
} }
func (s *DataStore) AggregatedMunkiVersion(ctx context.Context, teamID *uint) ([]fleet.AggregatedMunkiVersion, time.Time, error) { func (s *DataStore) AggregatedMunkiVersion(ctx context.Context, teamID *uint) ([]fleet.AggregatedMunkiVersion, time.Time, error) {
@ -1570,6 +1580,16 @@ func (s *DataStore) GenerateAggregatedMunkiAndMDM(ctx context.Context) error {
return s.GenerateAggregatedMunkiAndMDMFunc(ctx) return s.GenerateAggregatedMunkiAndMDMFunc(ctx)
} }
func (s *DataStore) GetMunkiIssue(ctx context.Context, munkiIssueID uint) (*fleet.MunkiIssue, error) {
s.GetMunkiIssueFuncInvoked = true
return s.GetMunkiIssueFunc(ctx, munkiIssueID)
}
func (s *DataStore) GetMDMSolution(ctx context.Context, mdmID uint) (*fleet.MDMSolution, error) {
s.GetMDMSolutionFuncInvoked = true
return s.GetMDMSolutionFunc(ctx, mdmID)
}
func (s *DataStore) OSVersions(ctx context.Context, teamID *uint, platform *string, name *string, version *string) (*fleet.OSVersions, error) { func (s *DataStore) OSVersions(ctx context.Context, teamID *uint, platform *string, name *string, version *string) (*fleet.OSVersions, error) {
s.OSVersionsFuncInvoked = true s.OSVersionsFuncInvoked = true
return s.OSVersionsFunc(ctx, teamID, platform, name, version) return s.OSVersionsFunc(ctx, teamID, platform, name, version)

View File

@ -72,12 +72,12 @@ type listHostsResponse struct {
// MDMSolution is populated with the MDM solution corresponding to the mdm_id // MDMSolution is populated with the MDM solution corresponding to the mdm_id
// filter if one is provided with the request (and it exists in the // filter if one is provided with the request (and it exists in the
// database). It is nil otherwise and absent of the JSON response payload. // database). It is nil otherwise and absent of the JSON response payload.
MDMSolution *fleet.AggregatedMDMSolutions `json:"mobile_device_management_solution,omitempty"` MDMSolution *fleet.MDMSolution `json:"mobile_device_management_solution,omitempty"`
// MunkiIssue is populated with the munki issue corresponding to the // MunkiIssue is populated with the munki issue corresponding to the
// munki_issue_id filter if one is provided with the request (and it exists // munki_issue_id filter if one is provided with the request (and it exists
// in the database). It is nil otherwise and absent of the JSON response // in the database). It is nil otherwise and absent of the JSON response
// payload. // payload.
MunkiIssue *fleet.AggregatedMunkiIssue `json:"munki_issue,omitempty"` MunkiIssue *fleet.MunkiIssue `json:"munki_issue,omitempty"`
Err error `json:"error,omitempty"` Err error `json:"error,omitempty"`
} }
@ -96,20 +96,20 @@ func listHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Servi
} }
} }
var mdmSolution *fleet.AggregatedMDMSolutions var mdmSolution *fleet.MDMSolution
if req.Opts.MDMIDFilter != nil { if req.Opts.MDMIDFilter != nil {
var err error var err error
mdmSolution, err = svc.AggregatedMDMSolutions(ctx, req.Opts.TeamFilter, *req.Opts.MDMIDFilter) mdmSolution, err = svc.GetMDMSolution(ctx, *req.Opts.MDMIDFilter)
if err != nil { if err != nil && !fleet.IsNotFound(err) { // ignore not found, just return nil for the MDM solution in that case
return listHostsResponse{Err: err}, nil return listHostsResponse{Err: err}, nil
} }
} }
var munkiIssue *fleet.AggregatedMunkiIssue var munkiIssue *fleet.MunkiIssue
if req.Opts.MunkiIssueIDFilter != nil { if req.Opts.MunkiIssueIDFilter != nil {
var err error var err error
munkiIssue, err = svc.AggregatedMunkiIssue(ctx, req.Opts.TeamFilter, *req.Opts.MunkiIssueIDFilter) munkiIssue, err = svc.GetMunkiIssue(ctx, *req.Opts.MunkiIssueIDFilter)
if err != nil { if err != nil && !fleet.IsNotFound(err) { // ignore not found, just return nil for the munki issue in that case
return listHostsResponse{Err: err}, nil return listHostsResponse{Err: err}, nil
} }
} }
@ -136,67 +136,20 @@ func listHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Servi
}, nil }, nil
} }
func (svc *Service) AggregatedMDMSolutions(ctx context.Context, teamID *uint, mdmID uint) (*fleet.AggregatedMDMSolutions, error) { func (svc *Service) GetMDMSolution(ctx context.Context, mdmID uint) (*fleet.MDMSolution, error) {
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionList); err != nil { // require list hosts permission to view this information
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err return nil, err
} }
return svc.ds.GetMDMSolution(ctx, mdmID)
if teamID != nil {
_, err := svc.ds.Team(ctx, *teamID)
if err != nil {
return nil, err
}
}
// it is expected that there will be relatively few MDM solutions. This
// returns the slice of all aggregated stats (one entry per mdm_id), and we
// then iterate to return only the one that was requested (the slice is
// stored as-is in a JSON field in the database).
sols, _, err := svc.ds.AggregatedMDMSolutions(ctx, teamID)
if err != nil {
return nil, err
}
for _, sol := range sols {
// don't take the address of the loop variable (although it could be ok
// here, but just bad practice)
sol := sol
if sol.ID == mdmID {
return &sol, nil
}
}
return nil, nil
} }
func (svc *Service) AggregatedMunkiIssue(ctx context.Context, teamID *uint, munkiIssueID uint) (*fleet.AggregatedMunkiIssue, error) { func (svc *Service) GetMunkiIssue(ctx context.Context, munkiIssueID uint) (*fleet.MunkiIssue, error) {
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionList); err != nil { // require list hosts permission to view this information
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err return nil, err
} }
return svc.ds.GetMunkiIssue(ctx, munkiIssueID)
if teamID != nil {
_, err := svc.ds.Team(ctx, *teamID)
if err != nil {
return nil, err
}
}
// This returns the slice of all aggregated stats (one entry per
// munki_issue_id), and we then iterate to return only the one that was
// requested (the slice is stored as-is in a JSON field in the database).
issues, _, err := svc.ds.AggregatedMunkiIssues(ctx, teamID)
if err != nil {
return nil, err
}
for _, iss := range issues {
// don't take the address of the loop variable (although it could be ok
// here, but just bad practice)
iss := iss
if iss.ID == munkiIssueID {
return &iss, nil
}
}
return nil, nil
} }
func (svc *Service) ListHosts(ctx context.Context, opt fleet.HostListOptions) ([]*fleet.Host, error) { func (svc *Service) ListHosts(ctx context.Context, opt fleet.HostListOptions) ([]*fleet.Host, error) {
@ -982,7 +935,7 @@ func (svc *Service) MacadminsData(ctx context.Context, id uint) (*fleet.Macadmin
} }
var munkiInfo *fleet.HostMunkiInfo var munkiInfo *fleet.HostMunkiInfo
switch version, err := svc.ds.GetMunkiVersion(ctx, id); { switch version, err := svc.ds.GetHostMunkiVersion(ctx, id); {
case err != nil && !fleet.IsNotFound(err): case err != nil && !fleet.IsNotFound(err):
return nil, err return nil, err
case err == nil: case err == nil:
@ -990,7 +943,7 @@ func (svc *Service) MacadminsData(ctx context.Context, id uint) (*fleet.Macadmin
} }
var mdm *fleet.HostMDM var mdm *fleet.HostMDM
switch hmdm, err := svc.ds.GetMDM(ctx, id); { switch hmdm, err := svc.ds.GetHostMDM(ctx, id); {
case err != nil && !fleet.IsNotFound(err): case err != nil && !fleet.IsNotFound(err):
return nil, err return nil, err
case err == nil: case err == nil:
@ -998,7 +951,7 @@ func (svc *Service) MacadminsData(ctx context.Context, id uint) (*fleet.Macadmin
} }
var munkiIssues []*fleet.HostMunkiIssue var munkiIssues []*fleet.HostMunkiIssue
switch issues, err := svc.ds.GetMunkiIssues(ctx, id); { switch issues, err := svc.ds.GetHostMunkiIssues(ctx, id); {
case err != nil: case err != nil:
return nil, err return nil, err
case err == nil: case err == nil:

View File

@ -1120,7 +1120,6 @@ func (s *integrationTestSuite) TestListHosts() {
assert.Nil(t, resp.MunkiIssue) assert.Nil(t, resp.MunkiIssue)
require.NotNil(t, resp.MDMSolution) require.NotNil(t, resp.MDMSolution)
assert.Equal(t, mdmID, resp.MDMSolution.ID) assert.Equal(t, mdmID, resp.MDMSolution.ID)
assert.Equal(t, 1, resp.MDMSolution.HostsCount)
assert.Equal(t, fleet.WellKnownMDMSimpleMDM, resp.MDMSolution.Name) assert.Equal(t, fleet.WellKnownMDMSimpleMDM, resp.MDMSolution.Name)
assert.Equal(t, "https://simplemdm.com", resp.MDMSolution.ServerURL) assert.Equal(t, "https://simplemdm.com", resp.MDMSolution.ServerURL)
@ -1151,11 +1150,10 @@ func (s *integrationTestSuite) TestListHosts() {
assert.Nil(t, resp.Software) assert.Nil(t, resp.Software)
assert.Nil(t, resp.MDMSolution) assert.Nil(t, resp.MDMSolution)
require.NotNil(t, resp.MunkiIssue) require.NotNil(t, resp.MunkiIssue)
assert.Equal(t, fleet.AggregatedMunkiIssue{ assert.Equal(t, fleet.MunkiIssue{
ID: errMunkiID, ID: errMunkiID,
Name: "err", Name: "err",
IssueType: "error", IssueType: "error",
HostsCount: 1,
}, *resp.MunkiIssue) }, *resp.MunkiIssue)
// filters can be combined, no problem // filters can be combined, no problem
@ -2683,13 +2681,17 @@ func (s *integrationTestSuite) TestGetMacadminsData() {
agg.Macadmins.MunkiIssues[1].ID = 0 agg.Macadmins.MunkiIssues[1].ID = 0
assert.ElementsMatch(t, agg.Macadmins.MunkiIssues, []fleet.AggregatedMunkiIssue{ assert.ElementsMatch(t, agg.Macadmins.MunkiIssues, []fleet.AggregatedMunkiIssue{
{ {
Name: "error1", MunkiIssue: fleet.MunkiIssue{
IssueType: "error", Name: "error1",
IssueType: "error",
},
HostsCount: 1, HostsCount: 1,
}, },
{ {
Name: "warning1", MunkiIssue: fleet.MunkiIssue{
IssueType: "warning", Name: "warning1",
IssueType: "warning",
},
HostsCount: 1, HostsCount: 1,
}, },
}) })