1// Copyright 2016 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6//     http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package web
15
16import (
17	"context"
18	"encoding/json"
19	"fmt"
20	"io"
21	"io/ioutil"
22	"net"
23	"net/http"
24	"net/http/httptest"
25	"net/url"
26	"os"
27	"path/filepath"
28	"strconv"
29	"strings"
30	"sync"
31	"testing"
32	"time"
33
34	"github.com/prometheus/client_golang/prometheus"
35	prom_testutil "github.com/prometheus/client_golang/prometheus/testutil"
36	"github.com/stretchr/testify/require"
37
38	"github.com/prometheus/prometheus/config"
39	"github.com/prometheus/prometheus/notifier"
40	"github.com/prometheus/prometheus/rules"
41	"github.com/prometheus/prometheus/scrape"
42	"github.com/prometheus/prometheus/tsdb"
43)
44
45func TestMain(m *testing.M) {
46	// On linux with a global proxy the tests will fail as the go client(http,grpc) tries to connect through the proxy.
47	os.Setenv("no_proxy", "localhost,127.0.0.1,0.0.0.0,:")
48	os.Exit(m.Run())
49}
50
51func TestGlobalURL(t *testing.T) {
52	opts := &Options{
53		ListenAddress: ":9090",
54		ExternalURL: &url.URL{
55			Scheme: "https",
56			Host:   "externalhost:80",
57			Path:   "/path/prefix",
58		},
59	}
60
61	tests := []struct {
62		inURL  string
63		outURL string
64	}{
65		{
66			// Nothing should change if the input URL is not on localhost, even if the port is our listening port.
67			inURL:  "http://somehost:9090/metrics",
68			outURL: "http://somehost:9090/metrics",
69		},
70		{
71			// Port and host should change if target is on localhost and port is our listening port.
72			inURL:  "http://localhost:9090/metrics",
73			outURL: "https://externalhost:80/metrics",
74		},
75		{
76			// Only the host should change if the port is not our listening port, but the host is localhost.
77			inURL:  "http://localhost:8000/metrics",
78			outURL: "http://externalhost:8000/metrics",
79		},
80		{
81			// Alternative localhost representations should also work.
82			inURL:  "http://127.0.0.1:9090/metrics",
83			outURL: "https://externalhost:80/metrics",
84		},
85	}
86
87	for _, test := range tests {
88		inURL, err := url.Parse(test.inURL)
89
90		require.NoError(t, err)
91
92		globalURL := tmplFuncs("", opts)["globalURL"].(func(u *url.URL) *url.URL)
93		outURL := globalURL(inURL)
94
95		require.Equal(t, test.outURL, outURL.String())
96	}
97}
98
99type dbAdapter struct {
100	*tsdb.DB
101}
102
103func (a *dbAdapter) Stats(statsByLabelName string) (*tsdb.Stats, error) {
104	return a.Head().Stats(statsByLabelName), nil
105}
106
107func (a *dbAdapter) WALReplayStatus() (tsdb.WALReplayStatus, error) {
108	return tsdb.WALReplayStatus{}, nil
109}
110
111func TestReadyAndHealthy(t *testing.T) {
112	t.Parallel()
113
114	dbDir, err := ioutil.TempDir("", "tsdb-ready")
115	require.NoError(t, err)
116	defer func() { require.NoError(t, os.RemoveAll(dbDir)) }()
117
118	db, err := tsdb.Open(dbDir, nil, nil, nil, nil)
119	require.NoError(t, err)
120
121	opts := &Options{
122		ListenAddress:  ":9090",
123		ReadTimeout:    30 * time.Second,
124		MaxConnections: 512,
125		Context:        nil,
126		Storage:        nil,
127		LocalStorage:   &dbAdapter{db},
128		TSDBDir:        dbDir,
129		QueryEngine:    nil,
130		ScrapeManager:  &scrape.Manager{},
131		RuleManager:    &rules.Manager{},
132		Notifier:       nil,
133		RoutePrefix:    "/",
134		EnableAdminAPI: true,
135		ExternalURL: &url.URL{
136			Scheme: "http",
137			Host:   "localhost:9090",
138			Path:   "/",
139		},
140		Version:  &PrometheusVersion{},
141		Gatherer: prometheus.DefaultGatherer,
142	}
143
144	opts.Flags = map[string]string{}
145
146	webHandler := New(nil, opts)
147
148	webHandler.config = &config.Config{}
149	webHandler.notifier = &notifier.Manager{}
150	l, err := webHandler.Listener()
151	if err != nil {
152		panic(fmt.Sprintf("Unable to start web listener: %s", err))
153	}
154
155	ctx, cancel := context.WithCancel(context.Background())
156	defer cancel()
157	go func() {
158		err := webHandler.Run(ctx, l, "")
159		if err != nil {
160			panic(fmt.Sprintf("Can't start web handler:%s", err))
161		}
162	}()
163
164	// Give some time for the web goroutine to run since we need the server
165	// to be up before starting tests.
166	time.Sleep(5 * time.Second)
167
168	resp, err := http.Get("http://localhost:9090/-/healthy")
169	require.NoError(t, err)
170	require.Equal(t, http.StatusOK, resp.StatusCode)
171	cleanupTestResponse(t, resp)
172
173	for _, u := range []string{
174		"http://localhost:9090/-/ready",
175		"http://localhost:9090/classic/graph",
176		"http://localhost:9090/classic/flags",
177		"http://localhost:9090/classic/rules",
178		"http://localhost:9090/classic/service-discovery",
179		"http://localhost:9090/classic/targets",
180		"http://localhost:9090/classic/status",
181		"http://localhost:9090/classic/config",
182	} {
183		resp, err = http.Get(u)
184		require.NoError(t, err)
185		require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
186		cleanupTestResponse(t, resp)
187	}
188
189	resp, err = http.Post("http://localhost:9090/api/v1/admin/tsdb/snapshot", "", strings.NewReader(""))
190	require.NoError(t, err)
191	require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
192	cleanupTestResponse(t, resp)
193
194	resp, err = http.Post("http://localhost:9090/api/v1/admin/tsdb/delete_series", "", strings.NewReader("{}"))
195	require.NoError(t, err)
196	require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
197	cleanupTestResponse(t, resp)
198
199	// Set to ready.
200	webHandler.Ready()
201
202	for _, u := range []string{
203		"http://localhost:9090/-/healthy",
204		"http://localhost:9090/-/ready",
205		"http://localhost:9090/classic/graph",
206		"http://localhost:9090/classic/flags",
207		"http://localhost:9090/classic/rules",
208		"http://localhost:9090/classic/service-discovery",
209		"http://localhost:9090/classic/targets",
210		"http://localhost:9090/classic/status",
211		"http://localhost:9090/classic/config",
212	} {
213		resp, err = http.Get(u)
214		require.NoError(t, err)
215		require.Equal(t, http.StatusOK, resp.StatusCode)
216		cleanupTestResponse(t, resp)
217	}
218
219	resp, err = http.Post("http://localhost:9090/api/v1/admin/tsdb/snapshot", "", strings.NewReader(""))
220	require.NoError(t, err)
221	require.Equal(t, http.StatusOK, resp.StatusCode)
222	cleanupSnapshot(t, dbDir, resp)
223	cleanupTestResponse(t, resp)
224
225	resp, err = http.Post("http://localhost:9090/api/v1/admin/tsdb/delete_series?match[]=up", "", nil)
226	require.NoError(t, err)
227	require.Equal(t, http.StatusNoContent, resp.StatusCode)
228	cleanupTestResponse(t, resp)
229}
230
231func TestRoutePrefix(t *testing.T) {
232	t.Parallel()
233	dbDir, err := ioutil.TempDir("", "tsdb-ready")
234	require.NoError(t, err)
235	defer func() { require.NoError(t, os.RemoveAll(dbDir)) }()
236
237	db, err := tsdb.Open(dbDir, nil, nil, nil, nil)
238	require.NoError(t, err)
239
240	opts := &Options{
241		ListenAddress:  ":9091",
242		ReadTimeout:    30 * time.Second,
243		MaxConnections: 512,
244		Context:        nil,
245		TSDBDir:        dbDir,
246		LocalStorage:   &dbAdapter{db},
247		Storage:        nil,
248		QueryEngine:    nil,
249		ScrapeManager:  nil,
250		RuleManager:    nil,
251		Notifier:       nil,
252		RoutePrefix:    "/prometheus",
253		EnableAdminAPI: true,
254		ExternalURL: &url.URL{
255			Host:   "localhost.localdomain:9090",
256			Scheme: "http",
257		},
258	}
259
260	opts.Flags = map[string]string{}
261
262	webHandler := New(nil, opts)
263	l, err := webHandler.Listener()
264	if err != nil {
265		panic(fmt.Sprintf("Unable to start web listener: %s", err))
266	}
267	ctx, cancel := context.WithCancel(context.Background())
268	defer cancel()
269	go func() {
270		err := webHandler.Run(ctx, l, "")
271		if err != nil {
272			panic(fmt.Sprintf("Can't start web handler:%s", err))
273		}
274	}()
275
276	// Give some time for the web goroutine to run since we need the server
277	// to be up before starting tests.
278	time.Sleep(5 * time.Second)
279
280	resp, err := http.Get("http://localhost:9091" + opts.RoutePrefix + "/-/healthy")
281	require.NoError(t, err)
282	require.Equal(t, http.StatusOK, resp.StatusCode)
283	cleanupTestResponse(t, resp)
284
285	resp, err = http.Get("http://localhost:9091" + opts.RoutePrefix + "/-/ready")
286	require.NoError(t, err)
287	require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
288	cleanupTestResponse(t, resp)
289
290	resp, err = http.Post("http://localhost:9091"+opts.RoutePrefix+"/api/v1/admin/tsdb/snapshot", "", strings.NewReader(""))
291	require.NoError(t, err)
292	require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
293	cleanupTestResponse(t, resp)
294
295	resp, err = http.Post("http://localhost:9091"+opts.RoutePrefix+"/api/v1/admin/tsdb/delete_series", "", strings.NewReader("{}"))
296	require.NoError(t, err)
297	require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
298	cleanupTestResponse(t, resp)
299
300	// Set to ready.
301	webHandler.Ready()
302
303	resp, err = http.Get("http://localhost:9091" + opts.RoutePrefix + "/-/healthy")
304	require.NoError(t, err)
305	require.Equal(t, http.StatusOK, resp.StatusCode)
306	cleanupTestResponse(t, resp)
307
308	resp, err = http.Get("http://localhost:9091" + opts.RoutePrefix + "/-/ready")
309	require.NoError(t, err)
310	require.Equal(t, http.StatusOK, resp.StatusCode)
311	cleanupTestResponse(t, resp)
312
313	resp, err = http.Post("http://localhost:9091"+opts.RoutePrefix+"/api/v1/admin/tsdb/snapshot", "", strings.NewReader(""))
314	require.NoError(t, err)
315	require.Equal(t, http.StatusOK, resp.StatusCode)
316	cleanupSnapshot(t, dbDir, resp)
317	cleanupTestResponse(t, resp)
318
319	resp, err = http.Post("http://localhost:9091"+opts.RoutePrefix+"/api/v1/admin/tsdb/delete_series?match[]=up", "", nil)
320	require.NoError(t, err)
321	require.Equal(t, http.StatusNoContent, resp.StatusCode)
322	cleanupTestResponse(t, resp)
323}
324
325func TestDebugHandler(t *testing.T) {
326	for _, tc := range []struct {
327		prefix, url string
328		code        int
329	}{
330		{"/", "/debug/pprof/cmdline", 200},
331		{"/foo", "/foo/debug/pprof/cmdline", 200},
332
333		{"/", "/debug/pprof/goroutine", 200},
334		{"/foo", "/foo/debug/pprof/goroutine", 200},
335
336		{"/", "/debug/pprof/foo", 404},
337		{"/foo", "/bar/debug/pprof/goroutine", 404},
338	} {
339		opts := &Options{
340			RoutePrefix:   tc.prefix,
341			ListenAddress: "somehost:9090",
342			ExternalURL: &url.URL{
343				Host:   "localhost.localdomain:9090",
344				Scheme: "http",
345			},
346		}
347		handler := New(nil, opts)
348		handler.Ready()
349
350		w := httptest.NewRecorder()
351
352		req, err := http.NewRequest("GET", tc.url, nil)
353
354		require.NoError(t, err)
355
356		handler.router.ServeHTTP(w, req)
357
358		require.Equal(t, tc.code, w.Code)
359	}
360}
361
362func TestHTTPMetrics(t *testing.T) {
363	t.Parallel()
364	handler := New(nil, &Options{
365		RoutePrefix:   "/",
366		ListenAddress: "somehost:9090",
367		ExternalURL: &url.URL{
368			Host:   "localhost.localdomain:9090",
369			Scheme: "http",
370		},
371	})
372	getReady := func() int {
373		t.Helper()
374		w := httptest.NewRecorder()
375
376		req, err := http.NewRequest("GET", "/-/ready", nil)
377		require.NoError(t, err)
378
379		handler.router.ServeHTTP(w, req)
380		return w.Code
381	}
382
383	code := getReady()
384	require.Equal(t, http.StatusServiceUnavailable, code)
385	counter := handler.metrics.requestCounter
386	require.Equal(t, 1, int(prom_testutil.ToFloat64(counter.WithLabelValues("/-/ready", strconv.Itoa(http.StatusServiceUnavailable)))))
387
388	handler.Ready()
389	for range [2]int{} {
390		code = getReady()
391		require.Equal(t, http.StatusOK, code)
392	}
393	require.Equal(t, 2, int(prom_testutil.ToFloat64(counter.WithLabelValues("/-/ready", strconv.Itoa(http.StatusOK)))))
394	require.Equal(t, 1, int(prom_testutil.ToFloat64(counter.WithLabelValues("/-/ready", strconv.Itoa(http.StatusServiceUnavailable)))))
395}
396
397func TestShutdownWithStaleConnection(t *testing.T) {
398	dbDir, err := ioutil.TempDir("", "tsdb-ready")
399	require.NoError(t, err)
400	defer func() { require.NoError(t, os.RemoveAll(dbDir)) }()
401
402	db, err := tsdb.Open(dbDir, nil, nil, nil, nil)
403	require.NoError(t, err)
404
405	timeout := 10 * time.Second
406
407	opts := &Options{
408		ListenAddress:  ":9090",
409		ReadTimeout:    timeout,
410		MaxConnections: 512,
411		Context:        nil,
412		Storage:        nil,
413		LocalStorage:   &dbAdapter{db},
414		TSDBDir:        dbDir,
415		QueryEngine:    nil,
416		ScrapeManager:  &scrape.Manager{},
417		RuleManager:    &rules.Manager{},
418		Notifier:       nil,
419		RoutePrefix:    "/",
420		ExternalURL: &url.URL{
421			Scheme: "http",
422			Host:   "localhost:9090",
423			Path:   "/",
424		},
425		Version:  &PrometheusVersion{},
426		Gatherer: prometheus.DefaultGatherer,
427	}
428
429	opts.Flags = map[string]string{}
430
431	webHandler := New(nil, opts)
432
433	webHandler.config = &config.Config{}
434	webHandler.notifier = &notifier.Manager{}
435	l, err := webHandler.Listener()
436	if err != nil {
437		panic(fmt.Sprintf("Unable to start web listener: %s", err))
438	}
439
440	closed := make(chan struct{})
441
442	ctx, cancel := context.WithCancel(context.Background())
443	go func() {
444		err := webHandler.Run(ctx, l, "")
445		if err != nil {
446			panic(fmt.Sprintf("Can't start web handler:%s", err))
447		}
448		close(closed)
449	}()
450
451	// Give some time for the web goroutine to run since we need the server
452	// to be up before starting tests.
453	time.Sleep(5 * time.Second)
454
455	// Open a socket, and don't use it. This connection should then be closed
456	// after the ReadTimeout.
457	c, err := net.Dial("tcp", "localhost:9090")
458	require.NoError(t, err)
459	t.Cleanup(func() { require.NoError(t, c.Close()) })
460
461	// Stop the web handler.
462	cancel()
463
464	select {
465	case <-closed:
466	case <-time.After(timeout + 5*time.Second):
467		t.Fatalf("Server still running after read timeout.")
468	}
469}
470
471func TestHandleMultipleQuitRequests(t *testing.T) {
472	opts := &Options{
473		ListenAddress:   ":9090",
474		MaxConnections:  512,
475		EnableLifecycle: true,
476		RoutePrefix:     "/",
477		ExternalURL: &url.URL{
478			Scheme: "http",
479			Host:   "localhost:9090",
480			Path:   "/",
481		},
482	}
483	webHandler := New(nil, opts)
484	webHandler.config = &config.Config{}
485	webHandler.notifier = &notifier.Manager{}
486	l, err := webHandler.Listener()
487	if err != nil {
488		panic(fmt.Sprintf("Unable to start web listener: %s", err))
489	}
490	ctx, cancel := context.WithCancel(context.Background())
491	closed := make(chan struct{})
492	go func() {
493		err := webHandler.Run(ctx, l, "")
494		if err != nil {
495			panic(fmt.Sprintf("Can't start web handler:%s", err))
496		}
497		close(closed)
498	}()
499
500	// Give some time for the web goroutine to run since we need the server
501	// to be up before starting tests.
502	time.Sleep(5 * time.Second)
503
504	start := make(chan struct{})
505	var wg sync.WaitGroup
506	for i := 0; i < 3; i++ {
507		wg.Add(1)
508		go func() {
509			defer wg.Done()
510			<-start
511			resp, err := http.Post("http://localhost:9090/-/quit", "", strings.NewReader(""))
512			require.NoError(t, err)
513			require.Equal(t, http.StatusOK, resp.StatusCode)
514		}()
515	}
516	close(start)
517	wg.Wait()
518
519	// Stop the web handler.
520	cancel()
521
522	select {
523	case <-closed:
524	case <-time.After(5 * time.Second):
525		t.Fatalf("Server still running after 5 seconds.")
526	}
527}
528
529func cleanupTestResponse(t *testing.T, resp *http.Response) {
530	_, err := io.Copy(ioutil.Discard, resp.Body)
531	require.NoError(t, err)
532	require.NoError(t, resp.Body.Close())
533}
534
535func cleanupSnapshot(t *testing.T, dbDir string, resp *http.Response) {
536	snapshot := &struct {
537		Data struct {
538			Name string `json:"name"`
539		} `json:"data"`
540	}{}
541	b, err := ioutil.ReadAll(resp.Body)
542	require.NoError(t, err)
543	require.NoError(t, json.Unmarshal(b, snapshot))
544	require.NotZero(t, snapshot.Data.Name, "snapshot directory not returned")
545	require.NoError(t, os.Remove(filepath.Join(dbDir, "snapshots", snapshot.Data.Name)))
546	require.NoError(t, os.Remove(filepath.Join(dbDir, "snapshots")))
547}
548