Configuration - Control over command timeouts

This commit is contained in:
Emmanuel BENOîT 2021-12-05 18:12:13 +01:00
parent dad5a17d36
commit 44eb5c5356
3 changed files with 61 additions and 14 deletions

View file

@ -61,9 +61,12 @@ type (
// Handlers. Each handler has a name and contains a list of commands. // Handlers. Each handler has a name and contains a list of commands.
tHandlers map[string][]string tHandlers map[string][]string
// Handler timeouts.
tHandlerTimeouts map[string]int
// Certificate file updates configuration. // Certificate file updates configuration.
tCertFileUpdateConfig struct { tCertFileUpdateConfig struct {
CmdTimeout *int `yaml:"command_timeout"`
PreCommands []string `yaml:"pre_commands"` PreCommands []string `yaml:"pre_commands"`
Handlers []string `yaml:"handlers"` Handlers []string `yaml:"handlers"`
PostCommands []string `yaml:"post_commands"` PostCommands []string `yaml:"post_commands"`
@ -86,9 +89,11 @@ type (
// Main configuration. // Main configuration.
tConfiguration struct { tConfiguration struct {
CmdTimeout int `yaml:"command_timeout"`
Socket *tSocketConfig `yaml:"socket"` Socket *tSocketConfig `yaml:"socket"`
LdapConfig tLdapConfig `yaml:"ldap"` LdapConfig tLdapConfig `yaml:"ldap"`
Handlers tHandlers `yaml:"handlers"` Handlers tHandlers `yaml:"handlers"`
HandlerTimeouts tHandlerTimeouts `yaml:"handler_timeouts"`
Certificates []tCertificateFileConfig `yaml:"certificates"` Certificates []tCertificateFileConfig `yaml:"certificates"`
} }
) )
@ -235,7 +240,7 @@ func checkFileList(files []string) error {
return nil return nil
} }
// Validate the list of handles // Validate the list of handlers and the timeout.
func (c *tCertFileUpdateConfig) Validate(handlers *tHandlers) error { func (c *tCertFileUpdateConfig) Validate(handlers *tHandlers) error {
set := make(map[string]bool) set := make(map[string]bool)
for _, handler := range c.Handlers { for _, handler := range c.Handlers {
@ -247,6 +252,9 @@ func (c *tCertFileUpdateConfig) Validate(handlers *tHandlers) error {
} }
set[handler] = true set[handler] = true
} }
if c.CmdTimeout != nil && *c.CmdTimeout <= 0 {
return fmt.Errorf("Command timeout must be >0.")
}
return nil return nil
} }
@ -289,6 +297,9 @@ func (c *tCertificateFileConfig) Validate(handlers *tHandlers) error {
// Validate the configuration // Validate the configuration
func (c *tConfiguration) Validate() error { func (c *tConfiguration) Validate() error {
if c.CmdTimeout <= 0 {
return fmt.Errorf("Default command timeout must be >0.")
}
if c.Socket != nil { if c.Socket != nil {
err := c.Socket.Validate() err := c.Socket.Validate()
if err != nil { if err != nil {
@ -299,6 +310,14 @@ func (c *tConfiguration) Validate() error {
if err != nil { if err != nil {
return err return err
} }
for hdl, timeout := range c.HandlerTimeouts {
if _, exists := c.Handlers[hdl]; !exists {
return fmt.Errorf("Can't set timeout for unknown handler %s", hdl)
}
if timeout <= 0 {
return fmt.Errorf("Command timeout for handler %s must be >0.", hdl)
}
}
for idx, cfc := range c.Certificates { for idx, cfc := range c.Certificates {
if cfc.Path == "" { if cfc.Path == "" {
return fmt.Errorf("Certificate file entry #%d has no path.", idx+1) return fmt.Errorf("Certificate file entry #%d has no path.", idx+1)
@ -314,6 +333,7 @@ func (c *tConfiguration) Validate() error {
// Create a configuration data structure containing default values. // Create a configuration data structure containing default values.
func defaultConfiguration() tConfiguration { func defaultConfiguration() tConfiguration {
cfg := tConfiguration{} cfg := tConfiguration{}
cfg.CmdTimeout = 5
cfg.LdapConfig.Defaults.TLS = "no" cfg.LdapConfig.Defaults.TLS = "no"
cfg.LdapConfig.Structure.CAChaining = "seeAlso" cfg.LdapConfig.Structure.CAChaining = "seeAlso"
return cfg return cfg

View file

@ -1,6 +1,9 @@
# fetchcert configuration example / documentation # fetchcert configuration example / documentation
# =============================================== # ===============================================
# Default command execution timeout (seconds). 5 seconds is the default.
command_timeout: 5
# The UNIX socket the main program listens on. May be omitted if the program # The UNIX socket the main program listens on. May be omitted if the program
# is intended to run in standalone mode only. # is intended to run in standalone mode only.
socket: socket:
@ -63,6 +66,11 @@ handlers:
- /usr/sbin/apache2ctl configtest - /usr/sbin/apache2ctl configtest
- /usr/sbin/apache2ctl graceful - /usr/sbin/apache2ctl graceful
# Handler command timeouts. If this section is missing, or if no entry is
# present for a handler, the default command timeout will be used.
handler_timeouts:
apache: 1
# Certificates that must be updated # Certificates that must be updated
certificates: certificates:
@ -101,6 +109,10 @@ certificates:
- /some/other/file.pem - /some/other/file.pem
# Define what must be done after an update. # Define what must be done after an update.
after_update: after_update:
# Command execution timeout for pre- and post-commands. If this entry is
# missing, the default from command_timeout above will be used. This does
# not affect handlers.
command_timeout: 1
# Commands to execute before handlers are run. The order of the commands # Commands to execute before handlers are run. The order of the commands
# is respected. If a command fails to run, execution stops. # is respected. If a command fails to run, execution stops.
pre_commands: [] pre_commands: []

View file

@ -122,7 +122,11 @@ func (u *tUpdate) runPreCommands() {
l := log.WithField("file", u.config.Certificates[i].Path) l := log.WithField("file", u.config.Certificates[i].Path)
l.Info("Running pre-commands") l.Info("Running pre-commands")
err := u.runCommands(commands, l) timeout := u.config.CmdTimeout
if u.config.Certificates[i].AfterUpdate.CmdTimeout != nil {
timeout = *u.config.Certificates[i].AfterUpdate.CmdTimeout
}
err := u.runCommands(timeout, commands, l)
if err == nil { if err == nil {
continue continue
} }
@ -159,7 +163,11 @@ func (u *tUpdate) runHandlers(handlers []string) map[string]bool {
for _, handler := range handlers { for _, handler := range handlers {
l := log.WithField("handler", handler) l := log.WithField("handler", handler)
l.Info("Running handler") l.Info("Running handler")
err := u.runCommands(u.config.Handlers[handler], l) timeout := u.config.CmdTimeout
if ht, exists := u.config.HandlerTimeouts[handler]; exists {
timeout = ht
}
err := u.runCommands(timeout, u.config.Handlers[handler], l)
if err == nil { if err == nil {
continue continue
} }
@ -204,7 +212,11 @@ func (u *tUpdate) runPostCommands() {
l := log.WithField("file", u.config.Certificates[i].Path) l := log.WithField("file", u.config.Certificates[i].Path)
l.Info("Running post-commands") l.Info("Running post-commands")
err := u.runCommands(commands, l) timeout := u.config.CmdTimeout
if u.config.Certificates[i].AfterUpdate.CmdTimeout != nil {
timeout = *u.config.Certificates[i].AfterUpdate.CmdTimeout
}
err := u.runCommands(timeout, commands, l)
if err == nil { if err == nil {
continue continue
} }
@ -215,10 +227,10 @@ func (u *tUpdate) runPostCommands() {
} }
} }
// Run a list of commands // Run a list of commands.
func (u *tUpdate) runCommands(commands []string, log *logrus.Entry) error { func (u *tUpdate) runCommands(timeout int, commands []string, log *logrus.Entry) error {
for i := range commands { for i := range commands {
err := u.runCommand(commands[i], log) err := u.runCommand(timeout, commands[i], log)
if err != nil { if err != nil {
return fmt.Errorf( return fmt.Errorf(
"Failed while executing command '%s': %w", "Failed while executing command '%s': %w",
@ -230,11 +242,14 @@ func (u *tUpdate) runCommands(commands []string, log *logrus.Entry) error {
} }
// Run a command through the `sh` shell. // Run a command through the `sh` shell.
func (b *tUpdate) runCommand(command string, log *logrus.Entry) error { func (b *tUpdate) runCommand(timeout int, command string, log *logrus.Entry) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel() defer cancel()
log = log.WithField("command", command) log = log.WithFields(logrus.Fields{
"command": command,
"timeout": timeout,
})
log.Debug("Executing command") log.Debug("Executing command")
cmd := exec.CommandContext(ctx, "sh", "-c", command) cmd := exec.CommandContext(ctx, "sh", "-c", command)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()