internal/resources/fleetpolicy.go (351 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 resources
import (
"errors"
"fmt"
"slices"
"strings"
"github.com/elastic/go-resource"
"github.com/elastic/elastic-package/internal/common"
"github.com/elastic/elastic-package/internal/kibana"
"github.com/elastic/elastic-package/internal/packages"
)
type FleetAgentPolicy struct {
// Provider is the name of the provider to use, defaults to "kibana".
Provider string
// Name of the policy.
Name string
// ID of the policy.
ID string
// Description of the policy.
Description string
// Namespace to use for the policy.
Namespace string
// DataOutputID is the identifier of the output to use.
DataOutputID string
// Absent is set to true to indicate that the policy should not be present.
Absent bool
// PackagePolicies
PackagePolicies []FleetPackagePolicy
}
type FleetPackagePolicy struct {
// Provider is the name of the provider to use, defaults to "kibana".
Provider string
// Name of the policy.
Name string
// Disabled indicates whether this policy is disabled.
Disabled bool
// TemplateName is the policy template to use from the package manifest.
TemplateName string
// RootPath is the root of the source of the package to configure, from it we should
// be able to read the manifest, the data stream manifests and the policy template to use.
RootPath string
// DataStreamName is the name of the data stream to configure, for integration packages.
DataStreamName string
// InputName is the name of the input to select.
InputName string
// Vars contains the values for the variables.
Vars map[string]any
// DataStreamVars contains the values for the variables at the data stream level.
DataStreamVars map[string]any
// Absent is set to true to indicate that the policy should not be present.
Absent bool
}
func (f *FleetPackagePolicy) String() string {
return fmt.Sprintf("[FleetPackagePolicy:%s:%s]", f.Provider, f.Name)
}
func (f *FleetAgentPolicy) String() string {
return fmt.Sprintf("[FleetAgentPolicy:%s:%s]", f.Provider, f.Name)
}
func (f *FleetAgentPolicy) provider(ctx resource.Context) (*KibanaProvider, error) {
name := f.Provider
if name == "" {
name = DefaultKibanaProviderName
}
var provider *KibanaProvider
ok := ctx.Provider(name, &provider)
if !ok {
return nil, fmt.Errorf("provider %q must be explicitly defined", name)
}
return provider, nil
}
func (f *FleetAgentPolicy) Get(ctx resource.Context) (current resource.ResourceState, err error) {
state := FleetAgentPolicyState{
expected: !f.Absent,
}
if f.ID == "" {
return &state, nil
}
provider, err := f.provider(ctx)
if err != nil {
return nil, err
}
policy, err := provider.Client.GetPolicy(ctx, f.ID)
var notFoundError *kibana.ErrPolicyNotFound
if errors.As(err, ¬FoundError) {
return &state, nil
}
if err != nil {
return nil, fmt.Errorf("could not get policy %q with id %q: %w", f.Name, f.ID, err)
}
state.current = policy
return &state, nil
}
func (f *FleetAgentPolicy) Create(ctx resource.Context) error {
provider, err := f.provider(ctx)
if err != nil {
return err
}
policy, err := provider.Client.CreatePolicy(ctx, kibana.Policy{
ID: f.ID,
Name: f.Name,
Namespace: f.Namespace,
Description: f.Description,
DataOutputID: f.DataOutputID,
})
if err != nil {
return fmt.Errorf("could not create policy %q: %w", f.Name, err)
}
f.ID = policy.ID
for _, packagePolicy := range f.PackagePolicies {
policy, err := createPackagePolicy(*f, packagePolicy)
if err != nil {
return fmt.Errorf("could not prepare package policy: %w", err)
}
err = provider.Client.AddPackageDataStreamToPolicy(ctx, *policy)
if err != nil {
return fmt.Errorf("could not add package policy %q to agent policy %q: %w", packagePolicy.Name, f.Name, err)
}
}
return nil
}
func createPackagePolicy(policy FleetAgentPolicy, packagePolicy FleetPackagePolicy) (*kibana.PackageDataStream, error) {
manifest, err := packages.ReadPackageManifestFromPackageRoot(packagePolicy.RootPath)
if err != nil {
return nil, fmt.Errorf("could not read package manifest at %s: %w", packagePolicy.RootPath, err)
}
switch manifest.Type {
case "integration":
return createIntegrationPackagePolicy(policy, *manifest, packagePolicy)
case "input":
return createInputPackagePolicy(policy, *manifest, packagePolicy)
default:
return nil, fmt.Errorf("package type %q is not supported", manifest.Type)
}
}
func createIntegrationPackagePolicy(policy FleetAgentPolicy, manifest packages.PackageManifest, packagePolicy FleetPackagePolicy) (*kibana.PackageDataStream, error) {
if packagePolicy.DataStreamName == "" {
return nil, fmt.Errorf("expected data stream for integration package policy %q", packagePolicy.Name)
}
dsManifest, err := packages.ReadDataStreamManifestFromPackageRoot(packagePolicy.RootPath, packagePolicy.DataStreamName)
if err != nil {
return nil, fmt.Errorf("could not read %q data stream manifest for package at %s: %w", packagePolicy.DataStreamName, packagePolicy.RootPath, err)
}
policyTemplateName := packagePolicy.TemplateName
if policyTemplateName == "" {
name, err := findPolicyTemplateForDataStream(manifest, *dsManifest, packagePolicy.InputName)
if err != nil {
return nil, fmt.Errorf("failed to determine the associated policy_template: %w", err)
}
policyTemplateName = name
}
policyTemplate, err := selectPolicyTemplateByName(manifest.PolicyTemplates, policyTemplateName)
if err != nil {
return nil, fmt.Errorf("failed to find the selected policy_template: %w", err)
}
ds := kibana.PackageDataStream{
Name: packagePolicy.Name,
Namespace: policy.Namespace,
PolicyID: policy.ID,
Enabled: !packagePolicy.Disabled,
Inputs: []kibana.Input{
{
PolicyTemplate: policyTemplate.Name,
Enabled: !packagePolicy.Disabled,
},
},
}
ds.Package.Name = manifest.Name
ds.Package.Title = manifest.Title
ds.Package.Version = manifest.Version
stream := dsManifest.Streams[getDataStreamIndex(packagePolicy.InputName, *dsManifest)]
streamInput := stream.Input
ds.Inputs[0].Type = streamInput
streams := []kibana.Stream{
{
ID: fmt.Sprintf("%s-%s.%s", streamInput, manifest.Name, dsManifest.Name),
Enabled: !packagePolicy.Disabled,
DataStream: kibana.DataStream{
Type: dsManifest.Type,
Dataset: getDataStreamDataset(manifest, *dsManifest),
},
},
}
// Add dataStream-level vars
streams[0].Vars = setKibanaVariables(stream.Vars, common.MapStr(packagePolicy.DataStreamVars))
ds.Inputs[0].Streams = streams
// Add input-level vars
input := policyTemplate.FindInputByType(streamInput)
if input != nil {
ds.Inputs[0].Vars = setKibanaVariables(input.Vars, common.MapStr(packagePolicy.Vars))
}
// Add package-level vars
ds.Vars = setKibanaVariables(manifest.Vars, common.MapStr(packagePolicy.Vars))
return &ds, nil
}
func createInputPackagePolicy(policy FleetAgentPolicy, manifest packages.PackageManifest, packagePolicy FleetPackagePolicy) (*kibana.PackageDataStream, error) {
if dsName := packagePolicy.DataStreamName; dsName != "" {
return nil, fmt.Errorf("no data stream expected for input package policy %q, found %q", packagePolicy.Name, dsName)
}
policyTemplateName := packagePolicy.TemplateName
if policyTemplateName == "" {
name, err := findPolicyTemplateForInputPackage(manifest, packagePolicy.InputName)
if err != nil {
return nil, fmt.Errorf("failed to determine the associated policy_template: %w", err)
}
policyTemplateName = name
}
policyTemplate, err := selectPolicyTemplateByName(manifest.PolicyTemplates, policyTemplateName)
if err != nil {
return nil, fmt.Errorf("failed to find the selected policy_template: %w", err)
}
ds := kibana.PackageDataStream{
Name: packagePolicy.Name,
Namespace: policy.Namespace,
PolicyID: policy.ID,
Enabled: !packagePolicy.Disabled,
Inputs: []kibana.Input{
{
PolicyTemplate: policyTemplate.Name,
Enabled: !packagePolicy.Disabled,
},
},
}
ds.Package.Name = manifest.Name
ds.Package.Title = manifest.Title
ds.Package.Version = manifest.Version
streamInput := policyTemplate.Input
ds.Inputs[0].Type = streamInput
dataset := fmt.Sprintf("%s.%s", manifest.Name, policyTemplate.Name)
streams := []kibana.Stream{
{
ID: fmt.Sprintf("%s-%s.%s", streamInput, manifest.Name, policyTemplate.Name),
Enabled: !packagePolicy.Disabled,
DataStream: kibana.DataStream{
Type: policyTemplate.Type,
Dataset: dataset,
},
},
}
// Add policyTemplate-level vars.
vars := setKibanaVariables(policyTemplate.Vars, common.MapStr(packagePolicy.Vars))
if _, found := vars["data_stream.dataset"]; !found {
var value packages.VarValue
value.Unpack(dataset)
vars["data_stream.dataset"] = kibana.Var{
Value: value,
Type: "text",
}
}
streams[0].Vars = vars
ds.Inputs[0].Streams = streams
return &ds, nil
}
func setKibanaVariables(definitions []packages.Variable, values common.MapStr) kibana.Vars {
vars := kibana.Vars{}
for _, definition := range definitions {
val := definition.Default
value, err := values.GetValue(definition.Name)
if err == nil {
val = packages.VarValue{}
val.Unpack(value)
}
vars[definition.Name] = kibana.Var{
Type: definition.Type,
Value: val,
}
}
return vars
}
func (f *FleetAgentPolicy) Update(ctx resource.Context) error {
if f.Absent {
provider, err := f.provider(ctx)
if err != nil {
return err
}
err = provider.Client.DeletePolicy(ctx, f.ID)
if err != nil {
return fmt.Errorf("could not delete policy %q: %w", f.Name, err)
}
return nil
}
return errors.New("update not implemented")
}
type FleetAgentPolicyState struct {
current *kibana.Policy
expected bool
}
func (s *FleetAgentPolicyState) Found() bool {
return !s.expected || s.current != nil
}
func (s *FleetAgentPolicyState) NeedsUpdate(resource resource.Resource) (bool, error) {
policy := resource.(*FleetAgentPolicy)
return policy.Absent == (s.current != nil), nil
}
// getDataStreamIndex returns the index of the data stream whose input name
// matches. Otherwise it returns the 0.
func getDataStreamIndex(inputName string, ds packages.DataStreamManifest) int {
for i, s := range ds.Streams {
if s.Input == inputName {
return i
}
}
return 0
}
func getDataStreamDataset(pkg packages.PackageManifest, ds packages.DataStreamManifest) string {
if len(ds.Dataset) > 0 {
return ds.Dataset
}
return fmt.Sprintf("%s.%s", pkg.Name, ds.Name)
}
func findPolicyTemplateForDataStream(pkg packages.PackageManifest, ds packages.DataStreamManifest, inputName string) (string, error) {
if inputName == "" {
if len(ds.Streams) == 0 {
return "", errors.New("no streams declared in data stream manifest")
}
inputName = ds.Streams[getDataStreamIndex(inputName, ds)].Input
}
var matchedPolicyTemplates []string
for _, policyTemplate := range pkg.PolicyTemplates {
// Does this policy_template include this input type?
if policyTemplate.FindInputByType(inputName) == nil {
continue
}
// Does the policy_template apply to this data stream (when data streams are specified)?
if len(policyTemplate.DataStreams) > 0 && !slices.Contains(policyTemplate.DataStreams, ds.Name) {
continue
}
matchedPolicyTemplates = append(matchedPolicyTemplates, policyTemplate.Name)
}
switch len(matchedPolicyTemplates) {
case 1:
return matchedPolicyTemplates[0], nil
case 0:
return "", fmt.Errorf("no policy template was found for data stream %q "+
"with input type %q: verify that you have included the data stream "+
"and input in the package's policy_template list", ds.Name, inputName)
default:
return "", fmt.Errorf("ambiguous result: multiple policy templates ([%s]) "+
"were found that apply to data stream %q with input type %q: please "+
"specify the 'policy_template' in the system test config",
strings.Join(matchedPolicyTemplates, ", "), ds.Name, inputName)
}
}
func findPolicyTemplateForInputPackage(pkg packages.PackageManifest, inputName string) (string, error) {
if inputName == "" {
if len(pkg.PolicyTemplates) == 0 {
return "", errors.New("no policy templates specified for input package")
}
inputName = pkg.PolicyTemplates[0].Input
}
var matched []string
for _, policyTemplate := range pkg.PolicyTemplates {
if policyTemplate.Input != inputName {
continue
}
matched = append(matched, policyTemplate.Name)
}
switch len(matched) {
case 1:
return matched[0], nil
case 0:
return "", fmt.Errorf("no policy template was found"+
"with input type %q: verify that you have included the data stream "+
"and input in the package's policy_template list", inputName)
default:
return "", fmt.Errorf("ambiguous result: multiple policy templates ([%s]) "+
"with input type %q: please "+
"specify the 'policy_template' in the system test config",
strings.Join(matched, ", "), inputName)
}
}
func selectPolicyTemplateByName(policies []packages.PolicyTemplate, name string) (packages.PolicyTemplate, error) {
for _, policy := range policies {
if policy.Name == name {
return policy, nil
}
}
return packages.PolicyTemplate{}, fmt.Errorf("policy template %q not found", name)
}