Добавил сборку

This commit is contained in:
2026-06-14 16:10:21 +03:00
parent 08b707f602
commit 4af3ad2dde
5 changed files with 139 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
package main
import (
"context"
"flag"
"fmt"
"net"
"net/http"
"time"
"git.vakhrushev.me/av/jellybit/internal/config"
)
// runHealthcheck дёргает /healthz локального сервиса и завершается с кодом 0
// при 200, иначе ненулевым. Нужен для HEALTHCHECK в distroless-образе, где
// нет shell/curl: docker зовёт сам бинарь.
func runHealthcheck(args []string) error {
fs := flag.NewFlagSet("healthcheck", flag.ContinueOnError)
configPath := fs.String("config", "/data/config.toml", "путь к config.toml")
if err := fs.Parse(args); err != nil {
return err
}
cfg, err := config.Load(*configPath)
if err != nil {
return err
}
// listen вида ":8080" или "127.0.0.1:8080" → стучимся на localhost:<port>.
_, port, err := net.SplitHostPort(cfg.HTTP.Listen)
if err != nil {
return fmt.Errorf("parse http.listen %q: %w", cfg.HTTP.Listen, err)
}
url := "http://127.0.0.1:" + port + "/healthz"
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("healthcheck request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("healthcheck: status %d", resp.StatusCode)
}
return nil
}
+62
View File
@@ -0,0 +1,62 @@
package main
import (
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"testing"
)
// serveHealthz поднимает http-сервер на свободном порту, отдавая указанный
// статус на /healthz. Возвращает порт и стоп-функцию.
func serveHealthz(t *testing.T, status int) int {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(status)
})
srv := &http.Server{Handler: mux}
go func() { _ = srv.Serve(ln) }()
t.Cleanup(func() { _ = srv.Close() })
return ln.Addr().(*net.TCPAddr).Port
}
func writeConfig(t *testing.T, port int) string {
t.Helper()
path := filepath.Join(t.TempDir(), "config.toml")
content := "[http]\nlisten = \"127.0.0.1:" + strconv.Itoa(port) + "\"\n"
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
return path
}
func TestHealthcheck_OK(t *testing.T) {
port := serveHealthz(t, http.StatusOK)
if err := runHealthcheck([]string{"--config", writeConfig(t, port)}); err != nil {
t.Errorf("healthcheck: %v", err)
}
}
func TestHealthcheck_BadStatus(t *testing.T) {
port := serveHealthz(t, http.StatusServiceUnavailable)
if err := runHealthcheck([]string{"--config", writeConfig(t, port)}); err == nil {
t.Error("ожидалась ошибка при 503")
}
}
func TestHealthcheck_NoServer(t *testing.T) {
// Порт, на котором никто не слушает (берём свободный и закрываем).
ln, _ := net.Listen("tcp", "127.0.0.1:0")
port := ln.Addr().(*net.TCPAddr).Port
_ = ln.Close()
if err := runHealthcheck([]string{"--config", writeConfig(t, port)}); err == nil {
t.Error("ожидалась ошибка без сервера")
}
}
+3
View File
@@ -4,6 +4,7 @@
//
// jellybit [serve] --config <path> запустить сервис (по умолчанию)
// jellybit add <magnet> [--context] добавить загрузку через REST API сервиса
// jellybit healthcheck --config <p> проверить /healthz (для docker HEALTHCHECK)
package main
import (
@@ -27,6 +28,8 @@ func main() {
err = runServe(args)
case "add":
err = runAdd(args)
case "healthcheck":
err = runHealthcheck(args)
default:
_, _ = os.Stderr.WriteString("unknown command: " + cmd + "\n")
os.Exit(2)