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 }