github.com/equinix-metal/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  }