Добавить установщик, проверку версий и инструкцию деплоя на сервер.
Интерактивная настройка домена и БД, эндпоинты /health и /version, скрипты install/check для Linux и Windows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shop/internal/check"
|
||||
)
|
||||
|
||||
func main() {
|
||||
loadDotEnv()
|
||||
|
||||
ctx := context.Background()
|
||||
report := check.AppInfo()
|
||||
report.Items = append(report.Items, check.ToolVersions(ctx)...)
|
||||
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
report.Items = append(report.Items, check.Item{
|
||||
Name: "database",
|
||||
Status: check.StatusWarn,
|
||||
Detail: "DATABASE_URL не задан — запустите: go run ./cmd/install",
|
||||
})
|
||||
} else {
|
||||
pool, err := pgxpool.New(ctx, dbURL)
|
||||
if err != nil {
|
||||
report.Items = append(report.Items, check.Item{
|
||||
Name: "database",
|
||||
Status: check.StatusError,
|
||||
Detail: err.Error(),
|
||||
})
|
||||
} else {
|
||||
defer pool.Close()
|
||||
dbItems, err := check.Database(ctx, pool)
|
||||
if err != nil {
|
||||
report.Items = append(report.Items, check.Item{
|
||||
Name: "database",
|
||||
Status: check.StatusError,
|
||||
Detail: err.Error(),
|
||||
})
|
||||
} else {
|
||||
report.Items = append(report.Items, dbItems...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
_ = enc.Encode(report)
|
||||
|
||||
fmt.Println()
|
||||
printSummary(report)
|
||||
|
||||
if !report.Healthy() {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"shop/internal/setup"
|
||||
)
|
||||
|
||||
func main() {
|
||||
root, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ошибка: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(root, "docker-compose.yml")); os.IsNotExist(err) {
|
||||
fmt.Fprintln(os.Stderr, "запустите установщик из корня проекта (где docker-compose.yml)")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if _, err := setup.RunInteractive(root); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "установка не завершена: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
+16
-1
@@ -11,10 +11,12 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"shop/internal/check"
|
||||
"shop/internal/config"
|
||||
"shop/internal/database"
|
||||
"shop/internal/handlers"
|
||||
"shop/internal/repository"
|
||||
"shop/internal/version"
|
||||
"shop/internal/web"
|
||||
)
|
||||
|
||||
@@ -31,6 +33,17 @@ func main() {
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
startupReport, err := check.WithDatabase(ctx, pool)
|
||||
if err != nil {
|
||||
log.Fatalf("version check: %v", err)
|
||||
}
|
||||
for _, it := range startupReport.Items {
|
||||
if it.Name == "postgresql" && it.Status == check.StatusWarn {
|
||||
log.Printf("warning: %s — %s", it.Name, it.Detail)
|
||||
}
|
||||
}
|
||||
log.Printf("ShopNova %s | Go %s | PostgreSQL check OK", version.AppVersion, version.GoRuntime())
|
||||
|
||||
tmpl, err := loadTemplates()
|
||||
if err != nil {
|
||||
log.Fatalf("templates: %v", err)
|
||||
@@ -38,6 +51,7 @@ func main() {
|
||||
|
||||
products := repository.NewProductRepository(pool)
|
||||
home := handlers.NewHomeHandler(products, tmpl)
|
||||
health := handlers.NewHealthHandler(pool)
|
||||
|
||||
staticSub, err := fs.Sub(web.StaticFS, "static")
|
||||
if err != nil {
|
||||
@@ -46,7 +60,8 @@ func main() {
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticSub))))
|
||||
mux.Handle("GET /health", http.HandlerFunc(handlers.Health))
|
||||
mux.HandleFunc("GET /health", health.Health)
|
||||
mux.HandleFunc("GET /version", health.Version)
|
||||
mux.Handle("GET /", home)
|
||||
|
||||
srv := &http.Server{
|
||||
|
||||
Reference in New Issue
Block a user