Skip to content

Commit

Permalink
podinfo: Populate workload info
Browse files Browse the repository at this point in the history
Use Istio's GetDeployMetaFromPod function [^1] to infer workload info
from owner's references. I copied over the function instead of adding it
to go.mod to avoid pulling in a lot of extra dependencies.

[^1]: https://github.com/istio/istio/blob/47a41d44a2ed6b402c86ef2dfa11f640c87c385f/pkg/kube/util.go#L217

Signed-off-by: Michi Mutsuzaki <michi@isovalent.com>
  • Loading branch information
michi-covalent committed Sep 19, 2023
1 parent 7cfc0b1 commit c493ba0
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ issues:
# The file gets prefixed with "Code generated by cmd/cgo; DO NOT EDIT."
- linters: [goheader]
path: pkg/sensors/tracing/genericuprobe_cgo.go
# Files copied from Istio
- linters: [goheader]
path: operator/podinfo/istio
97 changes: 97 additions & 0 deletions operator/podinfo/istio/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package kube

import (
"regexp"
"strings"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
_ "k8s.io/client-go/plugin/pkg/client/auth" // allow out of cluster authentication
)

var cronJobNameRegexp = regexp.MustCompile(`(.+)-\d{8,10}$`)

// GetDeployMetaFromPod heuristically derives deployment metadata from the pod spec.
func GetDeployMetaFromPod(pod *corev1.Pod) (metav1.ObjectMeta, metav1.TypeMeta) {
if pod == nil {
return metav1.ObjectMeta{}, metav1.TypeMeta{}
}
// try to capture more useful namespace/name info for deployments, etc.
// TODO(dougreid): expand to enable lookup of OWNERs recursively a la kubernetesenv
deployMeta := pod.ObjectMeta
deployMeta.ManagedFields = nil
deployMeta.OwnerReferences = nil

typeMetadata := metav1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
}
if len(pod.GenerateName) > 0 {
// if the pod name was generated (or is scheduled for generation), we can begin an investigation into the controlling reference for the pod.
var controllerRef metav1.OwnerReference
controllerFound := false
for _, ref := range pod.GetOwnerReferences() {
if ref.Controller != nil && *ref.Controller {
controllerRef = ref
controllerFound = true
break
}
}
if controllerFound {
typeMetadata.APIVersion = controllerRef.APIVersion
typeMetadata.Kind = controllerRef.Kind

// heuristic for deployment detection
deployMeta.Name = controllerRef.Name
if typeMetadata.Kind == "ReplicaSet" && pod.Labels["pod-template-hash"] != "" && strings.HasSuffix(controllerRef.Name, pod.Labels["pod-template-hash"]) {
name := strings.TrimSuffix(controllerRef.Name, "-"+pod.Labels["pod-template-hash"])
deployMeta.Name = name
typeMetadata.Kind = "Deployment"
} else if typeMetadata.Kind == "ReplicationController" && pod.Labels["deploymentconfig"] != "" {
// If the pod is controlled by the replication controller, which is created by the DeploymentConfig resource in
// Openshift platform, set the deploy name to the deployment config's name, and the kind to 'DeploymentConfig'.
//
// nolint: lll
// For DeploymentConfig details, refer to
// https://docs.openshift.com/container-platform/4.1/applications/deployments/what-deployments-are.html#deployments-and-deploymentconfigs_what-deployments-are
//
// For the reference to the pod label 'deploymentconfig', refer to
// https://github.com/openshift/library-go/blob/7a65fdb398e28782ee1650959a5e0419121e97ae/pkg/apps/appsutil/const.go#L25
deployMeta.Name = pod.Labels["deploymentconfig"]
typeMetadata.Kind = "DeploymentConfig"
delete(deployMeta.Labels, "deploymentconfig")
} else if typeMetadata.Kind == "Job" {
// If job name suffixed with `-<digit-timestamp>`, where the length of digit timestamp is 8~10,
// trim the suffix and set kind to cron job.
if jn := cronJobNameRegexp.FindStringSubmatch(controllerRef.Name); len(jn) == 2 {
deployMeta.Name = jn[1]
typeMetadata.Kind = "CronJob"
// heuristically set cron job api version to v1beta1 as it cannot be derived from pod metadata.
// Cronjob is not GA yet and latest version is v1beta1: https://github.com/kubernetes/enhancements/pull/978
typeMetadata.APIVersion = "batch/v1beta1"
}
}
}
}

if deployMeta.Name == "" {
// if we haven't been able to extract a deployment name, then just give it the pod name
deployMeta.Name = pod.Name
}

return deployMeta, typeMetadata
}
175 changes: 175 additions & 0 deletions operator/podinfo/istio/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package kube

import (
"reflect"
"testing"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestCronJobMetadata(t *testing.T) {
tests := []struct {
name string
jobName string
wantTypeMetadata metav1.TypeMeta
wantObjectMetadata metav1.ObjectMeta
}{
{
name: "cron-job-name-sec",
jobName: "sec-1234567890",
wantTypeMetadata: metav1.TypeMeta{
Kind: "CronJob",
APIVersion: "batch/v1beta1",
},
wantObjectMetadata: metav1.ObjectMeta{
Name: "sec",
GenerateName: "sec-1234567890-pod",
},
},
{
name: "cron-job-name-min",
jobName: "min-12345678",
wantTypeMetadata: metav1.TypeMeta{
Kind: "CronJob",
APIVersion: "batch/v1beta1",
},
wantObjectMetadata: metav1.ObjectMeta{
Name: "min",
GenerateName: "min-12345678-pod",
},
},
{
name: "non-cron-job-name",
jobName: "job-123",
wantTypeMetadata: metav1.TypeMeta{
Kind: "Job",
APIVersion: "v1",
},
wantObjectMetadata: metav1.ObjectMeta{
Name: "job-123",
GenerateName: "job-123-pod",
},
},
}

for _, tt := range tests {
controller := true
t.Run(tt.name, func(t *testing.T) {
gotObjectMeta, gotTypeMeta := GetDeployMetaFromPod(
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
GenerateName: tt.jobName + "-pod",
OwnerReferences: []metav1.OwnerReference{{
APIVersion: "v1",
Controller: &controller,
Kind: "Job",
Name: tt.jobName,
}},
},
},
)
if !reflect.DeepEqual(gotObjectMeta, tt.wantObjectMetadata) {
t.Errorf("Object metadata got %+v want %+v", gotObjectMeta, tt.wantObjectMetadata)
}
if !reflect.DeepEqual(gotTypeMeta, tt.wantTypeMetadata) {
t.Errorf("Type metadata got %+v want %+v", gotTypeMeta, tt.wantTypeMetadata)
}
})
}
}

func TestDeploymentConfigMetadata(t *testing.T) {
tests := []struct {
name string
pod *corev1.Pod
wantTypeMetadata metav1.TypeMeta
wantObjectMetadata metav1.ObjectMeta
}{
{
name: "deployconfig-name-deploy",
pod: podForDeploymentConfig("deploy", true),
wantTypeMetadata: metav1.TypeMeta{
Kind: "DeploymentConfig",
APIVersion: "v1",
},
wantObjectMetadata: metav1.ObjectMeta{
Name: "deploy",
GenerateName: "deploy-rc-pod",
Labels: map[string]string{},
},
},
{
name: "deployconfig-name-deploy2",
pod: podForDeploymentConfig("deploy2", true),
wantTypeMetadata: metav1.TypeMeta{
Kind: "DeploymentConfig",
APIVersion: "v1",
},
wantObjectMetadata: metav1.ObjectMeta{
Name: "deploy2",
GenerateName: "deploy2-rc-pod",
Labels: map[string]string{},
},
},
{
name: "non-deployconfig-label",
pod: podForDeploymentConfig("dep", false),
wantTypeMetadata: metav1.TypeMeta{
Kind: "ReplicationController",
APIVersion: "v1",
},
wantObjectMetadata: metav1.ObjectMeta{
Name: "dep-rc",
GenerateName: "dep-rc-pod",
Labels: map[string]string{},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotObjectMeta, gotTypeMeta := GetDeployMetaFromPod(tt.pod)
if !reflect.DeepEqual(gotObjectMeta, tt.wantObjectMetadata) {
t.Errorf("Object metadata got %+v want %+v", gotObjectMeta, tt.wantObjectMetadata)
}
if !reflect.DeepEqual(gotTypeMeta, tt.wantTypeMetadata) {
t.Errorf("Type metadata got %+v want %+v", gotTypeMeta, tt.wantTypeMetadata)
}
})
}
}

func podForDeploymentConfig(deployConfigName string, hasDeployConfigLabel bool) *corev1.Pod {
controller := true
labels := make(map[string]string)
if hasDeployConfigLabel {
labels["deploymentconfig"] = deployConfigName
}
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
GenerateName: deployConfigName + "-rc-pod",
OwnerReferences: []metav1.OwnerReference{{
APIVersion: "v1",
Controller: &controller,
Kind: "ReplicationController",
Name: deployConfigName + "-rc",
}},
Labels: labels,
},
}
}
9 changes: 8 additions & 1 deletion operator/podinfo/podinfo_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"reflect"

kube "github.com/cilium/tetragon/operator/podinfo/istio"
ciliumiov1alpha1 "github.com/cilium/tetragon/pkg/k8s/apis/cilium.io/v1alpha1"
"golang.org/x/exp/maps"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -87,13 +88,16 @@ func equal(pod *corev1.Pod, podInfo *ciliumiov1alpha1.PodInfo) bool {
Controller: &controller,
BlockOwnerDeletion: &blockOwnerDeletion,
}
workloadObject, workloadType := kube.GetDeployMetaFromPod(pod)
return pod.Name == podInfo.Name &&
pod.Namespace == podInfo.Namespace &&
pod.Status.PodIP == podInfo.Status.PodIP &&
maps.Equal(pod.Annotations, podInfo.Annotations) &&
maps.Equal(pod.Labels, podInfo.Labels) &&
len(podInfo.OwnerReferences) == 1 &&
reflect.DeepEqual(podInfo.OwnerReferences[0], expectedOwnerReference)
reflect.DeepEqual(podInfo.OwnerReferences[0], expectedOwnerReference) &&
reflect.DeepEqual(podInfo.WorkloadObject, workloadObject) &&
reflect.DeepEqual(podInfo.WorkloadType, workloadType)
}

// hasAllRequiredFields checks if the necessary pod fields are available.
Expand All @@ -112,6 +116,7 @@ func generatePodInfo(pod *corev1.Pod) *ciliumiov1alpha1.PodInfo {
for _, podIP := range pod.Status.PodIPs {
podIPs = append(podIPs, ciliumiov1alpha1.PodIP{IP: podIP.IP})
}
workloadObject, workloadType := kube.GetDeployMetaFromPod(pod)
controller := true
blockOwnerDeletion := true
return &ciliumiov1alpha1.PodInfo{
Expand All @@ -136,6 +141,8 @@ func generatePodInfo(pod *corev1.Pod) *ciliumiov1alpha1.PodInfo {
PodIP: pod.Status.PodIP,
PodIPs: podIPs,
},
WorkloadType: workloadType,
WorkloadObject: workloadObject,
}
}

Expand Down
22 changes: 22 additions & 0 deletions operator/podinfo/podinfo_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
random "math/rand"
"testing"

kube "github.com/cilium/tetragon/operator/podinfo/istio"
ciliumv1alpha1 "github.com/cilium/tetragon/pkg/k8s/apis/cilium.io/v1alpha1"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -113,6 +114,7 @@ func TestGeneratePod(t *testing.T) {
for _, podIP := range pod.Status.PodIPs {
podIPs = append(podIPs, ciliumv1alpha1.PodIP{IP: podIP.IP})
}
workloadObject, workloadType := kube.GetDeployMetaFromPod(pod)
expectedPodInfo := &ciliumv1alpha1.PodInfo{
ObjectMeta: metav1.ObjectMeta{
Name: pod.Name,
Expand All @@ -134,6 +136,8 @@ func TestGeneratePod(t *testing.T) {
PodIP: pod.Status.PodIP,
PodIPs: podIPs,
},
WorkloadType: workloadType,
WorkloadObject: workloadObject,
}
generatedPodInfo := generatePodInfo(pod)
assert.Equal(t, expectedPodInfo, generatedPodInfo, "Generated incorrect PodInfo corresponding to the pod")
Expand Down Expand Up @@ -244,5 +248,23 @@ func TestEqual(t *testing.T) {
pod.Annotations = getRandMap()
assert.False(t, equal(pod, podInfo), "Pod Annotations changed, still returning pod not changed")
})

t.Run("Pod owner references changed", func(t *testing.T) {
pod := randomPodGenerator()
controller, blockOwnerDeletion := true, true
podInfo := generatePodInfo(pod)
pod.GenerateName = "tetragon-"
pod.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "apps/v1",
Kind: "DaemonSet",
Name: "tetragon",
UID: "00000000-0000-0000-0000-000000000000",
Controller: &controller,
BlockOwnerDeletion: &blockOwnerDeletion,
},
}
assert.False(t, equal(pod, podInfo), "Pod owner references changed, still returning pod not changed")
})
})
}
Loading

0 comments on commit c493ba0

Please sign in to comment.