k8s.io/client-go@v0.31.1/plugin/pkg/client/auth/exec/exec_test.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package exec
    18  
    19  import (
    20  	"bytes"
    21  	"crypto/ecdsa"
    22  	"crypto/elliptic"
    23  	"crypto/rand"
    24  	"crypto/tls"
    25  	"crypto/x509"
    26  	"crypto/x509/pkix"
    27  	"encoding/json"
    28  	"encoding/pem"
    29  	"fmt"
    30  	"io"
    31  	"math/big"
    32  	"net/http"
    33  	"net/http/httptest"
    34  	"reflect"
    35  	"strconv"
    36  	"strings"
    37  	"testing"
    38  	"time"
    39  
    40  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    41  	"k8s.io/apimachinery/pkg/runtime"
    42  	"k8s.io/client-go/pkg/apis/clientauthentication"
    43  	"k8s.io/client-go/tools/clientcmd/api"
    44  	"k8s.io/client-go/transport"
    45  	testingclock "k8s.io/utils/clock/testing"
    46  )
    47  
    48  var (
    49  	certData = []byte(`-----BEGIN CERTIFICATE-----
    50  MIIC6jCCAdSgAwIBAgIBCzALBgkqhkiG9w0BAQswIzEhMB8GA1UEAwwYMTAuMTMu
    51  MTI5LjEwNkAxNDIxMzU5MDU4MB4XDTE1MDExNTIyMDEzMVoXDTE2MDExNTIyMDEz
    52  MlowGzEZMBcGA1UEAxMQb3BlbnNoaWZ0LWNsaWVudDCCASIwDQYJKoZIhvcNAQEB
    53  BQADggEPADCCAQoCggEBAKtdhz0+uCLXw5cSYns9rU/XifFSpb/x24WDdrm72S/v
    54  b9BPYsAStiP148buylr1SOuNi8sTAZmlVDDIpIVwMLff+o2rKYDicn9fjbrTxTOj
    55  lI4pHJBH+JU3AJ0tbajupioh70jwFS0oYpwtneg2zcnE2Z4l6mhrj2okrc5Q1/X2
    56  I2HChtIU4JYTisObtin10QKJX01CLfYXJLa8upWzKZ4/GOcHG+eAV3jXWoXidtjb
    57  1Usw70amoTZ6mIVCkiu1QwCoa8+ycojGfZhvqMsAp1536ZcCul+Na+AbCv4zKS7F
    58  kQQaImVrXdUiFansIoofGlw/JNuoKK6ssVpS5Ic3pgcCAwEAAaM1MDMwDgYDVR0P
    59  AQH/BAQDAgCgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwCwYJ
    60  KoZIhvcNAQELA4IBAQCKLREH7bXtXtZ+8vI6cjD7W3QikiArGqbl36bAhhWsJLp/
    61  p/ndKz39iFNaiZ3GlwIURWOOKx3y3GA0x9m8FR+Llthf0EQ8sUjnwaknWs0Y6DQ3
    62  jjPFZOpV3KPCFrdMJ3++E3MgwFC/Ih/N2ebFX9EcV9Vcc6oVWMdwT0fsrhu683rq
    63  6GSR/3iVX1G/pmOiuaR0fNUaCyCfYrnI4zHBDgSfnlm3vIvN2lrsR/DQBakNL8DJ
    64  HBgKxMGeUPoneBv+c8DMXIL0EhaFXRlBv9QW45/GiAIOuyFJ0i6hCtGZpJjq4OpQ
    65  BRjCI+izPzFTjsxD4aORE+WOkyWFCGPWKfNejfw0
    66  -----END CERTIFICATE-----`)
    67  	keyData = []byte(`-----BEGIN RSA PRIVATE KEY-----
    68  MIIEowIBAAKCAQEAq12HPT64ItfDlxJiez2tT9eJ8VKlv/HbhYN2ubvZL+9v0E9i
    69  wBK2I/Xjxu7KWvVI642LyxMBmaVUMMikhXAwt9/6jaspgOJyf1+NutPFM6OUjikc
    70  kEf4lTcAnS1tqO6mKiHvSPAVLShinC2d6DbNycTZniXqaGuPaiStzlDX9fYjYcKG
    71  0hTglhOKw5u2KfXRAolfTUIt9hcktry6lbMpnj8Y5wcb54BXeNdaheJ22NvVSzDv
    72  RqahNnqYhUKSK7VDAKhrz7JyiMZ9mG+oywCnXnfplwK6X41r4BsK/jMpLsWRBBoi
    73  ZWtd1SIVqewiih8aXD8k26gorqyxWlLkhzemBwIDAQABAoIBAD2XYRs3JrGHQUpU
    74  FkdbVKZkvrSY0vAZOqBTLuH0zUv4UATb8487anGkWBjRDLQCgxH+jucPTrztekQK
    75  aW94clo0S3aNtV4YhbSYIHWs1a0It0UdK6ID7CmdWkAj6s0T8W8lQT7C46mWYVLm
    76  5mFnCTHi6aB42jZrqmEpC7sivWwuU0xqj3Ml8kkxQCGmyc9JjmCB4OrFFC8NNt6M
    77  ObvQkUI6Z3nO4phTbpxkE1/9dT0MmPIF7GhHVzJMS+EyyRYUDllZ0wvVSOM3qZT0
    78  JMUaBerkNwm9foKJ1+dv2nMKZZbJajv7suUDCfU44mVeaEO+4kmTKSGCGjjTBGkr
    79  7L1ySDECgYEA5ElIMhpdBzIivCuBIH8LlUeuzd93pqssO1G2Xg0jHtfM4tz7fyeI
    80  cr90dc8gpli24dkSxzLeg3Tn3wIj/Bu64m2TpZPZEIlukYvgdgArmRIPQVxerYey
    81  OkrfTNkxU1HXsYjLCdGcGXs5lmb+K/kuTcFxaMOs7jZi7La+jEONwf8CgYEAwCs/
    82  rUOOA0klDsWWisbivOiNPII79c9McZCNBqncCBfMUoiGe8uWDEO4TFHN60vFuVk9
    83  8PkwpCfvaBUX+ajvbafIfHxsnfk1M04WLGCeqQ/ym5Q4sQoQOcC1b1y9qc/xEWfg
    84  nIUuia0ukYRpl7qQa3tNg+BNFyjypW8zukUAC/kCgYB1/Kojuxx5q5/oQVPrx73k
    85  2bevD+B3c+DYh9MJqSCNwFtUpYIWpggPxoQan4LwdsmO0PKzocb/ilyNFj4i/vII
    86  NToqSc/WjDFpaDIKyuu9oWfhECye45NqLWhb/6VOuu4QA/Nsj7luMhIBehnEAHW+
    87  GkzTKM8oD1PxpEG3nPKXYQKBgQC6AuMPRt3XBl1NkCrpSBy/uObFlFaP2Enpf39S
    88  3OZ0Gv0XQrnSaL1kP8TMcz68rMrGX8DaWYsgytstR4W+jyy7WvZwsUu+GjTJ5aMG
    89  77uEcEBpIi9CBzivfn7hPccE8ZgqPf+n4i6q66yxBJflW5xhvafJqDtW2LcPNbW/
    90  bvzdmQKBgExALRUXpq+5dbmkdXBHtvXdRDZ6rVmrnjy4nI5bPw+1GqQqk6uAR6B/
    91  F6NmLCQOO4PDG/cuatNHIr2FrwTmGdEL6ObLUGWn9Oer9gJhHVqqsY5I4sEPo4XX
    92  stR0Yiw0buV6DL/moUO0HIM9Bjh96HJp+LxiIS6UCdIhMPp5HoQa
    93  -----END RSA PRIVATE KEY-----`)
    94  	validCert *tls.Certificate
    95  )
    96  
    97  func init() {
    98  	cert, err := tls.X509KeyPair(certData, keyData)
    99  	if err != nil {
   100  		panic(err)
   101  	}
   102  	cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
   103  	if err != nil {
   104  		panic(err)
   105  	}
   106  	validCert = &cert
   107  }
   108  
   109  func TestCacheKey(t *testing.T) {
   110  	c1 := &api.ExecConfig{
   111  		Command: "foo-bar",
   112  		Args:    []string{"1", "2"},
   113  		Env: []api.ExecEnvVar{
   114  			{Name: "3", Value: "4"},
   115  			{Name: "5", Value: "6"},
   116  			{Name: "7", Value: "8"},
   117  		},
   118  		APIVersion:         "client.authentication.k8s.io/v1beta1",
   119  		ProvideClusterInfo: true,
   120  	}
   121  	c1c := &clientauthentication.Cluster{
   122  		Server:                   "foo",
   123  		TLSServerName:            "bar",
   124  		CertificateAuthorityData: []byte("baz"),
   125  		Config: &runtime.Unknown{
   126  			TypeMeta: runtime.TypeMeta{
   127  				APIVersion: "",
   128  				Kind:       "",
   129  			},
   130  			Raw:             []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`),
   131  			ContentEncoding: "",
   132  			ContentType:     "application/json",
   133  		},
   134  	}
   135  
   136  	c2 := &api.ExecConfig{
   137  		Command: "foo-bar",
   138  		Args:    []string{"1", "2"},
   139  		Env: []api.ExecEnvVar{
   140  			{Name: "3", Value: "4"},
   141  			{Name: "5", Value: "6"},
   142  			{Name: "7", Value: "8"},
   143  		},
   144  		APIVersion:         "client.authentication.k8s.io/v1beta1",
   145  		ProvideClusterInfo: true,
   146  	}
   147  	c2c := &clientauthentication.Cluster{
   148  		Server:                   "foo",
   149  		TLSServerName:            "bar",
   150  		CertificateAuthorityData: []byte("baz"),
   151  		Config: &runtime.Unknown{
   152  			TypeMeta: runtime.TypeMeta{
   153  				APIVersion: "",
   154  				Kind:       "",
   155  			},
   156  			Raw:             []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`),
   157  			ContentEncoding: "",
   158  			ContentType:     "application/json",
   159  		},
   160  	}
   161  
   162  	c3 := &api.ExecConfig{
   163  		Command: "foo-bar",
   164  		Args:    []string{"1", "2"},
   165  		Env: []api.ExecEnvVar{
   166  			{Name: "3", Value: "4"},
   167  			{Name: "5", Value: "6"},
   168  		},
   169  		APIVersion: "client.authentication.k8s.io/v1beta1",
   170  	}
   171  	c3c := &clientauthentication.Cluster{
   172  		Server:                   "foo",
   173  		TLSServerName:            "bar",
   174  		CertificateAuthorityData: []byte("baz"),
   175  		Config: &runtime.Unknown{
   176  			TypeMeta: runtime.TypeMeta{
   177  				APIVersion: "",
   178  				Kind:       "",
   179  			},
   180  			Raw:             []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`),
   181  			ContentEncoding: "",
   182  			ContentType:     "application/json",
   183  		},
   184  	}
   185  
   186  	c4 := &api.ExecConfig{
   187  		Command: "foo-bar",
   188  		Args:    []string{"1", "2"},
   189  		Env: []api.ExecEnvVar{
   190  			{Name: "3", Value: "4"},
   191  			{Name: "5", Value: "6"},
   192  		},
   193  		APIVersion: "client.authentication.k8s.io/v1beta1",
   194  	}
   195  	c4c := &clientauthentication.Cluster{
   196  		Server:                   "foo",
   197  		TLSServerName:            "bar",
   198  		CertificateAuthorityData: []byte("baz"),
   199  		Config: &runtime.Unknown{
   200  			TypeMeta: runtime.TypeMeta{
   201  				APIVersion: "",
   202  				Kind:       "",
   203  			},
   204  			Raw:             []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"panda"}}`),
   205  			ContentEncoding: "",
   206  			ContentType:     "application/json",
   207  		},
   208  	}
   209  
   210  	// c5/c5c should be the same as c4/c4c, except c5 has ProvideClusterInfo set to true.
   211  	c5 := &api.ExecConfig{
   212  		Command: "foo-bar",
   213  		Args:    []string{"1", "2"},
   214  		Env: []api.ExecEnvVar{
   215  			{Name: "3", Value: "4"},
   216  			{Name: "5", Value: "6"},
   217  		},
   218  		APIVersion:         "client.authentication.k8s.io/v1beta1",
   219  		ProvideClusterInfo: true,
   220  	}
   221  	c5c := &clientauthentication.Cluster{
   222  		Server:                   "foo",
   223  		TLSServerName:            "bar",
   224  		CertificateAuthorityData: []byte("baz"),
   225  		Config: &runtime.Unknown{
   226  			TypeMeta: runtime.TypeMeta{
   227  				APIVersion: "",
   228  				Kind:       "",
   229  			},
   230  			Raw:             []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"panda"}}`),
   231  			ContentEncoding: "",
   232  			ContentType:     "application/json",
   233  		},
   234  	}
   235  
   236  	// c6 should be the same as c4, except c6 is passed with a nil cluster
   237  	c6 := &api.ExecConfig{
   238  		Command: "foo-bar",
   239  		Args:    []string{"1", "2"},
   240  		Env: []api.ExecEnvVar{
   241  			{Name: "3", Value: "4"},
   242  			{Name: "5", Value: "6"},
   243  		},
   244  		APIVersion: "client.authentication.k8s.io/v1betaa1",
   245  	}
   246  
   247  	// c7 should be the same as c6, except c7 has stdin marked as unavailable
   248  	c7 := &api.ExecConfig{
   249  		Command: "foo-bar",
   250  		Args:    []string{"1", "2"},
   251  		Env: []api.ExecEnvVar{
   252  			{Name: "3", Value: "4"},
   253  			{Name: "5", Value: "6"},
   254  		},
   255  		APIVersion:       "client.authentication.k8s.io/v1beta1",
   256  		StdinUnavailable: true,
   257  	}
   258  
   259  	key1 := cacheKey(c1, c1c)
   260  	key2 := cacheKey(c2, c2c)
   261  	key3 := cacheKey(c3, c3c)
   262  	key4 := cacheKey(c4, c4c)
   263  	key5 := cacheKey(c5, c5c)
   264  	key6 := cacheKey(c6, nil)
   265  	key7 := cacheKey(c7, nil)
   266  	if key1 != key2 {
   267  		t.Error("key1 and key2 didn't match")
   268  	}
   269  	if key1 == key3 {
   270  		t.Error("key1 and key3 matched")
   271  	}
   272  	if key2 == key3 {
   273  		t.Error("key2 and key3 matched")
   274  	}
   275  	if key3 == key4 {
   276  		t.Error("key3 and key4 matched")
   277  	}
   278  	if key4 == key5 {
   279  		t.Error("key3 and key4 matched")
   280  	}
   281  	if key6 == key4 {
   282  		t.Error("key6 and key4 matched")
   283  	}
   284  	if key6 == key7 {
   285  		t.Error("key6 and key7 matched")
   286  	}
   287  }
   288  
   289  func compJSON(t *testing.T, got, want []byte) {
   290  	t.Helper()
   291  	gotJSON := &bytes.Buffer{}
   292  	wantJSON := &bytes.Buffer{}
   293  
   294  	if err := json.Indent(gotJSON, got, "", "  "); err != nil {
   295  		t.Errorf("got invalid JSON: %v", err)
   296  	}
   297  	if err := json.Indent(wantJSON, want, "", "  "); err != nil {
   298  		t.Errorf("want invalid JSON: %v", err)
   299  	}
   300  	g := strings.TrimSpace(gotJSON.String())
   301  	w := strings.TrimSpace(wantJSON.String())
   302  	if g != w {
   303  		t.Errorf("wanted %q, got %q", w, g)
   304  	}
   305  }
   306  
   307  func TestRefreshCreds(t *testing.T) {
   308  	tests := []struct {
   309  		name             string
   310  		config           api.ExecConfig
   311  		stdinUnavailable bool
   312  		exitCode         int
   313  		cluster          *clientauthentication.Cluster
   314  		output           string
   315  		isTerminal       bool
   316  		wantInput        string
   317  		wantCreds        credentials
   318  		wantExpiry       time.Time
   319  		wantErr          bool
   320  		wantErrSubstr    string
   321  	}{
   322  		{
   323  			name: "beta-with-TLS-credentials",
   324  			config: api.ExecConfig{
   325  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   326  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   327  			},
   328  			wantInput: `{
   329  				"kind":"ExecCredential",
   330  				"apiVersion":"client.authentication.k8s.io/v1beta1",
   331  				"spec": {
   332  					"interactive": false
   333  				}
   334  			}`,
   335  			output: fmt.Sprintf(`{
   336  				"kind": "ExecCredential",
   337  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   338  				"status": {
   339  					"clientKeyData": %q,
   340  					"clientCertificateData": %q
   341  				}
   342  			}`, keyData, certData),
   343  			wantCreds: credentials{cert: validCert},
   344  		},
   345  		{
   346  			name: "beta-with-bad-TLS-credentials",
   347  			config: api.ExecConfig{
   348  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   349  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   350  			},
   351  			output: `{
   352  				"kind": "ExecCredential",
   353  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   354  				"status": {
   355  					"clientKeyData": "foo",
   356  					"clientCertificateData": "bar"
   357  				}
   358  			}`,
   359  			wantErr: true,
   360  		},
   361  		{
   362  			name: "beta-cert-but-no-key",
   363  			config: api.ExecConfig{
   364  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   365  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   366  			},
   367  			output: fmt.Sprintf(`{
   368  				"kind": "ExecCredential",
   369  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   370  				"status": {
   371  					"clientCertificateData": %q
   372  				}
   373  			}`, certData),
   374  			wantErr: true,
   375  		},
   376  		{
   377  			name: "beta-basic-request",
   378  			config: api.ExecConfig{
   379  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   380  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   381  			},
   382  			wantInput: `{
   383  				"kind": "ExecCredential",
   384  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   385  				"spec": {
   386  					"interactive": false
   387  				}
   388  			}`,
   389  			output: `{
   390  				"kind": "ExecCredential",
   391  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   392  				"status": {
   393  					"token": "foo-bar"
   394  				}
   395  			}`,
   396  			wantCreds: credentials{token: "foo-bar"},
   397  		},
   398  		{
   399  			name: "beta-basic-request-with-never-interactive-mode",
   400  			config: api.ExecConfig{
   401  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   402  				InteractiveMode: api.NeverExecInteractiveMode,
   403  			},
   404  			wantInput: `{
   405  				"kind": "ExecCredential",
   406  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   407  				"spec": {
   408  					"interactive": false
   409  				}
   410  			}`,
   411  			output: `{
   412  				"kind": "ExecCredential",
   413  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   414  				"status": {
   415  					"token": "foo-bar"
   416  				}
   417  			}`,
   418  			wantCreds: credentials{token: "foo-bar"},
   419  		},
   420  		{
   421  			name: "beta-basic-request-with-never-interactive-mode-and-stdin-unavailable",
   422  			config: api.ExecConfig{
   423  				APIVersion:       "client.authentication.k8s.io/v1beta1",
   424  				InteractiveMode:  api.NeverExecInteractiveMode,
   425  				StdinUnavailable: true,
   426  			},
   427  			wantInput: `{
   428  				"kind": "ExecCredential",
   429  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   430  				"spec": {
   431  					"interactive": false
   432  				}
   433  			}`,
   434  			output: `{
   435  				"kind": "ExecCredential",
   436  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   437  				"status": {
   438  					"token": "foo-bar"
   439  				}
   440  			}`,
   441  			wantCreds: credentials{token: "foo-bar"},
   442  		},
   443  		{
   444  			name: "beta-basic-request-with-if-available-interactive-mode",
   445  			config: api.ExecConfig{
   446  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   447  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   448  			},
   449  			wantInput: `{
   450  				"kind": "ExecCredential",
   451  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   452  				"spec": {
   453  					"interactive": false
   454  				}
   455  			}`,
   456  			output: `{
   457  				"kind": "ExecCredential",
   458  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   459  				"status": {
   460  					"token": "foo-bar"
   461  				}
   462  			}`,
   463  			wantCreds: credentials{token: "foo-bar"},
   464  		},
   465  		{
   466  			name: "beta-basic-request-with-if-available-interactive-mode-and-stdin-unavailable",
   467  			config: api.ExecConfig{
   468  				APIVersion:       "client.authentication.k8s.io/v1beta1",
   469  				InteractiveMode:  api.IfAvailableExecInteractiveMode,
   470  				StdinUnavailable: true,
   471  			},
   472  			wantInput: `{
   473  				"kind": "ExecCredential",
   474  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   475  				"spec": {
   476  					"interactive": false
   477  				}
   478  			}`,
   479  			output: `{
   480  				"kind": "ExecCredential",
   481  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   482  				"status": {
   483  					"token": "foo-bar"
   484  				}
   485  			}`,
   486  			wantCreds: credentials{token: "foo-bar"},
   487  		},
   488  		{
   489  			name: "beta-basic-request-with-if-available-interactive-mode-and-terminal",
   490  			config: api.ExecConfig{
   491  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   492  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   493  			},
   494  			isTerminal: true,
   495  			wantInput: `{
   496  				"kind": "ExecCredential",
   497  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   498  				"spec": {
   499  					"interactive": true
   500  				}
   501  			}`,
   502  			output: `{
   503  				"kind": "ExecCredential",
   504  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   505  				"status": {
   506  					"token": "foo-bar"
   507  				}
   508  			}`,
   509  			wantCreds: credentials{token: "foo-bar"},
   510  		},
   511  		{
   512  			name: "beta-basic-request-with-if-available-interactive-mode-and-terminal-and-stdin-unavailable",
   513  			config: api.ExecConfig{
   514  				APIVersion:       "client.authentication.k8s.io/v1beta1",
   515  				InteractiveMode:  api.IfAvailableExecInteractiveMode,
   516  				StdinUnavailable: true,
   517  			},
   518  			isTerminal: true,
   519  			wantInput: `{
   520  				"kind": "ExecCredential",
   521  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   522  				"spec": {
   523  					"interactive": false
   524  				}
   525  			}`,
   526  			output: `{
   527  				"kind": "ExecCredential",
   528  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   529  				"status": {
   530  					"token": "foo-bar"
   531  				}
   532  			}`,
   533  			wantCreds: credentials{token: "foo-bar"},
   534  		},
   535  		{
   536  			name: "beta-basic-request-with-always-interactive-mode",
   537  			config: api.ExecConfig{
   538  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   539  				InteractiveMode: api.AlwaysExecInteractiveMode,
   540  			},
   541  			wantErr:       true,
   542  			wantErrSubstr: "exec plugin cannot support interactive mode: standard input is not a terminal",
   543  		},
   544  		{
   545  			name: "beta-basic-request-with-always-interactive-mode-and-terminal-and-stdin-unavailable",
   546  			config: api.ExecConfig{
   547  				APIVersion:       "client.authentication.k8s.io/v1beta1",
   548  				InteractiveMode:  api.AlwaysExecInteractiveMode,
   549  				StdinUnavailable: true,
   550  			},
   551  			isTerminal:    true,
   552  			wantErr:       true,
   553  			wantErrSubstr: "exec plugin cannot support interactive mode: standard input is unavailable",
   554  		},
   555  		{
   556  			name: "beta-basic-request-with-always-interactive-mode-and-terminal-and-stdin-unavailable-with-message",
   557  			config: api.ExecConfig{
   558  				APIVersion:              "client.authentication.k8s.io/v1beta1",
   559  				InteractiveMode:         api.AlwaysExecInteractiveMode,
   560  				StdinUnavailable:        true,
   561  				StdinUnavailableMessage: "some message",
   562  			},
   563  			isTerminal:    true,
   564  			wantErr:       true,
   565  			wantErrSubstr: "exec plugin cannot support interactive mode: standard input is unavailable: some message",
   566  		},
   567  		{
   568  			name: "beta-basic-request-with-always-interactive-mode-and-terminal",
   569  			config: api.ExecConfig{
   570  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   571  				InteractiveMode: api.AlwaysExecInteractiveMode,
   572  			},
   573  			isTerminal: true,
   574  			wantInput: `{
   575  				"kind": "ExecCredential",
   576  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   577  				"spec": {
   578  					"interactive": true
   579  				}
   580  			}`,
   581  			output: `{
   582  				"kind": "ExecCredential",
   583  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   584  				"status": {
   585  					"token": "foo-bar"
   586  				}
   587  			}`,
   588  			wantCreds: credentials{token: "foo-bar"},
   589  		},
   590  		{
   591  			name: "beta-expiry",
   592  			config: api.ExecConfig{
   593  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   594  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   595  			},
   596  			wantInput: `{
   597  				"kind": "ExecCredential",
   598  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   599  				"spec": {
   600  					"interactive": false
   601  				}
   602  			}`,
   603  			output: `{
   604  				"kind": "ExecCredential",
   605  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   606  				"status": {
   607  					"token": "foo-bar",
   608  					"expirationTimestamp": "2006-01-02T15:04:05Z"
   609  				}
   610  			}`,
   611  			wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC),
   612  			wantCreds:  credentials{token: "foo-bar"},
   613  		},
   614  		{
   615  			name: "beta-no-group-version",
   616  			config: api.ExecConfig{
   617  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   618  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   619  			},
   620  			output: `{
   621  				"kind": "ExecCredential",
   622  				"status": {
   623  					"token": "foo-bar"
   624  				}
   625  			}`,
   626  			wantErr: true,
   627  		},
   628  		{
   629  			name: "beta-no-status",
   630  			config: api.ExecConfig{
   631  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   632  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   633  			},
   634  			output: `{
   635  				"kind": "ExecCredential",
   636  				"apiVersion":"client.authentication.k8s.io/v1beta1"
   637  			}`,
   638  			wantErr: true,
   639  		},
   640  		{
   641  			name: "beta-no-token",
   642  			config: api.ExecConfig{
   643  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   644  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   645  			},
   646  			output: `{
   647  				"kind": "ExecCredential",
   648  				"apiVersion":"client.authentication.k8s.io/v1beta1",
   649  				"status": {}
   650  			}`,
   651  			wantErr: true,
   652  		},
   653  		{
   654  			name: "unknown-binary",
   655  			config: api.ExecConfig{
   656  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   657  				Command:         "does not exist",
   658  				InstallHint:     "some install hint",
   659  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   660  			},
   661  			wantErr:       true,
   662  			wantErrSubstr: "some install hint",
   663  		},
   664  		{
   665  			name: "binary-fails",
   666  			config: api.ExecConfig{
   667  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   668  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   669  			},
   670  			exitCode:      73,
   671  			wantErr:       true,
   672  			wantErrSubstr: "73",
   673  		},
   674  		{
   675  			name: "beta-with-cluster-and-provide-cluster-info-is-serialized",
   676  			config: api.ExecConfig{
   677  				APIVersion:         "client.authentication.k8s.io/v1beta1",
   678  				ProvideClusterInfo: true,
   679  				InteractiveMode:    api.IfAvailableExecInteractiveMode,
   680  			},
   681  			cluster: &clientauthentication.Cluster{
   682  				Server:                   "foo",
   683  				TLSServerName:            "bar",
   684  				CertificateAuthorityData: []byte("baz"),
   685  				Config: &runtime.Unknown{
   686  					TypeMeta: runtime.TypeMeta{
   687  						APIVersion: "",
   688  						Kind:       "",
   689  					},
   690  					Raw:             []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`),
   691  					ContentEncoding: "",
   692  					ContentType:     "application/json",
   693  				},
   694  			},
   695  			wantInput: `{
   696  				"kind":"ExecCredential",
   697  				"apiVersion":"client.authentication.k8s.io/v1beta1",
   698  				"spec": {
   699  					"cluster": {
   700  						"server": "foo",
   701  						"tls-server-name": "bar",
   702  						"certificate-authority-data": "YmF6",
   703  						"config": {
   704  							"apiVersion": "group/v1",
   705  							"kind": "PluginConfig",
   706  							"spec": {
   707  								"audience": "snorlax"
   708  							}
   709  						}
   710  					},
   711  					"interactive": false
   712  				}
   713  			}`,
   714  			output: `{
   715  				"kind": "ExecCredential",
   716  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   717  				"status": {
   718  					"token": "foo-bar"
   719  				}
   720  			}`,
   721  			wantCreds: credentials{token: "foo-bar"},
   722  		},
   723  		{
   724  			name: "beta-with-cluster-and-without-provide-cluster-info-is-not-serialized",
   725  			config: api.ExecConfig{
   726  				APIVersion:      "client.authentication.k8s.io/v1beta1",
   727  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   728  			},
   729  			cluster: &clientauthentication.Cluster{
   730  				Server:                   "foo",
   731  				TLSServerName:            "bar",
   732  				CertificateAuthorityData: []byte("baz"),
   733  				Config: &runtime.Unknown{
   734  					TypeMeta: runtime.TypeMeta{
   735  						APIVersion: "",
   736  						Kind:       "",
   737  					},
   738  					Raw:             []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`),
   739  					ContentEncoding: "",
   740  					ContentType:     "application/json",
   741  				},
   742  			},
   743  			wantInput: `{
   744  				"kind":"ExecCredential",
   745  				"apiVersion":"client.authentication.k8s.io/v1beta1",
   746  				"spec": {
   747  					"interactive": false
   748  				}
   749  			}`,
   750  			output: `{
   751  				"kind": "ExecCredential",
   752  				"apiVersion": "client.authentication.k8s.io/v1beta1",
   753  				"status": {
   754  					"token": "foo-bar"
   755  				}
   756  			}`,
   757  			wantCreds: credentials{token: "foo-bar"},
   758  		},
   759  		{
   760  			name: "v1-basic-request",
   761  			config: api.ExecConfig{
   762  				APIVersion:      "client.authentication.k8s.io/v1",
   763  				InteractiveMode: api.IfAvailableExecInteractiveMode,
   764  			},
   765  			wantInput: `{
   766  				"kind": "ExecCredential",
   767  				"apiVersion": "client.authentication.k8s.io/v1",
   768  				"spec": {
   769  					"interactive": false
   770  				}
   771  			}`,
   772  			output: `{
   773  				"kind": "ExecCredential",
   774  				"apiVersion": "client.authentication.k8s.io/v1",
   775  				"status": {
   776  					"token": "foo-bar"
   777  				}
   778  			}`,
   779  			wantCreds: credentials{token: "foo-bar"},
   780  		},
   781  		{
   782  			name: "v1-with-missing-interactive-mode",
   783  			config: api.ExecConfig{
   784  				APIVersion: "client.authentication.k8s.io/v1",
   785  			},
   786  			wantErr:       true,
   787  			wantErrSubstr: `exec plugin cannot support interactive mode: unknown interactiveMode: ""`,
   788  		},
   789  	}
   790  
   791  	for _, test := range tests {
   792  		t.Run(test.name, func(t *testing.T) {
   793  			c := test.config
   794  
   795  			if c.Command == "" {
   796  				c.Command = "./testdata/test-plugin.sh"
   797  				c.Env = append(c.Env, api.ExecEnvVar{
   798  					Name:  "TEST_OUTPUT",
   799  					Value: test.output,
   800  				})
   801  				c.Env = append(c.Env, api.ExecEnvVar{
   802  					Name:  "TEST_EXIT_CODE",
   803  					Value: strconv.Itoa(test.exitCode),
   804  				})
   805  			}
   806  
   807  			a, err := newAuthenticator(newCache(), func(_ int) bool { return test.isTerminal }, &c, test.cluster)
   808  			if err != nil {
   809  				t.Fatal(err)
   810  			}
   811  
   812  			stderr := &bytes.Buffer{}
   813  			a.stderr = stderr
   814  			a.environ = func() []string { return nil }
   815  
   816  			if err := a.refreshCredsLocked(); err != nil {
   817  				if !test.wantErr {
   818  					t.Errorf("get token %v", err)
   819  				} else if !strings.Contains(err.Error(), test.wantErrSubstr) {
   820  					t.Errorf("expected error with substring '%v' got '%v'", test.wantErrSubstr, err.Error())
   821  				}
   822  				return
   823  			}
   824  			if test.wantErr {
   825  				t.Fatal("expected error getting token")
   826  			}
   827  
   828  			if !reflect.DeepEqual(a.cachedCreds, &test.wantCreds) {
   829  				t.Errorf("expected credentials %+v got %+v", &test.wantCreds, a.cachedCreds)
   830  			}
   831  
   832  			if !a.exp.Equal(test.wantExpiry) {
   833  				t.Errorf("expected expiry %v got %v", test.wantExpiry, a.exp)
   834  			}
   835  
   836  			if test.wantInput == "" {
   837  				if got := strings.TrimSpace(stderr.String()); got != "" {
   838  					t.Errorf("expected no input parameters, got %q", got)
   839  				}
   840  				return
   841  			}
   842  
   843  			compJSON(t, stderr.Bytes(), []byte(test.wantInput))
   844  		})
   845  	}
   846  }
   847  
   848  func TestRoundTripper(t *testing.T) {
   849  	wantToken := ""
   850  
   851  	n := time.Now()
   852  	now := func() time.Time { return n }
   853  
   854  	env := []string{""}
   855  	environ := func() []string {
   856  		s := make([]string, len(env))
   857  		copy(s, env)
   858  		return s
   859  	}
   860  
   861  	setOutput := func(s string) {
   862  		env[0] = "TEST_OUTPUT=" + s
   863  	}
   864  
   865  	handler := func(w http.ResponseWriter, r *http.Request) {
   866  		gotToken := ""
   867  		parts := strings.Split(r.Header.Get("Authorization"), " ")
   868  		if len(parts) > 1 && strings.EqualFold(parts[0], "bearer") {
   869  			gotToken = parts[1]
   870  		}
   871  
   872  		if wantToken != gotToken {
   873  			http.Error(w, "Unauthorized", http.StatusUnauthorized)
   874  			return
   875  		}
   876  		fmt.Fprintln(w, "ok")
   877  	}
   878  	server := httptest.NewServer(http.HandlerFunc(handler))
   879  
   880  	c := api.ExecConfig{
   881  		Command:         "./testdata/test-plugin.sh",
   882  		APIVersion:      "client.authentication.k8s.io/v1beta1",
   883  		InteractiveMode: api.IfAvailableExecInteractiveMode,
   884  	}
   885  	a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil)
   886  	if err != nil {
   887  		t.Fatal(err)
   888  	}
   889  	a.environ = environ
   890  	a.now = now
   891  	a.stderr = io.Discard
   892  
   893  	tc := &transport.Config{}
   894  	if err := a.UpdateTransportConfig(tc); err != nil {
   895  		t.Fatal(err)
   896  	}
   897  	client := http.Client{
   898  		Transport: tc.WrapTransport(http.DefaultTransport),
   899  	}
   900  
   901  	get := func(t *testing.T, statusCode int) {
   902  		t.Helper()
   903  		resp, err := client.Get(server.URL)
   904  		if err != nil {
   905  			t.Fatal(err)
   906  		}
   907  		defer resp.Body.Close()
   908  		if resp.StatusCode != statusCode {
   909  			t.Errorf("wanted status %d got %d", statusCode, resp.StatusCode)
   910  		}
   911  	}
   912  
   913  	setOutput(`{
   914  		"kind": "ExecCredential",
   915  		"apiVersion": "client.authentication.k8s.io/v1beta1",
   916  		"status": {
   917  			"token": "token1"
   918  		}
   919  	}`)
   920  	wantToken = "token1"
   921  	get(t, http.StatusOK)
   922  
   923  	setOutput(`{
   924  		"kind": "ExecCredential",
   925  		"apiVersion": "client.authentication.k8s.io/v1beta1",
   926  		"status": {
   927  			"token": "token2"
   928  		}
   929  	}`)
   930  	// Previous token should be cached
   931  	get(t, http.StatusOK)
   932  
   933  	wantToken = "token2"
   934  	// Token is still cached, hits unauthorized but causes token to rotate.
   935  	get(t, http.StatusUnauthorized)
   936  	// Follow up request uses the rotated token.
   937  	get(t, http.StatusOK)
   938  
   939  	setOutput(`{
   940  		"kind": "ExecCredential",
   941  		"apiVersion": "client.authentication.k8s.io/v1beta1",
   942  		"status": {
   943  			"token": "token3",
   944  			"expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `"
   945  		}
   946  	}`)
   947  	wantToken = "token3"
   948  	// Token is still cached, hit's unauthorized but causes rotation to token with an expiry.
   949  	get(t, http.StatusUnauthorized)
   950  	get(t, http.StatusOK)
   951  
   952  	// Move time forward 2 hours, "token3" is now expired.
   953  	n = n.Add(time.Hour * 2)
   954  	setOutput(`{
   955  		"kind": "ExecCredential",
   956  		"apiVersion": "client.authentication.k8s.io/v1beta1",
   957  		"status": {
   958  			"token": "token4",
   959  			"expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `"
   960  		}
   961  	}`)
   962  	wantToken = "token4"
   963  	// Old token is expired, should refresh automatically without hitting a 401.
   964  	get(t, http.StatusOK)
   965  }
   966  
   967  func TestAuthorizationHeaderPresentCancelsExecAction(t *testing.T) {
   968  	tests := []struct {
   969  		name               string
   970  		setTransportConfig func(*transport.Config)
   971  	}{
   972  		{
   973  			name: "bearer token",
   974  			setTransportConfig: func(config *transport.Config) {
   975  				config.BearerToken = "token1f"
   976  			},
   977  		},
   978  		{
   979  			name: "basic auth",
   980  			setTransportConfig: func(config *transport.Config) {
   981  				config.Username = "marshmallow"
   982  				config.Password = "zelda"
   983  			},
   984  		},
   985  		{
   986  			name: "cert auth",
   987  			setTransportConfig: func(config *transport.Config) {
   988  				config.TLS.CertData = []byte("some-cert-data")
   989  				config.TLS.KeyData = []byte("some-key-data")
   990  			},
   991  		},
   992  	}
   993  	for _, test := range tests {
   994  		t.Run(test.name, func(t *testing.T) {
   995  			a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &api.ExecConfig{
   996  				Command:    "./testdata/test-plugin.sh",
   997  				APIVersion: "client.authentication.k8s.io/v1beta1",
   998  			}, nil)
   999  			if err != nil {
  1000  				t.Fatal(err)
  1001  			}
  1002  
  1003  			// UpdateTransportConfig returns error on existing TLS certificate callback, unless a bearer token is present in the
  1004  			// transport config, in which case it takes precedence
  1005  			cert := func() (*tls.Certificate, error) {
  1006  				return nil, nil
  1007  			}
  1008  			tc := &transport.Config{TLS: transport.TLSConfig{Insecure: true, GetCertHolder: &transport.GetCertHolder{GetCert: cert}}}
  1009  			test.setTransportConfig(tc)
  1010  
  1011  			if err := a.UpdateTransportConfig(tc); err != nil {
  1012  				t.Error("Expected presence of bearer token in config to cancel exec action")
  1013  			}
  1014  		})
  1015  	}
  1016  }
  1017  
  1018  func TestTLSCredentials(t *testing.T) {
  1019  	now := time.Now()
  1020  
  1021  	certPool := x509.NewCertPool()
  1022  	cert, key := genClientCert(t)
  1023  	if !certPool.AppendCertsFromPEM(cert) {
  1024  		t.Fatal("failed to add client cert to CertPool")
  1025  	}
  1026  
  1027  	server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1028  		fmt.Fprintln(w, "ok")
  1029  	}))
  1030  	server.TLS = &tls.Config{
  1031  		ClientAuth: tls.RequireAndVerifyClientCert,
  1032  		ClientCAs:  certPool,
  1033  	}
  1034  	server.StartTLS()
  1035  	defer server.Close()
  1036  
  1037  	a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &api.ExecConfig{
  1038  		Command:         "./testdata/test-plugin.sh",
  1039  		APIVersion:      "client.authentication.k8s.io/v1beta1",
  1040  		InteractiveMode: api.IfAvailableExecInteractiveMode,
  1041  	}, nil)
  1042  	if err != nil {
  1043  		t.Fatal(err)
  1044  	}
  1045  	var output *clientauthentication.ExecCredential
  1046  	a.environ = func() []string {
  1047  		data, err := runtime.Encode(codecs.LegacyCodec(a.group), output)
  1048  		if err != nil {
  1049  			t.Fatal(err)
  1050  		}
  1051  		return []string{"TEST_OUTPUT=" + string(data)}
  1052  	}
  1053  	a.now = func() time.Time { return now }
  1054  	a.stderr = io.Discard
  1055  
  1056  	// We're not interested in server's cert, this test is about client cert.
  1057  	tc := &transport.Config{TLS: transport.TLSConfig{Insecure: true}}
  1058  	if err := a.UpdateTransportConfig(tc); err != nil {
  1059  		t.Fatal(err)
  1060  	}
  1061  
  1062  	get := func(t *testing.T, desc string, wantErr bool) {
  1063  		t.Run(desc, func(t *testing.T) {
  1064  			tlsCfg, err := transport.TLSConfigFor(tc)
  1065  			if err != nil {
  1066  				t.Fatal("TLSConfigFor:", err)
  1067  			}
  1068  			client := http.Client{
  1069  				Transport: &http.Transport{TLSClientConfig: tlsCfg},
  1070  			}
  1071  			resp, err := client.Get(server.URL)
  1072  			switch {
  1073  			case err != nil && !wantErr:
  1074  				t.Errorf("got client.Get error: %q, want nil", err)
  1075  			case err == nil && wantErr:
  1076  				t.Error("got nil client.Get error, want non-nil")
  1077  			}
  1078  			if err == nil {
  1079  				resp.Body.Close()
  1080  			}
  1081  		})
  1082  	}
  1083  
  1084  	output = &clientauthentication.ExecCredential{
  1085  		Status: &clientauthentication.ExecCredentialStatus{
  1086  			ClientCertificateData: string(cert),
  1087  			ClientKeyData:         string(key),
  1088  			ExpirationTimestamp:   &v1.Time{Time: now.Add(time.Hour)},
  1089  		},
  1090  	}
  1091  	get(t, "valid TLS cert", false)
  1092  
  1093  	// Advance time to force re-exec.
  1094  	nCert, nKey := genClientCert(t)
  1095  	now = now.Add(time.Hour * 2)
  1096  	output = &clientauthentication.ExecCredential{
  1097  		Status: &clientauthentication.ExecCredentialStatus{
  1098  			ClientCertificateData: string(nCert),
  1099  			ClientKeyData:         string(nKey),
  1100  			ExpirationTimestamp:   &v1.Time{Time: now.Add(time.Hour)},
  1101  		},
  1102  	}
  1103  	get(t, "untrusted TLS cert", true)
  1104  
  1105  	now = now.Add(time.Hour * 2)
  1106  	output = &clientauthentication.ExecCredential{
  1107  		Status: &clientauthentication.ExecCredentialStatus{
  1108  			ClientCertificateData: string(cert),
  1109  			ClientKeyData:         string(key),
  1110  			ExpirationTimestamp:   &v1.Time{Time: now.Add(time.Hour)},
  1111  		},
  1112  	}
  1113  	get(t, "valid TLS cert again", false)
  1114  }
  1115  
  1116  func TestConcurrentUpdateTransportConfig(t *testing.T) {
  1117  	n := time.Now()
  1118  	now := func() time.Time { return n }
  1119  
  1120  	env := []string{""}
  1121  	environ := func() []string {
  1122  		s := make([]string, len(env))
  1123  		copy(s, env)
  1124  		return s
  1125  	}
  1126  
  1127  	c := api.ExecConfig{
  1128  		Command:    "./testdata/test-plugin.sh",
  1129  		APIVersion: "client.authentication.k8s.io/v1beta1",
  1130  	}
  1131  	a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil)
  1132  	if err != nil {
  1133  		t.Fatal(err)
  1134  	}
  1135  	a.environ = environ
  1136  	a.now = now
  1137  	a.stderr = io.Discard
  1138  
  1139  	stopCh := make(chan struct{})
  1140  	defer close(stopCh)
  1141  
  1142  	numConcurrent := 2
  1143  
  1144  	for i := 0; i < numConcurrent; i++ {
  1145  		go func() {
  1146  			for {
  1147  				tc := &transport.Config{}
  1148  				a.UpdateTransportConfig(tc)
  1149  
  1150  				select {
  1151  				case <-stopCh:
  1152  					return
  1153  				default:
  1154  					continue
  1155  				}
  1156  			}
  1157  		}()
  1158  	}
  1159  	time.Sleep(2 * time.Second)
  1160  }
  1161  
  1162  func TestInstallHintRateLimit(t *testing.T) {
  1163  	tests := []struct {
  1164  		name string
  1165  
  1166  		threshold int
  1167  		interval  time.Duration
  1168  
  1169  		calls          int
  1170  		perCallAdvance time.Duration
  1171  
  1172  		wantInstallHint int
  1173  	}{
  1174  		{
  1175  			name:            "print-up-to-threshold",
  1176  			threshold:       2,
  1177  			interval:        time.Second,
  1178  			calls:           10,
  1179  			wantInstallHint: 2,
  1180  		},
  1181  		{
  1182  			name:            "after-interval-threshold-resets",
  1183  			threshold:       2,
  1184  			interval:        time.Second * 5,
  1185  			calls:           10,
  1186  			perCallAdvance:  time.Second,
  1187  			wantInstallHint: 4,
  1188  		},
  1189  	}
  1190  
  1191  	for _, test := range tests {
  1192  		t.Run(test.name, func(t *testing.T) {
  1193  			c := api.ExecConfig{
  1194  				Command:         "does not exist",
  1195  				APIVersion:      "client.authentication.k8s.io/v1beta1",
  1196  				InstallHint:     "some install hint",
  1197  				InteractiveMode: api.IfAvailableExecInteractiveMode,
  1198  			}
  1199  			a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil)
  1200  			if err != nil {
  1201  				t.Fatal(err)
  1202  			}
  1203  
  1204  			a.sometimes.threshold = test.threshold
  1205  			a.sometimes.interval = test.interval
  1206  
  1207  			clock := testingclock.NewFakeClock(time.Now())
  1208  			a.sometimes.clock = clock
  1209  
  1210  			count := 0
  1211  			for i := 0; i < test.calls; i++ {
  1212  				err := a.refreshCredsLocked()
  1213  				if strings.Contains(err.Error(), c.InstallHint) {
  1214  					count++
  1215  				}
  1216  
  1217  				clock.SetTime(clock.Now().Add(test.perCallAdvance))
  1218  			}
  1219  
  1220  			if test.wantInstallHint != count {
  1221  				t.Errorf(
  1222  					"%s: expected install hint %d times got %d",
  1223  					test.name,
  1224  					test.wantInstallHint,
  1225  					count,
  1226  				)
  1227  			}
  1228  		})
  1229  	}
  1230  }
  1231  
  1232  // genClientCert generates an x509 certificate for testing. Certificate and key
  1233  // are returned in PEM encoding. The generated cert expires in 24 hours.
  1234  func genClientCert(t *testing.T) ([]byte, []byte) {
  1235  	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  1236  	if err != nil {
  1237  		t.Fatal(err)
  1238  	}
  1239  	keyRaw, err := x509.MarshalECPrivateKey(key)
  1240  	if err != nil {
  1241  		t.Fatal(err)
  1242  	}
  1243  	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
  1244  	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
  1245  	if err != nil {
  1246  		t.Fatal(err)
  1247  	}
  1248  	cert := &x509.Certificate{
  1249  		SerialNumber: serialNumber,
  1250  		Subject:      pkix.Name{Organization: []string{"Acme Co"}},
  1251  		NotBefore:    time.Now(),
  1252  		NotAfter:     time.Now().Add(24 * time.Hour),
  1253  
  1254  		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
  1255  		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
  1256  		BasicConstraintsValid: true,
  1257  	}
  1258  	certRaw, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key)
  1259  	if err != nil {
  1260  		t.Fatal(err)
  1261  	}
  1262  	return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}),
  1263  		pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyRaw})
  1264  }