mirror of
https://github.com/kubernetes/client-go.git
synced 2025-06-25 06:31:35 +00:00
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:
parent
1bccfc8c60
commit
37ed584bed
@ -47,7 +47,7 @@ type ExecCredentialSpec struct {
|
||||
Response *Response
|
||||
|
||||
// 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
|
||||
Interactive bool
|
||||
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
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.
|
||||
return autoConvert_clientauthentication_ExecCredentialSpec_To_v1beta1_ExecCredentialSpec(in, out, s)
|
||||
}
|
||||
|
@ -46,6 +46,9 @@ type ExecCredentialSpec struct {
|
||||
// ExecConfig.ProvideClusterInfo).
|
||||
// +optional
|
||||
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.
|
||||
|
@ -149,6 +149,7 @@ func autoConvert_v1beta1_ExecCredentialSpec_To_clientauthentication_ExecCredenti
|
||||
} else {
|
||||
out.Cluster = nil
|
||||
}
|
||||
out.Interactive = in.Interactive
|
||||
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 {
|
||||
// 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 {
|
||||
in, out := &in.Cluster, &out.Cluster
|
||||
*out = new(Cluster)
|
||||
|
@ -162,10 +162,10 @@ func (s *sometimes) Do(f func()) {
|
||||
|
||||
// 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)
|
||||
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)
|
||||
if a, ok := c.get(key); ok {
|
||||
return a, nil
|
||||
@ -196,11 +196,11 @@ func newAuthenticator(c *cache, config *api.ExecConfig, cluster *clientauthentic
|
||||
clock: clock.RealClock{},
|
||||
},
|
||||
|
||||
stdin: os.Stdin,
|
||||
stderr: os.Stderr,
|
||||
interactive: term.IsTerminal(int(os.Stdin.Fd())),
|
||||
now: time.Now,
|
||||
environ: os.Environ,
|
||||
stdin: os.Stdin,
|
||||
stderr: os.Stderr,
|
||||
interactiveFunc: func() (bool, error) { return isInteractive(isTerminalFunc, config) },
|
||||
now: time.Now,
|
||||
environ: os.Environ,
|
||||
|
||||
defaultDialer: defaultDialer,
|
||||
connTracker: connTracker,
|
||||
@ -213,6 +213,33 @@ func newAuthenticator(c *cache, config *api.ExecConfig, cluster *clientauthentic
|
||||
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.
|
||||
// The plugin input and output are defined by the API group client.authentication.k8s.io.
|
||||
type Authenticator struct {
|
||||
@ -231,11 +258,11 @@ type Authenticator struct {
|
||||
installHint string
|
||||
|
||||
// Stubbable for testing
|
||||
stdin io.Reader
|
||||
stderr io.Writer
|
||||
interactive bool
|
||||
now func() time.Time
|
||||
environ func() []string
|
||||
stdin io.Reader
|
||||
stderr io.Writer
|
||||
interactiveFunc func() (bool, error)
|
||||
now func() time.Time
|
||||
environ func() []string
|
||||
|
||||
// defaultDialer is used for clients which don't specify a custom 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
|
||||
// stdout. It must be called while holding the Authenticator's mutex.
|
||||
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{
|
||||
Spec: clientauthentication.ExecCredentialSpec{
|
||||
Response: r,
|
||||
Interactive: a.interactive,
|
||||
Interactive: interactive,
|
||||
},
|
||||
}
|
||||
if a.provideClusterInfo {
|
||||
@ -398,7 +430,7 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err
|
||||
cmd.Env = env
|
||||
cmd.Stderr = a.stderr
|
||||
cmd.Stdout = stdout
|
||||
if a.interactive {
|
||||
if interactive {
|
||||
cmd.Stdin = a.stdin
|
||||
}
|
||||
|
||||
|
@ -244,12 +244,25 @@ func TestCacheKey(t *testing.T) {
|
||||
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)
|
||||
key2 := cacheKey(c2, c2c)
|
||||
key3 := cacheKey(c3, c3c)
|
||||
key4 := cacheKey(c4, c4c)
|
||||
key5 := cacheKey(c5, c5c)
|
||||
key6 := cacheKey(c6, nil)
|
||||
key7 := cacheKey(c7, nil)
|
||||
if key1 != key2 {
|
||||
t.Error("key1 and key2 didn't match")
|
||||
}
|
||||
@ -268,6 +281,9 @@ func TestCacheKey(t *testing.T) {
|
||||
if key6 == key4 {
|
||||
t.Error("key6 and key4 matched")
|
||||
}
|
||||
if key6 == key7 {
|
||||
t.Error("key6 and key7 matched")
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config api.ExecConfig
|
||||
exitCode int
|
||||
cluster *clientauthentication.Cluster
|
||||
output string
|
||||
interactive bool
|
||||
response *clientauthentication.Response
|
||||
wantInput string
|
||||
wantCreds credentials
|
||||
wantExpiry time.Time
|
||||
wantErr bool
|
||||
wantErrSubstr string
|
||||
name string
|
||||
config api.ExecConfig
|
||||
stdinUnavailable bool
|
||||
exitCode int
|
||||
cluster *clientauthentication.Cluster
|
||||
output string
|
||||
isTerminal bool
|
||||
response *clientauthentication.Response
|
||||
wantInput string
|
||||
wantCreds credentials
|
||||
wantExpiry time.Time
|
||||
wantErr bool
|
||||
wantErrSubstr string
|
||||
}{
|
||||
{
|
||||
name: "basic-request",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
wantInput: `{
|
||||
"kind":"ExecCredential",
|
||||
@ -325,9 +343,10 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "interactive",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
interactive: true,
|
||||
isTerminal: true,
|
||||
wantInput: `{
|
||||
"kind":"ExecCredential",
|
||||
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
||||
@ -347,7 +366,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "response",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
response: &clientauthentication.Response{
|
||||
Header: map[string][]string{
|
||||
@ -381,7 +401,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "expiry",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
wantInput: `{
|
||||
"kind":"ExecCredential",
|
||||
@ -402,7 +423,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "no-group-version",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
wantInput: `{
|
||||
"kind":"ExecCredential",
|
||||
@ -420,7 +442,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "no-status",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
wantInput: `{
|
||||
"kind":"ExecCredential",
|
||||
@ -436,7 +459,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "no-creds",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
wantInput: `{
|
||||
"kind":"ExecCredential",
|
||||
@ -453,7 +477,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "TLS credentials",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
wantInput: `{
|
||||
"kind":"ExecCredential",
|
||||
@ -473,7 +498,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "bad TLS credentials",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
wantInput: `{
|
||||
"kind":"ExecCredential",
|
||||
@ -493,7 +519,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "cert but no key",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
wantInput: `{
|
||||
"kind":"ExecCredential",
|
||||
@ -512,12 +539,207 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "beta-basic-request",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
wantInput: `{
|
||||
"kind": "ExecCredential",
|
||||
"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: `{
|
||||
"kind": "ExecCredential",
|
||||
@ -531,12 +753,15 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "beta-expiry",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
wantInput: `{
|
||||
"kind": "ExecCredential",
|
||||
"apiVersion": "client.authentication.k8s.io/v1beta1",
|
||||
"spec": {}
|
||||
"spec": {
|
||||
"interactive": false
|
||||
}
|
||||
}`,
|
||||
output: `{
|
||||
"kind": "ExecCredential",
|
||||
@ -552,7 +777,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "beta-no-group-version",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
output: `{
|
||||
"kind": "ExecCredential",
|
||||
@ -565,7 +791,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "beta-no-status",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
output: `{
|
||||
"kind": "ExecCredential",
|
||||
@ -576,7 +803,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "beta-no-token",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
output: `{
|
||||
"kind": "ExecCredential",
|
||||
@ -588,9 +816,10 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "unknown-binary",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
Command: "does not exist",
|
||||
InstallHint: "some install hint",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
Command: "does not exist",
|
||||
InstallHint: "some install hint",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrSubstr: "some install hint",
|
||||
@ -598,7 +827,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "binary-fails",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
exitCode: 73,
|
||||
wantErr: true,
|
||||
@ -607,7 +837,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "alpha-with-cluster-is-ignored",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
cluster: &clientauthentication.Cluster{
|
||||
Server: "foo",
|
||||
@ -657,6 +888,7 @@ func TestRefreshCreds(t *testing.T) {
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
ProvideClusterInfo: true,
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
cluster: &clientauthentication.Cluster{
|
||||
Server: "foo",
|
||||
@ -693,7 +925,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
"audience": "snorlax"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"interactive": false
|
||||
}
|
||||
}`,
|
||||
output: `{
|
||||
@ -708,7 +941,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
{
|
||||
name: "beta-with-cluster-and-without-provide-cluster-info-is-not-serialized",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
cluster: &clientauthentication.Cluster{
|
||||
Server: "foo",
|
||||
@ -733,7 +967,9 @@ func TestRefreshCreds(t *testing.T) {
|
||||
wantInput: `{
|
||||
"kind":"ExecCredential",
|
||||
"apiVersion":"client.authentication.k8s.io/v1beta1",
|
||||
"spec": {}
|
||||
"spec": {
|
||||
"interactive": false
|
||||
}
|
||||
}`,
|
||||
output: `{
|
||||
"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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stderr := &bytes.Buffer{}
|
||||
a.stderr = stderr
|
||||
a.interactive = test.interactive
|
||||
a.environ = func() []string { return nil }
|
||||
|
||||
if err := a.refreshCredsLocked(test.response); err != nil {
|
||||
@ -837,10 +1072,11 @@ func TestRoundTripper(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(handler))
|
||||
|
||||
c := api.ExecConfig{
|
||||
Command: "./testdata/test-plugin.sh",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
Command: "./testdata/test-plugin.sh",
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -943,7 +1179,7 @@ func TestAuthorizationHeaderPresentCancelsExecAction(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
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",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
}, nil)
|
||||
@ -985,9 +1221,10 @@ func TestTLSCredentials(t *testing.T) {
|
||||
server.StartTLS()
|
||||
defer server.Close()
|
||||
|
||||
a, err := newAuthenticator(newCache(), &api.ExecConfig{
|
||||
Command: "./testdata/test-plugin.sh",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &api.ExecConfig{
|
||||
Command: "./testdata/test-plugin.sh",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: api.IfAvailableExecInteractiveMode,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -1078,7 +1315,7 @@ func TestConcurrentUpdateTransportConfig(t *testing.T) {
|
||||
Command: "./testdata/test-plugin.sh",
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -1141,11 +1378,12 @@ func TestInstallHintRateLimit(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := api.ExecConfig{
|
||||
Command: "does not exist",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InstallHint: "some install hint",
|
||||
Command: "does not exist",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -140,9 +140,10 @@ func TestCallsMetric(t *testing.T) {
|
||||
{Name: "TEST_EXIT_CODE", Value: fmt.Sprintf("%d", exitCode)},
|
||||
{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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -171,10 +172,11 @@ func TestCallsMetric(t *testing.T) {
|
||||
// metric values.
|
||||
refreshCreds := func(command string) {
|
||||
c := api.ExecConfig{
|
||||
Command: "does not exist",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
Command: "does not exist",
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -626,7 +626,7 @@ func TestConfigSprint(t *testing.T) {
|
||||
Proxy: fakeProxyFunc,
|
||||
}
|
||||
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,
|
||||
)
|
||||
|
||||
|
@ -245,6 +245,33 @@ type ExecConfig struct {
|
||||
// to be stored directly in the kubeconfig.
|
||||
// +k8s:conversion-gen=false
|
||||
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)
|
||||
@ -271,7 +298,7 @@ func (c ExecConfig) String() string {
|
||||
if c.Config != nil {
|
||||
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
|
||||
@ -281,6 +308,26 @@ type ExecEnvVar struct {
|
||||
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
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
|
37
tools/clientcmd/api/v1/defaults.go
Normal file
37
tools/clientcmd/api/v1/defaults.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
84
tools/clientcmd/api/v1/defaults_test.go
Normal file
84
tools/clientcmd/api/v1/defaults_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -16,5 +16,6 @@ limitations under the License.
|
||||
|
||||
// +k8s:conversion-gen=k8s.io/client-go/tools/clientcmd/api
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:defaulter-gen=Kind
|
||||
|
||||
package v1
|
||||
|
@ -37,7 +37,7 @@ func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes)
|
||||
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
|
||||
}
|
||||
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
|
@ -221,6 +221,18 @@ type ExecConfig struct {
|
||||
// to false. Package k8s.io/client-go/tools/auth/exec provides helper methods for
|
||||
// reading this environment variable.
|
||||
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
|
||||
@ -229,3 +241,23 @@ type ExecEnvVar struct {
|
||||
Name string `json:"name"`
|
||||
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"
|
||||
)
|
||||
|
@ -376,6 +376,7 @@ func autoConvert_v1_ExecConfig_To_api_ExecConfig(in *ExecConfig, out *api.ExecCo
|
||||
out.APIVersion = in.APIVersion
|
||||
out.InstallHint = in.InstallHint
|
||||
out.ProvideClusterInfo = in.ProvideClusterInfo
|
||||
out.InteractiveMode = api.ExecInteractiveMode(in.InteractiveMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -392,6 +393,9 @@ func autoConvert_api_ExecConfig_To_v1_ExecConfig(in *api.ExecConfig, out *ExecCo
|
||||
out.InstallHint = in.InstallHint
|
||||
out.ProvideClusterInfo = in.ProvideClusterInfo
|
||||
// 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
|
||||
}
|
||||
|
||||
|
42
tools/clientcmd/api/v1/zz_generated.defaults.go
Normal file
42
tools/clientcmd/api/v1/zz_generated.defaults.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -630,9 +630,10 @@ func TestCreateAuthConfigExecInstallHintCleanup(t *testing.T) {
|
||||
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{
|
||||
AuthInfo: clientcmdapi.AuthInfo{
|
||||
Exec: &clientcmdapi.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
Command: "some-command",
|
||||
InstallHint: "some install hint with \x1b[1mcontrol chars\x1b[0m\nand a newline",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
Command: "some-command",
|
||||
InstallHint: "some install hint with \x1b[1mcontrol chars\x1b[0m\nand a newline",
|
||||
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
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
|
||||
|
@ -377,6 +377,7 @@ func TestValidateAuthInfoExec(t *testing.T) {
|
||||
Env: []clientcmdapi.ExecEnvVar{
|
||||
{Name: "foo", Value: "bar"},
|
||||
},
|
||||
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
}
|
||||
test := configValidationTest{
|
||||
@ -391,7 +392,8 @@ func TestValidateAuthInfoExecNoVersion(t *testing.T) {
|
||||
config := clientcmdapi.NewConfig()
|
||||
config.AuthInfos["user"] = &clientcmdapi.AuthInfo{
|
||||
Exec: &clientcmdapi.ExecConfig{
|
||||
Command: "/bin/example",
|
||||
Command: "/bin/example",
|
||||
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
}
|
||||
test := configValidationTest{
|
||||
@ -409,7 +411,8 @@ func TestValidateAuthInfoExecNoCommand(t *testing.T) {
|
||||
config := clientcmdapi.NewConfig()
|
||||
config.AuthInfos["user"] = &clientcmdapi.AuthInfo{
|
||||
Exec: &clientcmdapi.ExecConfig{
|
||||
APIVersion: "clientauthentication.k8s.io/v1alpha1",
|
||||
APIVersion: "clientauthentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
}
|
||||
test := configValidationTest{
|
||||
@ -430,8 +433,9 @@ func TestValidateAuthInfoExecWithAuthProvider(t *testing.T) {
|
||||
Name: "oidc",
|
||||
},
|
||||
Exec: &clientcmdapi.ExecConfig{
|
||||
Command: "/bin/example",
|
||||
APIVersion: "clientauthentication.k8s.io/v1alpha1",
|
||||
Command: "/bin/example",
|
||||
APIVersion: "clientauthentication.k8s.io/v1alpha1",
|
||||
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
}
|
||||
test := configValidationTest{
|
||||
@ -454,6 +458,7 @@ func TestValidateAuthInfoExecNoEnv(t *testing.T) {
|
||||
Env: []clientcmdapi.ExecEnvVar{
|
||||
{Name: "foo", Value: ""},
|
||||
},
|
||||
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
|
||||
},
|
||||
}
|
||||
test := configValidationTest{
|
||||
@ -464,6 +469,45 @@ func TestValidateAuthInfoExecNoEnv(t *testing.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 {
|
||||
config *clientcmdapi.Config
|
||||
expectedErrorSubstring []string
|
||||
|
Loading…
Reference in New Issue
Block a user