sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/template_test.go (about) 1 /* 2 Copyright 2020 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 cluster 18 19 import ( 20 "context" 21 "encoding/base64" 22 "fmt" 23 "net/http" 24 "net/http/httptest" 25 "net/url" 26 "os" 27 "path/filepath" 28 "testing" 29 30 "github.com/google/go-github/v53/github" 31 . "github.com/onsi/gomega" 32 corev1 "k8s.io/api/core/v1" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 35 "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" 36 "sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository" 37 yaml "sigs.k8s.io/cluster-api/cmd/clusterctl/client/yamlprocessor" 38 "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" 39 ) 40 41 var template = `apiVersion: cluster.x-k8s.io/v1beta1 42 kind: Cluster 43 --- 44 apiVersion: cluster.x-k8s.io/v1beta1 45 kind: Machine` 46 47 func Test_templateClient_GetFromConfigMap(t *testing.T) { 48 g := NewWithT(t) 49 50 configClient, err := config.New(context.Background(), "", config.InjectReader(test.NewFakeReader())) 51 g.Expect(err).ToNot(HaveOccurred()) 52 53 configMap := &corev1.ConfigMap{ 54 TypeMeta: metav1.TypeMeta{ 55 Kind: "ConfigMap", 56 APIVersion: "v1", 57 }, 58 ObjectMeta: metav1.ObjectMeta{ 59 Namespace: "ns1", 60 Name: "my-template", 61 }, 62 Data: map[string]string{ 63 "prod": template, 64 }, 65 } 66 67 type fields struct { 68 proxy Proxy 69 configClient config.Client 70 } 71 type args struct { 72 configMapNamespace string 73 configMapName string 74 configMapDataKey string 75 targetNamespace string 76 skipTemplateProcess bool 77 } 78 tests := []struct { 79 name string 80 fields fields 81 args args 82 want string 83 wantErr bool 84 }{ 85 { 86 name: "Return template", 87 fields: fields{ 88 proxy: test.NewFakeProxy().WithObjs(configMap), 89 configClient: configClient, 90 }, 91 args: args{ 92 configMapNamespace: "ns1", 93 configMapName: "my-template", 94 configMapDataKey: "prod", 95 targetNamespace: "", 96 skipTemplateProcess: false, 97 }, 98 want: template, 99 wantErr: false, 100 }, 101 { 102 name: "Config map does not exists", 103 fields: fields{ 104 proxy: test.NewFakeProxy().WithObjs(configMap), 105 configClient: configClient, 106 }, 107 args: args{ 108 configMapNamespace: "ns1", 109 configMapName: "something-else", 110 configMapDataKey: "prod", 111 targetNamespace: "", 112 skipTemplateProcess: false, 113 }, 114 want: "", 115 wantErr: true, 116 }, 117 { 118 name: "Config map key does not exists", 119 fields: fields{ 120 proxy: test.NewFakeProxy().WithObjs(configMap), 121 configClient: configClient, 122 }, 123 args: args{ 124 configMapNamespace: "ns1", 125 configMapName: "my-template", 126 configMapDataKey: "something-else", 127 targetNamespace: "", 128 skipTemplateProcess: false, 129 }, 130 want: "", 131 wantErr: true, 132 }, 133 } 134 for _, tt := range tests { 135 t.Run(tt.name, func(t *testing.T) { 136 g := NewWithT(t) 137 138 ctx := context.Background() 139 140 processor := yaml.NewSimpleProcessor() 141 tc := newTemplateClient(TemplateClientInput{tt.fields.proxy, tt.fields.configClient, processor}) 142 got, err := tc.GetFromConfigMap(ctx, tt.args.configMapNamespace, tt.args.configMapName, tt.args.configMapDataKey, tt.args.targetNamespace, tt.args.skipTemplateProcess) 143 if tt.wantErr { 144 g.Expect(err).To(HaveOccurred()) 145 return 146 } 147 g.Expect(err).ToNot(HaveOccurred()) 148 149 wantTemplate, err := repository.NewTemplate(repository.TemplateInput{ 150 RawArtifact: []byte(tt.want), 151 ConfigVariablesClient: configClient.Variables(), 152 Processor: processor, 153 TargetNamespace: tt.args.targetNamespace, 154 SkipTemplateProcess: tt.args.skipTemplateProcess, 155 }) 156 g.Expect(err).ToNot(HaveOccurred()) 157 g.Expect(got).To(Equal(wantTemplate)) 158 }) 159 } 160 } 161 162 func Test_templateClient_getGitHubFileContent(t *testing.T) { 163 g := NewWithT(t) 164 165 client, mux, teardown := test.NewFakeGitHub() 166 defer teardown() 167 168 configClient, err := config.New(context.Background(), "", config.InjectReader(test.NewFakeReader())) 169 g.Expect(err).ToNot(HaveOccurred()) 170 171 mux.HandleFunc("/repos/kubernetes-sigs/cluster-api/contents/config/default/cluster-template.yaml", func(w http.ResponseWriter, _ *http.Request) { 172 fmt.Fprint(w, `{ 173 "type": "file", 174 "encoding": "base64", 175 "content": "`+base64.StdEncoding.EncodeToString([]byte(template))+`", 176 "sha": "f5f369044773ff9c6383c087466d12adb6fa0828", 177 "size": 12, 178 "name": "cluster-template.yaml", 179 "path": "config/default/cluster-template.yaml" 180 }`) 181 }) 182 183 type args struct { 184 rURL *url.URL 185 } 186 tests := []struct { 187 name string 188 args args 189 want []byte 190 wantErr bool 191 }{ 192 { 193 name: "Return custom template", 194 args: args{ 195 rURL: mustParseURL("https://github.com/kubernetes-sigs/cluster-api/blob/main/config/default/cluster-template.yaml"), 196 }, 197 want: []byte(template), 198 wantErr: false, 199 }, 200 { 201 name: "Wrong url", 202 args: args{ 203 rURL: mustParseURL("https://github.com/kubernetes-sigs/cluster-api/blob/main/config/default/something-else.yaml"), 204 }, 205 want: nil, 206 wantErr: true, 207 }, 208 } 209 for _, tt := range tests { 210 t.Run(tt.name, func(t *testing.T) { 211 g := NewWithT(t) 212 213 ctx := context.Background() 214 215 c := &templateClient{ 216 configClient: configClient, 217 gitHubClientFactory: func(context.Context, config.VariablesClient) (*github.Client, error) { 218 return client, nil 219 }, 220 } 221 got, err := c.getGitHubFileContent(ctx, tt.args.rURL) 222 if tt.wantErr { 223 g.Expect(err).To(HaveOccurred()) 224 return 225 } 226 227 g.Expect(err).ToNot(HaveOccurred()) 228 229 g.Expect(got).To(Equal(tt.want)) 230 }) 231 } 232 } 233 234 func Test_templateClient_getRawUrlFileContent(t *testing.T) { 235 fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 236 fmt.Fprint(w, template) 237 })) 238 239 defer fakeServer.Close() 240 241 type args struct { 242 rURL string 243 } 244 tests := []struct { 245 name string 246 args args 247 want []byte 248 wantErr bool 249 }{ 250 { 251 name: "Return custom template", 252 args: args{ 253 rURL: fakeServer.URL, 254 }, 255 want: []byte(template), 256 wantErr: false, 257 }, 258 } 259 for _, tt := range tests { 260 t.Run(tt.name, func(t *testing.T) { 261 g := NewWithT(t) 262 263 ctx := context.Background() 264 265 c := newTemplateClient(TemplateClientInput{}) 266 got, err := c.getRawURLFileContent(ctx, tt.args.rURL) 267 if tt.wantErr { 268 g.Expect(err).To(HaveOccurred()) 269 return 270 } 271 272 g.Expect(err).ToNot(HaveOccurred()) 273 274 g.Expect(got).To(Equal(tt.want)) 275 }) 276 } 277 } 278 279 func Test_templateClient_getLocalFileContent(t *testing.T) { 280 g := NewWithT(t) 281 282 tmpDir, err := os.MkdirTemp("", "cc") 283 g.Expect(err).ToNot(HaveOccurred()) 284 defer os.RemoveAll(tmpDir) 285 286 path := filepath.Join(tmpDir, "cluster-template.yaml") 287 g.Expect(os.WriteFile(path, []byte(template), 0600)).To(Succeed()) 288 289 type args struct { 290 rURL *url.URL 291 } 292 tests := []struct { 293 name string 294 args args 295 want []byte 296 wantErr bool 297 }{ 298 { 299 name: "Return custom template", 300 args: args{ 301 rURL: mustParseURL(path), 302 }, 303 want: []byte(template), 304 wantErr: false, 305 }, 306 { 307 name: "Wrong path", 308 args: args{ 309 rURL: mustParseURL(filepath.Join(tmpDir, "something-else.yaml")), 310 }, 311 want: nil, 312 wantErr: true, 313 }, 314 } 315 for _, tt := range tests { 316 t.Run(tt.name, func(t *testing.T) { 317 g := NewWithT(t) 318 319 c := &templateClient{} 320 got, err := c.getLocalFileContent(tt.args.rURL) 321 if tt.wantErr { 322 g.Expect(err).To(HaveOccurred()) 323 return 324 } 325 326 g.Expect(err).ToNot(HaveOccurred()) 327 328 g.Expect(got).To(Equal(tt.want)) 329 }) 330 } 331 } 332 333 func Test_templateClient_GetFromURL(t *testing.T) { 334 g := NewWithT(t) 335 336 tmpDir, err := os.MkdirTemp("", "cc") 337 g.Expect(err).ToNot(HaveOccurred()) 338 defer os.RemoveAll(tmpDir) 339 340 configClient, err := config.New(context.Background(), "", config.InjectReader(test.NewFakeReader())) 341 g.Expect(err).ToNot(HaveOccurred()) 342 343 fakeGithubClient, mux, teardown := test.NewFakeGitHub() 344 defer teardown() 345 346 mux.HandleFunc("/repos/kubernetes-sigs/cluster-api/contents/config/default/cluster-template.yaml", func(w http.ResponseWriter, _ *http.Request) { 347 fmt.Fprint(w, `{ 348 "type": "file", 349 "encoding": "base64", 350 "content": "`+base64.StdEncoding.EncodeToString([]byte(template))+`", 351 "sha": "f5f369044773ff9c6383c087466d12adb6fa0828", 352 "size": 12, 353 "name": "cluster-template.yaml", 354 "path": "config/default/cluster-template.yaml" 355 }`) 356 }) 357 358 mux.HandleFunc("/repos/some-owner/some-repo/releases/tags/v1.0.0", func(w http.ResponseWriter, _ *http.Request) { 359 fmt.Fprint(w, `{ 360 "tag_name": "v1.0.0", 361 "name": "v1.0.0", 362 "id": 12345678, 363 "url": "https://api.github.com/repos/some-owner/some-repo/releases/12345678", 364 "assets": [ 365 { 366 "id": 87654321, 367 "name": "cluster-template.yaml" 368 } 369 ] 370 }`) 371 }) 372 373 mux.HandleFunc("/repos/some-owner/some-repo/releases/assets/87654321", func(w http.ResponseWriter, _ *http.Request) { 374 fmt.Fprint(w, template) 375 }) 376 377 mux.HandleFunc("/repos/some-owner/some-repo/releases/tags/v2.0.0", func(w http.ResponseWriter, _ *http.Request) { 378 fmt.Fprint(w, `{ 379 "tag_name": "v2.0.0", 380 "name": "v2.0.0", 381 "id": 12345678, 382 "url": "https://api.github.com/repos/some-owner/some-repo/releases/12345678", 383 "assets": [ 384 { 385 "id": 22222222, 386 "name": "cluster-template.yaml" 387 } 388 ] 389 }`) 390 }) 391 392 // redirect asset 393 mux.HandleFunc("/repos/some-owner/some-repo/releases/assets/22222222", func(w http.ResponseWriter, _ *http.Request) { 394 // add the "/api-v3" prefix to match the prefix of the fake github server 395 w.Header().Add("Location", "/api-v3/redirected/22222222") 396 w.WriteHeader(http.StatusFound) 397 }) 398 399 // redirect location 400 mux.HandleFunc("/redirected/22222222", func(w http.ResponseWriter, _ *http.Request) { 401 fmt.Fprint(w, template) 402 }) 403 404 path := filepath.Join(tmpDir, "cluster-template.yaml") 405 g.Expect(os.WriteFile(path, []byte(template), 0600)).To(Succeed()) 406 407 // redirect stdin 408 saveStdin := os.Stdin 409 defer func() { os.Stdin = saveStdin }() 410 os.Stdin, err = os.Open(path) //nolint:gosec 411 g.Expect(err).ToNot(HaveOccurred()) 412 413 type args struct { 414 templateURL string 415 targetNamespace string 416 skipTemplateProcess bool 417 } 418 tests := []struct { 419 name string 420 args args 421 want string 422 wantErr bool 423 }{ 424 { 425 name: "Get from local file system", 426 args: args{ 427 templateURL: path, 428 targetNamespace: "", 429 skipTemplateProcess: false, 430 }, 431 want: template, 432 wantErr: false, 433 }, 434 { 435 name: "Get from GitHub", 436 args: args{ 437 templateURL: "https://github.com/kubernetes-sigs/cluster-api/blob/main/config/default/cluster-template.yaml", 438 targetNamespace: "", 439 skipTemplateProcess: false, 440 }, 441 want: template, 442 wantErr: false, 443 }, 444 { 445 name: "Get asset from GitHub release", 446 args: args{ 447 templateURL: "https://github.com/some-owner/some-repo/releases/download/v1.0.0/cluster-template.yaml", 448 targetNamespace: "", 449 skipTemplateProcess: false, 450 }, 451 want: template, 452 wantErr: false, 453 }, 454 { 455 name: "Get asset from GitHub release + redirect", 456 args: args{ 457 templateURL: "https://github.com/some-owner/some-repo/releases/download/v2.0.0/cluster-template.yaml", 458 targetNamespace: "", 459 skipTemplateProcess: false, 460 }, 461 want: template, 462 wantErr: false, 463 }, 464 { 465 name: "Get asset from GitHub release with a wrong URL", 466 args: args{ 467 templateURL: "https://github.com/some-owner/some-repo/releases/wrong/v1.0.0/cluster-template.yaml", 468 targetNamespace: "", 469 skipTemplateProcess: false, 470 }, 471 want: "", 472 wantErr: true, 473 }, 474 { 475 name: "Get from stdin", 476 args: args{ 477 templateURL: "-", 478 targetNamespace: "", 479 skipTemplateProcess: false, 480 }, 481 want: template, 482 wantErr: false, 483 }, 484 } 485 for _, tt := range tests { 486 t.Run(tt.name, func(t *testing.T) { 487 g := NewWithT(t) 488 489 ctx := context.Background() 490 491 gitHubClientFactory := func(context.Context, config.VariablesClient) (*github.Client, error) { 492 return fakeGithubClient, nil 493 } 494 processor := yaml.NewSimpleProcessor() 495 c := newTemplateClient(TemplateClientInput{nil, configClient, processor}) 496 // override the github client factory 497 c.gitHubClientFactory = gitHubClientFactory 498 499 got, err := c.GetFromURL(ctx, tt.args.templateURL, tt.args.targetNamespace, tt.args.skipTemplateProcess) 500 if tt.wantErr { 501 g.Expect(err).To(HaveOccurred()) 502 return 503 } 504 505 g.Expect(err).ToNot(HaveOccurred()) 506 507 wantTemplate, err := repository.NewTemplate(repository.TemplateInput{ 508 RawArtifact: []byte(tt.want), 509 ConfigVariablesClient: configClient.Variables(), 510 Processor: processor, 511 TargetNamespace: tt.args.targetNamespace, 512 SkipTemplateProcess: tt.args.skipTemplateProcess, 513 }) 514 g.Expect(err).ToNot(HaveOccurred()) 515 g.Expect(got).To(Equal(wantTemplate)) 516 }) 517 } 518 } 519 520 func mustParseURL(rawURL string) *url.URL { 521 rURL, err := url.Parse(rawURL) 522 if err != nil { 523 panic(err) 524 } 525 return rURL 526 }