feat: implemented token auth
This commit is contained in:
11
.env.example
11
.env.example
@@ -49,6 +49,17 @@ DNS_REPO_BRANCH=main
|
|||||||
# Git remote name.
|
# Git remote name.
|
||||||
DNS_REPO_REMOTE=origin
|
DNS_REPO_REMOTE=origin
|
||||||
|
|
||||||
|
# Optional HTTPS token auth for git pull/push (when remote URL is https://...)
|
||||||
|
# If GIT_AUTH_TOKEN is set, watcher enables non-interactive GIT_ASKPASS mode.
|
||||||
|
# GitHub example:
|
||||||
|
# GIT_AUTH_USERNAME=x-access-token
|
||||||
|
# GIT_AUTH_TOKEN=ghp_xxx
|
||||||
|
# GitLab example:
|
||||||
|
# GIT_AUTH_USERNAME=oauth2
|
||||||
|
# GIT_AUTH_TOKEN=glpat-xxx
|
||||||
|
GIT_AUTH_USERNAME=x-access-token
|
||||||
|
GIT_AUTH_TOKEN=
|
||||||
|
|
||||||
# Directory inside the repository where dynamic zone files are stored.
|
# Directory inside the repository where dynamic zone files are stored.
|
||||||
# The watcher will only write to this directory; static zones are left untouched.
|
# The watcher will only write to this directory; static zones are left untouched.
|
||||||
DNS_REPO_DYNAMIC_DIR=zones-dynamic
|
DNS_REPO_DYNAMIC_DIR=zones-dynamic
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ type Config struct {
|
|||||||
TraefikUsername string
|
TraefikUsername string
|
||||||
TraefikPassword string
|
TraefikPassword string
|
||||||
|
|
||||||
|
GitAuthUsername string
|
||||||
|
GitAuthToken string
|
||||||
|
|
||||||
Zones []string
|
Zones []string
|
||||||
PublicIP string
|
PublicIP string
|
||||||
PublicIPv6 string // empty = no AAAA records
|
PublicIPv6 string // empty = no AAAA records
|
||||||
@@ -41,6 +44,11 @@ func LoadConfig() (*Config, error) {
|
|||||||
cfg.TraefikUsername = os.Getenv("TRAEFIK_USERNAME")
|
cfg.TraefikUsername = os.Getenv("TRAEFIK_USERNAME")
|
||||||
cfg.TraefikPassword = os.Getenv("TRAEFIK_PASSWORD")
|
cfg.TraefikPassword = os.Getenv("TRAEFIK_PASSWORD")
|
||||||
|
|
||||||
|
// Optional HTTPS git authentication credentials.
|
||||||
|
// If GIT_AUTH_TOKEN is set, git commands will run non-interactively via GIT_ASKPASS.
|
||||||
|
cfg.GitAuthUsername = envOrDefault("GIT_AUTH_USERNAME", "x-access-token")
|
||||||
|
cfg.GitAuthToken = os.Getenv("GIT_AUTH_TOKEN")
|
||||||
|
|
||||||
zonesStr := os.Getenv("DNS_ZONES")
|
zonesStr := os.Getenv("DNS_ZONES")
|
||||||
if zonesStr == "" {
|
if zonesStr == "" {
|
||||||
return nil, fmt.Errorf("DNS_ZONES is required (comma-separated list of zones, e.g. example.com,example.net)")
|
return nil, fmt.Errorf("DNS_ZONES is required (comma-separated list of zones, e.g. example.com,example.net)")
|
||||||
|
|||||||
57
git.go
57
git.go
@@ -3,14 +3,21 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// gitRun executes a git command in repoPath, returning a descriptive error
|
// gitRun executes a git command in repoPath, returning a descriptive error
|
||||||
// that includes the combined stdout+stderr on failure.
|
// that includes the combined stdout+stderr on failure.
|
||||||
func gitRun(repoPath string, args ...string) error {
|
func gitRun(cfg *Config, args ...string) error {
|
||||||
cmd := exec.Command("git", append([]string{"-C", repoPath}, args...)...)
|
env, err := gitCommandEnv(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("git", append([]string{"-C", cfg.RepoPath}, args...)...)
|
||||||
|
cmd.Env = env
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("git %s: %w\n%s",
|
return fmt.Errorf("git %s: %w\n%s",
|
||||||
@@ -20,8 +27,14 @@ func gitRun(repoPath string, args ...string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// gitOutput executes a git command and returns its stdout as a string.
|
// gitOutput executes a git command and returns its stdout as a string.
|
||||||
func gitOutput(repoPath string, args ...string) (string, error) {
|
func gitOutput(cfg *Config, args ...string) (string, error) {
|
||||||
cmd := exec.Command("git", append([]string{"-C", repoPath}, args...)...)
|
env, err := gitCommandEnv(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("git", append([]string{"-C", cfg.RepoPath}, args...)...)
|
||||||
|
cmd.Env = env
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var stderr []byte
|
var stderr []byte
|
||||||
@@ -34,14 +47,38 @@ func gitOutput(repoPath string, args ...string) (string, error) {
|
|||||||
return string(out), nil
|
return string(out), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gitCommandEnv builds the environment for git subprocesses.
|
||||||
|
// If GIT_AUTH_TOKEN is provided, non-interactive HTTPS auth is enabled via askpass.
|
||||||
|
func gitCommandEnv(cfg *Config) ([]string, error) {
|
||||||
|
env := os.Environ()
|
||||||
|
if cfg.GitAuthToken == "" {
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("resolve executable path for GIT_ASKPASS: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
env = append(env,
|
||||||
|
"GIT_TERMINAL_PROMPT=0",
|
||||||
|
"GIT_ASKPASS_REQUIRE=force",
|
||||||
|
"GIT_ASKPASS="+exePath,
|
||||||
|
"TDW_GIT_ASKPASS=1",
|
||||||
|
"GIT_AUTH_USERNAME="+cfg.GitAuthUsername,
|
||||||
|
"GIT_AUTH_TOKEN="+cfg.GitAuthToken,
|
||||||
|
)
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GitPull fetches from the remote and rebases the local branch.
|
// GitPull fetches from the remote and rebases the local branch.
|
||||||
// Using --autostash ensures any uncommitted changes are preserved across the rebase
|
// Using --autostash ensures any uncommitted changes are preserved across the rebase
|
||||||
// (should not normally happen but guards against manual edits).
|
// (should not normally happen but guards against manual edits).
|
||||||
func GitPull(cfg *Config) error {
|
func GitPull(cfg *Config) error {
|
||||||
if err := gitRun(cfg.RepoPath, "fetch", "--prune", cfg.RepoRemote); err != nil {
|
if err := gitRun(cfg, "fetch", "--prune", cfg.RepoRemote); err != nil {
|
||||||
return fmt.Errorf("git fetch: %w", err)
|
return fmt.Errorf("git fetch: %w", err)
|
||||||
}
|
}
|
||||||
if err := gitRun(cfg.RepoPath,
|
if err := gitRun(cfg,
|
||||||
"pull", "--rebase", "--autostash",
|
"pull", "--rebase", "--autostash",
|
||||||
cfg.RepoRemote, cfg.RepoBranch,
|
cfg.RepoRemote, cfg.RepoBranch,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -52,7 +89,7 @@ func GitPull(cfg *Config) error {
|
|||||||
|
|
||||||
// GitStatusChanged reports true when the working tree has uncommitted changes.
|
// GitStatusChanged reports true when the working tree has uncommitted changes.
|
||||||
func GitStatusChanged(cfg *Config) (bool, error) {
|
func GitStatusChanged(cfg *Config) (bool, error) {
|
||||||
out, err := gitOutput(cfg.RepoPath, "status", "--porcelain")
|
out, err := gitOutput(cfg, "status", "--porcelain")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -63,17 +100,17 @@ func GitStatusChanged(cfg *Config) (bool, error) {
|
|||||||
// and pushes to the configured remote branch.
|
// and pushes to the configured remote branch.
|
||||||
// Author identity is passed via git -c flags to avoid requiring global git config.
|
// Author identity is passed via git -c flags to avoid requiring global git config.
|
||||||
func GitCommitAndPush(cfg *Config, message string) error {
|
func GitCommitAndPush(cfg *Config, message string) error {
|
||||||
if err := gitRun(cfg.RepoPath, "add", cfg.DynamicDir); err != nil {
|
if err := gitRun(cfg, "add", cfg.DynamicDir); err != nil {
|
||||||
return fmt.Errorf("git add: %w", err)
|
return fmt.Errorf("git add: %w", err)
|
||||||
}
|
}
|
||||||
if err := gitRun(cfg.RepoPath,
|
if err := gitRun(cfg,
|
||||||
"-c", "user.name="+cfg.AuthorName,
|
"-c", "user.name="+cfg.AuthorName,
|
||||||
"-c", "user.email="+cfg.AuthorEmail,
|
"-c", "user.email="+cfg.AuthorEmail,
|
||||||
"commit", "-m", message,
|
"commit", "-m", message,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("git commit: %w", err)
|
return fmt.Errorf("git commit: %w", err)
|
||||||
}
|
}
|
||||||
if err := gitRun(cfg.RepoPath, "push", cfg.RepoRemote, cfg.RepoBranch); err != nil {
|
if err := gitRun(cfg, "push", cfg.RepoRemote, cfg.RepoBranch); err != nil {
|
||||||
return fmt.Errorf("git push: %w", err)
|
return fmt.Errorf("git push: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
31
main.go
31
main.go
@@ -11,12 +11,17 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if maybeHandleGitAskpass() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
Level: slog.LevelInfo,
|
Level: slog.LevelInfo,
|
||||||
})))
|
})))
|
||||||
@@ -32,8 +37,11 @@ func main() {
|
|||||||
"zones", cfg.Zones,
|
"zones", cfg.Zones,
|
||||||
"repo_path", cfg.RepoPath,
|
"repo_path", cfg.RepoPath,
|
||||||
"dynamic_dir", cfg.DynamicDir,
|
"dynamic_dir", cfg.DynamicDir,
|
||||||
|
"git_https_token_enabled", cfg.GitAuthToken != "",
|
||||||
|
"git_auth_username", cfg.GitAuthUsername,
|
||||||
"reconcile_interval", cfg.ReconcileInterval,
|
"reconcile_interval", cfg.ReconcileInterval,
|
||||||
"debounce_delay", cfg.DebounceDelay,
|
"debounce_delay", cfg.DebounceDelay,
|
||||||
|
"cf_auto_ttl", cfg.CloudflareAutoTTL,
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
||||||
@@ -107,6 +115,29 @@ func main() {
|
|||||||
slog.Info("traefik-dns-watcher stopped")
|
slog.Info("traefik-dns-watcher stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maybeHandleGitAskpass serves username/password for git HTTPS auth in non-interactive mode.
|
||||||
|
// This process mode is only enabled for git child processes that set TDW_GIT_ASKPASS=1.
|
||||||
|
func maybeHandleGitAskpass() bool {
|
||||||
|
enabled, _ := strconv.ParseBool(os.Getenv("TDW_GIT_ASKPASS"))
|
||||||
|
if !enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := ""
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
prompt = strings.ToLower(os.Args[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(prompt, "username") {
|
||||||
|
fmt.Fprint(os.Stdout, os.Getenv("GIT_AUTH_USERNAME"))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// For password/token prompts, return token by default.
|
||||||
|
fmt.Fprint(os.Stdout, os.Getenv("GIT_AUTH_TOKEN"))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// watchDockerEvents connects to the Docker daemon and forwards container lifecycle
|
// watchDockerEvents connects to the Docker daemon and forwards container lifecycle
|
||||||
// events to the trigger function. Reconnects with exponential backoff on failure.
|
// events to the trigger function. Reconnects with exponential backoff on failure.
|
||||||
func watchDockerEvents(ctx context.Context, trigger func()) {
|
func watchDockerEvents(ctx context.Context, trigger func()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user