github.com/wanddynosios/cli/v8@v8.7.9-0.20240221182337-1a92e3a7017f/api/shared/wrap_for_cf_on_k8s_test.go (about) 1 package shared_test 2 3 import ( 4 "encoding/base64" 5 "encoding/json" 6 "encoding/pem" 7 "errors" 8 "io/ioutil" 9 "net/http" 10 "os" 11 "strings" 12 "time" 13 14 "code.cloudfoundry.org/cli/actor/v7action/v7actionfakes" 15 "code.cloudfoundry.org/cli/api/shared" 16 "code.cloudfoundry.org/cli/api/shared/sharedfakes" 17 "code.cloudfoundry.org/cli/command/commandfakes" 18 "code.cloudfoundry.org/cli/integration/helpers" 19 20 "github.com/SermoDigital/jose/crypto" 21 "github.com/SermoDigital/jose/jws" 22 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" 24 "k8s.io/client-go/tools/clientcmd/api" 25 26 . "github.com/onsi/ginkgo" 27 . "github.com/onsi/gomega" 28 ) 29 30 const ( 31 clientCertData = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJVENDQWdtZ0F3SUJBZ0lJVk9iMUFIckxNUjh3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TVRFd01EVXhOVEExTURsYUZ3MHlNakV3TURVeE5UQTFNVEZhTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXJrdWxLbS9qTTJhZWZsdjkKK00zQk9Jc2QvVXZrRTBONGhWb3hSeWRBbE0xQXhWd3REYUdzL3dmUzRzb0xuNHJENTF3UE1SRlNJaitwSzdGYQprRGdaR0x4UFhrai96UkZOTzcvU3J2RHYwVGxjYjJENzNCS21qaXArQ2hBWkpQdWhMQlY2VnlTN0pXSWhOM1lOCktyamR5TnB5MHN3SjI1TW9CbW1saUpFc3V2dCtDaEhseERqWE9KenF1U2owa1hPQVVsWUFTN1dKK09JMU9HbzQKUjcvdHdHZlFTNW9oYXpRVVlDR2lZSllYcjVRNkVKTmJOVVI0RjdpRSthY1I5Rm9GNnNKSmkrQStET1VDUFFSKwptbjQ5Zm1pcFVHSGtMc3BicTNFZ0FEME40VW5jcmIyeUJEMFNVTmdLQmJjclY1S2hybFA2SzkwNkY5NEpubzNHCm1Id1JwUUlEQVFBQm8xWXdWREFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RBWURWUjBUQVFIL0JBSXdBREFmQmdOVkhTTUVHREFXZ0JUV2VNZ1ZBRkRhbWcraDRqS3hoRUh2Q1l5egp5akFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBUUxMWWFXQTRva1M2b3ZEWjQ1Z28ybkVZdUR4MklmRXZwYnh3CkNmYkFTNDY4M3lLT3FiYVBHOEpTVGhSbkh3TWlMcVBrbGFsdkJvV2R3aFB5Vkk0d2tVNHI4Y2c0UEpxNEZwWnQKVkNUQzFPZWVwRGpTMFpVQjRwSDVIZVlNQUxqSDBqcFV3RU96djFpaEtid05IMHFoZ2pGeUNTdld5TG9oZHdzbApJWXIvV1NEZm50NlBETC84TjFpcEJJbEN5Z1JHVGdoSFhPemhHUklPWG4rYWVOR29yWm9YWm0xbHErc1hyUnc5CktNdVZhRmdhaWVjSm0vbytyemFFSG9VZjRYOERKeVNubmVTa3ViaEx6ZERNc2o5eEs1cEJpdFgvaDlQMUQrMkcKeW5rcWdJVTJSWTM0SjBRcnU4Z0syNlJVT2pOcHIvRWJHQ0dUQUxiMXJnSDM0K2NFdlE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" 32 clientKeyData = "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQ3VTNlVxYitNelpwNSsKVy8zNHpjRTRpeDM5UytRVFEzaUZXakZISjBDVXpVREZYQzBOb2F6L0I5TGl5Z3VmaXNQblhBOHhFVklpUDZrcgpzVnFRT0JrWXZFOWVTUC9ORVUwN3Y5S3U4Ty9ST1Z4dllQdmNFcWFPS240S0VCa2srNkVzRlhwWEpMc2xZaUUzCmRnMHF1TjNJMm5MU3pBbmJreWdHYWFXSWtTeTYrMzRLRWVYRU9OYzRuT3E1S1BTUmM0QlNWZ0JMdFluNDRqVTQKYWpoSHYrM0FaOUJMbWlGck5CUmdJYUpnbGhldmxEb1FrMXMxUkhnWHVJVDVweEgwV2dYcXdrbUw0RDRNNVFJOQpCSDZhZmoxK2FLbFFZZVF1eWx1cmNTQUFQUTNoU2R5dHZiSUVQUkpRMkFvRnR5dFhrcUd1VS9vcjNUb1gzZ21lCmpjYVlmQkdsQWdNQkFBRUNnZ0VBZG80WndLM3VteTM0TFBjaDM3VUU4eE1keVFkd0VmSlk3a3dWTE5MMFNNTDgKaGNKWEd1aVlKYmtLcHh6TG55L2laV0xuS25jZnFSQW9ZQUg1R2hRdWJmYlkvY2NseURVMmxhZTdCU2Y1MkJUdQpYUXhaQks3aS85ekRjdERVYWFXSFVkY2lLbGhmdStQdHVDM2ljdWJnWlJqQjljUzRCOVVtNm9XK0JSREtuandICkduQ0lEZlNNQWt4VXdTaUwwa2NXelNpZ1BYMVN3UHcxOEZvZWgzTmJEd1VXTHhxUWZLVThydVlSTUsxYUg5M3cKcjFtbjlDWUwvd0hiVWRqcmtZMlIxTjVUR21ab2Vldm5qUXgyQVc2NkYzdEg1cGg4RTF6TEFQVTl4TFdRTW9KcwpXM0gzSTdUaEYvRnJuNERQa3hQbThUUVVhQUdvQ09SSWFUQkN5VlgxQVFLQmdRREkxbkRmNWYySHdHaldrTStpCk9YbGE1R1VnRUtXaGZpeGhidE5OclNpMDU5VnZQUEJwNXdtbGQzMHJKUDhWem8vbnFnUW5ISmpmaEQ2Y3NSMTQKL2VlMHZ1Um0zYzZwZzMrODdwOHhWY3lLNHhDd0JmdFFuMGRZWWFLMkRMOEtYb0liYThpN09EQmFoNW5OZWQwcgpKa1RPcE5NRGRkL0p0bEpPZ25jRXBlUk9oUUtCZ1FEZUt1L0R1MXU1QVR3Y3p5STRXOWV1L1YwTXRwMHdqM3RpClF2MmpObW83QU1zS3BwK0ZKVDFqWFhUKzZCTm02OWpxUVJwdlAyd2RhVUdqV1dLa1lHVEVpbUZCc2ZuKzJDOFAKOEc3Uk50YWpRdEV2QlR1ZDZPN0tZUkFoTU56dm9RcDkrZmJKY1ZsRG13Nlk0bUYzUTJXS3NmZU51TGtpY3VqNQpYVFV1ekVMd29RS0JnUUREU0IvQTFYVEx4cjhwd3V6aHBGam5sQ1R3Skwrb1kzTHIya01EeUZkSWNCUU1jWWlpCnNNK2tZS2NJaUpTdnM0WWhrQ014bEpEZzVVbXNPbHVhQmVpQ3l3cHpLMEdEZWlWK285ZU90UXFLRVhkc2NLU0oKSkJiUFRVQlZHOWUyVVdiWkd0aTNrazhSOThBSkYzR0NQMWV3Um53WFpVb1FiSU5qYTJBbTJOZEJzUUtCZ0Q4eApOVXVXTWl1NE56SDJsTVExRTI4NXI4cmE4bkVLanN6UFF6ZTJWWmI4emNQMHl2RGpPOGZVb0YrVkFWZklBOFgxCnlLQVdDUm1BZytRRG03UW5tdUh3Zm1OaVRUcDRvVUpHWUM3d0N6TWE0VWNmbE9xQWc5TmFzbXpPYWpsYXRCSkwKRkRBT0pwYTlOdlN6aDRlVnl2OGRTYzJzMmpQN1BWc1ljUFVqc25LaEFvR0JBSy9kQjlnVEFpME5nczVmaVNtWQovWkp3Yk52MjcyTHdKbWV4Vit2eWtjN3J5LzRraTRQb2xRd1BHNzQ5eFZ0T2NNc2FhRlVNMVVkclN2NlIwbjlkCmpTbXhCeTl2YWdzc1FmVDNSc3BvUUJKM0w5YWxiNHM2V2ZtUEpzNkFrQkhIZHNpVXFaaElYT2J2WE1lQ0k2aVMKOTQ2R0toekFxMlVGbjhFUGxXaFVNeEFiCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K" 33 ) 34 35 var _ = Describe("WrapForCFOnK8sAuth", func() { 36 var ( 37 config *commandfakes.FakeConfig 38 k8sConfigGetter *v7actionfakes.FakeKubernetesConfigGetter 39 req *http.Request 40 res *http.Response 41 actualRes *http.Response 42 kubeConfig *api.Config 43 wrapErr error 44 wrappedRoundTripper *sharedfakes.FakeRoundTripper 45 ) 46 47 BeforeEach(func() { 48 kubeConfig = &api.Config{ 49 Kind: "Config", 50 APIVersion: "v1", 51 Clusters: map[string]*api.Cluster{ 52 "my-cluster": { 53 Server: "https://example.org", 54 }, 55 }, 56 Contexts: map[string]*api.Context{ 57 "my-context": { 58 Cluster: "my-cluster", 59 AuthInfo: "my-auth-info", 60 Namespace: "my-namespace", 61 }, 62 }, 63 CurrentContext: "my-context", 64 AuthInfos: map[string]*api.AuthInfo{}, 65 } 66 67 k8sConfigGetter = new(v7actionfakes.FakeKubernetesConfigGetter) 68 k8sConfigGetter.GetReturns(kubeConfig, nil) 69 70 config = new(commandfakes.FakeConfig) 71 config.CurrentUserNameReturns("auth-test", nil) 72 73 var err error 74 req, err = http.NewRequest(http.MethodPost, "", strings.NewReader("hello")) 75 Expect(err).NotTo(HaveOccurred()) 76 77 wrappedRoundTripper = new(sharedfakes.FakeRoundTripper) 78 res = &http.Response{StatusCode: http.StatusTeapot} 79 80 wrappedRoundTripper.RoundTripReturns(res, nil) 81 actualRes = nil 82 }) 83 84 JustBeforeEach(func() { 85 var roundTripper http.RoundTripper 86 roundTripper, wrapErr = shared.WrapForCFOnK8sAuth(config, k8sConfigGetter, wrappedRoundTripper) 87 88 if wrapErr == nil { 89 actualRes, wrapErr = roundTripper.RoundTrip(req) 90 } 91 }) 92 93 When("getting the k8s config fails", func() { 94 BeforeEach(func() { 95 k8sConfigGetter.GetReturns(nil, errors.New("boom!")) 96 }) 97 98 It("returns the error", func() { 99 Expect(wrapErr).To(MatchError("boom!")) 100 }) 101 }) 102 103 When("no user is set in the config", func() { 104 BeforeEach(func() { 105 config.CurrentUserNameReturns("", nil) 106 }) 107 108 It("errors", func() { 109 Expect(wrapErr).To(MatchError(ContainSubstring("current user not set"))) 110 }) 111 }) 112 113 When("there is an error getting the current user from the config", func() { 114 BeforeEach(func() { 115 config.CurrentUserNameReturns("", errors.New("boom")) 116 }) 117 118 It("errors", func() { 119 Expect(wrapErr).To(MatchError(ContainSubstring("boom"))) 120 }) 121 }) 122 123 When("the chosen kubernetes auth info is not present in kubeconfig", func() { 124 BeforeEach(func() { 125 config.CurrentUserNameReturns("not-present", nil) 126 }) 127 128 It("errors", func() { 129 Expect(wrapErr).To(MatchError(ContainSubstring(`auth info "not-present" does not exist`))) 130 }) 131 }) 132 133 checkCalls := func() *http.Request { 134 Expect(wrapErr).NotTo(HaveOccurred()) 135 Expect(wrappedRoundTripper.RoundTripCallCount()).To(Equal(1)) 136 137 actualReq := wrappedRoundTripper.RoundTripArgsForCall(0) 138 139 body, err := ioutil.ReadAll(actualReq.Body) 140 Expect(err).NotTo(HaveOccurred()) 141 Expect(string(body)).To(Equal("hello")) 142 143 Expect(actualRes).To(Equal(res)) 144 145 return actualReq 146 } 147 148 checkBearerTokenInAuthHeader := func() { 149 actualReq := checkCalls() 150 151 token, err := jws.ParseJWTFromRequest(actualReq) 152 Expect(err).NotTo(HaveOccurred()) 153 Expect(token.Validate(keyPair.Public(), crypto.SigningMethodRS256)).To(Succeed()) 154 155 claims := token.Claims() 156 Expect(claims).To(HaveKeyWithValue("another", "thing")) 157 } 158 159 checkClientCertInAuthHeader := func() { 160 actualReq := checkCalls() 161 162 Expect(actualReq.Header).To(HaveKeyWithValue("Authorization", ConsistOf(HavePrefix("ClientCert ")))) 163 164 certAndKeyPEMBase64 := actualReq.Header.Get("Authorization")[11:] 165 certAndKeyPEM, err := base64.StdEncoding.DecodeString(certAndKeyPEMBase64) 166 Expect(err).NotTo(HaveOccurred()) 167 168 cert, rest := pem.Decode(certAndKeyPEM) 169 Expect(cert.Type).To(Equal(pemDecodeKubeConfigCertData(clientCertData).Type)) 170 Expect(cert.Bytes).To(Equal(pemDecodeKubeConfigCertData(clientCertData).Bytes)) 171 172 var key *pem.Block 173 key, rest = pem.Decode(rest) 174 Expect(key.Bytes).To(Equal(pemDecodeKubeConfigCertData(clientKeyData).Bytes)) 175 176 Expect(rest).To(BeEmpty()) 177 } 178 179 Describe("auth-provider", func() { 180 var token []byte 181 182 BeforeEach(func() { 183 jwt := jws.NewJWT(jws.Claims{ 184 "exp": time.Now().Add(time.Hour).Unix(), 185 "another": "thing", 186 }, crypto.SigningMethodRS256) 187 var err error 188 token, err = jwt.Serialize(keyPair) 189 Expect(err).NotTo(HaveOccurred()) 190 191 kubeConfig.AuthInfos["auth-test"] = &api.AuthInfo{ 192 AuthProvider: &api.AuthProviderConfig{ 193 Name: "oidc", 194 Config: map[string]string{ 195 "id-token": string(token), 196 "idp-issuer-url": "-", 197 "client-id": "-", 198 }, 199 }, 200 } 201 }) 202 203 It("uses the auth-provider to generate the Bearer token", func() { 204 checkBearerTokenInAuthHeader() 205 }) 206 }) 207 208 Describe("client certs", func() { 209 var ( 210 certFilePath string 211 keyFilePath string 212 ) 213 214 BeforeEach(func() { 215 certFilePath = writeToFile(clientCertData) 216 keyFilePath = writeToFile(clientKeyData) 217 }) 218 219 AfterEach(func() { 220 Expect(os.RemoveAll(certFilePath)).To(Succeed()) 221 Expect(os.RemoveAll(keyFilePath)).To(Succeed()) 222 }) 223 224 When("inline cert and key are provided", func() { 225 BeforeEach(func() { 226 kubeConfig.AuthInfos["auth-test"] = &api.AuthInfo{ 227 ClientCertificateData: []byte(base64Decode(clientCertData)), 228 ClientKeyData: []byte(base64Decode(clientKeyData)), 229 } 230 }) 231 232 It("puts concatenated client ceritificate and key data into the Authorization header", func() { 233 checkClientCertInAuthHeader() 234 }) 235 }) 236 237 When("cert and key are provided in files", func() { 238 BeforeEach(func() { 239 kubeConfig.AuthInfos["auth-test"] = &api.AuthInfo{ 240 ClientCertificate: certFilePath, 241 ClientKey: keyFilePath, 242 } 243 }) 244 245 It("puts concatenated client ceritificate and key data into the Authorization header", func() { 246 checkClientCertInAuthHeader() 247 }) 248 249 When("cert file cannot be read", func() { 250 BeforeEach(func() { 251 Expect(os.Remove(certFilePath)).To(Succeed()) 252 }) 253 254 It("returns an error", func() { 255 Expect(wrapErr).To(MatchError(ContainSubstring(certFilePath))) 256 }) 257 }) 258 259 When("key file cannot be read", func() { 260 BeforeEach(func() { 261 Expect(os.Remove(keyFilePath)).To(Succeed()) 262 }) 263 264 It("returns an error", func() { 265 Expect(wrapErr).To(MatchError(ContainSubstring(keyFilePath))) 266 }) 267 }) 268 }) 269 270 When("file and inline cert is provided", func() { 271 BeforeEach(func() { 272 kubeConfig.AuthInfos["auth-test"] = &api.AuthInfo{ 273 ClientCertificate: certFilePath, 274 ClientCertificateData: []byte(base64Decode(clientCertData)), 275 ClientKeyData: []byte(base64Decode(clientKeyData)), 276 } 277 }) 278 279 It("complains about invalid configuration", func() { 280 Expect(wrapErr).To(MatchError(ContainSubstring("client-cert-data and client-cert are both specified"))) 281 }) 282 }) 283 284 When("file and inline key is provided", func() { 285 BeforeEach(func() { 286 kubeConfig.AuthInfos["auth-test"] = &api.AuthInfo{ 287 ClientCertificateData: []byte(base64Decode(clientCertData)), 288 ClientKeyData: []byte(base64Decode(clientKeyData)), 289 ClientKey: keyFilePath, 290 } 291 }) 292 293 It("complains about invalid configuration", func() { 294 Expect(wrapErr).To(MatchError(ContainSubstring("client-key-data and client-key are both specified"))) 295 }) 296 }) 297 298 When("inline cert and key file are provided", func() { 299 BeforeEach(func() { 300 kubeConfig.AuthInfos["auth-test"] = &api.AuthInfo{ 301 ClientCertificateData: []byte(base64Decode(clientCertData)), 302 ClientKey: keyFilePath, 303 } 304 }) 305 306 It("uses the inline key", func() { 307 checkClientCertInAuthHeader() 308 }) 309 }) 310 311 When("cert file and inline key are provided", func() { 312 BeforeEach(func() { 313 kubeConfig.AuthInfos["auth-test"] = &api.AuthInfo{ 314 ClientCertificate: certFilePath, 315 ClientKeyData: []byte(base64Decode(clientKeyData)), 316 } 317 }) 318 319 It("uses the inline key", func() { 320 checkClientCertInAuthHeader() 321 }) 322 }) 323 }) 324 325 Describe("exec", func() { 326 BeforeEach(func() { 327 kubeConfig.AuthInfos["auth-test"] = &api.AuthInfo{ 328 Exec: &api.ExecConfig{ 329 APIVersion: "client.authentication.k8s.io/v1beta1", 330 InteractiveMode: "Never", 331 Command: "echo", 332 }, 333 } 334 }) 335 336 When("the command returns a token", func() { 337 BeforeEach(func() { 338 kubeConfig.AuthInfos["auth-test"].Exec.Args = []string{execCredential(&clientauthenticationv1beta1.ExecCredentialStatus{ 339 Token: "a-token", 340 })} 341 }) 342 343 It("uses the exec command to generate the Bearer token", func() { 344 helpers.SkipIfWindows() // We're getting "plugin returned version client.authentication.k8s.io/__internal" on Windows. This issue is unresolved in upstream library. 345 346 Expect(wrapErr).NotTo(HaveOccurred()) 347 Expect(wrappedRoundTripper.RoundTripCallCount()).To(Equal(1)) 348 349 actualReq := wrappedRoundTripper.RoundTripArgsForCall(0) 350 Expect(actualReq.Header.Get("Authorization")).To(Equal("Bearer a-token")) 351 352 Expect(actualRes).To(Equal(res)) 353 }) 354 }) 355 356 When("the command returns a client cert and key", func() { 357 BeforeEach(func() { 358 kubeConfig.AuthInfos["auth-test"].Exec.Args = []string{execCredential(&clientauthenticationv1beta1.ExecCredentialStatus{ 359 ClientCertificateData: base64Decode(clientCertData), 360 ClientKeyData: base64Decode(clientKeyData), 361 })} 362 }) 363 364 It("uses the exec command to generate client certs", func() { 365 checkClientCertInAuthHeader() 366 }) 367 }) 368 }) 369 370 Describe("tokens provided in config", func() { 371 var token []byte 372 373 BeforeEach(func() { 374 jwt := jws.NewJWT(jws.Claims{ 375 "exp": time.Now().Add(time.Hour).Unix(), 376 "another": "thing", 377 }, crypto.SigningMethodRS256) 378 var err error 379 token, err = jwt.Serialize(keyPair) 380 Expect(err).NotTo(HaveOccurred()) 381 }) 382 383 Context("inline tokens", func() { 384 BeforeEach(func() { 385 kubeConfig.AuthInfos["auth-test"] = &api.AuthInfo{ 386 Token: string(token), 387 } 388 }) 389 390 It("inserts the token in the authorization header", func() { 391 checkBearerTokenInAuthHeader() 392 }) 393 }) 394 395 Context("token file paths", func() { 396 var tokenFilePath string 397 398 BeforeEach(func() { 399 tokenFile, err := ioutil.TempFile("", "") 400 Expect(err).NotTo(HaveOccurred()) 401 defer tokenFile.Close() 402 _, err = tokenFile.Write(token) 403 Expect(err).NotTo(HaveOccurred()) 404 tokenFilePath = tokenFile.Name() 405 kubeConfig.AuthInfos["auth-test"] = &api.AuthInfo{ 406 TokenFile: tokenFilePath, 407 } 408 }) 409 410 AfterEach(func() { 411 Expect(os.RemoveAll(tokenFilePath)).To(Succeed()) 412 }) 413 414 It("inserts the token in the authorization header", func() { 415 checkBearerTokenInAuthHeader() 416 }) 417 }) 418 419 When("both file and inline token are provided", func() { 420 BeforeEach(func() { 421 kubeConfig.AuthInfos["auth-test"] = &api.AuthInfo{ 422 Token: string(token), 423 TokenFile: "some-path", 424 } 425 }) 426 427 It("the inline token takes precedence", func() { 428 checkBearerTokenInAuthHeader() 429 }) 430 }) 431 }) 432 }) 433 434 func pemDecodeKubeConfigCertData(data string) *pem.Block { 435 decodedData, err := base64.StdEncoding.DecodeString(data) 436 Expect(err).NotTo(HaveOccurred()) 437 pemDecodedBlock, rest := pem.Decode(decodedData) 438 Expect(rest).To(BeEmpty()) 439 return pemDecodedBlock 440 } 441 442 func base64Decode(encoded string) string { 443 decoded, err := base64.StdEncoding.DecodeString(encoded) 444 Expect(err).NotTo(HaveOccurred()) 445 return string(decoded) 446 } 447 448 func writeToFile(base64Data string) string { 449 file, err := ioutil.TempFile("", "") 450 Expect(err).NotTo(HaveOccurred()) 451 file.WriteString(base64Decode(base64Data)) 452 Expect(file.Close()).To(Succeed()) 453 return file.Name() 454 } 455 456 func execCredential(status *clientauthenticationv1beta1.ExecCredentialStatus) string { 457 execCred, err := json.Marshal(clientauthenticationv1beta1.ExecCredential{ 458 TypeMeta: metav1.TypeMeta{ 459 APIVersion: "client.authentication.k8s.io/v1beta1", 460 }, 461 Status: status, 462 }) 463 Expect(err).NotTo(HaveOccurred()) 464 return string(execCred) 465 }