Make service install converge: stop running service, refresh binary/config only on change, restart only if it was running
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -126,6 +127,12 @@ func copyFile(src, dst string, mode os.FileMode) error {
|
||||
// copyBin=false the current executable location and config path are
|
||||
// registered as-is instead. Needs root.
|
||||
//
|
||||
// Re-running install converges an existing unit instead of failing: it is
|
||||
// stopped first if active, the installed binary is replaced only when the
|
||||
// content differs, the unit file is rewritten (followed by daemon-reload)
|
||||
// only when it changed, and the service is started again only if it was
|
||||
// active before.
|
||||
//
|
||||
// The binary lives in the StateDirectory rather than /usr/local/sbin on
|
||||
// purpose: replacing a running binary needs write access to its
|
||||
// *directory*, and granting the sandboxed service user write access to a
|
||||
@@ -139,6 +146,16 @@ func Install(configPath string, copyBin bool) error {
|
||||
if abs, absErr := filepath.Abs(exe); absErr == nil {
|
||||
exe = abs
|
||||
}
|
||||
unit := Name + ".service"
|
||||
_, statErr := os.Stat(unitPath)
|
||||
fresh := os.IsNotExist(statErr)
|
||||
wasRunning := exec.Command("systemctl", "is-active", "--quiet", unit).Run() == nil
|
||||
if wasRunning {
|
||||
if out, err := exec.Command("systemctl", "stop", unit).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("systemctl stop (run as root): %w (%s)", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
cfg := etcConfig
|
||||
if copyBin {
|
||||
if err := os.MkdirAll(stateDir, 0o755); err != nil {
|
||||
@@ -146,8 +163,10 @@ func Install(configPath string, copyBin bool) error {
|
||||
}
|
||||
installedExe := filepath.Join(stateDir, Name)
|
||||
if exe != installedExe {
|
||||
if err := copyFile(exe, installedExe, 0o755); err != nil {
|
||||
return fmt.Errorf("install binary to %s: %w", installedExe, err)
|
||||
if same, _ := sameFileContent(exe, installedExe); !same {
|
||||
if err := copyFile(exe, installedExe, 0o755); err != nil {
|
||||
return fmt.Errorf("install binary to %s: %w", installedExe, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
exe = installedExe
|
||||
@@ -161,18 +180,51 @@ func Install(configPath string, copyBin bool) error {
|
||||
cfg = abs
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(unitPath, []byte(renderUnit(exe, cfg)), 0o644); err != nil {
|
||||
return fmt.Errorf("write %s (run as root): %w", unitPath, err)
|
||||
rendered := renderUnit(exe, cfg)
|
||||
if old, _ := os.ReadFile(unitPath); string(old) != rendered {
|
||||
if err := os.WriteFile(unitPath, []byte(rendered), 0o644); err != nil {
|
||||
return fmt.Errorf("write %s (run as root): %w", unitPath, err)
|
||||
}
|
||||
if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("systemctl daemon-reload: %w (%s)", err, out)
|
||||
}
|
||||
}
|
||||
if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("systemctl daemon-reload: %w (%s)", err, out)
|
||||
if fresh {
|
||||
if out, err := exec.Command("systemctl", "enable", "--now", unit).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("systemctl enable --now: %w (%s)", err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if out, err := exec.Command("systemctl", "enable", "--now", Name+".service").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("systemctl enable --now: %w (%s)", err, out)
|
||||
if exec.Command("systemctl", "is-enabled", "--quiet", unit).Run() != nil {
|
||||
if out, err := exec.Command("systemctl", "enable", unit).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("systemctl enable: %w (%s)", err, out)
|
||||
}
|
||||
}
|
||||
if wasRunning {
|
||||
if out, err := exec.Command("systemctl", "start", unit).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("systemctl start: %w (%s)", err, out)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sameFileContent reports whether two files hold identical bytes. A missing
|
||||
// destination is simply "different".
|
||||
func sameFileContent(a, b string) (bool, error) {
|
||||
ba, err := os.ReadFile(a)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
bb, err := os.ReadFile(b)
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return bytes.Equal(ba, bb), nil
|
||||
}
|
||||
|
||||
// Remove stops and disables the service and deletes the unit file and the
|
||||
// installed binary. The config file in /etc is left in place (user data).
|
||||
func Remove() error {
|
||||
|
||||
Reference in New Issue
Block a user