2021-09-10 19:26:39 +00:00
package main
import (
2021-09-14 12:11:07 +00:00
"context"
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
"os"
2023-12-14 20:14:43 +00:00
"sync"
2021-09-10 19:26:39 +00:00
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
2022-06-13 13:18:03 +00:00
"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
2021-09-10 19:26:39 +00:00
"github.com/fleetdm/fleet/v4/server/pubsub"
"github.com/fleetdm/fleet/v4/server/service"
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
2021-09-10 19:26:39 +00:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
2023-12-15 18:55:39 +00:00
func TestSavedLiveQuery ( t * testing . T ) {
2021-09-10 19:26:39 +00:00
rs := pubsub . NewInmemQueryResults ( )
2022-06-13 13:18:03 +00:00
lq := live_query_mock . New ( t )
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
logger := kitlog . NewJSONLogger ( os . Stdout )
logger = level . NewFilter ( logger , level . AllowDebug ( ) )
2022-05-10 15:29:17 +00:00
_ , ds := runServerWithMockedDS ( t , & service . TestServerOpts {
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
Rs : rs ,
Lq : lq ,
Logger : logger ,
} )
users , err := ds . ListUsersFunc ( context . Background ( ) , fleet . UserListOptions { } )
require . NoError ( t , err )
var admin * fleet . User
for _ , user := range users {
if user . GlobalRole != nil && * user . GlobalRole == fleet . RoleAdmin {
admin = user
}
}
2021-09-10 19:26:39 +00:00
2023-12-15 18:55:39 +00:00
const queryName = "saved-query"
const queryString = "select 42, * from time"
query := fleet . Query {
ID : 42 ,
Name : queryName ,
Query : queryString ,
Saved : true ,
}
2021-09-14 12:11:07 +00:00
ds . HostIDsByNameFunc = func ( ctx context . Context , filter fleet . TeamFilter , hostnames [ ] string ) ( [ ] uint , error ) {
2021-09-10 19:26:39 +00:00
return [ ] uint { 1234 } , nil
}
2024-01-26 16:00:58 +00:00
ds . LabelIDsByNameFunc = func ( ctx context . Context , labels [ ] string ) ( map [ string ] uint , error ) {
2021-09-10 19:26:39 +00:00
return nil , nil
}
2021-09-14 12:11:07 +00:00
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
2021-09-10 19:26:39 +00:00
return & fleet . AppConfig { } , nil
}
2023-12-15 18:55:39 +00:00
ds . ListQueriesFunc = func ( ctx context . Context , opt fleet . ListQueryOptions ) ( [ ] * fleet . Query , error ) {
if opt . MatchQuery == queryName {
return [ ] * fleet . Query { & query } , nil
}
return [ ] * fleet . Query { } , nil
2021-09-10 19:26:39 +00:00
}
2021-09-14 12:11:07 +00:00
ds . NewDistributedQueryCampaignFunc = func ( ctx context . Context , camp * fleet . DistributedQueryCampaign ) ( * fleet . DistributedQueryCampaign , error ) {
2021-09-10 19:26:39 +00:00
camp . ID = 321
return camp , nil
}
2021-09-14 12:11:07 +00:00
ds . NewDistributedQueryCampaignTargetFunc = func ( ctx context . Context , target * fleet . DistributedQueryCampaignTarget ) ( * fleet . DistributedQueryCampaignTarget , error ) {
2021-09-10 19:26:39 +00:00
return target , nil
}
2021-09-14 12:11:07 +00:00
ds . HostIDsInTargetsFunc = func ( ctx context . Context , filter fleet . TeamFilter , targets fleet . HostTargets ) ( [ ] uint , error ) {
2021-09-10 19:26:39 +00:00
return [ ] uint { 1 } , nil
}
2021-09-14 12:11:07 +00:00
ds . CountHostsInTargetsFunc = func ( ctx context . Context , filter fleet . TeamFilter , targets fleet . HostTargets , now time . Time ) ( fleet . TargetMetrics , error ) {
2021-09-10 19:26:39 +00:00
return fleet . TargetMetrics { TotalHosts : 1 , OnlineHosts : 1 } , nil
}
lq . On ( "QueriesForHost" , uint ( 1 ) ) . Return (
map [ string ] string {
2023-12-15 18:55:39 +00:00
"42" : queryString ,
2021-09-10 19:26:39 +00:00
} ,
nil ,
)
lq . On ( "QueryCompletedByHost" , "42" , 99 ) . Return ( nil )
2023-12-15 18:55:39 +00:00
lq . On ( "RunQuery" , "321" , queryString , [ ] uint { 1 } ) . Return ( nil )
2021-09-10 19:26:39 +00:00
2021-09-14 12:11:07 +00:00
ds . DistributedQueryCampaignTargetIDsFunc = func ( ctx context . Context , id uint ) ( targets * fleet . HostTargets , err error ) {
2021-09-10 19:26:39 +00:00
return & fleet . HostTargets { HostIDs : [ ] uint { 99 } } , nil
}
2021-09-14 12:11:07 +00:00
ds . DistributedQueryCampaignFunc = func ( ctx context . Context , id uint ) ( * fleet . DistributedQueryCampaign , error ) {
Add read replica testing helpers and fix non-sso login bug (#4908)
not set on the INSERT.
- OUT: Only sets the ID on the passed session and returns it. (`CreatedAt`, `AccessedAt`, are not set.)
New version:
```go
func (ds *Datastore) NewSession(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
sqlStatement := `
INSERT INTO sessions (
user_id,
` + "`key`" + `
)
VALUES(?,?)
`
result, err := ds.writer.ExecContext(ctx, sqlStatement, userID, sessionKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting session")
}
id, _ := result.LastInsertId() // cannot fail with the mysql driver
return ds.sessionByID(ctx, ds.writer, uint(id))
}
```
- IN: Define arguments that are truly used when creating a session.
- OUT: Load and return the fleet.Session struct with all values set (using the `ds.writer` to support read replicas correctly).
PS: The new `NewSession` version mimics what we already do with other entities, like policies (`Datastore.NewGlobalPolicy`).
2022-04-04 23:52:05 +00:00
return & fleet . DistributedQueryCampaign {
ID : 321 ,
UserID : admin . ID ,
} , nil
2021-09-10 19:26:39 +00:00
}
2021-09-14 12:11:07 +00:00
ds . SaveDistributedQueryCampaignFunc = func ( ctx context . Context , camp * fleet . DistributedQueryCampaign ) error {
2021-09-10 19:26:39 +00:00
return nil
}
2021-09-14 12:11:07 +00:00
ds . QueryFunc = func ( ctx context . Context , id uint ) ( * fleet . Query , error ) {
2023-12-15 18:55:39 +00:00
return & query , nil
2021-09-10 19:26:39 +00:00
}
2023-12-13 20:46:59 +00:00
ds . IsSavedQueryFunc = func ( ctx context . Context , queryID uint ) ( bool , error ) {
return true , nil
}
2023-12-14 20:14:43 +00:00
var GetLiveQueryStatsFuncWg sync . WaitGroup
GetLiveQueryStatsFuncWg . Add ( 1 )
2023-12-13 20:46:59 +00:00
ds . GetLiveQueryStatsFunc = func ( ctx context . Context , queryID uint , hostIDs [ ] uint ) ( [ ] * fleet . LiveQueryStats , error ) {
2023-12-14 20:14:43 +00:00
GetLiveQueryStatsFuncWg . Done ( )
2023-12-13 20:46:59 +00:00
return nil , nil
}
2023-12-14 20:14:43 +00:00
var UpdateLiveQueryStatsFuncWg sync . WaitGroup
UpdateLiveQueryStatsFuncWg . Add ( 1 )
2023-12-13 20:46:59 +00:00
ds . UpdateLiveQueryStatsFunc = func ( ctx context . Context , queryID uint , stats [ ] * fleet . LiveQueryStats ) error {
2023-12-14 20:14:43 +00:00
UpdateLiveQueryStatsFuncWg . Done ( )
2023-12-13 20:46:59 +00:00
return nil
}
2023-12-14 20:14:43 +00:00
var CalculateAggregatedPerfStatsPercentilesFuncWg sync . WaitGroup
CalculateAggregatedPerfStatsPercentilesFuncWg . Add ( 1 )
2023-12-13 20:46:59 +00:00
ds . CalculateAggregatedPerfStatsPercentilesFunc = func ( ctx context . Context , aggregate fleet . AggregatedStatsType , queryID uint ) error {
2023-12-14 20:14:43 +00:00
CalculateAggregatedPerfStatsPercentilesFuncWg . Done ( )
2023-12-13 20:46:59 +00:00
return nil
}
2021-09-10 19:26:39 +00:00
go func ( ) {
time . Sleep ( 2 * time . Second )
require . NoError ( t , rs . WriteResult (
fleet . DistributedQueryResult {
DistributedQueryCampaignID : 321 ,
Rows : [ ] map [ string ] string { { "bing" : "fds" } } ,
Reduce size of `DistributedQueryResult` to improve live query performance (#11882)
This was found while working on #10957.
When running a live query, a lot of unused host data is stored in Redis
and sent on every live query result message via websockets. The frontend
and fleetctl just need `id`, `hostname` and `display_name`. (This
becomes worse every time we add new fields to the `Host` struct.)
Sample of one websocket message result when running `SELECT * from
osquery_info;`:
size in `main`: 2234 bytes
```
a["{\"type\":\"result\",\"data\":{\"distributed_query_execution_id\":57,\"host\":
{\"created_at\":\"2023-05-22T12:14:11Z\",\"updated_at\":\"2023-05-23T12:31:51Z\",
\"software_updated_at\":\"0001-01-01T00:00:00Z\",\"id\":106,\"detail_updated_at\":\"2023-05-23T11:50:04Z\",
\"label_updated_at\":\"2023-05-23T11:50:04Z\",\"policy_updated_at\":\"1970-01-02T00:00:00Z\",
\"last_enrolled_at\":\"2023-05-22T12:14:12Z\",
\"seen_time\":\"2023-05-23T09:52:23.876311-03:00\",\"refetch_requested\":false,
\"hostname\":\"lucass-macbook-pro.local\",\"uuid\":\"BD4DFA10-E334-41D9-8136-D2163A8FE588\",\"platform\":\"darwin\",\"osquery_version\":\"5.8.2\",\"os_version\":\"macOS 13.3.1\",\"build\":\"22E261\",\"platform_like\":\"darwin\",\"code_name\":\"\",
\"uptime\":91125000000000,\"memory\":34359738368,\"cpu_type\":\"x86_64h\",\"cpu_subtype\":\"Intel x86-64h Haswell\",\"cpu_brand\":\"Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz\",\"cpu_physical_cores\":4,\"cpu_logical_cores\":8,\"hardware_vendor\":\"Apple Inc.\",\"hardware_model\":\"MacBookPro16,2\",\"hardware_version\":\"1.0\",
\"hardware_serial\":\"0DPQR4HMD1FZ\",
\"computer_name\":\"Lucas’s MacBook Pro\",\"public_ip\":\"\",
\"primary_ip\":\"192.168.0.230\",\"primary_mac\":\"68:2f:67:8e:b6:1f\",
\"distributed_interval\":1,\"config_tls_refresh\":60,\"logger_tls_period\":10,\"team_id\":null,
\"pack_stats\":null,\"team_name\":null,
\"gigs_disk_space_available\":386.23,\"percent_disk_space_available\":40,
\"issues\":{\"total_issues_count\":0,\"failing_policies_count\":0},
\"mdm\":{\"enrollment_status\":null,\"server_url\":null,\"name\":\"\",\"encryption_key_available\":false},
\"status\":\"online\",\"display_text\":\"lucass-macbook-pro.local\",\"display_name\":\"Lucas’s MacBook Pro\"},
\"rows\":[{\"build_distro\":\"10.14\",\"build_platform\":\"darwin\",
\"config_hash\":\"b7ee9363a7c686e76e99ffb122e9c5241a791e69\",\"config_valid\":\"1\",
\"extensions\":\"active\",\"host_display_name\":\"Lucas’s MacBook Pro\",
\"host_hostname\":\"lucass-macbook-pro.local\",\"instance_id\":\"cde5de81-344b-4c76-b1c5-dae964fdd4f2\",\"pid\":\"8370\",\"platform_mask\":\"21\",\"start_time\":\"1684757652\",
\"uuid\":\"BD4DFA10-E334-41D9-8136-D2163A8FE588\",
\"version\":\"5.8.2\",\"watcher\":\"8364\"}],\"error\":null}}"]
```
vs. size of the message result on this branch: 675 bytes
```
a["{\"type\":\"result\",\"data\":{\"distributed_query_execution_id\":59,
\"host\":{\"id\":106,\"hostname\":\"lucass-macbook-pro.local\",
\"display_name\":\"Lucas’s MacBook Pro\"},
\"rows\":[{\"build_distro\":\"10.14\",\"build_platform\":\"darwin\",
\"config_hash\":\"f80dee827635db39077a458243379b3ad63311fd\",
\"config_valid\":\"1\",\"extensions\":\"active\",\"host_display_name\":\"Lucas’s MacBook Pro\",
\"host_hostname\":\"lucass-macbook-pro.local\",
\"instance_id\":\"cde5de81-344b-4c76-b1c5-dae964fdd4f2\",\"pid\":\"8370\",\"platform_mask\":\"21\",
\"start_time\":\"1684757652\",\"uuid\":\"BD4DFA10-E334-41D9-8136-D2163A8FE588\",\"version\":\"5.8.2\",
\"watcher\":\"8364\"}]}}"]
```
Manual tests included running with an old fleetctl running with a new
fleet server, and vice-versa, a new fleetctl running against an old
fleet server.
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- ~For Orbit and Fleet Desktop changes:~
- ~[ ] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.~
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-05-25 11:11:53 +00:00
Host : fleet . ResultHostData {
ID : 99 ,
Hostname : "somehostname" ,
DisplayName : "somehostname" ,
} ,
2023-12-13 20:46:59 +00:00
Stats : & fleet . Stats {
WallTimeMs : 10 ,
UserTime : 20 ,
SystemTime : 30 ,
Memory : 40 ,
} ,
2021-09-10 19:26:39 +00:00
} ,
) )
} ( )
2022-10-17 13:52:25 +00:00
expected := ` { "host" : "somehostname" , "rows" : [ { "bing" : "fds" , "host_display_name" : "somehostname" , "host_hostname" : "somehostname" } ] }
2021-09-10 19:26:39 +00:00
`
2023-12-18 15:31:07 +00:00
// Note: runAppForTest never closes the WebSocket connection and does not exit,
// so we are unable to see the activity data that is written after WebSocket disconnects.
assert . Equal ( t , expected , runAppForTest ( t , [ ] string { "query" , "--hosts" , "1234" , "--query-name" , queryName } ) )
2023-12-14 20:14:43 +00:00
// We need to use waitGroups to detect whether Database functions were called because this is an asynchronous test which will flag data races otherwise.
c := make ( chan struct { } )
go func ( ) {
defer close ( c )
GetLiveQueryStatsFuncWg . Wait ( )
UpdateLiveQueryStatsFuncWg . Wait ( )
CalculateAggregatedPerfStatsPercentilesFuncWg . Wait ( )
} ( )
select {
case <- time . After ( time . Second ) :
require . Fail (
t ,
"Expected invocation of one of these Database functions did not happen: GetLiveQueryStats, UpdateLiveQueryStats, or CalculateAggregatedPerfStatsPercentiles" ,
)
case <- c : // All good
}
2021-09-10 19:26:39 +00:00
}
2023-12-15 18:55:39 +00:00
func TestAdHocLiveQuery ( t * testing . T ) {
rs := pubsub . NewInmemQueryResults ( )
lq := live_query_mock . New ( t )
logger := kitlog . NewJSONLogger ( os . Stdout )
logger = level . NewFilter ( logger , level . AllowDebug ( ) )
_ , ds := runServerWithMockedDS (
t , & service . TestServerOpts {
Rs : rs ,
Lq : lq ,
Logger : logger ,
} ,
)
users , err := ds . ListUsersFunc ( context . Background ( ) , fleet . UserListOptions { } )
require . NoError ( t , err )
var admin * fleet . User
for _ , user := range users {
if user . GlobalRole != nil && * user . GlobalRole == fleet . RoleAdmin {
admin = user
}
}
ds . HostIDsByNameFunc = func ( ctx context . Context , filter fleet . TeamFilter , hostnames [ ] string ) ( [ ] uint , error ) {
return [ ] uint { 1234 } , nil
}
2024-01-26 16:00:58 +00:00
ds . LabelIDsByNameFunc = func ( ctx context . Context , labels [ ] string ) ( map [ string ] uint , error ) {
2023-12-15 18:55:39 +00:00
return nil , nil
}
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { } , nil
}
ds . NewQueryFunc = func ( ctx context . Context , query * fleet . Query , opts ... fleet . OptionalArg ) ( * fleet . Query , error ) {
query . ID = 42
return query , nil
}
ds . NewDistributedQueryCampaignFunc = func ( ctx context . Context , camp * fleet . DistributedQueryCampaign ) (
* fleet . DistributedQueryCampaign , error ,
) {
camp . ID = 321
return camp , nil
}
ds . NewDistributedQueryCampaignTargetFunc = func (
ctx context . Context , target * fleet . DistributedQueryCampaignTarget ,
) ( * fleet . DistributedQueryCampaignTarget , error ) {
return target , nil
}
ds . HostIDsInTargetsFunc = func ( ctx context . Context , filter fleet . TeamFilter , targets fleet . HostTargets ) ( [ ] uint , error ) {
return [ ] uint { 1 } , nil
}
ds . CountHostsInTargetsFunc = func (
ctx context . Context , filter fleet . TeamFilter , targets fleet . HostTargets , now time . Time ,
) ( fleet . TargetMetrics , error ) {
return fleet . TargetMetrics { TotalHosts : 1 , OnlineHosts : 1 } , nil
}
lq . On ( "QueriesForHost" , uint ( 1 ) ) . Return (
map [ string ] string {
"42" : "select 42, * from time" ,
} ,
nil ,
)
lq . On ( "QueryCompletedByHost" , "42" , 99 ) . Return ( nil )
lq . On ( "RunQuery" , "321" , "select 42, * from time" , [ ] uint { 1 } ) . Return ( nil )
ds . DistributedQueryCampaignTargetIDsFunc = func ( ctx context . Context , id uint ) ( targets * fleet . HostTargets , err error ) {
return & fleet . HostTargets { HostIDs : [ ] uint { 99 } } , nil
}
ds . DistributedQueryCampaignFunc = func ( ctx context . Context , id uint ) ( * fleet . DistributedQueryCampaign , error ) {
return & fleet . DistributedQueryCampaign {
ID : 321 ,
UserID : admin . ID ,
} , nil
}
ds . SaveDistributedQueryCampaignFunc = func ( ctx context . Context , camp * fleet . DistributedQueryCampaign ) error {
return nil
}
ds . QueryFunc = func ( ctx context . Context , id uint ) ( * fleet . Query , error ) {
return & fleet . Query { } , nil
}
ds . IsSavedQueryFunc = func ( ctx context . Context , queryID uint ) ( bool , error ) {
return false , nil
}
go func ( ) {
time . Sleep ( 2 * time . Second )
require . NoError (
t , rs . WriteResult (
fleet . DistributedQueryResult {
DistributedQueryCampaignID : 321 ,
Rows : [ ] map [ string ] string { { "bing" : "fds" } } ,
Host : fleet . ResultHostData {
ID : 99 ,
Hostname : "somehostname" ,
DisplayName : "somehostname" ,
} ,
Stats : & fleet . Stats {
WallTimeMs : 10 ,
UserTime : 20 ,
SystemTime : 30 ,
Memory : 40 ,
} ,
} ,
) ,
)
} ( )
expected := ` { "host" : "somehostname" , "rows" : [ { "bing" : "fds" , "host_display_name" : "somehostname" , "host_hostname" : "somehostname" } ] }
`
assert . Equal ( t , expected , runAppForTest ( t , [ ] string { "query" , "--hosts" , "1234" , "--query" , "select 42, * from time" } ) )
}