diff --git a/.golangci.yml b/.golangci.yml index b69b602..1290b30 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -53,7 +53,7 @@ linters-settings: rules: # - name: add-constant - name: argument-limit - arguments: 8 + arguments: 6 - name: atomic - name: bare-return - name: blank-imports diff --git a/hack/Dockerfile b/hack/Dockerfile index 87fddd7..e3d73ec 100644 --- a/hack/Dockerfile +++ b/hack/Dockerfile @@ -31,6 +31,9 @@ ENV INIT_VERBOSE_LOGGING="true" ENV INIT_WATCH_INTERVAL="5s" ENV INIT_WATCH_PATH="/etc/" +# ENV INIT_PRE_RELOAD_COMMAND_PATH="/usr/bin/coreutils" +# ENV INIT_PRE_RELOAD_COMMAND_ARGS="--coreutils-prog=false" + # ENV INIT_K8S_BASE_DIRECTORY_PATH="/etc/k8s.d/" # ENV INIT_K8S_NAMESPACE="default" # ENV INIT_K8S_CONFIG_MAP_NAME="dnsmasq-config" diff --git a/pkg/config/minimal/config.go b/pkg/config/minimal/config.go index 6f35fef..7d25521 100644 --- a/pkg/config/minimal/config.go +++ b/pkg/config/minimal/config.go @@ -20,6 +20,13 @@ Available envars configuration options: - %PREFIX%WORK_DIRECTORY_PATH path to application new current working directory. + - %PREFIX%PRE_RELOAD_COMMAND_PATH + path to executable that is going to be run before + sending reload signal, signal will be sent + only on successful run of pre-reload command. + - %PREFIX%PRE_RELOAD_COMMAND_ARGS + pre-reload command arguments. + - %PREFIX%RELOAD_SIGNAL OS signal what triggers application config reload [default 'SIGHUP']. - %PREFIX%RELOAD_SIGNAL_TO_PGID @@ -52,9 +59,11 @@ type Config struct { pause chan bool // pause path watching - commandPath string - workDirectory string - commandArgs []string + workDirectory string + commandPath string + preReloadCommandPath string + commandArgs []string + preReloadCommandArgs []string reloadSignal unix.Signal watchInterval time.Duration @@ -83,20 +92,22 @@ func (*Config) GetDefaultEnvPrefix() string { return shared.DefaultEnvPrefix } func (*Config) GetDefaultLogPrefix() string { return shared.DefaultLogPrefix } func (*Config) GetDescriptionBody() string { return DescriptionBody } -func (c *Config) GetCommandArgs() []string { return c.commandArgs } -func (c *Config) GetCommandPath() string { return c.commandPath } -func (c *Config) GetEnvPrefix() string { return c.envPrefix } -func (c *Config) GetPauseChannel() chan bool { return c.pause } -func (c *Config) GetReloadSignal() unix.Signal { return c.reloadSignal } -func (c *Config) GetReloadSignalToPGID() bool { return c.reloadSignalToPGID } -func (c *Config) GetSignalToDirectChildOnly() bool { return c.signalToDirectChildOnly } -func (c *Config) GetVerboseLogging() bool { return c.verboseLogging } -func (c *Config) GetWatchInterval() time.Duration { return c.watchInterval } -func (c *Config) GetWatchPath() string { return c.watchPath } -func (c *Config) GetWorkDirectory() string { return c.workDirectory } +func (c *Config) GetCommandArgs() []string { return c.commandArgs } +func (c *Config) GetCommandPath() string { return c.commandPath } +func (c *Config) GetEnvPrefix() string { return c.envPrefix } +func (c *Config) GetPauseChannel() chan bool { return c.pause } +func (c *Config) GetPreReloadCommandArgs() []string { return c.preReloadCommandArgs } +func (c *Config) GetPreReloadCommandPath() string { return c.preReloadCommandPath } +func (c *Config) GetReloadSignal() unix.Signal { return c.reloadSignal } +func (c *Config) GetReloadSignalToPGID() bool { return c.reloadSignalToPGID } +func (c *Config) GetSignalToDirectChildOnly() bool { return c.signalToDirectChildOnly } +func (c *Config) GetVerboseLogging() bool { return c.verboseLogging } +func (c *Config) GetWatchInterval() time.Duration { return c.watchInterval } +func (c *Config) GetWatchPath() string { return c.watchPath } +func (c *Config) GetWorkDirectory() string { return c.workDirectory } // Get reads environment variables to update and validate configuration object. -func (c *Config) Get() error { +func (c *Config) Get() error { //nolint: cyclop // although cyclomatic complexity is high, function is readable due to similar setter calls if err := c.SetCommandPath("COMMAND_PATH"); err != nil { return err } @@ -105,6 +116,14 @@ func (c *Config) Get() error { return err } + if err := c.SetPreReloadCommandPath("PRE_RELOAD_COMMAND_PATH"); err != nil { + return err + } + + if err := c.SetPreReloadCommandArgs("PRE_RELOAD_COMMAND_ARGS"); err != nil { + return err + } + if err := c.SetWorkingDirectory("WORK_DIRECTORY_PATH"); err != nil { return err } @@ -330,3 +349,39 @@ func (c *Config) SetVerboseLogging(env string) error { return nil } + +// SetPreReloadCommandPath reads pre-reload command path from environ and updates its value inside config. +func (c *Config) SetPreReloadCommandPath(env string) error { + env = c.envPrefix + env + + val, ok, err := shared.LookupEnvValue(env) + if err != nil { + return err //nolint: wrapcheck // error string formed in external package is styled correctly + } + + if !ok { + return nil + } + + if err := validate.Executable(val); err != nil { + return fmt.Errorf("%s: %w", env, err) + } + + c.preReloadCommandPath = val + + return nil +} + +// SetPreReloadCommandArgs reads pre-reload command args from environ and updates its value inside config. +func (c *Config) SetPreReloadCommandArgs(env string) error { + env = c.envPrefix + env + + val, _, err := shared.LookupEnvValue(env) + if err != nil { + return err //nolint: wrapcheck // error string formed in external package is styled correctly + } + + c.preReloadCommandArgs = strings.Fields(val) + + return nil +} diff --git a/pkg/sysinit/cmd.go b/pkg/sysinit/cmd.go index 04a52b5..5c2c524 100644 --- a/pkg/sysinit/cmd.go +++ b/pkg/sysinit/cmd.go @@ -40,3 +40,33 @@ func configureExecCMD(ctx context.Context, c Config, _ logger.Logger) *exec.Cmd return cmd } + +func configurePreReloadExecCMD(ctx context.Context, c Config, _ logger.Logger) *exec.Cmd { + if c.GetPreReloadCommandPath() == "" { + return nil + } + + cmd := exec.CommandContext( //nolint: gosec // executing command passed from config + ctx, + c.GetPreReloadCommandPath(), + c.GetPreReloadCommandArgs()..., + ) + + if c.GetWorkDirectory() != "" { + cmd.Dir = c.GetWorkDirectory() + } + + cmd.Env = utils.FilterStringSlice( + os.Environ(), + func(x string) bool { + return !strings.HasPrefix(x, c.GetEnvPrefix()) + }, + ) + + cmd.SysProcAttr = &unix.SysProcAttr{ + // create a dedicated pidgroup for signal forwarding + Setpgid: true, + } + + return cmd +} diff --git a/pkg/sysinit/config.go b/pkg/sysinit/config.go index 591c6d8..ebd3ebb 100644 --- a/pkg/sysinit/config.go +++ b/pkg/sysinit/config.go @@ -18,4 +18,6 @@ type Config interface { GetWatchInterval() time.Duration GetWatchPath() string GetWorkDirectory() string + GetPreReloadCommandArgs() []string + GetPreReloadCommandPath() string } diff --git a/pkg/sysinit/events.go b/pkg/sysinit/events.go index 8a7e1fd..0c6ba3a 100644 --- a/pkg/sysinit/events.go +++ b/pkg/sysinit/events.go @@ -30,7 +30,7 @@ func signalEvent(c Config, log logger.Logger, sig os.Signal, cmd *exec.Cmd) { log.Debugf("sent '%v' signal to PID '%d'\n", sig, -cmd.Process.Pid) // can be very verbose } -func watcherEvent(c Config, log logger.Logger, v watcher.Message, cmd *exec.Cmd) { +func watcherEvent(c Config, log logger.Logger, v watcher.Message, cmd, preReloadCmd *exec.Cmd) { if v.Error != nil { log.Errorf("%v\n", v.Error) } @@ -45,6 +45,16 @@ func watcherEvent(c Config, log logger.Logger, v watcher.Message, cmd *exec.Cmd) pid = -cmd.Process.Pid } + if preReloadCmd != nil { + log.Debugf("pre-reload command defined: %s\n", preReloadCmd.String()) + + if err := preReloadCmd.Run(); err != nil { + log.Errorf("failed to send '%v' signal, pre-reload command failed: %v\n", c.GetReloadSignal(), err) + + return + } + } + sendSignal(log, pid, c.GetReloadSignal()) log.Infof("sent '%v' signal to PID '%d'\n", c.GetReloadSignal(), pid) diff --git a/pkg/sysinit/run.go b/pkg/sysinit/run.go index 3bfbb3e..943d4f3 100644 --- a/pkg/sysinit/run.go +++ b/pkg/sysinit/run.go @@ -32,6 +32,7 @@ func Run(ctx context.Context, wg *sync.WaitGroup, c Config, log logger.Logger) e defer signal.Reset() cmd := configureExecCMD(ctx, c, log) + preReloadCmd := configurePreReloadExecCMD(ctx, c, log) if err := cmd.Start(); err != nil { return err //nolint: wrapcheck // error message wrapping is done by `GetErrorMessage(err error) string` @@ -44,7 +45,15 @@ func Run(ctx context.Context, wg *sync.WaitGroup, c Config, log logger.Logger) e wg.Add(1) - go worker(ctx, wg, c, log, cmd, sigs, watch, reap) + go worker(ctx, wg, c, log, + &workerConfig{ + cmd: cmd, + preReloadCmd: preReloadCmd, + sigs: sigs, + watch: watch, + reap: reap, + }, + ) err := cmd.Wait() log.Infof("finished process '%v' with PID '%d'\n", cmd.String(), cmd.Process.Pid) diff --git a/pkg/sysinit/worker.go b/pkg/sysinit/worker.go index 8dbb456..9435e1a 100644 --- a/pkg/sysinit/worker.go +++ b/pkg/sysinit/worker.go @@ -11,15 +11,21 @@ import ( "github.com/s3rj1k/ninit/pkg/watcher" ) +type workerConfig struct { + cmd *exec.Cmd + preReloadCmd *exec.Cmd + + sigs <-chan os.Signal + watch <-chan watcher.Message + reap <-chan reaper.Message +} + func worker( ctx context.Context, wg *sync.WaitGroup, c Config, log logger.Logger, - cmd *exec.Cmd, - sigs <-chan os.Signal, - watch <-chan watcher.Message, - reap <-chan reaper.Message, + wc *workerConfig, ) { for { select { @@ -28,13 +34,13 @@ func worker( return - case sig := <-sigs: - signalEvent(c, log, sig, cmd) + case sig := <-wc.sigs: + signalEvent(c, log, sig, wc.cmd) - case v := <-watch: - watcherEvent(c, log, v, cmd) + case v := <-wc.watch: + watcherEvent(c, log, v, wc.cmd, wc.preReloadCmd) - case v := <-reap: + case v := <-wc.reap: reaperEvent(c, log, v) } } diff --git a/pkg/watcher/path.go b/pkg/watcher/path.go index 522e45a..8ec5af6 100644 --- a/pkg/watcher/path.go +++ b/pkg/watcher/path.go @@ -28,7 +28,14 @@ func Path(ctx context.Context, wg *sync.WaitGroup, path string, interval time.Du wg.Add(1) - go worker(ctx, wg, msg, path, interval, pause) + go worker(ctx, wg, + &workerConfig{ + ch: msg, + interval: interval, + path: path, + pause: pause, + }, + ) return msg } diff --git a/pkg/watcher/worker.go b/pkg/watcher/worker.go index 883c6e6..1d24442 100644 --- a/pkg/watcher/worker.go +++ b/pkg/watcher/worker.go @@ -8,8 +8,15 @@ import ( "github.com/s3rj1k/ninit/pkg/hash" ) -func worker(ctx context.Context, wg *sync.WaitGroup, ch chan<- Message, path string, interval time.Duration, pause <-chan bool) { - ticker := time.NewTicker(interval) +type workerConfig struct { + ch chan<- Message + pause <-chan bool + path string + interval time.Duration +} + +func worker(ctx context.Context, wg *sync.WaitGroup, wc *workerConfig) { + ticker := time.NewTicker(wc.interval) ignoreTicks := false defer func(wg *sync.WaitGroup, ch chan<- Message, ticker *time.Ticker) { @@ -17,25 +24,25 @@ func worker(ctx context.Context, wg *sync.WaitGroup, ch chan<- Message, path str ticker.Stop() close(ch) wg.Done() - }(wg, ch, ticker) + }(wg, wc.ch, ticker) - initialHash, err := hash.FromPath(path) + initialHash, err := hash.FromPath(wc.path) if err != nil { - ch <- hashError(path, err) + wc.ch <- hashError(wc.path, err) } for { select { case <-ctx.Done(): - ch <- shutdown(path) + wc.ch <- shutdown(wc.path) return - case ignoreTicks = <-pause: + case ignoreTicks = <-wc.pause: if ignoreTicks { - ch <- paused(path) + wc.ch <- paused(wc.path) } else { - ch <- resumed(path) + wc.ch <- resumed(wc.path) } case <-ticker.C: @@ -45,9 +52,9 @@ func worker(ctx context.Context, wg *sync.WaitGroup, ch chan<- Message, path str t1 := time.Now() - currentHash, err := hash.FromPath(path) + currentHash, err := hash.FromPath(wc.path) if err != nil { - ch <- hashError(path, err) + wc.ch <- hashError(wc.path, err) continue } @@ -55,7 +62,7 @@ func worker(ctx context.Context, wg *sync.WaitGroup, ch chan<- Message, path str t2 := time.Now() if currentHash != initialHash { - ch <- change(path, t2.Sub(t1)) + wc.ch <- change(wc.path, t2.Sub(t1)) initialHash = currentHash }