From 3e088d4af75b405ee305e360a1b7c7911e9a7893 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Emmanuel=20Beno=C3=AEt?= <tseeker@nocternity.net>
Date: Sat, 4 Dec 2021 10:40:15 +0100
Subject: [PATCH] New configuration for commands/handlers to run after updates

  * The new configuration includes specific commands per file, as well
    as handlers that can be triggered by updates but will only ever run
    once.
  * For now, commands from the pre_commands section are executed, the
    rest is ignored
---
 buildcert.go                   |  8 ++++----
 config.go                      | 32 +++++++++++++++++++++-----------
 fetch-certificates.yml.example | 23 ++++++++++++++++++++---
 3 files changed, 45 insertions(+), 18 deletions(-)

diff --git a/buildcert.go b/buildcert.go
index 4853c8d..b6e182f 100644
--- a/buildcert.go
+++ b/buildcert.go
@@ -206,12 +206,12 @@ func (b *tCertificateBuilder) RunCommandsIfChanged() error {
 		log.Debug("Not running commands")
 		return nil
 	}
-	for i := range b.config.AfterUpdate {
+	for i := range b.config.AfterUpdate.PreCommands {
 		err := b.RunCommand(i)
 		if err != nil {
 			return fmt.Errorf(
 				"Failed while executing command '%s': %w",
-				b.config.AfterUpdate[i],
+				b.config.AfterUpdate.PreCommands[i],
 				err,
 			)
 		}
@@ -224,9 +224,9 @@ func (b *tCertificateBuilder) RunCommand(pos int) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 
-	log := b.logger.WithField("command", b.config.AfterUpdate[pos])
+	log := b.logger.WithField("command", b.config.AfterUpdate.PreCommands[pos])
 	log.Debug("Executing command")
-	cmd := exec.CommandContext(ctx, "sh", "-c", b.config.AfterUpdate[pos])
+	cmd := exec.CommandContext(ctx, "sh", "-c", b.config.AfterUpdate.PreCommands[pos])
 	output, err := cmd.CombinedOutput()
 	if len(output) != 0 {
 		if utf8.Valid(output) {
diff --git a/config.go b/config.go
index 7d6050a..02458fe 100644
--- a/config.go
+++ b/config.go
@@ -59,19 +59,29 @@ type (
 		Servers   []tLdapServerConfig   `yaml:"servers"`
 	}
 
+	// Handlers. Each handler has a name and contains a list of commands.
+	tHandlers map[string][]string
+
+	// Certificate file updates configuration.
+	tCertFileUpdateConfig struct {
+		PreCommands  []string  `yaml:"pre_commands"`
+		Handlers     tHandlers `yaml:"handlers"`
+		PostCommands []string  `yaml:"post_commands"`
+	}
+
 	// Certificate file configuration.
 	tCertificateFileConfig struct {
-		Path           string      `yaml:"path"`
-		Mode           os.FileMode `yaml:"mode"`
-		Owner          string      `yaml:"owner"`
-		Group          string      `yaml:"group"`
-		PrependFiles   []string    `yaml:"prepend_files"`
-		Certificate    string      `yaml:"certificate"`
-		CACertificates []string    `yaml:"ca"`
-		CAChainOf      string      `yaml:"ca_chain_of"`
-		Reverse        bool        `yaml:"reverse"`
-		AppendFiles    []string    `yaml:"append_files"`
-		AfterUpdate    []string    `yaml:"after_update"`
+		Path           string                `yaml:"path"`
+		Mode           os.FileMode           `yaml:"mode"`
+		Owner          string                `yaml:"owner"`
+		Group          string                `yaml:"group"`
+		PrependFiles   []string              `yaml:"prepend_files"`
+		Certificate    string                `yaml:"certificate"`
+		CACertificates []string              `yaml:"ca"`
+		CAChainOf      string                `yaml:"ca_chain_of"`
+		Reverse        bool                  `yaml:"reverse"`
+		AppendFiles    []string              `yaml:"append_files"`
+		AfterUpdate    tCertFileUpdateConfig `yaml:"after_update"`
 	}
 
 	// Main configuration.
diff --git a/fetch-certificates.yml.example b/fetch-certificates.yml.example
index 4c5d492..2d637ae 100644
--- a/fetch-certificates.yml.example
+++ b/fetch-certificates.yml.example
@@ -53,6 +53,15 @@ ldap:
     - host: ldap1.example.org
     - host: ldap2.example.org
 
+# Handlers. Certificate updates can specify that a handler must be executed
+# if the PEM file is replaced. A handler will only be executed once for all
+# triggered updates. Each handler is a list of commands. When a handler runs,
+# the first command that fails will stop the execution.
+handlers:
+  apache:
+    - /usr/sbin/apache2ctl configtest
+    - /usr/sbin/apache2ctl graceful
+
 # Certificates that must be updated
 certificates:
 
@@ -89,7 +98,15 @@ certificates:
     # A list of files to append to the output.
     append_files:
       - /some/other/file.pem
-    # A list of commands that will be executed when the file is replaced.
-    # If one of the commands fails, execution will stop.
+    # Define what must be done after an update.
     after_update:
-      - apache2ctl graceful
+      # Commands to execute before handlers are run. The order of the commands
+      # is respected. If a command fails to run, execution stops.
+      pre_commands: []
+      # Handlers to trigger. Handlers will still be executed if a pre-command
+      # had failed but they were triggered by more than one update. Execution
+      # order is arbitrary.
+      handlers:
+        - apache
+      # Commands to execute after handlers are run.
+      post_commands: []