b6d97f8a73
Co-authored-by: Cursor <cursoragent@cursor.com>
193 lines
3.9 KiB
Go
193 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"shop/internal/check"
|
|
)
|
|
|
|
func main() {
|
|
loadDotEnv()
|
|
|
|
skipDB := os.Getenv("CHECK_SKIP_DB") == "1"
|
|
hostTools := os.Getenv("CHECK_HOST_TOOLS") != "0"
|
|
|
|
ctx := context.Background()
|
|
report := check.AppInfo()
|
|
if hostTools {
|
|
report.Items = append(report.Items, check.ToolVersions(ctx)...)
|
|
}
|
|
|
|
dbURL := os.Getenv("DATABASE_URL")
|
|
if skipDB {
|
|
report.Items = append(report.Items, check.Item{
|
|
Name: "database",
|
|
Status: check.StatusWarn,
|
|
Detail: "проверка отложена — сначала выполните: docker compose up -d, затем ./check.sh --after-start",
|
|
})
|
|
} else if dbURL == "" {
|
|
report.Items = append(report.Items, check.Item{
|
|
Name: "database",
|
|
Status: check.StatusWarn,
|
|
Detail: "DATABASE_URL не задан — запустите: ./install.sh",
|
|
})
|
|
} else {
|
|
appendDBChecks(ctx, &report, dbURL)
|
|
}
|
|
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
_ = enc.Encode(report)
|
|
|
|
fmt.Println()
|
|
printSummary(report)
|
|
|
|
if !report.Healthy() {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func appendDBChecks(ctx context.Context, report *check.Report, dbURL string) {
|
|
pool, err := pgxpool.New(ctx, dbURL)
|
|
if err != nil {
|
|
report.Items = append(report.Items, dbCheckItem(err, dbURL))
|
|
return
|
|
}
|
|
defer pool.Close()
|
|
|
|
dbItems, err := check.Database(ctx, pool)
|
|
if err != nil {
|
|
report.Items = append(report.Items, check.Item{
|
|
Name: "database",
|
|
Status: dbCheckStatus(err, dbURL),
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
report.Items = append(report.Items, dbItems...)
|
|
}
|
|
|
|
func dbCheckItem(err error, dbURL string) check.Item {
|
|
return check.Item{
|
|
Name: "database",
|
|
Status: dbCheckStatus(err, dbURL),
|
|
Detail: err.Error(),
|
|
}
|
|
}
|
|
|
|
func dbCheckStatus(err error, dbURL string) check.Status {
|
|
if err == nil {
|
|
return check.StatusOK
|
|
}
|
|
msg := strings.ToLower(err.Error())
|
|
if isDockerInternalHost(dbURL) && (strings.Contains(msg, "no such host") ||
|
|
strings.Contains(msg, "name or service not known") ||
|
|
strings.Contains(msg, "hostname resolving")) {
|
|
return check.StatusWarn
|
|
}
|
|
return check.StatusError
|
|
}
|
|
|
|
func isDockerInternalHost(dbURL string) bool {
|
|
u, err := url.Parse(dbURL)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
host := u.Hostname()
|
|
return host == "postgres" || host == "db" || host == "shop-postgres"
|
|
}
|
|
|
|
func printSummary(r check.Report) {
|
|
fmt.Printf("ShopNova %s | %s\n\n", r.AppVersion, r.GoVersion)
|
|
for _, it := range r.Items {
|
|
mark := "✓"
|
|
switch it.Status {
|
|
case check.StatusWarn:
|
|
mark = "!"
|
|
case check.StatusError:
|
|
mark = "✗"
|
|
}
|
|
line := fmt.Sprintf(" %s %-18s %s", mark, it.Name+":", it.Detail)
|
|
if it.Expected != "" {
|
|
line += " (ожидается " + it.Expected + ")"
|
|
}
|
|
fmt.Println(line)
|
|
}
|
|
}
|
|
|
|
func loadDotEnv() {
|
|
root, _ := os.Getwd()
|
|
path := filepath.Join(root, ".env")
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, line := range splitLines(string(data)) {
|
|
line = trimComment(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
k, v, ok := splitKV(line)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if os.Getenv(k) == "" {
|
|
_ = os.Setenv(k, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func splitLines(s string) []string {
|
|
var lines []string
|
|
start := 0
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '\n' {
|
|
lines = append(lines, s[start:i])
|
|
start = i + 1
|
|
}
|
|
}
|
|
if start < len(s) {
|
|
lines = append(lines, s[start:])
|
|
}
|
|
return lines
|
|
}
|
|
|
|
func trimComment(s string) string {
|
|
if i := indexByte(s, '#'); i >= 0 {
|
|
s = s[:i]
|
|
}
|
|
for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\r') {
|
|
s = s[:len(s)-1]
|
|
}
|
|
for len(s) > 0 && s[0] == ' ' {
|
|
s = s[1:]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func splitKV(s string) (string, string, bool) {
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '=' {
|
|
return s[:i], s[i+1:], true
|
|
}
|
|
}
|
|
return "", "", false
|
|
}
|
|
|
|
func indexByte(s string, c byte) int {
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == c {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|