Skip to content
33 changes: 30 additions & 3 deletions internal/pkg/api/handleOpAMP.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const (
kOpAMPMod = "opAMP"
serverCapabilities = uint64(protobufs.ServerCapabilities_ServerCapabilities_AcceptsStatus |
protobufs.ServerCapabilities_ServerCapabilities_AcceptsEffectiveConfig)
tagsKey = "tags"
)

type OpAMPT struct {
Expand Down Expand Up @@ -288,6 +289,7 @@ func (oa *OpAMPT) enrollAgent(zlog zerolog.Logger, agentID string, aToS *protobu
meta := localMetadata{}
meta.Elastic.Agent.ID = agentID
agentType := ""
var tags []string
var identifyingAttributes, nonIdentifyingAttributes json.RawMessage
if aToS.AgentDescription != nil {
// Extract agent version
Expand All @@ -302,7 +304,7 @@ func (oa *OpAMPT) enrollAgent(zlog zerolog.Logger, agentID string, aToS *protobu
}
zlog.Debug().Str("opamp.agent.version", meta.Elastic.Agent.Version).Msg("extracted agent version")

// Extract hostname
// Extract hostname and tags
for _, nia := range aToS.AgentDescription.NonIdentifyingAttributes {
switch attribute.Key(nia.Key) {
case semconv.HostNameKey:
Expand All @@ -312,6 +314,12 @@ func (oa *OpAMPT) enrollAgent(zlog zerolog.Logger, agentID string, aToS *protobu
case semconv.OSTypeKey:
osType := nia.GetValue().GetStringValue()
meta.Os.Platform = osType
case tagsKey:
for _, t := range strings.Split(nia.GetValue().GetStringValue(), ",") {
if t = strings.TrimSpace(t); t != "" {
tags = append(tags, t)
}
}
}
}
zlog.Debug().Str("hostname", meta.Host.Hostname).Msg("extracted hostname")
Expand All @@ -321,7 +329,13 @@ func (oa *OpAMPT) enrollAgent(zlog zerolog.Logger, agentID string, aToS *protobu
return nil, fmt.Errorf("failed to marshal identifying attributes: %w", err)
}

nonIdentifyingAttributes, err = ProtobufKVToRawMessage(zlog, aToS.AgentDescription.NonIdentifyingAttributes)
filteredNIA := make([]*protobufs.KeyValue, 0, len(aToS.AgentDescription.NonIdentifyingAttributes))
for _, nia := range aToS.AgentDescription.NonIdentifyingAttributes {
if nia.Key != tagsKey {
filteredNIA = append(filteredNIA, nia)
}
}
nonIdentifyingAttributes, err = ProtobufKVToRawMessage(zlog, filteredNIA)
if err != nil {
return nil, fmt.Errorf("failed to marshal non-identifying attributes: %w", err)
}
Expand Down Expand Up @@ -349,7 +363,7 @@ func (oa *OpAMPT) enrollAgent(zlog zerolog.Logger, agentID string, aToS *protobu
IdentifyingAttributes: identifyingAttributes,
NonIdentifyingAttributes: nonIdentifyingAttributes,
Type: "OPAMP",
Tags: []string{agentType},
Tags: dedupeSlice(append([]string{agentType}, tags...)),
}

data, err = json.Marshal(agent)
Expand Down Expand Up @@ -558,6 +572,19 @@ func isActiveStatus(status string) bool {
status == string(CheckinRequestStatusDegraded)
}

// dedupeSlice returns a copy of s with duplicate entries removed, preserving order.
func dedupeSlice(s []string) []string {
seen := make(map[string]struct{}, len(s))
result := make([]string, 0, len(s))
for _, v := range s {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
result = append(result, v)
}
}
return result
}

// decodeCapabilities converts capability bitmask to human-readable strings
func decodeCapabilities(caps uint64) []string {
var result []string
Expand Down
101 changes: 101 additions & 0 deletions internal/pkg/api/handleOpAMP_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,107 @@ func TestEnrollAgentWithAgentToServerMessage(t *testing.T) {
bulker.AssertExpectations(t)
}

func TestEnrollAgentTags(t *testing.T) {
cases := []struct {
name string
tagsValue string
wantTags []string
otherNIAKeys []string
}{
{
name: "no tags attribute",
tagsValue: "",
wantTags: []string{"otel-collector"},
otherNIAKeys: []string{string(semconv.HostNameKey)},
},
{
name: "single tag",
tagsValue: "dev",
wantTags: []string{"otel-collector", "dev"},
otherNIAKeys: []string{string(semconv.HostNameKey)},
},
{
name: "multiple tags",
tagsValue: "dev,west,us-west-1a",
Comment thread
juliaElastic marked this conversation as resolved.
wantTags: []string{"otel-collector", "dev", "west", "us-west-1a"},
otherNIAKeys: []string{string(semconv.HostNameKey)},
},
{
name: "tags with spaces",
tagsValue: " dev , west , us-west-1a ",
wantTags: []string{"otel-collector", "dev", "west", "us-west-1a"},
otherNIAKeys: []string{string(semconv.HostNameKey)},
},
Comment thread
ycombinator marked this conversation as resolved.
{
name: "duplicate of agent type is removed",
tagsValue: "otel-collector,dev",
wantTags: []string{"otel-collector", "dev"},
otherNIAKeys: []string{string(semconv.HostNameKey)},
},
{
name: "duplicate tags within list are removed",
tagsValue: "dev,west,dev",
wantTags: []string{"otel-collector", "dev", "west"},
otherNIAKeys: []string{string(semconv.HostNameKey)},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
bulker := ftesting.NewMockBulk()
enrollKey := model.EnrollmentAPIKey{ //nolint:gosec // test data, not real credentials
APIKeyID: "enroll-key-id",
PolicyID: "policy-123",
Active: true,
}
enrollKeyBytes, err := json.Marshal(enrollKey) //nolint:gosec // test data, not real credentials
require.NoError(t, err)
bulker.On("Search", mock.Anything, dl.FleetEnrollmentAPIKeys, mock.Anything, mock.Anything).
Return(&es.ResultT{HitsT: es.HitsT{Hits: []es.HitT{{Source: enrollKeyBytes}}}}, nil)
bulker.On("Create", mock.Anything, dl.FleetAgents, "agent-123", mock.Anything, mock.Anything).
Return("doc-id", nil)

nia := []*protobufs.KeyValue{
{
Key: string(semconv.HostNameKey),
Value: &protobufs.AnyValue{Value: &protobufs.AnyValue_StringValue{StringValue: "host-1"}},
},
}
if tc.tagsValue != "" {
nia = append(nia, &protobufs.KeyValue{
Key: "tags",
Value: &protobufs.AnyValue{Value: &protobufs.AnyValue_StringValue{StringValue: tc.tagsValue}},
})
}

msg := &protobufs.AgentToServer{
AgentDescription: &protobufs.AgentDescription{
IdentifyingAttributes: []*protobufs.KeyValue{
{
Key: string(semconv.ServiceNameKey),
Value: &protobufs.AnyValue{Value: &protobufs.AnyValue_StringValue{StringValue: "otel-collector"}},
},
},
NonIdentifyingAttributes: nia,
},
}

oa := &OpAMPT{bulk: bulker}
zlog := zerolog.New(io.Discard)
agent, err := oa.enrollAgent(zlog, "agent-123", msg, &apikey.APIKey{ID: "enroll-key-id"})
require.NoError(t, err)
require.Equal(t, tc.wantTags, agent.Tags)

// tags must not appear in stored NonIdentifyingAttributes
var niMap map[string]interface{}
require.NoError(t, json.Unmarshal(agent.NonIdentifyingAttributes, &niMap))
require.NotContains(t, niMap, "tags")
for _, k := range tc.otherNIAKeys {
require.Contains(t, niMap, k, "expected NIA key %q to be present", k)
}
})
}
}

func TestUpdateAgentWithAgentToServerMessage(t *testing.T) {
checker := &mockCheckin{}
oa := &OpAMPT{bc: checker}
Expand Down
Loading