diff --git a/.chloggen/fix-k8s-image-parsing.yaml b/.chloggen/fix-k8s-image-parsing.yaml new file mode 100644 index 000000000000..4d21584120ea --- /dev/null +++ b/.chloggen/fix-k8s-image-parsing.yaml @@ -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: bug_fix + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: processor/k8sattribute + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: fixes parsing of k8s image names to support images with tags and digests. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [36131] + +# (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] diff --git a/processor/k8sattributesprocessor/e2e_test.go b/processor/k8sattributesprocessor/e2e_test.go index 8ca836a0ee8d..ddb81633cdf5 100644 --- a/processor/k8sattributesprocessor/e2e_test.go +++ b/processor/k8sattributesprocessor/e2e_test.go @@ -52,7 +52,7 @@ func newExpectedValue(mode int, value string) *expectedValue { // TestE2E_ClusterRBAC tests the k8s attributes processor in a k8s cluster with the collector's service account having // cluster-wide permissions to list/watch namespaces, nodes, pods and replicasets. The config in the test does not -// set filter::namespace. +// set filter::namespace, and the telemetrygen image has a latest tag but no digest. // The test requires a prebuilt otelcontribcol image uploaded to a kind k8s cluster defined in // `/tmp/kube-config-otelcol-e2e-testing`. Run the following command prior to running the test locally: // @@ -432,7 +432,8 @@ func TestE2E_ClusterRBAC(t *testing.T) { } } -// Test with `filter::namespace` set and only role binding to collector's SA. We can't get node and namespace labels/annotations. +// Test with `filter::namespace` set and only role binding to collector's SA. We can't get node and namespace labels/annotations, +// and the telemetrygen image has a digest but no tag. func TestE2E_NamespacedRBAC(t *testing.T) { testDir := filepath.Join("testdata", "e2e", "namespacedrbac") @@ -504,7 +505,7 @@ func TestE2E_NamespacedRBAC(t *testing.T) { "k8s.container.name": newExpectedValue(equal, "telemetrygen"), "container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"), "container.image.repo_digests": newExpectedValue(regex, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen@sha256:[0-9a-fA-f]{64}"), - "container.image.tag": newExpectedValue(equal, "latest"), + "container.image.tag": newExpectedValue(shouldnotexist, ""), "container.id": newExpectedValue(exist, ""), }, }, @@ -528,7 +529,7 @@ func TestE2E_NamespacedRBAC(t *testing.T) { "k8s.container.name": newExpectedValue(equal, "telemetrygen"), "container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"), "container.image.repo_digests": newExpectedValue(regex, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen@sha256:[0-9a-fA-f]{64}"), - "container.image.tag": newExpectedValue(equal, "latest"), + "container.image.tag": newExpectedValue(shouldnotexist, ""), "container.id": newExpectedValue(exist, ""), }, }, @@ -552,7 +553,7 @@ func TestE2E_NamespacedRBAC(t *testing.T) { "k8s.container.name": newExpectedValue(equal, "telemetrygen"), "container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"), "container.image.repo_digests": newExpectedValue(regex, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen@sha256:[0-9a-fA-f]{64}"), - "container.image.tag": newExpectedValue(equal, "latest"), + "container.image.tag": newExpectedValue(shouldnotexist, ""), "container.id": newExpectedValue(exist, ""), }, }, @@ -575,7 +576,7 @@ func TestE2E_NamespacedRBAC(t *testing.T) { } // Test with `filter::namespace` set, role binding for namespace-scoped objects (pod, replicaset) and clusterrole -// binding for node and namespace objects. +// binding for node and namespace objects, and the telemetrygen image has a tag and digest. func TestE2E_MixRBAC(t *testing.T) { testDir := filepath.Join("testdata", "e2e", "mixrbac") @@ -662,7 +663,7 @@ func TestE2E_MixRBAC(t *testing.T) { "k8s.container.name": newExpectedValue(equal, "telemetrygen"), "container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"), "container.image.repo_digests": newExpectedValue(regex, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen@sha256:[0-9a-fA-f]{64}"), - "container.image.tag": newExpectedValue(equal, "latest"), + "container.image.tag": newExpectedValue(equal, "0.112.0"), "container.id": newExpectedValue(exist, ""), "k8s.namespace.labels.foons": newExpectedValue(equal, "barns"), "k8s.node.labels.foo": newExpectedValue(equal, "too"), @@ -689,7 +690,7 @@ func TestE2E_MixRBAC(t *testing.T) { "k8s.container.name": newExpectedValue(equal, "telemetrygen"), "container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"), "container.image.repo_digests": newExpectedValue(regex, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen@sha256:[0-9a-fA-f]{64}"), - "container.image.tag": newExpectedValue(equal, "latest"), + "container.image.tag": newExpectedValue(equal, "0.112.0"), "container.id": newExpectedValue(exist, ""), "k8s.namespace.labels.foons": newExpectedValue(equal, "barns"), "k8s.node.labels.foo": newExpectedValue(equal, "too"), @@ -716,7 +717,7 @@ func TestE2E_MixRBAC(t *testing.T) { "k8s.container.name": newExpectedValue(equal, "telemetrygen"), "container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"), "container.image.repo_digests": newExpectedValue(regex, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen@sha256:[0-9a-fA-f]{64}"), - "container.image.tag": newExpectedValue(equal, "latest"), + "container.image.tag": newExpectedValue(equal, "0.112.0"), "container.id": newExpectedValue(exist, ""), "k8s.namespace.labels.foons": newExpectedValue(equal, "barns"), "k8s.node.labels.foo": newExpectedValue(equal, "too"), @@ -745,7 +746,8 @@ func TestE2E_MixRBAC(t *testing.T) { // While `k8s.pod.ip` is not set in `k8sattributes:extract:metadata` and the `pod_association` is not `connection` // we expect that the `k8s.pod.ip` metadata is not added. // While `container.image.repo_digests` is not set in `k8sattributes::extract::metadata`, we expect -// that the `container.image.repo_digests` metadata is not added +// that the `container.image.repo_digests` metadata is not added. +// The telemetrygen image has neither a tag nor digest (implicitly latest version) func TestE2E_NamespacedRBACNoPodIP(t *testing.T) { testDir := filepath.Join("testdata", "e2e", "namespaced_rbac_no_pod_ip") @@ -816,7 +818,7 @@ func TestE2E_NamespacedRBACNoPodIP(t *testing.T) { "k8s.container.name": newExpectedValue(equal, "telemetrygen"), "container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"), "container.image.repo_digests": newExpectedValue(shouldnotexist, ""), - "container.image.tag": newExpectedValue(equal, "latest"), + "container.image.tag": newExpectedValue(shouldnotexist, ""), "container.id": newExpectedValue(exist, ""), }, }, @@ -840,7 +842,7 @@ func TestE2E_NamespacedRBACNoPodIP(t *testing.T) { "k8s.container.name": newExpectedValue(equal, "telemetrygen"), "container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"), "container.image.repo_digests": newExpectedValue(shouldnotexist, ""), - "container.image.tag": newExpectedValue(equal, "latest"), + "container.image.tag": newExpectedValue(shouldnotexist, ""), "container.id": newExpectedValue(exist, ""), }, }, @@ -864,7 +866,7 @@ func TestE2E_NamespacedRBACNoPodIP(t *testing.T) { "k8s.container.name": newExpectedValue(equal, "telemetrygen"), "container.image.name": newExpectedValue(equal, "ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen"), "container.image.repo_digests": newExpectedValue(shouldnotexist, ""), - "container.image.tag": newExpectedValue(equal, "latest"), + "container.image.tag": newExpectedValue(shouldnotexist, ""), "container.id": newExpectedValue(exist, ""), }, }, diff --git a/processor/k8sattributesprocessor/internal/kube/client.go b/processor/k8sattributesprocessor/internal/kube/client.go index a43049f09ebf..0631dc1699ab 100644 --- a/processor/k8sattributesprocessor/internal/kube/client.go +++ b/processor/k8sattributesprocessor/internal/kube/client.go @@ -5,6 +5,7 @@ package kube // import "github.com/open-telemetry/opentelemetry-collector-contri import ( "context" + "errors" "fmt" "regexp" "strings" @@ -632,6 +633,23 @@ func removeUnnecessaryPodData(pod *api_v1.Pod, rules ExtractionRules) *api_v1.Po return &transformedPod } +// parseAttributesFromImage parses the image name and tag for differently-formatted image names. +func parseNameAndTagFromImage(image string) (name, tag string, err error) { + ref, err := reference.Parse(image) + if err != nil { + return + } + namedRef, ok := ref.(reference.Named) + if !ok { + return "", "", errors.New("cannot retrieve image name") + } + name = namedRef.Name() + if taggedRef, ok := namedRef.(reference.Tagged); ok { + tag = taggedRef.Tag() + } + return +} + func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContainers { containers := PodContainers{ ByID: map[string]*Container{}, @@ -643,16 +661,14 @@ func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContain if c.Rules.ContainerImageName || c.Rules.ContainerImageTag { for _, spec := range append(pod.Spec.Containers, pod.Spec.InitContainers...) { container := &Container{} - nameTagSep := strings.LastIndex(spec.Image, ":") - if c.Rules.ContainerImageName { - if nameTagSep > 0 { - container.ImageName = spec.Image[:nameTagSep] - } else { - container.ImageName = spec.Image + name, tag, err := parseNameAndTagFromImage(spec.Image) + if err == nil { + if c.Rules.ContainerImageName { + container.ImageName = name + } + if c.Rules.ContainerImageTag { + container.ImageTag = tag } - } - if c.Rules.ContainerImageTag && nameTagSep > 0 { - container.ImageTag = spec.Image[nameTagSep+1:] } containers.ByName[spec.Name] = container } diff --git a/processor/k8sattributesprocessor/internal/kube/client_test.go b/processor/k8sattributesprocessor/internal/kube/client_test.go index 97b0cdc06b16..f414aa93f63e 100644 --- a/processor/k8sattributesprocessor/internal/kube/client_test.go +++ b/processor/k8sattributesprocessor/internal/kube/client_test.go @@ -1469,11 +1469,15 @@ func Test_extractPodContainersAttributes(t *testing.T) { Containers: []api_v1.Container{ { Name: "container1", - Image: "test/image1:0.1.0", + Image: "example.com:5000/test/image1:0.1.0", }, { Name: "container2", - Image: "example.com:port1/image2:0.2.0", + Image: "example.com:81/image2@sha256:430ac608abaa332de4ce45d68534447c7a206edc5e98aaff9923ecc12f8a80d9", + }, + { + Name: "container3", + Image: "example-website.com/image3:1.0@sha256:4b0b1b6f6cdd3e5b9e55f74a1e8d19ed93a3f5a04c6b6c3c57c4e6d19f6b7c4d", }, }, InitContainers: []api_v1.Container{ @@ -1494,7 +1498,13 @@ func Test_extractPodContainersAttributes(t *testing.T) { { Name: "container2", ContainerID: "docker://container2-id-456", - ImageID: "sha256:430ac608abaa332de4ce45d68534447c7a206edc5e98aaff9923ecc12f8a80d9", + ImageID: "sha256:4b0b1b6f6cdd3e5b9e55f74a1e8d19ed93a3f5a04c6b6c3c57c4e6d19f6b7c4d", + RestartCount: 2, + }, + { + Name: "container3", + ContainerID: "docker://container3-id-abc", + ImageID: "docker.io/otel/collector:2.0.0@sha256:430ac608abaa332de4ce45d68534447c7a206edc5e98aaff9923ecc12f8a80d9", RestartCount: 2, }, }, @@ -1538,13 +1548,15 @@ func Test_extractPodContainersAttributes(t *testing.T) { pod: &pod, want: PodContainers{ ByID: map[string]*Container{ - "container1-id-123": {ImageName: "test/image1"}, - "container2-id-456": {ImageName: "example.com:port1/image2"}, + "container1-id-123": {ImageName: "example.com:5000/test/image1"}, + "container2-id-456": {ImageName: "example.com:81/image2"}, + "container3-id-abc": {ImageName: "example-website.com/image3"}, "init-container-id-789": {ImageName: "test/init-image"}, }, ByName: map[string]*Container{ - "container1": {ImageName: "test/image1"}, - "container2": {ImageName: "example.com:port1/image2"}, + "container1": {ImageName: "example.com:5000/test/image1"}, + "container2": {ImageName: "example.com:81/image2"}, + "container3": {ImageName: "example-website.com/image3"}, "init_container": {ImageName: "test/init-image"}, }, }, @@ -1589,6 +1601,11 @@ func Test_extractPodContainersAttributes(t *testing.T) { 2: {ContainerID: "container2-id-456"}, }, }, + "container3-id-abc": { + Statuses: map[int]ContainerStatus{ + 2: {ContainerID: "container3-id-abc"}, + }, + }, "init-container-id-789": { Statuses: map[int]ContainerStatus{ 0: {ContainerID: "init-container-id-789"}, @@ -1606,6 +1623,11 @@ func Test_extractPodContainersAttributes(t *testing.T) { 2: {ContainerID: "container2-id-456"}, }, }, + "container3": { + Statuses: map[int]ContainerStatus{ + 2: {ContainerID: "container3-id-abc"}, + }, + }, "init_container": { Statuses: map[int]ContainerStatus{ 0: {ContainerID: "init-container-id-789"}, @@ -1632,6 +1654,11 @@ func Test_extractPodContainersAttributes(t *testing.T) { 2: {}, }, }, + "container3-id-abc": { + Statuses: map[int]ContainerStatus{ + 2: {ImageRepoDigest: "docker.io/otel/collector:2.0.0@sha256:430ac608abaa332de4ce45d68534447c7a206edc5e98aaff9923ecc12f8a80d9"}, + }, + }, "init-container-id-789": { Statuses: map[int]ContainerStatus{ 0: {ImageRepoDigest: "ghcr.io/initimage1@sha256:42e8ba40f9f70d604684c3a2a0ed321206b7e2e3509fdb2c8836d34f2edfb57b"}, @@ -1649,6 +1676,11 @@ func Test_extractPodContainersAttributes(t *testing.T) { 2: {}, }, }, + "container3": { + Statuses: map[int]ContainerStatus{ + 2: {ImageRepoDigest: "docker.io/otel/collector:2.0.0@sha256:430ac608abaa332de4ce45d68534447c7a206edc5e98aaff9923ecc12f8a80d9"}, + }, + }, "init_container": { Statuses: map[int]ContainerStatus{ 0: {ImageRepoDigest: "ghcr.io/initimage1@sha256:42e8ba40f9f70d604684c3a2a0ed321206b7e2e3509fdb2c8836d34f2edfb57b"}, @@ -1669,19 +1701,25 @@ func Test_extractPodContainersAttributes(t *testing.T) { want: PodContainers{ ByID: map[string]*Container{ "container1-id-123": { - ImageName: "test/image1", + ImageName: "example.com:5000/test/image1", ImageTag: "0.1.0", Statuses: map[int]ContainerStatus{ 0: {ContainerID: "container1-id-123", ImageRepoDigest: "docker.io/otel/collector@sha256:55d008bc28344c3178645d40e7d07df30f9d90abe4b53c3fc4e5e9c0295533da"}, }, }, "container2-id-456": { - ImageName: "example.com:port1/image2", - ImageTag: "0.2.0", + ImageName: "example.com:81/image2", Statuses: map[int]ContainerStatus{ 2: {ContainerID: "container2-id-456"}, }, }, + "container3-id-abc": { + ImageName: "example-website.com/image3", + ImageTag: "1.0", + Statuses: map[int]ContainerStatus{ + 2: {ContainerID: "container3-id-abc", ImageRepoDigest: "docker.io/otel/collector:2.0.0@sha256:430ac608abaa332de4ce45d68534447c7a206edc5e98aaff9923ecc12f8a80d9"}, + }, + }, "init-container-id-789": { ImageName: "test/init-image", ImageTag: "1.0.2", @@ -1692,19 +1730,25 @@ func Test_extractPodContainersAttributes(t *testing.T) { }, ByName: map[string]*Container{ "container1": { - ImageName: "test/image1", + ImageName: "example.com:5000/test/image1", ImageTag: "0.1.0", Statuses: map[int]ContainerStatus{ 0: {ContainerID: "container1-id-123", ImageRepoDigest: "docker.io/otel/collector@sha256:55d008bc28344c3178645d40e7d07df30f9d90abe4b53c3fc4e5e9c0295533da"}, }, }, "container2": { - ImageName: "example.com:port1/image2", - ImageTag: "0.2.0", + ImageName: "example.com:81/image2", Statuses: map[int]ContainerStatus{ 2: {ContainerID: "container2-id-456"}, }, }, + "container3": { + ImageName: "example-website.com/image3", + ImageTag: "1.0", + Statuses: map[int]ContainerStatus{ + 2: {ContainerID: "container3-id-abc", ImageRepoDigest: "docker.io/otel/collector:2.0.0@sha256:430ac608abaa332de4ce45d68534447c7a206edc5e98aaff9923ecc12f8a80d9"}, + }, + }, "init_container": { ImageName: "test/init-image", ImageTag: "1.0.2", diff --git a/processor/k8sattributesprocessor/processor_test.go b/processor/k8sattributesprocessor/processor_test.go index 4f9e8701b348..8a0da5ec357b 100644 --- a/processor/k8sattributesprocessor/processor_test.go +++ b/processor/k8sattributesprocessor/processor_test.go @@ -1131,7 +1131,7 @@ func TestProcessorAddContainerAttributes(t *testing.T) { ByName: map[string]*kube.Container{ "app": { Statuses: map[int]kube.ContainerStatus{ - 2: {ImageRepoDigest: "docker.io/otel/collector@sha256:deadbeef02"}, + 2: {ImageRepoDigest: "docker.io/otel/collector:1.2.3@sha256:deadbeef02"}, }, }, }, @@ -1145,7 +1145,7 @@ func TestProcessorAddContainerAttributes(t *testing.T) { wantAttrs: map[string]any{ kube.K8sIPLabelName: "1.1.1.1", conventions.AttributeK8SContainerName: "app", - containerImageRepoDigests: []string{"docker.io/otel/collector@sha256:deadbeef02"}, + containerImageRepoDigests: []string{"docker.io/otel/collector:1.2.3@sha256:deadbeef02"}, }, }, { diff --git a/processor/k8sattributesprocessor/testdata/e2e/mixrbac/telemetrygen/deployment.yaml b/processor/k8sattributesprocessor/testdata/e2e/mixrbac/telemetrygen/deployment.yaml index a09257347f5e..32a2f063566e 100644 --- a/processor/k8sattributesprocessor/testdata/e2e/mixrbac/telemetrygen/deployment.yaml +++ b/processor/k8sattributesprocessor/testdata/e2e/mixrbac/telemetrygen/deployment.yaml @@ -28,7 +28,7 @@ spec: {{- if eq .DataType "traces" }} - --status-code= {{- end }} - image: ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:latest + image: ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:0.112.0@sha256:b248ef911f93ae27cbbc85056d1ffacc87fd941bbdc2ffd951b6df8df72b8096 imagePullPolicy: IfNotPresent name: telemetrygen restartPolicy: Always diff --git a/processor/k8sattributesprocessor/testdata/e2e/namespaced_rbac_no_pod_ip/telemetrygen/deployment.yaml b/processor/k8sattributesprocessor/testdata/e2e/namespaced_rbac_no_pod_ip/telemetrygen/deployment.yaml index 953e21514e60..b64a7ac87acf 100644 --- a/processor/k8sattributesprocessor/testdata/e2e/namespaced_rbac_no_pod_ip/telemetrygen/deployment.yaml +++ b/processor/k8sattributesprocessor/testdata/e2e/namespaced_rbac_no_pod_ip/telemetrygen/deployment.yaml @@ -30,7 +30,7 @@ spec: {{- if eq .DataType "traces" }} - --status-code= {{- end }} - image: ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:latest + image: ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen imagePullPolicy: IfNotPresent name: telemetrygen restartPolicy: Always diff --git a/processor/k8sattributesprocessor/testdata/e2e/namespacedrbac/telemetrygen/deployment.yaml b/processor/k8sattributesprocessor/testdata/e2e/namespacedrbac/telemetrygen/deployment.yaml index fd56c39d4eff..18f35425807a 100644 --- a/processor/k8sattributesprocessor/testdata/e2e/namespacedrbac/telemetrygen/deployment.yaml +++ b/processor/k8sattributesprocessor/testdata/e2e/namespacedrbac/telemetrygen/deployment.yaml @@ -29,7 +29,7 @@ spec: {{- if eq .DataType "traces" }} - --status-code= {{- end }} - image: ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:latest + image: ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen@sha256:b248ef911f93ae27cbbc85056d1ffacc87fd941bbdc2ffd951b6df8df72b8096 imagePullPolicy: IfNotPresent name: telemetrygen restartPolicy: Always