Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Kind can be one of:
# - breaking-change: a change to previously-documented behavior
# - deprecation: functionality that is being removed in a later release
# - bug-fix: fixes a problem in a previous version
# - enhancement: extends functionality but does not break or fix existing behavior
# - feature: new functionality
# - known-issue: problems that we are aware of in a given version
# - security: impacts on the security of a product or a user’s deployment.
# - upgrade: important information for someone upgrading from a prior version
# - other: does not fit into any of the other categories
kind: bug-fix

# Change summary; a 80ish characters long description of the change.
summary: Decode all opamp-agent capabilites

# Long description; in case the summary is not enough to describe the change
# this field accommodate a description without length limits.
# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment.
#description:

# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
component: fleet-server

# PR URL; optional; the PR number that added the changeset.
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number.
# Please provide it if you are adding a fragment for a different PR.
#pr: https://github.com/owner/repo/1234

# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
# If not present is automatically filled by the tooling with the issue linked to the PR number.
issue: https://github.com/elastic/fleet-server/issues/6790
4 changes: 0 additions & 4 deletions docs/opamp.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ The following fields are ignored:
- **Sensitive value redaction.** Fleet-server redacts keys containing `password`, `token`, `key`, `secret`, `auth`, `certificate`, or `passphrase` from the effective config before persisting.
- **YAML-to-JSON conversion.** Fleet-server parses the effective config body as YAML and re-serializes it to JSON for storage.

### Capabilities

- **Partial capability decoding.** Fleet-server only decodes 6 of the 16 defined `AgentCapabilities` bits: `ReportsStatus`, `AcceptsRemoteConfig`, `ReportsEffectiveConfig`, `ReportsHealth`, `ReportsAvailableComponents`, `AcceptsRestartCommand`. Other capability bits are silently ignored.

### Throttling

- **HTTP-level rate limiting only.** The spec defines throttling via `ServerErrorResponse` with `UNAVAILABLE` type and `RetryInfo`. Fleet-server uses HTTP-level rate limiting middleware (returning 429) and returns 429 for Elasticsearch auth rate limits, but does not use the protobuf-level `RetryInfo` mechanism. Additionally, fleet-server may silenty drop connections before the TLS handshake completes if the server is overloaded.
Expand Down
20 changes: 7 additions & 13 deletions internal/pkg/api/handleOpAMP.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,10 @@ func (oa *OpAMPT) updateAgent(zlog zerolog.Logger, agent *model.Agent, aToS *pro
initialOpts = append(initialOpts, checkin.WithStatus(status))
initialOpts = append(initialOpts, checkin.WithSequenceNum(aToS.SequenceNum))

capabilities := decodeCapabilities(aToS.Capabilities)
initialOpts = append(initialOpts, checkin.WithCapabilities(capabilities))
if aToS.Capabilities != 0 {
capabilities := decodeCapabilities(aToS.Capabilities)
initialOpts = append(initialOpts, checkin.WithCapabilities(capabilities))
}

if aToS.EffectiveConfig != nil {
effectiveConfigBytes, err := ParseEffectiveConfig(aToS.EffectiveConfig)
Expand Down Expand Up @@ -535,17 +537,9 @@ func ProtobufKVToRawMessage(zlog zerolog.Logger, kv []*protobufs.KeyValue) (json
// decodeCapabilities converts capability bitmask to human-readable strings
func decodeCapabilities(caps uint64) []string {
var result []string
capMap := map[uint64]string{
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsStatus): "ReportsStatus",
uint64(protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig): "AcceptsRemoteConfig",
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig): "ReportsEffectiveConfig",
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsHealth): "ReportsHealth",
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsAvailableComponents): "ReportsAvailableComponents",
uint64(protobufs.AgentCapabilities_AgentCapabilities_AcceptsRestartCommand): "AcceptsRestartCommand",
}
for mask, name := range capMap {
if caps&mask != 0 {
result = append(result, name)
for mask, name := range protobufs.AgentCapabilities_name {
if caps&uint64(mask) != 0 { //nolint:gosec // mask values are not negative so no overflow is possible here
result = append(result, strings.TrimPrefix(name, "AgentCapabilities_"))
}
}
return result
Expand Down
74 changes: 74 additions & 0 deletions internal/pkg/api/handleOpAMP_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,80 @@ func pendingFromOptions(t *testing.T, opts []checkin.Option) reflect.Value {
return pendingPtr.Elem()
}

func TestDecodeCapabilities(t *testing.T) {
cases := []struct {
name string
caps uint64
want []string
}{
{
name: "zero returns empty",
caps: 0,
want: nil,
},
{
name: "single capability",
caps: uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsHealth),
want: []string{"ReportsHealth"},
},
{
name: "multiple capabilities",
caps: uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsHealth) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsOwnLogs),
want: []string{"AcceptsRemoteConfig", "ReportsOwnLogs", "ReportsHealth"},
},
{
name: "all capabilities",
caps: uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsStatus) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsOwnTraces) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsOwnMetrics) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsOwnLogs) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_AcceptsOpAMPConnectionSettings) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_AcceptsOtherConnectionSettings) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_AcceptsRestartCommand) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsHealth) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsRemoteConfig) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsHeartbeat) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsAvailableComponents) |
uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsConnectionSettingsStatus),
want: []string{
"ReportsStatus",
"AcceptsRemoteConfig",
"ReportsEffectiveConfig",
"AcceptsPackages",
"ReportsPackageStatuses",
"ReportsOwnTraces",
"ReportsOwnMetrics",
"ReportsOwnLogs",
"AcceptsOpAMPConnectionSettings",
"AcceptsOtherConnectionSettings",
"AcceptsRestartCommand",
"ReportsHealth",
"ReportsRemoteConfig",
"ReportsHeartbeat",
"ReportsAvailableComponents",
"ReportsConnectionSettingsStatus",
},
},
{
name: "unknown bits are ignored",
caps: uint64(protobufs.AgentCapabilities_AgentCapabilities_ReportsHealth) | (1 << 40),
want: []string{"ReportsHealth"},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := decodeCapabilities(tc.caps)
require.ElementsMatch(t, tc.want, got)
})
}
}

func getUnexportedField(v reflect.Value, name string) reflect.Value {
field := v.FieldByName(name)
return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem()
Expand Down
Loading