211
vendor/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go
generated
vendored
211
vendor/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go
generated
vendored
@@ -18,7 +18,6 @@ package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
@@ -29,15 +28,18 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/term"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/util/clock"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/pkg/apis/clientauthentication"
|
||||
"k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1"
|
||||
@@ -46,11 +48,16 @@ import (
|
||||
"k8s.io/client-go/tools/metrics"
|
||||
"k8s.io/client-go/transport"
|
||||
"k8s.io/client-go/util/connrotation"
|
||||
"k8s.io/klog"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const execInfoEnv = "KUBERNETES_EXEC_INFO"
|
||||
const onRotateListWarningLength = 1000
|
||||
const installHintVerboseHelp = `
|
||||
|
||||
It looks like you are trying to use a client-go credential plugin that is not installed.
|
||||
|
||||
To learn more about this feature, consult the documentation available at:
|
||||
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins`
|
||||
|
||||
var scheme = runtime.NewScheme()
|
||||
var codecs = serializer.NewCodecFactory(scheme)
|
||||
@@ -79,8 +86,15 @@ func newCache() *cache {
|
||||
|
||||
var spewConfig = &spew.ConfigState{DisableMethods: true, Indent: " "}
|
||||
|
||||
func cacheKey(c *api.ExecConfig) string {
|
||||
return spewConfig.Sprint(c)
|
||||
func cacheKey(conf *api.ExecConfig, cluster *clientauthentication.Cluster) string {
|
||||
key := struct {
|
||||
conf *api.ExecConfig
|
||||
cluster *clientauthentication.Cluster
|
||||
}{
|
||||
conf: conf,
|
||||
cluster: cluster,
|
||||
}
|
||||
return spewConfig.Sprint(key)
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
@@ -108,13 +122,51 @@ func (c *cache) put(s string, a *Authenticator) *Authenticator {
|
||||
return a
|
||||
}
|
||||
|
||||
// GetAuthenticator returns an exec-based plugin for providing client credentials.
|
||||
func GetAuthenticator(config *api.ExecConfig) (*Authenticator, error) {
|
||||
return newAuthenticator(globalCache, config)
|
||||
// sometimes rate limits how often a function f() is called. Specifically, Do()
|
||||
// will run the provided function f() up to threshold times every interval
|
||||
// duration.
|
||||
type sometimes struct {
|
||||
threshold int
|
||||
interval time.Duration
|
||||
|
||||
clock clock.Clock
|
||||
mu sync.Mutex
|
||||
|
||||
count int // times we have called f() in this window
|
||||
window time.Time // beginning of current window of length interval
|
||||
}
|
||||
|
||||
func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error) {
|
||||
key := cacheKey(config)
|
||||
func (s *sometimes) Do(f func()) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
now := s.clock.Now()
|
||||
if s.window.IsZero() {
|
||||
s.window = now
|
||||
}
|
||||
|
||||
// If we are no longer in our saved time window, then we get to reset our run
|
||||
// count back to 0 and start increasing towards the threshold again.
|
||||
if inWindow := now.Sub(s.window) < s.interval; !inWindow {
|
||||
s.window = now
|
||||
s.count = 0
|
||||
}
|
||||
|
||||
// If we have not run the function more than threshold times in this current
|
||||
// time window, we get to run it now!
|
||||
if underThreshold := s.count < s.threshold; underThreshold {
|
||||
s.count++
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
||||
// GetAuthenticator returns an exec-based plugin for providing client credentials.
|
||||
func GetAuthenticator(config *api.ExecConfig, cluster *clientauthentication.Cluster) (*Authenticator, error) {
|
||||
return newAuthenticator(globalCache, config, cluster)
|
||||
}
|
||||
|
||||
func newAuthenticator(c *cache, config *api.ExecConfig, cluster *clientauthentication.Cluster) (*Authenticator, error) {
|
||||
key := cacheKey(config, cluster)
|
||||
if a, ok := c.get(key); ok {
|
||||
return a, nil
|
||||
}
|
||||
@@ -124,16 +176,34 @@ func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error)
|
||||
return nil, fmt.Errorf("exec plugin: invalid apiVersion %q", config.APIVersion)
|
||||
}
|
||||
|
||||
connTracker := connrotation.NewConnectionTracker()
|
||||
defaultDialer := connrotation.NewDialerWithTracker(
|
||||
(&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
||||
connTracker,
|
||||
)
|
||||
|
||||
a := &Authenticator{
|
||||
cmd: config.Command,
|
||||
args: config.Args,
|
||||
group: gv,
|
||||
cmd: config.Command,
|
||||
args: config.Args,
|
||||
group: gv,
|
||||
cluster: cluster,
|
||||
provideClusterInfo: config.ProvideClusterInfo,
|
||||
|
||||
installHint: config.InstallHint,
|
||||
sometimes: &sometimes{
|
||||
threshold: 10,
|
||||
interval: time.Hour,
|
||||
clock: clock.RealClock{},
|
||||
},
|
||||
|
||||
stdin: os.Stdin,
|
||||
stderr: os.Stderr,
|
||||
interactive: terminal.IsTerminal(int(os.Stdout.Fd())),
|
||||
interactive: term.IsTerminal(int(os.Stdin.Fd())),
|
||||
now: time.Now,
|
||||
environ: os.Environ,
|
||||
|
||||
defaultDialer: defaultDialer,
|
||||
connTracker: connTracker,
|
||||
}
|
||||
|
||||
for _, env := range config.Env {
|
||||
@@ -147,10 +217,18 @@ func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error)
|
||||
// The plugin input and output are defined by the API group client.authentication.k8s.io.
|
||||
type Authenticator struct {
|
||||
// Set by the config
|
||||
cmd string
|
||||
args []string
|
||||
group schema.GroupVersion
|
||||
env []string
|
||||
cmd string
|
||||
args []string
|
||||
group schema.GroupVersion
|
||||
env []string
|
||||
cluster *clientauthentication.Cluster
|
||||
provideClusterInfo bool
|
||||
|
||||
// Used to avoid log spew by rate limiting install hint printing. We didn't do
|
||||
// this by interval based rate limiting alone since that way may have prevented
|
||||
// the install hint from showing up for kubectl users.
|
||||
sometimes *sometimes
|
||||
installHint string
|
||||
|
||||
// Stubbable for testing
|
||||
stdin io.Reader
|
||||
@@ -159,6 +237,11 @@ type Authenticator struct {
|
||||
now func() time.Time
|
||||
environ func() []string
|
||||
|
||||
// defaultDialer is used for clients which don't specify a custom dialer
|
||||
defaultDialer *connrotation.Dialer
|
||||
// connTracker tracks all connections opened that we need to close when rotating a client certificate
|
||||
connTracker *connrotation.ConnectionTracker
|
||||
|
||||
// Cached results.
|
||||
//
|
||||
// The mutex also guards calling the plugin. Since the plugin could be
|
||||
@@ -166,18 +249,25 @@ type Authenticator struct {
|
||||
mu sync.Mutex
|
||||
cachedCreds *credentials
|
||||
exp time.Time
|
||||
|
||||
onRotateList []func()
|
||||
}
|
||||
|
||||
type credentials struct {
|
||||
token string
|
||||
cert *tls.Certificate
|
||||
token string `datapolicy:"token"`
|
||||
cert *tls.Certificate `datapolicy:"secret-key"`
|
||||
}
|
||||
|
||||
// UpdateTransportConfig updates the transport.Config to use credentials
|
||||
// returned by the plugin.
|
||||
func (a *Authenticator) UpdateTransportConfig(c *transport.Config) error {
|
||||
// If a bearer token is present in the request - avoid the GetCert callback when
|
||||
// setting up the transport, as that triggers the exec action if the server is
|
||||
// also configured to allow client certificates for authentication. For requests
|
||||
// like "kubectl get --token (token) pods" we should assume the intention is to
|
||||
// use the provided token for authentication.
|
||||
if c.HasTokenAuth() {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||
return &roundTripper{a, rt}
|
||||
})
|
||||
@@ -187,20 +277,12 @@ func (a *Authenticator) UpdateTransportConfig(c *transport.Config) error {
|
||||
}
|
||||
c.TLS.GetCert = a.cert
|
||||
|
||||
var dial func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
var d *connrotation.Dialer
|
||||
if c.Dial != nil {
|
||||
dial = c.Dial
|
||||
// if c has a custom dialer, we have to wrap it
|
||||
d = connrotation.NewDialerWithTracker(c.Dial, a.connTracker)
|
||||
} else {
|
||||
dial = (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext
|
||||
}
|
||||
d := connrotation.NewDialer(dial)
|
||||
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.onRotateList = append(a.onRotateList, d.CloseAll)
|
||||
onRotateListLength := len(a.onRotateList)
|
||||
if onRotateListLength > onRotateListWarningLength {
|
||||
klog.Warningf("constructing many client instances from the same exec auth config can cause performance problems during cert rotation and can exhaust available network connections; %d clients constructed calling %q", onRotateListLength, a.cmd)
|
||||
d = a.defaultDialer
|
||||
}
|
||||
|
||||
c.Dial = d.DialContext
|
||||
@@ -299,19 +381,16 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err
|
||||
Interactive: a.interactive,
|
||||
},
|
||||
}
|
||||
if a.provideClusterInfo {
|
||||
cred.Spec.Cluster = a.cluster
|
||||
}
|
||||
|
||||
env := append(a.environ(), a.env...)
|
||||
if a.group == v1alpha1.SchemeGroupVersion {
|
||||
// Input spec disabled for beta due to lack of use. Possibly re-enable this later if
|
||||
// someone wants it back.
|
||||
//
|
||||
// See: https://github.com/kubernetes/kubernetes/issues/61796
|
||||
data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode ExecCredentials: %v", err)
|
||||
}
|
||||
env = append(env, fmt.Sprintf("%s=%s", execInfoEnv, data))
|
||||
data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode ExecCredentials: %v", err)
|
||||
}
|
||||
env = append(env, fmt.Sprintf("%s=%s", execInfoEnv, data))
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
cmd := exec.Command(a.cmd, a.args...)
|
||||
@@ -322,8 +401,10 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err
|
||||
cmd.Stdin = a.stdin
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("exec: %v", err)
|
||||
err = cmd.Run()
|
||||
incrementCallsMetric(err)
|
||||
if err != nil {
|
||||
return a.wrapCmdRunErrorLocked(err)
|
||||
}
|
||||
|
||||
_, gvk, err := codecs.UniversalDecoder(a.group).Decode(stdout.Bytes(), nil, cred)
|
||||
@@ -382,9 +463,7 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err
|
||||
if oldCreds.cert != nil && oldCreds.cert.Leaf != nil {
|
||||
metrics.ClientCertRotationAge.Observe(time.Now().Sub(oldCreds.cert.Leaf.NotBefore))
|
||||
}
|
||||
for _, onRotate := range a.onRotateList {
|
||||
onRotate()
|
||||
}
|
||||
a.connTracker.CloseAll()
|
||||
}
|
||||
|
||||
expiry := time.Time{}
|
||||
@@ -394,3 +473,35 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err
|
||||
expirationMetrics.set(a, expiry)
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapCmdRunErrorLocked pulls out the code to construct a helpful error message
|
||||
// for when the exec plugin's binary fails to Run().
|
||||
//
|
||||
// It must be called while holding the Authenticator's mutex.
|
||||
func (a *Authenticator) wrapCmdRunErrorLocked(err error) error {
|
||||
switch err.(type) {
|
||||
case *exec.Error: // Binary does not exist (see exec.Error).
|
||||
builder := strings.Builder{}
|
||||
fmt.Fprintf(&builder, "exec: executable %s not found", a.cmd)
|
||||
|
||||
a.sometimes.Do(func() {
|
||||
fmt.Fprint(&builder, installHintVerboseHelp)
|
||||
if a.installHint != "" {
|
||||
fmt.Fprintf(&builder, "\n\n%s", a.installHint)
|
||||
}
|
||||
})
|
||||
|
||||
return errors.New(builder.String())
|
||||
|
||||
case *exec.ExitError: // Binary execution failed (see exec.Cmd.Run()).
|
||||
e := err.(*exec.ExitError)
|
||||
return fmt.Errorf(
|
||||
"exec: executable %s failed with exit code %d",
|
||||
a.cmd,
|
||||
e.ProcessState.ExitCode(),
|
||||
)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("exec: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
49
vendor/k8s.io/client-go/plugin/pkg/client/auth/exec/metrics.go
generated
vendored
49
vendor/k8s.io/client-go/plugin/pkg/client/auth/exec/metrics.go
generated
vendored
@@ -17,12 +17,39 @@ limitations under the License.
|
||||
package exec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/client-go/tools/metrics"
|
||||
)
|
||||
|
||||
// The following constants shadow the special values used in the prometheus metrics implementation.
|
||||
const (
|
||||
// noError indicates that the plugin process was successfully started and exited with an exit
|
||||
// code of 0.
|
||||
noError = "no_error"
|
||||
// pluginExecutionError indicates that the plugin process was successfully started and then
|
||||
// it returned a non-zero exit code.
|
||||
pluginExecutionError = "plugin_execution_error"
|
||||
// pluginNotFoundError indicates that we could not find the exec plugin.
|
||||
pluginNotFoundError = "plugin_not_found_error"
|
||||
// clientInternalError indicates that we attempted to start the plugin process, but failed
|
||||
// for some reason.
|
||||
clientInternalError = "client_internal_error"
|
||||
|
||||
// successExitCode represents an exec plugin invocation that was successful.
|
||||
successExitCode = 0
|
||||
// failureExitCode represents an exec plugin invocation that was not successful. This code is
|
||||
// used in some failure modes (e.g., plugin not found, client internal error) so that someone
|
||||
// can more easily monitor all unsuccessful invocations.
|
||||
failureExitCode = 1
|
||||
)
|
||||
|
||||
type certificateExpirationTracker struct {
|
||||
mu sync.RWMutex
|
||||
m map[*Authenticator]time.Time
|
||||
@@ -58,3 +85,25 @@ func (c *certificateExpirationTracker) set(a *Authenticator, t time.Time) {
|
||||
c.metricSet(&earliest)
|
||||
}
|
||||
}
|
||||
|
||||
// incrementCallsMetric increments a global metrics counter for the number of calls to an exec
|
||||
// plugin, partitioned by exit code. The provided err should be the return value from
|
||||
// exec.Cmd.Run().
|
||||
func incrementCallsMetric(err error) {
|
||||
execExitError := &exec.ExitError{}
|
||||
execError := &exec.Error{}
|
||||
switch {
|
||||
case err == nil: // Binary execution succeeded.
|
||||
metrics.ExecPluginCalls.Increment(successExitCode, noError)
|
||||
|
||||
case errors.As(err, &execExitError): // Binary execution failed (see "os/exec".Cmd.Run()).
|
||||
metrics.ExecPluginCalls.Increment(execExitError.ExitCode(), pluginExecutionError)
|
||||
|
||||
case errors.As(err, &execError): // Binary does not exist (see exec.Error).
|
||||
metrics.ExecPluginCalls.Increment(failureExitCode, pluginNotFoundError)
|
||||
|
||||
default: // We don't know about this error type.
|
||||
klog.V(2).InfoS("unexpected exec plugin return error type", "type", reflect.TypeOf(err).String(), "err", err)
|
||||
metrics.ExecPluginCalls.Increment(failureExitCode, clientInternalError)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user