1
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
49
50
51
52 func TestMetadata(t *testing.T) {
53
54
55
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
83
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"
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
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
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"
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
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
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"}
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
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
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
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
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
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
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
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
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
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