Files
threshold/internal/cmd/commands/server/worker_shutdown_reload_test.go
T
Chris Marchesi a15961e461 Close connections on worker on shutdown, add force shutdown (#1354)
This adds functionality to ensure that connections are closed on a
worker when the server is shut down or reloaded.

Connections are closed, and then the shutdown process waits for the next
report to the controller to go through, waiting the grace period that
was added in #1330. The rest of shutdown is then proceeded with.
Listener shutdown has been moved to the start of the process to ensure
that new connections can't be made while we are shutting down.

Additionally, functionality has been added to force shut down servers
after a second interrupt has been received after the first one. This
takes the form of code in both the server and dev mode code paths that
just performs an os.Exit if another shutdown is received during the
normal shutdown process.

Additionally, the commit also includes some refactoring to tests so that
we could use some of the new E2E testing code to test session
connections during shutdown and reload.

Additionally, this fixes a panic in worker startup when there are no
controllers defined. Looks like the shutdown call was attempting to call
into the controller, not the worker.

Finally, this also removes the extra shutdown within the outer worker
startup.

Shutdown is actually handled within the inner StartWorker(), attempting
shutdown on errors was actually leading to potential use of an
uninitialized worker, which is leading to panics in testing.
2021-07-28 14:35:58 -07:00

217 lines
5.4 KiB
Go

// +build !hsm
package server
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/authmethods"
"github.com/hashicorp/boundary/api/authtokens"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/boundary/internal/cmd/config"
"github.com/hashicorp/boundary/internal/session"
"github.com/hashicorp/boundary/internal/tests/helper"
"github.com/hashicorp/go-hclog"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
)
const shutdownReloadApiAddr = "http://127.0.0.1:9203"
const shutdownReloadControllerConfig = `
disable_mlock = true
controller {
name = "dev-controller"
description = "A default controller created in dev mode"
database {
url = "%s"
}
}
kms "aead" {
purpose = "root"
aead_type = "aes-gcm"
key = "%s"
key_id = "global_root"
}
kms "aead" {
purpose = "worker-auth"
aead_type = "aes-gcm"
key = "%s"
key_id = "global_worker-auth"
}
listener "tcp" {
purpose = "api"
address = "127.0.0.1:9203"
tls_disable = true
cors_enabled = true
cors_allowed_origins = ["*"]
}
listener "tcp" {
purpose = "cluster"
address = "127.0.0.1:9204"
}
`
const shutdownReloadWorkerConfig = `
disable_mlock = true
worker {
name = "dev-worker"
description = "A default worker created in dev mode"
controllers = ["127.0.0.1:9204"]
}
kms "aead" {
purpose = "worker-auth"
aead_type = "aes-gcm"
key = "%s"
key_id = "global_worker-auth"
}
listener "tcp" {
purpose = "proxy"
address = "127.0.0.1:9205"
}
`
func TestServer_ShutdownWorker(t *testing.T) {
logger := hclog.New(&hclog.LoggerOptions{
Name: t.Name(),
Level: hclog.Trace,
})
require := require.New(t)
controllerKey, workerAuthKey, _ := config.DevKeyGeneration()
// Start the controller
controllerCmd := testServerCommand(t, testServerCommandOpts{
CreateDevDatabase: true,
ControllerKey: controllerKey,
UseDevAuthMethod: true,
UseDevTarget: true,
})
controllerDoneCh := make(chan struct{})
defer func() {
if controllerCmd.DevDatabaseCleanupFunc != nil {
require.NoError(controllerCmd.DevDatabaseCleanupFunc())
}
}()
controllerCmd.presetConfig = atomic.NewString(fmt.Sprintf(shutdownReloadControllerConfig, controllerCmd.DatabaseUrl, controllerKey, workerAuthKey))
go func() {
defer close(controllerDoneCh)
if code := controllerCmd.Run(nil); code != 0 {
output := controllerCmd.UI.(*cli.MockUi).ErrorWriter.String() + controllerCmd.UI.(*cli.MockUi).OutputWriter.String()
require.FailNow(output, "command exited with non-zero error code")
}
}()
waitCh(t, controllerCmd.startedCh)
// Start the worker
workerCmd := testServerCommand(t, testServerCommandOpts{})
workerDoneCh := make(chan struct{})
workerCmd.presetConfig = atomic.NewString(fmt.Sprintf(shutdownReloadWorkerConfig, workerAuthKey))
go func() {
defer close(workerDoneCh)
if code := workerCmd.Run(nil); code != 0 {
output := workerCmd.UI.(*cli.MockUi).ErrorWriter.String() + workerCmd.UI.(*cli.MockUi).OutputWriter.String()
require.FailNow(output, "command exited with non-zero error code")
}
}()
waitCh(t, workerCmd.startedCh)
// Set up the target
ctx := context.Background()
client := buildClient(t, shutdownReloadApiAddr)
setAuthToken(ctx, t, client)
tcl := targets.NewClient(client)
tgt, err := tcl.Read(ctx, "ttcp_1234567890")
require.NoError(err)
require.NotNil(tgt)
// Create test server, update default port on target
ts := helper.NewTestTcpServer(t, logger)
require.NotNil(ts)
defer ts.Close()
tgt, err = tcl.Update(ctx, tgt.Item.Id, tgt.Item.Version, targets.WithTcpTargetDefaultPort(ts.Port()))
require.NoError(err)
require.NotNil(tgt)
// Authorize and connect
sess := helper.NewTestSession(ctx, t, logger, tcl, "ttcp_1234567890")
sConn := sess.Connect(ctx, t)
// Run initial send/receive test, make sure things are working
logger.Debug("running initial send/recv test")
sConn.TestSendRecvAll(t)
// Now, shut the worker down.
close(workerCmd.ShutdownCh)
waitCh(t, workerDoneCh)
// Connection should fail, and the session should be closed on the DB.
sConn.TestSendRecvFail(t)
sess.ExpectConnectionStateOnController(ctx, t, controllerCmd.controller.SessionRepoFn, session.StatusClosed)
// We're done! Shutdown the controller, and that's it.
close(controllerCmd.ShutdownCh)
waitCh(t, controllerDoneCh)
}
// largely copied from controller/testing.go
func buildClient(t *testing.T, addr string) *api.Client {
require := require.New(t)
client, err := api.NewClient(nil)
require.NoError(err)
err = client.SetAddr(addr)
require.NoError(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("")
return client
}
func setAuthToken(ctx context.Context, t *testing.T, client *api.Client) {
require := require.New(t)
result, err := authmethods.NewClient(client).Authenticate(
ctx,
"ampw_1234567890",
"login",
map[string]interface{}{
"login_name": "admin",
"password": "passpass",
},
)
require.NoError(err)
token := new(authtokens.AuthToken)
err = json.Unmarshal(result.GetRawAttributes(), token)
require.NoError(err)
client.SetToken(token.Token)
}
func waitCh(t *testing.T, c chan struct{}) {
require := require.New(t)
select {
case <-c:
case <-time.After(15 * time.Second):
require.FailNow("timeout")
}
}