X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/ccfad8f850f6d3edc43e7757fbeed112864efd18..dab0c5596e39dc455d88bba797717e829fe5caf5:/services/keep-web/server_test.go diff --git a/services/keep-web/server_test.go b/services/keep-web/server_test.go index 7e738cb9f3..61c540808b 100644 --- a/services/keep-web/server_test.go +++ b/services/keep-web/server_test.go @@ -2,9 +2,11 @@ // // SPDX-License-Identifier: AGPL-3.0 -package main +package keepweb import ( + "bytes" + "context" "crypto/md5" "encoding/json" "fmt" @@ -12,15 +14,21 @@ import ( "io/ioutil" "net" "net/http" + "net/http/httptest" "os" "os/exec" + "regexp" "strings" "testing" + "time" - "git.curoverse.com/arvados.git/sdk/go/arvados" - "git.curoverse.com/arvados.git/sdk/go/arvadosclient" - "git.curoverse.com/arvados.git/sdk/go/arvadostest" - "git.curoverse.com/arvados.git/sdk/go/keepclient" + "git.arvados.org/arvados.git/lib/config" + "git.arvados.org/arvados.git/sdk/go/arvados" + "git.arvados.org/arvados.git/sdk/go/arvadosclient" + "git.arvados.org/arvados.git/sdk/go/arvadostest" + "git.arvados.org/arvados.git/sdk/go/ctxlog" + "git.arvados.org/arvados.git/sdk/go/httpserver" + "git.arvados.org/arvados.git/sdk/go/keepclient" check "gopkg.in/check.v1" ) @@ -30,7 +38,8 @@ var _ = check.Suite(&IntegrationSuite{}) // IntegrationSuite tests need an API server and a keep-web server type IntegrationSuite struct { - testServer *server + testServer *httptest.Server + handler *handler } func (s *IntegrationSuite) TestNoToken(c *check.C) { @@ -40,17 +49,17 @@ func (s *IntegrationSuite) TestNoToken(c *check.C) { } { hdr, body, _ := s.runCurl(c, token, "collections.example.com", "/collections/"+arvadostest.FooCollection+"/foo") c.Check(hdr, check.Matches, `(?s)HTTP/1.1 404 Not Found\r\n.*`) - c.Check(body, check.Equals, "") + c.Check(body, check.Equals, notFoundMessage+"\n") if token != "" { hdr, body, _ = s.runCurl(c, token, "collections.example.com", "/collections/download/"+arvadostest.FooCollection+"/"+token+"/foo") c.Check(hdr, check.Matches, `(?s)HTTP/1.1 404 Not Found\r\n.*`) - c.Check(body, check.Equals, "") + c.Check(body, check.Equals, notFoundMessage+"\n") } hdr, body, _ = s.runCurl(c, token, "collections.example.com", "/bad-route") c.Check(hdr, check.Matches, `(?s)HTTP/1.1 404 Not Found\r\n.*`) - c.Check(body, check.Equals, "") + c.Check(body, check.Equals, notFoundMessage+"\n") } } @@ -83,7 +92,7 @@ func (s *IntegrationSuite) Test404(c *check.C) { hdr, body, _ := s.runCurl(c, arvadostest.ActiveToken, "collections.example.com", uri) c.Check(hdr, check.Matches, "(?s)HTTP/1.1 404 Not Found\r\n.*") if len(body) > 0 { - c.Check(body, check.Equals, "404 page not found\n") + c.Check(body, check.Equals, notFoundMessage+"\n") } } } @@ -148,7 +157,7 @@ type curlCase struct { } func (s *IntegrationSuite) Test200(c *check.C) { - s.testServer.Config.AnonymousTokens = []string{arvadostest.AnonymousToken} + s.handler.Cluster.Users.AnonymousUserToken = arvadostest.AnonymousToken for _, spec := range []curlCase{ // My collection { @@ -164,16 +173,16 @@ func (s *IntegrationSuite) Test200(c *check.C) { dataMD5: "acbd18db4cc2f85cedef654fccc4a4d8", }, { - host: strings.Replace(arvadostest.FooPdh, "+", "-", 1) + ".collections.example.com", + host: strings.Replace(arvadostest.FooCollectionPDH, "+", "-", 1) + ".collections.example.com", path: "/t=" + arvadostest.ActiveToken + "/foo", dataMD5: "acbd18db4cc2f85cedef654fccc4a4d8", }, { - path: "/c=" + arvadostest.FooPdh + "/t=" + arvadostest.ActiveToken + "/foo", + path: "/c=" + arvadostest.FooCollectionPDH + "/t=" + arvadostest.ActiveToken + "/foo", dataMD5: "acbd18db4cc2f85cedef654fccc4a4d8", }, { - path: "/c=" + strings.Replace(arvadostest.FooPdh, "+", "-", 1) + "/t=" + arvadostest.ActiveToken + "/_/foo", + path: "/c=" + strings.Replace(arvadostest.FooCollectionPDH, "+", "-", 1) + "/t=" + arvadostest.ActiveToken + "/_/foo", dataMD5: "acbd18db4cc2f85cedef654fccc4a4d8", }, { @@ -254,21 +263,26 @@ func (s *IntegrationSuite) Test200(c *check.C) { } // Return header block and body. -func (s *IntegrationSuite) runCurl(c *check.C, token, host, uri string, args ...string) (hdr, bodyPart string, bodySize int64) { +func (s *IntegrationSuite) runCurl(c *check.C, auth, host, uri string, args ...string) (hdr, bodyPart string, bodySize int64) { curlArgs := []string{"--silent", "--show-error", "--include"} - testHost, testPort, _ := net.SplitHostPort(s.testServer.Addr) + testHost, testPort, _ := net.SplitHostPort(s.testServer.URL[7:]) curlArgs = append(curlArgs, "--resolve", host+":"+testPort+":"+testHost) - if token != "" { - curlArgs = append(curlArgs, "-H", "Authorization: OAuth2 "+token) + if strings.Contains(auth, " ") { + // caller supplied entire Authorization header value + curlArgs = append(curlArgs, "-H", "Authorization: "+auth) + } else if auth != "" { + // caller supplied Arvados token + curlArgs = append(curlArgs, "-H", "Authorization: Bearer "+auth) } curlArgs = append(curlArgs, args...) curlArgs = append(curlArgs, "http://"+host+":"+testPort+uri) c.Log(fmt.Sprintf("curlArgs == %#v", curlArgs)) cmd := exec.Command("curl", curlArgs...) stdout, err := cmd.StdoutPipe() - c.Assert(err, check.Equals, nil) - cmd.Stderr = cmd.Stdout - go cmd.Start() + c.Assert(err, check.IsNil) + cmd.Stderr = os.Stderr + err = cmd.Start() + c.Assert(err, check.IsNil) buf := make([]byte, 2<<27) n, err := io.ReadFull(stdout, buf) // Discard (but measure size of) anything past 128 MiB. @@ -276,9 +290,9 @@ func (s *IntegrationSuite) runCurl(c *check.C, token, host, uri string, args ... if err == io.ErrUnexpectedEOF { buf = buf[:n] } else { - c.Assert(err, check.Equals, nil) + c.Assert(err, check.IsNil) discarded, err = io.Copy(ioutil.Discard, stdout) - c.Assert(err, check.Equals, nil) + c.Assert(err, check.IsNil) } err = cmd.Wait() // Without "-f", curl exits 0 as long as it gets a valid HTTP @@ -296,18 +310,97 @@ func (s *IntegrationSuite) runCurl(c *check.C, token, host, uri string, args ... return } +// Run a full-featured server, including the metrics/health routes +// that are added by service.Command. +func (s *IntegrationSuite) runServer(c *check.C) (cluster arvados.Cluster, srvaddr string, logbuf *bytes.Buffer) { + logbuf = &bytes.Buffer{} + cluster = *s.handler.Cluster + cluster.Services.WebDAV.InternalURLs = map[arvados.URL]arvados.ServiceInstance{{Scheme: "http", Host: "0.0.0.0:0"}: {}} + cluster.Services.WebDAVDownload.InternalURLs = map[arvados.URL]arvados.ServiceInstance{{Scheme: "http", Host: "0.0.0.0:0"}: {}} + + var configjson bytes.Buffer + json.NewEncoder(&configjson).Encode(arvados.Config{Clusters: map[string]arvados.Cluster{"zzzzz": cluster}}) + go Command.RunCommand("keep-web", []string{"-config=-"}, &configjson, os.Stderr, io.MultiWriter(os.Stderr, logbuf)) + for deadline := time.Now().Add(time.Second); deadline.After(time.Now()); time.Sleep(time.Second / 100) { + if m := regexp.MustCompile(`"Listen":"(.*?)"`).FindStringSubmatch(logbuf.String()); m != nil { + srvaddr = "http://" + m[1] + break + } + } + if srvaddr == "" { + c.Fatal("timed out") + } + return +} + +// Ensure uploads can take longer than API.RequestTimeout. +// +// Currently, this works only by accident: service.Command cancels the +// request context as usual (there is no exemption), but +// webdav.Handler doesn't notice if the request context is cancelled +// while waiting to send or receive file data. +func (s *IntegrationSuite) TestRequestTimeoutExemption(c *check.C) { + s.handler.Cluster.API.RequestTimeout = arvados.Duration(time.Second / 2) + _, srvaddr, _ := s.runServer(c) + + var coll arvados.Collection + arv, err := arvadosclient.MakeArvadosClient() + c.Assert(err, check.IsNil) + arv.ApiToken = arvadostest.ActiveTokenV2 + err = arv.Create("collections", map[string]interface{}{"ensure_unique_name": true}, &coll) + c.Assert(err, check.IsNil) + + pr, pw := io.Pipe() + go func() { + time.Sleep(time.Second) + pw.Write(make([]byte, 10000000)) + pw.Close() + }() + req, _ := http.NewRequest("PUT", srvaddr+"/testfile", pr) + req.Host = coll.UUID + ".example" + req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveTokenV2) + resp, err := http.DefaultClient.Do(req) + c.Assert(err, check.IsNil) + c.Check(resp.StatusCode, check.Equals, http.StatusCreated) + + req, _ = http.NewRequest("GET", srvaddr+"/testfile", nil) + req.Host = coll.UUID + ".example" + req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveTokenV2) + resp, err = http.DefaultClient.Do(req) + c.Assert(err, check.IsNil) + c.Check(resp.StatusCode, check.Equals, http.StatusOK) + time.Sleep(time.Second) + body, err := ioutil.ReadAll(resp.Body) + c.Check(err, check.IsNil) + c.Check(len(body), check.Equals, 10000000) +} + +func (s *IntegrationSuite) TestHealthCheckPing(c *check.C) { + cluster, srvaddr, _ := s.runServer(c) + req, _ := http.NewRequest("GET", srvaddr+"/_health/ping", nil) + req.Header.Set("Authorization", "Bearer "+cluster.ManagementToken) + resp, err := http.DefaultClient.Do(req) + c.Assert(err, check.IsNil) + c.Check(resp.StatusCode, check.Equals, http.StatusOK) + body, _ := ioutil.ReadAll(resp.Body) + c.Check(string(body), check.Matches, `{"health":"OK"}\n`) +} + func (s *IntegrationSuite) TestMetrics(c *check.C) { - origin := "http://" + s.testServer.Addr - req, _ := http.NewRequest("GET", origin+"/notfound", nil) + cluster, srvaddr, _ := s.runServer(c) + + req, _ := http.NewRequest("GET", srvaddr+"/notfound", nil) + req.Host = cluster.Services.WebDAVDownload.ExternalURL.Host _, err := http.DefaultClient.Do(req) c.Assert(err, check.IsNil) - req, _ = http.NewRequest("GET", origin+"/by_id/", nil) + req, _ = http.NewRequest("GET", srvaddr+"/by_id/", nil) + req.Host = cluster.Services.WebDAVDownload.ExternalURL.Host req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken) resp, err := http.DefaultClient.Do(req) c.Assert(err, check.IsNil) - c.Check(resp.StatusCode, check.Equals, http.StatusOK) + c.Assert(resp.StatusCode, check.Equals, http.StatusOK) for i := 0; i < 2; i++ { - req, _ = http.NewRequest("GET", origin+"/foo", nil) + req, _ = http.NewRequest("GET", srvaddr+"/foo", nil) req.Host = arvadostest.FooCollection + ".example.com" req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken) resp, err = http.DefaultClient.Do(req) @@ -318,19 +411,30 @@ func (s *IntegrationSuite) TestMetrics(c *check.C) { resp.Body.Close() } - s.testServer.Config.Cache.updateGauges() + time.Sleep(metricsUpdateInterval * 2) + + req, _ = http.NewRequest("GET", srvaddr+"/metrics.json", nil) + req.Host = cluster.Services.WebDAVDownload.ExternalURL.Host + resp, err = http.DefaultClient.Do(req) + c.Assert(err, check.IsNil) + c.Check(resp.StatusCode, check.Equals, http.StatusUnauthorized) - req, _ = http.NewRequest("GET", origin+"/metrics.json", nil) + req, _ = http.NewRequest("GET", srvaddr+"/metrics.json", nil) + req.Host = cluster.Services.WebDAVDownload.ExternalURL.Host + req.Header.Set("Authorization", "Bearer badtoken") + resp, err = http.DefaultClient.Do(req) + c.Assert(err, check.IsNil) + c.Check(resp.StatusCode, check.Equals, http.StatusForbidden) + + req, _ = http.NewRequest("GET", srvaddr+"/metrics.json", nil) + req.Host = cluster.Services.WebDAVDownload.ExternalURL.Host + req.Header.Set("Authorization", "Bearer "+arvadostest.ManagementToken) resp, err = http.DefaultClient.Do(req) c.Assert(err, check.IsNil) c.Check(resp.StatusCode, check.Equals, http.StatusOK) type summary struct { - SampleCount string `json:"sample_count"` - SampleSum float64 `json:"sample_sum"` - Quantile []struct { - Quantile float64 - Value float64 - } + SampleCount string + SampleSum float64 } type counter struct { Value int64 @@ -372,27 +476,31 @@ func (s *IntegrationSuite) TestMetrics(c *check.C) { c.Check(summaries["request_duration_seconds/get/404"].SampleCount, check.Equals, "1") c.Check(summaries["time_to_status_seconds/get/404"].SampleCount, check.Equals, "1") c.Check(counters["arvados_keepweb_collectioncache_requests//"].Value, check.Equals, int64(2)) - c.Check(counters["arvados_keepweb_collectioncache_api_calls//"].Value, check.Equals, int64(1)) + c.Check(counters["arvados_keepweb_collectioncache_api_calls//"].Value, check.Equals, int64(2)) c.Check(counters["arvados_keepweb_collectioncache_hits//"].Value, check.Equals, int64(1)) c.Check(counters["arvados_keepweb_collectioncache_pdh_hits//"].Value, check.Equals, int64(1)) - c.Check(counters["arvados_keepweb_collectioncache_permission_hits//"].Value, check.Equals, int64(1)) c.Check(gauges["arvados_keepweb_collectioncache_cached_manifests//"].Value, check.Equals, float64(1)) - // FooCollection's cached manifest size is 45 ("1f4b0....+45") plus one 51-byte blob signature - c.Check(gauges["arvados_keepweb_collectioncache_cached_manifest_bytes//"].Value, check.Equals, float64(45+51)) + // FooCollection's cached manifest size is 45 ("1f4b0....+45") + // plus one 51-byte blob signature; session fs counts 3 inodes + // * 64 bytes. + c.Check(gauges["arvados_keepweb_sessions_cached_collection_bytes//"].Value, check.Equals, float64(45+51+64*3)) // If the Host header indicates a collection, /metrics.json // refers to a file in the collection -- the metrics handler - // must not intercept that route. - req, _ = http.NewRequest("GET", origin+"/metrics.json", nil) - req.Host = strings.Replace(arvadostest.FooCollectionPDH, "+", "-", -1) + ".example.com" - req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken) - resp, err = http.DefaultClient.Do(req) - c.Assert(err, check.IsNil) - c.Check(resp.StatusCode, check.Equals, http.StatusNotFound) + // must not intercept that route. Ditto health check paths. + for _, path := range []string{"/metrics.json", "/_health/ping"} { + c.Logf("path: %q", path) + req, _ = http.NewRequest("GET", srvaddr+path, nil) + req.Host = strings.Replace(arvadostest.FooCollectionPDH, "+", "-", -1) + ".example.com" + req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken) + resp, err = http.DefaultClient.Do(req) + c.Assert(err, check.IsNil) + c.Check(resp.StatusCode, check.Equals, http.StatusNotFound) + } } func (s *IntegrationSuite) SetUpSuite(c *check.C) { - arvadostest.StartAPI() + arvadostest.ResetDB(c) arvadostest.StartKeep(2, true) arv, err := arvadosclient.MakeArvadosClient() @@ -403,32 +511,40 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) { kc.PutB([]byte("Hello world\n")) kc.PutB([]byte("foo")) kc.PutB([]byte("foobar")) + kc.PutB([]byte("waz")) } func (s *IntegrationSuite) TearDownSuite(c *check.C) { arvadostest.StopKeep(2) - arvadostest.StopAPI() } func (s *IntegrationSuite) SetUpTest(c *check.C) { arvadostest.ResetEnv() - cfg := DefaultConfig() - cfg.Client = arvados.Client{ - APIHost: testAPIHost, - Insecure: true, - } - cfg.Listen = "127.0.0.1:0" - s.testServer = &server{Config: cfg} - err := s.testServer.Start() - c.Assert(err, check.Equals, nil) + logger := ctxlog.TestLogger(c) + ldr := config.NewLoader(&bytes.Buffer{}, logger) + cfg, err := ldr.Load() + c.Assert(err, check.IsNil) + cluster, err := cfg.GetCluster("") + c.Assert(err, check.IsNil) + + ctx := ctxlog.Context(context.Background(), logger) + + s.handler = newHandlerOrErrorHandler(ctx, cluster, cluster.SystemRootToken, nil).(*handler) + s.testServer = httptest.NewUnstartedServer( + httpserver.AddRequestIDs( + httpserver.LogRequests( + s.handler))) + s.testServer.Config.BaseContext = func(net.Listener) context.Context { return ctx } + s.testServer.Start() + + cluster.Services.WebDAV.InternalURLs = map[arvados.URL]arvados.ServiceInstance{{Host: s.testServer.URL[7:]}: {}} + cluster.Services.WebDAVDownload.InternalURLs = map[arvados.URL]arvados.ServiceInstance{{Host: s.testServer.URL[7:]}: {}} } func (s *IntegrationSuite) TearDownTest(c *check.C) { - var err error if s.testServer != nil { - err = s.testServer.Close() + s.testServer.Close() } - c.Check(err, check.Equals, nil) } // Gocheck boilerplate