github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/imagetranslation/transport_test.go (about) 1 /* 2 Copyright 2017 Mirantis 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 imagetranslation 18 19 import ( 20 "context" 21 "crypto/tls" 22 "fmt" 23 "io/ioutil" 24 "net/http" 25 "net/http/httptest" 26 "os" 27 "strings" 28 "testing" 29 "time" 30 31 "github.com/Mirantis/virtlet/pkg/api/virtlet.k8s/v1" 32 "github.com/Mirantis/virtlet/pkg/image" 33 testutils "github.com/Mirantis/virtlet/pkg/utils/testing" 34 ) 35 36 func translate(config v1.ImageTranslation, name string, server *httptest.Server) image.Endpoint { 37 for i, rule := range config.Rules { 38 config.Rules[i].URL = strings.Replace(rule.URL, "%", server.Listener.Addr().String(), 1) 39 } 40 configs := map[string]v1.ImageTranslation{"config": config} 41 42 translator := NewImageNameTranslator(true) 43 translator.LoadConfigs(context.Background(), NewFakeConfigSource(configs)) 44 return translator.Translate(name) 45 } 46 47 func intptr(v int) *int { 48 return &v 49 } 50 51 func download(t *testing.T, proto string, config v1.ImageTranslation, name string, server *httptest.Server) { 52 downloader := image.NewDownloader(proto) 53 if err := downloader.DownloadFile(context.Background(), translate(config, name, server), ioutil.Discard); err != nil { 54 t.Fatal(err) 55 } 56 } 57 58 func TestMain(m *testing.M) { 59 os.Unsetenv("HTTP_PROXY") 60 os.Unsetenv("HTTPS_PROXY") 61 m.Run() 62 } 63 64 func TestImageDownload(t *testing.T) { 65 handled := false 66 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 67 handled = true 68 if r.URL.String() != "/base.qcow2" { 69 t.Fatalf("unexpected URL %s", r.URL) 70 } 71 }) 72 ts := httptest.NewServer(handler) 73 defer ts.Close() 74 75 config := v1.ImageTranslation{ 76 Prefix: "test", 77 Rules: []v1.TranslationRule{ 78 { 79 Name: "image1", 80 URL: "http://%/base.qcow2", 81 }, 82 }, 83 } 84 85 download(t, "https", config, "test/image1", ts) 86 if !handled { 87 t.Fatal("image was not downloaded") 88 } 89 } 90 91 func TestImageDownloadRedirects(t *testing.T) { 92 var urls []string 93 var handledCount int 94 var maxRedirects int 95 96 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 97 urls = append(urls, r.URL.String()) 98 if handledCount < maxRedirects { 99 w.Header().Add("Location", fmt.Sprintf("/file%d", handledCount+1)) 100 w.WriteHeader(301) 101 } 102 handledCount++ 103 }) 104 ts := httptest.NewServer(handler) 105 defer ts.Close() 106 107 config := v1.ImageTranslation{ 108 Rules: []v1.TranslationRule{ 109 { 110 Name: "image1", 111 URL: "http://%/base.qcow2", 112 Transport: "profile1", 113 }, 114 { 115 Name: "image2", 116 URL: "http://%/base.qcow2", 117 Transport: "profile2", 118 }, 119 { 120 Name: "image3", 121 URL: "http://%/base.qcow2", 122 Transport: "profile3", 123 }, 124 { 125 Name: "image4", 126 URL: "http://%/base.qcow2", 127 Transport: "profile4", 128 }, 129 }, 130 Transports: map[string]v1.TransportProfile{ 131 "profile1": {MaxRedirects: intptr(0)}, 132 "profile2": {MaxRedirects: intptr(1)}, 133 "profile3": {MaxRedirects: intptr(5)}, 134 "profile4": {MaxRedirects: nil}, 135 }, 136 } 137 138 downloader := image.NewDownloader("http") 139 for _, tst := range []struct { 140 name string 141 image string 142 mr int 143 expectedURLs int 144 mustFail bool 145 message string 146 }{ 147 { 148 name: "0 redirects, 0 allowed", 149 image: "image1", 150 mr: 0, 151 expectedURLs: 1, 152 mustFail: false, 153 message: "image download without redirects must succeed even if no redirects allowed", 154 }, 155 { 156 name: "1 redirect, 0 allowed", 157 image: "image1", 158 mr: 1, 159 expectedURLs: 1, 160 mustFail: true, 161 message: "image download with redirects cannot succeed when no redirects allowed", 162 }, 163 { 164 name: "1 redirect, 1 allowed", 165 image: "image2", 166 mr: 1, 167 expectedURLs: 2, 168 mustFail: false, 169 message: "image download must succeed when number of redirects doesn't exceed maximum", 170 }, 171 { 172 name: "5 redirect, 5 allowed", 173 image: "image3", 174 mr: 5, 175 expectedURLs: 6, 176 mustFail: false, 177 message: "image download must succeed when number of redirects doesn't exceed maximum", 178 }, 179 { 180 name: "2 redirect, 1 allowed", 181 image: "image2", 182 mr: 2, 183 expectedURLs: 2, 184 mustFail: true, 185 message: "image download must fail when number of redirects exceeds maximum value", 186 }, 187 { 188 name: "10 redirect, 5 allowed", 189 image: "image3", 190 mr: 10, 191 expectedURLs: 6, 192 mustFail: true, 193 message: "image download must fail when number of redirects exceeds maximum value", 194 }, 195 { 196 name: "9 redirect, 9 (default) allowed", 197 image: "image4", 198 mr: 9, 199 expectedURLs: 10, 200 mustFail: false, 201 message: "image download must not fail when number of redirects doesn't exceed maximum value", 202 }, 203 { 204 name: "10 redirect, 9 (default) allowed", 205 image: "image4", 206 mr: 10, 207 expectedURLs: 10, 208 mustFail: true, 209 message: "image download must fail when number of redirects exceeds maximum value", 210 }, 211 } { 212 t.Run(tst.name, func(t *testing.T) { 213 urls = nil 214 handledCount = 0 215 maxRedirects = tst.mr 216 err := downloader.DownloadFile(context.Background(), translate(config, tst.image, ts), ioutil.Discard) 217 if handledCount == 0 { 218 t.Error("http handler wasn't called") 219 } else if (err != nil) != tst.mustFail { 220 t.Error(tst.message) 221 } 222 223 if len(urls) != tst.expectedURLs { 224 t.Errorf("unexpected number of redirects for %q: %d != %d", tst.image, len(urls), tst.expectedURLs) 225 } else { 226 for i, r := range urls { 227 if i == 0 && r != "/base.qcow2" || i > 0 && r != fmt.Sprintf("/file%d", i) { 228 t.Errorf("unexpected URL #%d %s for %q", i, r, tst.image) 229 } 230 } 231 } 232 }) 233 } 234 } 235 236 func TestImageDownloadWithProxy(t *testing.T) { 237 handled := false 238 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 239 handled = true 240 if r.URL.String() != "http://example.net/base.qcow2" { 241 t.Fatalf("proxy server was used for wrong URL %v", r.URL) 242 } 243 }) 244 ts := httptest.NewServer(handler) 245 defer ts.Close() 246 247 config := v1.ImageTranslation{ 248 Rules: []v1.TranslationRule{ 249 { 250 Name: "image1", 251 URL: "example.net/base.qcow2", 252 }, 253 }, 254 Transports: map[string]v1.TransportProfile{ 255 "": {Proxy: "http://" + ts.Listener.Addr().String()}, 256 }, 257 } 258 259 download(t, "http", config, "image1", ts) 260 if !handled { 261 t.Fatal("image was not downloaded") 262 } 263 } 264 265 func TestImageDownloadWithTimeout(t *testing.T) { 266 handled := false 267 var timeout time.Duration 268 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 269 handled = true 270 time.Sleep(timeout) 271 }) 272 ts := httptest.NewServer(handler) 273 defer ts.Close() 274 275 config := v1.ImageTranslation{ 276 Rules: []v1.TranslationRule{ 277 { 278 Name: "image", 279 URL: "%/base.qcow2", 280 }, 281 }, 282 Transports: map[string]v1.TransportProfile{ 283 "": {TimeoutMilliseconds: 250}, 284 }, 285 } 286 287 downloader := image.NewDownloader("http") 288 for _, tst := range []struct { 289 name string 290 timeout time.Duration 291 mustFail bool 292 }{ 293 { 294 name: "positive test", 295 timeout: time.Millisecond * 50, 296 mustFail: false, 297 }, 298 { 299 name: "negative test", 300 timeout: time.Millisecond * 350, 301 mustFail: true, 302 }, 303 } { 304 t.Run(tst.name, func(t *testing.T) { 305 handled = false 306 timeout = tst.timeout 307 err := downloader.DownloadFile(context.Background(), translate(config, "image", ts), ioutil.Discard) 308 if err == nil && tst.mustFail { 309 t.Error("no error happened when timeout was expected") 310 } else if err != nil && !tst.mustFail { 311 t.Fatal(err) 312 } 313 if !handled { 314 t.Fatal("image was not downloaded") 315 } 316 }) 317 } 318 } 319 320 func TestImageDownloadTLS(t *testing.T) { 321 ca, caKey := testutils.GenerateCert(t, true, "CA", nil, nil) 322 cert, key := testutils.GenerateCert(t, false, "127.0.0.1", ca, caKey) 323 324 handled := false 325 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 326 handled = r.TLS != nil 327 }) 328 ts := httptest.NewUnstartedServer(handler) 329 ts.TLS = &tls.Config{ 330 Certificates: []tls.Certificate{ 331 { 332 Certificate: [][]byte{cert.Raw}, 333 PrivateKey: key, 334 }, 335 }, 336 } 337 ts.StartTLS() 338 defer ts.Close() 339 340 config := v1.ImageTranslation{ 341 Rules: []v1.TranslationRule{ 342 { 343 Name: "image1", 344 URL: "%/base.qcow2", 345 Transport: "tlsProfile", 346 }, 347 }, 348 Transports: map[string]v1.TransportProfile{ 349 "tlsProfile": { 350 TLS: &v1.TLSConfig{ 351 Certificates: []v1.TLSCertificate{ 352 {Cert: testutils.EncodePEMCert(ca)}, 353 }, 354 }, 355 }, 356 }, 357 } 358 359 download(t, "https", config, "image1", ts) 360 if !handled { 361 t.Fatal("image was not downloaded") 362 } 363 } 364 365 func TestImageDownloadTLSWithClientCerts(t *testing.T) { 366 ca, caKey := testutils.GenerateCert(t, true, "CA", nil, nil) 367 serverCert, serverKey := testutils.GenerateCert(t, false, "127.0.0.1", ca, caKey) 368 clientCert, clientKey := testutils.GenerateCert(t, false, "127.0.0.1", serverCert, serverKey) 369 370 handled := false 371 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 372 handled = r.TLS != nil 373 if len(r.TLS.PeerCertificates) != 1 { 374 t.Fatal("client certificate wasn't used") 375 } 376 if r.TLS.PeerCertificates[0].SerialNumber.Cmp(clientCert.SerialNumber) != 0 { 377 t.Error("wrong certificate was used") 378 } 379 }) 380 ts := httptest.NewUnstartedServer(handler) 381 ts.TLS = &tls.Config{ 382 Certificates: []tls.Certificate{ 383 { 384 Certificate: [][]byte{serverCert.Raw}, 385 PrivateKey: serverKey, 386 }, 387 }, 388 ClientAuth: tls.RequestClientCert, 389 } 390 ts.StartTLS() 391 defer ts.Close() 392 393 config := v1.ImageTranslation{ 394 Rules: []v1.TranslationRule{ 395 { 396 Name: "image", 397 URL: "%/base.qcow2", 398 Transport: "tlsProfile", 399 }, 400 }, 401 Transports: map[string]v1.TransportProfile{ 402 "tlsProfile": { 403 TLS: &v1.TLSConfig{ 404 Certificates: []v1.TLSCertificate{ 405 { 406 Cert: testutils.EncodePEMCert(ca), 407 }, 408 { 409 Cert: testutils.EncodePEMCert(clientCert), 410 Key: testutils.EncodePEMKey(clientKey), 411 }, 412 }, 413 }, 414 }, 415 }, 416 } 417 418 download(t, "https", config, "image", ts) 419 if !handled { 420 t.Fatal("image was not downloaded") 421 } 422 } 423 424 func TestImageDownloadTLSWithServerName(t *testing.T) { 425 ca, caKey := testutils.GenerateCert(t, true, "CA", nil, nil) 426 cert, key := testutils.GenerateCert(t, false, "test.corp", ca, caKey) 427 428 handled := false 429 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 430 handled = r.TLS != nil 431 }) 432 ts := httptest.NewUnstartedServer(handler) 433 ts.TLS = &tls.Config{ 434 Certificates: []tls.Certificate{ 435 { 436 Certificate: [][]byte{cert.Raw}, 437 PrivateKey: key, 438 }, 439 }, 440 } 441 ts.StartTLS() 442 defer ts.Close() 443 444 config := v1.ImageTranslation{ 445 Rules: []v1.TranslationRule{ 446 { 447 Name: "image", 448 URL: "%/base.qcow2", 449 Transport: "tlsProfile", 450 }, 451 }, 452 Transports: map[string]v1.TransportProfile{ 453 "tlsProfile": { 454 TLS: &v1.TLSConfig{ 455 Certificates: []v1.TLSCertificate{ 456 {Cert: testutils.EncodePEMCert(ca)}, 457 }, 458 ServerName: "test.corp", 459 }, 460 }, 461 }, 462 } 463 464 download(t, "https", config, "image", ts) 465 if !handled { 466 t.Fatal("image was not downloaded") 467 } 468 } 469 470 func TestImageDownloadTLSWithoutCertValidation(t *testing.T) { 471 handled := false 472 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 473 handled = r.TLS != nil 474 }) 475 ts := httptest.NewUnstartedServer(handler) 476 ts.StartTLS() 477 defer ts.Close() 478 479 config := v1.ImageTranslation{ 480 Rules: []v1.TranslationRule{ 481 { 482 Name: "image", 483 URL: "%/base.qcow2", 484 Transport: "tlsProfile", 485 }, 486 }, 487 Transports: map[string]v1.TransportProfile{ 488 "tlsProfile": { 489 TLS: &v1.TLSConfig{Insecure: true}, 490 }, 491 }, 492 } 493 494 download(t, "https", config, "image", ts) 495 if !handled { 496 t.Fatal("image was not downloaded") 497 } 498 }