KEP-5284: add constrained impersonation metrics

See https://kep.k8s.io/5284 for details.

apiserver_impersonation_attempts_total{mode, decision}
apiserver_impersonation_attempts_duration_seconds{mode, decision}
apiserver_impersonation_authorization_attempts_total{mode, decision}
apiserver_impersonation_authorization_attempts_duration_seconds{mode, decision}

Signed-off-by: Monis Khan <mok@microsoft.com>
This commit is contained in:
Monis Khan
2026-02-27 11:33:08 -05:00
parent ca6034ad9a
commit ba2a68e1db
5 changed files with 873 additions and 48 deletions

View File

@@ -36,6 +36,8 @@ import (
"net/url"
"os"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"testing"
@@ -1110,6 +1112,8 @@ func TestConstrainedImpersonation(t *testing.T) {
})
t.Run("bob impersonating alice", func(t *testing.T) {
resetAllMetrics(t, ctx, superuserClient)
impersonatorClientConfig := rest.CopyConfig(kubeConfig)
impersonatorClientConfig.BearerToken = "bob"
impersonatorClientConfig.Impersonate = rest.ImpersonationConfig{
@@ -1164,9 +1168,29 @@ func TestConstrainedImpersonation(t *testing.T) {
if !errors.IsForbidden(err) {
t.Fatalf("expected forbidden error, got %T %v", err, err)
}
assertImpersonationMetrics(t, ctx, superuserClient, []string{
`apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="user-info"} 1`,
`apiserver_impersonation_attempts_duration_seconds_count{decision="denied",mode=""} 3`,
`apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="user-info"} FP`,
`apiserver_impersonation_attempts_duration_seconds_sum{decision="denied",mode=""} FP`,
`apiserver_impersonation_attempts_total{decision="allowed",mode="user-info"} 1`,
`apiserver_impersonation_attempts_total{decision="denied",mode=""} 3`,
`apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="user-info"} 2`,
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="legacy"} 3`,
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="user-info"} 3`,
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="user-info"} 2`,
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="legacy"} 3`,
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="user-info"} 3`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="user-info"} FP`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="legacy"} FP`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="user-info"} FP`,
})
})
t.Run("bob impersonating a node", func(t *testing.T) {
resetAllMetrics(t, ctx, superuserClient)
impersonatorClientConfig := rest.CopyConfig(kubeConfig)
impersonatorClientConfig.BearerToken = "bob"
impersonatorClientConfig.Impersonate = rest.ImpersonationConfig{
@@ -1221,9 +1245,29 @@ func TestConstrainedImpersonation(t *testing.T) {
if err != nil {
t.Fatalf("expected no error, got %T %v", err, err)
}
assertImpersonationMetrics(t, ctx, superuserClient, []string{
`apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="arbitrary-node"} 1`,
`apiserver_impersonation_attempts_duration_seconds_count{decision="denied",mode=""} 2`,
`apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="arbitrary-node"} FP`,
`apiserver_impersonation_attempts_duration_seconds_sum{decision="denied",mode=""} FP`,
`apiserver_impersonation_attempts_total{decision="allowed",mode="arbitrary-node"} 1`,
`apiserver_impersonation_attempts_total{decision="denied",mode=""} 2`,
`apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="arbitrary-node"} 3`,
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="arbitrary-node"} 2`,
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="legacy"} 2`,
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="arbitrary-node"} 3`,
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="arbitrary-node"} 2`,
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="legacy"} 2`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="arbitrary-node"} FP`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="arbitrary-node"} FP`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="legacy"} FP`,
})
})
t.Run("impersonating scheduled node", func(t *testing.T) {
resetAllMetrics(t, ctx, superuserClient)
impersonatorClientConfig := rest.CopyConfig(kubeConfig)
impersonatorClientConfig.BearerToken = "serviceaccount2"
impersonatorClientConfig.Impersonate = rest.ImpersonationConfig{
@@ -1275,9 +1319,29 @@ func TestConstrainedImpersonation(t *testing.T) {
if err != nil {
t.Fatalf("expected no error, got %T %v", err, err)
}
assertImpersonationMetrics(t, ctx, superuserClient, []string{
`apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="associated-node"} 1`,
`apiserver_impersonation_attempts_duration_seconds_count{decision="denied",mode=""} 1`,
`apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="associated-node"} FP`,
`apiserver_impersonation_attempts_duration_seconds_sum{decision="denied",mode=""} FP`,
`apiserver_impersonation_attempts_total{decision="allowed",mode="associated-node"} 1`,
`apiserver_impersonation_attempts_total{decision="denied",mode=""} 1`,
`apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="associated-node"} 2`,
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="arbitrary-node"} 1`,
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="legacy"} 1`,
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="associated-node"} 2`,
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="arbitrary-node"} 1`,
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="legacy"} 1`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="associated-node"} FP`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="arbitrary-node"} FP`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="legacy"} FP`,
})
})
t.Run("fallback to legacy impersonation", func(t *testing.T) {
resetAllMetrics(t, ctx, superuserClient)
impersonatorClientConfig := rest.CopyConfig(kubeConfig)
impersonatorClientConfig.BearerToken = "bob"
impersonatorClientConfig.Impersonate = rest.ImpersonationConfig{
@@ -1293,13 +1357,68 @@ func TestConstrainedImpersonation(t *testing.T) {
Resources: []string{"users"},
})
_, err := client.CoreV1().Pods(metav1.NamespaceAll).List(ctx, metav1.ListOptions{})
_, err := client.CoreV1().Pods(metav1.NamespaceAll).List(ctx, metav1.ListOptions{
LabelSelector: "app=panda", // force this request to have a different cache key than the earlier test
})
if err != nil {
t.Fatalf("expected no error, got %T %v", err, err)
}
assertImpersonationMetrics(t, ctx, superuserClient, []string{
`apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="legacy"} 1`,
`apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="legacy"} FP`,
`apiserver_impersonation_attempts_total{decision="allowed",mode="legacy"} 1`,
`apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="legacy"} 1`,
`apiserver_impersonation_authorization_attempts_total{decision="denied",mode="user-info"} 1`,
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="legacy"} 1`,
`apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="user-info"} 1`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="legacy"} FP`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="user-info"} FP`,
})
})
}
func resetAllMetrics(t *testing.T, ctx context.Context, client clientset.Interface) {
t.Helper()
if err := client.CoreV1().RESTClient().Delete().AbsPath("/metrics").Do(ctx).Error(); err != nil {
t.Fatalf("failed to reset metrics: %v", err)
}
}
func assertImpersonationMetrics(t *testing.T, ctx context.Context, client clientset.Interface, wantMetricStrings []string) {
t.Helper()
rc := client.CoreV1().RESTClient()
body, err := rc.Get().AbsPath("/metrics").DoRaw(ctx)
if err != nil {
t.Fatalf("failed to fetch metrics: %v", err)
}
var gotMetricStrings []string
trimFP := regexp.MustCompile(`(.*)(} \d+\.\d+.*)`)
for line := range strings.SplitSeq(string(body), "\n") {
if !strings.HasPrefix(line, "apiserver_impersonation_") {
continue
}
// skip histogram bucket lines to keep assertions manageable
if strings.Contains(line, "_bucket{") {
continue
}
if strings.Contains(line, "_seconds_sum") {
line = trimFP.ReplaceAllString(line, `$1`) + "} FP"
}
gotMetricStrings = append(gotMetricStrings, line)
}
slices.Sort(gotMetricStrings)
slices.Sort(wantMetricStrings)
if diff := cmp.Diff(wantMetricStrings, gotMetricStrings); diff != "" {
t.Errorf("unexpected impersonation metrics diff (-want +got): %s", diff)
}
}
// TestConstrainedImpersonationDisabled tests the impersonation behavior when the
// ConstrainedImpersonation feature gate is disabled. In this mode, the legacy
// impersonation behavior is expected, where a user only needs the "impersonate"