* oss: feat(proto): statistics rpc added to server coordination service * oss: feat(session): add update statistics status service * oss: feat(handlers): add Statistics method to worker service server * oss: feat(worker): add statistics ticking * oss: internal/proto: define the new RoutingInfo RPC * oss: internal/server: repository changes for RoutingInfo RPC * oss: internal/daemon: add RoutingInfo handler to worker service The new RoutingInfo RPC is used to get information from the worker that is used to make routing decisions on the controller. It also handles the initial request from the worker. * oss: feat(db): server_worker_session_info table * oss: feat(proto): session info rpc for server coordination service * oss: feat(server): worker session info request data type * oss: feat(worker): session info ticker * oss: feat(handler): server coordination handler for session info * oss: feat(session): utilize session info request time for session cleanup * oss: proto: fix some gotags comments Some of these fields were missing comments, while others had incorrectly formatted comments. * oss: worker: send connection status to server This is required for the correct tracking of orphaned connections * oss: internal/daemon/worker: add function to wait for Statistics RPC * oss: internal/daemon/worker: add routing info RPC The new RoutingInfo RPC replaces part of the Status RPC, namely the part which is used by the controller and worker to perform decisions around the establishment of new sessions and new worker connections. * oss: internal/daemon/worker: remove unnecessary error check * oss: internal/daemon/worker: lock access to config and upstreams The new confLock protects access to the conf value and to the addressReceivers slice. * oss: worker: replace Status RPC with RoutingInfo * oss: worker: deprecate the Status RPC and related types The Status RPC and related messages have been replaced by the RoutingInfo/SessionInfo/Statistics RPCs. The controller will continue to support the Status RPC until two new minor versions have been released. * oss: internal/event: add default deny for new worker RPCs * oss: internal/daemon/worker: allow RPC interval to be set The new SessionInfo/RoutingInfo/Statistics RPCs run at long intervals, which make some of our cluster tests take a long time to complete. This allows their interval to be configured in tests. * oss: all: use TestWorkerRPCInterval in slow tests This should speed up these tests significantly. * oss: internal/tests/cluster: reset sys eventer in test --------- Co-authored-by: Damian Debkowski <damian.debkowski@hashicorp.com>
989 lines
29 KiB
Go
989 lines
29 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package controller
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
|
"github.com/hashicorp/boundary/api"
|
|
"github.com/hashicorp/boundary/api/authmethods"
|
|
"github.com/hashicorp/boundary/api/authtokens"
|
|
"github.com/hashicorp/boundary/globals"
|
|
"github.com/hashicorp/boundary/internal/authtoken"
|
|
"github.com/hashicorp/boundary/internal/cmd/base"
|
|
"github.com/hashicorp/boundary/internal/cmd/config"
|
|
"github.com/hashicorp/boundary/internal/credential/vault"
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"github.com/hashicorp/boundary/internal/db/schema"
|
|
"github.com/hashicorp/boundary/internal/event"
|
|
"github.com/hashicorp/boundary/internal/gen/testing/interceptor"
|
|
"github.com/hashicorp/boundary/internal/host/plugin"
|
|
"github.com/hashicorp/boundary/internal/iam"
|
|
"github.com/hashicorp/boundary/internal/kms"
|
|
"github.com/hashicorp/boundary/internal/ratelimit"
|
|
"github.com/hashicorp/boundary/internal/scheduler"
|
|
"github.com/hashicorp/boundary/internal/server"
|
|
"github.com/hashicorp/boundary/internal/session"
|
|
"github.com/hashicorp/go-hclog"
|
|
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
|
"github.com/hashicorp/go-kms-wrapping/v2/extras/multi"
|
|
"github.com/hashicorp/go-secure-stdlib/base62"
|
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/test/bufconn"
|
|
)
|
|
|
|
const (
|
|
DefaultOrgId = "o_1234567890"
|
|
DefaultProjectId = "p_1234567890"
|
|
DefaultTestPasswordAuthMethodId = "ampw_1234567890"
|
|
DefaultTestOidcAuthMethodId = "amoidc_1234567890"
|
|
DefaultTestLdapAuthMethodId = globals.LdapAuthMethodPrefix + "_1234567890"
|
|
DefaultTestLoginName = "admin"
|
|
DefaultTestUnprivilegedLoginName = "user"
|
|
DefaultTestPassword = "passpass"
|
|
DefaultTestUserId = "u_1234567890"
|
|
DefaultTestPasswordAccountId = globals.PasswordAccountPrefix + "_1234567890"
|
|
DefaultTestOidcAccountId = "acctoidc_1234567890"
|
|
DefaultTestUnprivilegedPasswordAccountId = globals.PasswordAccountPrefix + "_0987654321"
|
|
DefaultTestUnprivilegedOidcAccountId = "acctoidc_0987654321"
|
|
DefaultTestPluginId = "pl_1234567890"
|
|
)
|
|
|
|
// TestController wraps a base.Server and Controller to provide a
|
|
// fully-programmatic controller for tests. Error checking (for instance, for
|
|
// valid config) is not stringent at the moment.
|
|
type TestController struct {
|
|
b *base.Server
|
|
c *Controller
|
|
t testing.TB
|
|
apiAddrs []string // The address the Controller API is listening on
|
|
clusterAddrs []string
|
|
client *api.Client
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
name string
|
|
opts *TestControllerOpts
|
|
shutdownDoneCh chan struct{}
|
|
shutdownOnce *sync.Once
|
|
}
|
|
|
|
// Server returns the underlying base server
|
|
func (tc *TestController) Server() *base.Server {
|
|
return tc.b
|
|
}
|
|
|
|
// Controller returns the underlying controller
|
|
func (tc *TestController) Controller() *Controller {
|
|
return tc.c
|
|
}
|
|
|
|
func (tc *TestController) Config() *Config {
|
|
return tc.c.conf
|
|
}
|
|
|
|
func (tc *TestController) Client() *api.Client {
|
|
return tc.client
|
|
}
|
|
|
|
func (tc *TestController) Context() context.Context {
|
|
return tc.ctx
|
|
}
|
|
|
|
func (tc *TestController) Kms() *kms.Kms {
|
|
return tc.c.kms
|
|
}
|
|
|
|
func (tc *TestController) IamRepo() *iam.Repository {
|
|
repo, err := tc.c.IamRepoFn()
|
|
if err != nil {
|
|
tc.t.Fatal(err)
|
|
}
|
|
return repo
|
|
}
|
|
|
|
func (tc *TestController) AuthTokenRepo() *authtoken.Repository {
|
|
repo, err := tc.c.AuthTokenRepoFn()
|
|
if err != nil {
|
|
tc.t.Fatal(err)
|
|
}
|
|
return repo
|
|
}
|
|
|
|
func (tc *TestController) ServersRepo() *server.Repository {
|
|
repo, err := tc.c.ServersRepoFn()
|
|
if err != nil {
|
|
tc.t.Fatal(err)
|
|
}
|
|
return repo
|
|
}
|
|
|
|
func (tc *TestController) ConnectionsRepo() *session.ConnectionRepository {
|
|
repo, err := tc.c.ConnectionRepoFn()
|
|
if err != nil {
|
|
tc.t.Fatal(err)
|
|
}
|
|
return repo
|
|
}
|
|
|
|
func (tc *TestController) PluginHostRepo() *plugin.Repository {
|
|
repo, err := tc.c.PluginHostRepoFn()
|
|
if err != nil {
|
|
tc.t.Fatal(err)
|
|
}
|
|
return repo
|
|
}
|
|
|
|
func (tc *TestController) VaultCredentialRepo() *vault.Repository {
|
|
repo, err := tc.c.VaultCredentialRepoFn()
|
|
if err != nil {
|
|
tc.t.Fatal(err)
|
|
}
|
|
return repo
|
|
}
|
|
|
|
func (tc *TestController) Scheduler() *scheduler.Scheduler {
|
|
return tc.Controller().scheduler
|
|
}
|
|
|
|
func (tc *TestController) Cancel() {
|
|
tc.cancel()
|
|
}
|
|
|
|
func (tc *TestController) Name() string {
|
|
return tc.name
|
|
}
|
|
|
|
func (tc *TestController) ApiAddrs() []string {
|
|
return tc.addrs("api")
|
|
}
|
|
|
|
func (tc *TestController) ClusterAddrs() []string {
|
|
return tc.addrs("cluster")
|
|
}
|
|
|
|
func (tc *TestController) DbConn() *db.DB {
|
|
return tc.b.Database
|
|
}
|
|
|
|
func (tc *TestController) Logger() hclog.Logger {
|
|
return tc.b.Logger
|
|
}
|
|
|
|
func (tc *TestController) Token() *authtokens.AuthToken {
|
|
if tc.opts.DisableAuthMethodCreation {
|
|
tc.t.Fatal("no default auth method ID configured")
|
|
return nil
|
|
}
|
|
result, err := authmethods.NewClient(tc.Client()).Authenticate(
|
|
tc.Context(),
|
|
tc.b.DevPasswordAuthMethodId,
|
|
"login",
|
|
map[string]any{
|
|
"login_name": tc.b.DevLoginName,
|
|
"password": tc.b.DevPassword,
|
|
},
|
|
)
|
|
if err != nil {
|
|
tc.t.Fatal(fmt.Errorf("error logging in: %w", err))
|
|
return nil
|
|
}
|
|
token := new(authtokens.AuthToken)
|
|
if err := json.Unmarshal(result.GetRawAttributes(), token); err != nil {
|
|
tc.t.Fatal(fmt.Errorf("error unmarshaling token: %w", err))
|
|
return nil
|
|
}
|
|
return token
|
|
}
|
|
|
|
func (tc *TestController) UnprivilegedToken() *authtokens.AuthToken {
|
|
if tc.opts.DisableAuthMethodCreation {
|
|
tc.t.Error("no default auth method ID configured")
|
|
return nil
|
|
}
|
|
result, err := authmethods.NewClient(tc.Client()).Authenticate(
|
|
tc.Context(),
|
|
tc.b.DevPasswordAuthMethodId,
|
|
"login",
|
|
map[string]any{
|
|
"login_name": tc.b.DevUnprivilegedLoginName,
|
|
"password": tc.b.DevUnprivilegedPassword,
|
|
},
|
|
)
|
|
if err != nil {
|
|
tc.t.Error(fmt.Errorf("error logging in: %w", err))
|
|
return nil
|
|
}
|
|
token := new(authtokens.AuthToken)
|
|
if err := json.Unmarshal(result.GetRawAttributes(), token); err != nil {
|
|
tc.t.Error(fmt.Errorf("error unmarshaling token: %w", err))
|
|
return nil
|
|
}
|
|
return token
|
|
}
|
|
|
|
func (tc *TestController) addrs(purpose string) []string {
|
|
var prefix string
|
|
switch purpose {
|
|
case "api":
|
|
if tc.apiAddrs != nil {
|
|
return tc.apiAddrs
|
|
}
|
|
prefix = "http://"
|
|
case "cluster":
|
|
if tc.clusterAddrs != nil {
|
|
return tc.clusterAddrs
|
|
}
|
|
}
|
|
|
|
addrs := make([]string, 0, len(tc.b.Listeners))
|
|
for _, listener := range tc.b.Listeners {
|
|
if listener.Config.Purpose[0] == purpose {
|
|
var addr net.Addr
|
|
switch purpose {
|
|
case "api":
|
|
addr = listener.ApiListener.Addr()
|
|
case "cluster":
|
|
addr = listener.ClusterListener.Addr()
|
|
case "ops":
|
|
addr = listener.OpsListener.Addr()
|
|
}
|
|
switch {
|
|
case strings.HasPrefix(addr.String(), "/"):
|
|
switch purpose {
|
|
case "api":
|
|
addrs = append(addrs, fmt.Sprintf("unix://%s", addr.String()))
|
|
default:
|
|
addrs = append(addrs, addr.String())
|
|
}
|
|
default:
|
|
tcpAddr, ok := addr.(*net.TCPAddr)
|
|
if !ok {
|
|
tc.t.Fatal("could not parse address as a TCP addr")
|
|
}
|
|
addr := fmt.Sprintf("%s%s", prefix, net.JoinHostPort(tcpAddr.IP.String(), strconv.Itoa(tcpAddr.Port)))
|
|
addrs = append(addrs, addr)
|
|
}
|
|
}
|
|
}
|
|
|
|
switch purpose {
|
|
case "api":
|
|
tc.apiAddrs = addrs
|
|
case "cluster":
|
|
tc.clusterAddrs = addrs
|
|
}
|
|
|
|
return addrs
|
|
}
|
|
|
|
func (tc *TestController) buildClient() {
|
|
client, err := api.NewClient(nil)
|
|
if err != nil {
|
|
tc.t.Fatal(fmt.Errorf("error creating client: %w", err))
|
|
}
|
|
apiAddrs := tc.ApiAddrs()
|
|
if len(apiAddrs) == 0 {
|
|
tc.t.Fatal("no API addresses found")
|
|
}
|
|
if err := client.SetAddr(apiAddrs[0]); err != nil {
|
|
tc.t.Fatal(fmt.Errorf("error setting client address: %w", err))
|
|
}
|
|
// Because this is using the real lib it can pick up from stored locations
|
|
// like the system keychain. Explicitly clear the token to ensure we
|
|
// understand the client state at the start of each test.
|
|
client.SetToken("")
|
|
|
|
tc.client = client
|
|
}
|
|
|
|
// Shutdown runs any cleanup functions; be sure to run this after your test is
|
|
// done
|
|
func (tc *TestController) Shutdown() {
|
|
tc.shutdownOnce.Do(func() {
|
|
if tc.b != nil {
|
|
tc.b.ContextCancel()
|
|
}
|
|
|
|
tc.cancel()
|
|
|
|
if tc.c != nil {
|
|
if err := tc.c.Shutdown(); err != nil {
|
|
tc.t.Error(err)
|
|
}
|
|
}
|
|
if tc.b != nil {
|
|
if err := tc.b.RunShutdownFuncs(); err != nil {
|
|
tc.t.Error(err)
|
|
}
|
|
if !tc.opts.DisableDatabaseDestruction {
|
|
if tc.b.DestroyDevDatabase(tc.ctx) != nil {
|
|
if err := tc.b.DestroyDevDatabase(tc.ctx); err != nil {
|
|
tc.t.Error(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
close(tc.shutdownDoneCh)
|
|
})
|
|
}
|
|
|
|
type TestControllerOpts struct {
|
|
// ConfigHcl is the HCL to be parsed to generate the initial config.
|
|
// Overrides Config if both are set.
|
|
ConfigHcl string
|
|
|
|
// Config; if not provided a dev one will be created, unless ConfigHcl is
|
|
// set.
|
|
Config *config.Config
|
|
|
|
// DefaultPasswordAuthMethodId is the default password method ID to use, if set.
|
|
DefaultPasswordAuthMethodId string
|
|
|
|
// DefaultOidcAuthMethodId is the default OIDC method ID to use, if set.
|
|
DefaultOidcAuthMethodId string
|
|
|
|
// DefaultLdapAuthMethodId is the default LDAP method ID to use, if set.
|
|
DefaultLdapAuthMethodId string
|
|
|
|
// DefaultLoginName is the login name used when creating the default admin account.
|
|
DefaultLoginName string
|
|
|
|
// DefaultUnprivilegedLoginName is the login name used when creating the default unprivileged account.
|
|
DefaultUnprivilegedLoginName string
|
|
|
|
// DefaultPassword is the password used when creating the default accounts.
|
|
DefaultPassword string
|
|
|
|
// DisableInitialLoginRoleCreation can be set true to disable creating the global
|
|
// scope default role automatically.
|
|
DisableInitialLoginRoleCreation bool
|
|
|
|
// DisableAuthMethodCreation can be set true to disable creating an auth
|
|
// method automatically.
|
|
DisableAuthMethodCreation bool
|
|
|
|
// DisableOidcAuthMethodCreation can be set true to disable the built-in
|
|
// OIDC listener. Useful for e.g. unix listener tests.
|
|
DisableOidcAuthMethodCreation bool
|
|
|
|
// DisableLdapAuthMethodCreation can be set true to disable the built-in
|
|
// ldap listener. Useful for e.g. unix listener tests.
|
|
DisableLdapAuthMethodCreation bool
|
|
|
|
// DisableScopesCreation can be set true to disable creating scopes
|
|
// automatically.
|
|
DisableScopesCreation bool
|
|
|
|
// DisableHostResourcesCreation can be set true to disable creating a host
|
|
// catalog and related resources automatically.
|
|
DisableHostResourcesCreation bool
|
|
|
|
// DisableTargetCreation can be set true to disable creating a target
|
|
// automatically.
|
|
DisableTargetCreation bool
|
|
|
|
// DisableDatabaseCreation can be set true to disable creating a dev
|
|
// database
|
|
DisableDatabaseCreation bool
|
|
|
|
// DisableDatabaseDestruction can be set true to allow a database to be
|
|
// created but examined after-the-fact
|
|
DisableDatabaseDestruction bool
|
|
|
|
// DatabaseUrl will cause the test controller to connect to an existing
|
|
// database given the url instead of creating a new one
|
|
DatabaseUrl string
|
|
|
|
// DisableDatabaseTemplate forces using a fresh Postgres instance in Docker
|
|
// instead of using a local templated version. Useful for CI of external
|
|
// repos, like Terraform.
|
|
DisableDatabaseTemplate bool
|
|
|
|
// If true, the controller will not be started
|
|
DisableAutoStart bool
|
|
|
|
// EnableEventing, if true the test controller will create sys and error
|
|
// events. You must not run the test in parallel (no calls to t.Parallel)
|
|
// since the this option relies on modifying the system wide default
|
|
// eventer.
|
|
EnableEventing bool
|
|
|
|
// EventerConfig allows specifying a custom event config. You must not run
|
|
// the test in parallel (no calls to t.Parallel) since the this option
|
|
// relies on modifying the system wide default eventer.
|
|
EventerConfig *event.EventerConfig
|
|
|
|
// DisableAuthorizationFailures will still cause authz checks to be
|
|
// performed but they won't cause 403 Forbidden. Useful for API-level
|
|
// testing to avoid a lot of faff.
|
|
DisableAuthorizationFailures bool
|
|
|
|
// The controller KMS to use, or one will be created
|
|
RootKms wrapping.Wrapper
|
|
|
|
// The worker auth KMS to use, or one will be created
|
|
WorkerAuthKms wrapping.Wrapper
|
|
|
|
// The downstream worker auth KMS to use, or one will be created
|
|
DownstreamWorkerAuthKms *multi.PooledWrapper
|
|
|
|
// The BSR wrapper to use, or one will be created
|
|
BsrKms wrapping.Wrapper
|
|
|
|
// The recovery KMS to use, or one will be created
|
|
RecoveryKms wrapping.Wrapper
|
|
|
|
// Disables KMS key creation. Only valid when a database url is specified,
|
|
// at the moment.
|
|
DisableKmsKeyCreation bool
|
|
|
|
// The name to use for the controller, otherwise one will be randomly
|
|
// generated, unless provided in a non-nil Config
|
|
Name string
|
|
|
|
// The suffix to use for initial resources
|
|
InitialResourcesSuffix string
|
|
|
|
// The logger to use, or one will be created
|
|
Logger hclog.Logger
|
|
|
|
// The registerer to use for registering all the collectors. Nil means
|
|
// no metrics are registered.
|
|
PrometheusRegisterer prometheus.Registerer
|
|
|
|
// A cluster address for overriding the advertised controller listener
|
|
// (overrides address provided in config, if any)
|
|
PublicClusterAddr string
|
|
|
|
// The amount of time to wait before marking connections as canceling when a
|
|
// worker has not reported in, and whether a worker is considered to be
|
|
// routable based on the last time it received a routing info report.
|
|
WorkerRPCGracePeriod time.Duration
|
|
|
|
// The period of time after which it will consider other controllers to be
|
|
// no longer accessible, based on time since their last status update in the
|
|
// database
|
|
LivenessTimeToStaleDuration time.Duration
|
|
|
|
// The amount of time between the scheduler waking up to run it's
|
|
// registered jobs.
|
|
//
|
|
// If t.Deadline() has a value and the value is under 1 minute, the
|
|
// default value is set to half the value of t.Deadline(). If
|
|
// t.Deadline() has a value and the value is 1 minute or more, the
|
|
// default value is set to 1 minute.
|
|
//
|
|
// Tests using the Vault test server should be aware that Vault
|
|
// Credential Stores only accept Vault tokens that have a TTL greater
|
|
// than the SchedulerRunJobInterval. The Vault test server, by default,
|
|
// creates Vault tokens with a TTL equal to the duration of time
|
|
// remaining until t.Deadline() is reached.
|
|
SchedulerRunJobInterval time.Duration
|
|
|
|
// The time to use for CA certificate lifetime for worker auth
|
|
WorkerAuthCaCertificateLifetime time.Duration
|
|
|
|
// Toggle worker auth debugging
|
|
WorkerAuthDebuggingEnabled *atomic.Bool
|
|
|
|
DisableRateLimiting bool
|
|
|
|
EnableIPv6 bool
|
|
}
|
|
|
|
func NewTestController(t testing.TB, opts *TestControllerOpts) *TestController {
|
|
const op = "controller.NewTestController"
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
if opts == nil {
|
|
opts = new(TestControllerOpts)
|
|
}
|
|
|
|
tc := &TestController{
|
|
t: t,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
opts: opts,
|
|
shutdownDoneCh: make(chan struct{}),
|
|
shutdownOnce: new(sync.Once),
|
|
}
|
|
t.Cleanup(tc.Shutdown)
|
|
|
|
conf := TestControllerConfig(t, ctx, tc, opts)
|
|
var err error
|
|
tc.c, err = New(ctx, conf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tc.buildClient()
|
|
|
|
// The real server functions will listen for shutdown cues and act so mimic
|
|
// that here, and ensure that channels get drained
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-tc.b.ShutdownCh:
|
|
tc.Shutdown()
|
|
case <-tc.b.ServerSideShutdownCh:
|
|
tc.Shutdown()
|
|
case <-tc.shutdownDoneCh:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
if !opts.DisableAutoStart {
|
|
if err := tc.c.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
return tc
|
|
}
|
|
|
|
// TestControllerConfig provides a way to create a config for a TestController.
|
|
// The tc passed as a parameter will be modified by this func.
|
|
func TestControllerConfig(t testing.TB, ctx context.Context, tc *TestController, opts *TestControllerOpts) *Config {
|
|
const op = "controller.TestControllerConfig"
|
|
if opts == nil {
|
|
opts = new(TestControllerOpts)
|
|
}
|
|
|
|
ctxTest, cancel := context.WithCancel(context.Background())
|
|
|
|
// Base server
|
|
tc.b = base.NewServer(&base.Command{
|
|
Context: ctxTest,
|
|
ContextCancel: cancel,
|
|
ShutdownCh: make(chan struct{}),
|
|
})
|
|
tc.b.WorkerAuthDebuggingEnabled = opts.WorkerAuthDebuggingEnabled
|
|
|
|
// Get dev config, or use a provided one
|
|
var err error
|
|
switch {
|
|
case opts.ConfigHcl != "":
|
|
cfg, err := config.Parse(opts.ConfigHcl)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts.Config = cfg
|
|
|
|
case opts.Config == nil:
|
|
cfgOpts := append([]config.Option{}, config.WithIPv6Enabled(true))
|
|
opts.Config, err = config.DevController(cfgOpts...)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts.Config.Controller.Name = opts.Name
|
|
}
|
|
|
|
if opts.DefaultPasswordAuthMethodId != "" {
|
|
tc.b.DevPasswordAuthMethodId = opts.DefaultPasswordAuthMethodId
|
|
} else {
|
|
tc.b.DevPasswordAuthMethodId = DefaultTestPasswordAuthMethodId
|
|
}
|
|
if opts.DefaultOidcAuthMethodId != "" {
|
|
tc.b.DevOidcAuthMethodId = opts.DefaultOidcAuthMethodId
|
|
} else {
|
|
tc.b.DevOidcAuthMethodId = DefaultTestOidcAuthMethodId
|
|
}
|
|
if opts.DefaultLdapAuthMethodId != "" {
|
|
tc.b.DevLdapAuthMethodId = opts.DefaultLdapAuthMethodId
|
|
} else {
|
|
tc.b.DevLdapAuthMethodId = DefaultTestLdapAuthMethodId
|
|
}
|
|
if opts.DefaultLoginName != "" {
|
|
tc.b.DevLoginName = opts.DefaultLoginName
|
|
} else {
|
|
tc.b.DevLoginName = DefaultTestLoginName
|
|
}
|
|
if opts.DefaultUnprivilegedLoginName != "" {
|
|
tc.b.DevUnprivilegedLoginName = opts.DefaultUnprivilegedLoginName
|
|
} else {
|
|
tc.b.DevUnprivilegedLoginName = DefaultTestUnprivilegedLoginName
|
|
}
|
|
if opts.DefaultPassword != "" {
|
|
tc.b.DevPassword = opts.DefaultPassword
|
|
tc.b.DevUnprivilegedPassword = opts.DefaultPassword
|
|
} else {
|
|
tc.b.DevPassword = DefaultTestPassword
|
|
tc.b.DevUnprivilegedPassword = DefaultTestPassword
|
|
}
|
|
tc.b.DevOrgId = DefaultOrgId
|
|
tc.b.DevProjectId = DefaultProjectId
|
|
tc.b.DevPasswordAccountId = DefaultTestPasswordAccountId
|
|
tc.b.DevOidcAccountId = DefaultTestOidcAccountId
|
|
tc.b.DevUnprivilegedPasswordAccountId = DefaultTestUnprivilegedPasswordAccountId
|
|
tc.b.DevUnprivilegedOidcAccountId = DefaultTestUnprivilegedOidcAccountId
|
|
tc.b.DevLoopbackPluginId = DefaultTestPluginId
|
|
|
|
// Alias targets are only used as a dev example
|
|
tc.b.SkipAliasTargetCreation = true
|
|
|
|
tc.b.EnabledPlugins = append(tc.b.EnabledPlugins, base.EnabledPluginLoopback)
|
|
|
|
// Start a logger
|
|
tc.b.Logger = opts.Logger
|
|
if tc.b.Logger == nil {
|
|
tc.b.Logger = hclog.New(&hclog.LoggerOptions{
|
|
Level: hclog.Trace,
|
|
Mutex: tc.b.StderrLock,
|
|
})
|
|
}
|
|
|
|
tc.b.PrometheusRegisterer = opts.PrometheusRegisterer
|
|
if opts.Config.Controller == nil {
|
|
opts.Config.Controller = new(config.Controller)
|
|
}
|
|
if opts.Config.Controller.Name == "" {
|
|
require.NoError(t, opts.Config.Controller.InitNameIfEmpty(ctxTest))
|
|
}
|
|
|
|
if opts.SchedulerRunJobInterval == 0 {
|
|
if t, ok := t.(*testing.T); ok {
|
|
if deadline, ok := t.Deadline(); ok {
|
|
opts.SchedulerRunJobInterval = 1 * time.Minute
|
|
if time.Until(deadline) < opts.SchedulerRunJobInterval {
|
|
half := int64(time.Until(deadline) / 2)
|
|
opts.SchedulerRunJobInterval = time.Duration(half)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
opts.Config.Controller.Scheduler.JobRunIntervalDuration = opts.SchedulerRunJobInterval
|
|
opts.Config.Controller.ApiRateLimiterMaxQuotas = ratelimit.DefaultLimiterMaxQuotas()
|
|
|
|
if opts.EnableEventing || opts.EventerConfig != nil {
|
|
opts.Config.Eventing = opts.EventerConfig
|
|
if opts.Config.Eventing == nil {
|
|
opts.Config.Eventing = &event.EventerConfig{
|
|
AuditEnabled: true,
|
|
ObservationsEnabled: true,
|
|
SysEventsEnabled: true,
|
|
ErrorEventsDisabled: true,
|
|
}
|
|
}
|
|
}
|
|
serverName, err := os.Hostname()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
serverName = fmt.Sprintf("%s/controller", serverName)
|
|
if err := tc.b.SetupEventing(ctxTest, tc.b.Logger, tc.b.StderrLock, serverName, base.WithEventerConfig(opts.Config.Eventing)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if opts.WorkerRPCGracePeriod != 0 {
|
|
opts.Config.Controller.WorkerRPCGracePeriodDuration = opts.WorkerRPCGracePeriod
|
|
}
|
|
if opts.LivenessTimeToStaleDuration != 0 {
|
|
opts.Config.Controller.LivenessTimeToStaleDuration = opts.LivenessTimeToStaleDuration
|
|
}
|
|
|
|
tc.name = opts.Config.Controller.Name
|
|
|
|
if opts.InitialResourcesSuffix != "" {
|
|
suffix := opts.InitialResourcesSuffix
|
|
tc.b.DevPasswordAuthMethodId = "ampw_" + suffix
|
|
tc.b.DevOidcAuthMethodId = "amoidc_" + suffix
|
|
tc.b.DevLdapAuthMethodId = globals.LdapAuthMethodPrefix + "_" + suffix
|
|
tc.b.DevHostCatalogId = "hcst_" + suffix
|
|
tc.b.DevHostId = "hst_" + suffix
|
|
tc.b.DevHostSetId = "hsst_" + suffix
|
|
tc.b.DevOrgId = "o_" + suffix
|
|
tc.b.DevProjectId = "p_" + suffix
|
|
tc.b.DevTargetId = "ttcp_" + suffix
|
|
tc.b.DevUserId = "u_" + suffix
|
|
tc.b.DevUnprivilegedUserId = "u_" + strutil.Reverse(strings.TrimPrefix(tc.b.DevUserId, "u_"))
|
|
} else {
|
|
tc.b.DevUserId = DefaultTestUserId
|
|
}
|
|
tc.b.DevUnprivilegedUserId = "u_" + strutil.Reverse(strings.TrimPrefix(tc.b.DevUserId, "u_"))
|
|
|
|
// Set up KMSes
|
|
if err := tc.b.SetupKMSes(tc.b.Context, nil, opts.Config); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if opts.RootKms != nil {
|
|
tc.b.RootKms = opts.RootKms
|
|
}
|
|
if opts.WorkerAuthKms != nil {
|
|
tc.b.WorkerAuthKms = opts.WorkerAuthKms
|
|
}
|
|
if opts.DownstreamWorkerAuthKms != nil {
|
|
tc.b.DownstreamWorkerAuthKms = opts.DownstreamWorkerAuthKms
|
|
}
|
|
if opts.BsrKms != nil {
|
|
tc.b.BsrKms = opts.BsrKms
|
|
}
|
|
if opts.RecoveryKms != nil {
|
|
tc.b.RecoveryKms = opts.RecoveryKms
|
|
}
|
|
|
|
// Ensure the listeners use random port allocation
|
|
for _, listener := range opts.Config.Listeners {
|
|
listener.RandomPort = true
|
|
}
|
|
if err := tc.b.SetupListeners(nil, opts.Config.SharedConfig, []string{"api", "cluster", "ops"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := opts.Config.SetupControllerPublicClusterAddress(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Set cluster address if we supplied one (overrides one in config)
|
|
if opts.PublicClusterAddr != "" {
|
|
opts.Config.Controller.PublicClusterAddr = opts.PublicClusterAddr
|
|
}
|
|
|
|
if opts.DatabaseUrl != "" {
|
|
tc.b.DatabaseUrl = opts.DatabaseUrl
|
|
if _, err := schema.MigrateStore(ctx, "postgres", tc.b.DatabaseUrl); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := tc.b.OpenAndSetServerDatabase(tc.ctx, "postgres"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !opts.DisableKmsKeyCreation {
|
|
if err := tc.b.CreateGlobalKmsKeys(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !opts.DisableInitialLoginRoleCreation {
|
|
if _, err := tc.b.CreateInitialLoginRole(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tc.b.CreateInitialAuthenticatedUserRole(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !opts.DisableAuthMethodCreation {
|
|
if _, _, err := tc.b.CreateInitialPasswordAuthMethod(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !opts.DisableOidcAuthMethodCreation {
|
|
if err := tc.b.CreateDevOidcAuthMethod(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if !opts.DisableLdapAuthMethodCreation {
|
|
if err := tc.b.CreateDevLdapAuthMethod(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if !opts.DisableScopesCreation {
|
|
if _, _, err := tc.b.CreateInitialScopes(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !opts.DisableHostResourcesCreation {
|
|
if _, _, _, err := tc.b.CreateInitialHostResources(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !opts.DisableTargetCreation {
|
|
if _, err := tc.b.CreateInitialTargetWithHostSources(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if !opts.DisableDatabaseCreation {
|
|
var createOpts []base.Option
|
|
if opts.DisableInitialLoginRoleCreation {
|
|
createOpts = append(createOpts, base.WithSkipDefaultRoleCreation())
|
|
}
|
|
if opts.DisableAuthMethodCreation {
|
|
createOpts = append(createOpts, base.WithSkipAuthMethodCreation())
|
|
}
|
|
if opts.DisableOidcAuthMethodCreation {
|
|
createOpts = append(createOpts, base.WithSkipOidcAuthMethodCreation())
|
|
}
|
|
if opts.DisableOidcAuthMethodCreation {
|
|
createOpts = append(createOpts, base.WithSkipLdapAuthMethodCreation())
|
|
}
|
|
if !opts.DisableDatabaseTemplate {
|
|
createOpts = append(createOpts, base.WithDatabaseTemplate("boundary_template"))
|
|
}
|
|
if err := tc.b.CreateDevDatabase(ctx, createOpts...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if opts.DisableRateLimiting {
|
|
opts.Config.Controller.ApiRateLimitDisable = true
|
|
}
|
|
|
|
return &Config{
|
|
RawConfig: opts.Config,
|
|
Server: tc.b,
|
|
DisableAuthorizationFailures: opts.DisableAuthorizationFailures,
|
|
TestOverrideWorkerAuthCaCertificateLifetime: opts.WorkerAuthCaCertificateLifetime,
|
|
}
|
|
}
|
|
|
|
func (tc *TestController) AddClusterControllerMember(t testing.TB, opts *TestControllerOpts) *TestController {
|
|
const op = "controller.(TestController).AddClusterControllerMember"
|
|
if opts == nil {
|
|
opts = new(TestControllerOpts)
|
|
}
|
|
nextOpts := &TestControllerOpts{
|
|
DatabaseUrl: tc.c.conf.DatabaseUrl,
|
|
DefaultPasswordAuthMethodId: tc.c.conf.DevPasswordAuthMethodId,
|
|
DefaultOidcAuthMethodId: tc.c.conf.DevOidcAuthMethodId,
|
|
DefaultLdapAuthMethodId: tc.c.conf.DevLdapAuthMethodId,
|
|
RootKms: tc.c.conf.RootKms,
|
|
WorkerAuthKms: tc.c.conf.WorkerAuthKms,
|
|
DownstreamWorkerAuthKms: tc.c.conf.DownstreamWorkerAuthKms,
|
|
BsrKms: tc.c.conf.BsrKms,
|
|
RecoveryKms: tc.c.conf.RecoveryKms,
|
|
Name: opts.Name,
|
|
Logger: tc.c.conf.Logger,
|
|
DefaultLoginName: tc.b.DevLoginName,
|
|
DefaultPassword: tc.b.DevPassword,
|
|
DisableKmsKeyCreation: true,
|
|
DisableAuthMethodCreation: true,
|
|
DisableAutoStart: opts.DisableAutoStart,
|
|
PublicClusterAddr: opts.PublicClusterAddr,
|
|
WorkerRPCGracePeriod: opts.WorkerRPCGracePeriod,
|
|
LivenessTimeToStaleDuration: opts.LivenessTimeToStaleDuration,
|
|
WorkerAuthCaCertificateLifetime: tc.c.conf.TestOverrideWorkerAuthCaCertificateLifetime,
|
|
WorkerAuthDebuggingEnabled: tc.c.conf.WorkerAuthDebuggingEnabled,
|
|
}
|
|
if opts.Logger != nil {
|
|
nextOpts.Logger = opts.Logger
|
|
}
|
|
if nextOpts.Name == "" {
|
|
var err error
|
|
nextOpts.Name, err = base62.Random(5)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
event.WriteSysEvent(context.TODO(), op, "controller name generated", "name", nextOpts.Name)
|
|
}
|
|
return NewTestController(t, nextOpts)
|
|
}
|
|
|
|
// WaitForNextWorkerRoutingInfoUpdate waits for the next routing info RPC from a worker to
|
|
// come in. If it does not come in within the default worker RPC grace
|
|
// period, this function returns an error.
|
|
func (tc *TestController) WaitForNextWorkerRoutingInfoUpdate(workerName string) error {
|
|
const op = "controller.(TestController).WaitForNextWorkerRoutingInfoUpdate"
|
|
waitStart := time.Now()
|
|
ctx, cancel := context.WithTimeout(tc.ctx, time.Duration(tc.c.workerRPCGracePeriod.Load()))
|
|
defer cancel()
|
|
event.WriteSysEvent(ctx, op, "waiting for next routing info from worker", "worker", workerName)
|
|
var err error
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
break
|
|
case <-time.After(time.Second):
|
|
// pass
|
|
}
|
|
|
|
var waitCurrent time.Time
|
|
tc.Controller().WorkerRoutingInfoUpdateTimes().Range(func(k, v any) bool {
|
|
if k == nil || v == nil {
|
|
err = fmt.Errorf("nil key or value on entry: key=%#v value=%#v", k, v)
|
|
return false
|
|
}
|
|
|
|
workerUpdateId, ok := k.(string)
|
|
if !ok {
|
|
err = fmt.Errorf("unexpected type %T for key: key=%#v value=%#v", k, k, v)
|
|
return false
|
|
}
|
|
|
|
workerUpdateTime, ok := v.(time.Time)
|
|
if !ok {
|
|
err = fmt.Errorf("unexpected type %T for value: key=%#v value=%#v", k, k, v)
|
|
return false
|
|
}
|
|
|
|
if workerUpdateId == workerName {
|
|
waitCurrent = workerUpdateTime
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
if waitCurrent.After(waitStart) {
|
|
break
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error waiting for next routing info RPC from worker", "worker", workerName))
|
|
return err
|
|
}
|
|
event.WriteSysEvent(ctx, op, "next routing info RPC from worker received successfully", "worker", workerName)
|
|
return nil
|
|
}
|
|
|
|
// startTestGreeterService is intended to facilitate the testing of
|
|
// interceptors. You provide a greeter service that produces appropriate
|
|
// responses to test your interceptor(s). This test function will start up a
|
|
// test greeter service wrapped with the specified interceptors and return an
|
|
// initialized client for the service. The test service will be stopped/cleaned
|
|
// up after the test (or subtests) have completed.
|
|
func startTestGreeterService(t testing.TB, greeter interceptor.GreeterServiceServer, interceptors ...grpc.UnaryServerInterceptor) interceptor.GreeterServiceClient {
|
|
t.Helper()
|
|
require := require.New(t)
|
|
dialCtx := context.Background()
|
|
|
|
buffer := 1024 * 1024
|
|
listener := bufconn.Listen(buffer)
|
|
|
|
s := grpc.NewServer(
|
|
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(interceptors...)),
|
|
)
|
|
interceptor.RegisterGreeterServiceServer(s, greeter)
|
|
// 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 <- s.Serve(listener)
|
|
}()
|
|
t.Cleanup(func() {
|
|
// Will block until we stopped serving
|
|
require.NoError(<-errChan)
|
|
})
|
|
|
|
conn, _ := grpc.DialContext(dialCtx, "", grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
|
|
return listener.Dial()
|
|
}), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
|
|
t.Cleanup(func() {
|
|
listener.Close()
|
|
s.Stop()
|
|
})
|
|
|
|
client := interceptor.NewGreeterServiceClient(conn)
|
|
return client
|
|
}
|