wallarm/internal/preflight/preflight.go

169 lines
4.4 KiB
Go

// Package preflight runs system readiness checks before any deployment.
// It is called on every binary start and returns a structured report.
package preflight
import (
"fmt"
"os"
"git.sechpoint.app/customer-engineering/wallarm/internal/shared"
)
// Result holds the outcome of all preflight checks.
type Result struct {
Passed bool `json:"passed"`
Checks []Check `json:"checks"`
USReachable bool `json:"us_reachable"`
EUReachable bool `json:"eu_reachable"`
}
// Check represents a single preflight check.
type Check struct {
Name string `json:"name"`
Passed bool `json:"passed"`
Detail string `json:"detail,omitempty"`
Warning bool `json:"warning,omitempty"`
}
// Cloud endpoints (from wallarm-lib.sh)
var euEndpoints = []string{
"api.wallarm.com:443",
"node-data0.eu1.wallarm.com:443",
"node-data1.eu1.wallarm.com:443",
}
var usEndpoints = []string{
"us1.api.wallarm.com:443",
"node-data0.us1.wallarm.com:443",
"node-data1.us1.wallarm.com:443",
}
// Run executes all preflight checks and returns the result.
func Run() Result {
r := Result{Passed: true}
// 1. Root check
if os.Geteuid() != 0 {
r.Checks = append(r.Checks, Check{
Name: "root", Passed: false, Detail: "must run as root for package installation and system config",
})
r.Passed = false
} else {
r.Checks = append(r.Checks, Check{Name: "root", Passed: true})
}
// 2. Init system
initSys := shared.InitSystem()
if initSys != "systemd" {
r.Checks = append(r.Checks, Check{
Name: "init", Passed: false, Detail: fmt.Sprintf("requires systemd, detected: %s", initSys),
})
r.Passed = false
} else {
r.Checks = append(r.Checks, Check{Name: "init", Passed: true, Detail: "systemd"})
}
// 3. Architecture
arch := shared.Arch()
supported := arch == "x86_64" || arch == "aarch64"
r.Checks = append(r.Checks, Check{
Name: "arch", Passed: supported, Detail: arch,
})
if !supported {
r.Passed = false
}
// 4. OS
id, ver := shared.OSInfo()
r.Checks = append(r.Checks, Check{Name: "os", Passed: true, Detail: id + " " + ver})
// 5. Required commands
requiredCmds := []string{"curl", "systemctl", "sed", "mkdir", "rm"}
for _, cmd := range requiredCmds {
ok := shared.CommandExists(cmd)
r.Checks = append(r.Checks, Check{Name: "cmd:" + cmd, Passed: ok})
if !ok {
r.Passed = false
}
}
// 6. Installer reachability
installerOk := shared.HTTPHead("https://storage.googleapis.com/meganode_storage/")
r.Checks = append(r.Checks, Check{
Name: "installer_reachable", Passed: installerOk,
Detail: "storage.googleapis.com",
})
if !installerOk {
r.Checks[len(r.Checks)-1].Warning = true
}
// 7. Cloud endpoints
r.USReachable = checkEndpoints(usEndpoints)
r.EUReachable = checkEndpoints(euEndpoints)
r.Checks = append(r.Checks, Check{
Name: "cloud:US", Passed: r.USReachable,
Detail: fmt.Sprintf("%d/%d reachable", countReachable(usEndpoints), len(usEndpoints)),
})
r.Checks = append(r.Checks, Check{
Name: "cloud:EU", Passed: r.EUReachable,
Detail: fmt.Sprintf("%d/%d reachable", countReachable(euEndpoints), len(euEndpoints)),
})
if !r.USReachable && !r.EUReachable {
r.Passed = false
}
// 8. Disk space (>= 2GB)
free, err := shared.FreeDiskMB("/opt")
if err == nil && free < 2048 {
r.Checks = append(r.Checks, Check{
Name: "disk", Passed: false,
Detail: fmt.Sprintf("%d MB free (need >= 2048 MB)", free),
})
r.Passed = false
} else {
r.Checks = append(r.Checks, Check{Name: "disk", Passed: true, Detail: fmt.Sprintf("%d MB free", free)})
}
// 9. Memory (>= 2GB, warning only)
mem, err := shared.FreeMemoryMB()
if err == nil && mem < 2048 {
r.Checks = append(r.Checks, Check{
Name: "memory", Passed: true, Warning: true,
Detail: fmt.Sprintf("%d MB (2GB+ recommended)", mem),
})
} else {
r.Checks = append(r.Checks, Check{Name: "memory", Passed: true, Detail: fmt.Sprintf("%d MB", mem)})
}
return r
}
func checkEndpoints(endpoints []string) bool {
for _, ep := range endpoints {
host, _ := splitHostPort(ep)
if shared.TCPConnect(host, 443) {
return true
}
}
return false
}
func countReachable(endpoints []string) int {
n := 0
for _, ep := range endpoints {
host, _ := splitHostPort(ep)
if shared.TCPConnect(host, 443) {
n++
}
}
return n
}
func splitHostPort(addr string) (string, string) {
for i := len(addr) - 1; i >= 0; i-- {
if addr[i] == ':' {
return addr[:i], addr[i+1:]
}
}
return addr, ""
}