From 44eb5c53562d68bd6577b7cb73dac9f9990e640b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Beno=C3=AEt?= Date: Sun, 5 Dec 2021 18:12:13 +0100 Subject: [PATCH] Configuration - Control over command timeouts --- config.go | 30 +++++++++++++++++++++++++----- fetch-certificates.yml.example | 12 ++++++++++++ update.go | 33 ++++++++++++++++++++++++--------- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/config.go b/config.go index a114c5f..5737359 100644 --- a/config.go +++ b/config.go @@ -61,9 +61,12 @@ type ( // Handlers. Each handler has a name and contains a list of commands. tHandlers map[string][]string + // Handler timeouts. + tHandlerTimeouts map[string]int // Certificate file updates configuration. tCertFileUpdateConfig struct { + CmdTimeout *int `yaml:"command_timeout"` PreCommands []string `yaml:"pre_commands"` Handlers []string `yaml:"handlers"` PostCommands []string `yaml:"post_commands"` @@ -86,10 +89,12 @@ type ( // Main configuration. tConfiguration struct { - Socket *tSocketConfig `yaml:"socket"` - LdapConfig tLdapConfig `yaml:"ldap"` - Handlers tHandlers `yaml:"handlers"` - Certificates []tCertificateFileConfig `yaml:"certificates"` + CmdTimeout int `yaml:"command_timeout"` + Socket *tSocketConfig `yaml:"socket"` + LdapConfig tLdapConfig `yaml:"ldap"` + Handlers tHandlers `yaml:"handlers"` + HandlerTimeouts tHandlerTimeouts `yaml:"handler_timeouts"` + Certificates []tCertificateFileConfig `yaml:"certificates"` } ) @@ -235,7 +240,7 @@ func checkFileList(files []string) error { return nil } -// Validate the list of handles +// Validate the list of handlers and the timeout. func (c *tCertFileUpdateConfig) Validate(handlers *tHandlers) error { set := make(map[string]bool) for _, handler := range c.Handlers { @@ -247,6 +252,9 @@ func (c *tCertFileUpdateConfig) Validate(handlers *tHandlers) error { } set[handler] = true } + if c.CmdTimeout != nil && *c.CmdTimeout <= 0 { + return fmt.Errorf("Command timeout must be >0.") + } return nil } @@ -289,6 +297,9 @@ func (c *tCertificateFileConfig) Validate(handlers *tHandlers) error { // Validate the configuration func (c *tConfiguration) Validate() error { + if c.CmdTimeout <= 0 { + return fmt.Errorf("Default command timeout must be >0.") + } if c.Socket != nil { err := c.Socket.Validate() if err != nil { @@ -299,6 +310,14 @@ func (c *tConfiguration) Validate() error { if err != nil { 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 { if cfc.Path == "" { 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. func defaultConfiguration() tConfiguration { cfg := tConfiguration{} + cfg.CmdTimeout = 5 cfg.LdapConfig.Defaults.TLS = "no" cfg.LdapConfig.Structure.CAChaining = "seeAlso" return cfg diff --git a/fetch-certificates.yml.example b/fetch-certificates.yml.example index 255ab60..8942d74 100644 --- a/fetch-certificates.yml.example +++ b/fetch-certificates.yml.example @@ -1,6 +1,9 @@ # 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 # is intended to run in standalone mode only. socket: @@ -63,6 +66,11 @@ handlers: - /usr/sbin/apache2ctl configtest - /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: @@ -101,6 +109,10 @@ certificates: - /some/other/file.pem # Define what must be done after an 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 # is respected. If a command fails to run, execution stops. pre_commands: [] diff --git a/update.go b/update.go index 74a8412..a37ae8f 100644 --- a/update.go +++ b/update.go @@ -122,7 +122,11 @@ func (u *tUpdate) runPreCommands() { l := log.WithField("file", u.config.Certificates[i].Path) 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 { continue } @@ -159,7 +163,11 @@ func (u *tUpdate) runHandlers(handlers []string) map[string]bool { for _, handler := range handlers { l := log.WithField("handler", 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 { continue } @@ -204,7 +212,11 @@ func (u *tUpdate) runPostCommands() { l := log.WithField("file", u.config.Certificates[i].Path) 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 { continue } @@ -215,10 +227,10 @@ func (u *tUpdate) runPostCommands() { } } -// Run a list of commands -func (u *tUpdate) runCommands(commands []string, log *logrus.Entry) error { +// Run a list of commands. +func (u *tUpdate) runCommands(timeout int, commands []string, log *logrus.Entry) error { for i := range commands { - err := u.runCommand(commands[i], log) + err := u.runCommand(timeout, commands[i], log) if err != nil { return fmt.Errorf( "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. -func (b *tUpdate) runCommand(command string, log *logrus.Entry) error { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) +func (b *tUpdate) runCommand(timeout int, command string, log *logrus.Entry) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() - log = log.WithField("command", command) + log = log.WithFields(logrus.Fields{ + "command": command, + "timeout": timeout, + }) log.Debug("Executing command") cmd := exec.CommandContext(ctx, "sh", "-c", command) output, err := cmd.CombinedOutput()