internal/kibana/policies.go (247 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package kibana
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/elastic/elastic-package/internal/packages"
)
// Policy represents an Agent Policy in Fleet.
type Policy struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Description string `json:"description"`
Namespace string `json:"namespace"`
Revision int `json:"revision,omitempty"`
MonitoringEnabled []string `json:"monitoring_enabled,omitempty"`
MonitoringOutputID string `json:"monitoring_output_id,omitempty"`
DataOutputID string `json:"data_output_id,omitempty"`
IsDefaultFleetServer bool `json:"is_default_fleet_server,omitempty"`
}
// DownloadedPolicy represents a policy as returned by the download policy API.
type DownloadedPolicy json.RawMessage
// CreatePolicy persists the given Policy in Fleet.
func (c *Client) CreatePolicy(ctx context.Context, p Policy) (*Policy, error) {
reqBody, err := json.Marshal(p)
if err != nil {
return nil, fmt.Errorf("could not convert policy (request) to JSON: %w", err)
}
statusCode, respBody, err := c.post(ctx, fmt.Sprintf("%s/agent_policies", FleetAPI), reqBody)
if err != nil {
return nil, fmt.Errorf("could not create policy: %w", err)
}
if statusCode == http.StatusConflict {
return nil, fmt.Errorf("could not create policy: %w", ErrConflict)
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("could not create policy; API status code = %d; response body = %s", statusCode, respBody)
}
var resp struct {
Item Policy `json:"item"`
}
if err := json.Unmarshal(respBody, &resp); err != nil {
return nil, fmt.Errorf("could not convert policy (response) to JSON: %w", err)
}
return &resp.Item, nil
}
type ErrPolicyNotFound struct {
id string
}
func (e *ErrPolicyNotFound) Error() string {
return fmt.Sprintf("policy %s not found", e.id)
}
// GetPolicy fetches the given Policy in Fleet.
func (c *Client) GetPolicy(ctx context.Context, policyID string) (*Policy, error) {
statusCode, respBody, err := c.get(ctx, fmt.Sprintf("%s/agent_policies/%s", FleetAPI, policyID))
if err != nil {
return nil, fmt.Errorf("could not get policy: %w", err)
}
if statusCode == http.StatusNotFound {
return nil, &ErrPolicyNotFound{id: policyID}
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("could not get policy; API status code = %d; response body = %s", statusCode, respBody)
}
var resp struct {
Item Policy `json:"item"`
}
if err := json.Unmarshal(respBody, &resp); err != nil {
return nil, fmt.Errorf("could not convert policy (response) to JSON: %w", err)
}
return &resp.Item, nil
}
// DownloadPolicy fetches the agent Policy as would be downloaded by an agent.
func (c *Client) DownloadPolicy(ctx context.Context, policyID string) (DownloadedPolicy, error) {
statusCode, respBody, err := c.get(ctx, fmt.Sprintf("%s/agent_policies/%s/download", FleetAPI, policyID))
if err != nil {
return nil, fmt.Errorf("could not get policy: %w", err)
}
if statusCode == http.StatusNotFound {
return nil, &ErrPolicyNotFound{id: policyID}
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("could not get policy; API status code = %d; response body = %s", statusCode, respBody)
}
return respBody, nil
}
// GetRawPolicy fetches the given Policy with all the fields in Fleet.
func (c *Client) GetRawPolicy(ctx context.Context, policyID string) (json.RawMessage, error) {
statusCode, respBody, err := c.get(ctx, fmt.Sprintf("%s/agent_policies/%s", FleetAPI, policyID))
if err != nil {
return nil, fmt.Errorf("could not get policy: %w", err)
}
if statusCode == http.StatusNotFound {
return nil, &ErrPolicyNotFound{id: policyID}
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("could not get policy; API status code = %d; response body = %s", statusCode, respBody)
}
var resp struct {
Item json.RawMessage `json:"item"`
}
if err := json.Unmarshal(respBody, &resp); err != nil {
return nil, fmt.Errorf("could not convert policy (response) to JSON: %w", err)
}
return resp.Item, nil
}
// ListRawPolicies fetches all the Policies in Fleet.
func (c *Client) ListRawPolicies(ctx context.Context) ([]json.RawMessage, error) {
itemsRetrieved := 0
currentPage := 1
var items []json.RawMessage
var resp struct {
Items []json.RawMessage `json:"items"`
Total int `json:"total"`
Page int `json:"page"`
PerPage int `json:"perPage"`
}
for finished := false; !finished; finished = itemsRetrieved == resp.Total {
statusCode, respBody, err := c.get(ctx, fmt.Sprintf("%s/agent_policies?full=true&page=%d", FleetAPI, currentPage))
if err != nil {
return nil, fmt.Errorf("could not get policies: %w", err)
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("could not get policies; API status code = %d; response body = %s", statusCode, respBody)
}
if err := json.Unmarshal(respBody, &resp); err != nil {
return nil, fmt.Errorf("could not convert policies (response) to JSON: %w", err)
}
itemsRetrieved += len(resp.Items)
currentPage += 1
items = append(items, resp.Items...)
}
return items, nil
}
// DeletePolicy removes the given Policy from Fleet.
func (c *Client) DeletePolicy(ctx context.Context, policyID string) error {
reqBody := `{ "agentPolicyId": "` + policyID + `" }`
statusCode, respBody, err := c.post(ctx, fmt.Sprintf("%s/agent_policies/delete", FleetAPI), []byte(reqBody))
if err != nil {
return fmt.Errorf("could not delete policy: %w", err)
}
if statusCode == http.StatusNotFound {
return &ErrPolicyNotFound{id: policyID}
}
if statusCode != http.StatusOK {
return fmt.Errorf("could not delete policy; API status code = %d; response body = %s", statusCode, respBody)
}
return nil
}
// Var represents a single variable at the package or
// data stream level, encapsulating the data type of the
// variable and it's value.
type Var struct {
Value packages.VarValue `json:"value"`
Type string `json:"type"`
}
// Vars is a collection of variables either at the package or
// data stream level.
type Vars map[string]Var
// DataStream represents a data stream within a package.
type DataStream struct {
Type string `json:"type"`
Dataset string `json:"dataset"`
}
// Stream encapsulates a data stream and it's variables.
type Stream struct {
ID string `json:"id"`
Enabled bool `json:"enabled"`
DataStream DataStream `json:"data_stream"`
Vars Vars `json:"vars"`
}
// Input represents a package-level input.
type Input struct {
PolicyTemplate string `json:"policy_template,omitempty"` // Name of policy_template from the package manifest that contains this input. If not specified the Kibana uses the first policy_template.
Type string `json:"type"`
Enabled bool `json:"enabled"`
Streams []Stream `json:"streams"`
Vars Vars `json:"vars,omitempty"`
}
// PackageDataStream represents a request to add a single package's single data stream to a
// Policy in Fleet.
type PackageDataStream struct {
Name string `json:"name"`
Description string `json:"description"`
Namespace string `json:"namespace"`
PolicyID string `json:"policy_id"`
Enabled bool `json:"enabled"`
OutputID string `json:"output_id"`
Inputs []Input `json:"inputs"`
Vars Vars `json:"vars,omitempty"`
Package struct {
Name string `json:"name"`
Title string `json:"title"`
Version string `json:"version"`
} `json:"package"`
}
// AddPackageDataStreamToPolicy adds a PackageDataStream to a Policy in Fleet.
func (c *Client) AddPackageDataStreamToPolicy(ctx context.Context, r PackageDataStream) error {
reqBody, err := json.Marshal(r)
if err != nil {
return fmt.Errorf("could not convert policy-package (request) to JSON: %w", err)
}
statusCode, respBody, err := c.post(ctx, fmt.Sprintf("%s/package_policies", FleetAPI), reqBody)
if err != nil {
return fmt.Errorf("could not add package to policy: %w", err)
}
if statusCode != http.StatusOK {
return fmt.Errorf("could not add package to policy; API status code = %d; response body = %s", statusCode, respBody)
}
return nil
}
// PackagePolicy represents an Package Policy in Fleet.
type PackagePolicy struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Description string `json:"description"`
Namespace string `json:"namespace"`
PolicyID string `json:"policy_id"`
Package struct {
Name string `json:"name"`
Version string `json:"version"`
} `json:"package"`
Inputs map[string]PackagePolicyInput `json:"inputs,omitempty"`
Force bool `json:"force"`
}
type PackagePolicyInput struct {
Enabled bool `json:"enabled"`
Vars map[string]interface{} `json:"vars,omitempty"`
Streams map[string]PackagePolicyStream `json:"streams,omitempty"`
}
type PackagePolicyStream struct {
Enabled bool `json:"enabled"`
Vars map[string]interface{} `json:"vars,omitempty"`
}
// CreatePackagePolicy persists the given Package Policy in Fleet.
func (c *Client) CreatePackagePolicy(ctx context.Context, p PackagePolicy) (*PackagePolicy, error) {
reqBody, err := json.Marshal(p)
if err != nil {
return nil, fmt.Errorf("could not convert package policy (request) to JSON: %w", err)
}
statusCode, respBody, err := c.post(ctx, fmt.Sprintf("%s/package_policies", FleetAPI), reqBody)
if err != nil {
return nil, fmt.Errorf("could not create package policy (req %s): %w", string(reqBody), err)
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("could not create package policy (req %s); API status code = %d; response body = %s", string(reqBody), statusCode, respBody)
}
// Response format for the policy is inconsistent with the creation one,
// we update the ID to avoid having a full type only to hold the response.
var resp struct {
Item struct {
ID string `json:"id"`
} `json:"item"`
}
if err := json.Unmarshal(respBody, &resp); err != nil {
return nil, fmt.Errorf("could not convert package policy (response) to JSON: %w", err)
}
p.ID = resp.Item.ID
return &p, nil
}
// DeletePackagePolicy removes the given Package Policy from Fleet.
func (c *Client) DeletePackagePolicy(ctx context.Context, p PackagePolicy) error {
statusCode, respBody, err := c.delete(ctx, fmt.Sprintf("%s/package_policies/%s", FleetAPI, p.ID))
if err != nil {
return fmt.Errorf("could not delete package policy: %w", err)
}
if statusCode != http.StatusOK {
return fmt.Errorf("could not delete package policy; API status code = %d; response body = %s", statusCode, respBody)
}
return nil
}