* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * Update copyright file headers to BUS-1.1 * Rerun make gen This will pick up the last of the license changes * Revert "Rerun make gen" This reverts commit dbe0968bffd92745d8663c6940debae098395a6b. * Remove copywrite from generation script The current version of copywrite is not sophisticated enough to only apply the copywrite changes to certain subdirectories, so to avoid accidentally relicensing our api and sdk modules, we will stop running the copywrite script until such time that it can do the right thing. * License protobuf API as MPL The protobuf API files are used to generate the SDK, since we want the SDK to be MPL, the protobuf files must also be MPL. This extends to all protobuf files whose sources end up in the SDK directory, including: - internal/proto/controller/api - internal/proto/controller/custom_options - internal/proto/plugin It also includes the generated service directory in internal/gen/controller/api/services, since they are generated from the protobuf sources used for the SDK. * Change website license file name This makes the naming scheme consistent with the rest of the repository. * Add note on licensing to README * Set copywrite license to BUSL * Revert "Add note on licensing to README" This reverts commit 4d865a91a7a4f11605f0dc4609355d7e3c3cfffe. --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
956 lines
27 KiB
Go
956 lines
27 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package ops
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang/protobuf/jsonpb"
|
|
"github.com/google/go-cmp/cmp"
|
|
"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/boundary/internal/daemon/worker"
|
|
pbs "github.com/hashicorp/boundary/internal/gen/ops/services"
|
|
pbhealth "github.com/hashicorp/boundary/internal/gen/worker/health"
|
|
"github.com/hashicorp/boundary/internal/server"
|
|
"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/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/testing/protocmp"
|
|
"google.golang.org/protobuf/types/known/wrapperspb"
|
|
)
|
|
|
|
func TestNewServer(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
logger hclog.Logger
|
|
c *controller.Controller
|
|
w *worker.Worker
|
|
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(context.Background(), tt.logger, tt.c, tt.w, 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(context.Background(), hclog.Default(), nil, 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)
|
|
|
|
// Controller has started and is set onto our Command object, start ops.
|
|
opsServer, err := NewServer(tc.Context(), hclog.Default(), tc.Controller(), nil, 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) {
|
|
ctx := context.Background()
|
|
tests := []struct {
|
|
name string
|
|
setupController bool
|
|
setupWorker 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
|
|
body, err := io.ReadAll(rsp.Body)
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, []byte("{}"), body)
|
|
|
|
rsp, err = http.Get("http://" + addr + "/health?worker_info=1")
|
|
require.NoError(t, err) // We can do the GET request
|
|
require.Equal(t, http.StatusOK, rsp.StatusCode) // And the endpoint exists
|
|
body, err = io.ReadAll(rsp.Body)
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, []byte("{}"), body)
|
|
},
|
|
},
|
|
{
|
|
name: "worker set",
|
|
setupWorker: 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
|
|
body, err := io.ReadAll(rsp.Body)
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, []byte("{}"), body)
|
|
|
|
rsp, err = http.Get("http://" + addr + "/health?worker_info=1")
|
|
require.NoError(t, err) // We can do the GET request
|
|
require.Equal(t, http.StatusOK, rsp.StatusCode) // And the endpoint exists
|
|
pbResp := &pbs.GetHealthResponse{}
|
|
require.NoError(t, jsonpb.Unmarshal(rsp.Body, pbResp))
|
|
want := &pbs.GetHealthResponse{WorkerProcessInfo: &pbhealth.HealthInfo{
|
|
State: server.ActiveOperationalState.String(),
|
|
ActiveSessionCount: wrapperspb.UInt32(0),
|
|
}}
|
|
assert.Empty(t, cmp.Diff(want, pbResp, protocmp.Transform()))
|
|
},
|
|
},
|
|
{
|
|
name: "worker and controller set",
|
|
setupController: true,
|
|
setupWorker: 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
|
|
body, err := io.ReadAll(rsp.Body)
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, []byte("{}"), body)
|
|
rsp, err = http.Get("http://" + addr + "/health?worker_info=1")
|
|
require.NoError(t, err) // We can do the GET request
|
|
require.Equal(t, http.StatusOK, rsp.StatusCode) // And the endpoint exists
|
|
pbResp := &pbs.GetHealthResponse{}
|
|
require.NoError(t, jsonpb.Unmarshal(rsp.Body, pbResp))
|
|
want := &pbs.GetHealthResponse{WorkerProcessInfo: &pbhealth.HealthInfo{
|
|
State: server.ActiveOperationalState.String(),
|
|
ActiveSessionCount: wrapperspb.UInt32(0),
|
|
}}
|
|
assert.Empty(t, cmp.Diff(want, pbResp, protocmp.Transform()))
|
|
},
|
|
},
|
|
{
|
|
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{})
|
|
|
|
c = tc.Controller()
|
|
}
|
|
var w *worker.Worker
|
|
if tt.setupWorker {
|
|
tc := worker.NewTestWorker(t, &worker.TestWorkerOpts{})
|
|
w = tc.Worker()
|
|
}
|
|
|
|
h, err := createOpsHandler(ctx, tt.lncfg, c, w)
|
|
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(ctx))
|
|
})
|
|
|
|
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
|
|
}
|