mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Support listing software hosts count filtered by team (#4388)
This commit is contained in:
parent
ad9fb8b36f
commit
4930ca2d0e
1
changes/issue-4268-list-software-hosts-count-per-team
Normal file
1
changes/issue-4268-list-software-hosts-count-per-team
Normal file
@ -0,0 +1 @@
|
||||
* Support filtering software hosts count per team.
|
@ -0,0 +1,28 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20220223113157, Down_20220223113157)
|
||||
}
|
||||
|
||||
func Up_20220223113157(tx *sql.Tx) error {
|
||||
alterStmt := `ALTER TABLE software_host_counts
|
||||
ADD COLUMN team_id INT(10) UNSIGNED NOT NULL DEFAULT 0,
|
||||
DROP PRIMARY KEY,
|
||||
ADD PRIMARY KEY (software_id, team_id),
|
||||
ADD INDEX idx_software_host_counts_team_id_hosts_count_software_id (team_id,hosts_count,software_id),
|
||||
DROP INDEX idx_software_host_counts_host_count_software_id`
|
||||
if _, err := tx.Exec(alterStmt); err != nil {
|
||||
return errors.Wrap(err, "alter software_host_counts table")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20220223113157(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20220223113157(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
execNoErr(t, db, `INSERT INTO software_host_counts (software_id, hosts_count) VALUES (1, 1)`)
|
||||
execNoErr(t, db, `INSERT INTO software_host_counts (software_id, hosts_count) VALUES (2, 10)`)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
var count int
|
||||
require.NoError(t, db.Get(&count, `SELECT count(*) FROM software_host_counts WHERE team_id = 0`))
|
||||
assert.Equal(t, 2, count)
|
||||
require.NoError(t, db.Get(&count, `SELECT SUM(hosts_count) FROM software_host_counts WHERE team_id = 0`))
|
||||
assert.Equal(t, 11, count)
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -347,6 +347,12 @@ func selectSoftwareSQL(hostID *uint, opts fleet.SoftwareListOptions) (string, []
|
||||
goqu.I("shc.hosts_count"),
|
||||
goqu.I("shc.updated_at").As("counts_updated_at"),
|
||||
)
|
||||
|
||||
if opts.TeamID != nil {
|
||||
ds = ds.Where(goqu.I("shc.team_id").Eq(opts.TeamID))
|
||||
} else {
|
||||
ds = ds.Where(goqu.I("shc.team_id").Eq(0))
|
||||
}
|
||||
}
|
||||
ds = appendListOptionsToSelect(ds, opts.ListOptions)
|
||||
|
||||
@ -640,88 +646,122 @@ func (ds *Datastore) SoftwareByID(ctx context.Context, id uint) (*fleet.Software
|
||||
// After aggregation, it cleans up unused software (e.g. software installed
|
||||
// on removed hosts, software uninstalled on hosts, etc.)
|
||||
func (ds *Datastore) CalculateHostsPerSoftware(ctx context.Context, updatedAt time.Time) error {
|
||||
resetStmt := `
|
||||
UPDATE software_host_counts
|
||||
SET hosts_count = 0, updated_at = ?`
|
||||
const (
|
||||
resetStmt = `
|
||||
UPDATE software_host_counts
|
||||
SET hosts_count = 0, updated_at = ?`
|
||||
|
||||
queryStmt := `
|
||||
SELECT count(*), software_id
|
||||
FROM host_software
|
||||
WHERE software_id > 0
|
||||
GROUP BY software_id`
|
||||
// team_id is added to the select list to have the same structure as
|
||||
// the teamCountsStmt, making it easier to use a common implementation
|
||||
globalCountsStmt = `
|
||||
SELECT count(*), 0 as team_id, software_id
|
||||
FROM host_software
|
||||
WHERE software_id > 0
|
||||
GROUP BY software_id`
|
||||
|
||||
insertStmt := `
|
||||
INSERT INTO software_host_counts
|
||||
(software_id, hosts_count, updated_at)
|
||||
VALUES
|
||||
%s
|
||||
ON DUPLICATE KEY UPDATE
|
||||
hosts_count = VALUES(hosts_count),
|
||||
updated_at = VALUES(updated_at)`
|
||||
valuesPart := `(?, ?, ?),`
|
||||
teamCountsStmt = `
|
||||
SELECT count(*), h.team_id, hs.software_id
|
||||
FROM host_software hs
|
||||
INNER JOIN hosts h
|
||||
ON hs.host_id = h.id
|
||||
WHERE h.team_id IS NOT NULL AND hs.software_id > 0
|
||||
GROUP BY hs.software_id, h.team_id`
|
||||
|
||||
insertStmt = `
|
||||
INSERT INTO software_host_counts
|
||||
(software_id, hosts_count, team_id, updated_at)
|
||||
VALUES
|
||||
%s
|
||||
ON DUPLICATE KEY UPDATE
|
||||
hosts_count = VALUES(hosts_count),
|
||||
updated_at = VALUES(updated_at)`
|
||||
|
||||
valuesPart = `(?, ?, ?, ?),`
|
||||
|
||||
cleanupSoftwareStmt = `
|
||||
DELETE s
|
||||
FROM software s
|
||||
LEFT JOIN software_host_counts shc
|
||||
ON s.id = shc.software_id
|
||||
WHERE
|
||||
shc.software_id IS NULL OR
|
||||
(shc.team_id = 0 AND shc.hosts_count = 0)`
|
||||
|
||||
cleanupTeamStmt = `
|
||||
DELETE shc
|
||||
FROM software_host_counts shc
|
||||
LEFT JOIN teams t
|
||||
ON t.id = shc.team_id
|
||||
WHERE
|
||||
shc.team_id > 0 AND
|
||||
t.id IS NULL`
|
||||
)
|
||||
|
||||
// first, reset all counts to 0
|
||||
if _, err := ds.writer.ExecContext(ctx, resetStmt, updatedAt); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "reset all software_host_counts to 0")
|
||||
}
|
||||
|
||||
// next get a cursor for the counts for each software
|
||||
rows, err := ds.reader.QueryContext(ctx, queryStmt)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "read counts from host_software")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// use a loop to iterate to prevent loading all in one go in memory, as it
|
||||
// could get pretty big at >100K hosts with 1000+ software each.
|
||||
const batchSize = 100
|
||||
var batchCount int
|
||||
args := make([]interface{}, 0, batchSize*3)
|
||||
for rows.Next() {
|
||||
var count int
|
||||
var sid uint
|
||||
|
||||
if err := rows.Scan(&count, &sid); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "scan row into variables")
|
||||
// next get a cursor for the global and team counts for each software
|
||||
stmtLabel := []string{"global", "team"}
|
||||
for i, countStmt := range []string{globalCountsStmt, teamCountsStmt} {
|
||||
rows, err := ds.reader.QueryContext(ctx, countStmt)
|
||||
if err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "read %s counts from host_software", stmtLabel[i])
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
args = append(args, sid, count, updatedAt)
|
||||
batchCount++
|
||||
// use a loop to iterate to prevent loading all in one go in memory, as it
|
||||
// could get pretty big at >100K hosts with 1000+ software each. Use a write
|
||||
// batch to prevent making too many single-row inserts.
|
||||
const batchSize = 100
|
||||
var batchCount int
|
||||
args := make([]interface{}, 0, batchSize*4)
|
||||
for rows.Next() {
|
||||
var (
|
||||
count int
|
||||
teamID uint
|
||||
sid uint
|
||||
)
|
||||
|
||||
if batchCount == batchSize {
|
||||
values := strings.TrimSuffix(strings.Repeat(valuesPart, batchCount), ",")
|
||||
if _, err := ds.writer.ExecContext(ctx, fmt.Sprintf(insertStmt, values), args...); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "insert batch into software_host_counts")
|
||||
if err := rows.Scan(&count, &teamID, &sid); err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "scan %s row into variables", stmtLabel[i])
|
||||
}
|
||||
|
||||
args = args[:0]
|
||||
batchCount = 0
|
||||
args = append(args, sid, count, teamID, updatedAt)
|
||||
batchCount++
|
||||
|
||||
if batchCount == batchSize {
|
||||
values := strings.TrimSuffix(strings.Repeat(valuesPart, batchCount), ",")
|
||||
if _, err := ds.writer.ExecContext(ctx, fmt.Sprintf(insertStmt, values), args...); err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "insert %s batch into software_host_counts", stmtLabel[i])
|
||||
}
|
||||
|
||||
args = args[:0]
|
||||
batchCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if batchCount > 0 {
|
||||
values := strings.TrimSuffix(strings.Repeat(valuesPart, batchCount), ",")
|
||||
if _, err := ds.writer.ExecContext(ctx, fmt.Sprintf(insertStmt, values), args...); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "insert last batch into software_host_counts")
|
||||
if batchCount > 0 {
|
||||
values := strings.TrimSuffix(strings.Repeat(valuesPart, batchCount), ",")
|
||||
if _, err := ds.writer.ExecContext(ctx, fmt.Sprintf(insertStmt, values), args...); err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "insert last %s batch into software_host_counts", stmtLabel[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "iterate over host_software counts")
|
||||
if err := rows.Err(); err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "iterate over %s host_software counts", stmtLabel[i])
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
|
||||
cleanupStmt := `
|
||||
DELETE FROM
|
||||
software
|
||||
WHERE
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM
|
||||
software_host_counts shc
|
||||
WHERE
|
||||
software.id = shc.software_id AND
|
||||
shc.hosts_count > 0)`
|
||||
if _, err := ds.writer.ExecContext(ctx, cleanupStmt); err != nil {
|
||||
// remove any unused software (global counts = 0)
|
||||
if _, err := ds.writer.ExecContext(ctx, cleanupSoftwareStmt); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "delete unused software")
|
||||
}
|
||||
|
||||
// remove any software count row for teams that don't exist anymore
|
||||
if _, err := ds.writer.ExecContext(ctx, cleanupTeamStmt); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "delete software_host_counts for non-existing teams")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -578,14 +579,14 @@ func testSoftwareCalculateHostsPerSoftware(t *testing.T, ds *Datastore) {
|
||||
{Name: "bar", Version: "0.0.3", Source: "deb_packages"},
|
||||
}
|
||||
|
||||
require.NoError(t, ds.UpdateHostSoftware(context.Background(), host1.ID, software1))
|
||||
require.NoError(t, ds.UpdateHostSoftware(context.Background(), host2.ID, software2))
|
||||
require.NoError(t, ds.UpdateHostSoftware(ctx, host1.ID, software1))
|
||||
require.NoError(t, ds.UpdateHostSoftware(ctx, host2.ID, software2))
|
||||
|
||||
err := ds.CalculateHostsPerSoftware(ctx, time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
swOpts := fleet.SoftwareListOptions{WithHostCounts: true, ListOptions: fleet.ListOptions{OrderKey: "hosts_count", OrderDirection: fleet.OrderDescending}}
|
||||
swCounts := listSoftwareCheckCount(t, ds, 4, 4, swOpts, false)
|
||||
globalOpts := fleet.SoftwareListOptions{WithHostCounts: true, ListOptions: fleet.ListOptions{OrderKey: "hosts_count", OrderDirection: fleet.OrderDescending}}
|
||||
globalCounts := listSoftwareCheckCount(t, ds, 4, 4, globalOpts, false)
|
||||
|
||||
want := []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", HostsCount: 2},
|
||||
@ -593,25 +594,25 @@ func testSoftwareCalculateHostsPerSoftware(t *testing.T, ds *Datastore) {
|
||||
{Name: "foo", Version: "v0.0.2", HostsCount: 1},
|
||||
{Name: "bar", Version: "0.0.3", HostsCount: 1},
|
||||
}
|
||||
cmpNameVersionCount(want, swCounts)
|
||||
cmpNameVersionCount(want, globalCounts)
|
||||
|
||||
// update host2, remove "bar" software
|
||||
software2 = []fleet.Software{
|
||||
{Name: "foo", Version: "v0.0.2", Source: "chrome_extensions"},
|
||||
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
||||
}
|
||||
require.NoError(t, ds.UpdateHostSoftware(context.Background(), host2.ID, software2))
|
||||
require.NoError(t, ds.UpdateHostSoftware(ctx, host2.ID, software2))
|
||||
|
||||
err = ds.CalculateHostsPerSoftware(ctx, time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
swCounts = listSoftwareCheckCount(t, ds, 3, 3, swOpts, false)
|
||||
globalCounts = listSoftwareCheckCount(t, ds, 3, 3, globalOpts, false)
|
||||
want = []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", HostsCount: 2},
|
||||
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
||||
{Name: "foo", Version: "v0.0.2", HostsCount: 1},
|
||||
}
|
||||
cmpNameVersionCount(want, swCounts)
|
||||
cmpNameVersionCount(want, globalCounts)
|
||||
|
||||
// create a software entry without any host and any counts
|
||||
_, err = ds.writer.ExecContext(ctx, `INSERT INTO software (name, version, source) VALUES ('baz', '0.0.1', 'testing')`)
|
||||
@ -638,6 +639,127 @@ func testSoftwareCalculateHostsPerSoftware(t *testing.T, ds *Datastore) {
|
||||
{Name: "foo", Version: "v0.0.2", HostsCount: 0},
|
||||
}
|
||||
cmpNameVersionCount(want, allSw)
|
||||
|
||||
// create 2 teams and assign a new host to each
|
||||
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
|
||||
require.NoError(t, err)
|
||||
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
|
||||
require.NoError(t, err)
|
||||
host3 := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now())
|
||||
require.NoError(t, ds.AddHostsToTeam(ctx, &team1.ID, []uint{host3.ID}))
|
||||
host4 := test.NewHost(t, ds, "host4", "", "host4key", "host4uuid", time.Now())
|
||||
require.NoError(t, ds.AddHostsToTeam(ctx, &team2.ID, []uint{host4.ID}))
|
||||
|
||||
// assign existing host1 to team1 too, so we have a team with multiple hosts
|
||||
require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{host1.ID}))
|
||||
// use some software for host3 and host4
|
||||
software3 := []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
||||
}
|
||||
software4 := []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
||||
{Name: "bar", Version: "0.0.3", Source: "deb_packages"},
|
||||
}
|
||||
require.NoError(t, ds.UpdateHostSoftware(ctx, host3.ID, software3))
|
||||
require.NoError(t, ds.UpdateHostSoftware(ctx, host4.ID, software4))
|
||||
|
||||
// at this point, there's no counts per team, only global counts
|
||||
globalCounts = listSoftwareCheckCount(t, ds, 3, 3, globalOpts, false)
|
||||
want = []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", HostsCount: 2},
|
||||
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
||||
{Name: "foo", Version: "v0.0.2", HostsCount: 1},
|
||||
}
|
||||
cmpNameVersionCount(want, globalCounts)
|
||||
|
||||
team1Opts := fleet.SoftwareListOptions{WithHostCounts: true, TeamID: ptr.Uint(team1.ID), ListOptions: fleet.ListOptions{OrderKey: "hosts_count", OrderDirection: fleet.OrderDescending}}
|
||||
team1Counts := listSoftwareCheckCount(t, ds, 0, 0, team1Opts, false)
|
||||
want = []fleet.Software{}
|
||||
cmpNameVersionCount(want, team1Counts)
|
||||
|
||||
// after a call to Calculate, the global counts are updated and the team counts appear
|
||||
err = ds.CalculateHostsPerSoftware(ctx, time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
globalCounts = listSoftwareCheckCount(t, ds, 4, 4, globalOpts, false)
|
||||
want = []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", HostsCount: 4},
|
||||
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
||||
{Name: "foo", Version: "v0.0.2", HostsCount: 1},
|
||||
{Name: "bar", Version: "0.0.3", HostsCount: 1},
|
||||
}
|
||||
cmpNameVersionCount(want, globalCounts)
|
||||
|
||||
team1Counts = listSoftwareCheckCount(t, ds, 2, 2, team1Opts, false)
|
||||
want = []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", HostsCount: 2},
|
||||
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
||||
}
|
||||
cmpNameVersionCount(want, team1Counts)
|
||||
|
||||
team2Opts := fleet.SoftwareListOptions{WithHostCounts: true, TeamID: ptr.Uint(team2.ID), ListOptions: fleet.ListOptions{OrderKey: "hosts_count", OrderDirection: fleet.OrderDescending}}
|
||||
team2Counts := listSoftwareCheckCount(t, ds, 2, 2, team2Opts, false)
|
||||
want = []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", HostsCount: 1},
|
||||
{Name: "bar", Version: "0.0.3", HostsCount: 1},
|
||||
}
|
||||
cmpNameVersionCount(want, team2Counts)
|
||||
|
||||
// update host4 (team2), remove "bar" software
|
||||
software4 = []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
||||
}
|
||||
require.NoError(t, ds.UpdateHostSoftware(ctx, host4.ID, software4))
|
||||
|
||||
err = ds.CalculateHostsPerSoftware(ctx, time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
globalCounts = listSoftwareCheckCount(t, ds, 3, 3, globalOpts, false)
|
||||
want = []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", HostsCount: 4},
|
||||
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
||||
{Name: "foo", Version: "v0.0.2", HostsCount: 1},
|
||||
}
|
||||
cmpNameVersionCount(want, globalCounts)
|
||||
|
||||
team1Counts = listSoftwareCheckCount(t, ds, 2, 2, team1Opts, false)
|
||||
want = []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", HostsCount: 2},
|
||||
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
||||
}
|
||||
cmpNameVersionCount(want, team1Counts)
|
||||
|
||||
team2Counts = listSoftwareCheckCount(t, ds, 1, 1, team2Opts, false)
|
||||
want = []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", HostsCount: 1},
|
||||
}
|
||||
cmpNameVersionCount(want, team2Counts)
|
||||
|
||||
// update host4 (team2), remove all software and delete team
|
||||
software4 = []fleet.Software{}
|
||||
require.NoError(t, ds.UpdateHostSoftware(ctx, host4.ID, software4))
|
||||
require.NoError(t, ds.DeleteTeam(ctx, team2.ID))
|
||||
|
||||
// this call will remove team2 from the software host counts table
|
||||
err = ds.CalculateHostsPerSoftware(ctx, time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
globalCounts = listSoftwareCheckCount(t, ds, 3, 3, globalOpts, false)
|
||||
want = []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", HostsCount: 3},
|
||||
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
||||
{Name: "foo", Version: "v0.0.2", HostsCount: 1},
|
||||
}
|
||||
cmpNameVersionCount(want, globalCounts)
|
||||
|
||||
team1Counts = listSoftwareCheckCount(t, ds, 2, 2, team1Opts, false)
|
||||
want = []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.3", HostsCount: 2},
|
||||
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
||||
}
|
||||
cmpNameVersionCount(want, team1Counts)
|
||||
|
||||
listSoftwareCheckCount(t, ds, 0, 0, team2Opts, false)
|
||||
}
|
||||
|
||||
func insertVulnSoftwareForTest(t *testing.T, ds *Datastore) {
|
||||
|
@ -2,7 +2,9 @@ package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -10,6 +12,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/WatchBeam/clock"
|
||||
@ -337,3 +340,39 @@ func TruncateTables(t testing.TB, ds *Datastore, tables ...string) {
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
// this is meant to be used for debugging/testing that statement uses an efficient
|
||||
// plan (e.g. makes use of an index, avoids full scans, etc.) using the data already
|
||||
// created for tests. Calls to this function should be temporary and removed when
|
||||
// done investigating the plan, so it is expected that this function will be detected
|
||||
// as unused.
|
||||
func explainSQLStatement(w io.Writer, db sqlx.QueryerContext, stmt string, args ...interface{}) { //nolint:deadcode,unused
|
||||
var rows []struct {
|
||||
ID int `db:"id"`
|
||||
SelectType string `db:"select_type"`
|
||||
Table sql.NullString `db:"table"`
|
||||
Partitions sql.NullString `db:"partitions"`
|
||||
Type sql.NullString `db:"type"`
|
||||
PossibleKeys sql.NullString `db:"possible_keys"`
|
||||
Key sql.NullString `db:"key"`
|
||||
KeyLen sql.NullInt64 `db:"key_len"`
|
||||
Ref sql.NullString `db:"ref"`
|
||||
Rows sql.NullInt64 `db:"rows"`
|
||||
Filtered sql.NullFloat64 `db:"filtered"`
|
||||
Extra sql.NullString `db:"Extra"`
|
||||
}
|
||||
if err := sqlx.SelectContext(context.Background(), db, &rows, "EXPLAIN "+stmt, args...); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Fprint(w, "\n\n", strings.Repeat("-", 60), "\n", stmt, "\n", strings.Repeat("-", 60), "\n")
|
||||
tw := tabwriter.NewWriter(w, 0, 1, 1, ' ', tabwriter.Debug)
|
||||
|
||||
fmt.Fprintln(tw, "id\tselect_type\ttable\tpartitions\ttype\tpossible_keys\tkey\tkey_len\tref\trows\tfiltered\textra")
|
||||
for _, row := range rows {
|
||||
fmt.Fprintf(tw, "%d\t%s\t%s\t%s\t%s\t%s\t%s\t%d\t%s\t%d\t%f\t%s\n", row.ID, row.SelectType, row.Table.String, row.Partitions.String,
|
||||
row.Type.String, row.PossibleKeys.String, row.Key.String, row.KeyLen.Int64, row.Ref.String, row.Rows.Int64, row.Filtered.Float64, row.Extra.String)
|
||||
}
|
||||
if err := tw.Flush(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
@ -2894,6 +2894,14 @@ func (s *integrationTestSuite) TestPaginateListSoftware() {
|
||||
}
|
||||
}
|
||||
|
||||
// create a team and make the last 3 hosts part of it (meaning 3 that use
|
||||
// sws[19], 2 for sws[18], and 1 for sws[17])
|
||||
tm, err := s.ds.NewTeam(context.Background(), &fleet.Team{
|
||||
Name: t.Name(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, s.ds.AddHostsToTeam(context.Background(), &tm.ID, []uint{hosts[19].ID, hosts[18].ID, hosts[17].ID}))
|
||||
|
||||
assertResp := func(resp listSoftwareResponse, want []fleet.Software, ts time.Time, counts ...int) {
|
||||
require.Len(t, resp.Software, len(want))
|
||||
for i := range resp.Software {
|
||||
@ -2915,6 +2923,11 @@ func (s *integrationTestSuite) TestPaginateListSoftware() {
|
||||
s.DoJSON("GET", "/api/v1/fleet/software", nil, http.StatusOK, &lsResp, "order_key", "hosts_count", "order_direction", "desc")
|
||||
assertResp(lsResp, nil, time.Time{})
|
||||
|
||||
// same with a team filter
|
||||
lsResp = listSoftwareResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software", nil, http.StatusOK, &lsResp, "order_key", "hosts_count", "order_direction", "desc", "team_id", fmt.Sprintf("%d", tm.ID))
|
||||
assertResp(lsResp, nil, time.Time{})
|
||||
|
||||
// calculate hosts counts
|
||||
hostsCountTs := time.Now().UTC()
|
||||
require.NoError(t, s.ds.CalculateHostsPerSoftware(context.Background(), hostsCountTs))
|
||||
@ -2968,6 +2981,16 @@ func (s *integrationTestSuite) TestPaginateListSoftware() {
|
||||
lsResp = listSoftwareResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software", nil, http.StatusOK, &lsResp, "vulnerable", "true", "per_page", "5", "page", "2", "order_key", "hosts_count", "order_direction", "desc")
|
||||
assertResp(lsResp, nil, time.Time{})
|
||||
|
||||
// filter by the team, 2 by page
|
||||
lsResp = listSoftwareResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software", nil, http.StatusOK, &lsResp, "per_page", "2", "page", "0", "order_key", "hosts_count", "order_direction", "desc", "team_id", fmt.Sprintf("%d", tm.ID))
|
||||
assertResp(lsResp, []fleet.Software{sws[19], sws[18]}, hostsCountTs, 3, 2)
|
||||
|
||||
// filter by the team, 2 by page, next page
|
||||
lsResp = listSoftwareResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software", nil, http.StatusOK, &lsResp, "per_page", "2", "page", "1", "order_key", "hosts_count", "order_direction", "desc", "team_id", fmt.Sprintf("%d", tm.ID))
|
||||
assertResp(lsResp, []fleet.Software{sws[17]}, hostsCountTs, 1)
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestChangeUserEmail() {
|
||||
|
Loading…
Reference in New Issue
Block a user