diff --git a/staging/src/k8s.io/cli-runtime/pkg/resource/builder.go b/staging/src/k8s.io/cli-runtime/pkg/resource/builder.go index a8ebf6410c8..c77b79bfd88 100644 --- a/staging/src/k8s.io/cli-runtime/pkg/resource/builder.go +++ b/staging/src/k8s.io/cli-runtime/pkg/resource/builder.go @@ -72,9 +72,10 @@ type Builder struct { errs []error - paths []Visitor - stream bool - dir bool + paths []Visitor + stream bool + stdinInUse bool + dir bool labelSelector *string fieldSelector *string @@ -121,6 +122,8 @@ Example resource specifications include: '-f rsrc.yaml' '--filename=rsrc.json'`) +var StdinMultiUseError = errors.New("standard input cannot be used for multiple arguments") + // TODO: expand this to include other errors. func IsUsageError(err error) bool { 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 // 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 { 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)) 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 // 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 @@ -911,9 +932,9 @@ func (b *Builder) getClient(gv schema.GroupVersion) (RESTClient, error) { case b.fakeClientFn != nil: client, err = b.fakeClientFn(gv) case b.negotiatedSerializer != nil: - client, err = b.clientConfigFn.clientForGroupVersion(gv, b.negotiatedSerializer) + client, err = b.clientConfigFn.withStdinUnavailable(b.stdinInUse).clientForGroupVersion(gv, b.negotiatedSerializer) default: - client, err = b.clientConfigFn.unstructuredClientForGroupVersion(gv) + client, err = b.clientConfigFn.withStdinUnavailable(b.stdinInUse).unstructuredClientForGroupVersion(gv) } if err != nil { diff --git a/staging/src/k8s.io/cli-runtime/pkg/resource/builder_test.go b/staging/src/k8s.io/cli-runtime/pkg/resource/builder_test.go index 5f5fe433ae1..6a46454195d 100644 --- a/staging/src/k8s.io/cli-runtime/pkg/resource/builder_test.go +++ b/staging/src/k8s.io/cli-runtime/pkg/resource/builder_test.go @@ -18,6 +18,7 @@ package resource import ( "bytes" + "errors" "fmt" "io" "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) + } +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/resource/client.go b/staging/src/k8s.io/cli-runtime/pkg/resource/client.go index 46380207f3e..cd52c304313 100644 --- a/staging/src/k8s.io/cli-runtime/pkg/resource/client.go +++ b/staging/src/k8s.io/cli-runtime/pkg/resource/client.go @@ -56,3 +56,14 @@ func (clientConfigFn ClientConfigFunc) unstructuredClientForGroupVersion(gv sche 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 + } +} diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/types.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/types.go index c1089979203..8daaa3f8f73 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/types.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/types.go @@ -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 diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/conversion.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/conversion.go index 441b7c44bd9..6741114dd8d 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/conversion.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/conversion.go @@ -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) } diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/types.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/types.go index fabc6f65e6c..714b0273ae9 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/types.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/types.go @@ -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. diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.conversion.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.conversion.go index 90f7935fefe..8daed805240 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.conversion.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.conversion.go @@ -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) diff --git a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go index ce24be17539..0964f1d88b7 100644 --- a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go +++ b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go @@ -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 ": " 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 } diff --git a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec_test.go b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec_test.go index d295391ef31..ca40eb0dc98 100644 --- a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec_test.go +++ b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec_test.go @@ -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) } diff --git a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/metrics_test.go b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/metrics_test.go index 3c3c8f26f05..80d84480bd3 100644 --- a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/metrics_test.go +++ b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/metrics_test.go @@ -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) } diff --git a/staging/src/k8s.io/client-go/rest/config_test.go b/staging/src/k8s.io/client-go/rest/config_test.go index bd857e0adfa..7650928daf6 100644 --- a/staging/src/k8s.io/client-go/rest/config_test.go +++ b/staging/src/k8s.io/client-go/rest/config_test.go @@ -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, ) diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/types.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/types.go index 24f469236ed..31716abf180 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/api/types.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/types.go @@ -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{ diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/defaults.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/defaults.go new file mode 100644 index 00000000000..bf513dc7c7b --- /dev/null +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/defaults.go @@ -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 + } + } +} diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/defaults_test.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/defaults_test.go new file mode 100644 index 00000000000..03fdb04276f --- /dev/null +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/defaults_test.go @@ -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) + } + }) + } +} diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/doc.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/doc.go index ba5572ab07d..3ccdebc1c37 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/doc.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/doc.go @@ -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 diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/register.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/register.go index 24f6284c574..4a4d4a55fb4 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/register.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/register.go @@ -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 { diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go index 8c29b39c1b2..7e8351032cf 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go @@ -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" +) diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/zz_generated.conversion.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/zz_generated.conversion.go index 26e96529d38..13dfa903cf8 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/zz_generated.conversion.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/zz_generated.conversion.go @@ -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 } diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/zz_generated.defaults.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/zz_generated.defaults.go new file mode 100644 index 00000000000..aae469033cc --- /dev/null +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/zz_generated.defaults.go @@ -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) + } + } +} diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/client_config_test.go b/staging/src/k8s.io/client-go/tools/clientcmd/client_config_test.go index 9d232f4f63f..a770dd61cc3 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/client_config_test.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/client_config_test.go @@ -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) diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/validation.go b/staging/src/k8s.io/client-go/tools/clientcmd/validation.go index f77ef04fe54..8541a08b2f1 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/validation.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/validation.go @@ -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 diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/validation_test.go b/staging/src/k8s.io/client-go/tools/clientcmd/validation_test.go index caf67054c16..676d1989751 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/validation_test.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/validation_test.go @@ -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 diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/replace/replace.go b/staging/src/k8s.io/kubectl/pkg/cmd/replace/replace.go index 3a7d3224bd0..5ac2047c7fd 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/replace/replace.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/replace/replace.go @@ -306,6 +306,7 @@ func (o *ReplaceOptions) Run(f cmdutil.Factory) error { } func (o *ReplaceOptions) forceReplace() error { + stdinInUse := false for i, filename := range o.DeleteOptions.FilenameOptions.Filenames { if filename == "-" { tempDir, err := ioutil.TempDir("", "kubectl_replace_") @@ -319,17 +320,21 @@ func (o *ReplaceOptions) forceReplace() error { return err } o.DeleteOptions.FilenameOptions.Filenames[i] = tempFilename + stdinInUse = true } } - r := o.Builder(). + b := o.Builder(). Unstructured(). ContinueOnError(). NamespaceParam(o.Namespace).DefaultNamespace(). ResourceTypeOrNameArgs(false, o.BuilderArgs...).RequireObject(false). FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions). - Flatten(). - Do() + Flatten() + if stdinInUse { + b = b.StdinInUse() + } + r := b.Do() if err := r.Err(); err != nil { return err } @@ -358,14 +363,17 @@ func (o *ReplaceOptions) forceReplace() error { return err } - r = o.Builder(). + b = o.Builder(). Unstructured(). Schema(o.Schema). ContinueOnError(). NamespaceParam(o.Namespace).DefaultNamespace(). FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions). - Flatten(). - Do() + Flatten() + if stdinInUse { + b = b.StdinInUse() + } + r = b.Do() err = r.Err() if err != nil { return err diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_parse.go b/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_parse.go index e93fed8311d..5f46237d29d 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_parse.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_parse.go @@ -23,7 +23,7 @@ import ( "regexp" "strings" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" "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. // 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{} exists := sets.NewString() var remove []string + usedStdin := false for _, envSpec := range spec { switch { case envSpec == "-": 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) if err != nil { - return nil, nil, err + return nil, nil, usedStdin, err } env = append(env, fileEnv...) + usedStdin = true case strings.Contains(envSpec, "="): parts := strings.SplitN(envSpec, "=", 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 { - 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]) env = append(env, v1.EnvVar{ @@ -90,20 +92,20 @@ func parseIntoEnvVar(spec []string, defaultReader io.Reader, envVarType string) case strings.HasSuffix(envSpec, "-"): remove = append(remove, envSpec[:len(envSpec)-1]) 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 { 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. -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") } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_parse_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_parse_test.go index e877e0c016d..f378312bb90 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_parse_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_parse_test.go @@ -40,12 +40,27 @@ func ExampleSplitEnvironmentFromResources() { // Output: [resource] [ENV\=ARG ONE\=MORE DASH-] true } -func ExampleParseEnv_good() { +func ExampleParseEnv_good_with_stdin() { r := strings.NewReader("FROM=READER") ss := []string{"ENV=VARIABLE", "ENV.TEST=VARIABLE", "AND=ANOTHER", "REMOVE-", "-"} fmt.Println(ParseEnv(ss, r)) // Output: - // [{ENV VARIABLE nil} {ENV.TEST VARIABLE nil} {AND ANOTHER nil} {FROM READER nil}] [REMOVE] + // [{ENV VARIABLE nil} {ENV.TEST VARIABLE nil} {AND ANOTHER nil} {FROM READER nil}] [REMOVE] true +} + +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 } func ExampleParseEnv_bad_first() { @@ -53,7 +68,7 @@ func ExampleParseEnv_bad_first() { bad := []string{"This not in the key=value format."} fmt.Println(ParseEnv(bad, r)) // 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() { @@ -61,7 +76,7 @@ func ExampleParseEnv_bad_second() { bad := []string{".=VARIABLE"} fmt.Println(ParseEnv(bad, r)) // Output: - // [] [] "." is not a valid key name: must not be '.' + // [] [] false "." is not a valid key name: must not be '.' } func ExampleParseEnv_bad_third() { @@ -69,7 +84,7 @@ func ExampleParseEnv_bad_third() { bad := []string{"..=VARIABLE"} fmt.Println(ParseEnv(bad, r)) // Output: - // [] [] ".." is not a valid key name: must not be '..' + // [] [] false ".." is not a valid key name: must not be '..' } func ExampleParseEnv_bad_fourth() { @@ -77,5 +92,5 @@ func ExampleParseEnv_bad_fourth() { bad := []string{"..ENV=VARIABLE"} fmt.Println(ParseEnv(bad, r)) // Output: - // [] [] "..ENV" is not a valid key name: must not start with '..' + // [] [] false "..ENV" is not a valid key name: must not start with '..' } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env.go b/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env.go index 82a96cb27aa..7986df4357e 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env.go @@ -270,7 +270,7 @@ func (o *EnvOptions) Validate() error { // RunEnv contains all the necessary functionality for the OpenShift cli env command 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 { return err } @@ -291,6 +291,10 @@ func (o *EnvOptions) RunEnv() error { Latest() } + if envFromStdin { + b = b.StdinInUse() + } + infos, err := b.Do().Infos() if err != nil { return err @@ -358,6 +362,10 @@ func (o *EnvOptions) RunEnv() error { Latest() } + if envFromStdin { + b = b.StdinInUse() + } + infos, err := b.Do().Infos() if err != nil { return err diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env_test.go index baa1a0188f5..4a610d892a6 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env_test.go @@ -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) +} diff --git a/test/cmd/apps.sh b/test/cmd/apps.sh index 8232c907881..e8ab6f8c670 100755 --- a/test/cmd/apps.sh +++ b/test/cmd/apps.sh @@ -432,6 +432,9 @@ run_deployment_tests() { kubectl set env deployment nginx-deployment --from=secret/test-set-env-secret "${kube_flags[@]:?}" # Remove specific env of deployment 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 kubectl delete deployment nginx-deployment "${kube_flags[@]:?}" kubectl delete configmap test-set-env-config "${kube_flags[@]:?}" diff --git a/test/cmd/authentication.sh b/test/cmd/authentication.sh index be1f36268f4..b6542ba55cc 100644 --- a/test/cmd/authentication.sh +++ b/test/cmd/authentication.sh @@ -92,7 +92,6 @@ users: user: exec: apiVersion: client.authentication.k8s.io/v1beta1 - # Any invalid exec credential plugin will do to demonstrate command: echo args: - '{"apiVersion":"client.authentication.k8s.io/v1beta1","status":{"token":"admin-token"}}' @@ -132,3 +131,89 @@ EOF set +o nounset 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 <"$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 +} \ No newline at end of file diff --git a/test/cmd/legacy-script.sh b/test/cmd/legacy-script.sh index 3dd3ed3d017..9b9bcd7aa71 100755 --- a/test/cmd/legacy-script.sh +++ b/test/cmd/legacy-script.sh @@ -775,6 +775,7 @@ runTests() { ######################## record_command run_exec_credentials_tests + record_command run_exec_credentials_interactive_tests ######################## # authorization.k8s.io # diff --git a/test/integration/client/exec_test.go b/test/integration/client/exec_test.go index c12da645038..5cf2e8bfd49 100644 --- a/test/integration/client/exec_test.go +++ b/test/integration/client/exec_test.go @@ -438,6 +438,7 @@ func TestExecPluginViaClient(t *testing.T) { "--random-arg-to-avoid-authenticator-cache-hits", rand.String(10), }, + InteractiveMode: clientcmdapi.IfAvailableExecInteractiveMode, } clientConfig.Wrap(func(rt http.RoundTripper) http.RoundTripper { return roundTripperFunc(func(req *http.Request) (*http.Response, error) { @@ -653,7 +654,8 @@ func TestExecPluginViaInformer(t *testing.T) { clientConfig.ExecProvider = &clientcmdapi.ExecConfig{ Command: "testdata/exec-plugin.sh", // 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 { @@ -687,7 +689,8 @@ func (e *execPlugin) config() *clientcmdapi.ExecConfig { return &clientcmdapi.ExecConfig{ Command: "testdata/exec-plugin.sh", // 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{ { Name: outputFileEnvVar,