mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-11 17:17:01 +02:00
* 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. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
383 lines
12 KiB
Go
383 lines
12 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package exec
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
ctconfig "github.com/hashicorp/consul-template/config"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-retryablehttp"
|
|
|
|
"github.com/hashicorp/vault/command/agent/config"
|
|
"github.com/hashicorp/vault/sdk/helper/logging"
|
|
"github.com/hashicorp/vault/sdk/helper/pointerutil"
|
|
)
|
|
|
|
func fakeVaultServer(t *testing.T) *httptest.Server {
|
|
t.Helper()
|
|
|
|
firstRequest := true
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1/kv/my-app/creds", func(w http.ResponseWriter, r *http.Request) {
|
|
// change the password on the second request to re-render the template
|
|
var password string
|
|
|
|
if firstRequest {
|
|
password = "s3cr3t"
|
|
} else {
|
|
password = "s3cr3t-two"
|
|
}
|
|
|
|
firstRequest = false
|
|
|
|
fmt.Fprintf(w, `{
|
|
"request_id": "8af096e9-518c-7351-eff5-5ba20554b21f",
|
|
"lease_id": "",
|
|
"renewable": false,
|
|
"lease_duration": 0,
|
|
"data": {
|
|
"data": {
|
|
"password": "%s",
|
|
"user": "app-user"
|
|
},
|
|
"metadata": {
|
|
"created_time": "2019-10-07T22:18:44.233247Z",
|
|
"deletion_time": "",
|
|
"destroyed": false,
|
|
"version": 3
|
|
}
|
|
},
|
|
"wrap_info": null,
|
|
"warnings": null,
|
|
"auth": null
|
|
}`,
|
|
password,
|
|
)
|
|
})
|
|
|
|
return httptest.NewServer(mux)
|
|
}
|
|
|
|
// TestExecServer_Run tests various scenarios of using vault agent as a process
|
|
// supervisor. At its core is a sample application referred to as 'test app',
|
|
// compiled from ./test-app/main.go. Each test case verifies that the test app
|
|
// is started and/or stopped correctly by exec.Server.Run(). There are 3
|
|
// high-level scenarios we want to test for:
|
|
//
|
|
// 1. test app is started and is injected with environment variables
|
|
// 2. test app exits early (either with zero or non-zero extit code)
|
|
// 3. test app needs to be stopped (and restarted) by exec.Server
|
|
func TestExecServer_Run(t *testing.T) {
|
|
// we must build a test-app binary since 'go run' does not propagate signals correctly
|
|
goBinary, err := exec.LookPath("go")
|
|
if err != nil {
|
|
t.Fatalf("could not find go binary on path: %s", err)
|
|
}
|
|
|
|
testAppBinary := filepath.Join(os.TempDir(), "test-app")
|
|
|
|
if err := exec.Command(goBinary, "build", "-o", testAppBinary, "./test-app").Run(); err != nil {
|
|
t.Fatalf("could not build the test application: %s", err)
|
|
}
|
|
defer func() {
|
|
if err := os.Remove(testAppBinary); err != nil {
|
|
t.Fatalf("could not remove %q test application: %s", testAppBinary, err)
|
|
}
|
|
}()
|
|
|
|
testCases := map[string]struct {
|
|
// skip this test case
|
|
skip bool
|
|
skipReason string
|
|
|
|
// inputs to the exec server
|
|
envTemplates []*ctconfig.TemplateConfig
|
|
staticSecretRenderInterval time.Duration
|
|
|
|
// test app parameters
|
|
testAppArgs []string
|
|
testAppStopSignal os.Signal
|
|
testAppPort int
|
|
|
|
// simulate a shutdown of agent, which, in turn stops the test app
|
|
simulateShutdown bool
|
|
simulateShutdownWaitDuration time.Duration
|
|
|
|
// expected results
|
|
expected map[string]string
|
|
expectedTestDuration time.Duration
|
|
expectedError error
|
|
}{
|
|
"ensure_environment_variables_are_injected": {
|
|
skip: true,
|
|
envTemplates: []*ctconfig.TemplateConfig{{
|
|
Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`),
|
|
MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"),
|
|
}, {
|
|
Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.password }}{{ end }}`),
|
|
MapToEnvironmentVariable: pointerutil.StringPtr("MY_PASSWORD"),
|
|
}},
|
|
testAppArgs: []string{"--stop-after", "10s"},
|
|
testAppStopSignal: syscall.SIGTERM,
|
|
testAppPort: 34001,
|
|
expected: map[string]string{
|
|
"MY_USER": "app-user",
|
|
"MY_PASSWORD": "s3cr3t",
|
|
},
|
|
expectedTestDuration: 15 * time.Second,
|
|
expectedError: nil,
|
|
},
|
|
|
|
"password_changes_test_app_should_restart": {
|
|
envTemplates: []*ctconfig.TemplateConfig{{
|
|
Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`),
|
|
MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"),
|
|
}, {
|
|
Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.password }}{{ end }}`),
|
|
MapToEnvironmentVariable: pointerutil.StringPtr("MY_PASSWORD"),
|
|
}},
|
|
staticSecretRenderInterval: 5 * time.Second,
|
|
testAppArgs: []string{"--stop-after", "15s", "--sleep-after-stop-signal", "0s"},
|
|
testAppStopSignal: syscall.SIGTERM,
|
|
testAppPort: 34002,
|
|
expected: map[string]string{
|
|
"MY_USER": "app-user",
|
|
"MY_PASSWORD": "s3cr3t-two",
|
|
},
|
|
expectedTestDuration: 15 * time.Second,
|
|
expectedError: nil,
|
|
},
|
|
|
|
"test_app_exits_early": {
|
|
skip: true,
|
|
envTemplates: []*ctconfig.TemplateConfig{{
|
|
Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`),
|
|
MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"),
|
|
}},
|
|
testAppArgs: []string{"--stop-after", "1s"},
|
|
testAppStopSignal: syscall.SIGTERM,
|
|
testAppPort: 34003,
|
|
expectedTestDuration: 15 * time.Second,
|
|
expectedError: &ProcessExitError{0},
|
|
},
|
|
|
|
"test_app_exits_early_non_zero": {
|
|
skip: true,
|
|
envTemplates: []*ctconfig.TemplateConfig{{
|
|
Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`),
|
|
MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"),
|
|
}},
|
|
testAppArgs: []string{"--stop-after", "1s", "--exit-code", "5"},
|
|
testAppStopSignal: syscall.SIGTERM,
|
|
testAppPort: 34004,
|
|
expectedTestDuration: 15 * time.Second,
|
|
expectedError: &ProcessExitError{5},
|
|
},
|
|
|
|
"send_sigterm_expect_test_app_exit": {
|
|
skip: true,
|
|
envTemplates: []*ctconfig.TemplateConfig{{
|
|
Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`),
|
|
MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"),
|
|
}},
|
|
testAppArgs: []string{"--stop-after", "30s", "--sleep-after-stop-signal", "1s"},
|
|
testAppStopSignal: syscall.SIGTERM,
|
|
testAppPort: 34005,
|
|
simulateShutdown: true,
|
|
simulateShutdownWaitDuration: 3 * time.Second,
|
|
expectedTestDuration: 15 * time.Second,
|
|
expectedError: nil,
|
|
},
|
|
|
|
"send_sigusr1_expect_test_app_exit": {
|
|
skip: true,
|
|
envTemplates: []*ctconfig.TemplateConfig{{
|
|
Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`),
|
|
MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"),
|
|
}},
|
|
testAppArgs: []string{"--stop-after", "30s", "--sleep-after-stop-signal", "1s", "--use-sigusr1"},
|
|
testAppStopSignal: syscall.SIGUSR1,
|
|
testAppPort: 34006,
|
|
simulateShutdown: true,
|
|
simulateShutdownWaitDuration: 3 * time.Second,
|
|
expectedTestDuration: 15 * time.Second,
|
|
expectedError: nil,
|
|
},
|
|
|
|
"test_app_ignores_stop_signal": {
|
|
skip: true,
|
|
skipReason: "This test currently fails with 'go test -race' (see hashicorp/consul-template/issues/1753).",
|
|
envTemplates: []*ctconfig.TemplateConfig{{
|
|
Contents: pointerutil.StringPtr(`{{ with secret "kv/my-app/creds" }}{{ .Data.data.user }}{{ end }}`),
|
|
MapToEnvironmentVariable: pointerutil.StringPtr("MY_USER"),
|
|
}},
|
|
testAppArgs: []string{"--stop-after", "60s", "--sleep-after-stop-signal", "60s"},
|
|
testAppStopSignal: syscall.SIGTERM,
|
|
testAppPort: 34007,
|
|
simulateShutdown: true,
|
|
simulateShutdownWaitDuration: 32 * time.Second, // the test app should be stopped immediately after 30s
|
|
expectedTestDuration: 45 * time.Second,
|
|
expectedError: nil,
|
|
},
|
|
}
|
|
|
|
for name, testCase := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
if testCase.skip {
|
|
t.Skip(testCase.skipReason)
|
|
}
|
|
|
|
t.Logf("test case %s: begin", name)
|
|
defer t.Logf("test case %s: end", name)
|
|
|
|
fakeVault := fakeVaultServer(t)
|
|
defer fakeVault.Close()
|
|
|
|
ctx, cancelContextFunc := context.WithTimeout(context.Background(), testCase.expectedTestDuration)
|
|
defer cancelContextFunc()
|
|
|
|
testAppCommand := []string{
|
|
testAppBinary,
|
|
"--port",
|
|
strconv.Itoa(testCase.testAppPort),
|
|
}
|
|
|
|
execServer := NewServer(&ServerConfig{
|
|
Logger: logging.NewVaultLogger(hclog.Trace),
|
|
AgentConfig: &config.Config{
|
|
Vault: &config.Vault{
|
|
Address: fakeVault.URL,
|
|
Retry: &config.Retry{
|
|
NumRetries: 3,
|
|
},
|
|
},
|
|
Exec: &config.ExecConfig{
|
|
RestartOnSecretChanges: "always",
|
|
Command: append(testAppCommand, testCase.testAppArgs...),
|
|
RestartStopSignal: testCase.testAppStopSignal,
|
|
},
|
|
EnvTemplates: testCase.envTemplates,
|
|
TemplateConfig: &config.TemplateConfig{
|
|
ExitOnRetryFailure: true,
|
|
StaticSecretRenderInt: testCase.staticSecretRenderInterval,
|
|
},
|
|
},
|
|
LogLevel: hclog.Trace,
|
|
LogWriter: hclog.DefaultOutput,
|
|
})
|
|
|
|
// start the exec server
|
|
var (
|
|
execServerErrCh = make(chan error)
|
|
execServerTokenCh = make(chan string, 1)
|
|
)
|
|
go func() {
|
|
execServerErrCh <- execServer.Run(ctx, execServerTokenCh)
|
|
}()
|
|
|
|
// send a dummy token to kick off the server
|
|
execServerTokenCh <- "my-token"
|
|
|
|
// ensure the test app is running after 3 seconds
|
|
var (
|
|
testAppAddr = fmt.Sprintf("http://localhost:%d", testCase.testAppPort)
|
|
testAppStartedCh = make(chan error)
|
|
)
|
|
if testCase.expectedError == nil {
|
|
time.AfterFunc(500*time.Millisecond, func() {
|
|
_, err := retryablehttp.Head(testAppAddr)
|
|
testAppStartedCh <- err
|
|
})
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal("timeout reached before templates were rendered")
|
|
|
|
case err := <-execServerErrCh:
|
|
if testCase.expectedError == nil && err != nil {
|
|
t.Fatalf("exec server did not expect an error, got: %v", err)
|
|
}
|
|
|
|
if errors.Is(err, testCase.expectedError) {
|
|
t.Fatalf("exec server expected error %v; got %v", testCase.expectedError, err)
|
|
}
|
|
|
|
t.Log("exec server exited without an error")
|
|
|
|
return
|
|
|
|
case err := <-testAppStartedCh:
|
|
if testCase.expectedError == nil && err != nil {
|
|
t.Fatalf("test app could not be started")
|
|
}
|
|
|
|
t.Log("test app started successfully")
|
|
}
|
|
|
|
// expect the test app to restart after staticSecretRenderInterval + debounce timer due to a password change
|
|
if testCase.staticSecretRenderInterval != 0 {
|
|
t.Logf("sleeping for %v to wait for application restart", testCase.staticSecretRenderInterval+5*time.Second)
|
|
time.Sleep(testCase.staticSecretRenderInterval + 5*time.Second)
|
|
}
|
|
|
|
// simulate a shutdown of agent, which, in turn stops the test app
|
|
if testCase.simulateShutdown {
|
|
cancelContextFunc()
|
|
|
|
time.Sleep(testCase.simulateShutdownWaitDuration)
|
|
|
|
// check if the test app is still alive
|
|
if _, err := http.Head(testAppAddr); err == nil {
|
|
t.Fatalf("the test app is still alive %v after a simulated shutdown!", testCase.simulateShutdownWaitDuration)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// verify the environment variables
|
|
t.Logf("verifying test-app's environment variables")
|
|
|
|
resp, err := retryablehttp.Get(testAppAddr)
|
|
if err != nil {
|
|
t.Fatalf("error making request to the test app: %s", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
decoder := json.NewDecoder(resp.Body)
|
|
var response struct {
|
|
EnvironmentVariables map[string]string `json:"environment_variables"`
|
|
ProcessID int `json:"process_id"`
|
|
}
|
|
if err := decoder.Decode(&response); err != nil {
|
|
t.Fatalf("unable to parse response from test app: %s", err)
|
|
}
|
|
|
|
for key, expectedValue := range testCase.expected {
|
|
actualValue, ok := response.EnvironmentVariables[key]
|
|
if !ok {
|
|
t.Fatalf("expected the test app to return %q environment variable", key)
|
|
}
|
|
if expectedValue != actualValue {
|
|
t.Fatalf("expected environment variable %s to have a value of %q but it has a value of %q", key, expectedValue, actualValue)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|