exec credential provider: InteractiveMode support

The value here is that the exec plugin author can use the kubeconfig to assert
how standard input is treated with respect to the exec plugin, e.g.,
- an exec plugin author can ensure that kubectl fails if it cannot provide
  standard input to an exec plugin that needs it (Always)
- an exec plugin author can ensure that an client-go process will still call an
  exec plugin that prefers standard input even if standard input is not
  available (IfAvailable)

Signed-off-by: Andrew Keesler <akeesler@vmware.com>

Kubernetes-commit: cd83d89ac94c5b61fdd38840098e7223e5af0d34
This commit is contained in:
Andrew Keesler 2021-06-14 17:15:36 -04:00 committed by Kubernetes Publisher
parent 1bccfc8c60
commit 37ed584bed
19 changed files with 659 additions and 83 deletions

View File

@ -47,7 +47,7 @@ type ExecCredentialSpec struct {
Response *Response Response *Response
// Interactive is true when the transport detects the command is being called from an // Interactive is true when the transport detects the command is being called from an
// interactive prompt. // interactive prompt, i.e., when stdin has been passed to this exec plugin.
// +optional // +optional
Interactive bool Interactive bool

View File

@ -22,7 +22,7 @@ import (
) )
func Convert_clientauthentication_ExecCredentialSpec_To_v1beta1_ExecCredentialSpec(in *clientauthentication.ExecCredentialSpec, out *ExecCredentialSpec, s conversion.Scope) error { func Convert_clientauthentication_ExecCredentialSpec_To_v1beta1_ExecCredentialSpec(in *clientauthentication.ExecCredentialSpec, out *ExecCredentialSpec, s conversion.Scope) error {
// This conversion intentionally omits the Response and Interactive fields, which were only // This conversion intentionally omits the Response field, which were only
// supported in v1alpha1. // supported in v1alpha1.
return autoConvert_clientauthentication_ExecCredentialSpec_To_v1beta1_ExecCredentialSpec(in, out, s) return autoConvert_clientauthentication_ExecCredentialSpec_To_v1beta1_ExecCredentialSpec(in, out, s)
} }

View File

@ -46,6 +46,9 @@ type ExecCredentialSpec struct {
// ExecConfig.ProvideClusterInfo). // ExecConfig.ProvideClusterInfo).
// +optional // +optional
Cluster *Cluster `json:"cluster,omitempty"` Cluster *Cluster `json:"cluster,omitempty"`
// Interactive declares whether stdin has been passed to this exec plugin.
Interactive bool `json:"interactive"`
} }
// ExecCredentialStatus holds credentials for the transport to use. // ExecCredentialStatus holds credentials for the transport to use.

View File

@ -149,6 +149,7 @@ func autoConvert_v1beta1_ExecCredentialSpec_To_clientauthentication_ExecCredenti
} else { } else {
out.Cluster = nil out.Cluster = nil
} }
out.Interactive = in.Interactive
return nil return nil
} }
@ -159,7 +160,7 @@ func Convert_v1beta1_ExecCredentialSpec_To_clientauthentication_ExecCredentialSp
func autoConvert_clientauthentication_ExecCredentialSpec_To_v1beta1_ExecCredentialSpec(in *clientauthentication.ExecCredentialSpec, out *ExecCredentialSpec, s conversion.Scope) error { func autoConvert_clientauthentication_ExecCredentialSpec_To_v1beta1_ExecCredentialSpec(in *clientauthentication.ExecCredentialSpec, out *ExecCredentialSpec, s conversion.Scope) error {
// WARNING: in.Response requires manual conversion: does not exist in peer-type // WARNING: in.Response requires manual conversion: does not exist in peer-type
// WARNING: in.Interactive requires manual conversion: does not exist in peer-type out.Interactive = in.Interactive
if in.Cluster != nil { if in.Cluster != nil {
in, out := &in.Cluster, &out.Cluster in, out := &in.Cluster, &out.Cluster
*out = new(Cluster) *out = new(Cluster)

View File

@ -162,10 +162,10 @@ func (s *sometimes) Do(f func()) {
// GetAuthenticator returns an exec-based plugin for providing client credentials. // GetAuthenticator returns an exec-based plugin for providing client credentials.
func GetAuthenticator(config *api.ExecConfig, cluster *clientauthentication.Cluster) (*Authenticator, error) { func GetAuthenticator(config *api.ExecConfig, cluster *clientauthentication.Cluster) (*Authenticator, error) {
return newAuthenticator(globalCache, config, cluster) return newAuthenticator(globalCache, term.IsTerminal, config, cluster)
} }
func newAuthenticator(c *cache, config *api.ExecConfig, cluster *clientauthentication.Cluster) (*Authenticator, error) { func newAuthenticator(c *cache, isTerminalFunc func(int) bool, config *api.ExecConfig, cluster *clientauthentication.Cluster) (*Authenticator, error) {
key := cacheKey(config, cluster) key := cacheKey(config, cluster)
if a, ok := c.get(key); ok { if a, ok := c.get(key); ok {
return a, nil return a, nil
@ -196,11 +196,11 @@ func newAuthenticator(c *cache, config *api.ExecConfig, cluster *clientauthentic
clock: clock.RealClock{}, clock: clock.RealClock{},
}, },
stdin: os.Stdin, stdin: os.Stdin,
stderr: os.Stderr, stderr: os.Stderr,
interactive: term.IsTerminal(int(os.Stdin.Fd())), interactiveFunc: func() (bool, error) { return isInteractive(isTerminalFunc, config) },
now: time.Now, now: time.Now,
environ: os.Environ, environ: os.Environ,
defaultDialer: defaultDialer, defaultDialer: defaultDialer,
connTracker: connTracker, connTracker: connTracker,
@ -213,6 +213,33 @@ func newAuthenticator(c *cache, config *api.ExecConfig, cluster *clientauthentic
return c.put(key, a), nil return c.put(key, a), nil
} }
func isInteractive(isTerminalFunc func(int) bool, config *api.ExecConfig) (bool, error) {
var shouldBeInteractive bool
switch config.InteractiveMode {
case api.NeverExecInteractiveMode:
shouldBeInteractive = false
case api.IfAvailableExecInteractiveMode:
shouldBeInteractive = !config.StdinUnavailable && isTerminalFunc(int(os.Stdin.Fd()))
case api.AlwaysExecInteractiveMode:
if !isTerminalFunc(int(os.Stdin.Fd())) {
return false, errors.New("standard input is not a terminal")
}
if config.StdinUnavailable {
suffix := ""
if len(config.StdinUnavailableMessage) > 0 {
// only print extra ": <message>" if the user actually specified a message
suffix = fmt.Sprintf(": %s", config.StdinUnavailableMessage)
}
return false, fmt.Errorf("standard input is unavailable%s", suffix)
}
shouldBeInteractive = true
default:
return false, fmt.Errorf("unknown interactiveMode: %q", config.InteractiveMode)
}
return shouldBeInteractive, nil
}
// Authenticator is a client credential provider that rotates credentials by executing a plugin. // Authenticator is a client credential provider that rotates credentials by executing a plugin.
// The plugin input and output are defined by the API group client.authentication.k8s.io. // The plugin input and output are defined by the API group client.authentication.k8s.io.
type Authenticator struct { type Authenticator struct {
@ -231,11 +258,11 @@ type Authenticator struct {
installHint string installHint string
// Stubbable for testing // Stubbable for testing
stdin io.Reader stdin io.Reader
stderr io.Writer stderr io.Writer
interactive bool interactiveFunc func() (bool, error)
now func() time.Time now func() time.Time
environ func() []string environ func() []string
// defaultDialer is used for clients which don't specify a custom dialer // defaultDialer is used for clients which don't specify a custom dialer
defaultDialer *connrotation.Dialer defaultDialer *connrotation.Dialer
@ -376,10 +403,15 @@ func (a *Authenticator) maybeRefreshCreds(creds *credentials, r *clientauthentic
// refreshCredsLocked executes the plugin and reads the credentials from // refreshCredsLocked executes the plugin and reads the credentials from
// stdout. It must be called while holding the Authenticator's mutex. // stdout. It must be called while holding the Authenticator's mutex.
func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) error { func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) error {
interactive, err := a.interactiveFunc()
if err != nil {
return fmt.Errorf("exec plugin cannot support interactive mode: %w", err)
}
cred := &clientauthentication.ExecCredential{ cred := &clientauthentication.ExecCredential{
Spec: clientauthentication.ExecCredentialSpec{ Spec: clientauthentication.ExecCredentialSpec{
Response: r, Response: r,
Interactive: a.interactive, Interactive: interactive,
}, },
} }
if a.provideClusterInfo { if a.provideClusterInfo {
@ -398,7 +430,7 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err
cmd.Env = env cmd.Env = env
cmd.Stderr = a.stderr cmd.Stderr = a.stderr
cmd.Stdout = stdout cmd.Stdout = stdout
if a.interactive { if interactive {
cmd.Stdin = a.stdin cmd.Stdin = a.stdin
} }

View File

@ -244,12 +244,25 @@ func TestCacheKey(t *testing.T) {
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
} }
// c7 should be the same as c6, except c7 has stdin marked as unavailable
c7 := &api.ExecConfig{
Command: "foo-bar",
Args: []string{"1", "2"},
Env: []api.ExecEnvVar{
{Name: "3", Value: "4"},
{Name: "5", Value: "6"},
},
APIVersion: "client.authentication.k8s.io/v1alpha1",
StdinUnavailable: true,
}
key1 := cacheKey(c1, c1c) key1 := cacheKey(c1, c1c)
key2 := cacheKey(c2, c2c) key2 := cacheKey(c2, c2c)
key3 := cacheKey(c3, c3c) key3 := cacheKey(c3, c3c)
key4 := cacheKey(c4, c4c) key4 := cacheKey(c4, c4c)
key5 := cacheKey(c5, c5c) key5 := cacheKey(c5, c5c)
key6 := cacheKey(c6, nil) key6 := cacheKey(c6, nil)
key7 := cacheKey(c7, nil)
if key1 != key2 { if key1 != key2 {
t.Error("key1 and key2 didn't match") t.Error("key1 and key2 didn't match")
} }
@ -268,6 +281,9 @@ func TestCacheKey(t *testing.T) {
if key6 == key4 { if key6 == key4 {
t.Error("key6 and key4 matched") t.Error("key6 and key4 matched")
} }
if key6 == key7 {
t.Error("key6 and key7 matched")
}
} }
func compJSON(t *testing.T, got, want []byte) { func compJSON(t *testing.T, got, want []byte) {
@ -290,23 +306,25 @@ func compJSON(t *testing.T, got, want []byte) {
func TestRefreshCreds(t *testing.T) { func TestRefreshCreds(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
config api.ExecConfig config api.ExecConfig
exitCode int stdinUnavailable bool
cluster *clientauthentication.Cluster exitCode int
output string cluster *clientauthentication.Cluster
interactive bool output string
response *clientauthentication.Response isTerminal bool
wantInput string response *clientauthentication.Response
wantCreds credentials wantInput string
wantExpiry time.Time wantCreds credentials
wantErr bool wantExpiry time.Time
wantErrSubstr string wantErr bool
wantErrSubstr string
}{ }{
{ {
name: "basic-request", name: "basic-request",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
wantInput: `{ wantInput: `{
"kind":"ExecCredential", "kind":"ExecCredential",
@ -325,9 +343,10 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "interactive", name: "interactive",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
interactive: true, isTerminal: true,
wantInput: `{ wantInput: `{
"kind":"ExecCredential", "kind":"ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1alpha1", "apiVersion":"client.authentication.k8s.io/v1alpha1",
@ -347,7 +366,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "response", name: "response",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
response: &clientauthentication.Response{ response: &clientauthentication.Response{
Header: map[string][]string{ Header: map[string][]string{
@ -381,7 +401,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "expiry", name: "expiry",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
wantInput: `{ wantInput: `{
"kind":"ExecCredential", "kind":"ExecCredential",
@ -402,7 +423,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "no-group-version", name: "no-group-version",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
wantInput: `{ wantInput: `{
"kind":"ExecCredential", "kind":"ExecCredential",
@ -420,7 +442,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "no-status", name: "no-status",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
wantInput: `{ wantInput: `{
"kind":"ExecCredential", "kind":"ExecCredential",
@ -436,7 +459,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "no-creds", name: "no-creds",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
wantInput: `{ wantInput: `{
"kind":"ExecCredential", "kind":"ExecCredential",
@ -453,7 +477,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "TLS credentials", name: "TLS credentials",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
wantInput: `{ wantInput: `{
"kind":"ExecCredential", "kind":"ExecCredential",
@ -473,7 +498,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "bad TLS credentials", name: "bad TLS credentials",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
wantInput: `{ wantInput: `{
"kind":"ExecCredential", "kind":"ExecCredential",
@ -493,7 +519,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "cert but no key", name: "cert but no key",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
wantInput: `{ wantInput: `{
"kind":"ExecCredential", "kind":"ExecCredential",
@ -512,12 +539,207 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "beta-basic-request", name: "beta-basic-request",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
wantInput: `{ wantInput: `{
"kind": "ExecCredential", "kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1", "apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {} "spec": {
"interactive": false
}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"status": {
"token": "foo-bar"
}
}`,
wantCreds: credentials{token: "foo-bar"},
},
{
name: "beta-basic-request-with-never-interactive-mode",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.NeverExecInteractiveMode,
},
wantInput: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {
"interactive": false
}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"status": {
"token": "foo-bar"
}
}`,
wantCreds: credentials{token: "foo-bar"},
},
{
name: "beta-basic-request-with-never-interactive-mode-and-stdin-unavailable",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.NeverExecInteractiveMode,
StdinUnavailable: true,
},
wantInput: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {
"interactive": false
}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"status": {
"token": "foo-bar"
}
}`,
wantCreds: credentials{token: "foo-bar"},
},
{
name: "beta-basic-request-with-if-available-interactive-mode",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
},
wantInput: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {
"interactive": false
}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"status": {
"token": "foo-bar"
}
}`,
wantCreds: credentials{token: "foo-bar"},
},
{
name: "beta-basic-request-with-if-available-interactive-mode-and-stdin-unavailable",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
StdinUnavailable: true,
},
wantInput: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {
"interactive": false
}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"status": {
"token": "foo-bar"
}
}`,
wantCreds: credentials{token: "foo-bar"},
},
{
name: "beta-basic-request-with-if-available-interactive-mode-and-terminal",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
},
isTerminal: true,
wantInput: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {
"interactive": true
}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"status": {
"token": "foo-bar"
}
}`,
wantCreds: credentials{token: "foo-bar"},
},
{
name: "beta-basic-request-with-if-available-interactive-mode-and-terminal-and-stdin-unavailable",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
StdinUnavailable: true,
},
isTerminal: true,
wantInput: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {
"interactive": false
}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"status": {
"token": "foo-bar"
}
}`,
wantCreds: credentials{token: "foo-bar"},
},
{
name: "beta-basic-request-with-always-interactive-mode",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.AlwaysExecInteractiveMode,
},
wantErr: true,
wantErrSubstr: "exec plugin cannot support interactive mode: standard input is not a terminal",
},
{
name: "beta-basic-request-with-always-interactive-mode-and-terminal-and-stdin-unavailable",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.AlwaysExecInteractiveMode,
StdinUnavailable: true,
},
isTerminal: true,
wantErr: true,
wantErrSubstr: "exec plugin cannot support interactive mode: standard input is unavailable",
},
{
name: "beta-basic-request-with-always-interactive-mode-and-terminal-and-stdin-unavailable-with-message",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.AlwaysExecInteractiveMode,
StdinUnavailable: true,
StdinUnavailableMessage: "some message",
},
isTerminal: true,
wantErr: true,
wantErrSubstr: "exec plugin cannot support interactive mode: standard input is unavailable: some message",
},
{
name: "beta-basic-request-with-always-interactive-mode-and-terminal",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.AlwaysExecInteractiveMode,
},
isTerminal: true,
wantInput: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {
"interactive": true
}
}`, }`,
output: `{ output: `{
"kind": "ExecCredential", "kind": "ExecCredential",
@ -531,12 +753,15 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "beta-expiry", name: "beta-expiry",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
wantInput: `{ wantInput: `{
"kind": "ExecCredential", "kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1", "apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {} "spec": {
"interactive": false
}
}`, }`,
output: `{ output: `{
"kind": "ExecCredential", "kind": "ExecCredential",
@ -552,7 +777,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "beta-no-group-version", name: "beta-no-group-version",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
output: `{ output: `{
"kind": "ExecCredential", "kind": "ExecCredential",
@ -565,7 +791,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "beta-no-status", name: "beta-no-status",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
output: `{ output: `{
"kind": "ExecCredential", "kind": "ExecCredential",
@ -576,7 +803,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "beta-no-token", name: "beta-no-token",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
output: `{ output: `{
"kind": "ExecCredential", "kind": "ExecCredential",
@ -588,9 +816,10 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "unknown-binary", name: "unknown-binary",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
Command: "does not exist", Command: "does not exist",
InstallHint: "some install hint", InstallHint: "some install hint",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
wantErr: true, wantErr: true,
wantErrSubstr: "some install hint", wantErrSubstr: "some install hint",
@ -598,7 +827,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "binary-fails", name: "binary-fails",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
exitCode: 73, exitCode: 73,
wantErr: true, wantErr: true,
@ -607,7 +837,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "alpha-with-cluster-is-ignored", name: "alpha-with-cluster-is-ignored",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
cluster: &clientauthentication.Cluster{ cluster: &clientauthentication.Cluster{
Server: "foo", Server: "foo",
@ -657,6 +888,7 @@ func TestRefreshCreds(t *testing.T) {
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
ProvideClusterInfo: true, ProvideClusterInfo: true,
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
cluster: &clientauthentication.Cluster{ cluster: &clientauthentication.Cluster{
Server: "foo", Server: "foo",
@ -693,7 +925,8 @@ func TestRefreshCreds(t *testing.T) {
"audience": "snorlax" "audience": "snorlax"
} }
} }
} },
"interactive": false
} }
}`, }`,
output: `{ output: `{
@ -708,7 +941,8 @@ func TestRefreshCreds(t *testing.T) {
{ {
name: "beta-with-cluster-and-without-provide-cluster-info-is-not-serialized", name: "beta-with-cluster-and-without-provide-cluster-info-is-not-serialized",
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, },
cluster: &clientauthentication.Cluster{ cluster: &clientauthentication.Cluster{
Server: "foo", Server: "foo",
@ -733,7 +967,9 @@ func TestRefreshCreds(t *testing.T) {
wantInput: `{ wantInput: `{
"kind":"ExecCredential", "kind":"ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1beta1", "apiVersion":"client.authentication.k8s.io/v1beta1",
"spec": {} "spec": {
"interactive": false
}
}`, }`,
output: `{ output: `{
"kind": "ExecCredential", "kind": "ExecCredential",
@ -762,14 +998,13 @@ func TestRefreshCreds(t *testing.T) {
}) })
} }
a, err := newAuthenticator(newCache(), &c, test.cluster) a, err := newAuthenticator(newCache(), func(_ int) bool { return test.isTerminal }, &c, test.cluster)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
stderr := &bytes.Buffer{} stderr := &bytes.Buffer{}
a.stderr = stderr a.stderr = stderr
a.interactive = test.interactive
a.environ = func() []string { return nil } a.environ = func() []string { return nil }
if err := a.refreshCredsLocked(test.response); err != nil { if err := a.refreshCredsLocked(test.response); err != nil {
@ -837,10 +1072,11 @@ func TestRoundTripper(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(handler)) server := httptest.NewServer(http.HandlerFunc(handler))
c := api.ExecConfig{ c := api.ExecConfig{
Command: "./testdata/test-plugin.sh", Command: "./testdata/test-plugin.sh",
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
} }
a, err := newAuthenticator(newCache(), &c, nil) a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -943,7 +1179,7 @@ func TestAuthorizationHeaderPresentCancelsExecAction(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
a, err := newAuthenticator(newCache(), &api.ExecConfig{ a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &api.ExecConfig{
Command: "./testdata/test-plugin.sh", Command: "./testdata/test-plugin.sh",
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
}, nil) }, nil)
@ -985,9 +1221,10 @@ func TestTLSCredentials(t *testing.T) {
server.StartTLS() server.StartTLS()
defer server.Close() defer server.Close()
a, err := newAuthenticator(newCache(), &api.ExecConfig{ a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &api.ExecConfig{
Command: "./testdata/test-plugin.sh", Command: "./testdata/test-plugin.sh",
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
}, nil) }, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -1078,7 +1315,7 @@ func TestConcurrentUpdateTransportConfig(t *testing.T) {
Command: "./testdata/test-plugin.sh", Command: "./testdata/test-plugin.sh",
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
} }
a, err := newAuthenticator(newCache(), &c, nil) a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -1141,11 +1378,12 @@ func TestInstallHintRateLimit(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
c := api.ExecConfig{ c := api.ExecConfig{
Command: "does not exist", Command: "does not exist",
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
InstallHint: "some install hint", InstallHint: "some install hint",
InteractiveMode: api.IfAvailableExecInteractiveMode,
} }
a, err := newAuthenticator(newCache(), &c, nil) a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -140,9 +140,10 @@ func TestCallsMetric(t *testing.T) {
{Name: "TEST_EXIT_CODE", Value: fmt.Sprintf("%d", exitCode)}, {Name: "TEST_EXIT_CODE", Value: fmt.Sprintf("%d", exitCode)},
{Name: "TEST_OUTPUT", Value: goodOutput}, {Name: "TEST_OUTPUT", Value: goodOutput},
}, },
InteractiveMode: api.IfAvailableExecInteractiveMode,
} }
a, err := newAuthenticator(newCache(), &c, nil) a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -171,10 +172,11 @@ func TestCallsMetric(t *testing.T) {
// metric values. // metric values.
refreshCreds := func(command string) { refreshCreds := func(command string) {
c := api.ExecConfig{ c := api.ExecConfig{
Command: "does not exist", Command: "does not exist",
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: api.IfAvailableExecInteractiveMode,
} }
a, err := newAuthenticator(newCache(), &c, nil) a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -626,7 +626,7 @@ func TestConfigSprint(t *testing.T) {
Proxy: fakeProxyFunc, Proxy: fakeProxyFunc,
} }
want := fmt.Sprintf( want := fmt.Sprintf(
`&rest.Config{Host:"localhost:8080", APIPath:"v1", ContentConfig:rest.ContentConfig{AcceptContentTypes:"application/json", ContentType:"application/json", GroupVersion:(*schema.GroupVersion)(nil), NegotiatedSerializer:runtime.NegotiatedSerializer(nil)}, Username:"gopher", Password:"--- REDACTED ---", BearerToken:"--- REDACTED ---", BearerTokenFile:"", Impersonate:rest.ImpersonationConfig{UserName:"gopher2", Groups:[]string(nil), Extra:map[string][]string(nil)}, AuthProvider:api.AuthProviderConfig{Name: "gopher", Config: map[string]string{--- REDACTED ---}}, AuthConfigPersister:rest.AuthProviderConfigPersister(--- REDACTED ---), ExecProvider:api.ExecConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: "", ProvideClusterInfo: true, Config: runtime.Object(--- REDACTED ---)}, TLSClientConfig:rest.sanitizedTLSClientConfig{Insecure:false, ServerName:"", CertFile:"a.crt", KeyFile:"a.key", CAFile:"", CertData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x54, 0x52, 0x55, 0x4e, 0x43, 0x41, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, KeyData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x52, 0x45, 0x44, 0x41, 0x43, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, CAData:[]uint8(nil), NextProtos:[]string{"h2", "http/1.1"}}, UserAgent:"gobot", DisableCompression:false, Transport:(*rest.fakeRoundTripper)(%p), WrapTransport:(transport.WrapperFunc)(%p), QPS:1, Burst:2, RateLimiter:(*rest.fakeLimiter)(%p), WarningHandler:rest.fakeWarningHandler{}, Timeout:3000000000, Dial:(func(context.Context, string, string) (net.Conn, error))(%p), Proxy:(func(*http.Request) (*url.URL, error))(%p)}`, `&rest.Config{Host:"localhost:8080", APIPath:"v1", ContentConfig:rest.ContentConfig{AcceptContentTypes:"application/json", ContentType:"application/json", GroupVersion:(*schema.GroupVersion)(nil), NegotiatedSerializer:runtime.NegotiatedSerializer(nil)}, Username:"gopher", Password:"--- REDACTED ---", BearerToken:"--- REDACTED ---", BearerTokenFile:"", Impersonate:rest.ImpersonationConfig{UserName:"gopher2", Groups:[]string(nil), Extra:map[string][]string(nil)}, AuthProvider:api.AuthProviderConfig{Name: "gopher", Config: map[string]string{--- REDACTED ---}}, AuthConfigPersister:rest.AuthProviderConfigPersister(--- REDACTED ---), ExecProvider:api.ExecConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: "", ProvideClusterInfo: true, Config: runtime.Object(--- REDACTED ---), StdinUnavailable: false}, TLSClientConfig:rest.sanitizedTLSClientConfig{Insecure:false, ServerName:"", CertFile:"a.crt", KeyFile:"a.key", CAFile:"", CertData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x54, 0x52, 0x55, 0x4e, 0x43, 0x41, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, KeyData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x52, 0x45, 0x44, 0x41, 0x43, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, CAData:[]uint8(nil), NextProtos:[]string{"h2", "http/1.1"}}, UserAgent:"gobot", DisableCompression:false, Transport:(*rest.fakeRoundTripper)(%p), WrapTransport:(transport.WrapperFunc)(%p), QPS:1, Burst:2, RateLimiter:(*rest.fakeLimiter)(%p), WarningHandler:rest.fakeWarningHandler{}, Timeout:3000000000, Dial:(func(context.Context, string, string) (net.Conn, error))(%p), Proxy:(func(*http.Request) (*url.URL, error))(%p)}`,
c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc, fakeProxyFunc, c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc, fakeProxyFunc,
) )

View File

@ -245,6 +245,33 @@ type ExecConfig struct {
// to be stored directly in the kubeconfig. // to be stored directly in the kubeconfig.
// +k8s:conversion-gen=false // +k8s:conversion-gen=false
Config runtime.Object Config runtime.Object
// InteractiveMode determines this plugin's relationship with standard input. Valid
// values are "Never" (this exec plugin never uses standard input), "IfAvailable" (this
// exec plugin wants to use standard input if it is available), or "Always" (this exec
// plugin requires standard input to function). See ExecInteractiveMode values for more
// details.
//
// If APIVersion is client.authentication.k8s.io/v1alpha1 or
// client.authentication.k8s.io/v1beta1, then this field is optional and defaults
// to "IfAvailable" when unset. Otherwise, this field is required.
// +optional
InteractiveMode ExecInteractiveMode
// StdinUnavailable indicates whether the exec authenticator can pass standard
// input through to this exec plugin. For example, a higher level entity might be using
// standard input for something else and therefore it would not be safe for the exec
// plugin to use standard input. This is kept here in order to keep all of the exec configuration
// together, but it is never serialized.
// +k8s:conversion-gen=false
StdinUnavailable bool
// StdinUnavailableMessage is an optional message to be displayed when the exec authenticator
// cannot successfully run this exec plugin because it needs to use standard input and
// StdinUnavailable is true. For example, a process that is already using standard input to
// read user instructions might set this to "used by my-program to read user instructions".
// +k8s:conversion-gen=false
StdinUnavailableMessage string
} }
var _ fmt.Stringer = new(ExecConfig) var _ fmt.Stringer = new(ExecConfig)
@ -271,7 +298,7 @@ func (c ExecConfig) String() string {
if c.Config != nil { if c.Config != nil {
config = "runtime.Object(--- REDACTED ---)" config = "runtime.Object(--- REDACTED ---)"
} }
return fmt.Sprintf("api.ExecConfig{Command: %q, Args: %#v, Env: %s, APIVersion: %q, ProvideClusterInfo: %t, Config: %s}", c.Command, args, env, c.APIVersion, c.ProvideClusterInfo, config) return fmt.Sprintf("api.ExecConfig{Command: %q, Args: %#v, Env: %s, APIVersion: %q, ProvideClusterInfo: %t, Config: %s, StdinUnavailable: %t}", c.Command, args, env, c.APIVersion, c.ProvideClusterInfo, config, c.StdinUnavailable)
} }
// ExecEnvVar is used for setting environment variables when executing an exec-based // ExecEnvVar is used for setting environment variables when executing an exec-based
@ -281,6 +308,26 @@ type ExecEnvVar struct {
Value string `json:"value"` Value string `json:"value"`
} }
// ExecInteractiveMode is a string that describes an exec plugin's relationship with standard input.
type ExecInteractiveMode string
const (
// NeverExecInteractiveMode declares that this exec plugin never needs to use standard
// input, and therefore the exec plugin will be run regardless of whether standard input is
// available for user input.
NeverExecInteractiveMode ExecInteractiveMode = "Never"
// IfAvailableExecInteractiveMode declares that this exec plugin would like to use standard input
// if it is available, but can still operate if standard input is not available. Therefore, the
// exec plugin will be run regardless of whether stdin is available for user input. If standard
// input is available for user input, then it will be provided to this exec plugin.
IfAvailableExecInteractiveMode ExecInteractiveMode = "IfAvailable"
// AlwaysExecInteractiveMode declares that this exec plugin requires standard input in order to
// run, and therefore the exec plugin will only be run if standard input is available for user
// input. If standard input is not available for user input, then the exec plugin will not be run
// and an error will be returned by the exec plugin runner.
AlwaysExecInteractiveMode ExecInteractiveMode = "Always"
)
// NewConfig is a convenience function that returns a new Config object with non-nil maps // NewConfig is a convenience function that returns a new Config object with non-nil maps
func NewConfig() *Config { func NewConfig() *Config {
return &Config{ return &Config{

View File

@ -0,0 +1,37 @@
/*
Copyright 2021 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 v1
import (
"k8s.io/apimachinery/pkg/runtime"
)
func addDefaultingFuncs(scheme *runtime.Scheme) error {
return RegisterDefaults(scheme)
}
func SetDefaults_ExecConfig(exec *ExecConfig) {
if len(exec.InteractiveMode) == 0 {
switch exec.APIVersion {
case "client.authentication.k8s.io/v1beta1", "client.authentication.k8s.io/v1alpha1":
// default to IfAvailableExecInteractiveMode for backwards compatibility
exec.InteractiveMode = IfAvailableExecInteractiveMode
default:
// require other versions to explicitly declare whether they want stdin or not
}
}
}

View File

@ -0,0 +1,84 @@
/*
Copyright 2021 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 v1
import (
"testing"
"github.com/google/go-cmp/cmp"
)
func TestSetDefaults_Config(t *testing.T) {
tests := []struct {
name string
in, wantOut *ExecConfig
}{
{
name: "alpha exec API with empty interactive mode",
in: &ExecConfig{APIVersion: "client.authentication.k8s.io/v1alpha1"},
wantOut: &ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: IfAvailableExecInteractiveMode,
},
},
{
name: "beta exec API with empty interactive mode",
in: &ExecConfig{APIVersion: "client.authentication.k8s.io/v1beta1"},
wantOut: &ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: IfAvailableExecInteractiveMode,
},
},
{
name: "alpha exec API with set interactive mode",
in: &ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: NeverExecInteractiveMode,
},
wantOut: &ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
InteractiveMode: NeverExecInteractiveMode,
},
},
{
name: "beta exec API with set interactive mode",
in: &ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: NeverExecInteractiveMode,
},
wantOut: &ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: NeverExecInteractiveMode,
},
},
{
name: "v1 exec API with empty interactive mode",
in: &ExecConfig{APIVersion: "client.authentication.k8s.io/v1"},
wantOut: &ExecConfig{APIVersion: "client.authentication.k8s.io/v1"},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
gotOut := test.in.DeepCopy()
SetDefaults_ExecConfig(gotOut)
if diff := cmp.Diff(test.wantOut, gotOut); diff != "" {
t.Errorf("unexpected defaulting; -want, +got:\n %s", diff)
}
})
}
}

View File

@ -16,5 +16,6 @@ limitations under the License.
// +k8s:conversion-gen=k8s.io/client-go/tools/clientcmd/api // +k8s:conversion-gen=k8s.io/client-go/tools/clientcmd/api
// +k8s:deepcopy-gen=package // +k8s:deepcopy-gen=package
// +k8s:defaulter-gen=Kind
package v1 package v1

View File

@ -37,7 +37,7 @@ func init() {
// We only register manually written functions here. The registration of the // We only register manually written functions here. The registration of the
// generated functions takes place in the generated files. The separation // generated functions takes place in the generated files. The separation
// makes the code compile even when the generated files are missing. // makes the code compile even when the generated files are missing.
localSchemeBuilder.Register(addKnownTypes) localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
} }
func addKnownTypes(scheme *runtime.Scheme) error { func addKnownTypes(scheme *runtime.Scheme) error {

View File

@ -221,6 +221,18 @@ type ExecConfig struct {
// to false. Package k8s.io/client-go/tools/auth/exec provides helper methods for // to false. Package k8s.io/client-go/tools/auth/exec provides helper methods for
// reading this environment variable. // reading this environment variable.
ProvideClusterInfo bool `json:"provideClusterInfo"` ProvideClusterInfo bool `json:"provideClusterInfo"`
// InteractiveMode determines this plugin's relationship with standard input. Valid
// values are "Never" (this exec plugin never uses standard input), "IfAvailable" (this
// exec plugin wants to use standard input if it is available), or "Always" (this exec
// plugin requires standard input to function). See ExecInteractiveMode values for more
// details.
//
// If APIVersion is client.authentication.k8s.io/v1alpha1 or
// client.authentication.k8s.io/v1beta1, then this field is optional and defaults
// to "IfAvailable" when unset. Otherwise, this field is required.
//+optional
InteractiveMode ExecInteractiveMode `json:"interactiveMode,omitempty"`
} }
// ExecEnvVar is used for setting environment variables when executing an exec-based // ExecEnvVar is used for setting environment variables when executing an exec-based
@ -229,3 +241,23 @@ type ExecEnvVar struct {
Name string `json:"name"` Name string `json:"name"`
Value string `json:"value"` Value string `json:"value"`
} }
// ExecInteractiveMode is a string that describes an exec plugin's relationship with standard input.
type ExecInteractiveMode string
const (
// NeverExecInteractiveMode declares that this exec plugin never needs to use standard
// input, and therefore the exec plugin will be run regardless of whether standard input is
// available for user input.
NeverExecInteractiveMode ExecInteractiveMode = "Never"
// IfAvailableExecInteractiveMode declares that this exec plugin would like to use standard input
// if it is available, but can still operate if standard input is not available. Therefore, the
// exec plugin will be run regardless of whether stdin is available for user input. If standard
// input is available for user input, then it will be provided to this exec plugin.
IfAvailableExecInteractiveMode ExecInteractiveMode = "IfAvailable"
// AlwaysExecInteractiveMode declares that this exec plugin requires standard input in order to
// run, and therefore the exec plugin will only be run if standard input is available for user
// input. If standard input is not available for user input, then the exec plugin will not be run
// and an error will be returned by the exec plugin runner.
AlwaysExecInteractiveMode ExecInteractiveMode = "Always"
)

View File

@ -376,6 +376,7 @@ func autoConvert_v1_ExecConfig_To_api_ExecConfig(in *ExecConfig, out *api.ExecCo
out.APIVersion = in.APIVersion out.APIVersion = in.APIVersion
out.InstallHint = in.InstallHint out.InstallHint = in.InstallHint
out.ProvideClusterInfo = in.ProvideClusterInfo out.ProvideClusterInfo = in.ProvideClusterInfo
out.InteractiveMode = api.ExecInteractiveMode(in.InteractiveMode)
return nil return nil
} }
@ -392,6 +393,9 @@ func autoConvert_api_ExecConfig_To_v1_ExecConfig(in *api.ExecConfig, out *ExecCo
out.InstallHint = in.InstallHint out.InstallHint = in.InstallHint
out.ProvideClusterInfo = in.ProvideClusterInfo out.ProvideClusterInfo = in.ProvideClusterInfo
// INFO: in.Config opted out of conversion generation // INFO: in.Config opted out of conversion generation
out.InteractiveMode = ExecInteractiveMode(in.InteractiveMode)
// INFO: in.StdinUnavailable opted out of conversion generation
// INFO: in.StdinUnavailableMessage opted out of conversion generation
return nil return nil
} }

View File

@ -0,0 +1,42 @@
// +build !ignore_autogenerated
/*
Copyright 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.
*/
// Code generated by defaulter-gen. DO NOT EDIT.
package v1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// RegisterDefaults adds defaulters functions to the given scheme.
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
scheme.AddTypeDefaultingFunc(&Config{}, func(obj interface{}) { SetObjectDefaults_Config(obj.(*Config)) })
return nil
}
func SetObjectDefaults_Config(in *Config) {
for i := range in.AuthInfos {
a := &in.AuthInfos[i]
if a.AuthInfo.Exec != nil {
SetDefaults_ExecConfig(a.AuthInfo.Exec)
}
}
}

View File

@ -630,9 +630,10 @@ func TestCreateAuthConfigExecInstallHintCleanup(t *testing.T) {
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{ clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{
AuthInfo: clientcmdapi.AuthInfo{ AuthInfo: clientcmdapi.AuthInfo{
Exec: &clientcmdapi.ExecConfig{ Exec: &clientcmdapi.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
Command: "some-command", Command: "some-command",
InstallHint: "some install hint with \x1b[1mcontrol chars\x1b[0m\nand a newline", InstallHint: "some install hint with \x1b[1mcontrol chars\x1b[0m\nand a newline",
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
}, },
}, },
}, nil) }, nil)

View File

@ -308,6 +308,14 @@ func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []err
validationErrors = append(validationErrors, fmt.Errorf("env variable name must be specified for %v to use exec authentication plugin", authInfoName)) validationErrors = append(validationErrors, fmt.Errorf("env variable name must be specified for %v to use exec authentication plugin", authInfoName))
} }
} }
switch authInfo.Exec.InteractiveMode {
case "":
validationErrors = append(validationErrors, fmt.Errorf("interactiveMode must be specified for %v to use exec authentication plugin", authInfoName))
case clientcmdapi.NeverExecInteractiveMode, clientcmdapi.IfAvailableExecInteractiveMode, clientcmdapi.AlwaysExecInteractiveMode:
// These are valid
default:
validationErrors = append(validationErrors, fmt.Errorf("invalid interactiveMode for %v: %q", authInfoName, authInfo.Exec.InteractiveMode))
}
} }
// authPath also provides information for the client to identify the server, so allow multiple auth methods in that case // authPath also provides information for the client to identify the server, so allow multiple auth methods in that case

View File

@ -377,6 +377,7 @@ func TestValidateAuthInfoExec(t *testing.T) {
Env: []clientcmdapi.ExecEnvVar{ Env: []clientcmdapi.ExecEnvVar{
{Name: "foo", Value: "bar"}, {Name: "foo", Value: "bar"},
}, },
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
}, },
} }
test := configValidationTest{ test := configValidationTest{
@ -391,7 +392,8 @@ func TestValidateAuthInfoExecNoVersion(t *testing.T) {
config := clientcmdapi.NewConfig() config := clientcmdapi.NewConfig()
config.AuthInfos["user"] = &clientcmdapi.AuthInfo{ config.AuthInfos["user"] = &clientcmdapi.AuthInfo{
Exec: &clientcmdapi.ExecConfig{ Exec: &clientcmdapi.ExecConfig{
Command: "/bin/example", Command: "/bin/example",
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
}, },
} }
test := configValidationTest{ test := configValidationTest{
@ -409,7 +411,8 @@ func TestValidateAuthInfoExecNoCommand(t *testing.T) {
config := clientcmdapi.NewConfig() config := clientcmdapi.NewConfig()
config.AuthInfos["user"] = &clientcmdapi.AuthInfo{ config.AuthInfos["user"] = &clientcmdapi.AuthInfo{
Exec: &clientcmdapi.ExecConfig{ Exec: &clientcmdapi.ExecConfig{
APIVersion: "clientauthentication.k8s.io/v1alpha1", APIVersion: "clientauthentication.k8s.io/v1alpha1",
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
}, },
} }
test := configValidationTest{ test := configValidationTest{
@ -430,8 +433,9 @@ func TestValidateAuthInfoExecWithAuthProvider(t *testing.T) {
Name: "oidc", Name: "oidc",
}, },
Exec: &clientcmdapi.ExecConfig{ Exec: &clientcmdapi.ExecConfig{
Command: "/bin/example", Command: "/bin/example",
APIVersion: "clientauthentication.k8s.io/v1alpha1", APIVersion: "clientauthentication.k8s.io/v1alpha1",
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
}, },
} }
test := configValidationTest{ test := configValidationTest{
@ -454,6 +458,7 @@ func TestValidateAuthInfoExecNoEnv(t *testing.T) {
Env: []clientcmdapi.ExecEnvVar{ Env: []clientcmdapi.ExecEnvVar{
{Name: "foo", Value: ""}, {Name: "foo", Value: ""},
}, },
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
}, },
} }
test := configValidationTest{ test := configValidationTest{
@ -464,6 +469,45 @@ func TestValidateAuthInfoExecNoEnv(t *testing.T) {
test.testConfig(t) test.testConfig(t)
} }
func TestValidateAuthInfoExecInteractiveModeMissing(t *testing.T) {
config := clientcmdapi.NewConfig()
config.AuthInfos["user"] = &clientcmdapi.AuthInfo{
Exec: &clientcmdapi.ExecConfig{
Command: "/bin/example",
APIVersion: "clientauthentication.k8s.io/v1alpha1",
},
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{
"interactiveMode must be specified for user to use exec authentication plugin",
},
}
test.testAuthInfo("user", t)
test.testConfig(t)
}
func TestValidateAuthInfoExecInteractiveModeInvalid(t *testing.T) {
config := clientcmdapi.NewConfig()
config.AuthInfos["user"] = &clientcmdapi.AuthInfo{
Exec: &clientcmdapi.ExecConfig{
Command: "/bin/example",
APIVersion: "clientauthentication.k8s.io/v1alpha1",
InteractiveMode: "invalid",
},
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{
`invalid interactiveMode for user: "invalid"`,
},
}
test.testAuthInfo("user", t)
test.testConfig(t)
}
type configValidationTest struct { type configValidationTest struct {
config *clientcmdapi.Config config *clientcmdapi.Config
expectedErrorSubstring []string expectedErrorSubstring []string