Compare commits

...

6 Commits

Author SHA1 Message Date
Mohammed Al Sahaf
a22ca046a9
Merge 5245045f44a8b90a3d6c3882dae447c57e2906de into 8861eae22350d9e8f94653db951faf85a50a82da 2025-03-03 11:18:04 -05:00
baruchyahalom
8861eae223
caddytest: Support configuration defaults override (#6850) 2025-03-03 14:35:54 +00:00
Mohammed Al Sahaf
5245045f44
Merge branch 'master' into acme-database 2024-02-24 02:26:57 +03:00
Mohammed Al Sahaf
1a3ba2890b
Merge branch 'master' into acme-database 2024-02-24 02:12:25 +03:00
Mohammed Al Sahaf
998d165b45
simplify getting the *caddy.Replacer line
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-02-11 16:09:51 +03:00
Mohammed Al Sahaf
f94affbc39
acmeserver: support additional database types beside bbolt 2024-02-11 12:49:14 +00:00
2 changed files with 81 additions and 28 deletions

View File

@ -31,8 +31,8 @@ import (
_ "github.com/caddyserver/caddy/v2/modules/standard" _ "github.com/caddyserver/caddy/v2/modules/standard"
) )
// Defaults store any configuration required to make the tests run // Config store any configuration required to make the tests run
type Defaults struct { type Config struct {
// Port we expect caddy to listening on // Port we expect caddy to listening on
AdminPort int AdminPort int
// Certificates we expect to be loaded before attempting to run the tests // Certificates we expect to be loaded before attempting to run the tests
@ -44,7 +44,7 @@ type Defaults struct {
} }
// Default testing values // Default testing values
var Default = Defaults{ var Default = Config{
AdminPort: 2999, // different from what a real server also running on a developer's machine might be AdminPort: 2999, // different from what a real server also running on a developer's machine might be
Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"}, Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
TestRequestTimeout: 5 * time.Second, TestRequestTimeout: 5 * time.Second,
@ -61,6 +61,7 @@ type Tester struct {
Client *http.Client Client *http.Client
configLoaded bool configLoaded bool
t testing.TB t testing.TB
config Config
} }
// NewTester will create a new testing client with an attached cookie jar // NewTester will create a new testing client with an attached cookie jar
@ -78,9 +79,29 @@ func NewTester(t testing.TB) *Tester {
}, },
configLoaded: false, configLoaded: false,
t: t, t: t,
config: Default,
} }
} }
// WithDefaultOverrides this will override the default test configuration with the provided values.
func (tc *Tester) WithDefaultOverrides(overrides Config) *Tester {
if overrides.AdminPort != 0 {
tc.config.AdminPort = overrides.AdminPort
}
if len(overrides.Certificates) > 0 {
tc.config.Certificates = overrides.Certificates
}
if overrides.TestRequestTimeout != 0 {
tc.config.TestRequestTimeout = overrides.TestRequestTimeout
tc.Client.Timeout = overrides.TestRequestTimeout
}
if overrides.LoadRequestTimeout != 0 {
tc.config.LoadRequestTimeout = overrides.LoadRequestTimeout
}
return tc
}
type configLoadError struct { type configLoadError struct {
Response string Response string
} }
@ -113,7 +134,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
return nil return nil
} }
err := validateTestPrerequisites(tc.t) err := validateTestPrerequisites(tc)
if err != nil { if err != nil {
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err) tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
return nil return nil
@ -121,7 +142,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
tc.t.Cleanup(func() { tc.t.Cleanup(func() {
if tc.t.Failed() && tc.configLoaded { if tc.t.Failed() && tc.configLoaded {
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort))
if err != nil { if err != nil {
tc.t.Log("unable to read the current config") tc.t.Log("unable to read the current config")
return return
@ -151,10 +172,10 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
tc.t.Logf("After: %s", rawConfig) tc.t.Logf("After: %s", rawConfig)
} }
client := &http.Client{ client := &http.Client{
Timeout: Default.LoadRequestTimeout, Timeout: tc.config.LoadRequestTimeout,
} }
start := time.Now() start := time.Now()
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig)) req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", tc.config.AdminPort), strings.NewReader(rawConfig))
if err != nil { if err != nil {
tc.t.Errorf("failed to create request. %s", err) tc.t.Errorf("failed to create request. %s", err)
return err return err
@ -205,11 +226,11 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
} }
client := &http.Client{ client := &http.Client{
Timeout: Default.LoadRequestTimeout, Timeout: tc.config.LoadRequestTimeout,
} }
fetchConfig := func(client *http.Client) any { fetchConfig := func(client *http.Client) any {
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort))
if err != nil { if err != nil {
return nil return nil
} }
@ -237,30 +258,30 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
} }
const initConfig = `{ const initConfig = `{
admin localhost:2999 admin localhost:%d
} }
` `
// validateTestPrerequisites ensures the certificates are available in the // validateTestPrerequisites ensures the certificates are available in the
// designated path and Caddy sub-process is running. // designated path and Caddy sub-process is running.
func validateTestPrerequisites(t testing.TB) error { func validateTestPrerequisites(tc *Tester) error {
// check certificates are found // check certificates are found
for _, certName := range Default.Certificates { for _, certName := range tc.config.Certificates {
if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) { if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("caddy integration test certificates (%s) not found", certName) return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
} }
} }
if isCaddyAdminRunning() != nil { if isCaddyAdminRunning(tc) != nil {
// setup the init config file, and set the cleanup afterwards // setup the init config file, and set the cleanup afterwards
f, err := os.CreateTemp("", "") f, err := os.CreateTemp("", "")
if err != nil { if err != nil {
return err return err
} }
t.Cleanup(func() { tc.t.Cleanup(func() {
os.Remove(f.Name()) os.Remove(f.Name())
}) })
if _, err := f.WriteString(initConfig); err != nil { if _, err := f.WriteString(fmt.Sprintf(initConfig, tc.config.AdminPort)); err != nil {
return err return err
} }
@ -271,23 +292,23 @@ func validateTestPrerequisites(t testing.TB) error {
}() }()
// wait for caddy to start serving the initial config // wait for caddy to start serving the initial config
for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- { for retries := 10; retries > 0 && isCaddyAdminRunning(tc) != nil; retries-- {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
} }
} }
// one more time to return the error // one more time to return the error
return isCaddyAdminRunning() return isCaddyAdminRunning(tc)
} }
func isCaddyAdminRunning() error { func isCaddyAdminRunning(tc *Tester) error {
// assert that caddy is running // assert that caddy is running
client := &http.Client{ client := &http.Client{
Timeout: Default.LoadRequestTimeout, Timeout: tc.config.LoadRequestTimeout,
} }
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort))
if err != nil { if err != nil {
return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", Default.AdminPort) return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", tc.config.AdminPort)
} }
resp.Body.Close() resp.Body.Close()

View File

@ -20,6 +20,7 @@ import (
weakrand "math/rand" weakrand "math/rand"
"net" "net"
"net/http" "net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -53,6 +54,10 @@ type Handler struct {
// the default ID is "local". // the default ID is "local".
CA string `json:"ca,omitempty"` CA string `json:"ca,omitempty"`
// The connection string of the database used for
// the account data of the ACME clients
Database string `json:"database,omitempty"`
// The lifetime for issued certificates // The lifetime for issued certificates
Lifetime caddy.Duration `json:"lifetime,omitempty"` Lifetime caddy.Duration `json:"lifetime,omitempty"`
@ -157,6 +162,12 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
return fmt.Errorf("certificate lifetime (%s) should be less than intermediate certificate lifetime (%s)", time.Duration(ash.Lifetime), time.Duration(ca.IntermediateLifetime)) return fmt.Errorf("certificate lifetime (%s) should be less than intermediate certificate lifetime (%s)", time.Duration(ash.Lifetime), time.Duration(ca.IntermediateLifetime))
} }
repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
if !ok {
repl = caddy.NewReplacer()
ctx.Context = context.WithValue(ctx.Context, caddy.ReplacerCtxKey, repl)
}
ash.Database = repl.ReplaceKnown(ash.Database, "")
database, err := ash.openDatabase() database, err := ash.openDatabase()
if err != nil { if err != nil {
return err return err
@ -259,17 +270,38 @@ func (ash Handler) Cleanup() error {
func (ash Handler) openDatabase() (*db.AuthDB, error) { func (ash Handler) openDatabase() (*db.AuthDB, error) {
key := ash.getDatabaseKey() key := ash.getDatabaseKey()
database, loaded, err := databasePool.LoadOrNew(key, func() (caddy.Destructor, error) { database, loaded, err := databasePool.LoadOrNew(key, func() (caddy.Destructor, error) {
dbFolder := filepath.Join(caddy.AppDataDir(), "acme_server", key) var dsn string
dbPath := filepath.Join(dbFolder, "db") dburl, err := url.Parse(ash.Database)
err := os.MkdirAll(dbFolder, 0o755)
if err != nil { if err != nil {
return nil, err
}
if dburl.Scheme == "" {
dburl.Scheme = "bbolt"
}
var dbtype string
switch dburl.Scheme {
case "postgresql", "postgres", "psql":
dbtype = nosql.PostgreSQLDriver // normalize the postgres identifier
dsn = ash.Database
case "mysql":
dbtype = nosql.MySQLDriver
dsn = ash.Database
case "bbolt":
dbtype = nosql.BBoltDriver
dbFolder := filepath.Join(caddy.AppDataDir(), "acme_server", key)
dsn = filepath.Join(dbFolder, "db")
if err := os.MkdirAll(dbFolder, 0o755); err != nil {
return nil, fmt.Errorf("making folder for CA database: %v", err) return nil, fmt.Errorf("making folder for CA database: %v", err)
} }
default:
// Although smallstep/nosql rejects unrecognized database, we
// reject them here to avoid surprises. We also reject 'badger'.
return nil, fmt.Errorf("unsupported database type: %s", dburl.Scheme)
}
dbConfig := &db.Config{ dbConfig := &db.Config{
Type: "bbolt", Type: dbtype,
DataSource: dbPath, DataSource: dsn,
} }
database, err := db.New(dbConfig) database, err := db.New(dbConfig)
return databaseCloser{&database}, err return databaseCloser{&database}, err