Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor fleet policy APIs, fix JSON issues #129

Merged
merged 3 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions kibana/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,18 @@ func extractError(result []byte) error {
}
}
if err := json.Unmarshal(result, &kibanaResult); err != nil {
return err
return fmt.Errorf("error extracting JSON for error response: %w", err)
}
var errs multierror.Errors
if kibanaResult.Message != "" {
for _, err := range kibanaResult.Attributes.Objects {
errs = append(errs, fmt.Errorf("id: %s, message: %s", err.ID, err.Error.Message))
}
if errs == nil {
return fmt.Errorf("%s", kibanaResult.Message)
}
return fmt.Errorf("%s: %w", kibanaResult.Message, errs.Err())

}
return nil
}
Expand Down Expand Up @@ -139,7 +143,7 @@ func NewClientWithConfig(config *ClientConfig, binaryName, version, commit, buil
return NewClientWithConfigDefault(config, 5601, binaryName, version, commit, buildtime)
}

// NewClientWithConfig creates and returns a kibana client using the given config
// NewClientWithConfigDefault creates and returns a kibana client using the given config
func NewClientWithConfigDefault(config *ClientConfig, defaultPort int, binaryName, version, commit, buildtime string) (*Client, error) {
if err := config.Validate(); err != nil {
return nil, err
Expand Down Expand Up @@ -237,7 +241,7 @@ func (conn *Connection) Request(method, extraPath string,
return resp.StatusCode, result, retError
}

// Sends an application/json request to Kibana with appropriate kbn headers
// Send an application/json request to Kibana with appropriate kbn headers
func (conn *Connection) Send(method, extraPath string,
params url.Values, headers http.Header, body io.Reader) (*http.Response, error) {

Expand Down
157 changes: 113 additions & 44 deletions kibana/fleet.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import (
"encoding/json"
"fmt"
"net/http"
"time"
)

const (
fleetAgentPoliciesAPI = "/api/fleet/agent_policies"
fleetAgentPolicyAPI = "/api/fleet/agent_policies/%s"
fleetAgentsDeleteAPI = "/api/fleet/agent_policies/delete"
fleetEnrollmentAPIKeysAPI = "/api/fleet/enrollment_api_keys" //nolint:gosec // no API key being leaked here
fleetAgentsAPI = "/api/fleet/agents"
fleetAgentAPI = "/api/fleet/agents/%s"
Expand All @@ -40,34 +42,69 @@ const (
// Create Policy
//

// MonitoringEnabledOption is a Kibana JSON value that specifies the various monitoring option types
type MonitoringEnabledOption string

const (
MonitoringEnabledLogs MonitoringEnabledOption = "logs"
// MonitoringEnabledLogs specifies log monitoring
MonitoringEnabledLogs MonitoringEnabledOption = "logs"
// MonitoringEnabledMetrics specifies metrics monitoring
MonitoringEnabledMetrics MonitoringEnabledOption = "metrics"
)

type PolicyCommon struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Description string `json:"description"`
MonitoringEnabled []MonitoringEnabledOption `json:"monitoring_enabled"`
FleetServerHostID string `json:"fleet_server_host_id"`
AgentFeatures []map[string]interface{} `json:"agent_features"`
Status string `json:"status"`
IsManaged bool `json:"is_managed"`
// AgentPolicy is the JSON that represents a agent policy. These fields are used by both the create policy request, and the GET request for an agent policy.
// see: https://github.com/elastic/kibana/blob/v8.8.0/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy_create_request.yaml
// and https://github.com/elastic/kibana/blob/v8.8.0/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy.yaml
type AgentPolicy struct {
ID string `json:"id,omitempty"`
// Name of the policy. Required to create a policy.
Name string `json:"name"`
// Namespace of the policy. Required to create a policy.
Namespace string `json:"namespace"`
Description string `json:"description,omitempty"`
MonitoringEnabled []MonitoringEnabledOption `json:"monitoring_enabled,omitempty"`
DataOutputID string `json:"data_output_id,omitempty"`
MonitoringOutputID string `json:"monitoring_output_id,omitempty"`
FleetServerHostID string `json:"fleet_server_host_id,omitempty"`
DownloadSourceID string `json:"download_source_id,omitempty"`
UnenrollTimeout int `json:"unenroll_timeout,omitempty"`
InactivityTImeout int `json:"inactivity_timeout,omitempty"`
AgentFeatures []map[string]interface{} `json:"agent_features,omitempty"`
}

type PolicyExisting struct {
ID string `json:"id"`
PolicyCommon `json:",inline"`
// PolicyResponse is the response JSON from a policy request
// This is returned on a GET request for a policy, and on a policy create request
// See https://github.com/elastic/kibana/blob/v8.8.0/x-pack/plugins/fleet/common/openapi/paths/agent_policies.yaml
type PolicyResponse struct {
AgentPolicy `json:",inline"`
UpdatedOn time.Time `json:"updated_on"`
UpdatedBy string `json:"updated_by"`
Revision int `json:"revision"`
IsProtected bool `json:"is_protected"`
PackagePolicies []map[string]interface{} `json:"package_policies"`
}

type CreatePolicyRequest PolicyCommon

type CreatePolicyResponse PolicyExisting
// AgentPolicyUpdateRequest is the JSON object for requesting an updated policy
// Unlike the Agent create and response structures, the update request does not contain an ID field.
// See https://github.com/elastic/kibana/blob/v8.8.0/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy_update_request.yaml
type AgentPolicyUpdateRequest struct {
// Name of the policy. Required in an update request.
Name string `json:"name"`
// Namespace of the policy. Required in an update request.
Namespace string `json:"namespace"`
Description string `json:"description,omitempty"`
MonitoringEnabled []MonitoringEnabledOption `json:"monitoring_enabled,omitempty"`
DataOutputID string `json:"data_output_id,omitempty"`
MonitoringOutputID string `json:"monitoring_output_id,omitempty"`
FleetServerHostID string `json:"fleet_server_host_id,omitempty"`
DownloadSourceID string `json:"download_source_id,omitempty"`
UnenrollTimeout int `json:"unenroll_timeout,omitempty"`
InactivityTImeout int `json:"inactivity_timeout,omitempty"`
AgentFeatures []map[string]interface{} `json:"agent_features,omitempty"`
}

func (client *Client) CreatePolicy(request CreatePolicyRequest) (*CreatePolicyResponse, error) {
// CreatePolicy creates a new agent policy with the given config
func (client *Client) CreatePolicy(request AgentPolicy) (*PolicyResponse, error) {
reqBody, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("unable to marshal create policy request into JSON: %w", err)
Expand All @@ -82,7 +119,7 @@ func (client *Client) CreatePolicy(request CreatePolicyRequest) (*CreatePolicyRe
}

var resp struct {
Item CreatePolicyResponse `json:"item"`
Item PolicyResponse `json:"item"`
}

if err := json.Unmarshal(respBody, &resp); err != nil {
Expand All @@ -92,18 +129,9 @@ func (client *Client) CreatePolicy(request CreatePolicyRequest) (*CreatePolicyRe
return &resp.Item, nil
}

//
// Get Policy
//

type GetPolicyRequest struct {
ID string
}

type GetPolicyResponse PolicyExisting

func (client *Client) GetPolicy(request GetPolicyRequest) (*GetPolicyResponse, error) {
apiURL := fmt.Sprintf(fleetAgentPolicyAPI, request.ID)
// GetPolicy returns the requested ID
func (client *Client) GetPolicy(id string) (*PolicyResponse, error) {
apiURL := fmt.Sprintf(fleetAgentPolicyAPI, id)
statusCode, respBody, err := client.Request(http.MethodGet, apiURL, nil, nil, nil)
if err != nil {
return nil, fmt.Errorf("error calling get policy API: %w", err)
Expand All @@ -113,7 +141,7 @@ func (client *Client) GetPolicy(request GetPolicyRequest) (*GetPolicyResponse, e
}

var resp struct {
Item GetPolicyResponse `json:"item"`
Item PolicyResponse `json:"item"`
}

if err := json.Unmarshal(respBody, &resp); err != nil {
Expand All @@ -123,21 +151,14 @@ func (client *Client) GetPolicy(request GetPolicyRequest) (*GetPolicyResponse, e
return &resp.Item, nil
}

//
// Update Policy
//

type UpdatePolicyRequest PolicyExisting

type UpdatePolicyResponse PolicyExisting

func (client *Client) UpdatePolicy(request UpdatePolicyRequest) (*UpdatePolicyResponse, error) {
// UpdatePolicy updates an existing agent policy.
func (client *Client) UpdatePolicy(ID string, request AgentPolicyUpdateRequest) (*PolicyResponse, error) {
reqBody, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("unable to marshal update policy request into JSON: %w", err)
}

apiURL := fmt.Sprintf(fleetAgentPolicyAPI, request.ID)
apiURL := fmt.Sprintf(fleetAgentPolicyAPI, ID)
statusCode, respBody, err := client.Request(http.MethodPut, apiURL, nil, nil, bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("error calling update policy API: %w", err)
Expand All @@ -147,7 +168,7 @@ func (client *Client) UpdatePolicy(request UpdatePolicyRequest) (*UpdatePolicyRe
}

var resp struct {
Item UpdatePolicyResponse `json:"item"`
Item PolicyResponse `json:"item"`
}

if err := json.Unmarshal(respBody, &resp); err != nil {
Expand All @@ -157,15 +178,41 @@ func (client *Client) UpdatePolicy(request UpdatePolicyRequest) (*UpdatePolicyRe
return &resp.Item, nil
}

// DeletePolicy deletes the policy with the given ID
func (client *Client) DeletePolicy(id string) error {
var delRequest = struct {
AgentPolicyID string `json:"agentPolicyId"`
}{
AgentPolicyID: id,
}

reqBody, err := json.Marshal(delRequest)
if err != nil {
return fmt.Errorf("unable to marshal update policy request into JSON: %w", err)
}

statusCode, respBody, err := client.Request(http.MethodPost, fleetAgentsDeleteAPI, nil, nil, bytes.NewReader(reqBody))
if err != nil {
return fmt.Errorf("error calling update policy API: %w", err)
}
if statusCode != 200 {
return fmt.Errorf("unable to update policy; API returned status code [%d] and body [%s]", statusCode, string(respBody))
}

return nil
}

//
// Create Enrollment API Key
//

// CreateEnrollmentAPIKeyRequest is the JSON object for requesting an enrollment API key
type CreateEnrollmentAPIKeyRequest struct {
Name string `json:"name"`
PolicyID string `json:"policy_id"`
}

// CreateEnrollmentAPIKeyResponse is the JSON response the an enrollment key request
type CreateEnrollmentAPIKeyResponse struct {
Active bool `json:"active"`
APIKey string `json:"api_key"`
Expand All @@ -175,6 +222,7 @@ type CreateEnrollmentAPIKeyResponse struct {
PolicyID string `json:"policy_id"`
}

// CreateEnrollmentAPIKey creates an enrollment API key
func (client *Client) CreateEnrollmentAPIKey(request CreateEnrollmentAPIKeyRequest) (*CreateEnrollmentAPIKeyResponse, error) {
reqBody, err := json.Marshal(request)
if err != nil {
Expand Down Expand Up @@ -204,6 +252,7 @@ func (client *Client) CreateEnrollmentAPIKey(request CreateEnrollmentAPIKeyReque
// List Agents
//

// AgentCommon represents common agent data used across APIs
type AgentCommon struct {
Active bool `json:"active"`
Status string `json:"status"`
Expand All @@ -220,20 +269,24 @@ type AgentCommon struct {
PolicyRevision int `json:"policy_revision"`
}

// AgentExisting is the data structure for an existing agent
type AgentExisting struct {
ID string `json:"id"`
AgentCommon `json:",inline"`
}

// ListAgentsRequest is currently unused
type ListAgentsRequest struct {
// For future use
}

// ListAgentsResponse is a list of agents returned by the API
type ListAgentsResponse struct {
Items []AgentExisting `json:"items"`
}

func (client *Client) ListAgents(request ListAgentsRequest) (*ListAgentsResponse, error) {
// ListAgents returns a list of agents known to Kibana
func (client *Client) ListAgents(_ ListAgentsRequest) (*ListAgentsResponse, error) {
statusCode, respBody, err := client.Request(http.MethodGet, fleetAgentsAPI, nil, nil, nil)
if err != nil {
return nil, fmt.Errorf("error calling list agents API: %w", err)
Expand All @@ -255,12 +308,15 @@ func (client *Client) ListAgents(request ListAgentsRequest) (*ListAgentsResponse
// Get Agent
//

// GetAgentRequest contains the ID used for fetching agent data
type GetAgentRequest struct {
ID string
}

// GetAgentResponse is the JSON response for GetAgent
type GetAgentResponse AgentExisting

// GetAgent fetches data for an agent
func (client *Client) GetAgent(request GetAgentRequest) (*GetAgentResponse, error) {
apiURL := fmt.Sprintf(fleetAgentAPI, request.ID)
statusCode, respBody, err := client.Request(http.MethodGet, apiURL, nil, nil, nil)
Expand All @@ -286,15 +342,18 @@ func (client *Client) GetAgent(request GetAgentRequest) (*GetAgentResponse, erro
// Unenroll Agent
//

// UnEnrollAgentRequest is the JSON request for unenrolling an agent
type UnEnrollAgentRequest struct {
ID string `json:"id"`
Revoke bool `json:"revoke"`
}

// UnEnrollAgentResponse is currently unused
type UnEnrollAgentResponse struct {
// For future use
}

// UnEnrollAgent removes the agent from fleet
func (client *Client) UnEnrollAgent(request UnEnrollAgentRequest) (*UnEnrollAgentResponse, error) {
reqBody, err := json.Marshal(request)
if err != nil {
Expand Down Expand Up @@ -323,15 +382,18 @@ func (client *Client) UnEnrollAgent(request UnEnrollAgentRequest) (*UnEnrollAgen
// Upgrade Agent
//

// UpgradeAgentRequest is the JSON request for an agent upgrade
type UpgradeAgentRequest struct {
ID string `json:"id"`
Version string `json:"version"`
}

// UpgradeAgentResponse is currently unused
type UpgradeAgentResponse struct {
// For future use
}

// UpgradeAgent upgrades the requested agent
func (client *Client) UpgradeAgent(request UpgradeAgentRequest) (*UpgradeAgentResponse, error) {
reqBody, err := json.Marshal(request)
if err != nil {
Expand Down Expand Up @@ -360,6 +422,7 @@ func (client *Client) UpgradeAgent(request UpgradeAgentRequest) (*UpgradeAgentRe
// List Fleet Server Hosts
//

// FleetServerHost handles JSON data for fleet server info
type FleetServerHost struct {
ID string `json:"id"`
Name string `json:"name"`
Expand All @@ -368,15 +431,18 @@ type FleetServerHost struct {
IsPreconfigured bool `json:"is_preconfigured"`
}

// ListFleetServerHostsRequest is currently unused
type ListFleetServerHostsRequest struct {
// For future use
}

// ListFleetServerHostsResponse is the JSON response for ListFleetServerHosts
type ListFleetServerHostsResponse struct {
Items []FleetServerHost `json:"items"`
}

func (client *Client) ListFleetServerHosts(request ListFleetServerHostsRequest) (*ListFleetServerHostsResponse, error) {
// ListFleetServerHosts returns a list of fleet server hosts
func (client *Client) ListFleetServerHosts(_ ListFleetServerHostsRequest) (*ListFleetServerHostsResponse, error) {
statusCode, respBody, err := client.Request(http.MethodGet, fleetFleetServerHostsAPI, nil, nil, nil)
if err != nil {
return nil, fmt.Errorf("error calling list fleet server hosts API: %w", err)
Expand All @@ -398,12 +464,15 @@ func (client *Client) ListFleetServerHosts(request ListFleetServerHostsRequest)
// Get Fleet Server Host
//

// GetFleetServerHostRequest is the ID for a request via GetFleetServerHost
type GetFleetServerHostRequest struct {
ID string
}

// GetFleetServerHostResponse is the JSON respose from GetFleetServerHost
type GetFleetServerHostResponse FleetServerHost

// GetFleetServerHost returns data on a fleet server
func (client *Client) GetFleetServerHost(request GetFleetServerHostRequest) (*GetFleetServerHostResponse, error) {
apiURL := fmt.Sprintf(fleetFleetServerHostAPI, request.ID)
statusCode, respBody, err := client.Request(http.MethodGet, apiURL, nil, nil, nil)
Expand Down
Loading