Repository: istio/istio (go)

Issue:
Incorrect workload name metadata of CronJobs in Kubernetes 1.21+
### Bug Description

`Job`s created by `CronJob`s have 8-digit decimal suffix since Kubernetes 1.21. (UNIX timestamp in minutes)

https://github.com/kubernetes/kubernetes/blob/a923852ba06e520c7b531e5cf102d78744dd6f53/pkg/controller/cronjob/cronjob_controllerv2.go#L612-L614

In #27195 the logic to infer `CronJob` name from `Job` name was added, but it assumes the suffix is 10 digits.

Incorrect workload name causes high cardinality of telemetry metrics - regression of #24058 for pods called by `CronJob`s frequently.

### Version

```prose
$ istioctl version
client version: 1.11.3
control plane version: 1.10.3
data plane version: 1.10.3 (51 proxies)
$ kubectl version --short
Client Version: v1.20.4-dirty
Server Version: v1.21.2-eks-0389ca3
```


### Additional Information

_No response_

Pull request:
Fix cronjob timestamp trimming.
**Please provide a description of this PR:**
fix #35563

Patch:
diff --git a/pkg/kube/util.go b/pkg/kube/util.go
index fb9387e772..4b0d3cf00f 100644
--- a/pkg/kube/util.go
+++ b/pkg/kube/util.go
@@ -18,7 +18,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
-	"strconv"
+	"regexp"
 	"strings"
 
 	kubeApiCore "k8s.io/api/core/v1"
@@ -35,6 +35,8 @@ import (
 	istioversion "istio.io/pkg/version"
 )
 
+var cronJobNameRegexp = regexp.MustCompile(`(.+)-\d{8,10}$`)
+
 // BuildClientConfig builds a client rest config from a kubeconfig filepath and context.
 // It overrides the current context with the one provided (empty to use default).
 //
@@ -229,11 +231,11 @@ func GetDeployMetaFromPod(pod *kubeApiCore.Pod) (metav1.ObjectMeta, metav1.TypeM
 				name := strings.TrimSuffix(controllerRef.Name, "-"+pod.Labels["pod-template-hash"])
 				deployMeta.Name = name
 				typeMetadata.Kind = "Deployment"
-			} else if typeMetadata.Kind == "Job" && len(controllerRef.Name) > 11 {
-				// If job name suffixed with `-<ten-digit-timestamp>`, trim the suffix and set kind to cron job.
-				l := len(controllerRef.Name)
-				if _, err := strconv.Atoi(controllerRef.Name[l-10:]); err == nil && string(controllerRef.Name[l-11]) == "-" {
-					deployMeta.Name = controllerRef.Name[:l-11]
+			} 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


Test patch:
diff --git a/pkg/kube/util_test.go b/pkg/kube/util_test.go
index 33e7032ad1..7183fb5902 100644
--- a/pkg/kube/util_test.go
+++ b/pkg/kube/util_test.go
@@ -18,7 +18,11 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"reflect"
 	"testing"
+
+	kubeApiCore "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 func TestBuildClientConfig(t *testing.T) {
@@ -141,3 +145,74 @@ users:
 	}
 	return filePath, nil
 }
+
+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(
+				&kubeApiCore.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)
+			}
+		})
+	}
+}