...

Source file src/k8s.io/kubernetes/pkg/credentialprovider/gcp/metadata_test.go

Documentation: k8s.io/kubernetes/pkg/credentialprovider/gcp

     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  }
   455  

View as plain text