260 lines
6.3 KiB
Go
260 lines
6.3 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"os/exec"
|
||
|
"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")
|
||
|
err := u.runCommands(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")
|
||
|
err := u.runCommands(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")
|
||
|
err := u.runCommands(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(commands []string, log *logrus.Entry) error {
|
||
|
for i := range commands {
|
||
|
err := u.runCommand(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(command string, log *logrus.Entry) error {
|
||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
|
defer cancel()
|
||
|
|
||
|
log = log.WithField("command", command)
|
||
|
log.Debug("Executing command")
|
||
|
cmd := exec.CommandContext(ctx, "sh", "-c", command)
|
||
|
output, err := cmd.CombinedOutput()
|
||
|
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()
|
||
|
}
|