feat: restart to apply update, privileged helper, rollback
This commit is contained in:
1 parent
70460e4170
commit
54e8fd5fed
63 files changed
+8635
-164
No files matched your search
@@ -0,0 +1,46 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
const readableFilePermissions = 0o644
|
||||
|
||||
func writeFileAtomically(path string, content []byte, permissions os.FileMode) error {
|
||||
temporary := path + ".tmp"
|
||||
if err := os.WriteFile(temporary, content, permissions); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(temporary, path); err != nil {
|
||||
_ = os.Remove(temporary)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeJSONAtomically(path string, value any, permissions os.FileMode) error {
|
||||
content, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeFileAtomically(path, content, permissions)
|
||||
}
|
||||
|
||||
func removeFile(path string) (bool, error) {
|
||||
err := os.Remove(path)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
func removeIfPresent(path string) error {
|
||||
_, err := removeFile(path)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
type FSRestartRequestStore struct {
|
||||
files domain.HelperFilesystem
|
||||
path string
|
||||
}
|
||||
|
||||
var _ domain.RestartRequestStore = &FSRestartRequestStore{}
|
||||
|
||||
func NewFSRestartRequestStore(files domain.HelperFilesystem, path string) *FSRestartRequestStore {
|
||||
return &FSRestartRequestStore{files: files, path: path}
|
||||
}
|
||||
|
||||
func (s *FSRestartRequestStore) Write(request domain.RestartRequest) error {
|
||||
return writeJSONThroughTemporary(s.files, s.path, request, statusFilePermissions)
|
||||
}
|
||||
|
||||
func (s *FSRestartRequestStore) Read() (domain.RestartRequest, error) {
|
||||
info, err := s.files.Lstat(s.path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return domain.RestartRequest{}, domain.ErrNoRestartRequest
|
||||
}
|
||||
if err != nil {
|
||||
return domain.RestartRequest{}, err
|
||||
}
|
||||
if info.IsSymlink {
|
||||
return domain.RestartRequest{}, domain.SymlinkError{Path: s.path}
|
||||
}
|
||||
if !info.IsRegular {
|
||||
return domain.RestartRequest{}, fmt.Errorf("%s is not a regular file", s.path)
|
||||
}
|
||||
content, err := s.files.ReadFile(s.path)
|
||||
if err != nil {
|
||||
return domain.RestartRequest{}, err
|
||||
}
|
||||
var request domain.RestartRequest
|
||||
return request, json.Unmarshal(content, &request)
|
||||
}
|
||||
|
||||
func (s *FSRestartRequestStore) Delete() error {
|
||||
return removeThrough(s.files, s.path)
|
||||
}
|
||||
|
||||
func removeThrough(files domain.HelperFilesystem, path string) error {
|
||||
if err := files.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeJSONThroughTemporary(files domain.HelperFilesystem, path string, value any, mode uint32) error {
|
||||
content, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeThroughTemporary(files, path, content, mode)
|
||||
}
|
||||
|
||||
func writeThroughTemporary(files domain.HelperFilesystem, path string, content []byte, mode uint32) error {
|
||||
temporary := path + domain.ResultTempSuffix
|
||||
if _, err := files.Lstat(temporary); err == nil {
|
||||
if err := files.Remove(temporary); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := files.CreateExclusive(temporary, content, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := files.Rename(temporary, path); err != nil {
|
||||
_ = files.Remove(temporary)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
)
|
||||
|
||||
type FSResultStore struct {
|
||||
files domain.HelperFilesystem
|
||||
path string
|
||||
}
|
||||
|
||||
var _ domain.ResultStore = &FSResultStore{}
|
||||
|
||||
func NewFSResultStore(files domain.HelperFilesystem, path string) *FSResultStore {
|
||||
return &FSResultStore{files: files, path: path}
|
||||
}
|
||||
|
||||
func (s *FSResultStore) Read() (domain.Result, bool) {
|
||||
info, err := s.files.Lstat(s.path)
|
||||
if err != nil || !info.IsRegular {
|
||||
return domain.Result{}, false
|
||||
}
|
||||
content, err := s.files.ReadFile(s.path)
|
||||
if err != nil {
|
||||
return domain.Result{}, false
|
||||
}
|
||||
return decodeResult(content)
|
||||
}
|
||||
|
||||
func (s *FSResultStore) Write(result domain.Result) error {
|
||||
return writeJSONThroughTemporary(s.files, s.path, result, readableFilePermissions)
|
||||
}
|
||||
|
||||
func (s *FSResultStore) Delete() error {
|
||||
return removeThrough(s.files, s.path)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const healthCheckTimeout = 2 * time.Second
|
||||
|
||||
type HTTPHealthChecker struct {
|
||||
url string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
var _ domain.HealthChecker = &HTTPHealthChecker{}
|
||||
|
||||
func NewHTTPHealthChecker(port string) *HTTPHealthChecker {
|
||||
return &HTTPHealthChecker{
|
||||
url: "http://127.0.0.1:" + port + "/healthz",
|
||||
client: &http.Client{Timeout: healthCheckTimeout},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPHealthChecker) Healthy() bool {
|
||||
response, err := c.client.Get(c.url)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer response.Body.Close()
|
||||
return response.StatusCode == http.StatusOK
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type JSONHelperStatusWriter struct{}
|
||||
|
||||
var _ domain.HelperStatusWriter = &JSONHelperStatusWriter{}
|
||||
|
||||
func NewJSONHelperStatusWriter() *JSONHelperStatusWriter {
|
||||
return &JSONHelperStatusWriter{}
|
||||
}
|
||||
|
||||
func (w *JSONHelperStatusWriter) Write(dataDir string, status domain.HelperStatus) error {
|
||||
return writeJSONAtomically(filepath.Join(dataDir, domain.HelperStatusFileName), status, readableFilePermissions)
|
||||
}
|
||||
|
||||
func (w *JSONHelperStatusWriter) Remove(dataDir string) (bool, error) {
|
||||
return removeFile(filepath.Join(dataDir, domain.HelperStatusFileName))
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type JSONRestartRequestStore struct {
|
||||
path string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var _ domain.RestartRequestStore = &JSONRestartRequestStore{}
|
||||
|
||||
func NewJSONRestartRequestStore(path string) *JSONRestartRequestStore {
|
||||
return &JSONRestartRequestStore{path: path}
|
||||
}
|
||||
|
||||
func (s *JSONRestartRequestStore) Write(request domain.RestartRequest) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return writeJSONAtomically(s.path, request, statusFilePermissions)
|
||||
}
|
||||
|
||||
func (s *JSONRestartRequestStore) Read() (domain.RestartRequest, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
info, err := os.Lstat(s.path)
|
||||
if errors.Is(err, fs.ErrNotExist) || (err == nil && info.IsDir()) {
|
||||
return domain.RestartRequest{}, domain.ErrNoRestartRequest
|
||||
}
|
||||
if err != nil {
|
||||
return domain.RestartRequest{}, err
|
||||
}
|
||||
content, err := os.ReadFile(s.path)
|
||||
if err != nil {
|
||||
return domain.RestartRequest{}, err
|
||||
}
|
||||
var request domain.RestartRequest
|
||||
return request, json.Unmarshal(content, &request)
|
||||
}
|
||||
|
||||
func (s *JSONRestartRequestStore) Delete() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return removeIfPresent(s.path)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type JSONResultStore struct {
|
||||
path string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var _ domain.ResultStore = &JSONResultStore{}
|
||||
|
||||
func NewJSONResultStore(path string) *JSONResultStore {
|
||||
return &JSONResultStore{path: path}
|
||||
}
|
||||
|
||||
func (s *JSONResultStore) Read() (domain.Result, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
content, err := os.ReadFile(s.path)
|
||||
if err != nil {
|
||||
return domain.Result{}, false
|
||||
}
|
||||
return decodeResult(content)
|
||||
}
|
||||
|
||||
func (s *JSONResultStore) Write(result domain.Result) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return writeJSONAtomically(s.path, result, readableFilePermissions)
|
||||
}
|
||||
|
||||
func (s *JSONResultStore) Delete() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return removeIfPresent(s.path)
|
||||
}
|
||||
|
||||
func decodeResult(content []byte) (domain.Result, bool) {
|
||||
var result domain.Result
|
||||
if err := json.Unmarshal(content, &result); err != nil || !result.IsKnown() {
|
||||
return domain.Result{}, false
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type JSONRunningInfoStore struct {
|
||||
path string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var _ domain.RunningInfoStore = &JSONRunningInfoStore{}
|
||||
|
||||
func NewJSONRunningInfoStore(path string) *JSONRunningInfoStore {
|
||||
return &JSONRunningInfoStore{path: path}
|
||||
}
|
||||
|
||||
func (s *JSONRunningInfoStore) Write(info domain.RunningInfo) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return writeJSONAtomically(s.path, info, readableFilePermissions)
|
||||
}
|
||||
|
||||
func (s *JSONRunningInfoStore) Read() (domain.RunningInfo, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
linkInfo, err := os.Lstat(s.path)
|
||||
if err != nil || !linkInfo.Mode().IsRegular() {
|
||||
return domain.RunningInfo{}, false
|
||||
}
|
||||
content, err := os.ReadFile(s.path)
|
||||
if err != nil {
|
||||
return domain.RunningInfo{}, false
|
||||
}
|
||||
var info domain.RunningInfo
|
||||
if err := json.Unmarshal(content, &info); err != nil || info.Version == "" || info.Pid == 0 {
|
||||
return domain.RunningInfo{}, false
|
||||
}
|
||||
return info, true
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//go:build linux
|
||||
|
||||
package update
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
privateTempDirPrefix = "renewcms-update-"
|
||||
readNoFollowFlags = os.O_RDONLY | syscall.O_NOFOLLOW | syscall.O_NONBLOCK
|
||||
createNoFollowFlags = os.O_WRONLY | os.O_CREATE | os.O_EXCL | syscall.O_NOFOLLOW
|
||||
directoryFlags = os.O_RDONLY | syscall.O_NOFOLLOW | syscall.O_DIRECTORY
|
||||
)
|
||||
|
||||
var errNotRegularFile = errors.New("not a regular file")
|
||||
|
||||
type NoFollowFilesystem struct{}
|
||||
|
||||
var _ domain.HelperFilesystem = &NoFollowFilesystem{}
|
||||
|
||||
func NewNoFollowFilesystem() *NoFollowFilesystem {
|
||||
return &NoFollowFilesystem{}
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) Lstat(path string) (domain.FileInfo, error) {
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return domain.FileInfo{}, err
|
||||
}
|
||||
mode := info.Mode()
|
||||
return domain.FileInfo{
|
||||
IsSymlink: mode&fs.ModeSymlink != 0,
|
||||
IsDir: mode.IsDir(),
|
||||
IsRegular: mode.IsRegular(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) ReadFile(path string) ([]byte, error) {
|
||||
file, err := openRegularNoFollow(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
return io.ReadAll(file)
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) CreateExclusive(path string, content []byte, mode uint32) error {
|
||||
file, err := os.OpenFile(path, createNoFollowFlags, fs.FileMode(mode))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := file.Write(content); err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
return finishWrite(file, mode)
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) Copy(source string, destination string, mode uint32) error {
|
||||
input, err := openRegularNoFollow(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer input.Close()
|
||||
output, err := os.OpenFile(destination, createNoFollowFlags, fs.FileMode(mode))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(output, input); err != nil {
|
||||
output.Close()
|
||||
return err
|
||||
}
|
||||
return finishWrite(output, mode)
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) Rename(oldPath string, newPath string) error {
|
||||
return os.Rename(oldPath, newPath)
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) Remove(path string) error {
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) RemoveAll(path string) error {
|
||||
info, err := os.Lstat(path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return os.Remove(path)
|
||||
}
|
||||
names, err := readDirectoryNames(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range names {
|
||||
if err := f.RemoveAll(filepath.Join(path, name)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) Chmod(path string, mode uint32) error {
|
||||
file, err := os.OpenFile(path, readNoFollowFlags, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return file.Chmod(fs.FileMode(mode))
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) MakePrivateTempDir() (string, error) {
|
||||
return os.MkdirTemp("", privateTempDirPrefix)
|
||||
}
|
||||
|
||||
func openRegularNoFollow(path string) (*os.File, error) {
|
||||
file, err := os.OpenFile(path, readNoFollowFlags, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
file.Close()
|
||||
return nil, errNotRegularFile
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func finishWrite(file *os.File, mode uint32) error {
|
||||
if err := file.Chmod(fs.FileMode(mode)); err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
if err := file.Sync(); err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
return file.Close()
|
||||
}
|
||||
|
||||
func readDirectoryNames(path string) ([]string, error) {
|
||||
directory, err := os.OpenFile(path, directoryFlags, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer directory.Close()
|
||||
return directory.Readdirnames(-1)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//go:build !linux
|
||||
|
||||
package update
|
||||
|
||||
import domain "RenewCMS/internal/domain/update"
|
||||
|
||||
type NoFollowFilesystem struct{}
|
||||
|
||||
var _ domain.HelperFilesystem = &NoFollowFilesystem{}
|
||||
|
||||
func NewNoFollowFilesystem() *NoFollowFilesystem {
|
||||
return &NoFollowFilesystem{}
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) Lstat(string) (domain.FileInfo, error) {
|
||||
return domain.FileInfo{}, domain.ErrHelperUnsupported
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) ReadFile(string) ([]byte, error) {
|
||||
return nil, domain.ErrHelperUnsupported
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) CreateExclusive(string, []byte, uint32) error {
|
||||
return domain.ErrHelperUnsupported
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) Copy(string, string, uint32) error {
|
||||
return domain.ErrHelperUnsupported
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) Rename(string, string) error {
|
||||
return domain.ErrHelperUnsupported
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) Remove(string) error {
|
||||
return domain.ErrHelperUnsupported
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) RemoveAll(string) error {
|
||||
return domain.ErrHelperUnsupported
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) Chmod(string, uint32) error {
|
||||
return domain.ErrHelperUnsupported
|
||||
}
|
||||
|
||||
func (f *NoFollowFilesystem) MakePrivateTempDir() (string, error) {
|
||||
return "", domain.ErrHelperUnsupported
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SystemClock struct{}
|
||||
|
||||
var _ domain.Clock = &SystemClock{}
|
||||
|
||||
func NewSystemClock() *SystemClock {
|
||||
return &SystemClock{}
|
||||
}
|
||||
|
||||
func (c *SystemClock) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (c *SystemClock) Sleep(duration time.Duration) {
|
||||
time.Sleep(duration)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
const systemdRuntimeDirectory = "/run/systemd/system"
|
||||
|
||||
type SystemMachineProbe struct{}
|
||||
|
||||
var _ domain.MachineProbe = &SystemMachineProbe{}
|
||||
|
||||
func NewSystemMachineProbe() *SystemMachineProbe {
|
||||
return &SystemMachineProbe{}
|
||||
}
|
||||
|
||||
func (p *SystemMachineProbe) OS() string {
|
||||
return runtime.GOOS
|
||||
}
|
||||
|
||||
func (p *SystemMachineProbe) SystemdRunning() bool {
|
||||
info, err := os.Stat(systemdRuntimeDirectory)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
func (p *SystemMachineProbe) IsRoot() bool {
|
||||
return os.Geteuid() == 0
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const systemctlCommand = "systemctl"
|
||||
|
||||
type SystemctlControl struct {
|
||||
service string
|
||||
}
|
||||
|
||||
var (
|
||||
_ domain.ServiceControl = &SystemctlControl{}
|
||||
_ domain.UnitManager = &SystemctlControl{}
|
||||
)
|
||||
|
||||
func NewSystemctlControl(service string) *SystemctlControl {
|
||||
return &SystemctlControl{service: service}
|
||||
}
|
||||
|
||||
func (c *SystemctlControl) Restart() error {
|
||||
return runSystemctl("restart", c.service)
|
||||
}
|
||||
|
||||
func (c *SystemctlControl) Status() (bool, error) {
|
||||
err := exec.Command(systemctlCommand, "is-active", "--quiet", c.service).Run()
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
return false, nil
|
||||
}
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func (c *SystemctlControl) DaemonReload() error {
|
||||
return runSystemctl("daemon-reload")
|
||||
}
|
||||
|
||||
func (c *SystemctlControl) EnableNow(unit string) error {
|
||||
return runSystemctl("enable", "--now", unit)
|
||||
}
|
||||
|
||||
func (c *SystemctlControl) DisableNow(unit string) error {
|
||||
return runSystemctl("disable", "--now", unit)
|
||||
}
|
||||
|
||||
func runSystemctl(arguments ...string) error {
|
||||
output, err := exec.Command(systemctlCommand, arguments...).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("systemctl %s: %w: %s", strings.Join(arguments, " "), err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
systemUnitDirectory = "/etc/systemd/system"
|
||||
unitFilePermissions = 0o644
|
||||
unitFileSuffix = ".service"
|
||||
)
|
||||
|
||||
type SystemdUnits struct {
|
||||
directory string
|
||||
}
|
||||
|
||||
var (
|
||||
_ domain.UnitWriter = &SystemdUnits{}
|
||||
_ domain.ServiceLocator = &SystemdUnits{}
|
||||
)
|
||||
|
||||
func NewSystemdUnits() *SystemdUnits {
|
||||
return NewSystemdUnitsIn(systemUnitDirectory)
|
||||
}
|
||||
|
||||
func NewSystemdUnitsIn(directory string) *SystemdUnits {
|
||||
return &SystemdUnits{directory: directory}
|
||||
}
|
||||
|
||||
func (u *SystemdUnits) Write(name string, content string) error {
|
||||
return os.WriteFile(filepath.Join(u.directory, name), []byte(content), unitFilePermissions)
|
||||
}
|
||||
|
||||
func (u *SystemdUnits) Remove(name string) (bool, error) {
|
||||
return removeFile(filepath.Join(u.directory, name))
|
||||
}
|
||||
|
||||
func (u *SystemdUnits) Locate(name string) (domain.ServiceDescription, bool) {
|
||||
content, err := os.ReadFile(filepath.Join(u.directory, name+unitFileSuffix))
|
||||
if err != nil {
|
||||
return domain.ServiceDescription{}, false
|
||||
}
|
||||
description := ParseServiceUnit(string(content))
|
||||
description.Name = name
|
||||
return description, true
|
||||
}
|
||||
|
||||
func (u *SystemdUnits) PortFromEnvironmentFile(path string) (string, bool) {
|
||||
if path == "" {
|
||||
return "", false
|
||||
}
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return portFromEnvironment(string(content))
|
||||
}
|
||||
|
||||
func ParseServiceUnit(content string) domain.ServiceDescription {
|
||||
var description domain.ServiceDescription
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
key, value, found := strings.Cut(strings.TrimSpace(line), "=")
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "ExecStart":
|
||||
description.BinaryPath = firstUnitPath(value, "@-:+!")
|
||||
case "EnvironmentFile":
|
||||
description.EnvironmentFile = firstUnitPath(value, "-")
|
||||
case "ReadWritePaths":
|
||||
if description.DataDir == "" {
|
||||
description.DataDir = firstUnitPath(value, "-+!")
|
||||
}
|
||||
}
|
||||
}
|
||||
return description
|
||||
}
|
||||
|
||||
func firstUnitPath(value string, prefixes string) string {
|
||||
fields := strings.Fields(value)
|
||||
if len(fields) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Trim(strings.TrimLeft(fields[0], prefixes), `"`)
|
||||
}
|
||||
|
||||
func portFromEnvironment(content string) (string, bool) {
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
key, value, found := strings.Cut(strings.TrimSpace(line), "=")
|
||||
if !found || strings.TrimSpace(key) != "PORT" {
|
||||
continue
|
||||
}
|
||||
port := strings.Trim(strings.TrimSpace(value), `"'`)
|
||||
if _, err := strconv.Atoi(port); err == nil {
|
||||
return port, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
Reference in new issue
Block a user