Добавил сборку
This commit is contained in:
@@ -8,4 +8,10 @@ FROM gcr.io/distroless/static-debian12
|
|||||||
COPY jellybit /usr/local/bin/jellybit
|
COPY jellybit /usr/local/bin/jellybit
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# В distroless нет shell/curl — проверку делает сам бинарь (порт берёт из
|
||||||
|
# /data/config.toml). compose может переопределить параметры healthcheck.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD ["/usr/local/bin/jellybit", "healthcheck"]
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/jellybit", "--config", "/data/config.toml"]
|
ENTRYPOINT ["/usr/local/bin/jellybit", "--config", "/data/config.toml"]
|
||||||
|
|||||||
@@ -71,3 +71,20 @@ task image # docker-образ из готового
|
|||||||
|
|
||||||
Сборка здесь → готовый бинарь копируется на медиа-сервер umbar
|
Сборка здесь → готовый бинарь копируется на медиа-сервер umbar
|
||||||
(`/home/av/projects/private/umbar`). Деплой-обвязка живёт в umbar.
|
(`/home/av/projects/private/umbar`). Деплой-обвязка живёт в umbar.
|
||||||
|
|
||||||
|
Артефакты этого репозитория: статический бинарь (`task build`) и `Dockerfile`
|
||||||
|
(упаковка в `distroless/static`). Образ собирается **на сервере** из
|
||||||
|
доставленного бинаря — Go-тулчейн на сервере не нужен. В distroless нет
|
||||||
|
shell/curl, поэтому HEALTHCHECK зовёт сам бинарь: `jellybit healthcheck`
|
||||||
|
(GET `/healthz` по порту из конфига, exit 0/1).
|
||||||
|
|
||||||
|
Деплой одной командой из umbar (собирает бинарь локально, доставляет, строит
|
||||||
|
образ на сервере, рендерит конфиг с секретами из vault, поднимает compose):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
inv pl -- jellybit # umbar/playbook-jellybit.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Контейнер: `user 1000:1000`, порт `8080` на хост, mount `/srv/media` (единая
|
||||||
|
песочница для хардлинков) + data-том с `config.toml`/SQLite, к qBittorrent —
|
||||||
|
через `host.docker.internal`.
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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("ожидалась ошибка без сервера")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
//
|
//
|
||||||
// jellybit [serve] --config <path> запустить сервис (по умолчанию)
|
// jellybit [serve] --config <path> запустить сервис (по умолчанию)
|
||||||
// jellybit add <magnet> [--context] добавить загрузку через REST API сервиса
|
// jellybit add <magnet> [--context] добавить загрузку через REST API сервиса
|
||||||
|
// jellybit healthcheck --config <p> проверить /healthz (для docker HEALTHCHECK)
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -27,6 +28,8 @@ func main() {
|
|||||||
err = runServe(args)
|
err = runServe(args)
|
||||||
case "add":
|
case "add":
|
||||||
err = runAdd(args)
|
err = runAdd(args)
|
||||||
|
case "healthcheck":
|
||||||
|
err = runHealthcheck(args)
|
||||||
default:
|
default:
|
||||||
_, _ = os.Stderr.WriteString("unknown command: " + cmd + "\n")
|
_, _ = os.Stderr.WriteString("unknown command: " + cmd + "\n")
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
|
|||||||
Reference in New Issue
Block a user