server: Add param for diagnostic address binding

This adds a new config option for the OPA server (along with plumbing
from `opa run` downward to the server) to configure separate
diagnostic addresses to listen on. These will only be configured to
serve the /metrics and /health.

This will allow for more secure OPA deployments with the normal "data"
or "policies" API's made to be only accessible on localhost.

Fixes: #2002
Signed-off-by: Patrick East <east.patrick@gmail.com>
This commit is contained in:
Patrick East
2020-05-05 15:12:02 -07:00
parent 2766f151fa
commit 8b5cbfa07c
8 changed files with 675 additions and 204 deletions
+1
View File
@@ -120,6 +120,7 @@ File paths can be specified as URLs to resolve ambiguity in paths containing col
runCommand.Flags().BoolVarP(&cmdParams.serverMode, "server", "s", false, "start the runtime in server mode")
runCommand.Flags().StringVarP(&cmdParams.rt.HistoryPath, "history", "H", historyPath(), "set path of history file")
cmdParams.rt.Addrs = runCommand.Flags().StringSliceP("addr", "a", []string{defaultAddr}, "set listening address of the server (e.g., [ip]:<port> for TCP, unix://<path> for UNIX domain socket)")
cmdParams.rt.DiagnosticAddrs = runCommand.Flags().StringSlice("diagnostic-addr", []string{}, "set read-only diagnostic listening address of the server for /health and /metric APIs (e.g., [ip]:<port> for TCP, unix://<path> for UNIX domain socket)")
runCommand.Flags().StringVarP(&cmdParams.rt.InsecureAddr, "insecure-addr", "", "", "set insecure listening address of the server")
runCommand.Flags().MarkDeprecated("insecure-addr", "use --addr instead")
runCommand.Flags().StringVarP(&cmdParams.rt.OutputFormat, "format", "f", "pretty", "set shell output format, i.e, pretty, json")
+53 -4
View File
@@ -14,7 +14,8 @@ import (
func TestRunServerBase(t *testing.T) {
params := newRunParams()
params.rt = e2e.NewAPIServerTestParams()
params.rt.Addrs = &[]string{":0"}
params.rt.DiagnosticAddrs = &[]string{}
params.serverMode = true
ctx, cancel := context.WithCancel(context.Background())
@@ -22,14 +23,62 @@ func TestRunServerBase(t *testing.T) {
testRuntime := e2e.WrapRuntime(ctx, cancel, rt)
go startRuntime(ctx, rt, true)
done := make(chan bool)
go func() {
startRuntime(ctx, rt, true)
done <- true
}()
err := testRuntime.WaitForServer()
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
err = testRuntime.UploadData(bytes.NewBufferString(`{"x": 1}`))
validateBasicServe(t, testRuntime)
cancel()
<-done
}
func TestRunServerWithDiagnosticAddr(t *testing.T) {
params := newRunParams()
params.rt.Addrs = &[]string{":0"}
params.rt.DiagnosticAddrs = &[]string{":0"}
ctx, cancel := context.WithCancel(context.Background())
rt := initRuntime(ctx, params, nil)
testRuntime := e2e.WrapRuntime(ctx, cancel, rt)
done := make(chan bool)
go func() {
startRuntime(ctx, rt, true)
done <- true
}()
err := testRuntime.WaitForServer()
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
validateBasicServe(t, testRuntime)
diagURL, err := testRuntime.AddrToURL(rt.DiagnosticAddrs()[0])
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
if err := testRuntime.HealthCheck(diagURL); err != nil {
t.Error(err)
}
cancel()
<-done
}
func validateBasicServe(t *testing.T, runtime *e2e.TestRuntime) {
t.Helper()
err := runtime.UploadData(bytes.NewBufferString(`{"x": 1}`))
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
@@ -37,7 +86,7 @@ func TestRunServerBase(t *testing.T) {
resp := struct {
Result int `json:"result"`
}{}
err = testRuntime.GetDataWithInputTyped("x", nil, &resp)
err = runtime.GetDataWithInputTyped("x", nil, &resp)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
+19
View File
@@ -455,6 +455,25 @@ curl: (35) error:14094412:SSL routines:ssl3_read_bytes:sslv3 alert bad certifica
As you can see, TLS-based authentication disallows these request completely.
## Secure Health and Monitoring
Often OPA is deployed locally to the host where the client resides (side-car or
similar model). In these deployments it is ideal to only expose the API via
`localhost` to prevent any remote clients from reaching OPA at all. The downside
to this approach is that it blocks remote monitoring systems that require access
to `/health` or `/metrics`.
The solution is to configure OPA with a separate diagnostic listener by
providing the `--diagnostic-addr` flag, for example:
```
$ opa run \
-s \
--addr localhost:8181 \
--diagnostic-addr :8282
```
The configuration above would expose only `/health` and `/metrics` API's on port
`8282` while keeping the normal REST API bound to `localhost:8181`
## Hardened Configuration Example
+34 -5
View File
@@ -64,6 +64,10 @@ type Params struct {
// Addrs are the listening addresses that the OPA server will bind to.
Addrs *[]string
// DiagnosticAddrs are the listening addresses that the OPA server will bind to
// for read-only diagnostic API's (/health, /metrics, etc)
DiagnosticAddrs *[]string
// InsecureAddr is the listening address that the OPA server will bind to
// in addition to Addr if TLS is enabled.
InsecureAddr string
@@ -238,11 +242,20 @@ func (rt *Runtime) StartServer(ctx context.Context) {
// will block until either: an error occurs, the context is canceled, or
// a SIGTERM or SIGKILL signal is sent.
func (rt *Runtime) Serve(ctx context.Context) error {
if rt.Params.Addrs == nil {
return fmt.Errorf("at least one address must be configured in runtime parameters")
}
if rt.Params.DiagnosticAddrs == nil {
rt.Params.DiagnosticAddrs = &[]string{}
}
setupLogging(rt.Params.Logging)
logrus.WithFields(logrus.Fields{
"addrs": *rt.Params.Addrs,
"insecure_addr": rt.Params.InsecureAddr,
"addrs": *rt.Params.Addrs,
"diagnostic-addrs": *rt.Params.DiagnosticAddrs,
"insecure_addr": rt.Params.InsecureAddr,
}).Info("Initializing server.")
if err := rt.Manager.Start(ctx); err != nil {
@@ -253,7 +266,7 @@ func (rt *Runtime) Serve(ctx context.Context) error {
defer rt.Manager.Stop(ctx)
var err error
rt.server, err = server.New().
rt.server = server.New().
WithStore(rt.Store).
WithManager(rt.Manager).
WithCompilerErrorLimit(rt.Params.ErrorLimit).
@@ -267,9 +280,13 @@ func (rt *Runtime) Serve(ctx context.Context) error {
WithDecisionIDFactory(rt.decisionIDFactory).
WithDecisionLoggerWithErr(rt.decisionLogger).
WithRuntime(rt.Manager.Info).
WithMetrics(rt.metrics).
Init(ctx)
WithMetrics(rt.metrics)
if rt.Params.DiagnosticAddrs != nil {
rt.server = rt.server.WithDiagnosticAddresses(*rt.Params.DiagnosticAddrs)
}
rt.server, err = rt.server.Init(ctx)
if err != nil {
logrus.WithField("err", err).Error("Unable to initialize server.")
return err
@@ -283,6 +300,7 @@ func (rt *Runtime) Serve(ctx context.Context) error {
}
rt.server.Handler = NewLoggingHandler(rt.server.Handler)
rt.server.DiagnosticHandler = NewLoggingHandler(rt.server.DiagnosticHandler)
loops, err := rt.server.Listeners()
if err != nil {
@@ -322,6 +340,17 @@ func (rt *Runtime) Addrs() []string {
return rt.server.Addrs()
}
// DiagnosticAddrs returns a list of diagnostic addresses that the runtime is
// listening on (when in server mode). Returns an empty list if it hasn't
// started listening.
func (rt *Runtime) DiagnosticAddrs() []string {
if rt.server == nil {
return nil
}
return rt.server.DiagnosticAddrs()
}
// StartREPL starts the runtime in REPL mode. This function will block the calling goroutine.
func (rt *Runtime) StartREPL(ctx context.Context) {
+180 -110
View File
@@ -86,10 +86,12 @@ var unsafeBuiltinsMap = map[string]struct{}{ast.HTTPSend.Name: struct{}{}}
// Server represents an instance of OPA running in server mode.
type Server struct {
Handler http.Handler
Handler http.Handler
DiagnosticHandler http.Handler
router *mux.Router
addrs []string
diagAddrs []string
insecureAddr string
authentication AuthenticationScheme
authorization AuthorizationScheme
@@ -132,26 +134,9 @@ func New() *Server {
// Init initializes the server. This function MUST be called before Loop.
func (s *Server) Init(ctx context.Context) (*Server, error) {
s.initRouter()
// Add authorization handler. This must come BEFORE authentication handler
// so that the latter can run first.
switch s.authorization {
case AuthorizationBasic:
s.Handler = authorizer.NewBasic(
s.Handler,
s.getCompiler,
s.store,
authorizer.Runtime(s.runtime),
authorizer.Decision(s.manager.Config.DefaultAuthorizationDecisionRef))
}
switch s.authentication {
case AuthenticationToken:
s.Handler = identifier.NewTokenBased(s.Handler)
case AuthenticationTLS:
s.Handler = identifier.NewTLSBased(s.Handler)
}
s.initRouters()
s.Handler = s.initHandlerAuth(s.Handler)
s.DiagnosticHandler = s.initHandlerAuth(s.DiagnosticHandler)
txn, err := s.store.NewTransaction(ctx, storage.WriteParams)
if err != nil {
@@ -224,6 +209,13 @@ func (s *Server) WithAddresses(addrs []string) *Server {
return s
}
// WithDiagnosticAddresses sets the listening addresses that the server will
// bind to and *only* serve read-only diagnostic API's.
func (s *Server) WithDiagnosticAddresses(addrs []string) *Server {
s.diagAddrs = addrs
return s
}
// WithInsecureAddress sets the listening address that the server will bind to.
func (s *Server) WithInsecureAddress(addr string) *Server {
s.insecureAddr = addr
@@ -323,28 +315,24 @@ func (s *Server) WithRouter(router *mux.Router) *Server {
// Listeners returns functions that listen and serve connections.
func (s *Server) Listeners() ([]Loop, error) {
loops := []Loop{}
for _, addr := range s.addrs {
parsedURL, err := parseURL(addr, s.cert != nil)
if err != nil {
return nil, err
handlerBindings := map[httpListenerType]struct {
addrs []string
handler http.Handler
}{
defaultListenerType: {s.addrs, s.Handler},
diagnosticListenerType: {s.diagAddrs, s.DiagnosticHandler},
}
for t, binding := range handlerBindings {
for _, addr := range binding.addrs {
loop, listener, err := s.getListener(addr, binding.handler, t)
if err != nil {
return nil, err
}
s.httpListeners = append(s.httpListeners, listener)
loops = append(loops, loop)
}
var loop Loop
var listener httpListener
switch parsedURL.Scheme {
case "unix":
loop, listener, err = s.getListenerForUNIXSocket(parsedURL)
case "http":
loop, listener, err = s.getListenerForHTTPServer(parsedURL)
case "https":
loop, listener, err = s.getListenerForHTTPSServer(parsedURL)
default:
err = fmt.Errorf("invalid url scheme %q", parsedURL.Scheme)
}
if err != nil {
return nil, err
}
s.httpListeners = append(s.httpListeners, listener)
loops = append(loops, loop)
}
if s.insecureAddr != "" {
@@ -352,7 +340,7 @@ func (s *Server) Listeners() ([]Loop, error) {
if err != nil {
return nil, err
}
loop, httpListener, err := s.getListenerForHTTPServer(parsedURL)
loop, httpListener, err := s.getListenerForHTTPServer(parsedURL, s.Handler, defaultListenerType)
if err != nil {
return nil, err
}
@@ -364,12 +352,23 @@ func (s *Server) Listeners() ([]Loop, error) {
}
// Addrs returns a list of addresses that the server is listening on.
// if the server hasn't been started it will not return an address.
// If the server hasn't been started it will not return an address.
func (s *Server) Addrs() []string {
return s.addrsForType(defaultListenerType)
}
// DiagnosticAddrs returns a list of addresses that the server is listening on
// for the read-only diagnostic API's (eg /health, /metrics, etc)
// If the server hasn't been started it will not return an address.
func (s *Server) DiagnosticAddrs() []string {
return s.addrsForType(diagnosticListenerType)
}
func (s *Server) addrsForType(t httpListenerType) []string {
var addrs []string
for _, l := range s.httpListeners {
a := l.Addr()
if a != "" {
if a != "" && l.Type() == t {
addrs = append(addrs, a)
}
}
@@ -390,27 +389,36 @@ func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
return tc, nil
}
type httpListenerType int
const (
defaultListenerType httpListenerType = iota
diagnosticListenerType
)
type httpListener interface {
Addr() string
ListenAndServe() error
ListenAndServeTLS(certFile, keyFile string) error
Shutdown(ctx context.Context) error
Type() httpListenerType
}
// baseHTTPListener is just a wrapper around http.Server
type baseHTTPListener struct {
s *http.Server
l net.Listener
t httpListenerType
}
var _ httpListener = (*baseHTTPListener)(nil)
func newHTTPListener(srvr *http.Server) httpListener {
return &baseHTTPListener{srvr, nil}
func newHTTPListener(srvr *http.Server, t httpListenerType) httpListener {
return &baseHTTPListener{s: srvr, t: t}
}
func newHTTPUnixSocketListener(srvr *http.Server, l net.Listener) httpListener {
return &baseHTTPListener{srvr, l}
func newHTTPUnixSocketListener(srvr *http.Server, l net.Listener, t httpListenerType) httpListener {
return &baseHTTPListener{s: srvr, l: l, t: t}
}
func (b *baseHTTPListener) ListenAndServe() error {
@@ -457,18 +465,44 @@ func (b *baseHTTPListener) Shutdown(ctx context.Context) error {
return b.s.Shutdown(ctx)
}
func (s *Server) getListenerForHTTPServer(u *url.URL) (Loop, httpListener, error) {
httpServer := http.Server{
Addr: u.Host,
Handler: s.Handler,
func (b *baseHTTPListener) Type() httpListenerType {
return b.t
}
func (s *Server) getListener(addr string, h http.Handler, t httpListenerType) (Loop, httpListener, error) {
parsedURL, err := parseURL(addr, s.cert != nil)
if err != nil {
return nil, nil, err
}
l := newHTTPListener(&httpServer)
var loop Loop
var listener httpListener
switch parsedURL.Scheme {
case "unix":
loop, listener, err = s.getListenerForUNIXSocket(parsedURL, h, t)
case "http":
loop, listener, err = s.getListenerForHTTPServer(parsedURL, h, t)
case "https":
loop, listener, err = s.getListenerForHTTPSServer(parsedURL, h, t)
default:
err = fmt.Errorf("invalid url scheme %q", parsedURL.Scheme)
}
return loop, listener, err
}
func (s *Server) getListenerForHTTPServer(u *url.URL, h http.Handler, t httpListenerType) (Loop, httpListener, error) {
httpServer := http.Server{
Addr: u.Host,
Handler: h,
}
l := newHTTPListener(&httpServer, t)
return l.ListenAndServe, l, nil
}
func (s *Server) getListenerForHTTPSServer(u *url.URL) (Loop, httpListener, error) {
func (s *Server) getListenerForHTTPSServer(u *url.URL, h http.Handler, t httpListenerType) (Loop, httpListener, error) {
if s.cert == nil {
return nil, nil, fmt.Errorf("TLS certificate required but not supplied")
@@ -476,7 +510,7 @@ func (s *Server) getListenerForHTTPSServer(u *url.URL) (Loop, httpListener, erro
httpsServer := http.Server{
Addr: u.Host,
Handler: s.Handler,
Handler: h,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{*s.cert},
ClientCAs: s.certPool,
@@ -486,101 +520,137 @@ func (s *Server) getListenerForHTTPSServer(u *url.URL) (Loop, httpListener, erro
httpsServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
}
l := newHTTPListener(&httpsServer)
l := newHTTPListener(&httpsServer, t)
httpsLoop := func() error { return l.ListenAndServeTLS("", "") }
return httpsLoop, l, nil
}
func (s *Server) getListenerForUNIXSocket(u *url.URL) (Loop, httpListener, error) {
func (s *Server) getListenerForUNIXSocket(u *url.URL, h http.Handler, t httpListenerType) (Loop, httpListener, error) {
socketPath := u.Host + u.Path
// Remove domain socket file in case it already exists.
os.Remove(socketPath)
domainSocketServer := http.Server{Handler: s.Handler}
domainSocketServer := http.Server{Handler: h}
unixListener, err := net.Listen("unix", socketPath)
if err != nil {
return nil, nil, err
}
l := newHTTPUnixSocketListener(&domainSocketServer, unixListener)
l := newHTTPUnixSocketListener(&domainSocketServer, unixListener, t)
domainSocketLoop := func() error { return domainSocketServer.Serve(unixListener) }
return domainSocketLoop, l, nil
}
func (s *Server) initRouter() {
router := s.router
if router == nil {
router = mux.NewRouter()
func (s *Server) initHandlerAuth(handler http.Handler) http.Handler {
// Add authorization handler. This must come BEFORE authentication handler
// so that the latter can run first.
switch s.authorization {
case AuthorizationBasic:
handler = authorizer.NewBasic(
handler,
s.getCompiler,
s.store,
authorizer.Runtime(s.runtime),
authorizer.Decision(s.manager.Config.DefaultAuthorizationDecisionRef))
}
router.UseEncodedPath()
router.StrictSlash(true)
if s.metrics != nil {
s.metrics.RegisterEndpoints(func(path, method string, handler http.Handler) {
router.Handle(path, handler).Methods(method)
})
switch s.authentication {
case AuthenticationToken:
handler = identifier.NewTokenBased(handler)
case AuthenticationTLS:
handler = identifier.NewTLSBased(handler)
}
router.Handle("/health", s.instrumentHandler(http.HandlerFunc(s.unversionedGetHealth), PromHandlerHealth)).Methods(http.MethodGet)
return handler
}
func (s *Server) initRouters() {
mainRouter := s.router
if mainRouter == nil {
mainRouter = mux.NewRouter()
}
diagRouter := mux.NewRouter()
// All routers get the same base configuration *and* diagnostic API's
for _, router := range []*mux.Router{mainRouter, diagRouter} {
router.StrictSlash(true)
router.UseEncodedPath()
router.StrictSlash(true)
if s.metrics != nil {
s.metrics.RegisterEndpoints(func(path, method string, handler http.Handler) {
router.Handle(path, handler).Methods(method)
})
}
router.Handle("/health", s.instrumentHandler(s.unversionedGetHealth, PromHandlerHealth)).Methods(http.MethodGet)
}
if s.pprofEnabled {
router.HandleFunc("/debug/pprof/", pprof.Index)
router.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
router.Handle("/debug/pprof/block", pprof.Handler("block"))
router.Handle("/debug/pprof/heap", pprof.Handler("heap"))
router.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
router.HandleFunc("/debug/pprof/profile", pprof.Profile)
router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
router.HandleFunc("/debug/pprof/trace", pprof.Trace)
mainRouter.HandleFunc("/debug/pprof/", pprof.Index)
mainRouter.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
mainRouter.Handle("/debug/pprof/block", pprof.Handler("block"))
mainRouter.Handle("/debug/pprof/heap", pprof.Handler("heap"))
mainRouter.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
mainRouter.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mainRouter.HandleFunc("/debug/pprof/profile", pprof.Profile)
mainRouter.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mainRouter.HandleFunc("/debug/pprof/trace", pprof.Trace)
}
s.registerHandler(router, 0, "/data/{path:.+}", http.MethodPost, s.instrumentHandler(s.v0DataPost, PromHandlerV0Data))
s.registerHandler(router, 0, "/data", http.MethodPost, s.instrumentHandler(s.v0DataPost, PromHandlerV0Data))
s.registerHandler(router, 1, "/data/{path:.+}", http.MethodDelete, s.instrumentHandler(s.v1DataDelete, PromHandlerV1Data))
s.registerHandler(router, 1, "/data/{path:.+}", http.MethodPut, s.instrumentHandler(s.v1DataPut, PromHandlerV1Data))
s.registerHandler(router, 1, "/data", http.MethodPut, s.instrumentHandler(s.v1DataPut, PromHandlerV1Data))
s.registerHandler(router, 1, "/data/{path:.+}", http.MethodGet, s.instrumentHandler(s.v1DataGet, PromHandlerV1Data))
s.registerHandler(router, 1, "/data", http.MethodGet, s.instrumentHandler(s.v1DataGet, PromHandlerV1Data))
s.registerHandler(router, 1, "/data/{path:.+}", http.MethodPatch, s.instrumentHandler(s.v1DataPatch, PromHandlerV1Data))
s.registerHandler(router, 1, "/data", http.MethodPatch, s.instrumentHandler(s.v1DataPatch, PromHandlerV1Data))
s.registerHandler(router, 1, "/data/{path:.+}", http.MethodPost, s.instrumentHandler(s.v1DataPost, PromHandlerV1Data))
s.registerHandler(router, 1, "/data", http.MethodPost, s.instrumentHandler(s.v1DataPost, PromHandlerV1Data))
s.registerHandler(router, 1, "/policies", http.MethodGet, s.instrumentHandler(s.v1PoliciesList, PromHandlerV1Policies))
s.registerHandler(router, 1, "/policies/{path:.+}", http.MethodDelete, s.instrumentHandler(s.v1PoliciesDelete, PromHandlerV1Policies))
s.registerHandler(router, 1, "/policies/{path:.+}", http.MethodGet, s.instrumentHandler(s.v1PoliciesGet, PromHandlerV1Policies))
s.registerHandler(router, 1, "/policies/{path:.+}", http.MethodPut, s.instrumentHandler(s.v1PoliciesPut, PromHandlerV1Policies))
s.registerHandler(router, 1, "/query", http.MethodGet, s.instrumentHandler(s.v1QueryGet, PromHandlerV1Query))
s.registerHandler(router, 1, "/query", http.MethodPost, s.instrumentHandler(s.v1QueryPost, PromHandlerV1Query))
s.registerHandler(router, 1, "/compile", http.MethodPost, s.instrumentHandler(s.v1CompilePost, PromHandlerV1Compile))
router.Handle("/", s.instrumentHandler(http.HandlerFunc(s.unversionedPost), PromHandlerIndex)).Methods(http.MethodPost)
router.Handle("/", s.instrumentHandler(http.HandlerFunc(s.indexGet), PromHandlerIndex)).Methods(http.MethodGet)
// Only the main mainRouter gets the OPA API's (data, policies, query, etc)
s.registerHandler(mainRouter, 0, "/data/{path:.+}", http.MethodPost, s.instrumentHandler(s.v0DataPost, PromHandlerV0Data))
s.registerHandler(mainRouter, 0, "/data", http.MethodPost, s.instrumentHandler(s.v0DataPost, PromHandlerV0Data))
s.registerHandler(mainRouter, 1, "/data/{path:.+}", http.MethodDelete, s.instrumentHandler(s.v1DataDelete, PromHandlerV1Data))
s.registerHandler(mainRouter, 1, "/data/{path:.+}", http.MethodPut, s.instrumentHandler(s.v1DataPut, PromHandlerV1Data))
s.registerHandler(mainRouter, 1, "/data", http.MethodPut, s.instrumentHandler(s.v1DataPut, PromHandlerV1Data))
s.registerHandler(mainRouter, 1, "/data/{path:.+}", http.MethodGet, s.instrumentHandler(s.v1DataGet, PromHandlerV1Data))
s.registerHandler(mainRouter, 1, "/data", http.MethodGet, s.instrumentHandler(s.v1DataGet, PromHandlerV1Data))
s.registerHandler(mainRouter, 1, "/data/{path:.+}", http.MethodPatch, s.instrumentHandler(s.v1DataPatch, PromHandlerV1Data))
s.registerHandler(mainRouter, 1, "/data", http.MethodPatch, s.instrumentHandler(s.v1DataPatch, PromHandlerV1Data))
s.registerHandler(mainRouter, 1, "/data/{path:.+}", http.MethodPost, s.instrumentHandler(s.v1DataPost, PromHandlerV1Data))
s.registerHandler(mainRouter, 1, "/data", http.MethodPost, s.instrumentHandler(s.v1DataPost, PromHandlerV1Data))
s.registerHandler(mainRouter, 1, "/policies", http.MethodGet, s.instrumentHandler(s.v1PoliciesList, PromHandlerV1Policies))
s.registerHandler(mainRouter, 1, "/policies/{path:.+}", http.MethodDelete, s.instrumentHandler(s.v1PoliciesDelete, PromHandlerV1Policies))
s.registerHandler(mainRouter, 1, "/policies/{path:.+}", http.MethodGet, s.instrumentHandler(s.v1PoliciesGet, PromHandlerV1Policies))
s.registerHandler(mainRouter, 1, "/policies/{path:.+}", http.MethodPut, s.instrumentHandler(s.v1PoliciesPut, PromHandlerV1Policies))
s.registerHandler(mainRouter, 1, "/query", http.MethodGet, s.instrumentHandler(s.v1QueryGet, PromHandlerV1Query))
s.registerHandler(mainRouter, 1, "/query", http.MethodPost, s.instrumentHandler(s.v1QueryPost, PromHandlerV1Query))
s.registerHandler(mainRouter, 1, "/compile", http.MethodPost, s.instrumentHandler(s.v1CompilePost, PromHandlerV1Compile))
mainRouter.Handle("/", s.instrumentHandler(s.unversionedPost, PromHandlerIndex)).Methods(http.MethodPost)
mainRouter.Handle("/", s.instrumentHandler(s.indexGet, PromHandlerIndex)).Methods(http.MethodGet)
// These are catch all handlers that respond 405 for resources that exist but the method is not allowed
router.Handle("/v0/data/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodGet, http.MethodHead,
mainRouter.Handle("/v0/data/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodGet, http.MethodHead,
http.MethodConnect, http.MethodDelete, http.MethodOptions, http.MethodPatch, http.MethodPut, http.MethodTrace)
router.Handle("/v0/data", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodGet, http.MethodHead,
mainRouter.Handle("/v0/data", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodGet, http.MethodHead,
http.MethodConnect, http.MethodDelete, http.MethodOptions, http.MethodPatch, http.MethodPut,
http.MethodTrace)
// v1 Data catch all
router.Handle("/v1/data/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
mainRouter.Handle("/v1/data/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
http.MethodConnect, http.MethodOptions, http.MethodTrace)
router.Handle("/v1/data", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
mainRouter.Handle("/v1/data", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
http.MethodConnect, http.MethodDelete, http.MethodOptions, http.MethodTrace)
// Policies catch all
router.Handle("/v1/policies", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
mainRouter.Handle("/v1/policies", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
http.MethodConnect, http.MethodDelete, http.MethodOptions, http.MethodTrace, http.MethodPost, http.MethodPut,
http.MethodPatch)
// Policies (/policies/{path.+} catch all
router.Handle("/v1/policies/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
mainRouter.Handle("/v1/policies/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
http.MethodConnect, http.MethodOptions, http.MethodTrace, http.MethodPost)
// Query catch all
router.Handle("/v1/query/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
mainRouter.Handle("/v1/query/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
http.MethodConnect, http.MethodDelete, http.MethodOptions, http.MethodTrace, http.MethodPost, http.MethodPut, http.MethodPatch)
router.Handle("/v1/query", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
mainRouter.Handle("/v1/query", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
http.MethodConnect, http.MethodDelete, http.MethodOptions, http.MethodTrace, http.MethodPut, http.MethodPatch)
s.Handler = router
s.Handler = mainRouter
s.DiagnosticHandler = diagRouter
}
func (s *Server) instrumentHandler(handler func(http.ResponseWriter, *http.Request), label string) http.Handler {
+247 -78
View File
@@ -54,23 +54,15 @@ type trw struct {
}
func TestUnversionedGetHealth(t *testing.T) {
f := newFixture(t)
req := newReqUnversioned(http.MethodGet, "/health", "")
if err := f.executeRequest(req, 200, `{}`); err != nil {
t.Fatalf("Unexpected error while health check: %v", err)
}
validateDiagnosticRequest(t, f, req, 200, `{}`)
}
func TestUnversionedGetHealthBundleNoBundleSet(t *testing.T) {
f := newFixture(t)
req := newReqUnversioned(http.MethodGet, "/health?bundles=true", "")
if err := f.executeRequest(req, 200, `{}`); err != nil {
t.Fatalf("Unexpected error while health check: %v", err)
}
validateDiagnosticRequest(t, f, req, 200, `{}`)
}
func TestUnversionedGetHealthCheckOnlyBundlePlugin(t *testing.T) {
@@ -83,18 +75,14 @@ func TestUnversionedGetHealthCheckOnlyBundlePlugin(t *testing.T) {
// The bundle hasn't been activated yet, expect the health check to fail
req := newReqUnversioned(http.MethodGet, "/health?bundles=true", "")
if err := f.executeRequest(req, 500, `{}`); err != nil {
t.Fatal(err)
}
validateDiagnosticRequest(t, f, req, 500, `{}`)
// Set the bundle to be activated.
f.server.manager.UpdatePluginStatus("bundle", &plugins.Status{State: plugins.StateOK})
// The heath check should now respond as healthy
req = newReqUnversioned(http.MethodGet, "/health?bundles=true", "")
if err := f.executeRequest(req, 200, `{}`); err != nil {
t.Fatal(err)
}
validateDiagnosticRequest(t, f, req, 200, `{}`)
}
func TestUnversionedGetHealthCheckDiscoveryWithBundle(t *testing.T) {
@@ -106,9 +94,7 @@ func TestUnversionedGetHealthCheckDiscoveryWithBundle(t *testing.T) {
// The discovery bundle hasn't been activated yet, expect the health check to fail
req := newReqUnversioned(http.MethodGet, "/health?bundles=true", "")
if err := f.executeRequest(req, 500, `{}`); err != nil {
t.Fatal(err)
}
validateDiagnosticRequest(t, f, req, 500, `{}`)
// Set the bundle to be not ready (plugin configured and created, but hasn't activated all bundles yet).
f.server.manager.UpdatePluginStatus("discovery", &plugins.Status{State: plugins.StateOK})
@@ -116,18 +102,14 @@ func TestUnversionedGetHealthCheckDiscoveryWithBundle(t *testing.T) {
// The discovery bundle is OK, but the newly configured bundle hasn't been activated yet, expect the health check to fail
req = newReqUnversioned(http.MethodGet, "/health?bundles=true", "")
if err := f.executeRequest(req, 500, `{}`); err != nil {
t.Fatal(err)
}
validateDiagnosticRequest(t, f, req, 500, `{}`)
// Set the bundle to be activated.
f.server.manager.UpdatePluginStatus("bundle", &plugins.Status{State: plugins.StateOK})
// The heath check should now respond as healthy
req = newReqUnversioned(http.MethodGet, "/health?bundles=true", "")
if err := f.executeRequest(req, 200, `{}`); err != nil {
t.Fatal(err)
}
validateDiagnosticRequest(t, f, req, 200, `{}`)
}
func TestUnversionedGetHealthCheckBundleActivationSingleLegacy(t *testing.T) {
@@ -140,9 +122,7 @@ func TestUnversionedGetHealthCheckBundleActivationSingleLegacy(t *testing.T) {
// The server doesn't know about any bundles, so return a healthy status
req := newReqUnversioned(http.MethodGet, "/health?bundle=true", "")
if err := f.executeRequest(req, 200, `{}`); err != nil {
t.Fatal(err)
}
validateDiagnosticRequest(t, f, req, 200, `{}`)
err := storage.Txn(ctx, f.server.store, storage.WriteParams, func(txn storage.Transaction) error {
return bundle.LegacyWriteManifestToStore(ctx, f.server.store, txn, bundle.Manifest{
@@ -156,9 +136,7 @@ func TestUnversionedGetHealthCheckBundleActivationSingleLegacy(t *testing.T) {
// The heath check still respond as healthy with a legacy bundle found in storage
req = newReqUnversioned(http.MethodGet, "/health?bundle=true", "")
if err := f.executeRequest(req, 200, `{}`); err != nil {
t.Fatal(err)
}
validateDiagnosticRequest(t, f, req, 200, `{}`)
}
func TestBundlesReady(t *testing.T) {
@@ -367,9 +345,7 @@ func TestUnversionedGetHealthCheckDiscoveryWithPlugins(t *testing.T) {
}
req := newReqUnversioned(http.MethodGet, "/health?plugins", "")
if err := f.executeRequest(req, tc.exp, `{}`); err != nil {
t.Fatal(err)
}
validateDiagnosticRequest(t, f, req, tc.exp, `{}`)
})
}
}
@@ -449,9 +425,7 @@ func TestUnversionedGetHealthCheckBundleAndPlugins(t *testing.T) {
}
req := newReqUnversioned(http.MethodGet, "/health?plugins&bundles", "")
if err := f.executeRequest(req, tc.exp, `{}`); err != nil {
t.Fatal(err)
}
validateDiagnosticRequest(t, f, req, tc.exp, `{}`)
})
}
}
@@ -3447,35 +3421,25 @@ func TestAuthorization(t *testing.T) {
panic(err)
}
recorder := httptest.NewRecorder()
// Test that bob can do stuff.
req1, err := http.NewRequest(http.MethodGet, "http://localhost:8182/v1/data/foo", nil)
req1, err := http.NewRequest(http.MethodGet, "http://localhost:8182/health", nil)
if err != nil {
panic(err)
}
req1 = identifier.SetIdentity(req1, "bob")
server.Handler.ServeHTTP(recorder, req1)
if recorder.Code != http.StatusOK {
t.Fatalf("Expected success but got: %v", recorder)
}
recorder = httptest.NewRecorder()
validateAuthorizedRequest(t, server, req1, http.StatusOK)
// Test that alice can't do stuff.
req2, err := http.NewRequest(http.MethodGet, "http://localhost:8182/v1/data/foo", nil)
req2, err := http.NewRequest(http.MethodGet, "http://localhost:8182/health", nil)
if err != nil {
panic(err)
}
req2 = identifier.SetIdentity(req2, "alice")
server.Handler.ServeHTTP(recorder, req2)
if recorder.Code != http.StatusUnauthorized {
t.Fatalf("Expected unauthorized but got: %v", recorder)
}
validateAuthorizedRequest(t, server, req2, http.StatusUnauthorized)
// Reverse the policy.
update := identifier.SetIdentity(newReqV1(http.MethodPut, "/policies/test", `
@@ -3490,24 +3454,38 @@ func TestAuthorization(t *testing.T) {
}
`), "bob")
recorder = httptest.NewRecorder()
recorder := httptest.NewRecorder()
server.Handler.ServeHTTP(recorder, update)
if recorder.Code != http.StatusOK {
t.Fatalf("Expected policy update to succeed but got: %v", recorder)
}
// Try alice again.
recorder = httptest.NewRecorder()
server.Handler.ServeHTTP(recorder, req2)
if recorder.Code != http.StatusOK {
t.Fatalf("Expected OK but got: %v", recorder)
}
validateAuthorizedRequest(t, server, req2, http.StatusOK)
// Try bob again.
recorder = httptest.NewRecorder()
server.Handler.ServeHTTP(recorder, req1)
if recorder.Code != http.StatusUnauthorized {
t.Fatalf("Expected 401 but got: %v", recorder)
validateAuthorizedRequest(t, server, req1, http.StatusUnauthorized)
}
func validateAuthorizedRequest(t *testing.T, s *Server, req *http.Request, exp int) {
t.Helper()
r := httptest.NewRecorder()
// First check the main router
s.Handler.ServeHTTP(r, req)
if r.Code != exp {
t.Fatalf("(Default Handler) Expected %v but got: %v", exp, r)
}
r = httptest.NewRecorder()
// Ensure that auth happens for the diagnostic handler as well
s.DiagnosticHandler.ServeHTTP(r, req)
if r.Code != exp {
t.Fatalf("(Diagnostic Handler) Expected %v but got: %v", exp, r)
}
}
@@ -3680,7 +3658,7 @@ func newFixture(t *testing.T, opts ...func(*Server)) *fixture {
}
server := New().
WithAddresses([]string{":8182"}).
WithAddresses([]string{"localhost:8182"}).
WithStore(store).
WithManager(m)
for _, opt := range opts {
@@ -3718,18 +3696,26 @@ func (f *fixture) v1TestRequests(trs []tr) error {
}
func (f *fixture) v1(method string, path string, body string, code int, resp string) error {
req := newReqV1(method, path, body)
return f.executeRequest(req, code, resp)
// All v1 API's should 404 for the diagnostic handler
if err := f.executeDiagnosticRequest(newReqV1(method, path, body), 404, ""); err != nil {
return err
}
return f.executeRequest(newReqV1(method, path, body), code, resp)
}
func (f *fixture) v0(method string, path string, body string, code int, resp string) error {
req := newReqV0(method, path, body)
return f.executeRequest(req, code, resp)
// All v0 API's should 404 for the diagnostic handler
if err := f.executeDiagnosticRequest(newReqV0(method, path, body), 404, ""); err != nil {
return err
}
return f.executeRequest(newReqV0(method, path, body), code, resp)
}
func (f *fixture) executeRequest(req *http.Request, code int, resp string) error {
func (f *fixture) executeRequestForHandler(h http.Handler, req *http.Request, code int, resp string) error {
f.reset()
f.server.Handler.ServeHTTP(f.recorder, req)
h.ServeHTTP(f.recorder, req)
if f.recorder.Code != code {
return fmt.Errorf("Expected code %v from %v %v but got: %+v", code, req.Method, req.URL, f.recorder)
}
@@ -3757,6 +3743,14 @@ func (f *fixture) executeRequest(req *http.Request, code int, resp string) error
return nil
}
func (f *fixture) executeRequest(req *http.Request, code int, resp string) error {
return f.executeRequestForHandler(f.server.Handler, req, code, resp)
}
func (f *fixture) executeDiagnosticRequest(req *http.Request, code int, resp string) error {
return f.executeRequestForHandler(f.server.DiagnosticHandler, req, code, resp)
}
func (f *fixture) reset() {
f.recorder = httptest.NewRecorder()
}
@@ -3780,6 +3774,17 @@ func executeRequestsv0(t *testing.T, reqs []tr) {
}
}
func validateDiagnosticRequest(t *testing.T, f *fixture, req *http.Request, code int, resp string) {
t.Helper()
// diagnostic requests need to be available on both the normal handler and diagnostic handler
if err := f.executeRequest(req, code, resp); err != nil {
t.Errorf("Unexpected error for request %v: %s", req, err)
}
if err := f.executeDiagnosticRequest(req, code, resp); err != nil {
t.Errorf("Unexpected error for request %v: %s", req, err)
}
}
func newPolicy(id, s string) types.PolicyV1 {
compiler := ast.NewCompiler()
parsed := ast.MustParseModule(s)
@@ -4041,7 +4046,9 @@ func newClient(t *testing.T, pool *x509.CertPool, clientKeyPair ...string) *http
}
func TestShutdown(t *testing.T) {
f := newFixture(t)
f := newFixture(t, func(s *Server) {
s.WithDiagnosticAddresses([]string{":8443"})
})
loops, err := f.server.Listeners()
if err != nil {
t.Errorf("unexpected error: %s", err.Error())
@@ -4063,13 +4070,15 @@ func TestShutdown(t *testing.T) {
}
func TestShutdownError(t *testing.T) {
f := newFixture(t)
f := newFixture(t, func(s *Server) {
s.WithDiagnosticAddresses([]string{":8443"})
})
errMsg := "failed to shutdown"
// Add a mock httpListener to the server
m := &mockHTTPListener{
ShutdownHook: func() error {
shutdownHook: func() error {
return errors.New(errMsg)
},
}
@@ -4086,7 +4095,9 @@ func TestShutdownError(t *testing.T) {
}
func TestShutdownMultipleErrors(t *testing.T) {
f := newFixture(t)
f := newFixture(t, func(s *Server) {
s.WithDiagnosticAddresses([]string{":8443"})
})
shutdownErrs := []error{errors.New("err1"), nil, errors.New("err3")}
@@ -4095,7 +4106,7 @@ func TestShutdownMultipleErrors(t *testing.T) {
m := &mockHTTPListener{}
if err != nil {
retVal := errors.New(err.Error())
m.ShutdownHook = func() error {
m.shutdownHook = func() error {
return retVal
}
}
@@ -4135,7 +4146,7 @@ func TestAddrsWithEmptyListenAddr(t *testing.T) {
func TestAddrsWithListenAddr(t *testing.T) {
s := New()
s.httpListeners = []httpListener{&mockHTTPListener{Addrs: ":8181"}}
s.httpListeners = []httpListener{&mockHTTPListener{addrs: ":8181"}}
a := s.Addrs()
if len(a) != 1 || a[0] != ":8181" {
t.Errorf("expected only an ':8181' address, got: %+v", a)
@@ -4149,7 +4160,7 @@ func TestAddrsWithMixedListenerAddr(t *testing.T) {
s.httpListeners = []httpListener{}
for _, addr := range addrs {
s.httpListeners = append(s.httpListeners, &mockHTTPListener{Addrs: addr})
s.httpListeners = append(s.httpListeners, &mockHTTPListener{addrs: addr, t: defaultListenerType})
}
a := s.Addrs()
@@ -4171,17 +4182,171 @@ func TestAddrsWithMixedListenerAddr(t *testing.T) {
}
}
func TestDiagnosticAddrsNoListeners(t *testing.T) {
s := New()
a := s.DiagnosticAddrs()
if len(a) != 0 {
t.Errorf("expected an empty list of addresses, got: %+v", a)
}
}
func TestDiagnosticAddrsWithEmptyListenAddr(t *testing.T) {
s := New()
s.httpListeners = []httpListener{&mockHTTPListener{t: diagnosticListenerType}}
a := s.DiagnosticAddrs()
if len(a) != 0 {
t.Errorf("expected an empty list of addresses, got: %+v", a)
}
}
func TestDiagnosticAddrsWithListenAddr(t *testing.T) {
s := New()
s.httpListeners = []httpListener{&mockHTTPListener{addrs: ":8181", t: diagnosticListenerType}}
a := s.DiagnosticAddrs()
if len(a) != 1 || a[0] != ":8181" {
t.Errorf("expected only an ':8181' address, got: %+v", a)
}
}
func TestDiagnosticAddrsWithMixedListenerAddr(t *testing.T) {
s := New()
addrs := []string{":8181", "", "unix:///var/tmp/foo.sock"}
expected := []string{":8181", "unix:///var/tmp/foo.sock"}
s.httpListeners = []httpListener{}
for _, addr := range addrs {
s.httpListeners = append(s.httpListeners, &mockHTTPListener{addrs: addr, t: diagnosticListenerType})
}
a := s.DiagnosticAddrs()
if len(a) != 2 {
t.Errorf("expected 2 addresses, got: %+v", a)
}
for _, expectedAddr := range expected {
found := false
for _, actualAddr := range a {
if expectedAddr == actualAddr {
found = true
break
}
}
if !found {
t.Errorf("expected %q in address list, got: %+v", expectedAddr, a)
}
}
}
func TestMixedAddrTypes(t *testing.T) {
s := New()
s.httpListeners = []httpListener{}
addrs := map[string]struct{}{"localhost:8181": {}, "localhost:1234": {}, "unix:///var/tmp/foo.sock": {}}
for addr := range addrs {
s.httpListeners = append(s.httpListeners, &mockHTTPListener{addrs: addr, t: defaultListenerType})
}
diagAddrs := map[string]struct{}{":8181": {}, "https://127.0.0.1": {}}
for addr := range diagAddrs {
s.httpListeners = append(s.httpListeners, &mockHTTPListener{addrs: addr, t: diagnosticListenerType})
}
actualAddrs := s.Addrs()
if len(actualAddrs) != len(addrs) {
t.Errorf("expected %d addresses, got: %+v", len(addrs), actualAddrs)
}
for _, addr := range actualAddrs {
if _, ok := addrs[addr]; !ok {
t.Errorf("Unexpected address %v", addr)
}
}
actualDiagAddrs := s.DiagnosticAddrs()
if len(actualDiagAddrs) != len(diagAddrs) {
t.Errorf("expected %d addresses, got: %+v", len(diagAddrs), actualDiagAddrs)
}
for _, addr := range actualDiagAddrs {
if _, ok := diagAddrs[addr]; !ok {
t.Errorf("Unexpected diagnostic address %v", addr)
}
}
}
func TestDiagnosticRoutes(t *testing.T) {
cases := []struct {
path string
should404 bool
}{
{"/health", false},
{"/metrics", false},
{"/debug/pprof/", true},
{"/v0/data", true},
{"/v0/data/foo", true},
{"/v1/data/", true},
{"/v1/data/foo", true},
{"/v1/policies", true},
{"/v1/policies/foo", true},
{"/v1/query", true},
{"/v1/compile", true},
{"/", true},
}
f := newFixture(t, func(s *Server) {
s.WithPprofEnabled(true)
s.WithMetrics(new(mockMetricsProvider))
})
for _, tc := range cases {
t.Run(tc.path, func(t *testing.T) {
req, err := http.NewRequest("GET", tc.path, nil)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
code := http.StatusOK
if tc.should404 {
code = http.StatusNotFound
}
f.reset()
f.server.DiagnosticHandler.ServeHTTP(f.recorder, req)
if f.recorder.Code != code {
t.Errorf("Expected code %v from %v %v but got: %+v", code, req.Method, req.URL, f.recorder)
}
})
}
}
type mockHTTPHandler struct{}
func (m *mockHTTPHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}
type mockMetricsProvider struct{}
func (m *mockMetricsProvider) RegisterEndpoints(registrar func(path, method string, handler http.Handler)) {
registrar("/metrics", "GET", new(mockHTTPHandler))
}
func (m *mockMetricsProvider) InstrumentHandler(handler http.Handler, label string) http.Handler {
return handler
}
type listenerHook func() error
type mockHTTPListener struct {
ShutdownHook listenerHook
Addrs string
shutdownHook listenerHook
addrs string
t httpListenerType
}
var _ httpListener = (*mockHTTPListener)(nil)
func (m mockHTTPListener) Addr() string {
return m.Addrs
return m.addrs
}
func (m mockHTTPListener) ListenAndServe() error {
@@ -4194,8 +4359,12 @@ func (m mockHTTPListener) ListenAndServeTLS(certFile, keyFile string) error {
func (m mockHTTPListener) Shutdown(ctx context.Context) error {
var err error
if m.ShutdownHook != nil {
err = m.ShutdownHook()
if m.shutdownHook != nil {
err = m.shutdownHook()
}
return err
}
func (m mockHTTPListener) Type() httpListenerType {
return m.t
}
+100
View File
@@ -0,0 +1,100 @@
package diagnostics
import (
"flag"
"fmt"
"net/http"
"os"
"testing"
"github.com/open-policy-agent/opa/test/e2e"
)
var testRuntime *e2e.TestRuntime
func TestMain(m *testing.M) {
flag.Parse()
testServerParams := e2e.NewAPIServerTestParams()
testServerParams.DiagnosticAddrs = &[]string{":0"}
var err error
testRuntime, err = e2e.NewTestRuntime(testServerParams)
if err != nil {
os.Exit(1)
}
os.Exit(testRuntime.RunAPIServerTests(m))
}
func TestServerWithDiagnosticAddrHealthCheck(t *testing.T) {
if err := testRuntime.HealthCheck(diagURL(t)); err != nil {
t.Fatal(err)
}
// Ensure the "main" listener is still OK
if err := testRuntime.HealthCheck(testRuntime.URL()); err != nil {
t.Fatal(err)
}
}
func TestServerWithDiagnosticAddrProtectedAPIs(t *testing.T) {
cases := []string{
"/",
"/v0/data",
"/v0/data/foo",
"/v1/data",
"/v1/data/foo",
"/v1/policies",
"/v1/policies/foo",
"/v1/query",
"/v1/compile",
}
baseURL := diagURL(t)
methods := []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodTrace,
http.MethodPatch,
http.MethodConnect,
http.MethodDelete,
http.MethodOptions,
http.MethodHead,
}
for _, tc := range cases {
url := baseURL + tc
for _, method := range methods {
t.Run(fmt.Sprintf("%s %s", method, tc), func(t *testing.T) {
assert404(t, method, url)
})
}
}
}
func diagURL(t *testing.T) string {
t.Helper()
addr := testRuntime.Runtime.DiagnosticAddrs()[0]
diagURL, err := testRuntime.AddrToURL(addr)
if err != nil {
t.Error("Unexpected error: ", err)
}
return diagURL
}
func assert404(t *testing.T, method string, url string) {
t.Helper()
req, err := http.NewRequest(method, url, nil)
if err != nil {
t.Errorf("Unexpected error creating request: %s", err)
}
resp, err := testRuntime.Client.Do(req)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if resp.StatusCode != http.StatusNotFound {
t.Errorf("Unexpected response, expected 404, got: %d %s", resp.StatusCode, resp.Status)
}
}
+41 -7
View File
@@ -57,6 +57,7 @@ type TestRuntime struct {
Cancel context.CancelFunc
Client *http.Client
url string
diagURL string
urlMtx *sync.Mutex
}
@@ -138,6 +139,22 @@ func (t *TestRuntime) URL() string {
// will need to determine the URLs themselves.
addr := addrs[0]
parsed, err := t.AddrToURL(addr)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
t.url = parsed
return t.url
}
// AddrToURL generates a full URL from an address, as configured on the runtime.
// This can include fully qualified urls, just host/ip, with port, or only port
// (eg, "localhost", ":8181", "http://foo", etc). If the runtime is configured
// with HTTPS certs it will generate an appropriate URL.
func (t *TestRuntime) AddrToURL(addr string) (string, error) {
if strings.HasPrefix(addr, ":") {
addr = "localhost" + addr
}
@@ -152,13 +169,10 @@ func (t *TestRuntime) URL() string {
parsed, err := url.Parse(addr)
if err != nil {
fmt.Printf("Failed to parse listening address of server: %s", err)
os.Exit(1)
return "", fmt.Errorf("failed to parse listening address of server: %s", err)
}
t.url = parsed.String()
return t.url
return parsed.String(), nil
}
func (t *TestRuntime) runTests(m *testing.M, suppressLogs bool) int {
@@ -209,8 +223,8 @@ func (t *TestRuntime) WaitForServer() error {
// First make sure it has started listening and we have an address
if t.URL() != "" {
// Then make sure it has started serving
resp, err := http.Get(t.URL() + "/health")
if err == nil && resp.StatusCode == http.StatusOK {
err := t.HealthCheck(t.URL())
if err == nil {
logrus.Infof("Test server ready and listening on: %s", t.URL())
return nil
}
@@ -292,3 +306,23 @@ func (t *TestRuntime) GetDataWithInputTyped(path string, input interface{}, resp
return json.Unmarshal(bs, response)
}
// HealthCheck will query /health and return an error if the server is not healthy
func (t *TestRuntime) HealthCheck(url string, params ...string) error {
reqURL := url + "/health"
if len(params) > 0 {
reqURL += "?" + strings.Join(params, "&")
}
req, err := http.NewRequest("GET", url+"/health", nil)
if err != nil {
return fmt.Errorf("unexpected error creating request: %s", err)
}
resp, err := t.Client.Do(req)
if err != nil {
return fmt.Errorf("unexpected error: %s", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected response: %d %s", resp.StatusCode, resp.Status)
}
return nil
}