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 }