fetchcert/update.go

284 lines
7.1 KiB
Go
Raw Permalink Normal View History

package main
import (
"context"
"fmt"
"os/exec"
"syscall"
"time"
"unicode/utf8"
"github.com/sirupsen/logrus"
)
type (
tUpdate struct {
// The current configuration
config *tConfiguration
// The selector for this update
selector string
// Whether the update must be forced.
force bool
// Certificate builders for each configured certificate file
builders []*tCertificateBuilder
// Whether errors occurred during the update.
errors bool
}
)
// Start a new update, based on the specified configuration. The update's
// parameters (selector and force flag) will be stored as well.
func NewUpdate(cfg *tConfiguration, selector string, force bool) tUpdate {
return tUpdate{
config: cfg,
selector: selector,
force: force,
builders: make([]*tCertificateBuilder, len(cfg.Certificates)),
}
}
// Execute the update. Builders will be initialized and filtered based on the
// selector, then used to write the certificates to files. After that, commands
// and handlers will be executed.
func (u *tUpdate) Execute() bool {
u.initBuilders()
u.writeFiles()
u.runPreCommands()
handlers := u.enumerateHandlers()
failedHandlers := u.runHandlers(handlers)
u.disableBuildersWithFailedHandlers(failedHandlers)
u.runPostCommands()
return !u.errors
}
// Initialise builders for all certificates that need to be updated. If errors
// occur while preparing one of the certificates, or if it doesn't match the
// selector, the builder will not be kept.
func (u *tUpdate) initBuilders() {
ldap := NewLdapConnection(u.config.LdapConfig)
if ldap == nil {
return
}
defer ldap.Close()
for i := range u.config.Certificates {
builder := NewCertificateBuilder(ldap, &u.config.Certificates[i])
err := builder.Build()
if err != nil {
log.WithField("error", err).Error(
"Failed to build data for certificate '",
builder.Config.Path, "'",
)
u.errors = true
} else if builder.SelectorMatches(u.selector) {
u.builders[i] = builder
}
}
}
// Write certificates to disk and set file ownership/privileges for all builders
// that were initalised.
func (u *tUpdate) writeFiles() {
for i, builder := range u.builders {
if builder == nil {
continue
}
if builder.MustWrite(u.force) {
err := builder.WriteFile()
if err != nil {
log.WithField("error", err).Error(
"Failed to write '",
builder.Config.Path, "'",
)
u.errors = true
continue
}
}
err := builder.UpdatePrivileges()
if err != nil {
log.WithField("error", err).Error(
"Failed to update privileges on '",
builder.Config.Path, "'",
)
u.errors = true
continue
}
if !builder.Changed() {
u.builders[i] = nil
}
}
}
// Run pre-commands for all builders.
func (u *tUpdate) runPreCommands() {
for i, builder := range u.builders {
if builder == nil {
continue
}
commands := u.config.Certificates[i].AfterUpdate.PreCommands
if len(commands) == 0 {
continue
}
l := log.WithField("file", u.config.Certificates[i].Path)
l.Info("Running pre-commands")
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
}
l.WithField("error", err).Error("Failed to run pre-commands")
u.builders[i] = nil
u.errors = true
}
}
// Returns a list of all handlers that must be executed based on the builders
// still listed as active.
func (u *tUpdate) enumerateHandlers() []string {
handlers := make(map[string]bool)
for i, builder := range u.builders {
if builder == nil {
continue
}
for _, handler := range u.config.Certificates[i].AfterUpdate.Handlers {
handlers[handler] = true
}
}
hdl_list := []string{}
for handler := range handlers {
hdl_list = append(hdl_list, handler)
}
return hdl_list
}
// Execute commands for all listed handlers, returning a map of handlers that
// failed to execute.
func (u *tUpdate) runHandlers(handlers []string) map[string]bool {
failures := make(map[string]bool)
for _, handler := range handlers {
l := log.WithField("handler", handler)
l.Info("Running handler")
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
}
l.WithField("error", err).Error("Failed to run handler commands")
failures[handler] = true
u.errors = true
}
return failures
}
// Disable builders that have one of the failed handlers in their list of
// handlers.
func (u *tUpdate) disableBuildersWithFailedHandlers(failedHandlers map[string]bool) {
for i, builder := range u.builders {
if builder == nil {
continue
}
for _, handler := range u.config.Certificates[i].AfterUpdate.Handlers {
if _, exists := failedHandlers[handler]; exists {
log.WithFields(logrus.Fields{
"handler": handler,
"file": u.config.Certificates[i].Path,
}).Debug("Disabling builder due to failed handler")
u.builders[i] = nil
break
}
}
}
}
// Run post-commands for all builders.
func (u *tUpdate) runPostCommands() {
for i, builder := range u.builders {
if builder == nil {
continue
}
commands := u.config.Certificates[i].AfterUpdate.PostCommands
if len(commands) == 0 {
continue
}
l := log.WithField("file", u.config.Certificates[i].Path)
l.Info("Running post-commands")
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
}
l.WithField("error", err).Error("Failed to run post-commands")
u.builders[i] = nil
u.errors = true
}
}
// Run a list of commands.
func (u *tUpdate) runCommands(timeout int, commands []string, log *logrus.Entry) error {
for i := range commands {
err := u.runCommand(timeout, commands[i], log)
if err != nil {
return fmt.Errorf(
"Failed while executing command '%s': %w",
commands[i], err,
)
}
}
return nil
}
// Run a command through the `sh` shell.
func (b *tUpdate) runCommand(timeout int, command string, log *logrus.Entry) error {
log = log.WithFields(logrus.Fields{
"command": command,
"timeout": timeout,
})
log.Debug("Executing command")
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
cmd := exec.CommandContext(ctx, "sh", "-c", command)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
go func() {
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
}
}()
output, err := cmd.CombinedOutput()
cancel()
if len(output) != 0 {
if utf8.Valid(output) {
log = log.WithField("output", string(output))
} else {
log = log.WithField("output", string(output))
}
}
if err == nil {
log.Info("Command executed")
} else {
log.WithField("error", err).Error("Command failed")
}
return err
}
func executeUpdate(cfg *tConfiguration, selector string, force bool) bool {
ex := NewUpdate(cfg, selector, force)
return ex.Execute()
}