github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/storage/cloud/http_storage_test.go (about)

     1  // Copyright 2019 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package cloud
    12  
    13  import (
    14  	"bytes"
    15  	"context"
    16  	"encoding/pem"
    17  	"fmt"
    18  	"io"
    19  	"io/ioutil"
    20  	"net/http"
    21  	"net/http/httptest"
    22  	"net/url"
    23  	"os"
    24  	"path/filepath"
    25  	"strconv"
    26  	"strings"
    27  	"testing"
    28  	"time"
    29  
    30  	"github.com/cockroachdb/cockroach/pkg/base"
    31  	"github.com/cockroachdb/cockroach/pkg/blobs"
    32  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    33  	"github.com/cockroachdb/cockroach/pkg/testutils"
    34  	"github.com/cockroachdb/cockroach/pkg/util/ctxgroup"
    35  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    36  	"github.com/cockroachdb/cockroach/pkg/util/retry"
    37  	"github.com/stretchr/testify/require"
    38  )
    39  
    40  func TestPutHttp(t *testing.T) {
    41  	defer leaktest.AfterTest(t)()
    42  
    43  	tmp, dirCleanup := testutils.TempDir(t)
    44  	defer dirCleanup()
    45  
    46  	const badHeadResponse = "bad-head-response"
    47  
    48  	makeServer := func() (*url.URL, func() int, func()) {
    49  		var files int
    50  		srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    51  			localfile := filepath.Join(tmp, filepath.Base(r.URL.Path))
    52  			switch r.Method {
    53  			case "PUT":
    54  				f, err := os.Create(localfile)
    55  				if err != nil {
    56  					http.Error(w, err.Error(), 500)
    57  					return
    58  				}
    59  				defer f.Close()
    60  				if _, err := io.Copy(f, r.Body); err != nil {
    61  					http.Error(w, err.Error(), 500)
    62  					return
    63  				}
    64  				files++
    65  				w.WriteHeader(201)
    66  			case "GET", "HEAD":
    67  				if filepath.Base(localfile) == badHeadResponse {
    68  					http.Error(w, "HEAD not implemented", 500)
    69  					return
    70  				}
    71  				http.ServeFile(w, r, localfile)
    72  			case "DELETE":
    73  				if err := os.Remove(localfile); err != nil {
    74  					http.Error(w, err.Error(), 500)
    75  					return
    76  				}
    77  				w.WriteHeader(204)
    78  			default:
    79  				http.Error(w, "unsupported method "+r.Method, 400)
    80  			}
    81  		}))
    82  
    83  		u := testSettings.MakeUpdater()
    84  		if err := u.Set(
    85  			cloudstorageHTTPCASetting,
    86  			string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: srv.Certificate().Raw})),
    87  			"s",
    88  		); err != nil {
    89  			t.Fatal(err)
    90  		}
    91  
    92  		cleanup := func() {
    93  			srv.Close()
    94  			if err := u.Set(cloudstorageHTTPCASetting, "", "s"); err != nil {
    95  				t.Fatal(err)
    96  			}
    97  		}
    98  
    99  		t.Logf("Mock HTTP Storage %q", srv.URL)
   100  		uri, err := url.Parse(srv.URL)
   101  		if err != nil {
   102  			srv.Close()
   103  			t.Fatal(err)
   104  		}
   105  		uri.Path = filepath.Join(uri.Path, "testing")
   106  		return uri, func() int { return files }, cleanup
   107  	}
   108  
   109  	t.Run("singleHost", func(t *testing.T) {
   110  		srv, files, cleanup := makeServer()
   111  		defer cleanup()
   112  		testExportStore(t, srv.String(), false)
   113  		if expected, actual := 13, files(); expected != actual {
   114  			t.Fatalf("expected %d files to be written to single http store, got %d", expected, actual)
   115  		}
   116  	})
   117  
   118  	t.Run("multiHost", func(t *testing.T) {
   119  		srv1, files1, cleanup1 := makeServer()
   120  		defer cleanup1()
   121  		srv2, files2, cleanup2 := makeServer()
   122  		defer cleanup2()
   123  		srv3, files3, cleanup3 := makeServer()
   124  		defer cleanup3()
   125  
   126  		combined := *srv1
   127  		combined.Host = strings.Join([]string{srv1.Host, srv2.Host, srv3.Host}, ",")
   128  
   129  		testExportStore(t, combined.String(), true)
   130  		if expected, actual := 3, files1(); expected != actual {
   131  			t.Fatalf("expected %d files written to http host 1, got %d", expected, actual)
   132  		}
   133  		if expected, actual := 4, files2(); expected != actual {
   134  			t.Fatalf("expected %d files written to http host 2, got %d", expected, actual)
   135  		}
   136  		if expected, actual := 4, files3(); expected != actual {
   137  			t.Fatalf("expected %d files written to http host 3, got %d", expected, actual)
   138  		}
   139  	})
   140  
   141  	// Ensure that servers that error on HEAD are handled gracefully.
   142  	t.Run("bad-head-response", func(t *testing.T) {
   143  		ctx := context.Background()
   144  
   145  		srv, _, cleanup := makeServer()
   146  		defer cleanup()
   147  
   148  		conf, err := ExternalStorageConfFromURI(srv.String())
   149  		if err != nil {
   150  			t.Fatal(err)
   151  		}
   152  		s, err := MakeExternalStorage(ctx, conf, base.ExternalIODirConfig{},
   153  			testSettings, blobs.TestEmptyBlobClientFactory)
   154  		if err != nil {
   155  			t.Fatal(err)
   156  		}
   157  		defer s.Close()
   158  
   159  		const file = "file"
   160  		var content = []byte("contents")
   161  		if err := s.WriteFile(ctx, file, bytes.NewReader(content)); err != nil {
   162  			t.Fatal(err)
   163  		}
   164  		if err := s.WriteFile(ctx, badHeadResponse, bytes.NewReader(content)); err != nil {
   165  			t.Fatal(err)
   166  		}
   167  		if sz, err := s.Size(ctx, file); err != nil {
   168  			t.Fatal(err)
   169  		} else if sz != int64(len(content)) {
   170  			t.Fatalf("expected %d, got %d", len(content), sz)
   171  		}
   172  		if sz, err := s.Size(ctx, badHeadResponse); !testutils.IsError(err, "500 Internal Server Error") {
   173  			t.Fatalf("unexpected error: %v", err)
   174  		} else if sz != 0 {
   175  			t.Fatalf("expected 0 size, got %d", sz)
   176  		}
   177  	})
   178  }
   179  
   180  func rangeStart(r string) (int, error) {
   181  	if len(r) == 0 {
   182  		return 0, nil
   183  	}
   184  	r = strings.TrimPrefix(r, "bytes=")
   185  
   186  	return strconv.Atoi(r[:strings.IndexByte(r, '-')])
   187  }
   188  
   189  func TestHttpGet(t *testing.T) {
   190  	defer leaktest.AfterTest(t)()
   191  	data := []byte("to serve, or not to serve.  c'est la question")
   192  
   193  	httpRetryOptions.InitialBackoff = 1 * time.Microsecond
   194  	httpRetryOptions.MaxBackoff = 10 * time.Millisecond
   195  	httpRetryOptions.MaxRetries = 100
   196  
   197  	for _, tc := range []int{1, 2, 5, 16, 32, len(data) - 1, len(data)} {
   198  		t.Run(fmt.Sprintf("read-%d", tc), func(t *testing.T) {
   199  			limit := tc
   200  			s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   201  				start, err := rangeStart(r.Header.Get("Range"))
   202  				if start < 0 || start >= len(data) {
   203  					t.Errorf("invalid start offset %d in range header %s",
   204  						start, r.Header.Get("Range"))
   205  				}
   206  				end := start + limit
   207  				if end > len(data) {
   208  					end = len(data)
   209  				}
   210  
   211  				w.Header().Add("Accept-Ranges", "bytes")
   212  				w.Header().Add("Content-Length", strconv.Itoa(len(data)-start))
   213  
   214  				if start > 0 {
   215  					w.Header().Add(
   216  						"Content-Range",
   217  						fmt.Sprintf("bytes %d-%d/%d", start, end, len(data)))
   218  				}
   219  
   220  				if err == nil {
   221  					_, err = w.Write(data[start:end])
   222  				}
   223  				if err != nil {
   224  					w.WriteHeader(http.StatusInternalServerError)
   225  				}
   226  			}))
   227  
   228  			// Start antagonist function that aggressively closes client connections.
   229  			ctx, cancelAntagonist := context.WithCancel(context.Background())
   230  			g := ctxgroup.WithContext(ctx)
   231  			g.GoCtx(func(ctx context.Context) error {
   232  				opts := retry.Options{
   233  					InitialBackoff: 500 * time.Microsecond,
   234  					MaxBackoff:     1 * time.Millisecond,
   235  				}
   236  				for attempt := retry.StartWithCtx(ctx, opts); attempt.Next(); {
   237  					s.CloseClientConnections()
   238  				}
   239  				return nil
   240  			})
   241  
   242  			store, err := makeHTTPStorage(s.URL, testSettings)
   243  			require.NoError(t, err)
   244  
   245  			var file io.ReadCloser
   246  
   247  			// Cleanup.
   248  			defer func() {
   249  				s.Close()
   250  				if store != nil {
   251  					require.NoError(t, store.Close())
   252  				}
   253  				if file != nil {
   254  					require.NoError(t, file.Close())
   255  				}
   256  				cancelAntagonist()
   257  				_ = g.Wait()
   258  			}()
   259  
   260  			// Read the file and verify results.
   261  			file, err = store.ReadFile(ctx, "/something")
   262  			require.NoError(t, err)
   263  
   264  			b, err := ioutil.ReadAll(file)
   265  			require.NoError(t, err)
   266  			require.EqualValues(t, data, b)
   267  		})
   268  	}
   269  }
   270  
   271  func TestHttpGetWithCancelledContext(t *testing.T) {
   272  	defer leaktest.AfterTest(t)()
   273  
   274  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
   275  	defer s.Close()
   276  
   277  	store, err := makeHTTPStorage(s.URL, testSettings)
   278  	require.NoError(t, err)
   279  	defer func() {
   280  		require.NoError(t, store.Close())
   281  	}()
   282  
   283  	ctx, cancel := context.WithCancel(context.Background())
   284  	cancel()
   285  
   286  	_, err = store.ReadFile(ctx, "/something")
   287  	require.Error(t, context.Canceled, err)
   288  }
   289  
   290  func TestCanDisableHttp(t *testing.T) {
   291  	conf := base.ExternalIODirConfig{
   292  		DisableHTTP: true,
   293  	}
   294  	s, err := MakeExternalStorage(
   295  		context.Background(),
   296  		roachpb.ExternalStorage{Provider: roachpb.ExternalStorageProvider_Http},
   297  		conf,
   298  		testSettings, blobs.TestEmptyBlobClientFactory)
   299  	require.Nil(t, s)
   300  	require.Error(t, err)
   301  }
   302  
   303  func TestExternalStorageCanUseHTTPProxy(t *testing.T) {
   304  	proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   305  		_, _ = w.Write([]byte(fmt.Sprintf("proxied-%s", r.URL)))
   306  	}))
   307  	defer proxy.Close()
   308  
   309  	// Normally, we would set proxy via HTTP_PROXY environment variable.
   310  	// However, if we run multiple tests in this package, and earlier tests
   311  	// happen to create an http client, then the DefaultTransport will have
   312  	// been been initialized with an empty Proxy.  So, set proxy directly.
   313  	http.DefaultTransport.(*http.Transport).Proxy = func(_ *http.Request) (*url.URL, error) {
   314  		return url.Parse(proxy.URL)
   315  	}
   316  	defer func() {
   317  		http.DefaultTransport.(*http.Transport).Proxy = nil
   318  	}()
   319  
   320  	conf, err := ExternalStorageConfFromURI("http://my-server")
   321  	require.NoError(t, err)
   322  	s, err := MakeExternalStorage(
   323  		context.Background(), conf, base.ExternalIODirConfig{}, testSettings, nil)
   324  	require.NoError(t, err)
   325  	stream, err := s.ReadFile(context.Background(), "file")
   326  	require.NoError(t, err)
   327  	defer stream.Close()
   328  	data, err := ioutil.ReadAll(stream)
   329  	require.NoError(t, err)
   330  
   331  	require.EqualValues(t, "proxied-http://my-server/file", string(data))
   332  }