mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-03 17:30:00 +00:00
Add allowed/denied metrics for authorizers
This commit is contained in:
parent
91ee30074b
commit
d5d3eddb95
@ -32,6 +32,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
|
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
|
||||||
|
authorizationmetrics "k8s.io/apiserver/pkg/authorization/metrics"
|
||||||
"k8s.io/apiserver/pkg/authorization/union"
|
"k8s.io/apiserver/pkg/authorization/union"
|
||||||
"k8s.io/apiserver/pkg/server/options/authorizationconfig/metrics"
|
"k8s.io/apiserver/pkg/server/options/authorizationconfig/metrics"
|
||||||
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||||
@ -101,21 +102,21 @@ func (r *reloadableAuthorizerResolver) newForConfig(authzConfig *authzconfig.Aut
|
|||||||
if r.nodeAuthorizer == nil {
|
if r.nodeAuthorizer == nil {
|
||||||
return nil, nil, fmt.Errorf("authorizer type Node is not allowed if it was not enabled at initial server startup")
|
return nil, nil, fmt.Errorf("authorizer type Node is not allowed if it was not enabled at initial server startup")
|
||||||
}
|
}
|
||||||
authorizers = append(authorizers, r.nodeAuthorizer)
|
authorizers = append(authorizers, authorizationmetrics.InstrumentedAuthorizer(string(configuredAuthorizer.Type), configuredAuthorizer.Name, r.nodeAuthorizer))
|
||||||
ruleResolvers = append(ruleResolvers, r.nodeAuthorizer)
|
ruleResolvers = append(ruleResolvers, r.nodeAuthorizer)
|
||||||
case authzconfig.AuthorizerType(modes.ModeAlwaysAllow):
|
case authzconfig.AuthorizerType(modes.ModeAlwaysAllow):
|
||||||
alwaysAllowAuthorizer := authorizerfactory.NewAlwaysAllowAuthorizer()
|
alwaysAllowAuthorizer := authorizerfactory.NewAlwaysAllowAuthorizer()
|
||||||
authorizers = append(authorizers, alwaysAllowAuthorizer)
|
authorizers = append(authorizers, authorizationmetrics.InstrumentedAuthorizer(string(configuredAuthorizer.Type), configuredAuthorizer.Name, alwaysAllowAuthorizer))
|
||||||
ruleResolvers = append(ruleResolvers, alwaysAllowAuthorizer)
|
ruleResolvers = append(ruleResolvers, alwaysAllowAuthorizer)
|
||||||
case authzconfig.AuthorizerType(modes.ModeAlwaysDeny):
|
case authzconfig.AuthorizerType(modes.ModeAlwaysDeny):
|
||||||
alwaysDenyAuthorizer := authorizerfactory.NewAlwaysDenyAuthorizer()
|
alwaysDenyAuthorizer := authorizerfactory.NewAlwaysDenyAuthorizer()
|
||||||
authorizers = append(authorizers, alwaysDenyAuthorizer)
|
authorizers = append(authorizers, authorizationmetrics.InstrumentedAuthorizer(string(configuredAuthorizer.Type), configuredAuthorizer.Name, alwaysDenyAuthorizer))
|
||||||
ruleResolvers = append(ruleResolvers, alwaysDenyAuthorizer)
|
ruleResolvers = append(ruleResolvers, alwaysDenyAuthorizer)
|
||||||
case authzconfig.AuthorizerType(modes.ModeABAC):
|
case authzconfig.AuthorizerType(modes.ModeABAC):
|
||||||
if r.abacAuthorizer == nil {
|
if r.abacAuthorizer == nil {
|
||||||
return nil, nil, fmt.Errorf("authorizer type ABAC is not allowed if it was not enabled at initial server startup")
|
return nil, nil, fmt.Errorf("authorizer type ABAC is not allowed if it was not enabled at initial server startup")
|
||||||
}
|
}
|
||||||
authorizers = append(authorizers, r.abacAuthorizer)
|
authorizers = append(authorizers, authorizationmetrics.InstrumentedAuthorizer(string(configuredAuthorizer.Type), configuredAuthorizer.Name, r.abacAuthorizer))
|
||||||
ruleResolvers = append(ruleResolvers, r.abacAuthorizer)
|
ruleResolvers = append(ruleResolvers, r.abacAuthorizer)
|
||||||
case authzconfig.AuthorizerType(modes.ModeWebhook):
|
case authzconfig.AuthorizerType(modes.ModeWebhook):
|
||||||
if r.initialConfig.WebhookRetryBackoff == nil {
|
if r.initialConfig.WebhookRetryBackoff == nil {
|
||||||
@ -145,13 +146,13 @@ func (r *reloadableAuthorizerResolver) newForConfig(authzConfig *authzconfig.Aut
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
authorizers = append(authorizers, webhookAuthorizer)
|
authorizers = append(authorizers, authorizationmetrics.InstrumentedAuthorizer(string(configuredAuthorizer.Type), configuredAuthorizer.Name, webhookAuthorizer))
|
||||||
ruleResolvers = append(ruleResolvers, webhookAuthorizer)
|
ruleResolvers = append(ruleResolvers, webhookAuthorizer)
|
||||||
case authzconfig.AuthorizerType(modes.ModeRBAC):
|
case authzconfig.AuthorizerType(modes.ModeRBAC):
|
||||||
if r.rbacAuthorizer == nil {
|
if r.rbacAuthorizer == nil {
|
||||||
return nil, nil, fmt.Errorf("authorizer type RBAC is not allowed if it was not enabled at initial server startup")
|
return nil, nil, fmt.Errorf("authorizer type RBAC is not allowed if it was not enabled at initial server startup")
|
||||||
}
|
}
|
||||||
authorizers = append(authorizers, r.rbacAuthorizer)
|
authorizers = append(authorizers, authorizationmetrics.InstrumentedAuthorizer(string(configuredAuthorizer.Type), configuredAuthorizer.Name, r.rbacAuthorizer))
|
||||||
ruleResolvers = append(ruleResolvers, r.rbacAuthorizer)
|
ruleResolvers = append(ruleResolvers, r.rbacAuthorizer)
|
||||||
default:
|
default:
|
||||||
return nil, nil, fmt.Errorf("unknown authorization mode %s specified", configuredAuthorizer.Type)
|
return nil, nil, fmt.Errorf("unknown authorization mode %s specified", configuredAuthorizer.Type)
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes 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 metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
"k8s.io/component-base/metrics"
|
||||||
|
"k8s.io/component-base/metrics/legacyregistry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
namespace = "apiserver"
|
||||||
|
subsystem = "authorization"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
authorizationDecisionsTotal = metrics.NewCounterVec(
|
||||||
|
&metrics.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: subsystem,
|
||||||
|
Name: "decisions_total",
|
||||||
|
Help: "Total number of terminal decisions made by an authorizer split by authorizer type, name, and decision.",
|
||||||
|
StabilityLevel: metrics.ALPHA,
|
||||||
|
},
|
||||||
|
[]string{"type", "name", "decision"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
var registerMetrics sync.Once
|
||||||
|
|
||||||
|
func RegisterMetrics() {
|
||||||
|
registerMetrics.Do(func() {
|
||||||
|
legacyregistry.MustRegister(authorizationDecisionsTotal)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResetMetricsForTest() {
|
||||||
|
authorizationDecisionsTotal.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecordAuthorizationDecision(authorizerType, authorizerName, decision string) {
|
||||||
|
authorizationDecisionsTotal.WithLabelValues(authorizerType, authorizerName, decision).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func InstrumentedAuthorizer(authorizerType string, authorizerName string, delegate authorizer.Authorizer) authorizer.Authorizer {
|
||||||
|
RegisterMetrics()
|
||||||
|
return &instrumentedAuthorizer{
|
||||||
|
authorizerType: string(authorizerType),
|
||||||
|
authorizerName: authorizerName,
|
||||||
|
delegate: delegate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type instrumentedAuthorizer struct {
|
||||||
|
authorizerType string
|
||||||
|
authorizerName string
|
||||||
|
delegate authorizer.Authorizer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *instrumentedAuthorizer) Authorize(ctx context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
|
decision, reason, err := a.delegate.Authorize(ctx, attributes)
|
||||||
|
switch decision {
|
||||||
|
case authorizer.DecisionNoOpinion:
|
||||||
|
// non-terminal, not reported
|
||||||
|
case authorizer.DecisionAllow:
|
||||||
|
// matches SubjectAccessReview status.allowed field name
|
||||||
|
RecordAuthorizationDecision(a.authorizerType, a.authorizerName, "allowed")
|
||||||
|
case authorizer.DecisionDeny:
|
||||||
|
// matches SubjectAccessReview status.denied field name
|
||||||
|
RecordAuthorizationDecision(a.authorizerType, a.authorizerName, "denied")
|
||||||
|
default:
|
||||||
|
RecordAuthorizationDecision(a.authorizerType, a.authorizerName, "unknown")
|
||||||
|
}
|
||||||
|
return decision, reason, err
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes 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 metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
"k8s.io/component-base/metrics/legacyregistry"
|
||||||
|
"k8s.io/component-base/metrics/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRecordAuthorizationDecisionsTotal(t *testing.T) {
|
||||||
|
prefix := `
|
||||||
|
# HELP apiserver_authorization_decisions_total [ALPHA] Total number of terminal decisions made by an authorizer split by authorizer type, name, and decision.
|
||||||
|
# TYPE apiserver_authorization_decisions_total counter`
|
||||||
|
metrics := []string{
|
||||||
|
namespace + "_" + subsystem + "_decisions_total",
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizationDecisionsTotal.Reset()
|
||||||
|
RegisterMetrics()
|
||||||
|
|
||||||
|
dummyAuthorizer := &dummyAuthorizer{}
|
||||||
|
a := InstrumentedAuthorizer("mytype", "myname", dummyAuthorizer)
|
||||||
|
|
||||||
|
// allow
|
||||||
|
{
|
||||||
|
dummyAuthorizer.decision = authorizer.DecisionAllow
|
||||||
|
_, _, _ = a.Authorize(context.Background(), nil)
|
||||||
|
expectedValue := prefix + `
|
||||||
|
apiserver_authorization_decisions_total{decision="allowed",name="myname",type="mytype"} 1
|
||||||
|
`
|
||||||
|
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), metrics...); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
authorizationDecisionsTotal.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// deny
|
||||||
|
{
|
||||||
|
dummyAuthorizer.decision = authorizer.DecisionDeny
|
||||||
|
_, _, _ = a.Authorize(context.Background(), nil)
|
||||||
|
_, _, _ = a.Authorize(context.Background(), nil)
|
||||||
|
expectedValue := prefix + `
|
||||||
|
apiserver_authorization_decisions_total{decision="denied",name="myname",type="mytype"} 2
|
||||||
|
`
|
||||||
|
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), metrics...); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
authorizationDecisionsTotal.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no-opinion emits no metric
|
||||||
|
{
|
||||||
|
dummyAuthorizer.decision = authorizer.DecisionNoOpinion
|
||||||
|
_, _, _ = a.Authorize(context.Background(), nil)
|
||||||
|
_, _, _ = a.Authorize(context.Background(), nil)
|
||||||
|
expectedValue := prefix + `
|
||||||
|
`
|
||||||
|
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), metrics...); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
authorizationDecisionsTotal.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknown decision emits a metric
|
||||||
|
{
|
||||||
|
dummyAuthorizer.decision = authorizer.DecisionDeny + 10
|
||||||
|
_, _, _ = a.Authorize(context.Background(), nil)
|
||||||
|
expectedValue := prefix + `
|
||||||
|
apiserver_authorization_decisions_total{decision="unknown",name="myname",type="mytype"} 1
|
||||||
|
`
|
||||||
|
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), metrics...); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
authorizationDecisionsTotal.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type dummyAuthorizer struct {
|
||||||
|
decision authorizer.Decision
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyAuthorizer) Authorize(ctx context.Context, attrs authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
|
return d.decision, "", d.err
|
||||||
|
}
|
@ -73,7 +73,6 @@ func RegisterMetrics() {
|
|||||||
func ResetMetricsForTest() {
|
func ResetMetricsForTest() {
|
||||||
authorizationConfigAutomaticReloadsTotal.Reset()
|
authorizationConfigAutomaticReloadsTotal.Reset()
|
||||||
authorizationConfigAutomaticReloadLastTimestampSeconds.Reset()
|
authorizationConfigAutomaticReloadLastTimestampSeconds.Reset()
|
||||||
legacyregistry.Reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RecordAuthorizationConfigAutomaticReloadFailure(apiServerID string) {
|
func RecordAuthorizationConfigAutomaticReloadFailure(apiServerID string) {
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@ -35,6 +36,7 @@ import (
|
|||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
authorizationmetrics "k8s.io/apiserver/pkg/authorization/metrics"
|
||||||
"k8s.io/apiserver/pkg/features"
|
"k8s.io/apiserver/pkg/features"
|
||||||
authzmetrics "k8s.io/apiserver/pkg/server/options/authorizationconfig/metrics"
|
authzmetrics "k8s.io/apiserver/pkg/server/options/authorizationconfig/metrics"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
@ -178,6 +180,7 @@ users:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns a deny response when called
|
// returns a deny response when called
|
||||||
|
denyName := "deny.example.com"
|
||||||
serverDenyCalled := atomic.Int32{}
|
serverDenyCalled := atomic.Int32{}
|
||||||
serverDeny := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
serverDeny := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
serverDenyCalled.Add(1)
|
serverDenyCalled.Add(1)
|
||||||
@ -221,6 +224,7 @@ users:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns an allow response when called
|
// returns an allow response when called
|
||||||
|
allowName := "allow.example.com"
|
||||||
serverAllowCalled := atomic.Int32{}
|
serverAllowCalled := atomic.Int32{}
|
||||||
serverAllow := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
serverAllow := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
serverAllowCalled.Add(1)
|
serverAllowCalled.Add(1)
|
||||||
@ -242,6 +246,7 @@ users:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns an allow response when called
|
// returns an allow response when called
|
||||||
|
allowReloadedName := "allowreloaded.example.com"
|
||||||
serverAllowReloadedCalled := atomic.Int32{}
|
serverAllowReloadedCalled := atomic.Int32{}
|
||||||
serverAllowReloaded := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
serverAllowReloaded := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
serverAllowReloadedCalled.Add(1)
|
serverAllowReloadedCalled.Add(1)
|
||||||
@ -269,9 +274,15 @@ users:
|
|||||||
serverNoOpinionCalled.Store(0)
|
serverNoOpinionCalled.Store(0)
|
||||||
serverAllowCalled.Store(0)
|
serverAllowCalled.Store(0)
|
||||||
serverAllowReloadedCalled.Store(0)
|
serverAllowReloadedCalled.Store(0)
|
||||||
|
authorizationmetrics.ResetMetricsForTest()
|
||||||
}
|
}
|
||||||
|
var adminClient *clientset.Clientset
|
||||||
assertCounts := func(errorCount, timeoutCount, denyCount, noOpinionCount, allowCount, allowReloadedCount int32) {
|
assertCounts := func(errorCount, timeoutCount, denyCount, noOpinionCount, allowCount, allowReloadedCount int32) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
metrics, err := getMetrics(t, adminClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error getting metrics: %v", err)
|
||||||
|
}
|
||||||
if e, a := errorCount, serverErrorCalled.Load(); e != a {
|
if e, a := errorCount, serverErrorCalled.Load(); e != a {
|
||||||
t.Errorf("expected fail webhook calls: %d, got %d", e, a)
|
t.Errorf("expected fail webhook calls: %d, got %d", e, a)
|
||||||
}
|
}
|
||||||
@ -281,15 +292,24 @@ users:
|
|||||||
if e, a := denyCount, serverDenyCalled.Load(); e != a {
|
if e, a := denyCount, serverDenyCalled.Load(); e != a {
|
||||||
t.Errorf("expected deny webhook calls: %d, got %d", e, a)
|
t.Errorf("expected deny webhook calls: %d, got %d", e, a)
|
||||||
}
|
}
|
||||||
|
if e, a := denyCount, metrics.decisions[authorizerKey{authorizerType: "Webhook", authorizerName: denyName}]["denied"]; e != int32(a) {
|
||||||
|
t.Errorf("expected deny webhook denied metrics calls: %d, got %d", e, a)
|
||||||
|
}
|
||||||
if e, a := noOpinionCount, serverNoOpinionCalled.Load(); e != a {
|
if e, a := noOpinionCount, serverNoOpinionCalled.Load(); e != a {
|
||||||
t.Errorf("expected noOpinion webhook calls: %d, got %d", e, a)
|
t.Errorf("expected noOpinion webhook calls: %d, got %d", e, a)
|
||||||
}
|
}
|
||||||
if e, a := allowCount, serverAllowCalled.Load(); e != a {
|
if e, a := allowCount, serverAllowCalled.Load(); e != a {
|
||||||
t.Errorf("expected allow webhook calls: %d, got %d", e, a)
|
t.Errorf("expected allow webhook calls: %d, got %d", e, a)
|
||||||
}
|
}
|
||||||
|
if e, a := allowCount, metrics.decisions[authorizerKey{authorizerType: "Webhook", authorizerName: allowName}]["allowed"]; e != int32(a) {
|
||||||
|
t.Errorf("expected allow webhook allowed metrics calls: %d, got %d", e, a)
|
||||||
|
}
|
||||||
if e, a := allowReloadedCount, serverAllowReloadedCalled.Load(); e != a {
|
if e, a := allowReloadedCount, serverAllowReloadedCalled.Load(); e != a {
|
||||||
t.Errorf("expected allowReloaded webhook calls: %d, got %d", e, a)
|
t.Errorf("expected allowReloaded webhook calls: %d, got %d", e, a)
|
||||||
}
|
}
|
||||||
|
if e, a := allowReloadedCount, metrics.decisions[authorizerKey{authorizerType: "Webhook", authorizerName: allowReloadedName}]["allowed"]; e != int32(a) {
|
||||||
|
t.Errorf("expected allowReloaded webhook allowed metrics calls: %d, got %d", e, a)
|
||||||
|
}
|
||||||
resetCounts()
|
resetCounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,7 +353,7 @@ authorizers:
|
|||||||
- expression: 'request.resourceAttributes.name == "timeout"'
|
- expression: 'request.resourceAttributes.name == "timeout"'
|
||||||
|
|
||||||
- type: Webhook
|
- type: Webhook
|
||||||
name: deny.example.com
|
name: `+denyName+`
|
||||||
webhook:
|
webhook:
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
failurePolicy: NoOpinion
|
failurePolicy: NoOpinion
|
||||||
@ -361,7 +381,7 @@ authorizers:
|
|||||||
kubeConfigFile: `+serverNoOpinionKubeconfigName+`
|
kubeConfigFile: `+serverNoOpinionKubeconfigName+`
|
||||||
|
|
||||||
- type: Webhook
|
- type: Webhook
|
||||||
name: allow.example.com
|
name: `+allowName+`
|
||||||
webhook:
|
webhook:
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
failurePolicy: Deny
|
failurePolicy: Deny
|
||||||
@ -383,7 +403,7 @@ authorizers:
|
|||||||
)
|
)
|
||||||
t.Cleanup(server.TearDownFn)
|
t.Cleanup(server.TearDownFn)
|
||||||
|
|
||||||
adminClient := clientset.NewForConfigOrDie(server.ClientConfig)
|
adminClient = clientset.NewForConfigOrDie(server.ClientConfig)
|
||||||
|
|
||||||
// malformed webhook short circuits
|
// malformed webhook short circuits
|
||||||
t.Log("checking error")
|
t.Log("checking error")
|
||||||
@ -470,14 +490,14 @@ authorizers:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check last loaded success/failure metric timestamps, ensure success is present, failure is not
|
// check last loaded success/failure metric timestamps, ensure success is present, failure is not
|
||||||
initialReloadSuccess, initialReloadFailure, err := getReloadTimes(t, adminClient)
|
initialMetrics, err := getMetrics(t, adminClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if initialReloadSuccess == nil {
|
if initialMetrics.reloadSuccess == nil {
|
||||||
t.Fatal("expected success timestamp, got none")
|
t.Fatal("expected success timestamp, got none")
|
||||||
}
|
}
|
||||||
if initialReloadFailure != nil {
|
if initialMetrics.reloadFailure != nil {
|
||||||
t.Fatal("expected no failure timestamp, got one")
|
t.Fatal("expected no failure timestamp, got one")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -487,24 +507,24 @@ authorizers:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wait for failure timestamp > success timestamp
|
// wait for failure timestamp > success timestamp
|
||||||
var reload1Success, reload1Failure *time.Time
|
var reload1Metrics *metrics
|
||||||
err = wait.PollUntilContextTimeout(context.TODO(), time.Second, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) {
|
err = wait.PollUntilContextTimeout(context.TODO(), time.Second, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) {
|
||||||
reload1Success, reload1Failure, err = getReloadTimes(t, adminClient)
|
reload1Metrics, err = getMetrics(t, adminClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if reload1Success == nil {
|
if reload1Metrics.reloadSuccess == nil {
|
||||||
t.Fatal("expected success timestamp, got none")
|
t.Fatal("expected success timestamp, got none")
|
||||||
}
|
}
|
||||||
if !reload1Success.Equal(*initialReloadSuccess) {
|
if !reload1Metrics.reloadSuccess.Equal(*initialMetrics.reloadSuccess) {
|
||||||
t.Fatalf("success timestamp changed from initial success %s to %s unexpectedly", initialReloadSuccess.String(), reload1Success.String())
|
t.Fatalf("success timestamp changed from initial success %s to %s unexpectedly", initialMetrics.reloadSuccess.String(), reload1Metrics.reloadSuccess.String())
|
||||||
}
|
}
|
||||||
if reload1Failure == nil {
|
if reload1Metrics.reloadFailure == nil {
|
||||||
t.Log("expected failure timestamp, got nil, retrying")
|
t.Log("expected failure timestamp, got nil, retrying")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
if !reload1Failure.After(*reload1Success) {
|
if !reload1Metrics.reloadFailure.After(*reload1Metrics.reloadSuccess) {
|
||||||
t.Fatalf("expected failure timestamp to be more recent than success timestamp, got %s <= %s", reload1Failure.String(), reload1Success.String())
|
t.Fatalf("expected failure timestamp to be more recent than success timestamp, got %s <= %s", reload1Metrics.reloadFailure.String(), reload1Metrics.reloadSuccess.String())
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
})
|
})
|
||||||
@ -539,7 +559,7 @@ apiVersion: apiserver.config.k8s.io/v1alpha1
|
|||||||
kind: AuthorizationConfiguration
|
kind: AuthorizationConfiguration
|
||||||
authorizers:
|
authorizers:
|
||||||
- type: Webhook
|
- type: Webhook
|
||||||
name: allowreloaded.example.com
|
name: `+allowReloadedName+`
|
||||||
webhook:
|
webhook:
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
failurePolicy: Deny
|
failurePolicy: Deny
|
||||||
@ -553,29 +573,29 @@ authorizers:
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for success timestamp > reload1Failure timestamp
|
// wait for success timestamp > reload1Metrics.reloadFailure timestamp
|
||||||
var reload2Success, reload2Failure *time.Time
|
var reload2Metrics *metrics
|
||||||
err = wait.PollUntilContextTimeout(context.TODO(), time.Second, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) {
|
err = wait.PollUntilContextTimeout(context.TODO(), time.Second, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) {
|
||||||
reload2Success, reload2Failure, err = getReloadTimes(t, adminClient)
|
reload2Metrics, err = getMetrics(t, adminClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if reload2Failure == nil {
|
if reload2Metrics.reloadFailure == nil {
|
||||||
t.Log("expected failure timestamp, got nil, retrying")
|
t.Log("expected failure timestamp, got nil, retrying")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
if !reload2Failure.Equal(*reload1Failure) {
|
if !reload2Metrics.reloadFailure.Equal(*reload1Metrics.reloadFailure) {
|
||||||
t.Fatalf("failure timestamp changed from reload1Failure %s to %s unexpectedly", reload1Failure.String(), reload2Failure.String())
|
t.Fatalf("failure timestamp changed from reload1Metrics.reloadFailure %s to %s unexpectedly", reload1Metrics.reloadFailure.String(), reload2Metrics.reloadFailure.String())
|
||||||
}
|
}
|
||||||
if reload2Success == nil {
|
if reload2Metrics.reloadSuccess == nil {
|
||||||
t.Fatal("expected success timestamp, got none")
|
t.Fatal("expected success timestamp, got none")
|
||||||
}
|
}
|
||||||
if reload2Success.Equal(*initialReloadSuccess) {
|
if reload2Metrics.reloadSuccess.Equal(*initialMetrics.reloadSuccess) {
|
||||||
t.Log("success timestamp hasn't updated from initial success, retrying")
|
t.Log("success timestamp hasn't updated from initial success, retrying")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
if !reload2Success.After(*reload2Failure) {
|
if !reload2Metrics.reloadSuccess.After(*reload2Metrics.reloadFailure) {
|
||||||
t.Fatalf("expected success timestamp to be more recent than failure, got %s <= %s", reload2Success.String(), reload2Failure.String())
|
t.Fatalf("expected success timestamp to be more recent than failure, got %s <= %s", reload2Metrics.reloadSuccess.String(), reload2Metrics.reloadFailure.String())
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
})
|
})
|
||||||
@ -610,28 +630,28 @@ authorizers:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wait for failure timestamp > success timestamp
|
// wait for failure timestamp > success timestamp
|
||||||
var reload3Success, reload3Failure *time.Time
|
var reload3Metrics *metrics
|
||||||
err = wait.PollUntilContextTimeout(context.TODO(), time.Second, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) {
|
err = wait.PollUntilContextTimeout(context.TODO(), time.Second, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) {
|
||||||
reload3Success, reload3Failure, err = getReloadTimes(t, adminClient)
|
reload3Metrics, err = getMetrics(t, adminClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if reload3Success == nil {
|
if reload3Metrics.reloadSuccess == nil {
|
||||||
t.Fatal("expected success timestamp, got none")
|
t.Fatal("expected success timestamp, got none")
|
||||||
}
|
}
|
||||||
if !reload3Success.Equal(*reload2Success) {
|
if !reload3Metrics.reloadSuccess.Equal(*reload2Metrics.reloadSuccess) {
|
||||||
t.Fatalf("success timestamp changed from %s to %s unexpectedly", reload2Success.String(), reload3Success.String())
|
t.Fatalf("success timestamp changed from %s to %s unexpectedly", reload2Metrics.reloadSuccess.String(), reload3Metrics.reloadSuccess.String())
|
||||||
}
|
}
|
||||||
if reload3Failure == nil {
|
if reload3Metrics.reloadFailure == nil {
|
||||||
t.Log("expected failure timestamp, got nil, retrying")
|
t.Log("expected failure timestamp, got nil, retrying")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
if reload3Failure.Equal(*reload2Failure) {
|
if reload3Metrics.reloadFailure.Equal(*reload2Metrics.reloadFailure) {
|
||||||
t.Log("failure timestamp hasn't updated, retrying")
|
t.Log("failure timestamp hasn't updated, retrying")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
if !reload3Failure.After(*reload3Success) {
|
if !reload3Metrics.reloadFailure.After(*reload3Metrics.reloadSuccess) {
|
||||||
t.Fatalf("expected failure timestamp to be more recent than success, got %s <= %s", reload3Failure.String(), reload3Success.String())
|
t.Fatalf("expected failure timestamp to be more recent than success, got %s <= %s", reload3Metrics.reloadFailure.String(), reload3Metrics.reloadSuccess.String())
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
})
|
})
|
||||||
@ -661,36 +681,69 @@ authorizers:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getReloadTimes(t *testing.T, client *clientset.Clientset) (*time.Time, *time.Time, error) {
|
type metrics struct {
|
||||||
|
reloadSuccess *time.Time
|
||||||
|
reloadFailure *time.Time
|
||||||
|
decisions map[authorizerKey]map[string]int
|
||||||
|
}
|
||||||
|
type authorizerKey struct {
|
||||||
|
authorizerType string
|
||||||
|
authorizerName string
|
||||||
|
}
|
||||||
|
|
||||||
|
var decisionMetric = regexp.MustCompile(`apiserver_authorization_decisions_total\{decision="(.*?)",name="(.*?)",type="(.*?)"\} (\d+)`)
|
||||||
|
|
||||||
|
func getMetrics(t *testing.T, client *clientset.Clientset) (*metrics, error) {
|
||||||
data, err := client.RESTClient().Get().AbsPath("/metrics").DoRaw(context.TODO())
|
data, err := client.RESTClient().Get().AbsPath("/metrics").DoRaw(context.TODO())
|
||||||
|
|
||||||
// apiserver_authorization_config_controller_automatic_reload_last_timestamp_seconds{apiserver_id_hash="sha256:4b86cfa719a83dd63a4dc6a9831edb2b59240d0f59cf215b2d51aacb3f5c395e",status="success"} 1.7002567356895502e+09
|
// apiserver_authorization_config_controller_automatic_reload_last_timestamp_seconds{apiserver_id_hash="sha256:4b86cfa719a83dd63a4dc6a9831edb2b59240d0f59cf215b2d51aacb3f5c395e",status="success"} 1.7002567356895502e+09
|
||||||
// apiserver_authorization_config_controller_automatic_reload_last_timestamp_seconds{apiserver_id_hash="sha256:4b86cfa719a83dd63a4dc6a9831edb2b59240d0f59cf215b2d51aacb3f5c395e",status="failure"} 1.7002567356895502e+09
|
// apiserver_authorization_config_controller_automatic_reload_last_timestamp_seconds{apiserver_id_hash="sha256:4b86cfa719a83dd63a4dc6a9831edb2b59240d0f59cf215b2d51aacb3f5c395e",status="failure"} 1.7002567356895502e+09
|
||||||
|
// apiserver_authorization_decisions_total{decision="allowed",name="allow.example.com",type="Webhook"} 2
|
||||||
|
// apiserver_authorization_decisions_total{decision="allowed",name="allowreloaded.example.com",type="Webhook"} 1
|
||||||
|
// apiserver_authorization_decisions_total{decision="denied",name="deny.example.com",type="Webhook"} 1
|
||||||
|
// apiserver_authorization_decisions_total{decision="denied",name="error.example.com",type="Webhook"} 1
|
||||||
|
// apiserver_authorization_decisions_total{decision="denied",name="timeout.example.com",type="Webhook"} 1
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var success, failure *time.Time
|
var m metrics
|
||||||
for _, line := range strings.Split(string(data), "\n") {
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
if matches := decisionMetric.FindStringSubmatch(line); matches != nil {
|
||||||
|
t.Log(line)
|
||||||
|
if m.decisions == nil {
|
||||||
|
m.decisions = map[authorizerKey]map[string]int{}
|
||||||
|
}
|
||||||
|
key := authorizerKey{authorizerType: matches[3], authorizerName: matches[2]}
|
||||||
|
if m.decisions[key] == nil {
|
||||||
|
m.decisions[key] = map[string]int{}
|
||||||
|
}
|
||||||
|
count, err := strconv.Atoi(matches[4])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m.decisions[key][matches[1]] = count
|
||||||
|
|
||||||
|
}
|
||||||
if strings.HasPrefix(line, "apiserver_authorization_config_controller_automatic_reload_last_timestamp_seconds") {
|
if strings.HasPrefix(line, "apiserver_authorization_config_controller_automatic_reload_last_timestamp_seconds") {
|
||||||
t.Log(line)
|
t.Log(line)
|
||||||
values := strings.Split(line, " ")
|
values := strings.Split(line, " ")
|
||||||
value, err := strconv.ParseFloat(values[len(values)-1], 64)
|
value, err := strconv.ParseFloat(values[len(values)-1], 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
seconds := int64(value)
|
seconds := int64(value)
|
||||||
nanoseconds := int64((value - float64(seconds)) * 1000000000)
|
nanoseconds := int64((value - float64(seconds)) * 1000000000)
|
||||||
tm := time.Unix(seconds, nanoseconds)
|
tm := time.Unix(seconds, nanoseconds)
|
||||||
if strings.Contains(line, `"success"`) {
|
if strings.Contains(line, `"success"`) {
|
||||||
success = &tm
|
m.reloadSuccess = &tm
|
||||||
t.Log("success", success.String())
|
t.Log("success", m.reloadSuccess.String())
|
||||||
}
|
}
|
||||||
if strings.Contains(line, `"failure"`) {
|
if strings.Contains(line, `"failure"`) {
|
||||||
failure = &tm
|
m.reloadFailure = &tm
|
||||||
t.Log("failure", failure.String())
|
t.Log("failure", m.reloadFailure.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return success, failure, nil
|
return &m, nil
|
||||||
}
|
}
|
||||||
|
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@ -1428,6 +1428,7 @@ k8s.io/apiserver/pkg/authentication/user
|
|||||||
k8s.io/apiserver/pkg/authorization/authorizer
|
k8s.io/apiserver/pkg/authorization/authorizer
|
||||||
k8s.io/apiserver/pkg/authorization/authorizerfactory
|
k8s.io/apiserver/pkg/authorization/authorizerfactory
|
||||||
k8s.io/apiserver/pkg/authorization/cel
|
k8s.io/apiserver/pkg/authorization/cel
|
||||||
|
k8s.io/apiserver/pkg/authorization/metrics
|
||||||
k8s.io/apiserver/pkg/authorization/path
|
k8s.io/apiserver/pkg/authorization/path
|
||||||
k8s.io/apiserver/pkg/authorization/union
|
k8s.io/apiserver/pkg/authorization/union
|
||||||
k8s.io/apiserver/pkg/cel
|
k8s.io/apiserver/pkg/cel
|
||||||
|
Loading…
Reference in New Issue
Block a user