fleet/tools/terraform/fleetdm_client/fleetdm_client.go

302 lines
8.4 KiB
Go
Raw Normal View History

package fleetdm_client
// This file gives us a nice API to use to call FleetDM's API. It's focused
// only on 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
}