From 2cab2ce1da6877e09066db6662ddfada1166d5e2 Mon Sep 17 00:00:00 2001 From: michel-laterman Date: Mon, 13 Apr 2026 11:59:40 -0700 Subject: [PATCH 1/3] Decode all opamp agent capabilites Decode all capabilites an opamp agent can send. --- ...32-Decode-all-opamp-agent-capabilites.yaml | 32 +++++++++++++++++++ docs/opamp.md | 4 --- internal/pkg/api/handleOpAMP.go | 20 ++++-------- 3 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 changelog/fragments/1776106832-Decode-all-opamp-agent-capabilites.yaml diff --git a/changelog/fragments/1776106832-Decode-all-opamp-agent-capabilites.yaml b/changelog/fragments/1776106832-Decode-all-opamp-agent-capabilites.yaml new file mode 100644 index 0000000000..8470523ae2 --- /dev/null +++ b/changelog/fragments/1776106832-Decode-all-opamp-agent-capabilites.yaml @@ -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 diff --git a/docs/opamp.md b/docs/opamp.md index 6dc6f2858c..13a1134d12 100644 --- a/docs/opamp.md +++ b/docs/opamp.md @@ -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. diff --git a/internal/pkg/api/handleOpAMP.go b/internal/pkg/api/handleOpAMP.go index be6fa695a5..c367a4fe41 100644 --- a/internal/pkg/api/handleOpAMP.go +++ b/internal/pkg/api/handleOpAMP.go @@ -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) @@ -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 { + result = append(result, strings.TrimPrefix(name, "AgentCapabilities_")) } } return result From e3febaa6c306438149f7cbfddd9e2e51344d195c Mon Sep 17 00:00:00 2001 From: michel-laterman Date: Mon, 13 Apr 2026 12:20:17 -0700 Subject: [PATCH 2/3] Silence linter --- internal/pkg/api/handleOpAMP.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkg/api/handleOpAMP.go b/internal/pkg/api/handleOpAMP.go index c367a4fe41..ebdb3b748a 100644 --- a/internal/pkg/api/handleOpAMP.go +++ b/internal/pkg/api/handleOpAMP.go @@ -538,7 +538,7 @@ func ProtobufKVToRawMessage(zlog zerolog.Logger, kv []*protobufs.KeyValue) (json func decodeCapabilities(caps uint64) []string { var result []string for mask, name := range protobufs.AgentCapabilities_name { - if caps&uint64(mask) != 0 { + 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_")) } } From d5dcb766cd38c82536e0a97ef902c191aa4920f8 Mon Sep 17 00:00:00 2001 From: michel-laterman Date: Mon, 13 Apr 2026 15:55:47 -0700 Subject: [PATCH 3/3] add unit tests --- internal/pkg/api/handleOpAMP_test.go | 74 ++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/internal/pkg/api/handleOpAMP_test.go b/internal/pkg/api/handleOpAMP_test.go index 5653269449..a4eff2f367 100644 --- a/internal/pkg/api/handleOpAMP_test.go +++ b/internal/pkg/api/handleOpAMP_test.go @@ -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()