Files
fleet/server/service/client_live_query.go
T
Scott Gress 4e35de2ac3 Update fleetctl client urls and params (#41463)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #41385 

# Details

This PR updates `fleetctl` to use the new API urls and params when
communicating with Fleet server. This avoids deprecation warnings
showing up on the server that users won't be able to fix.

Most of the changes are straightforward `team_id` -> `fleet_id`. A
couple of code changes have been pointed out. The most interesting is in
icon URLs, which can be persisted in the database (so we'll need to do a
migration in Fleet 5 if we want to drop support for `team_id`.

Similarly the FMA download urls are briefly persisted in the db for the
purpose of sending MDM commands. If we drop team_id support in Fleet 5
there could be a brief window where there are unprocessed commands in
the db still with `team_id` in them, so we'll probably want to migrate
those as well.

# Checklist for submitter

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

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
n/a - all internal

## Testing

- [X] Added/updated automated tests
- [X] QA'd all new/changed functionality manually
- [X] ran `fleetctl gitops` on main and saw a bunch of deprecation
warnings, ran it on this branch and the warnings were gone 💨
  - [X] same with `fleetctl generate-gitops`
- [X] ran `fleetctl get` commands and verified that the new URLs and
params were used
- [X] ran `fleetctl apply` commands and verified that the new URLs and
params were used
2026-03-13 08:38:55 -05:00

186 lines
5.1 KiB
Go

package service
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"flag"
"net/http"
"sync/atomic"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
ws "github.com/fleetdm/fleet/v4/server/websocket"
"github.com/gorilla/websocket"
)
// LiveQueryResultsHandler provides access to all of the information about an
// incoming stream of live query results.
type LiveQueryResultsHandler struct {
errors chan error
results chan fleet.DistributedQueryResult
totals atomic.Value // real type: targetTotals
status atomic.Value // real type: campaignStatus
}
func NewLiveQueryResultsHandler() *LiveQueryResultsHandler {
return &LiveQueryResultsHandler{
errors: make(chan error),
results: make(chan fleet.DistributedQueryResult),
}
}
// Errors returns a read channel that includes any errors returned by the
// server or receiving the results.
func (h *LiveQueryResultsHandler) Errors() <-chan error {
return h.errors
}
// Results returns a read channel including any received results
func (h *LiveQueryResultsHandler) Results() <-chan fleet.DistributedQueryResult {
return h.results
}
// Totals returns the current metadata of hosts targeted by the query
func (h *LiveQueryResultsHandler) Totals() *targetTotals {
t := h.totals.Load()
if t != nil {
return t.(*targetTotals)
}
return nil
}
func (h *LiveQueryResultsHandler) Status() *campaignStatus {
s := h.status.Load()
if s != nil {
return s.(*campaignStatus)
}
return nil
}
// LiveQuery creates a new live query and begins streaming results.
func (c *Client) LiveQuery(query string, queryID *uint, labels []string, hostIdentifiers []string) (*LiveQueryResultsHandler, error) {
return c.LiveQueryWithContext(context.Background(), query, queryID, labels, hostIdentifiers)
}
func (c *Client) LiveQueryWithContext(
ctx context.Context, query string, queryID *uint, labels []string, hostIdentifiers []string,
) (*LiveQueryResultsHandler, error) {
req := createDistributedQueryCampaignByIdentifierRequest{
QueryID: queryID,
QuerySQL: query,
Selected: distributedQueryCampaignTargetsByIdentifiers{Labels: labels, Hosts: hostIdentifiers},
}
verb, path := "POST", "/api/latest/fleet/reports/run_by_identifiers"
var responseBody createDistributedQueryCampaignResponse
err := c.authenticatedRequest(req, verb, path, &responseBody)
if err != nil {
return nil, ctxerr.Errorf(ctx, "create live query: %v", err)
}
// Copy default dialer but skip cert verification if set.
dialer := &websocket.Dialer{
Proxy: http.ProxyFromEnvironment,
HandshakeTimeout: 45 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.insecureSkipVerify},
}
wssURL := *c.baseURL
wssURL.Scheme = "wss"
if flag.Lookup("test.v") != nil {
wssURL.Scheme = "ws"
}
wssURL.Path = c.urlPrefix + "/api/latest/fleet/results/websocket"
// Ensure custom headers (set by config) are added to websocket request
headers := make(http.Header)
for k, v := range c.customHeaders {
headers.Set(k, v)
}
conn, _, err := dialer.Dial(wssURL.String(), headers)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "upgrade live query result websocket")
}
// Cannot defer connection closing here because we need it to remain
// open for the goroutine below. Manually close for the couple of error
// cases below until we enter that goroutine.
err = conn.WriteJSON(ws.JSONMessage{
Type: "auth",
Data: map[string]interface{}{"token": c.token},
})
if err != nil {
_ = conn.Close()
return nil, ctxerr.Wrap(ctx, err, "auth for results")
}
err = conn.WriteJSON(ws.JSONMessage{
Type: "select_campaign",
Data: map[string]interface{}{"campaign_id": responseBody.Campaign.ID},
})
if err != nil {
_ = conn.Close()
return nil, ctxerr.Wrap(ctx, err, "selecting results")
}
resHandler := NewLiveQueryResultsHandler()
go func() {
defer conn.Close()
for {
msg := struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}{}
doneReadingChan := make(chan error)
go func() {
doneReadingChan <- conn.ReadJSON(&msg)
}()
select {
case <-ctx.Done():
return
case err := <-doneReadingChan:
if err != nil {
resHandler.errors <- ctxerr.Wrap(ctx, err, "receive ws message")
if errors.Is(err, websocket.ErrCloseSent) {
return
}
}
}
close(doneReadingChan)
switch msg.Type {
case "result":
var res fleet.DistributedQueryResult
if err := json.Unmarshal(msg.Data, &res); err != nil {
resHandler.errors <- ctxerr.Wrap(ctx, err, "unmarshal results")
}
resHandler.results <- res
case "totals":
var totals targetTotals
if err := json.Unmarshal(msg.Data, &totals); err != nil {
resHandler.errors <- ctxerr.Wrap(ctx, err, "unmarshal totals")
}
resHandler.totals.Store(&totals)
case "status":
var status campaignStatus
if err := json.Unmarshal(msg.Data, &status); err != nil {
resHandler.errors <- ctxerr.Wrap(ctx, err, "unmarshal status")
}
resHandler.status.Store(&status)
default:
resHandler.errors <- ctxerr.Errorf(ctx, "unknown msg type %s", msg.Type)
}
}
}()
return resHandler, nil
}