// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package testutil import ( "strconv" "strings" "testing" "github.com/stretchr/testify/require" certificatesv1 "k8s.io/api/certificates/v1" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/discovery" ) func KubeServerSupportsCertificatesV1API(t *testing.T, discoveryClient discovery.DiscoveryInterface) bool { t.Helper() groupList, err := discoveryClient.ServerGroups() require.NoError(t, err) for _, group := range groupList.Groups { if group.Name == certificatesv1.GroupName { for _, version := range group.Versions { if version.Version == "v1" { // Note: v1 should exist in Kubernetes 1.19 and above return true } } } continue } return false } func PrintKubeServerVersion(t *testing.T, discoveryClient discovery.DiscoveryInterface) { t.Helper() version, err := discoveryClient.ServerVersion() require.NoError(t, err) t.Logf("K8s server version: %s\n%+v", version, version) } func KubeServerMinorVersionAtLeastInclusive(t *testing.T, discoveryClient discovery.DiscoveryInterface, min int) bool { return !KubeServerMinorVersionInBetweenInclusive(t, discoveryClient, 0, min-1) } func KubeServerMinorVersion(t *testing.T, discoveryClient discovery.DiscoveryInterface) int { t.Helper() version, err := discoveryClient.ServerVersion() require.NoError(t, err) require.Equal(t, "1", version.Major) minor, err := strconv.Atoi(strings.TrimSuffix(version.Minor, "+")) require.NoError(t, err) return minor } func KubeServerMinorVersionInBetweenInclusive(t *testing.T, discoveryClient discovery.DiscoveryInterface, min, max int) bool { minor := KubeServerMinorVersion(t, discoveryClient) return minor >= min && minor <= max } func convertMap[K1, K2 comparable, V1, V2 any](m1 map[K1]V1, fT func(K1) K2, fU func(V1) V2) map[K2]V2 { m2 := make(map[K2]V2) for k, v := range m1 { m2[fT(k)] = fU(v) } return m2 } func identity[T any](t T) T { return t } func CheckServiceAccountExtraFieldsAccountingForChangesInK8s1_30[M ~map[string]V, V ~[]string]( t *testing.T, discoveryClient discovery.DiscoveryInterface, actualExtras M, expectedPodValues *corev1.Pod, ) { t.Helper() extra := convertMap( actualExtras, identity[string], func(v V) []string { return v }, ) require.Equal(t, extra["authentication.kubernetes.io/pod-name"], []string{expectedPodValues.Name}) require.Equal(t, extra["authentication.kubernetes.io/pod-uid"], []string{string(expectedPodValues.UID)}) if KubeServerMinorVersionAtLeastInclusive(t, discoveryClient, 30) { // Starting in K8s 1.30, three additional `Extra` fields were added with unpredictable values. // This is because the following three feature gates were enabled by default in 1.30. // https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/ // - ServiceAccountTokenJTI // - ServiceAccountTokenNodeBindingValidation // - ServiceAccountTokenPodNodeInfo // These were added in source code in 1.29 but not enabled by default until 1.30. // <1.29: https://pkg.go.dev/k8s.io/apiserver@v0.28.7/pkg/authentication/serviceaccount // 1.29+: https://pkg.go.dev/k8s.io/apiserver@v0.29.0/pkg/authentication/serviceaccount require.Equal(t, 5, len(extra)) require.NotEmpty(t, extra["authentication.kubernetes.io/credential-id"]) require.NotEmpty(t, extra["authentication.kubernetes.io/node-name"]) require.NotEmpty(t, extra["authentication.kubernetes.io/node-uid"]) } else { require.Equal(t, 2, len(extra)) } }