Skip to content
This repository has been archived by the owner on Jan 22, 2022. It is now read-only.

Commit

Permalink
Add pre-reload command, application config test.
Browse files Browse the repository at this point in the history
Signed-off-by: s3rj1k <evasive.gyron@gmail.com>
  • Loading branch information
s3rj1k committed Apr 13, 2021
1 parent 4246eb4 commit f8447b5
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 40 deletions.
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions hack/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
85 changes: 70 additions & 15 deletions pkg/config/minimal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
30 changes: 30 additions & 0 deletions pkg/sysinit/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 2 additions & 0 deletions pkg/sysinit/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ type Config interface {
GetWatchInterval() time.Duration
GetWatchPath() string
GetWorkDirectory() string
GetPreReloadCommandArgs() []string
GetPreReloadCommandPath() string
}
12 changes: 11 additions & 1 deletion pkg/sysinit/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
Expand Down
11 changes: 10 additions & 1 deletion pkg/sysinit/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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)
Expand Down
24 changes: 15 additions & 9 deletions pkg/sysinit/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}
Expand Down
9 changes: 8 additions & 1 deletion pkg/watcher/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
31 changes: 19 additions & 12 deletions pkg/watcher/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,41 @@ 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) {
// defer inside goroutine works because we return when context is done
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:
Expand All @@ -45,17 +52,17 @@ 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
}

t2 := time.Now()

if currentHash != initialHash {
ch <- change(path, t2.Sub(t1))
wc.ch <- change(wc.path, t2.Sub(t1))

initialHash = currentHash
}
Expand Down

0 comments on commit f8447b5

Please sign in to comment.