Skip to content

Commit 19d0782

Browse files
louisallmitali-salvisky333999
authored
OTLP/ContainerInsights - KSM Node-metadata Enrichment (#2072)
Co-authored-by: Mitali Salvi <44349099+mitali-salvi@users.noreply.github.com> Co-authored-by: Kaushik Surya <108111936+sky333999@users.noreply.github.com>
1 parent f347e7f commit 19d0782

19 files changed

Lines changed: 1926 additions & 35 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ CWAGENT_VERSION
1010
terraform.*
1111
**/.terraform/*
1212
coverage.txt
13+
AGENTS.md

cfg/envconfig/envconfig.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const (
3838
CWAgentMergedOtelConfig = "CWAGENT_MERGED_OTEL_CONFIG"
3939
CWAgentLogsBackpressureMode = "CWAGENT_LOGS_BACKPRESSURE_MODE"
4040
SystemMetricsEnabled = "SYSTEM_METRICS_ENABLED"
41+
OtelCIVersion = "OTEL_CI_VERSION"
4142

4243
// confused deputy prevention related headers
4344
AmzSourceAccount = "AMZ_SOURCE_ACCOUNT" // populates the "x-amz-source-account" header

extension/entitystore/ec2Info.go

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ const (
2525
)
2626

2727
type EC2Info struct {
28-
InstanceID string
29-
AccountID string
28+
InstanceID string
29+
AccountID string
30+
InstanceType string
31+
ImageID string
32+
AvailabilityZone string
33+
Hostname string
3034

3135
// region is used while making call to describeTags Ec2 API for AutoScalingGroup
3236
Region string
@@ -42,7 +46,7 @@ func (ei *EC2Info) initEc2Info() {
4246
return
4347
}
4448
ei.logger.Debug("Initializing EC2Info")
45-
if err := ei.setInstanceIDAccountID(); err != nil {
49+
if err := ei.setEC2Metadata(); err != nil {
4650
return
4751
}
4852
ei.logger.Debug("Finished initializing EC2Info")
@@ -60,11 +64,35 @@ func (ei *EC2Info) GetAccountID() string {
6064
return ei.AccountID
6165
}
6266

63-
func (ei *EC2Info) setInstanceIDAccountID() error {
67+
func (ei *EC2Info) GetInstanceType() string {
68+
ei.mutex.RLock()
69+
defer ei.mutex.RUnlock()
70+
return ei.InstanceType
71+
}
72+
73+
func (ei *EC2Info) GetImageID() string {
74+
ei.mutex.RLock()
75+
defer ei.mutex.RUnlock()
76+
return ei.ImageID
77+
}
78+
79+
func (ei *EC2Info) GetAvailabilityZone() string {
80+
ei.mutex.RLock()
81+
defer ei.mutex.RUnlock()
82+
return ei.AvailabilityZone
83+
}
84+
85+
func (ei *EC2Info) GetHostname() string {
86+
ei.mutex.RLock()
87+
defer ei.mutex.RUnlock()
88+
return ei.Hostname
89+
}
90+
91+
func (ei *EC2Info) setEC2Metadata() error {
6492
for {
6593
metadataDoc, err := ei.metadataProvider.Get(context.Background())
6694
if err != nil {
67-
ei.logger.Debug("Failed to get Instance ID / Account ID through metadata provider", zap.Error(err))
95+
ei.logger.Debug("Failed to get EC2 metadata through metadata provider", zap.Error(err))
6896
wait := time.NewTimer(1 * time.Minute)
6997
select {
7098
case <-ei.done:
@@ -74,14 +102,24 @@ func (ei *EC2Info) setInstanceIDAccountID() error {
74102
continue
75103
}
76104
}
77-
ei.logger.Debug("Successfully retrieved Instance ID and Account ID")
105+
ei.logger.Debug("Successfully retrieved EC2 metadata")
106+
107+
hostname, err := ei.metadataProvider.Hostname(context.Background())
108+
if err != nil {
109+
ei.logger.Warn("Failed to get hostname from metadata provider, proceeding without it", zap.Error(err))
110+
}
111+
78112
ei.mutex.Lock()
79113
ei.InstanceID = metadataDoc.InstanceID
80114
if idLength := len(ei.InstanceID); idLength > instanceIdSizeMax {
81115
ei.logger.Warn("InstanceId length exceeds characters limit and will be ignored", zap.Int("length", idLength), zap.Int("character limit", instanceIdSizeMax))
82116
ei.InstanceID = ""
83117
}
84118
ei.AccountID = metadataDoc.AccountID
119+
ei.InstanceType = metadataDoc.InstanceType
120+
ei.ImageID = metadataDoc.ImageID
121+
ei.AvailabilityZone = metadataDoc.AvailabilityZone
122+
ei.Hostname = hostname
85123
ei.mutex.Unlock()
86124
return nil
87125
}

extension/entitystore/ec2Info_test.go

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,28 @@ import (
1717
)
1818

1919
var mockedInstanceIdentityDoc = &ec2metadata.EC2InstanceIdentityDocument{
20-
InstanceID: "i-01d2417c27a396e44",
21-
AccountID: "874389809020",
22-
Region: "us-east-1",
23-
InstanceType: "m5ad.large",
24-
ImageID: "ami-09edd32d9b0990d49",
20+
InstanceID: "i-01d2417c27a396e44",
21+
AccountID: "874389809020",
22+
Region: "us-east-1",
23+
InstanceType: "m5ad.large",
24+
ImageID: "ami-09edd32d9b0990d49",
25+
AvailabilityZone: "us-east-1a",
2526
}
2627

2728
var mockedInstanceIdentityDocWithLargeInstanceId = &ec2metadata.EC2InstanceIdentityDocument{
28-
InstanceID: "i-01d2417c27a396e44394824728",
29-
AccountID: "874389809020",
30-
Region: "us-east-1",
31-
InstanceType: "m5ad.large",
32-
ImageID: "ami-09edd32d9b0990d49",
29+
InstanceID: "i-01d2417c27a396e44394824728",
30+
AccountID: "874389809020",
31+
Region: "us-east-1",
32+
InstanceType: "m5ad.large",
33+
ImageID: "ami-09edd32d9b0990d49",
34+
AvailabilityZone: "us-east-1a",
3335
}
3436

3537
var (
3638
tagVal3 = "ASG-1"
3739
)
3840

39-
func TestSetInstanceIDAccountID(t *testing.T) {
41+
func TestSetEC2Metadata(t *testing.T) {
4042
type args struct {
4143
metadataProvider ec2metadataprovider.MetadataProvider
4244
}
@@ -53,8 +55,12 @@ func TestSetInstanceIDAccountID(t *testing.T) {
5355
},
5456
wantErr: false,
5557
want: EC2Info{
56-
InstanceID: mockedInstanceIdentityDoc.InstanceID,
57-
AccountID: mockedInstanceIdentityDoc.AccountID,
58+
InstanceID: mockedInstanceIdentityDoc.InstanceID,
59+
AccountID: mockedInstanceIdentityDoc.AccountID,
60+
InstanceType: mockedInstanceIdentityDoc.InstanceType,
61+
ImageID: mockedInstanceIdentityDoc.ImageID,
62+
AvailabilityZone: mockedInstanceIdentityDoc.AvailabilityZone,
63+
Hostname: "MockHostName",
5864
},
5965
},
6066
{
@@ -64,8 +70,12 @@ func TestSetInstanceIDAccountID(t *testing.T) {
6470
},
6571
wantErr: false,
6672
want: EC2Info{
67-
InstanceID: "",
68-
AccountID: mockedInstanceIdentityDocWithLargeInstanceId.AccountID,
73+
InstanceID: "",
74+
AccountID: mockedInstanceIdentityDocWithLargeInstanceId.AccountID,
75+
InstanceType: mockedInstanceIdentityDocWithLargeInstanceId.InstanceType,
76+
ImageID: mockedInstanceIdentityDocWithLargeInstanceId.ImageID,
77+
AvailabilityZone: mockedInstanceIdentityDocWithLargeInstanceId.AvailabilityZone,
78+
Hostname: "MockHostName",
6979
},
7080
},
7181
}
@@ -76,11 +86,15 @@ func TestSetInstanceIDAccountID(t *testing.T) {
7686
metadataProvider: tt.args.metadataProvider,
7787
logger: logger,
7888
}
79-
if err := ei.setInstanceIDAccountID(); (err != nil) != tt.wantErr {
80-
t.Errorf("setInstanceIDAccountID() error = %v, wantErr %v", err, tt.wantErr)
89+
if err := ei.setEC2Metadata(); (err != nil) != tt.wantErr {
90+
t.Errorf("setEC2Metadata() error = %v, wantErr %v", err, tt.wantErr)
8191
}
8292
assert.Equal(t, tt.want.InstanceID, ei.GetInstanceID())
8393
assert.Equal(t, tt.want.AccountID, ei.GetAccountID())
94+
assert.Equal(t, tt.want.InstanceType, ei.GetInstanceType())
95+
assert.Equal(t, tt.want.ImageID, ei.GetImageID())
96+
assert.Equal(t, tt.want.AvailabilityZone, ei.GetAvailabilityZone())
97+
assert.Equal(t, tt.want.Hostname, ei.GetHostname())
8498
})
8599
}
86100
}
@@ -157,3 +171,32 @@ func TestNotInitIfMetadataProviderIsEmpty(t *testing.T) {
157171
})
158172
}
159173
}
174+
175+
func TestGettersReturnEmptyBeforeInit(t *testing.T) {
176+
ei := &EC2Info{}
177+
assert.Equal(t, "", ei.GetInstanceID())
178+
assert.Equal(t, "", ei.GetAccountID())
179+
assert.Equal(t, "", ei.GetInstanceType())
180+
assert.Equal(t, "", ei.GetImageID())
181+
assert.Equal(t, "", ei.GetAvailabilityZone())
182+
assert.Equal(t, "", ei.GetHostname())
183+
}
184+
185+
func TestHostnameFailureProceedsWithoutIt(t *testing.T) {
186+
logger, _ := zap.NewDevelopment()
187+
ei := &EC2Info{
188+
metadataProvider: &mockMetadataProvider{
189+
InstanceIdentityDocument: mockedInstanceIdentityDoc,
190+
HostnameError: true,
191+
},
192+
logger: logger,
193+
}
194+
err := ei.setEC2Metadata()
195+
assert.NoError(t, err, "should succeed even when Hostname() fails")
196+
// Hostname is empty but all other fields are populated
197+
assert.Equal(t, "", ei.GetHostname())
198+
assert.Equal(t, mockedInstanceIdentityDoc.InstanceID, ei.GetInstanceID())
199+
assert.Equal(t, mockedInstanceIdentityDoc.InstanceType, ei.GetInstanceType())
200+
assert.Equal(t, mockedInstanceIdentityDoc.ImageID, ei.GetImageID())
201+
assert.Equal(t, mockedInstanceIdentityDoc.AvailabilityZone, ei.GetAvailabilityZone())
202+
}

extension/entitystore/extension.go

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package entitystore
55

66
import (
77
"context"
8+
"os"
89
"time"
910

1011
"github.com/aws/aws-sdk-go/aws"
@@ -16,8 +17,11 @@ import (
1617
"go.opentelemetry.io/collector/extension"
1718
"go.uber.org/atomic"
1819
"go.uber.org/zap"
20+
"k8s.io/client-go/kubernetes"
21+
"k8s.io/client-go/rest"
1922

2023
configaws "github.com/aws/amazon-cloudwatch-agent/cfg/aws"
24+
"github.com/aws/amazon-cloudwatch-agent/cfg/envconfig"
2125
"github.com/aws/amazon-cloudwatch-agent/internal/ec2metadataprovider"
2226
"github.com/aws/amazon-cloudwatch-agent/internal/retryer"
2327
"github.com/aws/amazon-cloudwatch-agent/plugins/processors/awsentity/entityattributes"
@@ -76,6 +80,10 @@ type EntityStore struct {
7680
metadataprovider ec2metadataprovider.MetadataProvider
7781

7882
podTerminationCheckInterval time.Duration
83+
84+
// leaseWriter creates and renews a Kubernetes Lease with IMDS metadata
85+
// for KSM node metadata enrichment
86+
leaseWriter *LeaseWriter
7987
}
8088

8189
var _ extension.Extension = (*EntityStore)(nil)
@@ -102,6 +110,12 @@ func (e *EntityStore) Start(ctx context.Context, host component.Host) error {
102110
// https://github.com/kubernetes/cloud-provider-aws/issues/762
103111
if e.kubernetesMode == "" {
104112
go e.serviceprovider.startServiceProvider()
113+
} else if getEnv(envconfig.OtelCIVersion) != "" {
114+
e.startLeaseWriter()
115+
} else {
116+
e.logger.Debug("Skipping LeaseWriter - OTEL_CI_VERSION not set",
117+
zap.String("OTEL_CI_VERSION", getEnv(envconfig.OtelCIVersion)),
118+
)
105119
}
106120
}
107121
if e.kubernetesMode != "" {
@@ -114,6 +128,9 @@ func (e *EntityStore) Start(ctx context.Context, host component.Host) error {
114128
}
115129

116130
func (e *EntityStore) Shutdown(_ context.Context) error {
131+
if e.leaseWriter != nil {
132+
e.leaseWriter.Stop()
133+
}
117134
close(e.done)
118135
if e.eksInfo != nil && e.eksInfo.podToServiceEnvMap != nil {
119136
e.eksInfo.podToServiceEnvMap.Stop()
@@ -260,21 +277,60 @@ func (e *EntityStore) createServiceKeyAttributes(serviceAttr ServiceAttribute) m
260277
return serviceKeyAttr
261278
}
262279

263-
var getMetaDataProvider = func() ec2metadataprovider.MetadataProvider {
264-
mdCredentialConfig := &configaws.CredentialConfig{}
265-
return ec2metadataprovider.NewMetadataProvider(mdCredentialConfig.Credentials(), retryer.GetDefaultRetryNumber())
266-
}
280+
// startLeaseWriter initializes and starts the LeaseWriter for publishing
281+
// IMDS metadata as a Kubernetes Lease. Must be called after ec2Info is
282+
// initialized (the LeaseWriter's waitForEC2Info handles the race).
283+
func (e *EntityStore) startLeaseWriter() {
284+
if e.leaseWriter != nil {
285+
return
286+
}
287+
nodeName := getEnv("K8S_NODE_NAME")
288+
if nodeName == "" {
289+
e.logger.Error("K8S_NODE_NAME env var not set, skipping LeaseWriter startup")
290+
return
291+
}
292+
namespace := getEnv("K8S_NAMESPACE")
293+
if namespace == "" {
294+
namespace = "amazon-cloudwatch"
295+
}
267296

268-
var getEC2Provider = func(region string, ec2CredentialConfig *configaws.CredentialConfig) ec2iface.EC2API {
269-
ec2CredentialConfig.Region = region
270-
return ec2.New(
271-
ec2CredentialConfig.Credentials(),
272-
&aws.Config{
273-
LogLevel: configaws.SDKLogLevel(),
274-
Logger: configaws.SDKLogger{},
275-
})
297+
k8sConfig, err := getK8sConfig()
298+
if err != nil {
299+
e.logger.Error("Failed to create in-cluster K8s config for LeaseWriter", zap.Error(err))
300+
return
301+
}
302+
clientset, err := kubernetes.NewForConfig(k8sConfig)
303+
if err != nil {
304+
e.logger.Error("Failed to create K8s clientset for LeaseWriter", zap.Error(err))
305+
return
306+
}
307+
308+
lw := NewLeaseWriter(&e.ec2Info, nodeName, namespace, clientset.CoordinationV1(), e.logger)
309+
lw.Start()
310+
e.leaseWriter = lw
276311
}
277312

313+
var (
314+
// Package-level vars for testability.
315+
getEnv = os.Getenv
316+
getK8sConfig = rest.InClusterConfig
317+
318+
getMetaDataProvider = func() ec2metadataprovider.MetadataProvider {
319+
mdCredentialConfig := &configaws.CredentialConfig{}
320+
return ec2metadataprovider.NewMetadataProvider(mdCredentialConfig.Credentials(), retryer.GetDefaultRetryNumber())
321+
}
322+
323+
getEC2Provider = func(region string, ec2CredentialConfig *configaws.CredentialConfig) ec2iface.EC2API {
324+
ec2CredentialConfig.Region = region
325+
return ec2.New(
326+
ec2CredentialConfig.Credentials(),
327+
&aws.Config{
328+
LogLevel: configaws.SDKLogLevel(),
329+
Logger: configaws.SDKLogger{},
330+
})
331+
}
332+
)
333+
278334
func addNonEmptyToMap(m map[string]*string, key, value string) {
279335
if value != "" {
280336
m[key] = aws.String(value)

0 commit comments

Comments
 (0)