sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/spyglass/storageartifact_fetcher_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 spyglass 18 19 import ( 20 "context" 21 "encoding/base64" 22 "fmt" 23 "os" 24 "strings" 25 "testing" 26 27 "github.com/google/go-cmp/cmp" 28 "k8s.io/apimachinery/pkg/util/sets" 29 30 prowv1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 31 "sigs.k8s.io/prow/pkg/config" 32 "sigs.k8s.io/prow/pkg/io" 33 ) 34 35 func TestNewGCSJobSource(t *testing.T) { 36 testCases := []struct { 37 name string 38 src string 39 exJobPrefix string 40 exBucket string 41 exName string 42 exBuildID string 43 exLinkPrefix string 44 exSource string 45 expectedErr error 46 }{ 47 { 48 name: "Test standard GCS link (old format)", 49 src: "test-bucket/logs/example-ci-run/403", 50 exBucket: "test-bucket", 51 exJobPrefix: "logs/example-ci-run/403/", 52 exName: "example-ci-run", 53 exBuildID: "403", 54 exLinkPrefix: "gs://", 55 exSource: "gs://test-bucket/logs/example-ci-run/403", 56 expectedErr: nil, 57 }, 58 { 59 name: "Test GCS link with trailing / (old format)", 60 src: "test-bucket/logs/example-ci-run/403/", 61 exBucket: "test-bucket", 62 exJobPrefix: "logs/example-ci-run/403/", 63 exName: "example-ci-run", 64 exBuildID: "403", 65 exLinkPrefix: "gs://", 66 exSource: "gs://test-bucket/logs/example-ci-run/403/", 67 expectedErr: nil, 68 }, 69 { 70 name: "Test GCS link with org name (old format)", 71 src: "test-bucket/logs/sig-flexing/example-ci-run/403", 72 exBucket: "test-bucket", 73 exJobPrefix: "logs/sig-flexing/example-ci-run/403/", 74 exName: "example-ci-run", 75 exBuildID: "403", 76 exLinkPrefix: "gs://", 77 exSource: "gs://test-bucket/logs/sig-flexing/example-ci-run/403", 78 expectedErr: nil, 79 }, 80 { 81 name: "Test standard GCS link (new format)", 82 src: "gs://test-bucket/logs/example-ci-run/403", 83 exBucket: "test-bucket", 84 exJobPrefix: "logs/example-ci-run/403/", 85 exName: "example-ci-run", 86 exBuildID: "403", 87 exLinkPrefix: "gs://", 88 exSource: "gs://test-bucket/logs/example-ci-run/403", 89 expectedErr: nil, 90 }, 91 { 92 name: "Test standard GCS link (new format) with bucket alias", 93 src: "gs://alias/logs/example-ci-run/403", 94 exBucket: "test-bucket", 95 exJobPrefix: "logs/example-ci-run/403/", 96 exName: "example-ci-run", 97 exBuildID: "403", 98 exLinkPrefix: "gs://", 99 exSource: "gs://test-bucket/logs/example-ci-run/403", 100 expectedErr: nil, 101 }, 102 { 103 name: "Test GCS link with trailing / (new format)", 104 src: "gs://test-bucket/logs/example-ci-run/403/", 105 exBucket: "test-bucket", 106 exJobPrefix: "logs/example-ci-run/403/", 107 exName: "example-ci-run", 108 exBuildID: "403", 109 exLinkPrefix: "gs://", 110 exSource: "gs://test-bucket/logs/example-ci-run/403/", 111 expectedErr: nil, 112 }, 113 { 114 name: "Test GCS link with org name (new format)", 115 src: "gs://test-bucket/logs/sig-flexing/example-ci-run/403", 116 exBucket: "test-bucket", 117 exJobPrefix: "logs/sig-flexing/example-ci-run/403/", 118 exName: "example-ci-run", 119 exBuildID: "403", 120 exLinkPrefix: "gs://", 121 exSource: "gs://test-bucket/logs/sig-flexing/example-ci-run/403", 122 expectedErr: nil, 123 }, 124 { 125 name: "Test standard S3 link", 126 src: "s3://test-bucket/logs/example-ci-run/403", 127 exBucket: "test-bucket", 128 exJobPrefix: "logs/example-ci-run/403/", 129 exName: "example-ci-run", 130 exBuildID: "403", 131 exLinkPrefix: "s3://", 132 exSource: "s3://test-bucket/logs/example-ci-run/403", 133 expectedErr: nil, 134 }, 135 { 136 name: "Test S3 link with trailing /", 137 src: "s3://test-bucket/logs/example-ci-run/403/", 138 exBucket: "test-bucket", 139 exJobPrefix: "logs/example-ci-run/403/", 140 exName: "example-ci-run", 141 exBuildID: "403", 142 exLinkPrefix: "s3://", 143 exSource: "s3://test-bucket/logs/example-ci-run/403/", 144 expectedErr: nil, 145 }, 146 { 147 name: "Test S3 link with org name", 148 src: "s3://test-bucket/logs/sig-flexing/example-ci-run/403", 149 exBucket: "test-bucket", 150 exJobPrefix: "logs/sig-flexing/example-ci-run/403/", 151 exName: "example-ci-run", 152 exBuildID: "403", 153 exLinkPrefix: "s3://", 154 exSource: "s3://test-bucket/logs/sig-flexing/example-ci-run/403", 155 expectedErr: nil, 156 }, 157 { 158 name: "Test S3 link which cannot be parsed", 159 src: "s3;://test-bucket/logs/sig-flexing/example-ci-run/403", 160 expectedErr: ErrCannotParseSource, 161 }, 162 } 163 for _, tc := range testCases { 164 t.Run(tc.name, func(t *testing.T) { 165 cfg := createConfigGetter("test-bucket") 166 cfg().Deck.Spyglass.BucketAliases = map[string]string{"alias": "test-bucket"} 167 af := NewStorageArtifactFetcher(nil, cfg, false) 168 jobSource, err := af.newStorageJobSource(tc.src) 169 if err != tc.expectedErr { 170 t.Errorf("Expected err: %v, got err: %v", tc.expectedErr, err) 171 } 172 if tc.exBucket != jobSource.bucket { 173 t.Errorf("Expected bucket %s, got %s", tc.exBucket, jobSource.bucket) 174 } 175 if tc.exName != jobSource.jobName { 176 t.Errorf("Expected jobName %s, got %s", tc.exName, jobSource.jobName) 177 } 178 if tc.exJobPrefix != jobSource.jobPrefix { 179 t.Errorf("Expected jobPrefix %s, got %s", tc.exJobPrefix, jobSource.jobPrefix) 180 } 181 if tc.exLinkPrefix != jobSource.linkPrefix { 182 t.Errorf("Expected linkPrefix %s, got %s", tc.exLinkPrefix, jobSource.linkPrefix) 183 } 184 if tc.exSource != jobSource.source { 185 t.Errorf("Expected source %s, got %s", tc.exSource, jobSource.source) 186 } 187 }) 188 } 189 } 190 191 // Tests listing objects associated with the current job in GCS 192 func TestArtifacts_ListGCS(t *testing.T) { 193 cfg := createConfigGetter("test-bucket") 194 cfg().Deck.Spyglass.BucketAliases = map[string]string{"alias": "test-bucket"} 195 fakeGCSClient := fakeGCSServer.Client() 196 testAf := NewStorageArtifactFetcher(io.NewGCSOpener(fakeGCSClient), cfg, false) 197 testCases := []struct { 198 name string 199 handle artifactHandle 200 source string 201 expectedArtifacts []string 202 }{ 203 { 204 name: "Test ArtifactFetcher simple list artifacts (old format)", 205 source: "test-bucket/logs/example-ci-run/403", 206 expectedArtifacts: []string{ 207 "build-log.txt", 208 prowv1.StartedStatusFile, 209 prowv1.FinishedStatusFile, 210 "junit_01.xml", 211 "long-log.txt", 212 }, 213 }, 214 { 215 name: "Test ArtifactFetcher list artifacts on source with no artifacts (old format)", 216 source: "test-bucket/logs/example-ci/404", 217 expectedArtifacts: []string{}, 218 }, 219 { 220 name: "Test ArtifactFetcher simple list artifacts (new format)", 221 source: "gs://test-bucket/logs/example-ci-run/403", 222 expectedArtifacts: []string{ 223 "build-log.txt", 224 prowv1.StartedStatusFile, 225 prowv1.FinishedStatusFile, 226 "junit_01.xml", 227 "long-log.txt", 228 }, 229 }, 230 { 231 name: "Test ArtifactFetcher list artifacts on source with no artifacts (new format)", 232 source: "gs://test-bucket/logs/example-ci/404", 233 expectedArtifacts: []string{}, 234 }, 235 { 236 name: "Test ArtifactFetcher list artifacts with bucket alias configured", 237 source: "gs://alias/logs/example-ci-run/403", 238 expectedArtifacts: []string{ 239 "build-log.txt", 240 prowv1.StartedStatusFile, 241 prowv1.FinishedStatusFile, 242 "junit_01.xml", 243 "long-log.txt", 244 }, 245 }, 246 } 247 248 for _, tc := range testCases { 249 t.Run(tc.name, func(nested *testing.T) { 250 actualArtifacts, err := testAf.artifacts(context.Background(), tc.source) 251 if err != nil { 252 nested.Fatalf("Failed to get artifact names: %v", err) 253 } 254 for _, ea := range tc.expectedArtifacts { 255 found := false 256 for _, aa := range actualArtifacts { 257 if ea == aa { 258 found = true 259 break 260 } 261 } 262 if !found { 263 nested.Fatalf("failed to retrieve the following artifact: %s\nRetrieved: %s.", ea, actualArtifacts) 264 } 265 266 } 267 if len(tc.expectedArtifacts) != len(actualArtifacts) { 268 nested.Fatalf("produced more artifacts than expected. Expected: %s\nActual: %s.", tc.expectedArtifacts, actualArtifacts) 269 } 270 }) 271 } 272 } 273 274 // Tests getting handles to objects associated with the current job in GCS 275 func TestFetchArtifacts_GCS(t *testing.T) { 276 cfg := createConfigGetter("test-bucket") 277 fakeGCSClient := fakeGCSServer.Client() 278 testAf := NewStorageArtifactFetcher(io.NewGCSOpener(fakeGCSClient), cfg, false) 279 maxSize := int64(500e6) 280 testCases := []struct { 281 name string 282 artifactName string 283 source string 284 expectedSize int64 285 expectedMetadata map[string]string 286 expectErr bool 287 }{ 288 { 289 name: "Fetch build-log.txt from valid source", 290 artifactName: "build-log.txt", 291 source: "test-bucket/logs/example-ci-run/403", 292 expectedSize: 25, 293 expectedMetadata: map[string]string{ 294 "foo": "bar", 295 }, 296 }, 297 { 298 name: "Fetch build-log.txt from invalid source", 299 artifactName: "build-log.txt", 300 source: "test-bucket/logs/example-ci-run/404", 301 expectErr: true, 302 }, 303 { 304 name: "Fetch build-log.txt from valid source", 305 artifactName: "build-log.txt", 306 source: "gs://test-bucket/logs/example-ci-run/403", 307 expectedSize: 25, 308 expectedMetadata: map[string]string{ 309 "foo": "bar", 310 }, 311 }, 312 { 313 name: "Fetch build-log.txt from invalid source", 314 artifactName: "build-log.txt", 315 source: "gs://test-bucket/logs/example-ci-run/404", 316 expectErr: true, 317 }, 318 } 319 320 for _, tc := range testCases { 321 t.Run(tc.name, func(t *testing.T) { 322 artifact, err := testAf.Artifact(context.Background(), tc.source, tc.artifactName, maxSize) 323 if err != nil { 324 t.Errorf("Failed to get artifacts: %v", err) 325 } 326 size, err := artifact.Size() 327 if err != nil && !tc.expectErr { 328 t.Fatalf("Failed getting size for artifact %s, err: %v", artifact.JobPath(), err) 329 } 330 if err == nil && tc.expectErr { 331 t.Error("Expected error, got no error") 332 } 333 334 if size != tc.expectedSize { 335 t.Errorf("Expected artifact with size %d but got %d", tc.expectedSize, size) 336 } 337 meta, err := artifact.Metadata() 338 if err != nil && !tc.expectErr { 339 t.Fatalf("Failed getting metadata for artifact %s, err: %v", artifact.JobPath(), err) 340 } 341 if err == nil && tc.expectErr { 342 t.Errorf("Expected error, got no error") 343 } 344 if diff := cmp.Diff(tc.expectedMetadata, meta); diff != "" { 345 t.Errorf("Metadata got unexpected diff (-want +got):\n%s", diff) 346 } 347 }) 348 } 349 } 350 351 func TestSignURL(t *testing.T) { 352 // This fake key is revoked and thus worthless but still make its contents less obvious 353 fakeKeyBuf, err := base64.StdEncoding.DecodeString(` 354 LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tXG5NSUlFdlFJQkFEQU5CZ2txaGtpRzl3MEJBUUVG 355 QUFTQ0JLY3dnZ1NqQWdFQUFvSUJBUUN4MEF2aW1yMjcwZDdaXG5pamw3b1FRUW1oZTFOb3dpeWMy 356 UStuQW95aFE1YkQvUW1jb01zcWg2YldneVI0UU90aXVBbHM2VWhJenF4Q25pXG5PazRmbWJqVnhp 357 STl1Ri9EVTV6ZE5wM0dkQWFiUlVPNW5yWkpMelN0VXhudFBEcjZvK281RHM5YWJJWkNYYUVTXG5o 358 UWxOdTBrUm5HbHZGUHNkV1JYMmtSN01Yb3pkcXczcHZZRXZyaGlhRStYZnRhUzhKdmZEc0NPT2RQ 359 OWp5TzNTXG5aR2lkaU5hRmhYK2xnZEcrdHdqOUE3UDFlb1NMbTZCdXVhcjRDOGhlOEVkVGVEbXVk 360 a1BPeWwvb2tHWU5tSzJkXG5yUkQ0WHBhcy93VGxsTXBLRUZxWllZeVdkRnJvVWQwMFVhQnhHV0cz 361 UlZ2TWZoRk80QUhrSkNwZlE1U00rSElmXG5VN2lkRjAyYkFnTUJBQUVDZ2dFQURIaVhoTTZ1bFFB 362 OHZZdzB5T2Q3cGdCd3ZqeHpxckwxc0gvb0l1dzlhK09jXG5QREMxRzV2aU5pZjdRVitEc3haeXlh 363 T0tISitKVktQcWZodnh3OFNmMHBxQlowdkpwNlR6SVE3R0ZSZXBLUFc4XG5NTVloYWRPZVFiUE00 364 emN3dWNpS1VuTW45dU1hcllmc2xxUnZDUjBrSEZDWWtucHB2RjYxckNQMGdZZjJJRXZUXG5qNVlV 365 QWFrNDlVRDQyaUdEZnh2OGUzMGlMTmRRWE1iMHE3V2dyRGdxL0ttUHM2Q2dOaGRzME1uSlRFbUE5 366 YlFtXG52MHV0K2hUYWpXalcxVWNyUTBnM2JjNng1VWN2V1VjK1ZndUllVmxVcEgvM2dJNXVYZkxn 367 bTVQNThNa0s4UlhTXG5YYW92Rk05VkNNRFhTK25PWk1uSXoyNVd5QmhkNmdpVWs5UkJhc05Tb1FL 368 QmdRRGFxUXpyYWJUZEZNY1hwVlNnXG41TUpuNEcvSFVPWUxveVM5cE9UZi9qbFN1ZUYrNkt6RGJV 369 N1F6TC9wT1JtYjJldVdxdmpmZDVBaU1oUnY2Snk1XG41ZVNpa3dYRDZJeS9sZGh3QUdtMUZrZ1ZX 370 TXJ3ZHlqYjJpV2I2Um4rNXRBYjgwdzNEN2ZTWWhEWkxUOWJCNjdCXG4ybGxiOGFycEJRcndZUFFB 371 U2pUVUVYQnVJUUtCZ1FEUUxVemkrd0tHNEhLeko1NE1sQjFoR3cwSFZlWEV4T0pmXG53bS9IVjhl 372 aThDeHZLMTRoRXpCT3JXQi9aNlo4VFFxWnA0eENnYkNiY0hwY3pLRUxvcDA2K2hqa1N3ZkR2TUJZ 373 XG5mNnN6U2RSenNYVTI1NndmcG1hRjJ0TlJZZFpVblh2QWc5MFIrb1BFSjhrRHd4cjdiMGZmL3lu 374 b0UrWUx0ckowXG53dklad3Joc093S0JnQWVPbWlTMHRZeUNnRkwvNHNuZ3ZodEs5WElGQ0w1VU9C 375 dlp6Qk0xdlJOdjJ5eEFyRi9nXG5zajJqSmVyUWoyTUVpQkRmL2RQelZPYnBwaTByOCthMDNFOEdG 376 OGZxakpxK2VnbDg2aXBaQjhxOUU5NTFyOUxSXG5Xa1ZtTEFEVVIxTC8rSjFhakxiWHJzOWlzZkxh 377 ZEI2OUJpT1lXWmpPRk0reitocmNkYkR5blZraEFvR0FJbW42XG50ZU1zN2NNWTh3anZsY0MrZ3Br 378 SU5GZzgzYVIyajhJQzNIOWtYMGs0N3ovS0ZjbW9TTGxjcEhNc0VJeGozamJXXG5kd0FkZy9TNkpi 379 RW1SbGdoaWVoaVNRc21RM05ta0xxNlFJWkorcjR4VkZ4RUZnOWFEM0szVUZMT0xickRCSFpJXG5D 380 M3JRWVpMNkpnY1E1TlBtbTk4QXZIN2RucjRiRGpaVDgzSS9McFVDZ1lFQWttNXlvVUtZY0tXMVQz 381 R1hadUNIXG40SDNWVGVzZDZyb3pKWUhmTWVkNE9jQ3l1bnBIVmZmSmFCMFIxRjZ2MjFQaitCVWlW 382 WjBzU010RjEvTE1uQkc4XG5TQVlQUnVxOHVNUUdNQTFpdE1Hc2VhMmg1V2RhbXNGODhXRFd4VEoy 383 QXVnblJHNERsdmJLUDhPQmVLUFFKeDhEXG5RMzJ2SVpNUVkyV1hVMVhwUkMrNWs5RT1cbi0tLS0t 384 RU5EIFBSSVZBVEUgS0VZLS0tLS1cbgo=`) 385 if err != nil { 386 t.Fatalf("Failed to decode fake key: %v", err) 387 } 388 fakePrivateKey := strings.TrimSpace(string(fakeKeyBuf)) 389 cases := []struct { 390 name string 391 fakeCreds string 392 useCookie bool 393 expected string 394 contains []string 395 err string 396 }{ 397 { 398 name: "anon auth works", 399 expected: fmt.Sprintf("https://%s/foo/bar/stuff", io.GSAnonHost), 400 }, 401 { 402 name: "cookie auth works", 403 useCookie: true, 404 expected: fmt.Sprintf("https://%s/foo/bar/stuff", io.GSCookieHost), 405 }, 406 { 407 name: "invalid json file errors", 408 fakeCreds: "yaml: 123", 409 err: "dialing: invalid character 'y' looking for beginning of value", 410 }, 411 { 412 name: "bad private key errors", 413 fakeCreds: `{ 414 "type": "service_account", 415 "private_key": "-----BEGIN PRIVATE KEY-----\nMIIE==\n-----END PRIVATE KEY-----\n", 416 "client_email": "fake-user@k8s.io" 417 }`, 418 err: "asn1: structure error: tags don't match (16 vs {class:0 tag:13 length:45 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue:<nil> tag:<nil> stringType:0 timeType:0 set:false omitEmpty:false} pkcs1PrivateKey @2", 419 }, 420 { 421 name: "bad type errors", 422 fakeCreds: `{ 423 "type": "user", 424 "private_key": "` + fakePrivateKey + `", 425 "client_email": "fake-user@k8s.io" 426 }`, 427 err: "dialing: unknown credential type: \"user\"", 428 }, 429 { 430 name: "signed URLs work", 431 fakeCreds: `{ 432 "type": "service_account", 433 "private_key": "` + fakePrivateKey + `", 434 "client_email": "fake-user@k8s.io" 435 }`, 436 contains: []string{ 437 "https://storage.googleapis.com/foo/bar/stuff?", 438 "GoogleAccessId=fake-user%40k8s.io", 439 "Signature=", // Do not particularly care about the Signature contents 440 }, 441 }, 442 } 443 444 for _, tc := range cases { 445 t.Run(tc.name, func(t *testing.T) { 446 var path string 447 if tc.fakeCreds != "" { 448 fp, err := os.CreateTemp("", "fake-creds") 449 if err != nil { 450 t.Fatalf("Failed to create fake creds: %v", err) 451 } 452 453 path = fp.Name() 454 defer os.Remove(path) 455 if _, err := fp.Write([]byte(tc.fakeCreds)); err != nil { 456 t.Fatalf("Failed to write fake creds %s: %v", path, err) 457 } 458 459 if err := fp.Close(); err != nil { 460 t.Fatalf("Failed to close fake creds %s: %v", path, err) 461 } 462 } 463 // We're testing the combination of NewOpener and signURL here 464 // to make sure that the behaviour is more or less the same as before 465 // we moved the signURL code to the io package. 466 // The errors which were previously tested on the signURL method are now 467 // already returned by newOpener. 468 // Before, these error should have already lead to errors on gcs client creation, so signURL probably was never able to produce these errors during runtime. 469 // (because deck crashed on gcsClient creation) 470 var actual string 471 cfg := createConfigGetter("test-bucket") 472 opener, err := io.NewOpener(context.Background(), path, "") 473 if err == nil { 474 af := NewStorageArtifactFetcher(opener, cfg, tc.useCookie) 475 actual, err = af.signURL(context.Background(), "gs://foo/bar/stuff") 476 } 477 switch { 478 case err != nil: 479 if tc.err != err.Error() { 480 t.Errorf("expected error: %v, got: %v", tc.err, err) 481 } 482 case tc.err != "": 483 t.Errorf("Failed to receive an expected error, got %q", actual) 484 case len(tc.contains) == 0 && actual != tc.expected: 485 t.Errorf("signURL(): got %q, want %q", actual, tc.expected) 486 default: 487 for _, part := range tc.contains { 488 if !strings.Contains(actual, part) { 489 t.Errorf("signURL(): got %q, does not contain %q", actual, part) 490 } 491 } 492 } 493 }) 494 } 495 } 496 497 func createConfigGetter(bucketNames ...string) config.Getter { 498 ca := config.Agent{} 499 ca.Set(&config.Config{ 500 ProwConfig: config.ProwConfig{ 501 Deck: config.Deck{ 502 AllKnownStorageBuckets: sets.New[string](bucketNames...), 503 }, 504 }, 505 }) 506 return ca.Config 507 }