initial commit
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
base string // resolved data base dir
|
||||
)
|
||||
|
||||
// DataDir returns the base directory for data (db.bolt, certs, logs, www, etc).
|
||||
// Respects DATA_DIR env (absolute or relative). Falls back to "data" relative to CWD.
|
||||
func DataDir() string {
|
||||
once.Do(func() {
|
||||
if d := os.Getenv("DATA_DIR"); d != "" {
|
||||
base = d
|
||||
return
|
||||
}
|
||||
// default relative to cwd
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
wd = "."
|
||||
}
|
||||
base = filepath.Join(wd, "data")
|
||||
})
|
||||
return base
|
||||
}
|
||||
|
||||
// Resolve returns an absolute or relative path under the data dir for the given subpath (e.g. "db.bolt", "certs", "www").
|
||||
// If the corresponding *_DIR env is set it is used as base instead.
|
||||
func Resolve(sub string) string {
|
||||
// Allow per-subdir override e.g. CERTS_DIR, WWW_DIR, LOGS_DIR, CHALLENGE_DIR
|
||||
key := ""
|
||||
switch sub {
|
||||
case "db.bolt":
|
||||
key = "DATA_DIR"
|
||||
case "certs", "custom_ssl":
|
||||
key = "CERTS_DIR"
|
||||
case "logs":
|
||||
key = "LOGS_DIR"
|
||||
case "letsencrypt-acme-challenge", "challenge":
|
||||
key = "CHALLENGE_DIR"
|
||||
case "www":
|
||||
if d := os.Getenv("WWW_DIR"); d != "" {
|
||||
return filepath.Join(d, "") // allow override to point directly
|
||||
}
|
||||
key = "DATA_DIR"
|
||||
case "html":
|
||||
if d := os.Getenv("HTML_DIR"); d != "" {
|
||||
return filepath.Join(d, "")
|
||||
}
|
||||
key = "DATA_DIR"
|
||||
default:
|
||||
key = "DATA_DIR"
|
||||
}
|
||||
|
||||
dir := DataDir()
|
||||
if key != "" {
|
||||
if override := os.Getenv(key); override != "" {
|
||||
dir = override
|
||||
}
|
||||
}
|
||||
return filepath.Join(dir, sub)
|
||||
}
|
||||
|
||||
// EnsureDataDirs creates the expected subdirs under the resolved data location (and www inside data by default).
|
||||
// Also pre-creates the acme-challenge subdir layout that lego webroot + our handler expect.
|
||||
func EnsureDataDirs() error {
|
||||
dirs := []string{
|
||||
Resolve(""),
|
||||
Resolve("certs"),
|
||||
Resolve("logs"),
|
||||
Resolve("letsencrypt-acme-challenge"),
|
||||
Resolve("www"), // data/www by default
|
||||
}
|
||||
for _, d := range dirs {
|
||||
if err := os.MkdirAll(d, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// full layout for http-01 challenges written by lego
|
||||
acme := filepath.Join(Resolve("letsencrypt-acme-challenge"), ".well-known", "acme-challenge")
|
||||
if err := os.MkdirAll(acme, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetForTest clears the cached data dir (sync.Once) so tests can change
|
||||
// DATA_DIR / *_DIR envs via t.Setenv and observe fresh resolution.
|
||||
// Call at the start of such tests. This is the supported way to make
|
||||
// config testable without globals leaking across tests.
|
||||
func ResetForTest() {
|
||||
once = sync.Once{}
|
||||
base = ""
|
||||
}
|
||||
|
||||
// IsDevelopment reports whether PROXY_MODE=development (or "dev").
|
||||
// Defaults to false (production mode).
|
||||
// When true, all letsencrypt certs use self-signed test certs (via the cert manager's
|
||||
// issueTestCert path). No domain tricks (.localtest etc.) are used. In production
|
||||
// (default) real LE issuance is always attempted.
|
||||
func IsDevelopment() bool {
|
||||
mode := strings.ToLower(os.Getenv("PROXY_MODE"))
|
||||
return mode == "development" || mode == "dev"
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDataDir(t *testing.T) {
|
||||
ResetForTest()
|
||||
t.Setenv("DATA_DIR", "")
|
||||
// default should be data under cwd
|
||||
d := DataDir()
|
||||
if d == "" {
|
||||
t.Fatal("DataDir empty")
|
||||
}
|
||||
// once behavior: change env shouldn't affect without reset
|
||||
t.Setenv("DATA_DIR", "/tmp/should-not-affect")
|
||||
d2 := DataDir()
|
||||
if d != d2 {
|
||||
t.Error("DataDir ignored Once")
|
||||
}
|
||||
|
||||
ResetForTest()
|
||||
t.Setenv("DATA_DIR", "/tmp/testdata123")
|
||||
d3 := DataDir()
|
||||
if d3 != "/tmp/testdata123" {
|
||||
t.Errorf("DATA_DIR override failed: got %s", d3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
ResetForTest()
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("DATA_DIR", tmp)
|
||||
|
||||
tests := []struct {
|
||||
sub string
|
||||
wantBase string // suffix check
|
||||
}{
|
||||
{"db.bolt", filepath.Join(tmp, "db.bolt")},
|
||||
{"certs", filepath.Join(tmp, "certs")},
|
||||
{"logs", filepath.Join(tmp, "logs")},
|
||||
{"letsencrypt-acme-challenge", filepath.Join(tmp, "letsencrypt-acme-challenge")},
|
||||
{"www", filepath.Join(tmp, "www")},
|
||||
{"unknown", filepath.Join(tmp, "unknown")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := Resolve(tt.sub)
|
||||
if got != tt.wantBase {
|
||||
t.Errorf("Resolve(%q) = %q want %q", tt.sub, got, tt.wantBase)
|
||||
}
|
||||
}
|
||||
|
||||
// override CERTS_DIR (uses as base + join sub per current impl)
|
||||
ResetForTest()
|
||||
certsBase := filepath.Join(tmp, "mycerts-base")
|
||||
t.Setenv("CERTS_DIR", certsBase)
|
||||
t.Setenv("DATA_DIR", tmp)
|
||||
wantCerts := filepath.Join(certsBase, "certs")
|
||||
if got := Resolve("certs"); got != wantCerts {
|
||||
t.Errorf("CERTS_DIR override: got %s want %s", got, wantCerts)
|
||||
}
|
||||
|
||||
// WWW_DIR / HTML_DIR (special case returns dir directly; overrides conditioned on exact sub to avoid cross-contamination)
|
||||
ResetForTest()
|
||||
wwwDir := filepath.Join(tmp, "mywww")
|
||||
t.Setenv("WWW_DIR", wwwDir)
|
||||
if got := Resolve("www"); got != wwwDir {
|
||||
t.Errorf("WWW_DIR: got %s", got)
|
||||
}
|
||||
ResetForTest()
|
||||
htmlDir := filepath.Join(tmp, "myhtml")
|
||||
t.Setenv("HTML_DIR", htmlDir)
|
||||
if got := Resolve("html"); got != htmlDir {
|
||||
t.Errorf("HTML_DIR: got %s", got)
|
||||
}
|
||||
|
||||
// mixed overrides: only HTML set should not affect Resolve("www") (falls to DATA_DIR/www); vice-versa
|
||||
// (explicitly clear the other *_DIR to counter t.Setenv lingering within one TestResolve func)
|
||||
ResetForTest()
|
||||
t.Setenv("WWW_DIR", "")
|
||||
t.Setenv("HTML_DIR", htmlDir)
|
||||
t.Setenv("DATA_DIR", tmp)
|
||||
if got := Resolve("www"); got != filepath.Join(tmp, "www") {
|
||||
t.Errorf("mixed: only HTML should not affect www: got %s", got)
|
||||
}
|
||||
ResetForTest()
|
||||
t.Setenv("HTML_DIR", "")
|
||||
t.Setenv("WWW_DIR", wwwDir)
|
||||
t.Setenv("DATA_DIR", tmp)
|
||||
if got := Resolve("html"); got != filepath.Join(tmp, "html") {
|
||||
t.Errorf("mixed: only WWW should not affect html: got %s", got)
|
||||
}
|
||||
|
||||
// CHALLENGE_DIR (uses as base + join per impl)
|
||||
ResetForTest()
|
||||
chBase := filepath.Join(tmp, "ch-base")
|
||||
t.Setenv("CHALLENGE_DIR", chBase)
|
||||
wantCh := filepath.Join(chBase, "letsencrypt-acme-challenge")
|
||||
if got := Resolve("letsencrypt-acme-challenge"); got != wantCh {
|
||||
t.Errorf("CHALLENGE: got %s want %s", got, wantCh)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDataDirs(t *testing.T) {
|
||||
ResetForTest()
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("DATA_DIR", tmp)
|
||||
|
||||
if err := EnsureDataDirs(); err != nil {
|
||||
t.Fatalf("EnsureDataDirs: %v", err)
|
||||
}
|
||||
|
||||
// verify layout
|
||||
dirs := []string{
|
||||
tmp,
|
||||
filepath.Join(tmp, "certs"),
|
||||
filepath.Join(tmp, "logs"),
|
||||
filepath.Join(tmp, "letsencrypt-acme-challenge"),
|
||||
filepath.Join(tmp, "www"),
|
||||
filepath.Join(tmp, "letsencrypt-acme-challenge", ".well-known", "acme-challenge"),
|
||||
}
|
||||
for _, d := range dirs {
|
||||
if fi, err := os.Stat(d); err != nil || !fi.IsDir() {
|
||||
t.Errorf("expected dir %s: %v", d, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDevelopment(t *testing.T) {
|
||||
ResetForTest()
|
||||
t.Setenv("PROXY_MODE", "")
|
||||
if IsDevelopment() {
|
||||
t.Error("default should be production (not dev)")
|
||||
}
|
||||
|
||||
t.Setenv("PROXY_MODE", "development")
|
||||
if !IsDevelopment() {
|
||||
t.Error("PROXY_MODE=development should be dev")
|
||||
}
|
||||
|
||||
t.Setenv("PROXY_MODE", "dev")
|
||||
if !IsDevelopment() {
|
||||
t.Error("PROXY_MODE=dev should be dev")
|
||||
}
|
||||
|
||||
t.Setenv("PROXY_MODE", "production")
|
||||
if IsDevelopment() {
|
||||
t.Error("PROXY_MODE=production should not be dev")
|
||||
}
|
||||
|
||||
// case insensitive
|
||||
t.Setenv("PROXY_MODE", "DEVELOPMENT")
|
||||
if !IsDevelopment() {
|
||||
t.Error("case insensitive")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user