Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e2eb13a
Allow agent to override fleet settings
michalpristas Dec 2, 2025
df77bf7
Remove duplicate test
michalpristas Dec 2, 2025
4ae51b3
nil check
michalpristas Dec 2, 2025
1929087
moved implementation to custom patcher
michalpristas Dec 3, 2025
1651558
added missing headers
michalpristas Dec 3, 2025
3a5a76f
add output to test
michalpristas Dec 4, 2025
58a2557
reverted patcher
michalpristas Dec 4, 2025
d80817c
updated tests
michalpristas Dec 4, 2025
cb7fe0c
Merge branch 'main' of github.com:elastic/elastic-agent into feat/all…
michalpristas Dec 4, 2025
34717c0
Merge branch 'main' into feat/allow-fleet-config-override
michalpristas Dec 5, 2025
a8f379c
check permissions for root for capabilities
michalpristas Dec 5, 2025
cfb47b3
Merge branch 'feat/allow-fleet-config-override' of github.com:michalp…
michalpristas Dec 5, 2025
7ff467c
wrapping because different uid type on windows
michalpristas Dec 5, 2025
aa22ec2
ignore not found
michalpristas Dec 5, 2025
feb9132
differentiate between desired and accidental root
michalpristas Dec 5, 2025
b13ce20
differentiate between desired and accidental root
michalpristas Dec 5, 2025
a34fd86
skip check uid owner for backward compatibility
michalpristas Dec 5, 2025
7bc72a1
t push Merge branch 'main' of github.com:elastic/elastic-agent into f…
michalpristas Dec 5, 2025
4e8cf1b
separate functions for ownership check to improve readability
michalpristas Dec 10, 2025
baf469b
Merge branch 'main' of github.com:elastic/elastic-agent into feat/all…
michalpristas Dec 10, 2025
a2709d9
disably overrides when containerized
michalpristas Dec 10, 2025
9822981
Merge branch 'main' into feat/allow-fleet-config-override
michalpristas Dec 10, 2025
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
13 changes: 11 additions & 2 deletions internal/pkg/agent/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,11 @@ func New(
log.Info("Parsed configuration and determined agent is in Fleet Server bootstrap mode")

compModifiers = append(compModifiers, FleetServerComponentModifier(cfg.Fleet.Server))
configMgr = coordinator.NewConfigPatchManager(newFleetServerBootstrapManager(log), PatchAPMConfig(log, rawConfig))
configMgr = coordinator.NewConfigPatchManager(
newFleetServerBootstrapManager(log),
PatchAPMConfig(log, rawConfig),
PatchFleetConfig(log, rawConfig, caps, isManaged),
)
} else {
log.Info("Parsed configuration and determined agent is managed by Fleet")

Expand Down Expand Up @@ -259,7 +263,12 @@ func New(
if err != nil {
return nil, nil, nil, err
}
configMgr = coordinator.NewConfigPatchManager(managed, injectOutputOverrides(log, rawConfig), PatchAPMConfig(log, rawConfig))
configMgr = coordinator.NewConfigPatchManager(
managed,
injectOutputOverrides(log, rawConfig),
PatchAPMConfig(log, rawConfig),
PatchFleetConfig(log, rawConfig, caps, isManaged),
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
agent:
monitoring:
enabled: true
use_output: default
logs: false
metrics: true
http:
port: 6774
32 changes: 32 additions & 0 deletions internal/pkg/agent/application/fleet_config_patcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.

package application

import (
"github.com/elastic/elastic-agent/internal/pkg/agent/application/coordinator"
"github.com/elastic/elastic-agent/internal/pkg/capabilities"
"github.com/elastic/elastic-agent/internal/pkg/config"
"github.com/elastic/elastic-agent/pkg/core/logger"
)

func PatchFleetConfig(log *logger.Logger,
rawConfig *config.Config, // original config from fleet, this one won't reload
caps capabilities.Capabilities,
isManaged bool,
) func(change coordinator.ConfigChange) coordinator.ConfigChange {
if !isManaged || // no need to override fleet config when not managed
caps == nil || !caps.AllowFleetOverride() {
return noop
}

return func(change coordinator.ConfigChange) coordinator.ConfigChange {
newConfig := change.Config()
if err := newConfig.Merge(rawConfig); err != nil {
log.Errorf("error merging fleet config into config change: %v", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure you just want to log on failure? In the context of agentless, this would leave the pod running without valid configuration wouldn'it it?

How would you alert on that or gate promotion on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see this as non fatal for use cases we have today. we want to allow monitoring of agent, liveness checks... what's more important getting data in or being observable. my take is former, but if you think this should be fatal I can fail hard

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking forward a bit I think we can likely use this to inject a rate limit processor required by all agentless integrations, using the OTel processors section that can be used to add global processors with some other not quite done work.

I think if you only look at the liveness issue it isn't fatal, but I can see this being used for things in the future that should be fatal.

In the context of agentless I would rather have incomplete configs be fatal errors so we always know there is a problem, than silently continue to run with a configuration we don't want (e.g. missing rate limit for example).

}

return change
}
}
91 changes: 91 additions & 0 deletions internal/pkg/agent/application/fleet_config_patcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.

package application

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/elastic/elastic-agent/internal/pkg/agent/configuration"
"github.com/elastic/elastic-agent/internal/pkg/config"
"github.com/elastic/elastic-agent/pkg/core/logger/loggertest"
)

func Test_FleetPatcher(t *testing.T) {
configFile := filepath.Join(".", "coordinator", "testdata", "overrides.yml")

testCases := []struct {
name string
isManaged bool
featureEnable bool
expectedLogs bool
}{
{name: "managed - enabled", isManaged: true, featureEnable: true, expectedLogs: false},
{name: "managed - disabled", isManaged: true, featureEnable: false, expectedLogs: true},
{name: "not managed - enabled", isManaged: false, featureEnable: true, expectedLogs: true},
{name: "not managed - disabled", isManaged: false, featureEnable: false, expectedLogs: true},
}

overridesFile, err := os.OpenFile(configFile, os.O_RDONLY, 0)
require.NoError(t, err)
defer overridesFile.Close()

rawConfig, err := config.NewConfigFrom(overridesFile)
require.NoError(t, err)

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
caps := &mockCapabilities{}
caps.On("AllowFleetOverride").Return(tc.featureEnable)

log, _ := loggertest.New(t.Name())

cfg, err := config.LoadFile(filepath.Join(".", "coordinator", "testdata", "config.yaml"))
require.NoError(t, err)

configChange := &mockConfigChange{
c: cfg,
}

patcher := PatchFleetConfig(log, rawConfig, caps, tc.isManaged)
patcher(configChange)

c := &configuration.Configuration{}
require.NoError(t, cfg.Agent.Unpack(&c))
assert.Equal(t, tc.expectedLogs, c.Settings.MonitoringConfig.MonitorLogs)
require.True(t, c.Settings.MonitoringConfig.MonitorMetrics)
require.True(t, c.Settings.MonitoringConfig.Enabled)
})
}
}

type mockCapabilities struct {
mock.Mock
}

func (m *mockCapabilities) AllowUpgrade(version string, sourceURI string) bool {
args := m.Called(version, sourceURI)
return args.Bool(0)
}

func (m *mockCapabilities) AllowInput(name string) bool {
args := m.Called()
return args.Bool(0)
}

func (m *mockCapabilities) AllowOutput(name string) bool {
args := m.Called()
return args.Bool(0)
}

func (m *mockCapabilities) AllowFleetOverride() bool {
args := m.Called()
return args.Bool(0)
}
22 changes: 15 additions & 7 deletions internal/pkg/capabilities/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ type Capabilities interface {
AllowUpgrade(version string, sourceURI string) bool
AllowInput(name string) bool
AllowOutput(name string) bool
AllowFleetOverride() bool
}

type capabilitiesManager struct {
log *logger.Logger
inputChecks []*stringMatcher
outputChecks []*stringMatcher
upgradeCaps []*upgradeCapability
log *logger.Logger
inputChecks []*stringMatcher
outputChecks []*stringMatcher
upgradeCaps []*upgradeCapability
fleetOverrideCaps []*fleetOverrideCapability
}

func (cm *capabilitiesManager) AllowInput(inputType string) bool {
Expand All @@ -40,6 +42,10 @@ func (cm *capabilitiesManager) AllowUpgrade(version string, uri string) bool {
return allowUpgrade(cm.log, version, uri, cm.upgradeCaps)
}

func (cm *capabilitiesManager) AllowFleetOverride() bool {
return allowFleetOverride(cm.log, cm.fleetOverrideCaps)
}

func LoadFile(capsFile string, log *logger.Logger) (Capabilities, error) {
// load capabilities from file
fd, err := os.Open(capsFile)
Expand Down Expand Up @@ -68,8 +74,10 @@ func Load(capsReader io.Reader, log *logger.Logger) (Capabilities, error) {
caps := spec.Capabilities

return &capabilitiesManager{
inputChecks: caps.inputChecks,
outputChecks: caps.outputChecks,
upgradeCaps: caps.upgradeChecks,
log: log,
inputChecks: caps.inputChecks,
outputChecks: caps.outputChecks,
upgradeCaps: caps.upgradeChecks,
fleetOverrideCaps: caps.fleetOverrideChecks,
}, nil
}
42 changes: 42 additions & 0 deletions internal/pkg/capabilities/fleet_override.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.

package capabilities

import "github.com/elastic/elastic-agent/pkg/core/logger"

type fleetOverrideCapability struct {
// Whether a successful condition check lets an upgrade proceed or blocks it
rule allowOrDeny
}

func newFleetOverrideCapability(rule allowOrDeny) *fleetOverrideCapability {
return &fleetOverrideCapability{
rule: rule,
}
}

func allowFleetOverride(
log *logger.Logger,
fleetOverrideCaps []*fleetOverrideCapability,
) bool {
// first match wins
for _, cap := range fleetOverrideCaps {
if cap == nil {
// being defensive here, should not happen
continue
}

switch cap.rule {
case ruleTypeAllow:
log.Debugf("Fleet override allowed by capability")
return true
case ruleTypeDeny:
log.Debugf("Fleet override denied by capability")
return false
}
}
// No matching capability found, disable by default
return false
}
16 changes: 13 additions & 3 deletions internal/pkg/capabilities/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import (
// capabilitiesList deserializes a YAML list of capabilities into organized
// arrays based on their type, for easy use by capabilitiesManager.
type capabilitiesList struct {
inputChecks []*stringMatcher
outputChecks []*stringMatcher
upgradeChecks []*upgradeCapability
inputChecks []*stringMatcher
outputChecks []*stringMatcher
upgradeChecks []*upgradeCapability
fleetOverrideChecks []*fleetOverrideCapability
}

// a type for capability values that must equal "allow" or "deny", enforced
Expand Down Expand Up @@ -81,6 +82,15 @@ func (r *capabilitiesList) UnmarshalYAML(unmarshal func(interface{}) error) erro
return err
}
r.upgradeChecks = append(r.upgradeChecks, cap)
} else if _, found = mm["fleet_override"]; found {
spec := struct {
Type allowOrDeny `yaml:"rule"`
}{}
if err := yaml.Unmarshal(partialYaml, &spec); err != nil {
return err
}
cap := newFleetOverrideCapability(spec.Type)
r.fleetOverrideChecks = append(r.fleetOverrideChecks, cap)
} else {
return fmt.Errorf("unexpected capability type for definition number '%d'", i)
}
Expand Down