Все проверки выполнены успешно
CI / test (push) Successful in 2m24s
Docker / Build and publish worker image (push) Successful in 13m24s
226 строки
5.3 KiB
Go
226 строки
5.3 KiB
Go
package installer
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var dockerImagePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/:@-]*$`)
|
|
|
|
const (
|
|
DefaultURL = "https://rsmon.ru"
|
|
DefaultImage = "reg.rsxx.ru/rsmon/rsmon-worker:latest"
|
|
binaryPath = "/usr/local/bin/rsmon-worker"
|
|
envPath = "/etc/rsmon-worker/worker.env"
|
|
unitPath = "/etc/systemd/system/rsmon-worker.service"
|
|
)
|
|
|
|
const systemdUnit = `[Unit]
|
|
Description=RSMon distributed monitoring worker
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=root
|
|
EnvironmentFile=/etc/rsmon-worker/worker.env
|
|
ExecStart=/usr/local/bin/rsmon-worker
|
|
Restart=on-failure
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
`
|
|
|
|
const dockerSystemdUnit = `[Unit]
|
|
Description=RSMon distributed monitoring worker (Docker)
|
|
After=network-online.target docker.service
|
|
Requires=docker.service
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=root
|
|
ExecStartPre=-docker rm -f rsmon-worker
|
|
ExecStart=docker run --rm --name rsmon-worker --network host --cap-add NET_RAW --env-file /etc/rsmon-worker/worker.env -v rsmon-worker-data:/var/lib/rsmon-worker %s
|
|
ExecStop=docker stop rsmon-worker
|
|
Restart=on-failure
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
`
|
|
|
|
type InstallOptions struct {
|
|
Binary string
|
|
EnvFile string
|
|
Token string
|
|
URL string
|
|
Docker bool
|
|
Image string
|
|
NoStart bool
|
|
}
|
|
|
|
func Install(opts InstallOptions) error {
|
|
if os.Geteuid() != 0 {
|
|
return errors.New("install must be run as root")
|
|
}
|
|
if opts.URL == "" {
|
|
opts.URL = DefaultURL
|
|
}
|
|
if err := ValidateURL(opts.URL); err != nil {
|
|
return err
|
|
}
|
|
if opts.EnvFile == "" {
|
|
if err := ValidateToken(opts.Token); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if opts.Image == "" {
|
|
opts.Image = DefaultImage
|
|
}
|
|
if opts.Docker {
|
|
if err := ValidateImage(opts.Image); err != nil {
|
|
return err
|
|
}
|
|
} else if opts.Binary == "" {
|
|
var err error
|
|
opts.Binary, err = os.Executable()
|
|
if err != nil {
|
|
return fmt.Errorf("locate worker executable: %w", err)
|
|
}
|
|
}
|
|
unit := systemdUnit
|
|
if opts.Docker {
|
|
if _, err := exec.LookPath("docker"); err != nil {
|
|
return errors.New("docker is required for --docker installation")
|
|
}
|
|
if err := command("docker", "pull", opts.Image); err != nil {
|
|
return err
|
|
}
|
|
unit = DockerUnit(opts.Image)
|
|
} else if err := copyAtomic(opts.Binary, binaryPath, 0755); err != nil {
|
|
return fmt.Errorf("install binary: %w", err)
|
|
}
|
|
if err := writeAtomic(unitPath, []byte(unit), 0644); err != nil {
|
|
return fmt.Errorf("install systemd unit: %w", err)
|
|
}
|
|
if opts.EnvFile != "" {
|
|
if err := copyAtomic(opts.EnvFile, envPath, 0600); err != nil {
|
|
return fmt.Errorf("install environment: %w", err)
|
|
}
|
|
} else {
|
|
if err := writeAtomic(envPath, Environment(opts.URL, opts.Token), 0600); err != nil {
|
|
return fmt.Errorf("install environment: %w", err)
|
|
}
|
|
}
|
|
if err := command("systemctl", "daemon-reload"); err != nil {
|
|
return err
|
|
}
|
|
if err := command("systemctl", "enable", "rsmon-worker.service"); err != nil {
|
|
return err
|
|
}
|
|
if !opts.NoStart {
|
|
if err := command("systemctl", "restart", "rsmon-worker.service"); err != nil {
|
|
return err
|
|
}
|
|
if err := command("systemctl", "is-active", "--quiet", "rsmon-worker.service"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateURL(raw string) error {
|
|
if strings.ContainsAny(raw, "\r\n") {
|
|
return fmt.Errorf("server URL must be an absolute HTTP(S) URL")
|
|
}
|
|
u, err := url.Parse(raw)
|
|
if err != nil || u.Host == "" || (u.Scheme != "https" && u.Scheme != "http") {
|
|
return fmt.Errorf("server URL must be an absolute HTTP(S) URL")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Environment(serverURL, token string) []byte {
|
|
return []byte("RSMON_URL=" + serverURL + "\nRSMON_TOKEN=" + token + "\nWORKER_HOST=127.0.0.1\n")
|
|
}
|
|
|
|
func ValidateToken(token string) error {
|
|
if strings.TrimSpace(token) == "" || strings.ContainsAny(token, "\r\n") {
|
|
return errors.New("worker token must be non-empty and contain no newlines")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateImage(image string) error {
|
|
if !dockerImagePattern.MatchString(image) {
|
|
return errors.New("Docker image must be one non-option argument")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DockerUnit(image string) string {
|
|
return fmt.Sprintf(dockerSystemdUnit, image)
|
|
}
|
|
|
|
func copyAtomic(src, dst string, mode os.FileMode) error {
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer in.Close()
|
|
return atomicFile(dst, mode, func(out *os.File) error {
|
|
_, err := io.Copy(out, in)
|
|
return err
|
|
})
|
|
}
|
|
|
|
func writeAtomic(dst string, data []byte, mode os.FileMode) error {
|
|
return atomicFile(dst, mode, func(out *os.File) error {
|
|
_, err := out.Write(data)
|
|
return err
|
|
})
|
|
}
|
|
|
|
func atomicFile(dst string, mode os.FileMode, write func(*os.File) error) error {
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
|
return err
|
|
}
|
|
tmp, err := os.CreateTemp(filepath.Dir(dst), ".rsmon-worker-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name := tmp.Name()
|
|
defer os.Remove(name)
|
|
if err := tmp.Chmod(mode); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if err := write(tmp); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if err := tmp.Sync(); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(name, dst)
|
|
}
|
|
|
|
func command(name string, args ...string) error {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("%s failed: %w", name, err)
|
|
}
|
|
return nil
|
|
}
|