vault/command/agent/exec/exec.go
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* 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>
2023-08-10 18:14:03 -07:00

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
}