<!-- 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
186 lines
5.1 KiB
Go
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
|
|
}
|