No One Can Stop Me Now
This commit is contained in:
144
services/setec-manager/internal/deploy/git.go
Normal file
144
services/setec-manager/internal/deploy/git.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CommitInfo holds metadata for a single git commit.
|
||||
type CommitInfo struct {
|
||||
Hash string
|
||||
Author string
|
||||
Date string
|
||||
Message string
|
||||
}
|
||||
|
||||
// Clone clones a git repository into dest, checking out the given branch.
|
||||
func Clone(repo, branch, dest string) (string, error) {
|
||||
git, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("git not found: %w", err)
|
||||
}
|
||||
|
||||
args := []string{"clone", "--branch", branch, "--progress", repo, dest}
|
||||
out, err := exec.Command(git, args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return string(out), fmt.Errorf("git clone: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// Pull performs a fast-forward-only pull in the given directory.
|
||||
func Pull(dir string) (string, error) {
|
||||
git, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("git not found: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(git, "pull", "--ff-only")
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return string(out), fmt.Errorf("git pull: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// CurrentCommit returns the hash and message of the latest commit in dir.
|
||||
func CurrentCommit(dir string) (hash string, message string, err error) {
|
||||
git, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("git not found: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(git, "log", "--oneline", "-1")
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("git log: %w", err)
|
||||
}
|
||||
|
||||
line := strings.TrimSpace(string(out))
|
||||
if line == "" {
|
||||
return "", "", fmt.Errorf("git log: no commits found")
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
hash = parts[0]
|
||||
if len(parts) > 1 {
|
||||
message = parts[1]
|
||||
}
|
||||
return hash, message, nil
|
||||
}
|
||||
|
||||
// GetBranch returns the current branch name for the repository in dir.
|
||||
func GetBranch(dir string) (string, error) {
|
||||
git, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("git not found: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(git, "rev-parse", "--abbrev-ref", "HEAD")
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("git rev-parse: %w", err)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
// HasChanges returns true if the working tree in dir has uncommitted changes.
|
||||
func HasChanges(dir string) (bool, error) {
|
||||
git, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("git not found: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(git, "status", "--porcelain")
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("git status: %w", err)
|
||||
}
|
||||
return strings.TrimSpace(string(out)) != "", nil
|
||||
}
|
||||
|
||||
// Log returns the last n commits from the repository in dir.
|
||||
func Log(dir string, n int) ([]CommitInfo, error) {
|
||||
git, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("git not found: %w", err)
|
||||
}
|
||||
|
||||
// Use a delimiter unlikely to appear in commit messages.
|
||||
const sep = "||SETEC||"
|
||||
format := fmt.Sprintf("%%h%s%%an%s%%ai%s%%s", sep, sep, sep)
|
||||
|
||||
cmd := exec.Command(git, "log", fmt.Sprintf("-n%d", n), fmt.Sprintf("--format=%s", format))
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("git log: %w", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
var commits []CommitInfo
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, sep, 4)
|
||||
if len(parts) < 4 {
|
||||
continue
|
||||
}
|
||||
commits = append(commits, CommitInfo{
|
||||
Hash: parts[0],
|
||||
Author: parts[1],
|
||||
Date: parts[2],
|
||||
Message: parts[3],
|
||||
})
|
||||
}
|
||||
return commits, nil
|
||||
}
|
||||
100
services/setec-manager/internal/deploy/node.go
Normal file
100
services/setec-manager/internal/deploy/node.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NpmInstall runs npm install in the given directory.
|
||||
func NpmInstall(dir string) (string, error) {
|
||||
npm, err := exec.LookPath("npm")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("npm not found: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(npm, "install")
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return string(out), fmt.Errorf("npm install: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// NpmBuild runs npm run build in the given directory.
|
||||
func NpmBuild(dir string) (string, error) {
|
||||
npm, err := exec.LookPath("npm")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("npm not found: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(npm, "run", "build")
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return string(out), fmt.Errorf("npm run build: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// NpmAudit runs npm audit in the given directory and returns the report.
|
||||
func NpmAudit(dir string) (string, error) {
|
||||
npm, err := exec.LookPath("npm")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("npm not found: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(npm, "audit")
|
||||
cmd.Dir = dir
|
||||
// npm audit exits non-zero when vulnerabilities are found, which is not
|
||||
// an execution error — we still want the output.
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
// Return the output even on non-zero exit; the caller can inspect it.
|
||||
return string(out), fmt.Errorf("npm audit: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// HasPackageJSON returns true if a package.json file exists in dir.
|
||||
func HasPackageJSON(dir string) bool {
|
||||
info, err := os.Stat(filepath.Join(dir, "package.json"))
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
// HasNodeModules returns true if a node_modules directory exists in dir.
|
||||
func HasNodeModules(dir string) bool {
|
||||
info, err := os.Stat(filepath.Join(dir, "node_modules"))
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
// NodeVersion returns the installed Node.js version string.
|
||||
func NodeVersion() (string, error) {
|
||||
node, err := exec.LookPath("node")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("node not found: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(node, "--version").Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("node --version: %w", err)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
// NpmVersion returns the installed npm version string.
|
||||
func NpmVersion() (string, error) {
|
||||
npm, err := exec.LookPath("npm")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("npm not found: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(npm, "--version").Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("npm --version: %w", err)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
93
services/setec-manager/internal/deploy/python.go
Normal file
93
services/setec-manager/internal/deploy/python.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PipPackage holds the name and version of an installed pip package.
|
||||
type PipPackage struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// CreateVenv creates a Python virtual environment at <dir>/venv.
|
||||
func CreateVenv(dir string) error {
|
||||
python, err := exec.LookPath("python3")
|
||||
if err != nil {
|
||||
return fmt.Errorf("python3 not found: %w", err)
|
||||
}
|
||||
|
||||
venvPath := filepath.Join(dir, "venv")
|
||||
out, err := exec.Command(python, "-m", "venv", venvPath).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create venv: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpgradePip upgrades pip, setuptools, and wheel inside the virtual environment
|
||||
// rooted at venvDir.
|
||||
func UpgradePip(venvDir string) error {
|
||||
pip := filepath.Join(venvDir, "bin", "pip")
|
||||
if _, err := os.Stat(pip); err != nil {
|
||||
return fmt.Errorf("pip not found at %s: %w", pip, err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(pip, "install", "--upgrade", "pip", "setuptools", "wheel").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("upgrade pip: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallRequirements installs packages from a requirements file into the
|
||||
// virtual environment rooted at venvDir.
|
||||
func InstallRequirements(venvDir, reqFile string) (string, error) {
|
||||
pip := filepath.Join(venvDir, "bin", "pip")
|
||||
if _, err := os.Stat(pip); err != nil {
|
||||
return "", fmt.Errorf("pip not found at %s: %w", pip, err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(reqFile); err != nil {
|
||||
return "", fmt.Errorf("requirements file not found: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(pip, "install", "-r", reqFile).CombinedOutput()
|
||||
if err != nil {
|
||||
return string(out), fmt.Errorf("pip install: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// ListPackages returns all installed packages in the virtual environment
|
||||
// rooted at venvDir.
|
||||
func ListPackages(venvDir string) ([]PipPackage, error) {
|
||||
pip := filepath.Join(venvDir, "bin", "pip")
|
||||
if _, err := os.Stat(pip); err != nil {
|
||||
return nil, fmt.Errorf("pip not found at %s: %w", pip, err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(pip, "list", "--format=json").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pip list: %w", err)
|
||||
}
|
||||
|
||||
var packages []PipPackage
|
||||
if err := json.Unmarshal(out, &packages); err != nil {
|
||||
return nil, fmt.Errorf("parse pip list output: %w", err)
|
||||
}
|
||||
return packages, nil
|
||||
}
|
||||
|
||||
// VenvExists returns true if a virtual environment with a working python3
|
||||
// binary exists at <dir>/venv.
|
||||
func VenvExists(dir string) bool {
|
||||
python := filepath.Join(dir, "venv", "bin", "python3")
|
||||
info, err := os.Stat(python)
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
246
services/setec-manager/internal/deploy/systemd.go
Normal file
246
services/setec-manager/internal/deploy/systemd.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UnitConfig holds the parameters needed to generate a systemd unit file.
|
||||
type UnitConfig struct {
|
||||
Name string
|
||||
Description string
|
||||
ExecStart string
|
||||
WorkingDirectory string
|
||||
User string
|
||||
Environment map[string]string
|
||||
After string
|
||||
RestartPolicy string
|
||||
}
|
||||
|
||||
// GenerateUnit produces the contents of a systemd service unit file from cfg.
|
||||
func GenerateUnit(cfg UnitConfig) string {
|
||||
var b strings.Builder
|
||||
|
||||
// [Unit]
|
||||
b.WriteString("[Unit]\n")
|
||||
if cfg.Description != "" {
|
||||
fmt.Fprintf(&b, "Description=%s\n", cfg.Description)
|
||||
}
|
||||
after := cfg.After
|
||||
if after == "" {
|
||||
after = "network.target"
|
||||
}
|
||||
fmt.Fprintf(&b, "After=%s\n", after)
|
||||
|
||||
// [Service]
|
||||
b.WriteString("\n[Service]\n")
|
||||
b.WriteString("Type=simple\n")
|
||||
if cfg.User != "" {
|
||||
fmt.Fprintf(&b, "User=%s\n", cfg.User)
|
||||
}
|
||||
if cfg.WorkingDirectory != "" {
|
||||
fmt.Fprintf(&b, "WorkingDirectory=%s\n", cfg.WorkingDirectory)
|
||||
}
|
||||
fmt.Fprintf(&b, "ExecStart=%s\n", cfg.ExecStart)
|
||||
|
||||
restart := cfg.RestartPolicy
|
||||
if restart == "" {
|
||||
restart = "on-failure"
|
||||
}
|
||||
fmt.Fprintf(&b, "Restart=%s\n", restart)
|
||||
b.WriteString("RestartSec=5\n")
|
||||
|
||||
// Environment variables — sorted for deterministic output.
|
||||
if len(cfg.Environment) > 0 {
|
||||
keys := make([]string, 0, len(cfg.Environment))
|
||||
for k := range cfg.Environment {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(&b, "Environment=%s=%s\n", k, cfg.Environment[k])
|
||||
}
|
||||
}
|
||||
|
||||
// [Install]
|
||||
b.WriteString("\n[Install]\n")
|
||||
b.WriteString("WantedBy=multi-user.target\n")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// InstallUnit writes a systemd unit file and reloads the daemon.
|
||||
func InstallUnit(name, content string) error {
|
||||
systemctl, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("systemctl not found: %w", err)
|
||||
}
|
||||
|
||||
unitPath := filepath.Join("/etc/systemd/system", name+".service")
|
||||
if err := os.WriteFile(unitPath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("write unit file: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(systemctl, "daemon-reload").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("daemon-reload: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveUnit stops, disables, and removes a systemd unit file, then reloads.
|
||||
func RemoveUnit(name string) error {
|
||||
systemctl, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("systemctl not found: %w", err)
|
||||
}
|
||||
|
||||
unit := name + ".service"
|
||||
|
||||
// Best-effort stop and disable — ignore errors if already stopped/disabled.
|
||||
exec.Command(systemctl, "stop", unit).Run()
|
||||
exec.Command(systemctl, "disable", unit).Run()
|
||||
|
||||
unitPath := filepath.Join("/etc/systemd/system", unit)
|
||||
if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("remove unit file: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(systemctl, "daemon-reload").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("daemon-reload: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts a systemd unit.
|
||||
func Start(unit string) error {
|
||||
systemctl, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("systemctl not found: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(systemctl, "start", unit).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("start %s: %s: %w", unit, strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops a systemd unit.
|
||||
func Stop(unit string) error {
|
||||
systemctl, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("systemctl not found: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(systemctl, "stop", unit).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stop %s: %s: %w", unit, strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restart restarts a systemd unit.
|
||||
func Restart(unit string) error {
|
||||
systemctl, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("systemctl not found: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(systemctl, "restart", unit).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("restart %s: %s: %w", unit, strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enable enables a systemd unit to start on boot.
|
||||
func Enable(unit string) error {
|
||||
systemctl, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("systemctl not found: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(systemctl, "enable", unit).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("enable %s: %s: %w", unit, strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disable disables a systemd unit from starting on boot.
|
||||
func Disable(unit string) error {
|
||||
systemctl, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("systemctl not found: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(systemctl, "disable", unit).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("disable %s: %s: %w", unit, strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsActive returns true if the given systemd unit is currently active.
|
||||
func IsActive(unit string) (bool, error) {
|
||||
systemctl, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("systemctl not found: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(systemctl, "is-active", unit).Output()
|
||||
status := strings.TrimSpace(string(out))
|
||||
if status == "active" {
|
||||
return true, nil
|
||||
}
|
||||
// is-active exits non-zero for inactive/failed — that is not an error
|
||||
// in our context, just means the unit is not active.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Status returns the full systemctl status output for a unit.
|
||||
func Status(unit string) (string, error) {
|
||||
systemctl, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("systemctl not found: %w", err)
|
||||
}
|
||||
|
||||
// systemctl status exits non-zero for stopped services, so we use
|
||||
// CombinedOutput and only treat missing-binary as a real error.
|
||||
out, _ := exec.Command(systemctl, "status", unit).CombinedOutput()
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// Logs returns the last n lines of journal output for a systemd unit.
|
||||
func Logs(unit string, lines int) (string, error) {
|
||||
journalctl, err := exec.LookPath("journalctl")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("journalctl not found: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(journalctl, "-u", unit, "-n", fmt.Sprintf("%d", lines), "--no-pager").CombinedOutput()
|
||||
if err != nil {
|
||||
return string(out), fmt.Errorf("journalctl: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// DaemonReload runs systemctl daemon-reload.
|
||||
func DaemonReload() error {
|
||||
systemctl, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("systemctl not found: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(systemctl, "daemon-reload").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("daemon-reload: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user