web/v1/api: add tests for admin actions (#4767)

Signed-off-by: Simon Pasquier <spasquie@redhat.com>
This commit is contained in:
Simon Pasquier 2018-11-15 14:22:16 +01:00 committed by GitHub
parent 5464c64853
commit 6fa8de132b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 265 additions and 36 deletions

View file

@ -34,7 +34,7 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
"github.com/prometheus/common/route" "github.com/prometheus/common/route"
"github.com/prometheus/tsdb" tsdbLabels "github.com/prometheus/tsdb/labels"
"github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/pkg/gate" "github.com/prometheus/prometheus/pkg/gate"
@ -49,7 +49,6 @@ import (
"github.com/prometheus/prometheus/storage/remote" "github.com/prometheus/prometheus/storage/remote"
"github.com/prometheus/prometheus/util/httputil" "github.com/prometheus/prometheus/util/httputil"
"github.com/prometheus/prometheus/util/stats" "github.com/prometheus/prometheus/util/stats"
tsdbLabels "github.com/prometheus/tsdb/labels"
) )
const ( const (
@ -131,6 +130,14 @@ func setCORS(w http.ResponseWriter) {
type apiFunc func(r *http.Request) (interface{}, *apiError, func()) type apiFunc func(r *http.Request) (interface{}, *apiError, func())
// TSDBAdmin defines the tsdb interfaces used by the v1 API for admin operations.
type TSDBAdmin interface {
CleanTombstones() error
Delete(mint, maxt int64, ms ...tsdbLabels.Matcher) error
Dir() string
Snapshot(dir string, withHead bool) error
}
// API can register a set of endpoints in a router and handle // API can register a set of endpoints in a router and handle
// them using the provided storage and query engine. // them using the provided storage and query engine.
type API struct { type API struct {
@ -145,7 +152,7 @@ type API struct {
flagsMap map[string]string flagsMap map[string]string
ready func(http.HandlerFunc) http.HandlerFunc ready func(http.HandlerFunc) http.HandlerFunc
db func() *tsdb.DB db func() TSDBAdmin
enableAdmin bool enableAdmin bool
logger log.Logger logger log.Logger
remoteReadSampleLimit int remoteReadSampleLimit int
@ -166,7 +173,7 @@ func NewAPI(
configFunc func() config.Config, configFunc func() config.Config,
flagsMap map[string]string, flagsMap map[string]string,
readyFunc func(http.HandlerFunc) http.HandlerFunc, readyFunc func(http.HandlerFunc) http.HandlerFunc,
db func() *tsdb.DB, db func() TSDBAdmin,
enableAdmin bool, enableAdmin bool,
logger log.Logger, logger log.Logger,
rr rulesRetriever, rr rulesRetriever,
@ -950,7 +957,7 @@ func (api *API) snapshot(r *http.Request) (interface{}, *apiError, func()) {
if r.FormValue("skip_head") != "" { if r.FormValue("skip_head") != "" {
skipHead, err = strconv.ParseBool(r.FormValue("skip_head")) skipHead, err = strconv.ParseBool(r.FormValue("skip_head"))
if err != nil { if err != nil {
return nil, &apiError{errorUnavailable, fmt.Errorf("unable to parse boolean 'skip_head' argument: %v", err)}, nil return nil, &apiError{errorBadData, fmt.Errorf("unable to parse boolean 'skip_head' argument: %v", err)}, nil
} }
} }

View file

@ -24,13 +24,13 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/go-kit/kit/log" "github.com/go-kit/kit/log"
"github.com/gogo/protobuf/proto" "github.com/gogo/protobuf/proto"
"github.com/golang/snappy" "github.com/golang/snappy"
config_util "github.com/prometheus/common/config" config_util "github.com/prometheus/common/config"
@ -49,6 +49,7 @@ import (
"github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/storage/remote" "github.com/prometheus/prometheus/storage/remote"
"github.com/prometheus/prometheus/util/testutil" "github.com/prometheus/prometheus/util/testutil"
tsdbLabels "github.com/prometheus/tsdb/labels"
) )
type testTargetRetriever struct{} type testTargetRetriever struct{}
@ -809,39 +810,49 @@ func testEndpoints(t *testing.T, api *API, testLabelAPI bool) {
t.Fatal(err) t.Fatal(err)
} }
resp, apiErr, _ := test.endpoint(req.WithContext(ctx)) resp, apiErr, _ := test.endpoint(req.WithContext(ctx))
if apiErr != nil { assertAPIError(t, apiErr, test.errType)
if test.errType == errorNone { assertAPIResponse(t, resp, test.response)
t.Fatalf("Unexpected error: %s", apiErr)
}
if test.errType != apiErr.typ {
t.Fatalf("Expected error of type %q but got type %q", test.errType, apiErr.typ)
}
continue
}
if apiErr == nil && test.errType != errorNone {
t.Fatalf("Expected error of type %q but got none", test.errType)
}
if !reflect.DeepEqual(resp, test.response) {
respJSON, err := json.Marshal(resp)
if err != nil {
t.Fatalf("failed to marshal response as JSON: %v", err.Error())
}
expectedRespJSON, err := json.Marshal(test.response)
if err != nil {
t.Fatalf("failed to marshal expected response as JSON: %v", err.Error())
}
t.Fatalf(
"Response does not match, expected:\n%+v\ngot:\n%+v",
string(expectedRespJSON),
string(respJSON),
)
}
} }
} }
} }
func assertAPIError(t *testing.T, got *apiError, exp errorType) {
t.Helper()
if got != nil {
if exp == errorNone {
t.Fatalf("Unexpected error: %s", got)
}
if exp != got.typ {
t.Fatalf("Expected error of type %q but got type %q (%q)", exp, got.typ, got)
}
return
}
if got == nil && exp != errorNone {
t.Fatalf("Expected error of type %q but got none", exp)
}
}
func assertAPIResponse(t *testing.T, got interface{}, exp interface{}) {
if !reflect.DeepEqual(exp, got) {
respJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("failed to marshal response as JSON: %v", err.Error())
}
expectedRespJSON, err := json.Marshal(exp)
if err != nil {
t.Fatalf("failed to marshal expected response as JSON: %v", err.Error())
}
t.Fatalf(
"Response does not match, expected:\n%+v\ngot:\n%+v",
string(expectedRespJSON),
string(respJSON),
)
}
}
func TestReadEndpoint(t *testing.T) { func TestReadEndpoint(t *testing.T) {
suite, err := promql.NewTest(t, ` suite, err := promql.NewTest(t, `
load 1m load 1m
@ -944,6 +955,211 @@ func TestReadEndpoint(t *testing.T) {
} }
} }
type fakeDB struct {
err error
closer func()
}
func (f *fakeDB) CleanTombstones() error { return f.err }
func (f *fakeDB) Delete(mint, maxt int64, ms ...tsdbLabels.Matcher) error { return f.err }
func (f *fakeDB) Dir() string {
dir, _ := ioutil.TempDir("", "fakeDB")
f.closer = func() {
os.RemoveAll(dir)
}
return dir
}
func (f *fakeDB) Snapshot(dir string, withHead bool) error { return f.err }
func TestAdminEndpoints(t *testing.T) {
tsdb, tsdbWithError := &fakeDB{}, &fakeDB{err: fmt.Errorf("some error")}
snapshotAPI := func(api *API) apiFunc { return api.snapshot }
cleanAPI := func(api *API) apiFunc { return api.cleanTombstones }
deleteAPI := func(api *API) apiFunc { return api.deleteSeries }
for i, tc := range []struct {
db *fakeDB
enableAdmin bool
endpoint func(api *API) apiFunc
method string
values url.Values
errType errorType
}{
// Tests for the snapshot endpoint.
{
db: tsdb,
enableAdmin: false,
endpoint: snapshotAPI,
errType: errorUnavailable,
},
{
db: tsdb,
enableAdmin: true,
endpoint: snapshotAPI,
errType: errorNone,
},
{
db: tsdb,
enableAdmin: true,
endpoint: snapshotAPI,
values: map[string][]string{"skip_head": []string{"true"}},
errType: errorNone,
},
{
db: tsdb,
enableAdmin: true,
endpoint: snapshotAPI,
values: map[string][]string{"skip_head": []string{"xxx"}},
errType: errorBadData,
},
{
db: tsdbWithError,
enableAdmin: true,
endpoint: snapshotAPI,
errType: errorInternal,
},
{
db: nil,
enableAdmin: true,
endpoint: snapshotAPI,
errType: errorUnavailable,
},
// Tests for the cleanTombstones endpoint.
{
db: tsdb,
enableAdmin: false,
endpoint: cleanAPI,
errType: errorUnavailable,
},
{
db: tsdb,
enableAdmin: true,
endpoint: cleanAPI,
errType: errorNone,
},
{
db: tsdbWithError,
enableAdmin: true,
endpoint: cleanAPI,
errType: errorInternal,
},
{
db: nil,
enableAdmin: true,
endpoint: cleanAPI,
errType: errorUnavailable,
},
// Tests for the deleteSeries endpoint.
{
db: tsdb,
enableAdmin: false,
endpoint: deleteAPI,
errType: errorUnavailable,
},
{
db: tsdb,
enableAdmin: true,
endpoint: deleteAPI,
errType: errorBadData,
},
{
db: tsdb,
enableAdmin: true,
endpoint: deleteAPI,
values: map[string][]string{"match[]": []string{"123"}},
errType: errorBadData,
},
{
db: tsdb,
enableAdmin: true,
endpoint: deleteAPI,
values: map[string][]string{"match[]": []string{"up"}, "start": []string{"xxx"}},
errType: errorBadData,
},
{
db: tsdb,
enableAdmin: true,
endpoint: deleteAPI,
values: map[string][]string{"match[]": []string{"up"}, "end": []string{"xxx"}},
errType: errorBadData,
},
{
db: tsdb,
enableAdmin: true,
endpoint: deleteAPI,
values: map[string][]string{"match[]": []string{"up"}},
errType: errorNone,
},
{
db: tsdb,
enableAdmin: true,
endpoint: deleteAPI,
values: map[string][]string{"match[]": []string{"up{job!=\"foo\"}", "{job=~\"bar.+\"}", "up{instance!~\"fred.+\"}"}},
errType: errorNone,
},
{
db: tsdbWithError,
enableAdmin: true,
endpoint: deleteAPI,
values: map[string][]string{"match[]": []string{"up"}},
errType: errorInternal,
},
{
db: nil,
enableAdmin: true,
endpoint: deleteAPI,
errType: errorUnavailable,
},
} {
tc := tc
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
api := &API{
db: func() TSDBAdmin {
if tc.db != nil {
return tc.db
}
return nil
},
ready: func(f http.HandlerFunc) http.HandlerFunc { return f },
enableAdmin: tc.enableAdmin,
}
defer func() {
if tc.db != nil && tc.db.closer != nil {
tc.db.closer()
}
}()
endpoint := tc.endpoint(api)
req, err := http.NewRequest(tc.method, fmt.Sprintf("?%s", tc.values.Encode()), nil)
if err != nil {
t.Fatalf("Error when creating test request: %s", err)
}
_, apiErr, _ := endpoint(req)
assertAPIError(t, apiErr, tc.errType)
})
}
}
func TestRespondSuccess(t *testing.T) { func TestRespondSuccess(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
api := API{} api := API{}

View file

@ -225,7 +225,12 @@ func New(logger log.Logger, o *Options) *Handler {
}, },
o.Flags, o.Flags,
h.testReady, h.testReady,
h.options.TSDB, func() api_v1.TSDBAdmin {
if db := h.options.TSDB(); db != nil {
return db
}
return nil
},
h.options.EnableAdminAPI, h.options.EnableAdminAPI,
logger, logger,
h.ruleManager, h.ruleManager,

View file

@ -287,6 +287,7 @@ func TestRoutePrefix(t *testing.T) {
testutil.Ok(t, err) testutil.Ok(t, err)
testutil.Equals(t, http.StatusOK, resp.StatusCode) testutil.Equals(t, http.StatusOK, resp.StatusCode)
} }
func TestDebugHandler(t *testing.T) { func TestDebugHandler(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
prefix, url string prefix, url string