Skip to content

Commit

Permalink
[processor/k8sattributes] Add container.image.repo_digests (open-te…
Browse files Browse the repository at this point in the history
…lemetry#34031)

**Description:**

Adds support for extracting canonical repo digest image references for a
container from k8s attributes

**Link to tracking Issue:** open-telemetry#34029 

**Testing:**

1. Adds unit tests for extracting the k8s `.imageID` field from
container status API if it is of the `reference.Canonical` form, and
storing it in a `ImageRepoDigest` string field. (Note: k8s will only
ever return one value here, although the semantic conventions for
`container.image.repo_digests` define it to be a `Slice`. We convert
into a Slice lazily when we populate the pcommon data structure.
2. Adds unit tests for including `container.image.repo_digests` in rules
for extraction.
3. Includes `container.image.repo_digests` in the e2e tests for
k8sattributes processor.


**Documentation:**

Added `container.image.repo_digests` as an example of attributes that
can be extracted from a container

---------

Co-authored-by: Christos Markou <[email protected]>
  • Loading branch information
evantorrie and ChrsMark authored Aug 6, 2024
1 parent e982922 commit 528678e
Show file tree
Hide file tree
Showing 21 changed files with 708 additions and 503 deletions.
27 changes: 27 additions & 0 deletions .chloggen/container-repo-digests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: processor/k8sattributes

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add support for `container.image.repo_digests` metadata

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [34029]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [user]
2 changes: 2 additions & 0 deletions processor/k8sattributesprocessor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,11 @@ Additional container level attributes can be extracted provided that certain res
- k8s.container.name
- container.image.name
- container.image.tag
- container.image.repo_digests (if k8s CRI populates [repository digest field](https://github.com/open-telemetry/semantic-conventions/blob/v1.26.0/model/registry/container.yaml#L60-L71))
2. If the `k8s.container.name` resource attribute is provided, the following additional attributes will be available:
- container.image.name
- container.image.tag
- container.image.repo_digests (if k8s CRI populates [repository digest field](https://github.com/open-telemetry/semantic-conventions/blob/v1.26.0/model/registry/container.yaml#L60-L71))
3. If the `k8s.container.restart_count` resource attribute is provided, it can be used to associate with a particular container
instance. If it's not set, the latest container instance will be used:
- container.id (not added by default, has to be specified in `metadata`)
Expand Down
6 changes: 3 additions & 3 deletions processor/k8sattributesprocessor/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (cfg *Config) Validate() error {
conventions.AttributeK8SNodeName, conventions.AttributeK8SNodeUID,
conventions.AttributeK8SContainerName, conventions.AttributeContainerID,
conventions.AttributeContainerImageName, conventions.AttributeContainerImageTag,
clusterUID:
containerImageRepoDigests, clusterUID:
default:
return fmt.Errorf("\"%s\" is not a supported metadata field", field)
}
Expand Down Expand Up @@ -143,8 +143,8 @@ type ExtractConfig struct {
// k8s.daemonset.name, k8s.daemonset.uid,
// k8s.job.name, k8s.job.uid, k8s.cronjob.name,
// k8s.statefulset.name, k8s.statefulset.uid,
// k8s.container.name, container.image.name,
// container.image.tag, container.id
// k8s.container.name, container.id, container.image.name,
// container.image.tag, container.image.repo_digests
// k8s.cluster.uid
//
// Specifying anything other than these values will result in an error.
Expand Down
1 change: 1 addition & 0 deletions processor/k8sattributesprocessor/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
| ---- | ----------- | ------ | ------- |
| container.id | Container ID. Usually a UUID, as for example used to identify Docker containers. The UUID might be abbreviated. Requires k8s.container.restart_count. | Any Str | false |
| container.image.name | Name of the image the container was built on. Requires container.id or k8s.container.name. | Any Str | true |
| container.image.repo_digests | Repo digests of the container image as provided by the container runtime. | Any Slice | false |
| container.image.tag | Container image tag. Requires container.id or k8s.container.name. | Any Str | true |
| k8s.cluster.uid | Gives cluster uid identified with kube-system namespace | Any Str | false |
| k8s.container.name | The name of the Container in a Pod template. Requires container.id. | Any Str | false |
Expand Down
743 changes: 383 additions & 360 deletions processor/k8sattributesprocessor/e2e_test.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion processor/k8sattributesprocessor/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/open-telemetry/opentelemetry-collector-contrib/processor/k8sat
go 1.21.0

require (
github.com/distribution/reference v0.5.0
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.106.1
Expand Down Expand Up @@ -38,7 +39,6 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/docker v26.1.5+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
Expand Down
47 changes: 33 additions & 14 deletions processor/k8sattributesprocessor/internal/kube/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"sync"
"time"

"github.com/distribution/reference"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/featuregate"
conventions "go.opentelemetry.io/collector/semconv/v1.6.1"
Expand Down Expand Up @@ -572,24 +573,26 @@ func removeUnnecessaryPodData(pod *api_v1.Pod, rules ExtractionRules) *api_v1.Po
}

if needContainerAttributes(rules) {
removeUnnecessaryContainerStatus := func(c api_v1.ContainerStatus) api_v1.ContainerStatus {
transformedContainerStatus := api_v1.ContainerStatus{
Name: c.Name,
ContainerID: c.ContainerID,
RestartCount: c.RestartCount,
}
if rules.ContainerImageRepoDigests {
transformedContainerStatus.ImageID = c.ImageID
}
return transformedContainerStatus
}

for _, containerStatus := range pod.Status.ContainerStatuses {
transformedPod.Status.ContainerStatuses = append(
transformedPod.Status.ContainerStatuses,
api_v1.ContainerStatus{
Name: containerStatus.Name,
ContainerID: containerStatus.ContainerID,
RestartCount: containerStatus.RestartCount,
},
transformedPod.Status.ContainerStatuses, removeUnnecessaryContainerStatus(containerStatus),
)
}
for _, containerStatus := range pod.Status.InitContainerStatuses {
transformedPod.Status.InitContainerStatuses = append(
transformedPod.Status.InitContainerStatuses,
api_v1.ContainerStatus{
Name: containerStatus.Name,
ContainerID: containerStatus.ContainerID,
RestartCount: containerStatus.RestartCount,
},
transformedPod.Status.InitContainerStatuses, removeUnnecessaryContainerStatus(containerStatus),
)
}

Expand Down Expand Up @@ -670,11 +673,26 @@ func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContain
containerID = parts[1]
}
containers.ByID[containerID] = container
if c.Rules.ContainerID {
if c.Rules.ContainerID || c.Rules.ContainerImageRepoDigests {
if container.Statuses == nil {
container.Statuses = map[int]ContainerStatus{}
}
container.Statuses[int(apiStatus.RestartCount)] = ContainerStatus{containerID}
containerStatus := ContainerStatus{}
if c.Rules.ContainerID {
containerStatus.ContainerID = containerID
}

if c.Rules.ContainerImageRepoDigests {
if parsed, err := reference.ParseAnyReference(apiStatus.ImageID); err == nil {
switch parsed.(type) {
case reference.Canonical:
containerStatus.ImageRepoDigest = parsed.String()
default:
}
}
}

container.Statuses[int(apiStatus.RestartCount)] = containerStatus
}
}
return containers
Expand Down Expand Up @@ -965,6 +983,7 @@ func needContainerAttributes(rules ExtractionRules) bool {
return rules.ContainerImageName ||
rules.ContainerName ||
rules.ContainerImageTag ||
rules.ContainerImageRepoDigests ||
rules.ContainerID
}

Expand Down
73 changes: 60 additions & 13 deletions processor/k8sattributesprocessor/internal/kube/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1488,18 +1488,21 @@ func Test_extractPodContainersAttributes(t *testing.T) {
{
Name: "container1",
ContainerID: "docker://container1-id-123",
ImageID: "docker.io/otel/collector@sha256:55d008bc28344c3178645d40e7d07df30f9d90abe4b53c3fc4e5e9c0295533da",
RestartCount: 0,
},
{
Name: "container2",
ContainerID: "docker://container2-id-456",
ImageID: "sha256:430ac608abaa332de4ce45d68534447c7a206edc5e98aaff9923ecc12f8a80d9",
RestartCount: 2,
},
},
InitContainerStatuses: []api_v1.ContainerStatus{
{
Name: "init_container",
ContainerID: "containerd://init-container-id-123",
ContainerID: "containerd://init-container-id-789",
ImageID: "ghcr.io/initimage1@sha256:42e8ba40f9f70d604684c3a2a0ed321206b7e2e3509fdb2c8836d34f2edfb57b",
RestartCount: 0,
},
},
Expand Down Expand Up @@ -1537,7 +1540,7 @@ func Test_extractPodContainersAttributes(t *testing.T) {
ByID: map[string]*Container{
"container1-id-123": {ImageName: "test/image1"},
"container2-id-456": {ImageName: "example.com:port1/image2"},
"init-container-id-123": {ImageName: "test/init-image"},
"init-container-id-789": {ImageName: "test/init-image"},
},
ByName: map[string]*Container{
"container1": {ImageName: "test/image1"},
Expand Down Expand Up @@ -1586,9 +1589,9 @@ func Test_extractPodContainersAttributes(t *testing.T) {
2: {ContainerID: "container2-id-456"},
},
},
"init-container-id-123": {
"init-container-id-789": {
Statuses: map[int]ContainerStatus{
0: {ContainerID: "init-container-id-123"},
0: {ContainerID: "init-container-id-789"},
},
},
},
Expand All @@ -1605,7 +1608,50 @@ func Test_extractPodContainersAttributes(t *testing.T) {
},
"init_container": {
Statuses: map[int]ContainerStatus{
0: {ContainerID: "init-container-id-123"},
0: {ContainerID: "init-container-id-789"},
},
},
},
},
},
{
name: "container-image-repo-digest-only",
rules: ExtractionRules{
ContainerImageRepoDigests: true,
},
pod: &pod,
want: PodContainers{
ByID: map[string]*Container{
"container1-id-123": {
Statuses: map[int]ContainerStatus{
0: {ImageRepoDigest: "docker.io/otel/collector@sha256:55d008bc28344c3178645d40e7d07df30f9d90abe4b53c3fc4e5e9c0295533da"},
},
},
"container2-id-456": {
Statuses: map[int]ContainerStatus{
2: {},
},
},
"init-container-id-789": {
Statuses: map[int]ContainerStatus{
0: {ImageRepoDigest: "ghcr.io/initimage1@sha256:42e8ba40f9f70d604684c3a2a0ed321206b7e2e3509fdb2c8836d34f2edfb57b"},
},
},
},
ByName: map[string]*Container{
"container1": {
Statuses: map[int]ContainerStatus{
0: {ImageRepoDigest: "docker.io/otel/collector@sha256:55d008bc28344c3178645d40e7d07df30f9d90abe4b53c3fc4e5e9c0295533da"},
},
},
"container2": {
Statuses: map[int]ContainerStatus{
2: {},
},
},
"init_container": {
Statuses: map[int]ContainerStatus{
0: {ImageRepoDigest: "ghcr.io/initimage1@sha256:42e8ba40f9f70d604684c3a2a0ed321206b7e2e3509fdb2c8836d34f2edfb57b"},
},
},
},
Expand All @@ -1614,9 +1660,10 @@ func Test_extractPodContainersAttributes(t *testing.T) {
{
name: "all-container-attributes",
rules: ExtractionRules{
ContainerImageName: true,
ContainerImageTag: true,
ContainerID: true,
ContainerImageName: true,
ContainerImageTag: true,
ContainerID: true,
ContainerImageRepoDigests: true,
},
pod: &pod,
want: PodContainers{
Expand All @@ -1625,7 +1672,7 @@ func Test_extractPodContainersAttributes(t *testing.T) {
ImageName: "test/image1",
ImageTag: "0.1.0",
Statuses: map[int]ContainerStatus{
0: {ContainerID: "container1-id-123"},
0: {ContainerID: "container1-id-123", ImageRepoDigest: "docker.io/otel/collector@sha256:55d008bc28344c3178645d40e7d07df30f9d90abe4b53c3fc4e5e9c0295533da"},
},
},
"container2-id-456": {
Expand All @@ -1635,11 +1682,11 @@ func Test_extractPodContainersAttributes(t *testing.T) {
2: {ContainerID: "container2-id-456"},
},
},
"init-container-id-123": {
"init-container-id-789": {
ImageName: "test/init-image",
ImageTag: "1.0.2",
Statuses: map[int]ContainerStatus{
0: {ContainerID: "init-container-id-123"},
0: {ContainerID: "init-container-id-789", ImageRepoDigest: "ghcr.io/initimage1@sha256:42e8ba40f9f70d604684c3a2a0ed321206b7e2e3509fdb2c8836d34f2edfb57b"},
},
},
},
Expand All @@ -1648,7 +1695,7 @@ func Test_extractPodContainersAttributes(t *testing.T) {
ImageName: "test/image1",
ImageTag: "0.1.0",
Statuses: map[int]ContainerStatus{
0: {ContainerID: "container1-id-123"},
0: {ContainerID: "container1-id-123", ImageRepoDigest: "docker.io/otel/collector@sha256:55d008bc28344c3178645d40e7d07df30f9d90abe4b53c3fc4e5e9c0295533da"},
},
},
"container2": {
Expand All @@ -1662,7 +1709,7 @@ func Test_extractPodContainersAttributes(t *testing.T) {
ImageName: "test/init-image",
ImageTag: "1.0.2",
Statuses: map[int]ContainerStatus{
0: {ContainerID: "init-container-id-123"},
0: {ContainerID: "init-container-id-789", ImageRepoDigest: "ghcr.io/initimage1@sha256:42e8ba40f9f70d604684c3a2a0ed321206b7e2e3509fdb2c8836d34f2edfb57b"},
},
},
},
Expand Down
52 changes: 27 additions & 25 deletions processor/k8sattributesprocessor/internal/kube/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ type Container struct {

// ContainerStatus stores resource attributes for a particular container run defined by k8s pod status.
type ContainerStatus struct {
ContainerID string
ContainerID string
ImageRepoDigest string
}

// Namespace represents a kubernetes namespace.
Expand Down Expand Up @@ -194,30 +195,31 @@ type FieldFilter struct {
// ExtractionRules is used to specify the information that needs to be extracted
// from pods and added to the spans as tags.
type ExtractionRules struct {
CronJobName bool
DeploymentName bool
DeploymentUID bool
DaemonSetUID bool
DaemonSetName bool
JobUID bool
JobName bool
Namespace bool
PodName bool
PodUID bool
PodHostName bool
PodIP bool
ReplicaSetID bool
ReplicaSetName bool
StatefulSetUID bool
StatefulSetName bool
Node bool
NodeUID bool
StartTime bool
ContainerName bool
ContainerID bool
ContainerImageName bool
ContainerImageTag bool
ClusterUID bool
CronJobName bool
DeploymentName bool
DeploymentUID bool
DaemonSetUID bool
DaemonSetName bool
JobUID bool
JobName bool
Namespace bool
PodName bool
PodUID bool
PodHostName bool
PodIP bool
ReplicaSetID bool
ReplicaSetName bool
StatefulSetUID bool
StatefulSetName bool
Node bool
NodeUID bool
StartTime bool
ContainerName bool
ContainerID bool
ContainerImageName bool
ContainerImageRepoDigests bool
ContainerImageTag bool
ClusterUID bool

Annotations []FieldExtractionRule
Labels []FieldExtractionRule
Expand Down
Loading

0 comments on commit 528678e

Please sign in to comment.