feat: add resilient updates and access controls
Container-Image veröffentlichen / container (push) Has been cancelled

This commit is contained in:
admin committed 2026-08-28 14:02:07 +02:00
1 parent a000152423
commit f02ec9977c
41 files changed
+3277 -151

No files matched your search

+27 -1
View File
@@ -11,6 +11,7 @@ import (
"syscall"
"time"
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/appupdate"
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/config"
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/hetzner"
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/ipresolve"
@@ -53,6 +54,10 @@ func run(logger *slog.Logger) error {
}
provider := hetzner.New(cfg.HCloudBaseURL)
updater := scheduler.New(database, provider, ipresolve.New(), cipher, logger)
softwareUpdates, err := appupdate.New(database, cipher, cfg.UpdateAPIURL, cfg.UpdateAgentSock, cfg.UpdateInterval, logger)
if err != nil {
return err
}
settings, err := database.GetSettings(ctx)
if err != nil {
return err
@@ -65,7 +70,7 @@ func run(logger *slog.Logger) error {
logger.Info("removed AAAA records for disabled IPv6", "count", deleted)
}
}
serverUI, err := web.New(cfg, database, cipher, updater, logger)
serverUI, err := web.New(cfg, database, cipher, updater, softwareUpdates, logger)
if err != nil {
return err
}
@@ -73,6 +78,8 @@ func run(logger *slog.Logger) error {
appCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
updater.Start(appCtx)
softwareUpdates.Start(appCtx)
go pruneStore(appCtx, database, logger)
httpServer := &http.Server{
Addr: cfg.Address,
Handler: serverUI.Handler(),
@@ -80,6 +87,7 @@ func run(logger *slog.Logger) error {
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 16 << 10,
}
serverErrors := make(chan error, 1)
go func() {
@@ -100,6 +108,24 @@ func run(logger *slog.Logger) error {
}
}
func pruneStore(ctx context.Context, database *store.Store, logger *slog.Logger) {
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
pruneCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
err := database.Prune(pruneCtx)
cancel()
if err != nil && !errors.Is(err, context.Canceled) {
logger.Warn("prune old local data", "error", err)
}
}
}
}
func healthcheck() int {
url := strings.TrimSpace(os.Getenv("APP_HEALTHCHECK_URL"))
if url == "" {
+88
View File
@@ -0,0 +1,88 @@
package main
import (
"context"
"errors"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/updateagent"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
if err := run(logger); err != nil {
logger.Error("update agent stopped", "error", err)
os.Exit(1)
}
}
func run(logger *slog.Logger) error {
socket := env("UPDATE_AGENT_SOCKET", "/run/domain-control-update/agent.sock")
dockerSocket := env("UPDATE_DOCKER_SOCKET", "/var/run/docker.sock")
image := env("UPDATE_IMAGE_REPOSITORY", "git.j-mahler.de/admin/domain-control-webinterface-prod")
targetLabel := env("UPDATE_TARGET_LABEL", "de.j-mahler.domain-control.role=app")
directory := filepath.Dir(socket)
if err := os.MkdirAll(directory, 0o711); err != nil {
return err
}
if err := os.Chmod(directory, 0o711); err != nil {
return err
}
if err := os.Remove(socket); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
listener, err := net.Listen("unix", socket)
if err != nil {
return err
}
defer listener.Close()
defer os.Remove(socket)
// The container runs as uid 0 with gid 10001. Group-only access lets the
// unprivileged web process connect without exposing the socket to others.
if err := os.Chmod(socket, 0o660); err != nil {
return err
}
runner := updateagent.NewDockerRunner(dockerSocket, image, targetLabel, logger)
controller := updateagent.NewController(runner, logger)
server := &http.Server{
Handler: controller.Handler(),
ReadHeaderTimeout: 3 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 15 * time.Second,
MaxHeaderBytes: 8 << 10,
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
serverErrors := make(chan error, 1)
go func() {
logger.Info("update agent ready", "socket", socket)
serverErrors <- server.Serve(listener)
}()
select {
case err := <-serverErrors:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return server.Shutdown(shutdownCtx)
}
}
func env(name, fallback string) string {
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
return value
}
return fallback
}