mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-11 09:07:00 +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>
336 lines
9.7 KiB
Go
336 lines
9.7 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package exec
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/hashicorp/consul-template/child"
|
|
ctconfig "github.com/hashicorp/consul-template/config"
|
|
"github.com/hashicorp/consul-template/manager"
|
|
"github.com/hashicorp/go-hclog"
|
|
"golang.org/x/exp/slices"
|
|
|
|
"github.com/hashicorp/vault/command/agent/config"
|
|
"github.com/hashicorp/vault/command/agent/internal/ctmanager"
|
|
"github.com/hashicorp/vault/helper/useragent"
|
|
"github.com/hashicorp/vault/sdk/helper/pointerutil"
|
|
)
|
|
|
|
type childProcessState uint8
|
|
|
|
const (
|
|
childProcessStateNotStarted childProcessState = iota
|
|
childProcessStateRunning
|
|
childProcessStateRestarting
|
|
childProcessStateStopped
|
|
)
|
|
|
|
type ServerConfig struct {
|
|
Logger hclog.Logger
|
|
AgentConfig *config.Config
|
|
|
|
Namespace string
|
|
|
|
// LogLevel is needed to set the internal Consul Template Runner's log level
|
|
// to match the log level of Vault Agent. The internal Runner creates it's own
|
|
// logger and can't be set externally or copied from the Template Server.
|
|
//
|
|
// LogWriter is needed to initialize Consul Template's internal logger to use
|
|
// the same io.Writer that Vault Agent itself is using.
|
|
LogLevel hclog.Level
|
|
LogWriter io.Writer
|
|
}
|
|
|
|
type Server struct {
|
|
// config holds the ServerConfig used to create it. It's passed along in other
|
|
// methods
|
|
config *ServerConfig
|
|
|
|
// runner is the consul-template runner
|
|
runner *manager.Runner
|
|
|
|
// numberOfTemplates is the count of templates determined by consul-template,
|
|
// we keep the value to ensure all templates have been rendered before
|
|
// starting the child process
|
|
// NOTE: each template may have more than one TemplateConfig, so the numbers may not match up
|
|
numberOfTemplates int
|
|
|
|
logger hclog.Logger
|
|
|
|
childProcess *child.Child
|
|
childProcessState childProcessState
|
|
childProcessLock sync.Mutex
|
|
|
|
// exit channel of the child process
|
|
childProcessExitCh chan int
|
|
|
|
// lastRenderedEnvVars is the cached value of all environment variables
|
|
// rendered by the templating engine; it is used for detecting changes
|
|
lastRenderedEnvVars []string
|
|
}
|
|
|
|
type ProcessExitError struct {
|
|
ExitCode int
|
|
}
|
|
|
|
func (e *ProcessExitError) Error() string {
|
|
return fmt.Sprintf("process exited with %d", e.ExitCode)
|
|
}
|
|
|
|
func NewServer(cfg *ServerConfig) *Server {
|
|
server := Server{
|
|
logger: cfg.Logger,
|
|
config: cfg,
|
|
childProcessState: childProcessStateNotStarted,
|
|
childProcessExitCh: make(chan int),
|
|
}
|
|
|
|
return &server
|
|
}
|
|
|
|
func (s *Server) Run(ctx context.Context, incomingVaultToken chan string) error {
|
|
latestToken := new(string)
|
|
s.logger.Info("starting exec server")
|
|
defer func() {
|
|
s.logger.Info("exec server stopped")
|
|
}()
|
|
|
|
if len(s.config.AgentConfig.EnvTemplates) == 0 || s.config.AgentConfig.Exec == nil {
|
|
s.logger.Info("no env templates or exec config, exiting")
|
|
<-ctx.Done()
|
|
return nil
|
|
}
|
|
|
|
managerConfig := ctmanager.ManagerConfig{
|
|
AgentConfig: s.config.AgentConfig,
|
|
Namespace: s.config.Namespace,
|
|
LogLevel: s.config.LogLevel,
|
|
LogWriter: s.config.LogWriter,
|
|
}
|
|
|
|
runnerConfig, err := ctmanager.NewConfig(managerConfig, s.config.AgentConfig.EnvTemplates)
|
|
if err != nil {
|
|
return fmt.Errorf("template server failed to generate runner config: %w", err)
|
|
}
|
|
|
|
// We leave this in "dry" mode, as there are no files to render;
|
|
// we will get the environment variables rendered contents from the incoming events
|
|
s.runner, err = manager.NewRunner(runnerConfig, true)
|
|
if err != nil {
|
|
return fmt.Errorf("template server failed to create: %w", err)
|
|
}
|
|
|
|
// prevent the templates from being rendered to stdout in "dry" mode
|
|
s.runner.SetOutStream(io.Discard)
|
|
|
|
s.numberOfTemplates = len(s.runner.TemplateConfigMapping())
|
|
|
|
// We receive multiple events every staticSecretRenderInterval
|
|
// from <-s.runner.TemplateRenderedCh(), one for each secret. Only the last
|
|
// event in a batch will contain the latest set of all secrets and the
|
|
// corresponding environment variables. This timer will fire after 2 seconds
|
|
// unless an event comes in which resets the timer back to 2 seconds.
|
|
var debounceTimer *time.Timer
|
|
|
|
// capture the errors related to restarting the child process
|
|
restartChildProcessErrCh := make(chan error)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
s.runner.Stop()
|
|
s.childProcessLock.Lock()
|
|
if s.childProcess != nil {
|
|
s.childProcess.Stop()
|
|
}
|
|
s.childProcessState = childProcessStateStopped
|
|
s.childProcessLock.Unlock()
|
|
return nil
|
|
|
|
case token := <-incomingVaultToken:
|
|
if token != *latestToken {
|
|
s.logger.Info("exec server received new token")
|
|
|
|
s.runner.Stop()
|
|
*latestToken = token
|
|
newTokenConfig := ctconfig.Config{
|
|
Vault: &ctconfig.VaultConfig{
|
|
Token: latestToken,
|
|
ClientUserAgent: pointerutil.StringPtr(useragent.AgentTemplatingString()),
|
|
},
|
|
}
|
|
|
|
// got a new auth token, merge it in with the existing config
|
|
runnerConfig = runnerConfig.Merge(&newTokenConfig)
|
|
s.runner, err = manager.NewRunner(runnerConfig, true)
|
|
if err != nil {
|
|
s.logger.Error("template server failed with new Vault token", "error", err)
|
|
continue
|
|
}
|
|
|
|
// prevent the templates from being rendered to stdout in "dry" mode
|
|
s.runner.SetOutStream(io.Discard)
|
|
|
|
go s.runner.Start()
|
|
}
|
|
|
|
case err := <-s.runner.ErrCh:
|
|
s.logger.Error("template server error", "error", err.Error())
|
|
s.runner.StopImmediately()
|
|
|
|
// Return after stopping the runner if exit on retry failure was specified
|
|
if s.config.AgentConfig.TemplateConfig != nil && s.config.AgentConfig.TemplateConfig.ExitOnRetryFailure {
|
|
return fmt.Errorf("template server: %w", err)
|
|
}
|
|
|
|
s.runner, err = manager.NewRunner(runnerConfig, true)
|
|
if err != nil {
|
|
return fmt.Errorf("template server failed to create: %w", err)
|
|
}
|
|
go s.runner.Start()
|
|
|
|
case <-s.runner.TemplateRenderedCh():
|
|
// A template has been rendered, figure out what to do
|
|
s.logger.Trace("template rendered")
|
|
events := s.runner.RenderEvents()
|
|
|
|
// This checks if we've finished rendering the initial set of templates,
|
|
// for every consecutive re-render len(events) should equal s.numberOfTemplates
|
|
if len(events) < s.numberOfTemplates {
|
|
// Not all templates have been rendered yet
|
|
continue
|
|
}
|
|
|
|
// assume the renders are finished, until we find otherwise
|
|
doneRendering := true
|
|
var renderedEnvVars []string
|
|
for _, event := range events {
|
|
// This template hasn't been rendered
|
|
if event.LastWouldRender.IsZero() {
|
|
doneRendering = false
|
|
break
|
|
} else {
|
|
for _, tcfg := range event.TemplateConfigs {
|
|
envVar := fmt.Sprintf("%s=%s", *tcfg.MapToEnvironmentVariable, event.Contents)
|
|
renderedEnvVars = append(renderedEnvVars, envVar)
|
|
}
|
|
}
|
|
}
|
|
if !doneRendering {
|
|
continue
|
|
}
|
|
|
|
// sort the environment variables for a deterministic output and easy comparison
|
|
sort.Strings(renderedEnvVars)
|
|
|
|
s.logger.Trace("done rendering templates")
|
|
|
|
// don't restart the process unless a change is detected
|
|
if slices.Equal(s.lastRenderedEnvVars, renderedEnvVars) {
|
|
continue
|
|
}
|
|
|
|
s.lastRenderedEnvVars = renderedEnvVars
|
|
|
|
s.logger.Debug("detected a change in the environment variables: restarting the child process")
|
|
|
|
// if a timer exists, stop it
|
|
if debounceTimer != nil {
|
|
debounceTimer.Stop()
|
|
}
|
|
debounceTimer = time.AfterFunc(2*time.Second, func() {
|
|
if err := s.restartChildProcess(renderedEnvVars); err != nil {
|
|
restartChildProcessErrCh <- fmt.Errorf("unable to restart the child process: %w", err)
|
|
}
|
|
})
|
|
|
|
case err := <-restartChildProcessErrCh:
|
|
// catch the error from restarting
|
|
return err
|
|
|
|
case exitCode := <-s.childProcessExitCh:
|
|
// process exited on its own
|
|
return &ProcessExitError{ExitCode: exitCode}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) restartChildProcess(newEnvVars []string) error {
|
|
s.childProcessLock.Lock()
|
|
defer s.childProcessLock.Unlock()
|
|
|
|
switch s.config.AgentConfig.Exec.RestartOnSecretChanges {
|
|
case "always":
|
|
if s.childProcessState == childProcessStateRunning {
|
|
// process is running, need to kill it first
|
|
s.logger.Info("stopping process", "process_id", s.childProcess.Pid())
|
|
s.childProcessState = childProcessStateRestarting
|
|
s.childProcess.Stop()
|
|
}
|
|
case "never":
|
|
if s.childProcessState == childProcessStateRunning {
|
|
s.logger.Info("detected update, but not restarting process", "process_id", s.childProcess.Pid())
|
|
return nil
|
|
}
|
|
default:
|
|
return fmt.Errorf("invalid value for restart-on-secret-changes: %q", s.config.AgentConfig.Exec.RestartOnSecretChanges)
|
|
}
|
|
|
|
args, subshell, err := child.CommandPrep(s.config.AgentConfig.Exec.Command)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse command: %w", err)
|
|
}
|
|
|
|
childInput := &child.NewInput{
|
|
Stdin: os.Stdin,
|
|
Stdout: os.Stdout,
|
|
Stderr: os.Stderr,
|
|
Command: args[0],
|
|
Args: args[1:],
|
|
Timeout: 0, // let it run forever
|
|
Env: append(os.Environ(), newEnvVars...),
|
|
ReloadSignal: nil, // can't reload w/ new env vars
|
|
KillSignal: s.config.AgentConfig.Exec.RestartStopSignal,
|
|
KillTimeout: 30 * time.Second,
|
|
Splay: 0,
|
|
Setpgid: subshell,
|
|
Logger: s.logger.StandardLogger(nil),
|
|
}
|
|
|
|
proc, err := child.New(childInput)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.childProcess = proc
|
|
|
|
if err := s.childProcess.Start(); err != nil {
|
|
return fmt.Errorf("error starting the child process: %w", err)
|
|
}
|
|
|
|
s.childProcessState = childProcessStateRunning
|
|
|
|
// Listen if the child process exits and bubble it up to the main loop.
|
|
//
|
|
// NOTE: this must be invoked after child.Start() to avoid a potential
|
|
// race condition with ExitCh not being initialized.
|
|
go func() {
|
|
select {
|
|
case exitCode, ok := <-proc.ExitCh():
|
|
// ignore ExitCh channel closures caused by our restarts
|
|
if ok {
|
|
s.childProcessExitCh <- exitCode
|
|
}
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|