For the following issue: - https://github.com/fleetdm/fleet/issues/41419 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated tooling and documentation wording across scripts, utilities, and infrastructure files to reflect the product terminology change from “teams” to “fleets” (references now note “fleets” and indicate the former “teams” naming). <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Rachael Shaw <r@rachael.wtf>
302 lines
8.4 KiB
Go
302 lines
8.4 KiB
Go
package fleetdm_client
|
|
|
|
// This file gives us a nice API to use to call Fleet's API. It's focused
|
|
// only on fleets (formerly "teams").
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const prefix = "/api/v1/fleet"
|
|
const teamPrefix = prefix + "/teams"
|
|
|
|
type Team struct {
|
|
Name string `json:"name"`
|
|
ID int64 `json:"id"`
|
|
Description string `json:"description"`
|
|
AgentOptions interface{} `json:"agent_options"`
|
|
Scripts interface{} `json:"scripts"`
|
|
Secrets []struct {
|
|
Secret string `json:"secret"`
|
|
Created time.Time `json:"created_at"`
|
|
TeamID int `json:"team_id"`
|
|
} `json:"secrets"`
|
|
}
|
|
|
|
type TeamCreate struct {
|
|
Name string `json:"name,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
|
|
type TeamGetResponse struct {
|
|
Team Team `json:"team"`
|
|
}
|
|
|
|
type TeamQueryResponse struct {
|
|
Teams []Team `json:"teams"`
|
|
}
|
|
|
|
// FleetDMClient is a FleetDM HTTP client that overrides http.DefaultClient.
|
|
type FleetDMClient struct {
|
|
*http.Client
|
|
URL string
|
|
APIKey string
|
|
}
|
|
|
|
// NewFleetDMClient creates a new instance of FleetDMClient with the provided
|
|
// URL and API key.
|
|
func NewFleetDMClient(url, apiKey string) *FleetDMClient {
|
|
// Ensure the URL ends with a trailing slash for bizarre parsing reasons
|
|
if !strings.HasSuffix(url, "/") {
|
|
url += "/"
|
|
}
|
|
return &FleetDMClient{
|
|
Client: http.DefaultClient,
|
|
URL: url,
|
|
APIKey: apiKey,
|
|
}
|
|
}
|
|
|
|
// Do will add necessary headers and call the http.Client.Do method.
|
|
func (c *FleetDMClient) do(req *http.Request, query *url.Values) (*http.Response, error) {
|
|
// Add the API key to the request header
|
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.APIKey))
|
|
req.Header.Add("Accept", `application/json`)
|
|
// Set the request URL based on the client URL
|
|
baseURL, err := url.Parse(c.URL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse base URL %s: %w", c.URL, err)
|
|
}
|
|
req.URL = baseURL.JoinPath(req.URL.Path)
|
|
if query != nil {
|
|
req.URL.RawQuery = query.Encode()
|
|
}
|
|
// Send the request using the embedded http.Client
|
|
return c.Client.Do(req)
|
|
}
|
|
|
|
// TeamNameToID will return the ID of a team given the name.
|
|
func (c *FleetDMClient) TeamNameToID(name string) (int64, error) {
|
|
req, err := http.NewRequest(http.MethodGet, teamPrefix, nil)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to create GET request for %s: %w", teamPrefix, err)
|
|
}
|
|
vals := make(url.Values)
|
|
vals.Add("query", name)
|
|
resp, err := c.do(req, &vals)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to GET %s %s: %w", teamPrefix, name, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return 0, fmt.Errorf("failed to get team: %s %s", name, resp.Status)
|
|
}
|
|
|
|
var teamqry TeamQueryResponse
|
|
b, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to read team response: %w", err)
|
|
}
|
|
err = json.Unmarshal(b, &teamqry)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to decode get team response: %w", err)
|
|
}
|
|
|
|
for _, team := range teamqry.Teams {
|
|
if team.Name == name {
|
|
return team.ID, nil
|
|
}
|
|
}
|
|
|
|
return 0, fmt.Errorf("team %s not found", name)
|
|
}
|
|
|
|
// CreateTeam creates a new team with the provided name and description.
|
|
func (c *FleetDMClient) CreateTeam(name string, description string) (*TeamGetResponse, error) {
|
|
teamCreate := TeamCreate{
|
|
Name: name,
|
|
Description: description,
|
|
}
|
|
nameJson, err := json.Marshal(teamCreate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create team body: %w", err)
|
|
}
|
|
req, err := http.NewRequest(http.MethodPost, teamPrefix, bytes.NewReader(nameJson))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create POST request for %s name %s: %w",
|
|
req.URL.String(), name, err)
|
|
}
|
|
resp, err := c.do(req, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to POST %s name %s: %w",
|
|
req.URL.String(), name, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
b, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("failed to create team %s: %s %s", name, resp.Status, string(b))
|
|
}
|
|
|
|
var newTeam TeamGetResponse
|
|
b, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read team response: %w", err)
|
|
}
|
|
err = json.Unmarshal(b, &newTeam)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
return &newTeam, nil
|
|
}
|
|
|
|
// GetTeam returns the team with the provided ID.
|
|
func (c *FleetDMClient) GetTeam(id int64) (*TeamGetResponse, error) {
|
|
url := teamPrefix + "/" + strconv.FormatInt(id, 10)
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GET request for %s: %w",
|
|
url, err)
|
|
}
|
|
resp, err := c.do(req, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to GET %s: %w", url, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
b, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("failed to get team %d: %s %s", id, resp.Status, string(b))
|
|
}
|
|
|
|
var team TeamGetResponse
|
|
b, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read team response: %w", err)
|
|
}
|
|
err = json.Unmarshal(b, &team)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
return &team, nil
|
|
}
|
|
|
|
// UpdateTeam updates the team with the provided ID with the provided name and description.
|
|
func (c *FleetDMClient) UpdateTeam(id int64, name, description *string) (*TeamGetResponse, error) {
|
|
if name == nil && description == nil {
|
|
return nil, fmt.Errorf("both name and description are nil")
|
|
}
|
|
|
|
url := teamPrefix + "/" + strconv.FormatInt(id, 10)
|
|
var teamUpdate TeamCreate
|
|
if name != nil {
|
|
teamUpdate.Name = *name
|
|
}
|
|
if description != nil {
|
|
teamUpdate.Description = *description
|
|
}
|
|
updateJson, err := json.Marshal(teamUpdate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update team body request: %w", err)
|
|
}
|
|
req, err := http.NewRequest(http.MethodPatch, url, bytes.NewReader(updateJson))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create PATCH request for %s body %s: %w",
|
|
url, updateJson, err)
|
|
}
|
|
resp, err := c.do(req, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to PATCH %s body %s: %w",
|
|
url, updateJson, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
b, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("failed PATCH %s body %s: %s %s",
|
|
url, updateJson, resp.Status, string(b))
|
|
}
|
|
|
|
var newTeam TeamGetResponse
|
|
b, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read team response: %w", err)
|
|
}
|
|
err = json.Unmarshal(b, &newTeam)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
return &newTeam, nil
|
|
}
|
|
|
|
// UpdateAgentOptions pretends that the agent options are a string, when it's really actually json.
|
|
// Strangely the body that comes back is a team, not just the agent options.
|
|
func (c *FleetDMClient) UpdateAgentOptions(id int64, ao string) (*TeamGetResponse, error) {
|
|
|
|
// First verify it's actually json.
|
|
valid := json.Valid([]byte(ao))
|
|
if !valid {
|
|
return nil, fmt.Errorf("agent_options isn't json: %s", ao)
|
|
}
|
|
|
|
aoUrl := teamPrefix + "/" + strconv.FormatInt(id, 10) + "/" + "agent_options"
|
|
req, err := http.NewRequest(http.MethodPost, aoUrl, strings.NewReader(ao))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create agent_options POST request for %s id %d: %w",
|
|
teamPrefix, id, err)
|
|
}
|
|
resp, err := c.do(req, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to POST agent_options %s id %d: %w",
|
|
teamPrefix, id, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
b, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("failed to modify agent_options %d: %s %s", id, resp.Status, string(b))
|
|
}
|
|
|
|
var team TeamGetResponse
|
|
b, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read team response: %w", err)
|
|
}
|
|
err = json.Unmarshal(b, &team)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode agent_options team response: %w", err)
|
|
}
|
|
return &team, nil
|
|
}
|
|
|
|
// DeleteTeam deletes the team with the provided ID.
|
|
func (c *FleetDMClient) DeleteTeam(id int64) error {
|
|
url := teamPrefix + "/" + strconv.FormatInt(id, 10)
|
|
req, err := http.NewRequest(http.MethodDelete, url, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create DELETE request for %s: %w", url, err)
|
|
}
|
|
resp, err := c.do(req, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to DELETE %s: %w", url, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
return fmt.Errorf("failed to delete team %d: %s", id, resp.Status)
|
|
}
|
|
|
|
return nil
|
|
}
|