mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-03 20:12:58 +00:00
184 lines
3.7 KiB
Go
184 lines
3.7 KiB
Go
package authruntime
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
type Config struct {
|
|
Enabled bool
|
|
NodeBinary string
|
|
Entrypoint string
|
|
Port int
|
|
StartupTimeout time.Duration
|
|
}
|
|
|
|
type Manager struct {
|
|
cmd *exec.Cmd
|
|
done chan error
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
func Start(cfg Config) (*Manager, error) {
|
|
if !cfg.Enabled {
|
|
return nil, nil
|
|
}
|
|
|
|
nodeBinary := cfg.NodeBinary
|
|
if nodeBinary == "" {
|
|
nodeBinary = "node"
|
|
}
|
|
|
|
if _, err := exec.LookPath(nodeBinary); err != nil {
|
|
return nil, fmt.Errorf("better auth node runtime not available: %w", err)
|
|
}
|
|
|
|
scriptPath, err := resolveEntrypoint(cfg.Entrypoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd := exec.Command(nodeBinary, scriptPath)
|
|
cmd.Env = os.Environ()
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
done := make(chan error, 1)
|
|
if err := cmd.Start(); err != nil {
|
|
return nil, fmt.Errorf("failed to start better auth runtime: %w", err)
|
|
}
|
|
|
|
manager := &Manager{
|
|
cmd: cmd,
|
|
done: done,
|
|
}
|
|
|
|
go func() {
|
|
done <- cmd.Wait()
|
|
}()
|
|
|
|
if err := waitForHealth(manager.done, cfg.healthURL(), cfg.StartupTimeout); err != nil {
|
|
_ = manager.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return manager, nil
|
|
}
|
|
|
|
func (m *Manager) Close() error {
|
|
if m == nil || m.cmd == nil || m.cmd.Process == nil {
|
|
return nil
|
|
}
|
|
|
|
var closeErr error
|
|
m.closeOnce.Do(func() {
|
|
_ = m.cmd.Process.Signal(syscall.SIGTERM)
|
|
|
|
select {
|
|
case err := <-m.done:
|
|
closeErr = normalizeWaitErr(err)
|
|
case <-time.After(5 * time.Second):
|
|
_ = m.cmd.Process.Kill()
|
|
closeErr = normalizeWaitErr(<-m.done)
|
|
}
|
|
})
|
|
|
|
return closeErr
|
|
}
|
|
|
|
func waitForHealth(done <-chan error, healthURL string, timeout time.Duration) error {
|
|
if timeout <= 0 {
|
|
timeout = 20 * time.Second
|
|
}
|
|
|
|
deadline := time.Now().Add(timeout)
|
|
client := &http.Client{Timeout: time.Second}
|
|
|
|
for time.Now().Before(deadline) {
|
|
select {
|
|
case err := <-done:
|
|
return fmt.Errorf("better auth runtime exited before becoming healthy: %w", normalizeWaitErr(err))
|
|
default:
|
|
}
|
|
|
|
request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, healthURL, nil)
|
|
if err == nil {
|
|
response, reqErr := client.Do(request)
|
|
if reqErr == nil {
|
|
_ = response.Body.Close()
|
|
if response.StatusCode == http.StatusOK {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
time.Sleep(250 * time.Millisecond)
|
|
}
|
|
|
|
return fmt.Errorf("timed out waiting for better auth runtime health at %s", healthURL)
|
|
}
|
|
|
|
func resolveEntrypoint(entrypoint string) (string, error) {
|
|
if entrypoint == "" {
|
|
entrypoint = "auth/src/server.js"
|
|
}
|
|
|
|
if filepath.IsAbs(entrypoint) {
|
|
if _, err := os.Stat(entrypoint); err != nil {
|
|
return "", fmt.Errorf("better auth entrypoint not found: %w", err)
|
|
}
|
|
return entrypoint, nil
|
|
}
|
|
|
|
cwd, err := os.Getwd()
|
|
if err == nil {
|
|
candidate := filepath.Join(cwd, entrypoint)
|
|
if _, statErr := os.Stat(candidate); statErr == nil {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
|
|
executablePath, err := os.Executable()
|
|
if err == nil {
|
|
candidate := filepath.Join(filepath.Dir(executablePath), entrypoint)
|
|
if _, statErr := os.Stat(candidate); statErr == nil {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("better auth entrypoint %q not found", entrypoint)
|
|
}
|
|
|
|
func normalizeWaitErr(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
var exitErr *exec.ExitError
|
|
if errors.As(err, &exitErr) {
|
|
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
|
if status.Signaled() && (status.Signal() == syscall.SIGTERM || status.Signal() == syscall.SIGKILL) {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (c Config) healthURL() string {
|
|
port := c.Port
|
|
if port == 0 {
|
|
port = 3001
|
|
}
|
|
return fmt.Sprintf("http://127.0.0.1:%d/health", port)
|
|
}
|