package haproxy import ( "bytes" "context" "crypto/sha256" "fmt" "io" "io/ioutil" "os" "os/exec" "sync" "syscall" "text/template" ) const Nobody = 65534 type ServiceManager interface { Reload(context.Context) error Status(context.Context) error Start(context.Context) error Stop(context.Context) error } type HAProxyManager struct { // protects config update sync.Mutex configFile string svc ServiceManager template *template.Template hash []byte reload bool } func NewHAProxyManager(configFile, service string) (ha *HAProxyManager, err error) { t, err := template.New("haproxy").Parse(haproxyConfigTemplate) if err != nil { return } ha = &HAProxyManager{ configFile: configFile, svc: NewSystemdManager(service), template: t, } return } func (ha *HAProxyManager) checkConfig(ctx context.Context, lbs Config) (hash []byte, err error) { tmp, err := ioutil.TempFile(os.TempDir(), "haproxy_") if err != nil { return } defer tmp.Close() defer os.Remove(tmp.Name()) h := sha256.New() w := io.MultiWriter(tmp, h) err = ha.template.Execute(w, lbs) if err != nil { return } hash = h.Sum(nil) err = os.Chmod(tmp.Name(), 0640) if err != nil { return } err = os.Chown(tmp.Name(), -1, Nobody) if err != nil { return } cmd := exec.CommandContext(ctx, "haproxy", "-c", "-f", tmp.Name()) cmd.Dir = os.TempDir() cmd.SysProcAttr = &syscall.SysProcAttr{ Credential: &syscall.Credential{ Uid: Nobody, Gid: Nobody, }, Setsid: true, } out, err := cmd.CombinedOutput() if err != nil { err = fmt.Errorf("%s:%w", string(out), err) } return hash, err } func (ha *HAProxyManager) updateConfig(ctx context.Context, lbs Config) error { hash, err := ha.checkConfig(ctx, lbs) if err != nil { return err } if bytes.Equal(ha.hash, hash) { ha.reload = false return nil } ha.reload = true file, err := os.OpenFile(ha.configFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err } err = ha.template.Execute(file, lbs) if err != nil { file.Close() return err } err = file.Close() ha.hash = hash return err } func (ha *HAProxyManager) UpdateConfig(ctx context.Context, lbs Config) error { ha.Lock() defer ha.Unlock() err := ha.updateConfig(ctx, lbs) if err != nil { return err } if ha.reload { return ha.svc.Reload(ctx) } return nil } func (ha *HAProxyManager) Start(ctx context.Context) error { err := ha.svc.Status(ctx) if err != nil { err = ha.updateConfig(ctx, Config{}) if err != nil { return err } err = ha.svc.Start(ctx) } return err } func (ha *HAProxyManager) IsRunning(ctx context.Context) error { return ha.svc.Status(ctx) } func (ha *HAProxyManager) Stop(ctx context.Context) error { return ha.svc.Stop(ctx) }