k8s.io/kubernetes@v1.29.3/pkg/credentialprovider/gcp/metadata_test.go (about) 1 /* 2 Copyright 2014 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 gcp 18 19 import ( 20 "encoding/base64" 21 "encoding/json" 22 "fmt" 23 "net/http" 24 "net/http/httptest" 25 "net/url" 26 "os" 27 "reflect" 28 "runtime" 29 "strings" 30 "testing" 31 32 utilnet "k8s.io/apimachinery/pkg/util/net" 33 utilfeature "k8s.io/apiserver/pkg/util/feature" 34 featuregatetesting "k8s.io/component-base/featuregate/testing" 35 "k8s.io/kubernetes/pkg/credentialprovider" 36 kubefeatures "k8s.io/kubernetes/pkg/features" 37 "k8s.io/legacy-cloud-providers/gce/gcpcredential" 38 ) 39 40 func createProductNameFile() (string, error) { 41 file, err := os.CreateTemp("", "") 42 if err != nil { 43 return "", fmt.Errorf("failed to create temporary test file: %v", err) 44 } 45 return file.Name(), os.WriteFile(file.Name(), []byte("Google"), 0600) 46 } 47 48 // The tests here are run in this fashion to ensure TestAllProvidersNoMetadata 49 // is run after the others, since that test currently relies upon the file 50 // referenced by gceProductNameFile being removed, which is the opposite of 51 // the other tests 52 func TestMetadata(t *testing.T) { 53 // This test requires onGCEVM to return True. On Linux, this can be faked by creating a 54 // Product Name File. But on Windows, onGCEVM makes the following syscall instead: 55 // wmic computersystem get model 56 if runtime.GOOS == "windows" && !onGCEVM() { 57 t.Skip("Skipping test on Windows, not on GCE.") 58 } 59 60 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, kubefeatures.DisableKubeletCloudCredentialProviders, false)() 61 62 var err error 63 gceProductNameFile, err = createProductNameFile() 64 if err != nil { 65 t.Errorf("failed to create gce product name file: %v", err) 66 } 67 defer os.Remove(gceProductNameFile) 68 t.Run("productNameDependent", func(t *testing.T) { 69 t.Run("DockerKeyringFromGoogleDockerConfigMetadata", 70 DockerKeyringFromGoogleDockerConfigMetadata) 71 t.Run("DockerKeyringFromGoogleDockerConfigMetadataUrl", 72 DockerKeyringFromGoogleDockerConfigMetadataURL) 73 t.Run("ContainerRegistryNoServiceAccount", 74 ContainerRegistryNoServiceAccount) 75 t.Run("ContainerRegistryBasics", 76 ContainerRegistryBasics) 77 t.Run("ContainerRegistryNoStorageScope", 78 ContainerRegistryNoStorageScope) 79 t.Run("ComputePlatformScopeSubstitutesStorageScope", 80 ComputePlatformScopeSubstitutesStorageScope) 81 }) 82 // We defer os.Remove in case of an unexpected exit, but this os.Remove call 83 // is the normal teardown call so AllProvidersNoMetadata executes properly 84 os.Remove(gceProductNameFile) 85 t.Run("AllProvidersNoMetadata", 86 AllProvidersNoMetadata) 87 } 88 89 func DockerKeyringFromGoogleDockerConfigMetadata(t *testing.T) { 90 t.Parallel() 91 registryURL := "hello.kubernetes.io" 92 email := "foo@bar.baz" 93 username := "foo" 94 password := "bar" // Fake value for testing. 95 auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))) 96 sampleDockerConfig := fmt.Sprintf(`{ 97 "https://%s": { 98 "email": %q, 99 "auth": %q 100 } 101 }`, registryURL, email, auth) 102 const probeEndpoint = "/computeMetadata/v1/" 103 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 104 // Only serve the one metadata key. 105 if probeEndpoint == r.URL.Path { 106 w.WriteHeader(http.StatusOK) 107 } else if strings.HasSuffix(gcpcredential.DockerConfigKey, r.URL.Path) { 108 w.WriteHeader(http.StatusOK) 109 w.Header().Set("Content-Type", "application/json") 110 fmt.Fprintln(w, sampleDockerConfig) 111 } else { 112 http.Error(w, "", http.StatusNotFound) 113 } 114 })) 115 defer server.Close() 116 117 // Make a transport that reroutes all traffic to the example server 118 transport := utilnet.SetTransportDefaults(&http.Transport{ 119 Proxy: func(req *http.Request) (*url.URL, error) { 120 return url.Parse(server.URL + req.URL.Path) 121 }, 122 }) 123 124 keyring := &credentialprovider.BasicDockerKeyring{} 125 provider := &DockerConfigKeyProvider{ 126 MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}}, 127 } 128 129 if !provider.Enabled() { 130 t.Errorf("Provider is unexpectedly disabled") 131 } 132 133 keyring.Add(provider.Provide("")) 134 135 creds, ok := keyring.Lookup(registryURL) 136 if !ok { 137 t.Errorf("Didn't find expected URL: %s", registryURL) 138 return 139 } 140 if len(creds) > 1 { 141 t.Errorf("Got more hits than expected: %s", creds) 142 } 143 val := creds[0] 144 145 if username != val.Username { 146 t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username) 147 } 148 if password != val.Password { 149 t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password) 150 } 151 if email != val.Email { 152 t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email) 153 } 154 } 155 156 func DockerKeyringFromGoogleDockerConfigMetadataURL(t *testing.T) { 157 t.Parallel() 158 registryURL := "hello.kubernetes.io" 159 email := "foo@bar.baz" 160 username := "foo" 161 password := "bar" // Fake value for testing. 162 auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))) 163 sampleDockerConfig := fmt.Sprintf(`{ 164 "https://%s": { 165 "email": %q, 166 "auth": %q 167 } 168 }`, registryURL, email, auth) 169 const probeEndpoint = "/computeMetadata/v1/" 170 const valueEndpoint = "/my/value" 171 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 172 // Only serve the URL key and the value endpoint 173 if probeEndpoint == r.URL.Path { 174 w.WriteHeader(http.StatusOK) 175 } else if valueEndpoint == r.URL.Path { 176 w.WriteHeader(http.StatusOK) 177 w.Header().Set("Content-Type", "application/json") 178 fmt.Fprintln(w, sampleDockerConfig) 179 } else if strings.HasSuffix(gcpcredential.DockerConfigURLKey, r.URL.Path) { 180 w.WriteHeader(http.StatusOK) 181 w.Header().Set("Content-Type", "application/text") 182 fmt.Fprint(w, "http://foo.bar.com"+valueEndpoint) 183 } else { 184 http.Error(w, "", http.StatusNotFound) 185 } 186 })) 187 defer server.Close() 188 189 // Make a transport that reroutes all traffic to the example server 190 transport := utilnet.SetTransportDefaults(&http.Transport{ 191 Proxy: func(req *http.Request) (*url.URL, error) { 192 return url.Parse(server.URL + req.URL.Path) 193 }, 194 }) 195 196 keyring := &credentialprovider.BasicDockerKeyring{} 197 provider := &DockerConfigURLKeyProvider{ 198 MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}}, 199 } 200 201 if !provider.Enabled() { 202 t.Errorf("Provider is unexpectedly disabled") 203 } 204 205 keyring.Add(provider.Provide("")) 206 207 creds, ok := keyring.Lookup(registryURL) 208 if !ok { 209 t.Errorf("Didn't find expected URL: %s", registryURL) 210 return 211 } 212 if len(creds) > 1 { 213 t.Errorf("Got more hits than expected: %s", creds) 214 } 215 val := creds[0] 216 217 if username != val.Username { 218 t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username) 219 } 220 if password != val.Password { 221 t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password) 222 } 223 if email != val.Email { 224 t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email) 225 } 226 } 227 228 func ContainerRegistryBasics(t *testing.T) { 229 t.Parallel() 230 registryURLs := []string{"container.cloud.google.com", "eu.gcr.io", "us-west2-docker.pkg.dev"} 231 for _, registryURL := range registryURLs { 232 t.Run(registryURL, func(t *testing.T) { 233 email := "1234@project.gserviceaccount.com" 234 token := &gcpcredential.TokenBlob{AccessToken: "ya26.lots-of-indiscernible-garbage"} // Fake value for testing. 235 236 const ( 237 serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/" 238 defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/" 239 scopeEndpoint = defaultEndpoint + "scopes" 240 emailEndpoint = defaultEndpoint + "email" 241 tokenEndpoint = defaultEndpoint + "token" 242 ) 243 244 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 245 // Only serve the URL key and the value endpoint 246 if scopeEndpoint == r.URL.Path { 247 w.WriteHeader(http.StatusOK) 248 w.Header().Set("Content-Type", "application/json") 249 fmt.Fprintf(w, `["%s.read_write"]`, gcpcredential.StorageScopePrefix) 250 } else if emailEndpoint == r.URL.Path { 251 w.WriteHeader(http.StatusOK) 252 fmt.Fprint(w, email) 253 } else if tokenEndpoint == r.URL.Path { 254 w.WriteHeader(http.StatusOK) 255 w.Header().Set("Content-Type", "application/json") 256 bytes, err := json.Marshal(token) 257 if err != nil { 258 t.Fatalf("unexpected error: %v", err) 259 } 260 fmt.Fprintln(w, string(bytes)) 261 } else if serviceAccountsEndpoint == r.URL.Path { 262 w.WriteHeader(http.StatusOK) 263 fmt.Fprintln(w, "default/\ncustom") 264 } else { 265 http.Error(w, "", http.StatusNotFound) 266 } 267 })) 268 defer server.Close() 269 270 // Make a transport that reroutes all traffic to the example server 271 transport := utilnet.SetTransportDefaults(&http.Transport{ 272 Proxy: func(req *http.Request) (*url.URL, error) { 273 return url.Parse(server.URL + req.URL.Path) 274 }, 275 }) 276 277 keyring := &credentialprovider.BasicDockerKeyring{} 278 provider := &ContainerRegistryProvider{ 279 MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}}, 280 } 281 282 if !provider.Enabled() { 283 t.Errorf("Provider is unexpectedly disabled") 284 } 285 286 keyring.Add(provider.Provide("")) 287 288 creds, ok := keyring.Lookup(registryURL) 289 if !ok { 290 t.Errorf("Didn't find expected URL: %s", registryURL) 291 return 292 } 293 if len(creds) > 1 { 294 t.Errorf("Got more hits than expected: %s", creds) 295 } 296 val := creds[0] 297 298 if val.Username != "_token" { 299 t.Errorf("Unexpected username value, want: %s, got: %s", "_token", val.Username) 300 } 301 if token.AccessToken != val.Password { 302 t.Errorf("Unexpected password value, want: %s, got: %s", token.AccessToken, val.Password) 303 } 304 if email != val.Email { 305 t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email) 306 } 307 }) 308 } 309 } 310 311 func ContainerRegistryNoServiceAccount(t *testing.T) { 312 const ( 313 serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/" 314 ) 315 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 316 // Only serve the URL key and the value endpoint 317 if serviceAccountsEndpoint == r.URL.Path { 318 w.WriteHeader(http.StatusOK) 319 w.Header().Set("Content-Type", "application/json") 320 bytes, err := json.Marshal([]string{}) 321 if err != nil { 322 t.Fatalf("unexpected error: %v", err) 323 } 324 fmt.Fprintln(w, string(bytes)) 325 } else { 326 http.Error(w, "", http.StatusNotFound) 327 } 328 })) 329 defer server.Close() 330 331 // Make a transport that reroutes all traffic to the example server 332 transport := utilnet.SetTransportDefaults(&http.Transport{ 333 Proxy: func(req *http.Request) (*url.URL, error) { 334 return url.Parse(server.URL + req.URL.Path) 335 }, 336 }) 337 338 provider := &ContainerRegistryProvider{ 339 MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}}, 340 } 341 342 if provider.Enabled() { 343 t.Errorf("Provider is unexpectedly enabled") 344 } 345 } 346 347 func ContainerRegistryNoStorageScope(t *testing.T) { 348 t.Parallel() 349 const ( 350 serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/" 351 defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/" 352 scopeEndpoint = defaultEndpoint + "scopes" 353 ) 354 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 355 // Only serve the URL key and the value endpoint 356 if scopeEndpoint == r.URL.Path { 357 w.WriteHeader(http.StatusOK) 358 w.Header().Set("Content-Type", "application/json") 359 fmt.Fprint(w, `["https://www.googleapis.com/auth/compute.read_write"]`) 360 } else if serviceAccountsEndpoint == r.URL.Path { 361 w.WriteHeader(http.StatusOK) 362 fmt.Fprintln(w, "default/\ncustom") 363 } else { 364 http.Error(w, "", http.StatusNotFound) 365 } 366 })) 367 defer server.Close() 368 369 // Make a transport that reroutes all traffic to the example server 370 transport := utilnet.SetTransportDefaults(&http.Transport{ 371 Proxy: func(req *http.Request) (*url.URL, error) { 372 return url.Parse(server.URL + req.URL.Path) 373 }, 374 }) 375 376 provider := &ContainerRegistryProvider{ 377 MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}}, 378 } 379 380 if provider.Enabled() { 381 t.Errorf("Provider is unexpectedly enabled") 382 } 383 } 384 385 func ComputePlatformScopeSubstitutesStorageScope(t *testing.T) { 386 t.Parallel() 387 const ( 388 serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/" 389 defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/" 390 scopeEndpoint = defaultEndpoint + "scopes" 391 ) 392 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 393 // Only serve the URL key and the value endpoint 394 if scopeEndpoint == r.URL.Path { 395 w.WriteHeader(http.StatusOK) 396 w.Header().Set("Content-Type", "application/json") 397 fmt.Fprint(w, `["https://www.googleapis.com/auth/compute.read_write","https://www.googleapis.com/auth/cloud-platform.read-only"]`) 398 } else if serviceAccountsEndpoint == r.URL.Path { 399 w.WriteHeader(http.StatusOK) 400 w.Header().Set("Content-Type", "application/json") 401 fmt.Fprintln(w, "default/\ncustom") 402 } else { 403 http.Error(w, "", http.StatusNotFound) 404 } 405 })) 406 defer server.Close() 407 408 // Make a transport that reroutes all traffic to the example server 409 transport := utilnet.SetTransportDefaults(&http.Transport{ 410 Proxy: func(req *http.Request) (*url.URL, error) { 411 return url.Parse(server.URL + req.URL.Path) 412 }, 413 }) 414 415 provider := &ContainerRegistryProvider{ 416 MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}}, 417 } 418 419 if !provider.Enabled() { 420 t.Errorf("Provider is unexpectedly disabled") 421 } 422 } 423 424 func AllProvidersNoMetadata(t *testing.T) { 425 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 426 http.Error(w, "", http.StatusNotFound) 427 })) 428 defer server.Close() 429 430 // Make a transport that reroutes all traffic to the example server 431 transport := utilnet.SetTransportDefaults(&http.Transport{ 432 Proxy: func(req *http.Request) (*url.URL, error) { 433 return url.Parse(server.URL + req.URL.Path) 434 }, 435 }) 436 437 providers := []credentialprovider.DockerConfigProvider{ 438 &DockerConfigKeyProvider{ 439 MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}}, 440 }, 441 &DockerConfigURLKeyProvider{ 442 MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}}, 443 }, 444 &ContainerRegistryProvider{ 445 MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}}, 446 }, 447 } 448 449 for _, provider := range providers { 450 if provider.Enabled() { 451 t.Errorf("Provider %s is unexpectedly enabled", reflect.TypeOf(provider).String()) 452 } 453 } 454 }