Skip to content

fix(detector-aws): extract full container ID from ECS Fargate cgroup #2855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
Expand Up @@ -163,10 +163,8 @@ export class AwsEcsDetector implements ResourceDetector {
);
const splitData = rawData.trim().split('\n');
for (const str of splitData) {
if (str.length > AwsEcsDetector.CONTAINER_ID_LENGTH) {
containerId = str.substring(
str.length - AwsEcsDetector.CONTAINER_ID_LENGTH
);
containerId = this._extractContainerIdFromLine(str);
if (containerId) {
break;
}
}
Expand All @@ -176,6 +174,74 @@ export class AwsEcsDetector implements ResourceDetector {
return containerId;
}

/**
* Extract container ID from a cgroup line.
* Handles the new AWS ECS Fargate format: /ecs/<taskId>/<taskId>-<containerId>
* Returns the last segment after the final '/' which should be the complete container ID.
*/
private _extractContainerIdFromLine(line: string): string | undefined {
if (!line) {
return undefined;
}

// Split by '/' and get the last segment
const segments = line.split('/');
if (segments.length <= 1) {
// Fallback to original logic if no '/' found
if (line.length > AwsEcsDetector.CONTAINER_ID_LENGTH) {
return line.substring(line.length - AwsEcsDetector.CONTAINER_ID_LENGTH);
}
return undefined;
}

let lastSegment = segments[segments.length - 1];

// Handle containerd v1.5.0+ format with systemd cgroup driver (e.g., ending with :cri-containerd:containerid)
const colonIndex = lastSegment.lastIndexOf(':');
if (colonIndex !== -1) {
lastSegment = lastSegment.substring(colonIndex + 1);
}

// Remove known prefixes if they exist
const prefixes = ['docker-', 'crio-', 'cri-containerd-'];
for (const prefix of prefixes) {
if (lastSegment.startsWith(prefix)) {
lastSegment = lastSegment.substring(prefix.length);
break;
}
}

// Remove anything after the first period (like .scope)
if (lastSegment.includes('.')) {
lastSegment = lastSegment.split('.')[0];
}

// Basic validation: should not be empty and should have reasonable length
if (!lastSegment || lastSegment.length < 8) {
return undefined;
}

// AWS ECS container IDs can be in various formats:
// 1. Pure hex strings: 'abcdef123456'
// 2. ECS format: 'taskId-containerId'
// 3. Mixed alphanumeric with hyphens
// We'll be more permissive and allow alphanumeric characters and hyphens
const containerIdPattern = /^[a-zA-Z0-9\-_]+$/;

if (containerIdPattern.test(lastSegment)) {
return lastSegment;
}

// If the pattern doesn't match but the segment looks reasonable,
// still try to return it (last resort for edge cases)
if (lastSegment.length >= 12 && lastSegment.length <= 128) {
diag.debug(`AwsEcsDetector: Using container ID with non-standard format: ${lastSegment}`);
return lastSegment;
}

return undefined;
}

/**
* Add metadata-v4-related resource attributes to `data` (in-place)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,3 +453,291 @@ describe('AwsEcsResourceDetector', () => {
});
});
});

describe('AwsEcsDetector - Container ID extraction improvements', () => {
let readStub: sinon.SinonStub;

beforeEach(() => {
process.env.ECS_CONTAINER_METADATA_URI_V4 = 'http://169.254.170.2/v4/test';
});

afterEach(() => {
sinon.restore();
});

describe('New AWS ECS Fargate cgroup format support', () => {
it('should extract full container ID from new AWS ECS Fargate format', async () => {
const taskId = 'c23e5f76c09d438aa1824ca4058bdcab';
const containerId = '1234567890abcdef';
const cgroupData = `/ecs/${taskId}/${taskId}-${containerId}`;

sinon.stub(os, 'hostname').returns('test-hostname');
readStub = sinon
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves(cgroupData);

// Mock the metadata requests
const nockScope = nock('http://169.254.170.2:80')
.persist(false)
.get('/v4/test')
.reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' })
.get('/v4/test/task')
.reply(200, {
TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test',
Family: 'test-family',
Revision: '1',
Cluster: 'test-cluster',
LaunchType: 'FARGATE'
});

const resource = detectResources({ detectors: [awsEcsDetector] });
await resource.waitForAsyncAttributes?.();

sinon.assert.calledOnce(readStub);
assert.ok(resource);
assertEcsResource(resource, {});
assertContainerResource(resource, {
name: 'test-hostname',
id: `${taskId}-${containerId}`, // Expected: full taskId-containerId, not truncated
});

nockScope.done();
});

it('should extract container ID from long cgroup path without truncation', async () => {
// Simulate the actual issue where the path is longer than 64 chars
const longTaskId = 'abcdefgh12345678abcdefgh12345678abcdefgh12345678';
const containerId = '1234567890abcdef';
const cgroupData = `/ecs/${longTaskId}/${longTaskId}-${containerId}`;

sinon.stub(os, 'hostname').returns('test-hostname');
readStub = sinon
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves(cgroupData);

// Mock the metadata requests
const nockScope = nock('http://169.254.170.2:80')
.persist(false)
.get('/v4/test')
.reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' })
.get('/v4/test/task')
.reply(200, {
TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test',
Family: 'test-family',
Revision: '1',
Cluster: 'test-cluster',
LaunchType: 'FARGATE'
});

const resource = detectResources({ detectors: [awsEcsDetector] });
await resource.waitForAsyncAttributes?.();

sinon.assert.calledOnce(readStub);
assert.ok(resource);
assertEcsResource(resource, {});
assertContainerResource(resource, {
name: 'test-hostname',
id: `${longTaskId}-${containerId}`, // Should get full ID, not truncated
});

nockScope.done();
});

it('should handle multiple cgroup lines and pick the valid one', async () => {
const taskId = 'c23e5f76c09d438aa1824ca4058bdcab';
const containerId = '1234567890abcdef';
const cgroupData = [
'12:memory:/ecs',
'11:cpu:/ecs/task-id',
`10:devices:/ecs/${taskId}/${taskId}-${containerId}`,
'9:freezer:/ecs'
].join('\n');

sinon.stub(os, 'hostname').returns('test-hostname');
readStub = sinon
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves(cgroupData);

// Mock the metadata requests
const nockScope = nock('http://169.254.170.2:80')
.persist(false)
.get('/v4/test')
.reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' })
.get('/v4/test/task')
.reply(200, {
TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test',
Family: 'test-family',
Revision: '1',
Cluster: 'test-cluster',
LaunchType: 'FARGATE'
});

const resource = detectResources({ detectors: [awsEcsDetector] });
await resource.waitForAsyncAttributes?.();

sinon.assert.calledOnce(readStub);
assert.ok(resource);
assertEcsResource(resource, {});
assertContainerResource(resource, {
name: 'test-hostname',
id: `${taskId}-${containerId}`,
});

nockScope.done();
});
});

describe('Edge cases and format variations', () => {
it('should handle containerd format with colon separators', async () => {
const taskId = 'c23e5f76c09d438aa1824ca4058bdcab';
const containerId = '1234567890abcdef';
const cgroupData = `0::/system.slice/containerd.service/kubepods-burstable-pod.slice:cri-containerd:${taskId}-${containerId}`;

sinon.stub(os, 'hostname').returns('test-hostname');
readStub = sinon
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves(cgroupData);

// Mock the metadata requests
const nockScope = nock('http://169.254.170.2:80')
.persist(false)
.get('/v4/test')
.reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' })
.get('/v4/test/task')
.reply(200, {
TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test',
Family: 'test-family',
Revision: '1',
Cluster: 'test-cluster',
LaunchType: 'FARGATE'
});

const resource = detectResources({ detectors: [awsEcsDetector] });
await resource.waitForAsyncAttributes?.();

sinon.assert.calledOnce(readStub);
assert.ok(resource);
assertEcsResource(resource, {});
assertContainerResource(resource, {
name: 'test-hostname',
id: `${taskId}-${containerId}`,
});

nockScope.done();
});

it('should handle docker prefix and scope suffix', async () => {
const taskId = 'c23e5f76c09d438aa1824ca4058bdcab';
const containerId = '1234567890abcdef';
const cgroupData = `/docker/docker-${taskId}-${containerId}.scope`;

sinon.stub(os, 'hostname').returns('test-hostname');
readStub = sinon
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves(cgroupData);

// Mock the metadata requests
const nockScope = nock('http://169.254.170.2:80')
.persist(false)
.get('/v4/test')
.reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' })
.get('/v4/test/task')
.reply(200, {
TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test',
Family: 'test-family',
Revision: '1',
Cluster: 'test-cluster',
LaunchType: 'FARGATE'
});

const resource = detectResources({ detectors: [awsEcsDetector] });
await resource.waitForAsyncAttributes?.();

sinon.assert.calledOnce(readStub);
assert.ok(resource);
assertEcsResource(resource, {});
assertContainerResource(resource, {
name: 'test-hostname',
id: `${taskId}-${containerId}`,
});

nockScope.done();
});

it('should return undefined for invalid container ID formats', async () => {
const invalidCgroupData = '/invalid/path/with/non-hex-characters!!!';

sinon.stub(os, 'hostname').returns('test-hostname');
readStub = sinon
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves(invalidCgroupData);

// Mock the metadata requests
const nockScope = nock('http://169.254.170.2:80')
.persist(false)
.get('/v4/test')
.reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' })
.get('/v4/test/task')
.reply(200, {
TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test',
Family: 'test-family',
Revision: '1',
Cluster: 'test-cluster',
LaunchType: 'FARGATE'
});

const resource = detectResources({ detectors: [awsEcsDetector] });
await resource.waitForAsyncAttributes?.();

sinon.assert.calledOnce(readStub);
assert.ok(resource);
assertEcsResource(resource, {});
assertContainerResource(resource, {
name: 'test-hostname',
// id should be undefined due to invalid format
});

nockScope.done();
});
});

describe('Backward compatibility', () => {
it('should fallback to original logic for legacy format', async () => {
// Test backward compatibility with existing 64-char format
const legacyContainerId = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm';

sinon.stub(os, 'hostname').returns('test-hostname');
readStub = sinon
.stub(AwsEcsDetector, 'readFileAsync' as any)
.resolves(legacyContainerId);

// Mock the metadata requests
const nockScope = nock('http://169.254.170.2:80')
.persist(false)
.get('/v4/test')
.reply(200, { ContainerARN: 'arn:aws:ecs:us-west-2:111122223333:container/test' })
.get('/v4/test/task')
.reply(200, {
TaskARN: 'arn:aws:ecs:us-west-2:111122223333:task/default/test',
Family: 'test-family',
Revision: '1',
Cluster: 'test-cluster',
LaunchType: 'FARGATE'
});

const resource = detectResources({ detectors: [awsEcsDetector] });
await resource.waitForAsyncAttributes?.();

sinon.assert.calledOnce(readStub);
assert.ok(resource);
assertEcsResource(resource, {});
assertContainerResource(resource, {
name: 'test-hostname',
id: 'bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm', // Last 64 chars
});

nockScope.done();
});
});
});