mirror of
https://github.com/tiennm99/keepalive.git
synced 2026-10-14 00:16:49 +00:00
feat(config): support multi-service yaml config
This commit is contained in:
1 parent
655db8dcbf
commit
f3a23ee91e
19 files changed
+709
-268
No files matched your search
+22
-5
@@ -17,16 +17,33 @@ type Adapter interface {
|
||||
Close(ctx context.Context) error
|
||||
}
|
||||
|
||||
type Factory func() (Adapter, error)
|
||||
type Config map[string]string
|
||||
|
||||
func (c Config) Required(name string) (string, error) {
|
||||
v, ok := c[name]
|
||||
if !ok || v == "" {
|
||||
return "", fmt.Errorf("config %s is required", name)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (c Config) Optional(name, def string) string {
|
||||
if v, ok := c[name]; ok && v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
type Factory func(Config) (Adapter, error)
|
||||
|
||||
var Registry = map[string]Factory{}
|
||||
|
||||
func New(dbType string) (Adapter, error) {
|
||||
f, ok := Registry[dbType]
|
||||
func New(adapterType string, cfg Config) (Adapter, error) {
|
||||
f, ok := Registry[adapterType]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown adapter %q (known: %v)", dbType, Known())
|
||||
return nil, fmt.Errorf("unknown adapter %q (known: %v)", adapterType, Known())
|
||||
}
|
||||
return f()
|
||||
return f(cfg)
|
||||
}
|
||||
|
||||
func Known() []string {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigRequiredReturnsConfiguredValue(t *testing.T) {
|
||||
cfg := Config{"url": "redis://127.0.0.1:6379"}
|
||||
|
||||
got, err := cfg.Required("url")
|
||||
if err != nil {
|
||||
t.Fatalf("Required returned error: %v", err)
|
||||
}
|
||||
if got != "redis://127.0.0.1:6379" {
|
||||
t.Fatalf("Required() = %q, want configured value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigRequiredReportsMissingName(t *testing.T) {
|
||||
cfg := Config{}
|
||||
|
||||
_, err := cfg.Required("url")
|
||||
if err == nil {
|
||||
t.Fatal("Required returned nil error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "url") {
|
||||
t.Fatalf("error %q does not include config key", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigOptionalReturnsConfiguredValue(t *testing.T) {
|
||||
cfg := Config{"counter_key": "custom"}
|
||||
|
||||
got := cfg.Optional("counter_key", "counter")
|
||||
if got != "custom" {
|
||||
t.Fatalf("Optional() = %q, want custom", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigOptionalUsesDefault(t *testing.T) {
|
||||
cfg := Config{}
|
||||
|
||||
got := cfg.Optional("counter_key", "counter")
|
||||
if got != "counter" {
|
||||
t.Fatalf("Optional() = %q, want counter", got)
|
||||
}
|
||||
}
|
||||
+48
-34
@@ -8,58 +8,72 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
Registry["couchbase"] = func() (Adapter, error) { return &couchbaseAdapter{}, nil }
|
||||
Registry["couchbase"] = func(cfg Config) (Adapter, error) {
|
||||
conn, err := cfg.Required("connection_string")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := cfg.Required("username")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pass, err := cfg.Required("password")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bucket, err := cfg.Required("bucket_name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope, err := cfg.Required("scope_name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
collName, err := cfg.Required("collection_name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &couchbaseAdapter{
|
||||
conn: conn,
|
||||
user: user,
|
||||
pass: pass,
|
||||
bucket: bucket,
|
||||
scope: scope,
|
||||
collName: collName,
|
||||
docID: cfg.Optional("counter_key", "counter"),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type couchbaseAdapter struct {
|
||||
cluster *gocb.Cluster
|
||||
coll *gocb.Collection
|
||||
docID string
|
||||
cluster *gocb.Cluster
|
||||
coll *gocb.Collection
|
||||
conn string
|
||||
user string
|
||||
pass string
|
||||
bucket string
|
||||
scope string
|
||||
collName string
|
||||
docID string
|
||||
}
|
||||
|
||||
func (a *couchbaseAdapter) Connect(_ context.Context) error {
|
||||
conn, err := envOrFail("KEEPALIVE_COUCHBASE_CONNECTION_STRING")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := envOrFail("KEEPALIVE_COUCHBASE_USERNAME")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pass, err := envOrFail("KEEPALIVE_COUCHBASE_PASSWORD")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bucket, err := envOrFail("KEEPALIVE_COUCHBASE_BUCKET_NAME")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scope, err := envOrFail("KEEPALIVE_COUCHBASE_SCOPE_NAME")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
collName, err := envOrFail("KEEPALIVE_COUCHBASE_COLLECTION_NAME")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := gocb.ClusterOptions{
|
||||
Authenticator: gocb.PasswordAuthenticator{Username: user, Password: pass},
|
||||
Authenticator: gocb.PasswordAuthenticator{Username: a.user, Password: a.pass},
|
||||
}
|
||||
if err := opts.ApplyProfile(gocb.ClusterConfigProfileWanDevelopment); err != nil {
|
||||
return err
|
||||
}
|
||||
cluster, err := gocb.Connect(conn, opts)
|
||||
cluster, err := gocb.Connect(a.conn, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := cluster.Bucket(bucket)
|
||||
b := cluster.Bucket(a.bucket)
|
||||
if err := b.WaitUntilReady(5*time.Second, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
a.cluster = cluster
|
||||
a.coll = b.Scope(scope).Collection(collName)
|
||||
a.docID = envOr("KEEPALIVE_COUNTER_KEY", "counter")
|
||||
a.coll = b.Scope(a.scope).Collection(a.collName)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func envOrFail(name string) (string, error) {
|
||||
v, ok := os.LookupEnv(name)
|
||||
if !ok || v == "" {
|
||||
return "", fmt.Errorf("env %s is required", name)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func envOr(name, def string) string {
|
||||
if v, ok := os.LookupEnv(name); ok && v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnvOrFailReturnsConfiguredValue(t *testing.T) {
|
||||
t.Setenv("KEEPALIVE_TEST_REQUIRED", "configured")
|
||||
|
||||
got, err := envOrFail("KEEPALIVE_TEST_REQUIRED")
|
||||
if err != nil {
|
||||
t.Fatalf("envOrFail returned error: %v", err)
|
||||
}
|
||||
if got != "configured" {
|
||||
t.Fatalf("envOrFail() = %q, want configured", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvOrFailReportsMissingName(t *testing.T) {
|
||||
_, err := envOrFail("KEEPALIVE_TEST_MISSING_REQUIRED")
|
||||
if err == nil {
|
||||
t.Fatal("envOrFail returned nil error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "KEEPALIVE_TEST_MISSING_REQUIRED") {
|
||||
t.Fatalf("error %q does not include env name", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvOrReturnsConfiguredValue(t *testing.T) {
|
||||
t.Setenv("KEEPALIVE_TEST_OPTIONAL", "configured")
|
||||
|
||||
got := envOr("KEEPALIVE_TEST_OPTIONAL", "default")
|
||||
if got != "configured" {
|
||||
t.Fatalf("envOr() = %q, want configured", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvOrUsesDefaultWhenNoNamesAreSet(t *testing.T) {
|
||||
got := envOr("KEEPALIVE_TEST_MISSING_DEFAULT", "default")
|
||||
if got != "default" {
|
||||
t.Fatalf("envOr() = %q, want default", got)
|
||||
}
|
||||
}
|
||||
+28
-19
@@ -9,36 +9,45 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
Registry["mongodb"] = func() (Adapter, error) { return &mongoAdapter{}, nil }
|
||||
Registry["mongodb"] = func(cfg Config) (Adapter, error) {
|
||||
uri, err := cfg.Required("uri")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbName, err := cfg.Required("database")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
collName, err := cfg.Required("collection")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mongoAdapter{
|
||||
uri: uri,
|
||||
dbName: dbName,
|
||||
collName: collName,
|
||||
docID: cfg.Optional("counter_key", "counter"),
|
||||
}, nil
|
||||
}
|
||||
Registry["mongo"] = Registry["mongodb"]
|
||||
}
|
||||
|
||||
type mongoAdapter struct {
|
||||
client *mongo.Client
|
||||
coll *mongo.Collection
|
||||
docID string
|
||||
client *mongo.Client
|
||||
coll *mongo.Collection
|
||||
uri string
|
||||
dbName string
|
||||
collName string
|
||||
docID string
|
||||
}
|
||||
|
||||
func (a *mongoAdapter) Connect(ctx context.Context) error {
|
||||
uri, err := envOrFail("KEEPALIVE_MONGODB_URI")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbName, err := envOrFail("KEEPALIVE_MONGODB_DATABASE")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
collName, err := envOrFail("KEEPALIVE_MONGODB_COLLECTION")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := mongo.Connect(options.Client().ApplyURI(uri))
|
||||
client, err := mongo.Connect(options.Client().ApplyURI(a.uri))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.client = client
|
||||
a.coll = client.Database(dbName).Collection(collName)
|
||||
a.docID = envOr("KEEPALIVE_COUNTER_KEY", "counter")
|
||||
a.coll = client.Database(a.dbName).Collection(a.collName)
|
||||
return client.Ping(ctx, nil)
|
||||
}
|
||||
|
||||
|
||||
+12
-7
@@ -9,20 +9,26 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
Registry["mysql"] = func() (Adapter, error) { return &mysqlAdapter{}, nil }
|
||||
Registry["mysql"] = func(cfg Config) (Adapter, error) {
|
||||
dsn, err := cfg.Required("dsn")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mysqlAdapter{
|
||||
dsn: dsn,
|
||||
key: cfg.Optional("counter_key", "counter"),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type mysqlAdapter struct {
|
||||
db *sql.DB
|
||||
dsn string
|
||||
key string
|
||||
}
|
||||
|
||||
func (a *mysqlAdapter) Connect(ctx context.Context) error {
|
||||
dsn, err := envOrFail("KEEPALIVE_MYSQL_DSN")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
db, err := sql.Open("mysql", a.dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -30,7 +36,6 @@ func (a *mysqlAdapter) Connect(ctx context.Context) error {
|
||||
db.SetMaxOpenConns(10)
|
||||
db.SetMaxIdleConns(10)
|
||||
a.db = db
|
||||
a.key = envOr("KEEPALIVE_COUNTER_KEY", "counter")
|
||||
return a.db.PingContext(ctx)
|
||||
}
|
||||
|
||||
|
||||
+12
-7
@@ -8,26 +8,31 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
Registry["postgresql"] = func() (Adapter, error) { return &postgresAdapter{}, nil }
|
||||
Registry["postgresql"] = func(cfg Config) (Adapter, error) {
|
||||
url, err := cfg.Required("url")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &postgresAdapter{
|
||||
url: url,
|
||||
key: cfg.Optional("counter_key", "counter"),
|
||||
}, nil
|
||||
}
|
||||
Registry["postgres"] = Registry["postgresql"]
|
||||
}
|
||||
|
||||
type postgresAdapter struct {
|
||||
db *sql.DB
|
||||
url string
|
||||
key string
|
||||
}
|
||||
|
||||
func (a *postgresAdapter) Connect(ctx context.Context) error {
|
||||
uri, err := envOrFail("KEEPALIVE_POSTGRESQL_URL")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db, err := sql.Open("postgres", uri)
|
||||
db, err := sql.Open("postgres", a.url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.db = db
|
||||
a.key = envOr("KEEPALIVE_COUNTER_KEY", "counter")
|
||||
return a.db.PingContext(ctx)
|
||||
}
|
||||
|
||||
|
||||
+12
-7
@@ -7,25 +7,30 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
Registry["redis"] = func() (Adapter, error) { return &redisAdapter{}, nil }
|
||||
Registry["redis"] = func(cfg Config) (Adapter, error) {
|
||||
url, err := cfg.Required("url")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &redisAdapter{
|
||||
url: url,
|
||||
key: cfg.Optional("counter_key", "counter"),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type redisAdapter struct {
|
||||
client *redis.Client
|
||||
url string
|
||||
key string
|
||||
}
|
||||
|
||||
func (a *redisAdapter) Connect(ctx context.Context) error {
|
||||
url, err := envOrFail("KEEPALIVE_REDIS_URL")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opt, err := redis.ParseURL(url)
|
||||
opt, err := redis.ParseURL(a.url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.client = redis.NewClient(opt)
|
||||
a.key = envOr("KEEPALIVE_COUNTER_KEY", "counter")
|
||||
return a.client.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
|
||||
+12
-7
@@ -7,20 +7,26 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
Registry["valkey"] = func() (Adapter, error) { return &valkeyAdapter{}, nil }
|
||||
Registry["valkey"] = func(cfg Config) (Adapter, error) {
|
||||
url, err := cfg.Required("url")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &valkeyAdapter{
|
||||
url: url,
|
||||
key: cfg.Optional("counter_key", "counter"),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type valkeyAdapter struct {
|
||||
client valkey.Client
|
||||
url string
|
||||
key string
|
||||
}
|
||||
|
||||
func (a *valkeyAdapter) Connect(_ context.Context) error {
|
||||
url, err := envOrFail("KEEPALIVE_VALKEY_URL")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opt, err := valkey.ParseURL(url)
|
||||
opt, err := valkey.ParseURL(a.url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -29,7 +35,6 @@ func (a *valkeyAdapter) Connect(_ context.Context) error {
|
||||
return err
|
||||
}
|
||||
a.client = client
|
||||
a.key = envOr("KEEPALIVE_COUNTER_KEY", "counter")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user