* Move the server daemons into their own package With this change the internal/server package now is dedicated to the server domain (workers and controllers) and not to the server daemons. The hope is that this will make it easier to keep the daemons decoupled from the representation of the servers in the database.
879 lines
23 KiB
Go
879 lines
23 KiB
Go
package ops
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/internal/cmd/base"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/health"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-secure-stdlib/base62"
|
|
"github.com/hashicorp/go-secure-stdlib/configutil/v2"
|
|
"github.com/hashicorp/go-secure-stdlib/listenerutil"
|
|
"github.com/hashicorp/go-secure-stdlib/reloadutil"
|
|
"github.com/mitchellh/cli"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewServer(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
logger hclog.Logger
|
|
c *controller.Controller
|
|
listeners []*base.ServerListener
|
|
assertions func(t *testing.T, s *Server)
|
|
expErr bool
|
|
expErrMsg string
|
|
}{
|
|
{
|
|
name: "nil logger",
|
|
logger: nil,
|
|
listeners: []*base.ServerListener{{}},
|
|
expErr: true,
|
|
expErrMsg: "ops.NewServer(): missing logger",
|
|
},
|
|
{
|
|
name: "nil ServerListeners",
|
|
logger: hclog.Default(),
|
|
listeners: nil,
|
|
expErr: false,
|
|
assertions: func(t *testing.T, s *Server) {
|
|
require.Len(t, s.bundles, 0)
|
|
},
|
|
},
|
|
{
|
|
name: "empty ServerListeners",
|
|
logger: hclog.Default(),
|
|
listeners: []*base.ServerListener{},
|
|
expErr: false,
|
|
assertions: func(t *testing.T, s *Server) {
|
|
require.Len(t, s.bundles, 0)
|
|
},
|
|
},
|
|
{
|
|
name: "nil listeners",
|
|
logger: hclog.Default(),
|
|
listeners: []*base.ServerListener{nil, nil},
|
|
expErr: false,
|
|
assertions: func(t *testing.T, s *Server) {
|
|
require.Len(t, s.bundles, 0)
|
|
},
|
|
},
|
|
{
|
|
name: "listeners with nil config",
|
|
logger: hclog.Default(),
|
|
listeners: []*base.ServerListener{
|
|
{
|
|
Config: nil,
|
|
},
|
|
{
|
|
Config: nil,
|
|
},
|
|
},
|
|
expErr: false,
|
|
assertions: func(t *testing.T, s *Server) {
|
|
require.Len(t, s.bundles, 0)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s, err := NewServer(tt.logger, tt.c, tt.listeners...)
|
|
if tt.expErr {
|
|
require.EqualError(t, err, tt.expErrMsg)
|
|
require.Nil(t, s)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, s)
|
|
|
|
if tt.assertions != nil {
|
|
tt.assertions(t, s)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewServerIntegration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setup func(t *testing.T)
|
|
listeners []*listenerutil.ListenerConfig
|
|
assertions func(t *testing.T, addrs []string)
|
|
expErr bool
|
|
expErrMsg string
|
|
}{
|
|
{
|
|
name: "one tcp ops listener",
|
|
listeners: []*listenerutil.ListenerConfig{
|
|
{
|
|
Type: "tcp",
|
|
Purpose: []string{"ops"},
|
|
Address: "127.0.0.1:0",
|
|
TLSDisable: true,
|
|
},
|
|
},
|
|
assertions: func(t *testing.T, addrs []string) {
|
|
_, err := http.Get("http://" + addrs[0])
|
|
require.NoError(t, err)
|
|
},
|
|
},
|
|
{
|
|
name: "multiple tcp ops listeners",
|
|
listeners: []*listenerutil.ListenerConfig{
|
|
{
|
|
Type: "tcp",
|
|
Purpose: []string{"ops"},
|
|
Address: "127.0.0.1:0",
|
|
TLSDisable: true,
|
|
},
|
|
{
|
|
Type: "tcp",
|
|
Purpose: []string{"ops"},
|
|
Address: "127.0.0.1:0",
|
|
TLSDisable: true,
|
|
},
|
|
},
|
|
assertions: func(t *testing.T, addrs []string) {
|
|
_, err := http.Get("http://" + addrs[0])
|
|
require.NoError(t, err)
|
|
|
|
_, err = http.Get("http://" + addrs[1])
|
|
require.NoError(t, err)
|
|
},
|
|
},
|
|
{
|
|
name: "one unix socket ops listener",
|
|
listeners: []*listenerutil.ListenerConfig{
|
|
{
|
|
Type: "unix",
|
|
Purpose: []string{"ops"},
|
|
Address: fmt.Sprintf("/tmp/boundary-opslistener-test-%s.sock", base62.MustRandom(5)),
|
|
TLSDisable: true,
|
|
},
|
|
},
|
|
expErr: false,
|
|
assertions: func(t *testing.T, addrs []string) {
|
|
conn, err := net.Dial("unix", addrs[0])
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { conn.Close() })
|
|
|
|
cl := http.Client{
|
|
Transport: &http.Transport{
|
|
Dial: func(network, addr string) (net.Conn, error) { return conn, nil },
|
|
},
|
|
}
|
|
_, err = cl.Get("http://random.domain")
|
|
require.NoError(t, err)
|
|
},
|
|
},
|
|
{
|
|
name: "multiple unix socket ops listeners",
|
|
listeners: []*listenerutil.ListenerConfig{
|
|
{
|
|
Type: "unix",
|
|
Purpose: []string{"ops"},
|
|
Address: fmt.Sprintf("/tmp/boundary-opslistener-test-%s.sock", base62.MustRandom(5)),
|
|
TLSDisable: true,
|
|
},
|
|
{
|
|
Type: "unix",
|
|
Purpose: []string{"ops"},
|
|
Address: fmt.Sprintf("/tmp/boundary-opslistener-test-%s.sock", base62.MustRandom(5)),
|
|
TLSDisable: true,
|
|
},
|
|
},
|
|
expErr: false,
|
|
assertions: func(t *testing.T, addrs []string) {
|
|
for _, addr := range addrs {
|
|
conn, err := net.Dial("unix", addr)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { conn.Close() })
|
|
|
|
cl := http.Client{
|
|
Transport: &http.Transport{
|
|
Dial: func(network, addr string) (net.Conn, error) { return conn, nil },
|
|
},
|
|
}
|
|
_, err = cl.Get("http://random.domain")
|
|
require.NoError(t, err)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "multiple tcp ops listeners (tls)",
|
|
setup: func(t *testing.T) {
|
|
testTlsSetup(t, "./boundary-opslistener-cert1.cert", "./boundary-opslistener-pk1.key")
|
|
testTlsSetup(t, "./boundary-opslistener-cert2.cert", "./boundary-opslistener-pk2.key")
|
|
},
|
|
listeners: []*listenerutil.ListenerConfig{
|
|
{
|
|
Type: "tcp",
|
|
Purpose: []string{"ops"},
|
|
Address: "127.0.0.1:0",
|
|
TLSKeyFile: "./boundary-opslistener-pk1.key",
|
|
TLSCertFile: "./boundary-opslistener-cert1.cert",
|
|
},
|
|
{
|
|
Type: "tcp",
|
|
Purpose: []string{"ops"},
|
|
Address: "127.0.0.1:0",
|
|
TLSKeyFile: "./boundary-opslistener-pk2.key",
|
|
TLSCertFile: "./boundary-opslistener-cert2.cert",
|
|
},
|
|
},
|
|
assertions: func(t *testing.T, addrs []string) {
|
|
cl := testTlsHttpClient(t, "./boundary-opslistener-cert1.cert")
|
|
_, err := cl.Get("https://" + addrs[0])
|
|
require.NoError(t, err)
|
|
|
|
cl = testTlsHttpClient(t, "./boundary-opslistener-cert2.cert")
|
|
_, err = cl.Get("https://" + addrs[1])
|
|
require.NoError(t, err)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.setup != nil {
|
|
tt.setup(t)
|
|
}
|
|
|
|
bs := &base.Server{ // These fields are only needed to successfully call `SetupListeners`
|
|
Info: make(map[string]string),
|
|
ReloadFuncs: make(map[string][]reloadutil.ReloadFunc),
|
|
ReloadFuncsLock: &sync.RWMutex{},
|
|
Logger: hclog.Default(),
|
|
}
|
|
err := bs.SetupListeners(nil, &configutil.SharedConfig{Listeners: tt.listeners}, []string{"ops"})
|
|
require.NoError(t, err)
|
|
|
|
s, err := NewServer(hclog.Default(), nil, bs.Listeners...)
|
|
if tt.expErr {
|
|
require.EqualError(t, err, tt.expErrMsg)
|
|
require.Nil(t, s)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, s)
|
|
s.Start()
|
|
t.Cleanup(func() { s.Shutdown() })
|
|
|
|
addrs := make([]string, 0, len(s.bundles))
|
|
for _, b := range s.bundles {
|
|
addrs = append(addrs, b.ln.OpsListener.Addr().String())
|
|
}
|
|
if tt.assertions != nil {
|
|
tt.assertions(t, addrs)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestShutdown(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
opsServerFn func(t *testing.T) *Server
|
|
assertions func(t *testing.T, o *Server)
|
|
expErr bool
|
|
expErrStr string
|
|
}{
|
|
{
|
|
name: "nil bundle slice",
|
|
opsServerFn: func(t *testing.T) *Server {
|
|
return &Server{bundles: nil}
|
|
},
|
|
expErr: false, // No-op
|
|
},
|
|
{
|
|
name: "empty bundle slice",
|
|
opsServerFn: func(t *testing.T) *Server {
|
|
return &Server{bundles: []*opsBundle{}}
|
|
},
|
|
expErr: false, // No-op
|
|
},
|
|
{
|
|
name: "nil bundle",
|
|
opsServerFn: func(t *testing.T) *Server {
|
|
return &Server{bundles: []*opsBundle{nil}}
|
|
},
|
|
expErr: true,
|
|
expErrStr: "ops.(Server).Shutdown: missing bundle, listener or its fields",
|
|
},
|
|
{
|
|
name: "empty bundle",
|
|
opsServerFn: func(t *testing.T) *Server {
|
|
return &Server{bundles: []*opsBundle{{}}}
|
|
},
|
|
expErr: true,
|
|
expErrStr: "ops.(Server).Shutdown: missing bundle, listener or its fields",
|
|
},
|
|
{
|
|
name: "missing listener config",
|
|
opsServerFn: func(t *testing.T) *Server {
|
|
return &Server{
|
|
bundles: []*opsBundle{
|
|
{
|
|
ln: &base.ServerListener{
|
|
HTTPServer: &http.Server{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
expErr: true,
|
|
expErrStr: "ops.(Server).Shutdown: missing bundle, listener or its fields",
|
|
},
|
|
{
|
|
name: "missing listener mux",
|
|
opsServerFn: func(t *testing.T) *Server {
|
|
return &Server{
|
|
bundles: []*opsBundle{
|
|
{
|
|
ln: &base.ServerListener{
|
|
Config: &listenerutil.ListenerConfig{},
|
|
HTTPServer: &http.Server{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
expErr: true,
|
|
expErrStr: "ops.(Server).Shutdown: missing bundle, listener or its fields",
|
|
},
|
|
{
|
|
name: "missing http server",
|
|
opsServerFn: func(t *testing.T) *Server {
|
|
return &Server{
|
|
bundles: []*opsBundle{
|
|
{
|
|
ln: &base.ServerListener{
|
|
Config: &listenerutil.ListenerConfig{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
expErr: true,
|
|
expErrStr: "ops.(Server).Shutdown: missing bundle, listener or its fields",
|
|
},
|
|
{
|
|
name: "listener already closed",
|
|
opsServerFn: func(t *testing.T) *Server {
|
|
l1, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
require.NoError(t, l1.Close())
|
|
|
|
s1 := &http.Server{}
|
|
return &Server{
|
|
bundles: []*opsBundle{
|
|
{
|
|
ln: &base.ServerListener{
|
|
HTTPServer: s1,
|
|
OpsListener: l1,
|
|
Config: &listenerutil.ListenerConfig{Type: "tcp", Purpose: []string{"ops"}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
assertions: func(t *testing.T, s *Server) {
|
|
// The HTTP Server must be closed.
|
|
require.ErrorIs(t, s.bundles[0].ln.HTTPServer.Serve(s.bundles[0].ln.OpsListener), http.ErrServerClosed)
|
|
|
|
// The underlying listener must be closed.
|
|
require.ErrorIs(t, s.bundles[0].ln.OpsListener.Close(), net.ErrClosed)
|
|
},
|
|
expErr: false,
|
|
},
|
|
{
|
|
name: "http server already closed",
|
|
opsServerFn: func(t *testing.T) *Server {
|
|
l1, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
|
|
s1 := &http.Server{}
|
|
// Use error channel so that we can use test assertions on the returned error.
|
|
// It is illegal to call `t.FailNow()` from a goroutine.
|
|
// https://pkg.go.dev/testing#T.FailNow
|
|
errChan := make(chan error)
|
|
go func() {
|
|
errChan <- s1.Serve(l1)
|
|
}()
|
|
t.Cleanup(func() {
|
|
// Will block until we stopped serving
|
|
require.ErrorIs(t, <-errChan, http.ErrServerClosed)
|
|
})
|
|
err = s1.Shutdown(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
return &Server{
|
|
bundles: []*opsBundle{
|
|
{
|
|
ln: &base.ServerListener{
|
|
OpsListener: l1,
|
|
HTTPServer: s1,
|
|
Config: &listenerutil.ListenerConfig{Type: "tcp", Purpose: []string{"ops"}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
assertions: func(t *testing.T, s *Server) {
|
|
// The HTTP Server must be closed.
|
|
require.ErrorIs(t, s.bundles[0].ln.HTTPServer.Serve(s.bundles[0].ln.OpsListener), http.ErrServerClosed)
|
|
|
|
// The underlying listener must be closed.
|
|
require.ErrorIs(t, s.bundles[0].ln.OpsListener.Close(), net.ErrClosed)
|
|
},
|
|
},
|
|
{
|
|
name: "multiple bundles to close",
|
|
opsServerFn: func(t *testing.T) *Server {
|
|
l1, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
|
|
s1 := &http.Server{}
|
|
// Use error channel so that we can use test assertions on the returned error.
|
|
// It is illegal to call `t.FailNow()` from a goroutine.
|
|
// https://pkg.go.dev/testing#T.FailNow
|
|
s1ErrChan := make(chan error)
|
|
go func() {
|
|
s1ErrChan <- s1.Serve(l1)
|
|
}()
|
|
t.Cleanup(func() {
|
|
require.ErrorIs(t, <-s1ErrChan, http.ErrServerClosed)
|
|
})
|
|
|
|
l2, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
|
|
s2 := &http.Server{}
|
|
s2ErrChan := make(chan error)
|
|
go func() {
|
|
s2ErrChan <- s2.Serve(l2)
|
|
}()
|
|
t.Cleanup(func() {
|
|
require.ErrorIs(t, <-s2ErrChan, http.ErrServerClosed)
|
|
})
|
|
|
|
return &Server{
|
|
bundles: []*opsBundle{
|
|
{
|
|
ln: &base.ServerListener{
|
|
OpsListener: l1,
|
|
HTTPServer: s1,
|
|
Config: &listenerutil.ListenerConfig{Type: "tcp", Purpose: []string{"ops"}},
|
|
},
|
|
},
|
|
{
|
|
ln: &base.ServerListener{
|
|
OpsListener: l2,
|
|
HTTPServer: s2,
|
|
Config: &listenerutil.ListenerConfig{Type: "tcp", Purpose: []string{"ops"}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
assertions: func(t *testing.T, s *Server) {
|
|
// The HTTP Server must be closed.
|
|
require.ErrorIs(t, s.bundles[0].ln.HTTPServer.Serve(s.bundles[0].ln.OpsListener), http.ErrServerClosed)
|
|
require.ErrorIs(t, s.bundles[1].ln.HTTPServer.Serve(s.bundles[1].ln.OpsListener), http.ErrServerClosed)
|
|
|
|
// The underlying listener must be closed.
|
|
require.ErrorIs(t, s.bundles[0].ln.OpsListener.Close(), net.ErrClosed)
|
|
require.ErrorIs(t, s.bundles[1].ln.OpsListener.Close(), net.ErrClosed)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
o := tt.opsServerFn(t)
|
|
err := o.Shutdown()
|
|
if tt.expErr {
|
|
require.EqualError(t, err, tt.expErrStr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
if tt.assertions != nil {
|
|
tt.assertions(t, o)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHealthEndpointLifecycle(t *testing.T) {
|
|
listeners := []*listenerutil.ListenerConfig{
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:0",
|
|
Purpose: []string{"ops"},
|
|
TLSDisable: true,
|
|
},
|
|
|
|
// We only care about the ops listener, but the following
|
|
// listeners are required to start a Controller.
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:0",
|
|
Purpose: []string{"api"},
|
|
TLSDisable: true,
|
|
},
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:0",
|
|
Purpose: []string{"cluster"},
|
|
},
|
|
}
|
|
|
|
// Get test a controller so we can actually start a controller.
|
|
tc := controller.NewTestController(t, &controller.TestControllerOpts{DisableAutoStart: true})
|
|
tc.Config().RawConfig.SharedConfig.Listeners = listeners // Override with our own listeners.
|
|
|
|
// Setup listeners that we've set up above to turn them into correct `ServerListener` objects
|
|
// that the controller can handle. This will set `Server.Listeners`.
|
|
err := tc.Server().SetupListeners(nil, tc.Config().RawConfig.SharedConfig, []string{"api", "cluster", "ops"})
|
|
require.NoError(t, err)
|
|
|
|
// Start controller. This sets `cmd.controller`.
|
|
err = tc.Controller().Start()
|
|
require.NoError(t, err)
|
|
t.Cleanup(tc.Shutdown)
|
|
|
|
// Controller has started and is set onto our Command object, start ops.
|
|
opsServer, err := NewServer(hclog.Default(), tc.Controller(), tc.Config().Listeners...)
|
|
require.NoError(t, err)
|
|
opsServer.Start()
|
|
|
|
// Assert the ops endpoint is up and returning 200 OK.
|
|
rsp, err := http.Get("http://" + tc.Config().Listeners[0].OpsListener.Addr().String() + "/health")
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, rsp.StatusCode)
|
|
|
|
// Start replying with 503 Service Unavailable from the health endpoint.
|
|
tc.Controller().HealthService.StartServiceUnavailableReplies()
|
|
|
|
// Assert we're receiving 503 Service Unavailable now instead of 200 OK.
|
|
rsp, err = http.Get("http://" + tc.Config().Listeners[0].OpsListener.Addr().String() + "/health")
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusServiceUnavailable, rsp.StatusCode)
|
|
}
|
|
|
|
func TestWaitIfHealthExists(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
bundles []*opsBundle
|
|
gracePeriod time.Duration
|
|
controller *controller.Controller
|
|
expMinWait time.Duration
|
|
expMaxWait time.Duration
|
|
}{
|
|
{
|
|
name: "nil bundles",
|
|
bundles: nil,
|
|
controller: &controller.Controller{HealthService: &health.Service{}},
|
|
gracePeriod: 1 * time.Minute,
|
|
expMinWait: 0 * time.Second,
|
|
expMaxWait: 500 * time.Microsecond,
|
|
},
|
|
{
|
|
name: "no bundles",
|
|
bundles: []*opsBundle{},
|
|
controller: &controller.Controller{HealthService: &health.Service{}},
|
|
gracePeriod: 1 * time.Minute,
|
|
expMinWait: 0 * time.Second,
|
|
expMaxWait: 500 * time.Microsecond,
|
|
},
|
|
{
|
|
name: "nil controller",
|
|
controller: nil,
|
|
gracePeriod: 1 * time.Minute,
|
|
expMinWait: 0 * time.Second,
|
|
expMaxWait: 500 * time.Microsecond,
|
|
},
|
|
{
|
|
name: "controller with no health service",
|
|
controller: &controller.Controller{HealthService: nil},
|
|
gracePeriod: 1 * time.Minute,
|
|
expMinWait: 0 * time.Second,
|
|
expMaxWait: 500 * time.Microsecond,
|
|
},
|
|
{
|
|
name: "no grace period set",
|
|
controller: &controller.Controller{HealthService: &health.Service{}},
|
|
expMinWait: 0 * time.Second,
|
|
expMaxWait: 500 * time.Microsecond,
|
|
},
|
|
{
|
|
name: "wait 500ms",
|
|
bundles: []*opsBundle{{}},
|
|
controller: &controller.Controller{HealthService: &health.Service{}},
|
|
gracePeriod: 500 * time.Millisecond,
|
|
expMinWait: 500 * time.Millisecond,
|
|
expMaxWait: 600 * time.Millisecond,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s := Server{
|
|
bundles: tt.bundles,
|
|
controller: tt.controller,
|
|
}
|
|
|
|
start := time.Now()
|
|
s.WaitIfHealthExists(tt.gracePeriod, cli.NewMockUi())
|
|
actualWait := time.Since(start)
|
|
|
|
require.GreaterOrEqual(t, actualWait, tt.expMinWait)
|
|
require.LessOrEqual(t, actualWait, tt.expMaxWait)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateOpsHandler(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupController bool
|
|
lncfg *listenerutil.ListenerConfig
|
|
expErr bool
|
|
expErrMsg string
|
|
assertions func(t *testing.T, addr string)
|
|
}{
|
|
{
|
|
name: "no controller set",
|
|
setupController: false,
|
|
lncfg: &listenerutil.ListenerConfig{},
|
|
assertions: func(t *testing.T, addr string) {
|
|
rsp, err := http.Get("http://" + addr + "/health")
|
|
require.NoError(t, err) // We can do the GET request
|
|
require.Equal(t, http.StatusNotFound, rsp.StatusCode) // But the endpoint doesn't exist
|
|
},
|
|
},
|
|
{
|
|
name: "controller set",
|
|
setupController: true,
|
|
lncfg: &listenerutil.ListenerConfig{},
|
|
assertions: func(t *testing.T, addr string) {
|
|
rsp, err := http.Get("http://" + addr + "/health")
|
|
require.NoError(t, err) // We can do the GET request
|
|
require.Equal(t, http.StatusOK, rsp.StatusCode) // And the endpoint exists
|
|
},
|
|
},
|
|
{
|
|
name: "controller set, but nil listener config",
|
|
setupController: true,
|
|
lncfg: nil,
|
|
expErr: true,
|
|
expErrMsg: "controller.(Controller).GetHealthHandler: received nil listener config",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var c *controller.Controller
|
|
if tt.setupController {
|
|
tc := controller.NewTestController(t, &controller.TestControllerOpts{})
|
|
t.Cleanup(tc.Shutdown)
|
|
|
|
c = tc.Controller()
|
|
}
|
|
|
|
h, err := createOpsHandler(tt.lncfg, c)
|
|
if tt.expErr {
|
|
require.EqualError(t, err, tt.expErrMsg)
|
|
require.Nil(t, h)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
s := http.Server{Handler: h}
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
go s.Serve(l)
|
|
|
|
t.Cleanup(func() {
|
|
require.NoError(t, s.Shutdown(context.Background()))
|
|
})
|
|
|
|
tt.assertions(t, l.Addr().String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateHttpServer(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
lncfg *listenerutil.ListenerConfig
|
|
assertions func(t *testing.T, s *http.Server)
|
|
}{
|
|
{
|
|
name: "default values are set if none are set in listener config",
|
|
lncfg: &listenerutil.ListenerConfig{},
|
|
assertions: func(t *testing.T, s *http.Server) {
|
|
require.Equal(t, 10*time.Second, s.ReadHeaderTimeout)
|
|
require.Equal(t, 30*time.Second, s.ReadTimeout)
|
|
require.Equal(t, 5*time.Minute, s.IdleTimeout)
|
|
},
|
|
},
|
|
{
|
|
name: "configured values are respected",
|
|
lncfg: &listenerutil.ListenerConfig{
|
|
HTTPReadHeaderTimeout: time.Minute,
|
|
HTTPReadTimeout: 2 * time.Minute,
|
|
HTTPWriteTimeout: 3 * time.Minute,
|
|
HTTPIdleTimeout: 4 * time.Minute,
|
|
},
|
|
assertions: func(t *testing.T, s *http.Server) {
|
|
require.Equal(t, time.Minute, s.ReadHeaderTimeout)
|
|
require.Equal(t, 2*time.Minute, s.ReadTimeout)
|
|
require.Equal(t, 3*time.Minute, s.WriteTimeout)
|
|
require.Equal(t, 4*time.Minute, s.IdleTimeout)
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
l := hclog.Default()
|
|
|
|
s := createHttpServer(l, http.NotFoundHandler(), tt.lncfg)
|
|
require.NotNil(t, s)
|
|
require.NotNil(t, s.Handler)
|
|
require.NotNil(t, s.ErrorLog)
|
|
|
|
tt.assertions(t, s)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListenerCloseErrorCheck(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
lnType string
|
|
err error
|
|
expErr error
|
|
}{
|
|
{
|
|
name: "nil err",
|
|
lnType: "tcp",
|
|
err: nil,
|
|
expErr: nil,
|
|
},
|
|
{
|
|
name: "net.Closed",
|
|
lnType: "tcp",
|
|
err: net.ErrClosed,
|
|
expErr: nil,
|
|
},
|
|
{
|
|
name: "path err not unix type",
|
|
lnType: "tcp",
|
|
err: &os.PathError{Op: "test"},
|
|
expErr: &os.PathError{Op: "test"},
|
|
},
|
|
{
|
|
name: "path err unix type",
|
|
lnType: "unix",
|
|
err: &os.PathError{Op: "test"},
|
|
expErr: nil,
|
|
},
|
|
{
|
|
name: "literally anything else",
|
|
lnType: "tcp",
|
|
err: fmt.Errorf("oops I errored"),
|
|
expErr: fmt.Errorf("oops I errored"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := listenerCloseErrorCheck(tt.lnType, tt.err)
|
|
require.Equal(t, tt.expErr, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testTlsSetup(t *testing.T, certPath, keyPath string) {
|
|
t.Cleanup(func() {
|
|
require.NoError(t, os.Remove(certPath))
|
|
require.NoError(t, os.Remove(keyPath))
|
|
})
|
|
|
|
certBytes, _, priv := createTestCert(t)
|
|
|
|
certFile, err := os.Create(certPath)
|
|
require.NoError(t, err)
|
|
require.NoError(t, pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes}))
|
|
require.NoError(t, certFile.Close())
|
|
|
|
keyFile, err := os.Create(keyPath)
|
|
require.NoError(t, err)
|
|
|
|
marshaledKey, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, pem.Encode(keyFile, &pem.Block{Type: "PRIVATE KEY", Bytes: marshaledKey}))
|
|
require.NoError(t, keyFile.Close())
|
|
}
|
|
|
|
func testTlsHttpClient(t *testing.T, certPath string) *http.Client {
|
|
f, err := os.Open(certPath)
|
|
require.NoError(t, err)
|
|
|
|
certBytes, err := ioutil.ReadAll(f)
|
|
require.NoError(t, err)
|
|
require.NoError(t, f.Close())
|
|
|
|
certPool := x509.NewCertPool()
|
|
certPool.AppendCertsFromPEM(certBytes)
|
|
|
|
return &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{RootCAs: certPool},
|
|
},
|
|
}
|
|
}
|
|
|
|
func createTestCert(t *testing.T) ([]byte, ed25519.PublicKey, ed25519.PrivateKey) {
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
template := &x509.Certificate{
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign,
|
|
SerialNumber: big.NewInt(0),
|
|
NotBefore: time.Now().Add(-30 * time.Second),
|
|
NotAfter: time.Now().Add(5 * time.Minute),
|
|
BasicConstraintsValid: true,
|
|
IsCA: true,
|
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
|
DNSNames: []string{"/tmp/boundary-opslistener-test0.sock", "/tmp/boundary-opslistener-test1.sock"},
|
|
}
|
|
|
|
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv)
|
|
require.NoError(t, err)
|
|
|
|
return certBytes, pub, priv
|
|
}
|