sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/repository/repository_github_test.go (about) 1 /* 2 Copyright 2019 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 repository 18 19 import ( 20 "context" 21 "fmt" 22 "net/http" 23 "strings" 24 "testing" 25 "time" 26 27 "github.com/google/go-github/v53/github" 28 . "github.com/onsi/gomega" 29 "k8s.io/utils/ptr" 30 31 clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" 32 "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" 33 "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" 34 "sigs.k8s.io/cluster-api/internal/goproxy" 35 goproxytest "sigs.k8s.io/cluster-api/internal/goproxy/test" 36 ) 37 38 func Test_gitHubRepository_GetVersions(t *testing.T) { 39 retryableOperationInterval = 200 * time.Millisecond 40 retryableOperationTimeout = 1 * time.Second 41 42 client, mux, teardown := test.NewFakeGitHub() 43 defer teardown() 44 45 // Setup an handler for returning 5 fake releases. 46 mux.HandleFunc("/repos/o/r1/releases", func(w http.ResponseWriter, r *http.Request) { 47 goproxytest.HTTPTestMethod(t, r, "GET") 48 fmt.Fprint(w, `[`) 49 fmt.Fprint(w, `{"id":1, "tag_name": "v0.4.0"},`) 50 fmt.Fprint(w, `{"id":2, "tag_name": "v0.4.1"},`) 51 fmt.Fprint(w, `{"id":3, "tag_name": "v0.4.2"},`) 52 fmt.Fprint(w, `{"id":4, "tag_name": "v0.4.3-alpha"}`) // Pre-release 53 fmt.Fprint(w, `]`) 54 }) 55 56 scheme, host, muxGoproxy, teardownGoproxy := goproxytest.NewFakeGoproxy() 57 clientGoproxy := goproxy.NewClient(scheme, host) 58 defer teardownGoproxy() 59 60 // Setup a handler for returning 4 fake releases. 61 muxGoproxy.HandleFunc("/github.com/o/r2/@v/list", func(w http.ResponseWriter, r *http.Request) { 62 goproxytest.HTTPTestMethod(t, r, "GET") 63 fmt.Fprint(w, "v0.5.0\n") 64 fmt.Fprint(w, "v0.4.0\n") 65 fmt.Fprint(w, "v0.3.2\n") 66 fmt.Fprint(w, "v0.3.1\n") 67 }) 68 69 // Setup a handler for returning 3 different major fake releases. 70 muxGoproxy.HandleFunc("/github.com/o/r3/@v/list", func(w http.ResponseWriter, r *http.Request) { 71 goproxytest.HTTPTestMethod(t, r, "GET") 72 fmt.Fprint(w, "v1.0.0\n") 73 fmt.Fprint(w, "v0.1.0\n") 74 }) 75 muxGoproxy.HandleFunc("/github.com/o/r3/v2/@v/list", func(w http.ResponseWriter, r *http.Request) { 76 goproxytest.HTTPTestMethod(t, r, "GET") 77 fmt.Fprint(w, "v2.0.0\n") 78 }) 79 muxGoproxy.HandleFunc("/github.com/o/r3/v3/@v/list", func(w http.ResponseWriter, r *http.Request) { 80 goproxytest.HTTPTestMethod(t, r, "GET") 81 fmt.Fprint(w, "v3.0.0\n") 82 }) 83 84 configVariablesClient := test.NewFakeVariableClient() 85 86 tests := []struct { 87 name string 88 providerConfig config.Provider 89 want []string 90 wantErr bool 91 }{ 92 { 93 name: "fallback to github", 94 providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.0/path", clusterctlv1.CoreProviderType), 95 want: []string{"v0.4.0", "v0.4.1", "v0.4.2", "v0.4.3-alpha"}, 96 wantErr: false, 97 }, 98 { 99 name: "use goproxy", 100 providerConfig: config.NewProvider("test", "https://github.com/o/r2/releases/v0.4.0/path", clusterctlv1.CoreProviderType), 101 want: []string{"v0.3.1", "v0.3.2", "v0.4.0", "v0.5.0"}, 102 wantErr: false, 103 }, 104 { 105 name: "use goproxy having multiple majors", 106 providerConfig: config.NewProvider("test", "https://github.com/o/r3/releases/v3.0.0/path", clusterctlv1.CoreProviderType), 107 want: []string{"v0.1.0", "v1.0.0", "v2.0.0", "v3.0.0"}, 108 wantErr: false, 109 }, 110 { 111 name: "failure", 112 providerConfig: config.NewProvider("test", "https://github.com/o/unknown/releases/v0.4.0/path", clusterctlv1.CoreProviderType), 113 wantErr: true, 114 }, 115 } 116 for _, tt := range tests { 117 t.Run(tt.name, func(t *testing.T) { 118 g := NewWithT(t) 119 120 ctx := context.Background() 121 122 resetCaches() 123 124 gRepo, err := NewGitHubRepository(ctx, tt.providerConfig, configVariablesClient, injectGithubClient(client), injectGoproxyClient(clientGoproxy)) 125 g.Expect(err).ToNot(HaveOccurred()) 126 127 got, err := gRepo.GetVersions(ctx) 128 if tt.wantErr { 129 g.Expect(err).To(HaveOccurred()) 130 return 131 } 132 g.Expect(err).ToNot(HaveOccurred()) 133 g.Expect(got).To(Equal(tt.want)) 134 }) 135 } 136 } 137 138 func Test_githubRepository_newGitHubRepository(t *testing.T) { 139 retryableOperationInterval = 200 * time.Millisecond 140 retryableOperationTimeout = 1 * time.Second 141 type field struct { 142 providerConfig config.Provider 143 variableClient config.VariablesClient 144 } 145 tests := []struct { 146 name string 147 field field 148 want *gitHubRepository 149 wantErr bool 150 }{ 151 { 152 name: "can create a new GitHub repo", 153 field: field{ 154 providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.1/path", clusterctlv1.CoreProviderType), 155 variableClient: test.NewFakeVariableClient(), 156 }, 157 want: &gitHubRepository{ 158 providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.1/path", clusterctlv1.CoreProviderType), 159 configVariablesClient: test.NewFakeVariableClient(), 160 authenticatingHTTPClient: nil, 161 owner: "o", 162 repository: "r1", 163 defaultVersion: "v0.4.1", 164 rootPath: ".", 165 componentsPath: "path", 166 injectClient: nil, 167 }, 168 wantErr: false, 169 }, 170 { 171 name: "missing variableClient", 172 field: field{ 173 providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.1/path", clusterctlv1.CoreProviderType), 174 variableClient: nil, 175 }, 176 want: nil, 177 wantErr: true, 178 }, 179 { 180 name: "provider url is not valid", 181 field: field{ 182 providerConfig: config.NewProvider("test", "%gh&%ij", clusterctlv1.CoreProviderType), 183 variableClient: test.NewFakeVariableClient(), 184 }, 185 want: nil, 186 wantErr: true, 187 }, 188 { 189 name: "provider url should be in https", 190 field: field{ 191 providerConfig: config.NewProvider("test", "http://github.com/blabla", clusterctlv1.CoreProviderType), 192 variableClient: test.NewFakeVariableClient(), 193 }, 194 want: nil, 195 wantErr: true, 196 }, 197 { 198 name: "provider url should be in github", 199 field: field{ 200 providerConfig: config.NewProvider("test", "http://gitlab.com/blabla", clusterctlv1.CoreProviderType), 201 variableClient: test.NewFakeVariableClient(), 202 }, 203 want: &gitHubRepository{}, 204 wantErr: true, 205 }, 206 { 207 name: "provider url should be in https://github.com/{owner}/{Repository}/%s/{latest|version-tag}/{componentsClient.yaml} format", 208 field: field{ 209 providerConfig: config.NewProvider("test", "https://github.com/dd/", clusterctlv1.CoreProviderType), 210 variableClient: test.NewFakeVariableClient(), 211 }, 212 want: nil, 213 wantErr: true, 214 }, 215 } 216 217 for _, tt := range tests { 218 t.Run(tt.name, func(t *testing.T) { 219 g := NewWithT(t) 220 resetCaches() 221 222 gitHub, err := NewGitHubRepository(context.Background(), tt.field.providerConfig, tt.field.variableClient) 223 if tt.wantErr { 224 g.Expect(err).To(HaveOccurred()) 225 return 226 } 227 228 g.Expect(err).ToNot(HaveOccurred()) 229 g.Expect(gitHub).To(Equal(tt.want)) 230 }) 231 } 232 } 233 234 func Test_githubRepository_getComponentsPath(t *testing.T) { 235 tests := []struct { 236 name string 237 path string 238 rootPath string 239 want string 240 }{ 241 { 242 name: "get the file name", 243 path: "github.com/o/r/releases/v0.4.1/file.yaml", 244 rootPath: "github.com/o/r/releases/v0.4.1/", 245 want: "file.yaml", 246 }, 247 { 248 name: "trim github.com", 249 path: "github.com/o/r/releases/v0.4.1/file.yaml", 250 rootPath: "github.com", 251 want: "o/r/releases/v0.4.1/file.yaml", 252 }, 253 } 254 255 for _, tt := range tests { 256 t.Run(tt.name, func(t *testing.T) { 257 g := NewWithT(t) 258 resetCaches() 259 260 g.Expect(getComponentsPath(tt.path, tt.rootPath)).To(Equal(tt.want)) 261 }) 262 } 263 } 264 265 func Test_githubRepository_getFile(t *testing.T) { 266 retryableOperationInterval = 200 * time.Millisecond 267 retryableOperationTimeout = 1 * time.Second 268 client, mux, teardown := test.NewFakeGitHub() 269 defer teardown() 270 271 providerConfig := config.NewProvider("test", "https://github.com/o/r/releases/v0.4.1/file.yaml", clusterctlv1.CoreProviderType) 272 273 // Setup a handler for returning a fake release. 274 mux.HandleFunc("/repos/o/r/releases/tags/v0.4.1", func(w http.ResponseWriter, r *http.Request) { 275 goproxytest.HTTPTestMethod(t, r, "GET") 276 fmt.Fprint(w, `{"id":13, "tag_name": "v0.4.1", "assets": [{"id": 1, "name": "file.yaml"}] }`) 277 }) 278 279 // Setup a handler for returning a fake release asset. 280 mux.HandleFunc("/repos/o/r/releases/assets/1", func(w http.ResponseWriter, r *http.Request) { 281 goproxytest.HTTPTestMethod(t, r, "GET") 282 w.Header().Set("Content-Type", "application/octet-stream") 283 w.Header().Set("Content-Disposition", "attachment; filename=file.yaml") 284 fmt.Fprint(w, "content") 285 }) 286 287 configVariablesClient := test.NewFakeVariableClient() 288 289 tests := []struct { 290 name string 291 release string 292 fileName string 293 want []byte 294 wantErr bool 295 }{ 296 { 297 name: "Release and file exist", 298 release: "v0.4.1", 299 fileName: "file.yaml", 300 want: []byte("content"), 301 wantErr: false, 302 }, 303 { 304 name: "Release does not exist", 305 release: "not-a-release", 306 fileName: "file.yaml", 307 want: nil, 308 wantErr: true, 309 }, 310 { 311 name: "File does not exist", 312 release: "v0.4.1", 313 fileName: "404.file", 314 want: nil, 315 wantErr: true, 316 }, 317 } 318 319 for _, tt := range tests { 320 t.Run(tt.name, func(t *testing.T) { 321 g := NewWithT(t) 322 resetCaches() 323 324 gitHub, err := NewGitHubRepository(context.Background(), providerConfig, configVariablesClient, injectGithubClient(client)) 325 g.Expect(err).ToNot(HaveOccurred()) 326 327 got, err := gitHub.GetFile(context.Background(), tt.release, tt.fileName) 328 if tt.wantErr { 329 g.Expect(err).To(HaveOccurred()) 330 return 331 } 332 333 g.Expect(err).ToNot(HaveOccurred()) 334 g.Expect(got).To(Equal(tt.want)) 335 }) 336 } 337 } 338 339 func Test_gitHubRepository_getVersions(t *testing.T) { 340 retryableOperationInterval = 200 * time.Millisecond 341 retryableOperationTimeout = 1 * time.Second 342 client, mux, teardown := test.NewFakeGitHub() 343 defer teardown() 344 345 // Setup a handler for returning fake releases in a paginated manner 346 // Each response contains a link to the next page (if available) which 347 // is parsed by the handler to navigate through all pages 348 mux.HandleFunc("/repos/o/r1/releases", func(w http.ResponseWriter, r *http.Request) { 349 goproxytest.HTTPTestMethod(t, r, "GET") 350 page := r.URL.Query().Get("page") 351 switch page { 352 case "", "1": 353 // Page 1 354 w.Header().Set("Link", `<https://api.github.com/repositories/12345/releases?page=2>; rel="next"`) // Link to page 2 355 fmt.Fprint(w, `[`) 356 fmt.Fprint(w, `{"id":1, "tag_name": "v0.4.0"},`) 357 fmt.Fprint(w, `{"id":2, "tag_name": "v0.4.1"}`) 358 fmt.Fprint(w, `]`) 359 case "2": 360 // Page 2 361 w.Header().Set("Link", `<https://api.github.com/repositories/12345/releases?page=3>; rel="next"`) // Link to page 3 362 fmt.Fprint(w, `[`) 363 fmt.Fprint(w, `{"id":3, "tag_name": "v0.4.2"},`) 364 fmt.Fprint(w, `{"id":4, "tag_name": "v0.4.3-alpha"}`) // Pre-release 365 fmt.Fprint(w, `]`) 366 case "3": 367 // Page 3 (last page) 368 fmt.Fprint(w, `[`) 369 fmt.Fprint(w, `{"id":4, "tag_name": "v0.4.4-beta"},`) // Pre-release 370 fmt.Fprint(w, `{"id":5, "tag_name": "foo"}`) // No semantic version tag 371 fmt.Fprint(w, `]`) 372 default: 373 t.Fatalf("unexpected page requested") 374 } 375 }) 376 377 configVariablesClient := test.NewFakeVariableClient() 378 379 type field struct { 380 providerConfig config.Provider 381 } 382 tests := []struct { 383 name string 384 field field 385 want []string 386 wantErr bool 387 }{ 388 { 389 name: "Get versions with all releases", 390 field: field{ 391 providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.1/path", clusterctlv1.CoreProviderType), 392 }, 393 want: []string{"v0.4.0", "v0.4.1", "v0.4.2", "v0.4.3-alpha", "v0.4.4-beta"}, 394 wantErr: false, 395 }, 396 } 397 for _, tt := range tests { 398 t.Run(tt.name, func(t *testing.T) { 399 g := NewWithT(t) 400 401 ctx := context.Background() 402 403 resetCaches() 404 405 gitHub, err := NewGitHubRepository(ctx, tt.field.providerConfig, configVariablesClient, injectGithubClient(client)) 406 g.Expect(err).ToNot(HaveOccurred()) 407 408 got, err := gitHub.(*gitHubRepository).getVersions(ctx) 409 if tt.wantErr { 410 g.Expect(err).To(HaveOccurred()) 411 return 412 } 413 g.Expect(err).ToNot(HaveOccurred()) 414 415 g.Expect(got).To(ConsistOf(tt.want)) 416 }) 417 } 418 } 419 420 func Test_gitHubRepository_getLatestContractRelease(t *testing.T) { 421 retryableOperationInterval = 200 * time.Millisecond 422 retryableOperationTimeout = 1 * time.Second 423 client, mux, teardown := test.NewFakeGitHub() 424 defer teardown() 425 426 // Setup a handler for returning a fake release. 427 mux.HandleFunc("/repos/o/r1/releases/tags/v0.5.0", func(w http.ResponseWriter, r *http.Request) { 428 goproxytest.HTTPTestMethod(t, r, "GET") 429 fmt.Fprint(w, `{"id":13, "tag_name": "v0.5.0", "assets": [{"id": 1, "name": "metadata.yaml"}] }`) 430 }) 431 432 mux.HandleFunc("/repos/o/r1/releases/tags/v0.3.2", func(w http.ResponseWriter, r *http.Request) { 433 goproxytest.HTTPTestMethod(t, r, "GET") 434 fmt.Fprint(w, `{"id":14, "tag_name": "v0.3.2", "assets": [{"id": 2, "name": "metadata.yaml"}] }`) 435 }) 436 437 // Setup a handler for returning a fake release metadata file. 438 mux.HandleFunc("/repos/o/r1/releases/assets/1", func(w http.ResponseWriter, r *http.Request) { 439 goproxytest.HTTPTestMethod(t, r, "GET") 440 w.Header().Set("Content-Type", "application/octet-stream") 441 w.Header().Set("Content-Disposition", "attachment; filename=metadata.yaml") 442 fmt.Fprint(w, "apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3\nreleaseSeries:\n - major: 0\n minor: 4\n contract: v1alpha4\n - major: 0\n minor: 5\n contract: v1alpha4\n - major: 0\n minor: 3\n contract: v1alpha3\n") 443 }) 444 445 mux.HandleFunc("/repos/o/r1/releases/assets/2", func(w http.ResponseWriter, r *http.Request) { 446 goproxytest.HTTPTestMethod(t, r, "GET") 447 w.Header().Set("Content-Type", "application/octet-stream") 448 w.Header().Set("Content-Disposition", "attachment; filename=metadata.yaml") 449 fmt.Fprint(w, "apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3\nreleaseSeries:\n - major: 0\n minor: 4\n contract: v1alpha4\n - major: 0\n minor: 5\n contract: v1alpha4\n - major: 0\n minor: 3\n contract: v1alpha3\n") 450 }) 451 452 scheme, host, muxGoproxy, teardownGoproxy := goproxytest.NewFakeGoproxy() 453 clientGoproxy := goproxy.NewClient(scheme, host) 454 455 defer teardownGoproxy() 456 457 // Setup a handler for returning 4 fake releases. 458 muxGoproxy.HandleFunc("/github.com/o/r1/@v/list", func(w http.ResponseWriter, r *http.Request) { 459 goproxytest.HTTPTestMethod(t, r, "GET") 460 fmt.Fprint(w, "v0.5.0\n") 461 fmt.Fprint(w, "v0.4.0\n") 462 fmt.Fprint(w, "v0.3.2\n") 463 fmt.Fprint(w, "v0.3.1\n") 464 }) 465 466 // setup an handler for returning 4 fake releases but no actual tagged release 467 muxGoproxy.HandleFunc("/github.com/o/r2/@v/list", func(w http.ResponseWriter, r *http.Request) { 468 goproxytest.HTTPTestMethod(t, r, "GET") 469 fmt.Fprint(w, "v0.5.0\n") 470 fmt.Fprint(w, "v0.4.0\n") 471 fmt.Fprint(w, "v0.3.2\n") 472 fmt.Fprint(w, "v0.3.1\n") 473 }) 474 475 configVariablesClient := test.NewFakeVariableClient() 476 477 type field struct { 478 providerConfig config.Provider 479 } 480 tests := []struct { 481 name string 482 field field 483 contract string 484 want string 485 wantErr bool 486 }{ 487 { 488 name: "Get latest release if it matches the contract", 489 field: field{ 490 providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/latest/path", clusterctlv1.CoreProviderType), 491 }, 492 contract: "v1alpha4", 493 want: "v0.5.0", 494 wantErr: false, 495 }, 496 { 497 name: "Get previous release if the latest doesn't match the contract", 498 field: field{ 499 providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/latest/path", clusterctlv1.CoreProviderType), 500 }, 501 contract: "v1alpha3", 502 want: "v0.3.2", 503 wantErr: false, 504 }, 505 { 506 name: "Return the latest release if the contract doesn't exist", 507 field: field{ 508 providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/latest/path", clusterctlv1.CoreProviderType), 509 }, 510 want: "v0.5.0", 511 contract: "foo", 512 wantErr: false, 513 }, 514 { 515 name: "Return 404 if there is no release for the tag", 516 field: field{ 517 providerConfig: config.NewProvider("test", "https://github.com/o/r2/releases/v0.99.0/path", clusterctlv1.CoreProviderType), 518 }, 519 want: "0.99.0", 520 contract: "v1alpha4", 521 wantErr: true, 522 }, 523 } 524 for _, tt := range tests { 525 t.Run(tt.name, func(t *testing.T) { 526 g := NewWithT(t) 527 resetCaches() 528 529 gRepo, err := NewGitHubRepository(context.Background(), tt.field.providerConfig, configVariablesClient, injectGithubClient(client), injectGoproxyClient(clientGoproxy)) 530 g.Expect(err).ToNot(HaveOccurred()) 531 532 got, err := latestContractRelease(context.Background(), gRepo, tt.contract) 533 if tt.wantErr { 534 g.Expect(err).To(HaveOccurred()) 535 return 536 } 537 g.Expect(err).ToNot(HaveOccurred()) 538 g.Expect(got).To(Equal(tt.want)) 539 }) 540 } 541 } 542 543 func Test_gitHubRepository_getLatestRelease(t *testing.T) { 544 retryableOperationInterval = 200 * time.Millisecond 545 retryableOperationTimeout = 1 * time.Second 546 scheme, host, muxGoproxy, teardownGoproxy := goproxytest.NewFakeGoproxy() 547 clientGoproxy := goproxy.NewClient(scheme, host) 548 defer teardownGoproxy() 549 550 client, mux, teardown := test.NewFakeGitHub() 551 defer teardown() 552 553 // Setup a handler for returning 4 fake releases. 554 muxGoproxy.HandleFunc("/github.com/o/r1/@v/list", func(w http.ResponseWriter, r *http.Request) { 555 goproxytest.HTTPTestMethod(t, r, "GET") 556 fmt.Fprint(w, "v0.4.1\n") 557 fmt.Fprint(w, "v0.4.2\n") 558 fmt.Fprint(w, "v0.4.3-alpha\n") // prerelease 559 fmt.Fprint(w, "foo\n") // no semantic version tag 560 }) 561 // And also expose a release for them 562 mux.HandleFunc("/repos/o/r1/releases/tags/v0.4.2", func(w http.ResponseWriter, r *http.Request) { 563 goproxytest.HTTPTestMethod(t, r, "GET") 564 fmt.Fprint(w, `{"id":13, "tag_name": "v0.4.2", "assets": [{"id": 1, "name": "metadata.yaml"}] }`) 565 }) 566 mux.HandleFunc("/repos/o/r3/releases/tags/v0.1.0-alpha.2", func(w http.ResponseWriter, r *http.Request) { 567 goproxytest.HTTPTestMethod(t, r, "GET") 568 fmt.Fprint(w, `{"id":14, "tag_name": "v0.1.0-alpha.2", "assets": [{"id": 2, "name": "metadata.yaml"}] }`) 569 }) 570 571 // Setup a handler for returning no releases. 572 muxGoproxy.HandleFunc("/github.com/o/r2/@v/list", func(_ http.ResponseWriter, r *http.Request) { 573 goproxytest.HTTPTestMethod(t, r, "GET") 574 // no releases 575 }) 576 577 // Setup a handler for returning fake prereleases only. 578 muxGoproxy.HandleFunc("/github.com/o/r3/@v/list", func(w http.ResponseWriter, r *http.Request) { 579 goproxytest.HTTPTestMethod(t, r, "GET") 580 fmt.Fprint(w, "v0.1.0-alpha.0\n") 581 fmt.Fprint(w, "v0.1.0-alpha.1\n") 582 fmt.Fprint(w, "v0.1.0-alpha.2\n") 583 }) 584 585 // Setup a handler for returning a fake release metadata file. 586 mux.HandleFunc("/repos/o/r1/releases/assets/1", func(w http.ResponseWriter, r *http.Request) { 587 goproxytest.HTTPTestMethod(t, r, "GET") 588 w.Header().Set("Content-Type", "application/octet-stream") 589 w.Header().Set("Content-Disposition", "attachment; filename=metadata.yaml") 590 fmt.Fprint(w, "apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3\nreleaseSeries:\n - major: 0\n minor: 4\n contract: v1alpha4\n - major: 0\n minor: 5\n contract: v1alpha4\n - major: 0\n minor: 3\n contract: v1alpha3\n") 591 }) 592 593 mux.HandleFunc("/repos/o/r3/releases/assets/2", func(w http.ResponseWriter, r *http.Request) { 594 goproxytest.HTTPTestMethod(t, r, "GET") 595 w.Header().Set("Content-Type", "application/octet-stream") 596 w.Header().Set("Content-Disposition", "attachment; filename=metadata.yaml") 597 fmt.Fprint(w, "apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3\nreleaseSeries:\n - major: 0\n minor: 4\n contract: v1alpha4\n - major: 0\n minor: 5\n contract: v1alpha4\n - major: 0\n minor: 3\n contract: v1alpha3\n") 598 }) 599 600 configVariablesClient := test.NewFakeVariableClient() 601 602 type field struct { 603 providerConfig config.Provider 604 } 605 tests := []struct { 606 name string 607 field field 608 want string 609 wantErr bool 610 }{ 611 { 612 name: "Get latest release, ignores pre-release version", 613 field: field{ 614 providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.2/path", clusterctlv1.CoreProviderType), 615 }, 616 want: "v0.4.2", 617 wantErr: false, 618 }, 619 { 620 name: "Fails, when no release found", 621 field: field{ 622 providerConfig: config.NewProvider("test", "https://github.com/o/r2/releases/v0.4.1/path", clusterctlv1.CoreProviderType), 623 }, 624 want: "", 625 wantErr: true, 626 }, 627 { 628 name: "Falls back to latest prerelease when no official release present", 629 field: field{ 630 providerConfig: config.NewProvider("test", "https://github.com/o/r3/releases/v0.1.0-alpha.2/path", clusterctlv1.CoreProviderType), 631 }, 632 want: "v0.1.0-alpha.2", 633 wantErr: false, 634 }, 635 } 636 for _, tt := range tests { 637 t.Run(tt.name, func(t *testing.T) { 638 g := NewWithT(t) 639 640 ctx := context.Background() 641 642 resetCaches() 643 644 gRepo, err := NewGitHubRepository(ctx, tt.field.providerConfig, configVariablesClient, injectGoproxyClient(clientGoproxy), injectGithubClient(client)) 645 g.Expect(err).ToNot(HaveOccurred()) 646 647 got, err := latestRelease(ctx, gRepo) 648 if tt.wantErr { 649 g.Expect(err).To(HaveOccurred()) 650 return 651 } 652 g.Expect(err).ToNot(HaveOccurred()) 653 g.Expect(got).To(Equal(tt.want)) 654 g.Expect(gRepo.(*gitHubRepository).defaultVersion).To(Equal(tt.want)) 655 }) 656 } 657 } 658 659 func Test_gitHubRepository_getLatestPatchRelease(t *testing.T) { 660 retryableOperationInterval = 200 * time.Millisecond 661 retryableOperationTimeout = 1 * time.Second 662 scheme, host, muxGoproxy, teardownGoproxy := goproxytest.NewFakeGoproxy() 663 clientGoproxy := goproxy.NewClient(scheme, host) 664 defer teardownGoproxy() 665 666 client, mux, teardown := test.NewFakeGitHub() 667 defer teardown() 668 669 // Setup a handler for returning 4 fake releases. 670 muxGoproxy.HandleFunc("/github.com/o/r1/@v/list", func(w http.ResponseWriter, r *http.Request) { 671 goproxytest.HTTPTestMethod(t, r, "GET") 672 fmt.Fprint(w, "v0.4.0\n") 673 fmt.Fprint(w, "v0.3.2\n") 674 fmt.Fprint(w, "v1.3.2\n") 675 }) 676 677 // Setup a handler for returning a fake release. 678 mux.HandleFunc("/repos/o/r1/releases/tags/v0.4.0", func(w http.ResponseWriter, r *http.Request) { 679 goproxytest.HTTPTestMethod(t, r, "GET") 680 fmt.Fprint(w, `{"id":13, "tag_name": "v0.4.0", "assets": [{"id": 1, "name": "metadata.yaml"}] }`) 681 }) 682 683 mux.HandleFunc("/repos/o/r1/releases/tags/v0.3.2", func(w http.ResponseWriter, r *http.Request) { 684 goproxytest.HTTPTestMethod(t, r, "GET") 685 fmt.Fprint(w, `{"id":14, "tag_name": "v0.3.2", "assets": [{"id": 1, "name": "metadata.yaml"}] }`) 686 }) 687 688 mux.HandleFunc("/repos/o/r1/releases/tags/v1.3.2", func(w http.ResponseWriter, r *http.Request) { 689 goproxytest.HTTPTestMethod(t, r, "GET") 690 fmt.Fprint(w, `{"id":15, "tag_name": "v1.3.2", "assets": [{"id": 1, "name": "metadata.yaml"}] }`) 691 }) 692 693 // Setup a handler for returning a fake release metadata file. 694 mux.HandleFunc("/repos/o/r1/releases/assets/1", func(w http.ResponseWriter, r *http.Request) { 695 goproxytest.HTTPTestMethod(t, r, "GET") 696 w.Header().Set("Content-Type", "application/octet-stream") 697 w.Header().Set("Content-Disposition", "attachment; filename=metadata.yaml") 698 fmt.Fprint(w, "apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3\nreleaseSeries:\n - major: 0\n minor: 4\n contract: v1alpha4\n - major: 0\n minor: 5\n contract: v1alpha4\n - major: 0\n minor: 3\n contract: v1alpha3\n") 699 }) 700 701 major0 := uint(0) 702 minor3 := uint(3) 703 minor4 := uint(4) 704 705 configVariablesClient := test.NewFakeVariableClient() 706 707 type field struct { 708 providerConfig config.Provider 709 } 710 tests := []struct { 711 name string 712 field field 713 major *uint 714 minor *uint 715 want string 716 wantErr bool 717 }{ 718 { 719 name: "Get latest patch release, no Major/Minor specified", 720 field: field{ 721 providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v1.3.2/path", clusterctlv1.CoreProviderType), 722 }, 723 minor: nil, 724 major: nil, 725 want: "v1.3.2", 726 wantErr: false, 727 }, 728 { 729 name: "Get latest patch release, for Major 0 and Minor 3", 730 field: field{ 731 providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.3.2/path", clusterctlv1.CoreProviderType), 732 }, 733 major: &major0, 734 minor: &minor3, 735 want: "v0.3.2", 736 wantErr: false, 737 }, 738 { 739 name: "Get latest patch release, for Major 0 and Minor 4", 740 field: field{ 741 providerConfig: config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.0/path", clusterctlv1.CoreProviderType), 742 }, 743 major: &major0, 744 minor: &minor4, 745 want: "v0.4.0", 746 wantErr: false, 747 }, 748 } 749 for _, tt := range tests { 750 t.Run(tt.name, func(t *testing.T) { 751 g := NewWithT(t) 752 753 ctx := context.Background() 754 755 resetCaches() 756 757 gRepo, err := NewGitHubRepository(ctx, tt.field.providerConfig, configVariablesClient, injectGoproxyClient(clientGoproxy), injectGithubClient(client)) 758 g.Expect(err).ToNot(HaveOccurred()) 759 760 got, err := latestPatchRelease(ctx, gRepo, tt.major, tt.minor) 761 if tt.wantErr { 762 g.Expect(err).To(HaveOccurred()) 763 return 764 } 765 g.Expect(err).ToNot(HaveOccurred()) 766 g.Expect(got).To(Equal(tt.want)) 767 }) 768 } 769 } 770 771 func Test_gitHubRepository_getReleaseByTag(t *testing.T) { 772 retryableOperationInterval = 200 * time.Millisecond 773 retryableOperationTimeout = 1 * time.Second 774 client, mux, teardown := test.NewFakeGitHub() 775 defer teardown() 776 777 providerConfig := config.NewProvider("test", "https://github.com/o/r/releases/v0.4.1/path", clusterctlv1.CoreProviderType) 778 779 // Setup a handler for returning a fake release. 780 mux.HandleFunc("/repos/o/r/releases/tags/foo", func(w http.ResponseWriter, r *http.Request) { 781 goproxytest.HTTPTestMethod(t, r, "GET") 782 fmt.Fprint(w, `{"id":13, "tag_name": "v0.4.1"}`) 783 }) 784 785 configVariablesClient := test.NewFakeVariableClient() 786 787 type args struct { 788 tag string 789 } 790 tests := []struct { 791 name string 792 args args 793 wantTagName *string 794 wantErr bool 795 }{ 796 { 797 name: "Return existing version", 798 args: args{ 799 tag: "foo", 800 }, 801 wantTagName: ptr.To("v0.4.1"), 802 wantErr: false, 803 }, 804 { 805 name: "Fails if version does not exists", 806 args: args{ 807 tag: "bar", 808 }, 809 wantTagName: nil, 810 wantErr: true, 811 }, 812 } 813 for _, tt := range tests { 814 t.Run(tt.name, func(t *testing.T) { 815 g := NewWithT(t) 816 817 ctx := context.Background() 818 819 resetCaches() 820 821 gRepo, err := NewGitHubRepository(ctx, providerConfig, configVariablesClient, injectGithubClient(client)) 822 g.Expect(err).ToNot(HaveOccurred()) 823 824 got, err := gRepo.(*gitHubRepository).getReleaseByTag(ctx, tt.args.tag) 825 if tt.wantErr { 826 g.Expect(err).To(HaveOccurred()) 827 return 828 } 829 g.Expect(err).ToNot(HaveOccurred()) 830 831 if tt.wantTagName == nil { 832 g.Expect(got).To(BeNil()) 833 return 834 } 835 836 g.Expect(got.TagName).To(Equal(tt.wantTagName)) 837 }) 838 } 839 } 840 841 func Test_gitHubRepository_downloadFilesFromRelease(t *testing.T) { 842 retryableOperationInterval = 200 * time.Millisecond 843 retryableOperationTimeout = 1 * time.Second 844 client, mux, teardown := test.NewFakeGitHub() 845 defer teardown() 846 847 providerConfig := config.NewProvider("test", "https://github.com/o/r/releases/v0.4.1/file.yaml", clusterctlv1.CoreProviderType) // tree/main/path not relevant for the test 848 providerConfigWithRedirect := config.NewProvider("test", "https://github.com/o/r-with-redirect/releases/v0.4.1/file.yaml", clusterctlv1.CoreProviderType) // tree/main/path not relevant for the test 849 850 // Setup a handler for returning a fake release asset. 851 mux.HandleFunc("/repos/o/r/releases/assets/1", func(w http.ResponseWriter, r *http.Request) { 852 goproxytest.HTTPTestMethod(t, r, "GET") 853 w.Header().Set("Content-Type", "application/octet-stream") 854 w.Header().Set("Content-Disposition", "attachment; filename=file.yaml") 855 fmt.Fprint(w, "content") 856 }) 857 // Setup a handler which redirects to a different location. 858 mux.HandleFunc("/repos/o/r-with-redirect/releases/assets/1", func(w http.ResponseWriter, r *http.Request) { 859 goproxytest.HTTPTestMethod(t, r, "GET") 860 http.Redirect(w, r, "/api-v3/repos/o/r/releases/assets/1", http.StatusFound) 861 }) 862 863 configVariablesClient := test.NewFakeVariableClient() 864 865 var id1 int64 = 1 866 var id2 int64 = 2 867 tagName := "vO.3.3" 868 file := "file.yaml" 869 870 type args struct { 871 release *github.RepositoryRelease 872 fileName string 873 } 874 tests := []struct { 875 name string 876 args args 877 providerConfig config.Provider 878 want []byte 879 wantErr bool 880 }{ 881 { 882 name: "Pass if file exists", 883 args: args{ 884 release: &github.RepositoryRelease{ 885 TagName: &tagName, 886 Assets: []*github.ReleaseAsset{ 887 { 888 ID: &id1, 889 Name: &file, 890 }, 891 }, 892 }, 893 fileName: file, 894 }, 895 providerConfig: providerConfig, 896 want: []byte("content"), 897 wantErr: false, 898 }, 899 { 900 name: "Pass if file exists with redirect", 901 args: args{ 902 release: &github.RepositoryRelease{ 903 TagName: &tagName, 904 Assets: []*github.ReleaseAsset{ 905 { 906 ID: &id1, 907 Name: &file, 908 }, 909 }, 910 }, 911 fileName: file, 912 }, 913 providerConfig: providerConfigWithRedirect, 914 want: []byte("content"), 915 wantErr: false, 916 }, 917 { 918 name: "Fails if file does not exists", 919 args: args{ 920 release: &github.RepositoryRelease{ 921 TagName: &tagName, 922 Assets: []*github.ReleaseAsset{ 923 { 924 ID: &id1, 925 Name: &file, 926 }, 927 }, 928 }, 929 fileName: "another file", 930 }, 931 providerConfig: providerConfig, 932 wantErr: true, 933 }, 934 { 935 name: "Fails if file does not exists", 936 args: args{ 937 release: &github.RepositoryRelease{ 938 TagName: &tagName, 939 Assets: []*github.ReleaseAsset{ 940 { 941 ID: &id2, // id does not match any file (this should not happen) 942 Name: &file, 943 }, 944 }, 945 }, 946 fileName: "another file", 947 }, 948 providerConfig: providerConfig, 949 wantErr: true, 950 }, 951 } 952 for _, tt := range tests { 953 t.Run(tt.name, func(t *testing.T) { 954 g := NewWithT(t) 955 resetCaches() 956 957 gRepo, err := NewGitHubRepository(context.Background(), tt.providerConfig, configVariablesClient, injectGithubClient(client)) 958 g.Expect(err).ToNot(HaveOccurred()) 959 960 got, err := gRepo.(*gitHubRepository).downloadFilesFromRelease(context.Background(), tt.args.release, tt.args.fileName) 961 if tt.wantErr { 962 g.Expect(err).To(HaveOccurred()) 963 return 964 } 965 966 g.Expect(err).ToNot(HaveOccurred()) 967 g.Expect(got).To(Equal(tt.want)) 968 }) 969 } 970 } 971 972 // resetCaches is called repeatedly throughout tests to help avoid cross-test pollution. 973 func resetCaches() { 974 cacheVersions = map[string][]string{} 975 cacheReleases = map[string]*github.RepositoryRelease{} 976 cacheFiles = map[string][]byte{} 977 } 978 979 func Test_gitHubRepository_releaseNotFound(t *testing.T) { 980 retryableOperationInterval = 200 * time.Millisecond 981 retryableOperationTimeout = 1 * time.Second 982 983 tests := []struct { 984 name string 985 releaseTags []string 986 ghReleases []string 987 want string 988 wantErr bool 989 }{ 990 { 991 name: "One release", 992 releaseTags: []string{"v0.4.2"}, 993 ghReleases: []string{"v0.4.2"}, 994 want: "v0.4.2", 995 wantErr: false, 996 }, 997 { 998 name: "Latest tag without a release", 999 releaseTags: []string{"v0.5.0", "v0.4.2"}, 1000 ghReleases: []string{"v0.4.2"}, 1001 want: "v0.4.2", 1002 wantErr: false, 1003 }, 1004 { 1005 name: "Two tags without releases", 1006 releaseTags: []string{"v0.6.0", "v0.5.0", "v0.4.2"}, 1007 ghReleases: []string{"v0.4.2"}, 1008 want: "v0.4.2", 1009 wantErr: false, 1010 }, 1011 { 1012 name: "Five tags without releases", 1013 releaseTags: []string{"v0.9.0", "v0.8.0", "v0.7.0", "v0.6.0", "v0.5.0", "v0.4.2"}, 1014 ghReleases: []string{"v0.4.2"}, 1015 wantErr: true, 1016 }, 1017 { 1018 name: "Pre-releases have lower priority", 1019 releaseTags: []string{"v0.7.0-alpha", "v0.6.0-alpha", "v0.5.0-alpha", "v0.4.2"}, 1020 ghReleases: []string{"v0.4.2"}, 1021 want: "v0.4.2", 1022 wantErr: false, 1023 }, 1024 { 1025 name: "Two Github releases", 1026 releaseTags: []string{"v0.7.0", "v0.6.0", "v0.5.0", "v0.4.2"}, 1027 ghReleases: []string{"v0.5.0", "v0.4.2"}, 1028 want: "v0.5.0", 1029 wantErr: false, 1030 }, 1031 { 1032 name: "Github release and prerelease", 1033 releaseTags: []string{"v0.6.0", "v0.5.0-alpha", "v0.4.2"}, 1034 ghReleases: []string{"v0.5.0-alpha", "v0.4.2"}, 1035 want: "v0.4.2", 1036 wantErr: false, 1037 }, 1038 { 1039 name: "No Github releases", 1040 releaseTags: []string{"v0.6.0", "v0.5.0", "v0.4.2"}, 1041 ghReleases: []string{}, 1042 wantErr: true, 1043 }, 1044 { 1045 name: "Pre-releases only", 1046 releaseTags: []string{"v0.6.0-alpha", "v0.5.0-alpha", "v0.4.2-alpha"}, 1047 ghReleases: []string{"v0.5.0-alpha"}, 1048 want: "v0.5.0-alpha", 1049 wantErr: false, 1050 }, 1051 } 1052 for _, tt := range tests { 1053 t.Run(tt.name, func(t *testing.T) { 1054 g := NewWithT(t) 1055 1056 ctx := context.Background() 1057 1058 configVariablesClient := test.NewFakeVariableClient() 1059 1060 resetCaches() 1061 1062 client, mux, teardown := test.NewFakeGitHub() 1063 defer teardown() 1064 1065 providerConfig := config.NewProvider("test", "https://github.com/o/r1/releases/v0.4.1/file.yaml", clusterctlv1.CoreProviderType) 1066 1067 scheme, host, muxGoproxy, teardownGoproxy := goproxytest.NewFakeGoproxy() 1068 clientGoproxy := goproxy.NewClient(scheme, host) 1069 1070 defer teardownGoproxy() 1071 1072 // First, register tags within goproxy. 1073 muxGoproxy.HandleFunc("/github.com/o/r1/@v/list", func(w http.ResponseWriter, r *http.Request) { 1074 goproxytest.HTTPTestMethod(t, r, "GET") 1075 for _, release := range tt.releaseTags { 1076 fmt.Fprint(w, release+"\n") 1077 } 1078 }) 1079 1080 // Second, register releases in GitHub. 1081 for _, release := range tt.ghReleases { 1082 mux.HandleFunc(fmt.Sprintf("/repos/o/r1/releases/tags/%s", release), func(w http.ResponseWriter, r *http.Request) { 1083 goproxytest.HTTPTestMethod(t, r, "GET") 1084 parts := strings.Split(r.RequestURI, "/") 1085 version := parts[len(parts)-1] 1086 fmt.Fprintf(w, "{\"id\":13, \"tag_name\": %q, \"assets\": [{\"id\": 1, \"name\": \"metadata.yaml\"}] }", version) 1087 }) 1088 } 1089 1090 // Third, setup a handler for returning a fake release metadata file. 1091 mux.HandleFunc("/repos/o/r1/releases/assets/1", func(w http.ResponseWriter, r *http.Request) { 1092 goproxytest.HTTPTestMethod(t, r, "GET") 1093 w.Header().Set("Content-Type", "application/octet-stream") 1094 w.Header().Set("Content-Disposition", "attachment; filename=metadata.yaml") 1095 fmt.Fprint(w, "apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3\nreleaseSeries:\n - major: 0\n minor: 4\n contract: v1alpha4\n - major: 0\n minor: 5\n contract: v1alpha4\n - major: 0\n minor: 3\n contract: v1alpha3\n") 1096 }) 1097 1098 gRepo, err := NewGitHubRepository(ctx, providerConfig, configVariablesClient, injectGithubClient(client), injectGoproxyClient(clientGoproxy)) 1099 g.Expect(err).ToNot(HaveOccurred()) 1100 1101 got, err := latestRelease(ctx, gRepo) 1102 if tt.wantErr { 1103 g.Expect(err).To(HaveOccurred()) 1104 return 1105 } 1106 g.Expect(err).ToNot(HaveOccurred()) 1107 g.Expect(got).To(Equal(tt.want)) 1108 }) 1109 } 1110 }