Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added rest client metrics for client TTL and rot. #84382

Merged
merged 7 commits into from Nov 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 10 additions & 2 deletions staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/BUILD
Expand Up @@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = ["exec.go"],
srcs = [
"exec.go",
"metrics.go",
],
importmap = "k8s.io/kubernetes/vendor/k8s.io/client-go/plugin/pkg/client/auth/exec",
importpath = "k8s.io/client-go/plugin/pkg/client/auth/exec",
visibility = ["//visibility:public"],
Expand All @@ -16,6 +19,7 @@ go_library(
"//staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1:go_default_library",
"//staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/tools/clientcmd/api:go_default_library",
"//staging/src/k8s.io/client-go/tools/metrics:go_default_library",
"//staging/src/k8s.io/client-go/transport:go_default_library",
"//staging/src/k8s.io/client-go/util/connrotation:go_default_library",
"//vendor/github.com/davecgh/go-spew/spew:go_default_library",
Expand All @@ -26,14 +30,18 @@ go_library(

go_test(
name = "go_default_test",
srcs = ["exec_test.go"],
srcs = [
"exec_test.go",
"metrics_test.go",
],
data = glob(["testdata/**"]),
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/client-go/pkg/apis/clientauthentication:go_default_library",
"//staging/src/k8s.io/client-go/tools/clientcmd/api:go_default_library",
"//staging/src/k8s.io/client-go/tools/metrics:go_default_library",
"//staging/src/k8s.io/client-go/transport:go_default_library",
],
)
Expand Down
28 changes: 27 additions & 1 deletion staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go
Expand Up @@ -20,6 +20,7 @@ import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
Expand All @@ -42,6 +43,7 @@ import (
"k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1"
"k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/tools/metrics"
"k8s.io/client-go/transport"
"k8s.io/client-go/util/connrotation"
"k8s.io/klog"
Expand Down Expand Up @@ -260,13 +262,16 @@ func (a *Authenticator) cert() (*tls.Certificate, error) {
func (a *Authenticator) getCreds() (*credentials, error) {
a.mu.Lock()
defer a.mu.Unlock()
defer expirationMetrics.report(time.Now)

if a.cachedCreds != nil && !a.credsExpired() {
return a.cachedCreds, nil
}

if err := a.refreshCredsLocked(nil); err != nil {
return nil, err
}

return a.cachedCreds, nil
}

Expand Down Expand Up @@ -355,17 +360,38 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err
if err != nil {
return fmt.Errorf("failed parsing client key/certificate: %v", err)
}

// Leaf is initialized to be nil:
// https://golang.org/pkg/crypto/tls/#X509KeyPair
// Leaf certificate is the first certificate:
// https://golang.org/pkg/crypto/tls/#Certificate
// Populating leaf is useful for quickly accessing the underlying x509
// certificate values.
cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return fmt.Errorf("failed parsing client leaf certificate: %v", err)
}
newCreds.cert = &cert
}

oldCreds := a.cachedCreds
a.cachedCreds = newCreds
// Only close all connections when TLS cert rotates. Token rotation doesn't
// need the extra noise.
if len(a.onRotateList) > 0 && oldCreds != nil && !reflect.DeepEqual(oldCreds.cert, a.cachedCreds.cert) {
if oldCreds != nil && !reflect.DeepEqual(oldCreds.cert, a.cachedCreds.cert) {
// Can be nil if the exec auth plugin only returned token auth.
if oldCreds.cert != nil && oldCreds.cert.Leaf != nil {
sambdavidson marked this conversation as resolved.
Show resolved Hide resolved
metrics.ClientCertRotationAge.Observe(time.Now().Sub(oldCreds.cert.Leaf.NotBefore))
sambdavidson marked this conversation as resolved.
Show resolved Hide resolved
}
for _, onRotate := range a.onRotateList {
onRotate()
}
}

expiry := time.Time{}
if a.cachedCreds.cert != nil && a.cachedCreds.cert.Leaf != nil {
expiry = a.cachedCreds.cert.Leaf.NotAfter
}
expirationMetrics.set(a, expiry)
return nil
}
Expand Up @@ -97,6 +97,10 @@ func init() {
if err != nil {
panic(err)
}
cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
if err != nil {
panic(err)
}
validCert = &cert
}

Expand Down Expand Up @@ -760,7 +764,7 @@ func TestConcurrentUpdateTransportConfig(t *testing.T) {
}

// genClientCert generates an x509 certificate for testing. Certificate and key
// are returned in PEM encoding.
// are returned in PEM encoding. The generated cert expires in 24 hours.
func genClientCert(t *testing.T) ([]byte, []byte) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
Expand Down
@@ -0,0 +1,64 @@
/*
Copyright 2018 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 exec
sambdavidson marked this conversation as resolved.
Show resolved Hide resolved

import (
"sync"
"time"

"k8s.io/client-go/tools/metrics"
)

type certificateExpirationTracker struct {
mu sync.RWMutex
m map[*Authenticator]time.Time
earliest time.Time
}

var expirationMetrics = &certificateExpirationTracker{m: map[*Authenticator]time.Time{}}

// set stores the given expiration time and updates the updates earliest.
func (c *certificateExpirationTracker) set(a *Authenticator, t time.Time) {
c.mu.Lock()
defer c.mu.Unlock()
c.m[a] = t

// update earliest
earliest := time.Time{}
for _, t := range c.m {
if t.IsZero() {
continue
}
if earliest.IsZero() || earliest.After(t) {
earliest = t
}
}
c.earliest = earliest
}

// report reports the ttl to the earliest reported expiration time.
// If no Authenticators have reported a certificate expiration, this reports nil.
func (c *certificateExpirationTracker) report(now func() time.Time) {
c.mu.RLock()
defer c.mu.RUnlock()
if c.earliest.IsZero() {
metrics.ClientCertTTL.Set(nil)
} else {
ttl := c.earliest.Sub(now())
metrics.ClientCertTTL.Set(&ttl)
}
}
@@ -0,0 +1,106 @@
/*
Copyright 2018 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 exec
sambdavidson marked this conversation as resolved.
Show resolved Hide resolved

import (
"testing"
"time"

"k8s.io/client-go/tools/metrics"
)

type mockTTLGauge struct {
v *time.Duration
}

func (m *mockTTLGauge) Set(d *time.Duration) {
m.v = d
}

func ptr(d time.Duration) *time.Duration {
return &d
}

func TestCertificateExpirationTracker(t *testing.T) {
now := time.Now()
nowFn := func() time.Time { return now }
mockMetric := &mockTTLGauge{}
realMetric := metrics.ClientCertTTL
metrics.ClientCertTTL = mockMetric
defer func() {
metrics.ClientCertTTL = realMetric
}()

tracker := &certificateExpirationTracker{m: map[*Authenticator]time.Time{}}
tracker.report(nowFn)
if mockMetric.v != nil {
t.Error("empty tracker should record nil value")
}

firstAuthenticator := &Authenticator{}
secondAuthenticator := &Authenticator{}
for _, tc := range []struct {
desc string
auth *Authenticator
time time.Time
want *time.Duration
}{
{
desc: "ttl for one authenticator",
auth: firstAuthenticator,
time: now.Add(time.Minute * 10),
want: ptr(time.Minute * 10),
},
{
desc: "second authenticator shorter ttl",
auth: secondAuthenticator,
time: now.Add(time.Minute * 5),
want: ptr(time.Minute * 5),
},
{
desc: "update shorter to be longer",
auth: secondAuthenticator,
time: now.Add(time.Minute * 15),
want: ptr(time.Minute * 10),
},
{
desc: "update shorter to be zero time",
auth: firstAuthenticator,
time: time.Time{},
want: ptr(time.Minute * 15),
},
{
desc: "update last to be zero time records nil",
auth: secondAuthenticator,
time: time.Time{},
want: nil,
},
} {
// Must run in series as the tests build off each other.
t.Run(tc.desc, func(t *testing.T) {
tracker.set(tc.auth, tc.time)
tracker.report(nowFn)
if mockMetric.v != nil && tc.want != nil {
if mockMetric.v.Seconds() != tc.want.Seconds() {
t.Errorf("got: %v; want: %v", mockMetric.v, tc.want)
}
} else if mockMetric.v != tc.want {
t.Errorf("got: %v; want: %v", mockMetric.v, tc.want)
}
})
}
}
46 changes: 43 additions & 3 deletions staging/src/k8s.io/client-go/tools/metrics/metrics.go
Expand Up @@ -26,6 +26,16 @@ import (

var registerMetrics sync.Once

// DurationMetric is a measurement of some amount of time.
type DurationMetric interface {
Observe(duration time.Duration)
}

// TTLMetric sets the time to live of something.
type TTLMetric interface {
Set(ttl *time.Duration)
}

// LatencyMetric observes client latency partitioned by verb and url.
type LatencyMetric interface {
Observe(verb string, u url.URL, latency time.Duration)
Expand All @@ -37,21 +47,51 @@ type ResultMetric interface {
}

var (
// ClientCertTTL is the time to live of a client certificate
ClientCertTTL TTLMetric = noopTTL{}
// ClientCertRotationAge is the age of a certificate that has just been rotated.
sambdavidson marked this conversation as resolved.
Show resolved Hide resolved
ClientCertRotationAge DurationMetric = noopDuration{}
// RequestLatency is the latency metric that rest clients will update.
RequestLatency LatencyMetric = noopLatency{}
// RequestResult is the result metric that rest clients will update.
RequestResult ResultMetric = noopResult{}
)

// RegisterOpts contains all the metrics to register. Metrics may be nil.
type RegisterOpts struct {
ClientCertTTL TTLMetric
ClientCertRotationAge DurationMetric
RequestLatency LatencyMetric
RequestResult ResultMetric
}

// Register registers metrics for the rest client to use. This can
// only be called once.
func Register(lm LatencyMetric, rm ResultMetric) {
func Register(opts RegisterOpts) {
registerMetrics.Do(func() {
RequestLatency = lm
RequestResult = rm
if opts.ClientCertTTL != nil {
ClientCertTTL = opts.ClientCertTTL
}
if opts.ClientCertRotationAge != nil {
ClientCertRotationAge = opts.ClientCertRotationAge
}
if opts.RequestLatency != nil {
RequestLatency = opts.RequestLatency
}
if opts.RequestResult != nil {
RequestResult = opts.RequestResult
}
})
}

type noopDuration struct{}

func (noopDuration) Observe(time.Duration) {}

type noopTTL struct{}

func (noopTTL) Set(*time.Duration) {}

type noopLatency struct{}

func (noopLatency) Observe(string, url.URL, time.Duration) {}
Expand Down