Merge pull request #99310 from ankeesler/exec-plugin-interactive

exec credential provider: InteractiveMode support
This commit is contained in:
Kubernetes Prow Robot 2021-06-15 09:46:01 -07:00 committed by GitHub
commit 37da905c0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 888 additions and 116 deletions

View File

@ -74,6 +74,7 @@ type Builder struct {
paths []Visitor paths []Visitor
stream bool stream bool
stdinInUse bool
dir bool dir bool
labelSelector *string labelSelector *string
@ -121,6 +122,8 @@ Example resource specifications include:
'-f rsrc.yaml' '-f rsrc.yaml'
'--filename=rsrc.json'`) '--filename=rsrc.json'`)
var StdinMultiUseError = errors.New("standard input cannot be used for multiple arguments")
// TODO: expand this to include other errors. // TODO: expand this to include other errors.
func IsUsageError(err error) bool { func IsUsageError(err error) bool {
if err == nil { if err == nil {
@ -362,13 +365,31 @@ func (b *Builder) URL(httpAttemptCount int, urls ...*url.URL) *Builder {
// Stdin will read objects from the standard input. If ContinueOnError() is set // Stdin will read objects from the standard input. If ContinueOnError() is set
// prior to this method being called, objects in the stream that are unrecognized // prior to this method being called, objects in the stream that are unrecognized
// will be ignored (but logged at V(2)). // will be ignored (but logged at V(2)). If StdinInUse() is set prior to this method
// being called, an error will be recorded as there are multiple entities trying to use
// the single standard input stream.
func (b *Builder) Stdin() *Builder { func (b *Builder) Stdin() *Builder {
b.stream = true b.stream = true
if b.stdinInUse {
b.errs = append(b.errs, StdinMultiUseError)
}
b.stdinInUse = true
b.paths = append(b.paths, FileVisitorForSTDIN(b.mapper, b.schema)) b.paths = append(b.paths, FileVisitorForSTDIN(b.mapper, b.schema))
return b return b
} }
// StdinInUse will mark standard input as in use by this Builder, and therefore standard
// input should not be used by another entity. If Stdin() is set prior to this method
// being called, an error will be recorded as there are multiple entities trying to use
// the single standard input stream.
func (b *Builder) StdinInUse() *Builder {
if b.stdinInUse {
b.errs = append(b.errs, StdinMultiUseError)
}
b.stdinInUse = true
return b
}
// Stream will read objects from the provided reader, and if an error occurs will // Stream will read objects from the provided reader, and if an error occurs will
// include the name string in the error message. If ContinueOnError() is set // include the name string in the error message. If ContinueOnError() is set
// prior to this method being called, objects in the stream that are unrecognized // prior to this method being called, objects in the stream that are unrecognized
@ -911,9 +932,9 @@ func (b *Builder) getClient(gv schema.GroupVersion) (RESTClient, error) {
case b.fakeClientFn != nil: case b.fakeClientFn != nil:
client, err = b.fakeClientFn(gv) client, err = b.fakeClientFn(gv)
case b.negotiatedSerializer != nil: case b.negotiatedSerializer != nil:
client, err = b.clientConfigFn.clientForGroupVersion(gv, b.negotiatedSerializer) client, err = b.clientConfigFn.withStdinUnavailable(b.stdinInUse).clientForGroupVersion(gv, b.negotiatedSerializer)
default: default:
client, err = b.clientConfigFn.unstructuredClientForGroupVersion(gv) client, err = b.clientConfigFn.withStdinUnavailable(b.stdinInUse).unstructuredClientForGroupVersion(gv)
} }
if err != nil { if err != nil {

View File

@ -18,6 +18,7 @@ package resource
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -1793,3 +1794,12 @@ func TestUnstructured(t *testing.T) {
}) })
} }
} }
func TestStdinMultiUseError(t *testing.T) {
if got, want := newUnstructuredDefaultBuilder().Stdin().StdinInUse().Do().Err(), StdinMultiUseError; !errors.Is(got, want) {
t.Errorf("got: %q, want: %q", got, want)
}
if got, want := newUnstructuredDefaultBuilder().StdinInUse().Stdin().Do().Err(), StdinMultiUseError; !errors.Is(got, want) {
t.Errorf("got: %q, want: %q", got, want)
}
}

View File

@ -56,3 +56,14 @@ func (clientConfigFn ClientConfigFunc) unstructuredClientForGroupVersion(gv sche
return rest.RESTClientFor(cfg) return rest.RESTClientFor(cfg)
} }
func (clientConfigFn ClientConfigFunc) withStdinUnavailable(stdinUnavailable bool) ClientConfigFunc {
return func() (*rest.Config, error) {
cfg, err := clientConfigFn()
if stdinUnavailable && cfg != nil && cfg.ExecProvider != nil {
cfg.ExecProvider.StdinUnavailable = stdinUnavailable
cfg.ExecProvider.StdinUnavailableMessage = "used by stdin resource manifest reader"
}
return cfg, err
}
}

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
@ -198,7 +198,7 @@ func newAuthenticator(c *cache, config *api.ExecConfig, cluster *clientauthentic
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,
@ -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 {
@ -233,7 +260,7 @@ type Authenticator struct {
// 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
@ -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) {
@ -292,10 +308,11 @@ func TestRefreshCreds(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
config api.ExecConfig config api.ExecConfig
stdinUnavailable bool
exitCode int exitCode int
cluster *clientauthentication.Cluster cluster *clientauthentication.Cluster
output string output string
interactive bool isTerminal bool
response *clientauthentication.Response response *clientauthentication.Response
wantInput string wantInput string
wantCreds credentials wantCreds credentials
@ -307,6 +324,7 @@ func TestRefreshCreds(t *testing.T) {
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",
@ -326,8 +344,9 @@ 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",
@ -348,6 +367,7 @@ 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{
@ -382,6 +402,7 @@ 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",
@ -403,6 +424,7 @@ 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",
@ -421,6 +443,7 @@ 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",
@ -437,6 +460,7 @@ 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",
@ -454,6 +478,7 @@ 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",
@ -474,6 +499,7 @@ 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",
@ -494,6 +520,7 @@ 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",
@ -513,11 +540,206 @@ 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",
@ -532,11 +754,14 @@ 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",
@ -553,6 +778,7 @@ 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",
@ -566,6 +792,7 @@ 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",
@ -577,6 +804,7 @@ 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",
@ -591,6 +819,7 @@ func TestRefreshCreds(t *testing.T) {
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",
@ -599,6 +828,7 @@ 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,
@ -608,6 +838,7 @@ 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: `{
@ -709,6 +942,7 @@ 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 {
@ -839,8 +1074,9 @@ func TestRoundTripper(t *testing.T) {
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)
} }
@ -1144,8 +1381,9 @@ func TestInstallHintRateLimit(t *testing.T) {
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)
} }
@ -173,8 +174,9 @@ func TestCallsMetric(t *testing.T) {
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

@ -633,6 +633,7 @@ func TestCreateAuthConfigExecInstallHintCleanup(t *testing.T) {
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{
@ -392,6 +393,7 @@ func TestValidateAuthInfoExecNoVersion(t *testing.T) {
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{
@ -410,6 +412,7 @@ func TestValidateAuthInfoExecNoCommand(t *testing.T) {
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{
@ -432,6 +435,7 @@ func TestValidateAuthInfoExecWithAuthProvider(t *testing.T) {
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

View File

@ -306,6 +306,7 @@ func (o *ReplaceOptions) Run(f cmdutil.Factory) error {
} }
func (o *ReplaceOptions) forceReplace() error { func (o *ReplaceOptions) forceReplace() error {
stdinInUse := false
for i, filename := range o.DeleteOptions.FilenameOptions.Filenames { for i, filename := range o.DeleteOptions.FilenameOptions.Filenames {
if filename == "-" { if filename == "-" {
tempDir, err := ioutil.TempDir("", "kubectl_replace_") tempDir, err := ioutil.TempDir("", "kubectl_replace_")
@ -319,17 +320,21 @@ func (o *ReplaceOptions) forceReplace() error {
return err return err
} }
o.DeleteOptions.FilenameOptions.Filenames[i] = tempFilename o.DeleteOptions.FilenameOptions.Filenames[i] = tempFilename
stdinInUse = true
} }
} }
r := o.Builder(). b := o.Builder().
Unstructured(). Unstructured().
ContinueOnError(). ContinueOnError().
NamespaceParam(o.Namespace).DefaultNamespace(). NamespaceParam(o.Namespace).DefaultNamespace().
ResourceTypeOrNameArgs(false, o.BuilderArgs...).RequireObject(false). ResourceTypeOrNameArgs(false, o.BuilderArgs...).RequireObject(false).
FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions). FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
Flatten(). Flatten()
Do() if stdinInUse {
b = b.StdinInUse()
}
r := b.Do()
if err := r.Err(); err != nil { if err := r.Err(); err != nil {
return err return err
} }
@ -358,14 +363,17 @@ func (o *ReplaceOptions) forceReplace() error {
return err return err
} }
r = o.Builder(). b = o.Builder().
Unstructured(). Unstructured().
Schema(o.Schema). Schema(o.Schema).
ContinueOnError(). ContinueOnError().
NamespaceParam(o.Namespace).DefaultNamespace(). NamespaceParam(o.Namespace).DefaultNamespace().
FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions). FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
Flatten(). Flatten()
Do() if stdinInUse {
b = b.StdinInUse()
}
r = b.Do()
err = r.Err() err = r.Err()
if err != nil { if err != nil {
return err return err

View File

@ -23,7 +23,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation"
) )
@ -59,28 +59,30 @@ func SplitEnvironmentFromResources(args []string) (resources, envArgs []string,
// parseIntoEnvVar parses the list of key-value pairs into kubernetes EnvVar. // parseIntoEnvVar parses the list of key-value pairs into kubernetes EnvVar.
// envVarType is for making errors more specific to user intentions. // envVarType is for making errors more specific to user intentions.
func parseIntoEnvVar(spec []string, defaultReader io.Reader, envVarType string) ([]v1.EnvVar, []string, error) { func parseIntoEnvVar(spec []string, defaultReader io.Reader, envVarType string) ([]v1.EnvVar, []string, bool, error) {
env := []v1.EnvVar{} env := []v1.EnvVar{}
exists := sets.NewString() exists := sets.NewString()
var remove []string var remove []string
usedStdin := false
for _, envSpec := range spec { for _, envSpec := range spec {
switch { switch {
case envSpec == "-": case envSpec == "-":
if defaultReader == nil { if defaultReader == nil {
return nil, nil, fmt.Errorf("when '-' is used, STDIN must be open") return nil, nil, usedStdin, fmt.Errorf("when '-' is used, STDIN must be open")
} }
fileEnv, err := readEnv(defaultReader, envVarType) fileEnv, err := readEnv(defaultReader, envVarType)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, usedStdin, err
} }
env = append(env, fileEnv...) env = append(env, fileEnv...)
usedStdin = true
case strings.Contains(envSpec, "="): case strings.Contains(envSpec, "="):
parts := strings.SplitN(envSpec, "=", 2) parts := strings.SplitN(envSpec, "=", 2)
if len(parts) != 2 { if len(parts) != 2 {
return nil, nil, fmt.Errorf("invalid %s: %v", envVarType, envSpec) return nil, nil, usedStdin, fmt.Errorf("invalid %s: %v", envVarType, envSpec)
} }
if errs := validation.IsEnvVarName(parts[0]); len(errs) != 0 { if errs := validation.IsEnvVarName(parts[0]); len(errs) != 0 {
return nil, nil, fmt.Errorf("%q is not a valid key name: %s", parts[0], strings.Join(errs, ";")) return nil, nil, usedStdin, fmt.Errorf("%q is not a valid key name: %s", parts[0], strings.Join(errs, ";"))
} }
exists.Insert(parts[0]) exists.Insert(parts[0])
env = append(env, v1.EnvVar{ env = append(env, v1.EnvVar{
@ -90,20 +92,20 @@ func parseIntoEnvVar(spec []string, defaultReader io.Reader, envVarType string)
case strings.HasSuffix(envSpec, "-"): case strings.HasSuffix(envSpec, "-"):
remove = append(remove, envSpec[:len(envSpec)-1]) remove = append(remove, envSpec[:len(envSpec)-1])
default: default:
return nil, nil, fmt.Errorf("unknown %s: %v", envVarType, envSpec) return nil, nil, usedStdin, fmt.Errorf("unknown %s: %v", envVarType, envSpec)
} }
} }
for _, removeLabel := range remove { for _, removeLabel := range remove {
if _, found := exists[removeLabel]; found { if _, found := exists[removeLabel]; found {
return nil, nil, fmt.Errorf("can not both modify and remove the same %s in the same command", envVarType) return nil, nil, usedStdin, fmt.Errorf("can not both modify and remove the same %s in the same command", envVarType)
} }
} }
return env, remove, nil return env, remove, usedStdin, nil
} }
// ParseEnv parses the elements of the first argument looking for environment variables in key=value form and, if one of those values is "-", it also scans the reader. // ParseEnv parses the elements of the first argument looking for environment variables in key=value form and, if one of those values is "-", it also scans the reader and returns true for its third return value.
// The same environment variable cannot be both modified and removed in the same command. // The same environment variable cannot be both modified and removed in the same command.
func ParseEnv(spec []string, defaultReader io.Reader) ([]v1.EnvVar, []string, error) { func ParseEnv(spec []string, defaultReader io.Reader) ([]v1.EnvVar, []string, bool, error) {
return parseIntoEnvVar(spec, defaultReader, "environment variable") return parseIntoEnvVar(spec, defaultReader, "environment variable")
} }

View File

@ -40,12 +40,27 @@ func ExampleSplitEnvironmentFromResources() {
// Output: [resource] [ENV\=ARG ONE\=MORE DASH-] true // Output: [resource] [ENV\=ARG ONE\=MORE DASH-] true
} }
func ExampleParseEnv_good() { func ExampleParseEnv_good_with_stdin() {
r := strings.NewReader("FROM=READER") r := strings.NewReader("FROM=READER")
ss := []string{"ENV=VARIABLE", "ENV.TEST=VARIABLE", "AND=ANOTHER", "REMOVE-", "-"} ss := []string{"ENV=VARIABLE", "ENV.TEST=VARIABLE", "AND=ANOTHER", "REMOVE-", "-"}
fmt.Println(ParseEnv(ss, r)) fmt.Println(ParseEnv(ss, r))
// Output: // Output:
// [{ENV VARIABLE nil} {ENV.TEST VARIABLE nil} {AND ANOTHER nil} {FROM READER nil}] [REMOVE] <nil> // [{ENV VARIABLE nil} {ENV.TEST VARIABLE nil} {AND ANOTHER nil} {FROM READER nil}] [REMOVE] true <nil>
}
func ExampleParseEnv_good_with_stdin_and_error() {
r := strings.NewReader("FROM=READER")
ss := []string{"-", "This not in the key=value format."}
fmt.Println(ParseEnv(ss, r))
// Output:
// [] [] true "This not in the key" is not a valid key name: a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1', regex used for validation is '[-._a-zA-Z][-._a-zA-Z0-9]*')
}
func ExampleParseEnv_good_without_stdin() {
ss := []string{"ENV=VARIABLE", "ENV.TEST=VARIABLE", "AND=ANOTHER", "REMOVE-"}
fmt.Println(ParseEnv(ss, nil))
// Output:
// [{ENV VARIABLE nil} {ENV.TEST VARIABLE nil} {AND ANOTHER nil}] [REMOVE] false <nil>
} }
func ExampleParseEnv_bad_first() { func ExampleParseEnv_bad_first() {
@ -53,7 +68,7 @@ func ExampleParseEnv_bad_first() {
bad := []string{"This not in the key=value format."} bad := []string{"This not in the key=value format."}
fmt.Println(ParseEnv(bad, r)) fmt.Println(ParseEnv(bad, r))
// Output: // Output:
// [] [] "This not in the key" is not a valid key name: a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1', regex used for validation is '[-._a-zA-Z][-._a-zA-Z0-9]*') // [] [] false "This not in the key" is not a valid key name: a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1', regex used for validation is '[-._a-zA-Z][-._a-zA-Z0-9]*')
} }
func ExampleParseEnv_bad_second() { func ExampleParseEnv_bad_second() {
@ -61,7 +76,7 @@ func ExampleParseEnv_bad_second() {
bad := []string{".=VARIABLE"} bad := []string{".=VARIABLE"}
fmt.Println(ParseEnv(bad, r)) fmt.Println(ParseEnv(bad, r))
// Output: // Output:
// [] [] "." is not a valid key name: must not be '.' // [] [] false "." is not a valid key name: must not be '.'
} }
func ExampleParseEnv_bad_third() { func ExampleParseEnv_bad_third() {
@ -69,7 +84,7 @@ func ExampleParseEnv_bad_third() {
bad := []string{"..=VARIABLE"} bad := []string{"..=VARIABLE"}
fmt.Println(ParseEnv(bad, r)) fmt.Println(ParseEnv(bad, r))
// Output: // Output:
// [] [] ".." is not a valid key name: must not be '..' // [] [] false ".." is not a valid key name: must not be '..'
} }
func ExampleParseEnv_bad_fourth() { func ExampleParseEnv_bad_fourth() {
@ -77,5 +92,5 @@ func ExampleParseEnv_bad_fourth() {
bad := []string{"..ENV=VARIABLE"} bad := []string{"..ENV=VARIABLE"}
fmt.Println(ParseEnv(bad, r)) fmt.Println(ParseEnv(bad, r))
// Output: // Output:
// [] [] "..ENV" is not a valid key name: must not start with '..' // [] [] false "..ENV" is not a valid key name: must not start with '..'
} }

View File

@ -270,7 +270,7 @@ func (o *EnvOptions) Validate() error {
// RunEnv contains all the necessary functionality for the OpenShift cli env command // RunEnv contains all the necessary functionality for the OpenShift cli env command
func (o *EnvOptions) RunEnv() error { func (o *EnvOptions) RunEnv() error {
env, remove, err := envutil.ParseEnv(append(o.EnvParams, o.envArgs...), o.In) env, remove, envFromStdin, err := envutil.ParseEnv(append(o.EnvParams, o.envArgs...), o.In)
if err != nil { if err != nil {
return err return err
} }
@ -291,6 +291,10 @@ func (o *EnvOptions) RunEnv() error {
Latest() Latest()
} }
if envFromStdin {
b = b.StdinInUse()
}
infos, err := b.Do().Infos() infos, err := b.Do().Infos()
if err != nil { if err != nil {
return err return err
@ -358,6 +362,10 @@ func (o *EnvOptions) RunEnv() error {
Latest() Latest()
} }
if envFromStdin {
b = b.StdinInUse()
}
infos, err := b.Do().Infos() infos, err := b.Do().Infos()
if err != nil { if err != nil {
return err return err

View File

@ -765,3 +765,32 @@ func TestSetEnvRemoteWithSpecificContainers(t *testing.T) {
}) })
} }
} }
func TestSetEnvDoubleStdinUsage(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
tf.Client = &fake.RESTClient{
GroupVersion: schema.GroupVersion{Version: ""},
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
return nil, nil
}),
}
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}}
streams, bufIn, _, _ := genericclioptions.NewTestIOStreams()
bufIn.WriteString("SOME_ENV_VAR_KEY=SOME_ENV_VAR_VAL")
opts := NewEnvOptions(streams)
opts.FilenameOptions = resource.FilenameOptions{
Filenames: []string{"-"},
}
err := opts.Complete(tf, NewCmdEnv(tf, streams), []string{"-"})
assert.NoError(t, err)
err = opts.Validate()
assert.NoError(t, err)
err = opts.RunEnv()
assert.ErrorIs(t, err, resource.StdinMultiUseError)
}

View File

@ -432,6 +432,9 @@ run_deployment_tests() {
kubectl set env deployment nginx-deployment --from=secret/test-set-env-secret "${kube_flags[@]:?}" kubectl set env deployment nginx-deployment --from=secret/test-set-env-secret "${kube_flags[@]:?}"
# Remove specific env of deployment # Remove specific env of deployment
kubectl set env deployment nginx-deployment env- kubectl set env deployment nginx-deployment env-
# Assert that we cannot use standard input for both resource and environment variable
output_message="$(echo SOME_ENV_VAR_KEY=SOME_ENV_VAR_VAL | kubectl set env -f - - "${kube_flags[@]:?}" 2>&1 || true)"
kube::test::if_has_string "${output_message}" 'standard input cannot be used for multiple arguments'
# Clean up # Clean up
kubectl delete deployment nginx-deployment "${kube_flags[@]:?}" kubectl delete deployment nginx-deployment "${kube_flags[@]:?}"
kubectl delete configmap test-set-env-config "${kube_flags[@]:?}" kubectl delete configmap test-set-env-config "${kube_flags[@]:?}"

View File

@ -92,7 +92,6 @@ users:
user: user:
exec: exec:
apiVersion: client.authentication.k8s.io/v1beta1 apiVersion: client.authentication.k8s.io/v1beta1
# Any invalid exec credential plugin will do to demonstrate
command: echo command: echo
args: args:
- '{"apiVersion":"client.authentication.k8s.io/v1beta1","status":{"token":"admin-token"}}' - '{"apiVersion":"client.authentication.k8s.io/v1beta1","status":{"token":"admin-token"}}'
@ -132,3 +131,89 @@ EOF
set +o nounset set +o nounset
set +o errexit set +o errexit
} }
run_exec_credentials_interactive_tests() {
set -o nounset
set -o errexit
kube::log::status "Testing kubectl with configured interactive exec credentials plugin"
cat > "${TMPDIR:-/tmp}"/always_interactive_exec_plugin.yaml << EOF
apiVersion: v1
clusters:
- cluster:
name: test
contexts:
- context:
cluster: test
user: always_interactive_token_user
name: test
current-context: test
kind: Config
preferences: {}
users:
- name: always_interactive_token_user
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: echo
args:
- '{"apiVersion":"client.authentication.k8s.io/v1beta1","status":{"token":"admin-token"}}'
interactiveMode: Always
EOF
### The exec credential plugin should not be run if it kubectl already uses standard input
# Pre-condition: The kubectl command requires standard input
some_resource='{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"some-resource"}}'
# Declare map from kubectl command to standard input data
declare -A kubectl_commands
kubectl_commands["apply -f -"]="$some_resource"
kubectl_commands["set env deployment/some-deployment -"]="SOME_ENV_VAR_KEY=SOME_ENV_VAR_VAL"
kubectl_commands["replace -f - --force"]="$some_resource"
failure=
for kubectl_command in "${!kubectl_commands[@]}"; do
# Use a separate bash script for the command here so that script(1) will not get confused with kubectl flags
script_file="${TMPDIR:-/tmp}/test-cmd-exec-credentials-script-file.sh"
cat <<EOF >"$script_file"
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
kubectl ${kube_flags_without_token[*]:?} --kubeconfig=${TMPDIR:-/tmp}/always_interactive_exec_plugin.yaml ${kubectl_command} 2>&1 || true
EOF
chmod +x "$script_file"
# Run kubectl as child of script(1) so kubectl will always run with a PTY
# Dynamically build script(1) command so that we can conditionally add flags on Linux
script_command="script -q /dev/null"
if [[ "$(uname)" == "Linux" ]]; then script_command="${script_command} -c"; fi
script_command="${script_command} ${script_file}"
# Specify SHELL env var when we call script(1) since it is picky about the format of the env var
shell="$(which bash)"
kube::log::status "Running command '$script_command' (kubectl command: '$kubectl_command') with input '${kubectl_commands[$kubectl_command]}'"
output=$(echo "${kubectl_commands[$kubectl_command]}" | SHELL="$shell" $script_command)
if [[ "${output}" =~ "used by stdin resource manifest reader" ]]; then
kube::log::status "exec credential plugin not run because kubectl already uses standard input"
else
kube::log::status "Unexpected output when running kubectl command that uses standard input. Output: ${output}"
failure=yup
fi
done
if [[ -n "$failure" ]]; then
exit 1
fi
# Post-condition: None
rm "${TMPDIR:-/tmp}"/always_interactive_exec_plugin.yaml
set +o nounset
set +o errexit
}

View File

@ -775,6 +775,7 @@ runTests() {
######################## ########################
record_command run_exec_credentials_tests record_command run_exec_credentials_tests
record_command run_exec_credentials_interactive_tests
######################## ########################
# authorization.k8s.io # # authorization.k8s.io #

View File

@ -438,6 +438,7 @@ func TestExecPluginViaClient(t *testing.T) {
"--random-arg-to-avoid-authenticator-cache-hits", "--random-arg-to-avoid-authenticator-cache-hits",
rand.String(10), rand.String(10),
}, },
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
} }
clientConfig.Wrap(func(rt http.RoundTripper) http.RoundTripper { clientConfig.Wrap(func(rt http.RoundTripper) http.RoundTripper {
return roundTripperFunc(func(req *http.Request) (*http.Response, error) { return roundTripperFunc(func(req *http.Request) (*http.Response, error) {
@ -654,6 +655,7 @@ func TestExecPluginViaInformer(t *testing.T) {
Command: "testdata/exec-plugin.sh", Command: "testdata/exec-plugin.sh",
// TODO(ankeesler): move to v1 once exec plugins go GA. // TODO(ankeesler): move to v1 once exec plugins go GA.
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
} }
if test.clientConfigFunc != nil { if test.clientConfigFunc != nil {
@ -688,6 +690,7 @@ func (e *execPlugin) config() *clientcmdapi.ExecConfig {
Command: "testdata/exec-plugin.sh", Command: "testdata/exec-plugin.sh",
// TODO(ankeesler): move to v1 once exec plugins go GA. // TODO(ankeesler): move to v1 once exec plugins go GA.
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode,
Env: []clientcmdapi.ExecEnvVar{ Env: []clientcmdapi.ExecEnvVar{
{ {
Name: outputFileEnvVar, Name: outputFileEnvVar,