1
0
Fork 0

Refactor config package

- Parse configuration only once during startup time
- Store configuration values in a global variable
This commit is contained in:
Frédéric Guillot 2019-06-01 18:18:09 -07:00 committed by fguillot
parent 04d85b3c63
commit 228862fefa
28 changed files with 922 additions and 624 deletions

View file

@ -4,271 +4,11 @@
package config // import "miniflux.app/config"
import (
"net/url"
"os"
"strconv"
"strings"
// Opts contains configuration options after parsing.
var Opts *Options
"miniflux.app/logger"
)
const (
defaultBaseURL = "http://localhost"
defaultDatabaseURL = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
defaultWorkerPoolSize = 5
defaultPollingFrequency = 60
defaultBatchSize = 10
defaultDatabaseMaxConns = 20
defaultDatabaseMinConns = 1
defaultArchiveReadDays = 60
defaultListenAddr = "127.0.0.1:8080"
defaultCertFile = ""
defaultKeyFile = ""
defaultCertDomain = ""
defaultCertCache = "/tmp/cert_cache"
defaultCleanupFrequency = 24
defaultProxyImages = "http-only"
defaultOAuth2ClientID = ""
defaultOAuth2ClientSecret = ""
defaultOAuth2RedirectURL = ""
defaultOAuth2Provider = ""
)
// Config manages configuration parameters.
type Config struct {
IsHTTPS bool
baseURL string
rootURL string
basePath string
}
func (c *Config) parseBaseURL() {
baseURL := os.Getenv("BASE_URL")
if baseURL == "" {
return
}
if baseURL[len(baseURL)-1:] == "/" {
baseURL = baseURL[:len(baseURL)-1]
}
u, err := url.Parse(baseURL)
if err != nil {
logger.Error("Invalid BASE_URL: %v", err)
return
}
scheme := strings.ToLower(u.Scheme)
if scheme != "https" && scheme != "http" {
logger.Error("Invalid BASE_URL: scheme must be http or https")
return
}
c.baseURL = baseURL
c.basePath = u.Path
u.Path = ""
c.rootURL = u.String()
}
// HasDebugMode returns true if debug mode is enabled.
func (c *Config) HasDebugMode() bool {
return getBooleanValue("DEBUG")
}
// BaseURL returns the application base URL with path.
func (c *Config) BaseURL() string {
return c.baseURL
}
// RootURL returns the base URL without path.
func (c *Config) RootURL() string {
return c.rootURL
}
// BasePath returns the application base path according to the base URL.
func (c *Config) BasePath() string {
return c.basePath
}
// DatabaseURL returns the database URL.
func (c *Config) DatabaseURL() string {
value, exists := os.LookupEnv("DATABASE_URL")
if !exists {
logger.Info("The environment variable DATABASE_URL is not configured (the default value is used instead)")
}
if value == "" {
value = defaultDatabaseURL
}
return value
}
// DatabaseMaxConns returns the maximum number of database connections.
func (c *Config) DatabaseMaxConns() int {
return getIntValue("DATABASE_MAX_CONNS", defaultDatabaseMaxConns)
}
// DatabaseMinConns returns the minimum number of database connections.
func (c *Config) DatabaseMinConns() int {
return getIntValue("DATABASE_MIN_CONNS", defaultDatabaseMinConns)
}
// ListenAddr returns the listen address for the HTTP server.
func (c *Config) ListenAddr() string {
if port := os.Getenv("PORT"); port != "" {
return ":" + port
}
return getStringValue("LISTEN_ADDR", defaultListenAddr)
}
// CertFile returns the SSL certificate filename if any.
func (c *Config) CertFile() string {
return getStringValue("CERT_FILE", defaultCertFile)
}
// KeyFile returns the private key filename for custom SSL certificate.
func (c *Config) KeyFile() string {
return getStringValue("KEY_FILE", defaultKeyFile)
}
// CertDomain returns the domain to use for Let's Encrypt certificate.
func (c *Config) CertDomain() string {
return getStringValue("CERT_DOMAIN", defaultCertDomain)
}
// CertCache returns the directory to use for Let's Encrypt session cache.
func (c *Config) CertCache() string {
return getStringValue("CERT_CACHE", defaultCertCache)
}
// CleanupFrequency returns the interval for cleanup jobs.
func (c *Config) CleanupFrequency() int {
return getIntValue("CLEANUP_FREQUENCY", defaultCleanupFrequency)
}
// WorkerPoolSize returns the number of background worker.
func (c *Config) WorkerPoolSize() int {
return getIntValue("WORKER_POOL_SIZE", defaultWorkerPoolSize)
}
// PollingFrequency returns the interval to refresh feeds in the background.
func (c *Config) PollingFrequency() int {
return getIntValue("POLLING_FREQUENCY", defaultPollingFrequency)
}
// BatchSize returns the number of feeds to send for background processing.
func (c *Config) BatchSize() int {
return getIntValue("BATCH_SIZE", defaultBatchSize)
}
// IsOAuth2UserCreationAllowed returns true if user creation is allowed for OAuth2 users.
func (c *Config) IsOAuth2UserCreationAllowed() bool {
return getBooleanValue("OAUTH2_USER_CREATION")
}
// OAuth2ClientID returns the OAuth2 Client ID.
func (c *Config) OAuth2ClientID() string {
return getStringValue("OAUTH2_CLIENT_ID", defaultOAuth2ClientID)
}
// OAuth2ClientSecret returns the OAuth2 client secret.
func (c *Config) OAuth2ClientSecret() string {
return getStringValue("OAUTH2_CLIENT_SECRET", defaultOAuth2ClientSecret)
}
// OAuth2RedirectURL returns the OAuth2 redirect URL.
func (c *Config) OAuth2RedirectURL() string {
return getStringValue("OAUTH2_REDIRECT_URL", defaultOAuth2RedirectURL)
}
// OAuth2Provider returns the name of the OAuth2 provider configured.
func (c *Config) OAuth2Provider() string {
return getStringValue("OAUTH2_PROVIDER", defaultOAuth2Provider)
}
// HasHSTS returns true if HTTP Strict Transport Security is enabled.
func (c *Config) HasHSTS() bool {
return !getBooleanValue("DISABLE_HSTS")
}
// RunMigrations returns true if the environment variable RUN_MIGRATIONS is not empty.
func (c *Config) RunMigrations() bool {
return getBooleanValue("RUN_MIGRATIONS")
}
// CreateAdmin returns true if the environment variable CREATE_ADMIN is not empty.
func (c *Config) CreateAdmin() bool {
return getBooleanValue("CREATE_ADMIN")
}
// PocketConsumerKey returns the Pocket Consumer Key if defined as environment variable.
func (c *Config) PocketConsumerKey(defaultValue string) string {
return getStringValue("POCKET_CONSUMER_KEY", defaultValue)
}
// ProxyImages returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
func (c *Config) ProxyImages() string {
return getStringValue("PROXY_IMAGES", defaultProxyImages)
}
// HasHTTPService returns true if the HTTP service is enabled.
func (c *Config) HasHTTPService() bool {
return !getBooleanValue("DISABLE_HTTP_SERVICE")
}
// HasSchedulerService returns true if the scheduler service is enabled.
func (c *Config) HasSchedulerService() bool {
return !getBooleanValue("DISABLE_SCHEDULER_SERVICE")
}
// ArchiveReadDays returns the number of days after which marking read items as removed.
func (c *Config) ArchiveReadDays() int {
return getIntValue("ARCHIVE_READ_DAYS", defaultArchiveReadDays)
}
// NewConfig returns a new Config.
func NewConfig() *Config {
cfg := &Config{
baseURL: defaultBaseURL,
rootURL: defaultBaseURL,
IsHTTPS: getBooleanValue("HTTPS"),
}
cfg.parseBaseURL()
return cfg
}
func getBooleanValue(key string) bool {
value := strings.ToLower(os.Getenv(key))
if value == "1" || value == "yes" || value == "true" || value == "on" {
return true
}
return false
}
func getStringValue(key, fallback string) string {
value := os.Getenv(key)
if value == "" {
return fallback
}
return value
}
func getIntValue(key string, fallback int) int {
value := os.Getenv(key)
if value == "" {
return fallback
}
v, err := strconv.Atoi(value)
if err != nil {
return fallback
}
return v
// ParseConfig parses configuration options.
func ParseConfig() (err error) {
Opts, err = parse()
return err
}

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Copyright 2019 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
/*
Package config handles configuration values for the application.
Package config handles configuration management for the application.
*/
package config // import "miniflux.app/config"

214
config/options.go Normal file
View file

@ -0,0 +1,214 @@
// Copyright 2019 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package config // import "miniflux.app/config"
const (
defaultBaseURL = "http://localhost"
defaultWorkerPoolSize = 5
defaultPollingFrequency = 60
defaultBatchSize = 10
defaultDatabaseURL = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
defaultDatabaseMaxConns = 20
defaultDatabaseMinConns = 1
defaultArchiveReadDays = 60
defaultListenAddr = "127.0.0.1:8080"
defaultCertFile = ""
defaultKeyFile = ""
defaultCertDomain = ""
defaultCertCache = "/tmp/cert_cache"
defaultCleanupFrequency = 24
defaultProxyImages = "http-only"
defaultOAuth2ClientID = ""
defaultOAuth2ClientSecret = ""
defaultOAuth2RedirectURL = ""
defaultOAuth2Provider = ""
)
// Options contains configuration options.
type Options struct {
HTTPS bool
hsts bool
httpService bool
schedulerService bool
debug bool
baseURL string
rootURL string
basePath string
databaseURL string
databaseMaxConns int
databaseMinConns int
runMigrations bool
listenAddr string
certFile string
certDomain string
certCache string
certKeyFile string
cleanupFrequency int
archiveReadDays int
pollingFrequency int
batchSize int
workerPoolSize int
createAdmin bool
proxyImages string
oauth2UserCreationAllowed bool
oauth2ClientID string
oauth2ClientSecret string
oauth2RedirectURL string
oauth2Provider string
pocketConsumerKey string
}
// HasDebugMode returns true if debug mode is enabled.
func (o *Options) HasDebugMode() bool {
return o.debug
}
// BaseURL returns the application base URL with path.
func (o *Options) BaseURL() string {
return o.baseURL
}
// RootURL returns the base URL without path.
func (o *Options) RootURL() string {
return o.rootURL
}
// BasePath returns the application base path according to the base URL.
func (o *Options) BasePath() string {
return o.basePath
}
// IsDefaultDatabaseURL returns true if the default database URL is used.
func (o *Options) IsDefaultDatabaseURL() bool {
return o.databaseURL == defaultDatabaseURL
}
// DatabaseURL returns the database URL.
func (o *Options) DatabaseURL() string {
return o.databaseURL
}
// DatabaseMaxConns returns the maximum number of database connections.
func (o *Options) DatabaseMaxConns() int {
return o.databaseMaxConns
}
// DatabaseMinConns returns the minimum number of database connections.
func (o *Options) DatabaseMinConns() int {
return o.databaseMinConns
}
// ListenAddr returns the listen address for the HTTP server.
func (o *Options) ListenAddr() string {
return o.listenAddr
}
// CertFile returns the SSL certificate filename if any.
func (o *Options) CertFile() string {
return o.certFile
}
// CertKeyFile returns the private key filename for custom SSL certificate.
func (o *Options) CertKeyFile() string {
return o.certKeyFile
}
// CertDomain returns the domain to use for Let's Encrypt certificate.
func (o *Options) CertDomain() string {
return o.certDomain
}
// CertCache returns the directory to use for Let's Encrypt session cache.
func (o *Options) CertCache() string {
return o.certCache
}
// CleanupFrequency returns the interval for cleanup jobs.
func (o *Options) CleanupFrequency() int {
return o.cleanupFrequency
}
// WorkerPoolSize returns the number of background worker.
func (o *Options) WorkerPoolSize() int {
return o.workerPoolSize
}
// PollingFrequency returns the interval to refresh feeds in the background.
func (o *Options) PollingFrequency() int {
return o.pollingFrequency
}
// BatchSize returns the number of feeds to send for background processing.
func (o *Options) BatchSize() int {
return o.batchSize
}
// IsOAuth2UserCreationAllowed returns true if user creation is allowed for OAuth2 users.
func (o *Options) IsOAuth2UserCreationAllowed() bool {
return o.oauth2UserCreationAllowed
}
// OAuth2ClientID returns the OAuth2 Client ID.
func (o *Options) OAuth2ClientID() string {
return o.oauth2ClientID
}
// OAuth2ClientSecret returns the OAuth2 client secret.
func (o *Options) OAuth2ClientSecret() string {
return o.oauth2ClientSecret
}
// OAuth2RedirectURL returns the OAuth2 redirect URL.
func (o *Options) OAuth2RedirectURL() string {
return o.oauth2RedirectURL
}
// OAuth2Provider returns the name of the OAuth2 provider configured.
func (o *Options) OAuth2Provider() string {
return o.oauth2Provider
}
// HasHSTS returns true if HTTP Strict Transport Security is enabled.
func (o *Options) HasHSTS() bool {
return o.hsts
}
// RunMigrations returns true if the environment variable RUN_MIGRATIONS is not empty.
func (o *Options) RunMigrations() bool {
return o.runMigrations
}
// CreateAdmin returns true if the environment variable CREATE_ADMIN is not empty.
func (o *Options) CreateAdmin() bool {
return o.createAdmin
}
// ProxyImages returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
func (o *Options) ProxyImages() string {
return o.proxyImages
}
// HasHTTPService returns true if the HTTP service is enabled.
func (o *Options) HasHTTPService() bool {
return o.httpService
}
// HasSchedulerService returns true if the scheduler service is enabled.
func (o *Options) HasSchedulerService() bool {
return o.schedulerService
}
// ArchiveReadDays returns the number of days after which marking read items as removed.
func (o *Options) ArchiveReadDays() int {
return o.archiveReadDays
}
// PocketConsumerKey returns the Pocket Consumer Key if configured.
func (o *Options) PocketConsumerKey(defaultValue string) string {
if o.pocketConsumerKey != "" {
return o.pocketConsumerKey
}
return defaultValue
}

124
config/parser.go Normal file
View file

@ -0,0 +1,124 @@
// Copyright 2019 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package config // import "miniflux.app/config"
import (
"errors"
"fmt"
"net/url"
"os"
"strconv"
"strings"
)
func parse() (opts *Options, err error) {
opts = &Options{}
opts.baseURL, opts.rootURL, opts.basePath, err = parseBaseURL()
if err != nil {
return nil, err
}
opts.debug = getBooleanValue("DEBUG")
opts.listenAddr = parseListenAddr()
opts.databaseURL = getStringValue("DATABASE_URL", defaultDatabaseURL)
opts.databaseMaxConns = getIntValue("DATABASE_MAX_CONNS", defaultDatabaseMaxConns)
opts.databaseMinConns = getIntValue("DATABASE_MIN_CONNS", defaultDatabaseMinConns)
opts.runMigrations = getBooleanValue("RUN_MIGRATIONS")
opts.hsts = !getBooleanValue("DISABLE_HSTS")
opts.HTTPS = getBooleanValue("HTTPS")
opts.schedulerService = !getBooleanValue("DISABLE_SCHEDULER_SERVICE")
opts.httpService = !getBooleanValue("DISABLE_HTTP_SERVICE")
opts.certFile = getStringValue("CERT_FILE", defaultCertFile)
opts.certKeyFile = getStringValue("KEY_FILE", defaultKeyFile)
opts.certDomain = getStringValue("CERT_DOMAIN", defaultCertDomain)
opts.certCache = getStringValue("CERT_CACHE", defaultCertCache)
opts.cleanupFrequency = getIntValue("CLEANUP_FREQUENCY", defaultCleanupFrequency)
opts.workerPoolSize = getIntValue("WORKER_POOL_SIZE", defaultWorkerPoolSize)
opts.pollingFrequency = getIntValue("POLLING_FREQUENCY", defaultPollingFrequency)
opts.batchSize = getIntValue("BATCH_SIZE", defaultBatchSize)
opts.archiveReadDays = getIntValue("ARCHIVE_READ_DAYS", defaultArchiveReadDays)
opts.proxyImages = getStringValue("PROXY_IMAGES", defaultProxyImages)
opts.oauth2UserCreationAllowed = getBooleanValue("OAUTH2_USER_CREATION")
opts.oauth2ClientID = getStringValue("OAUTH2_CLIENT_ID", defaultOAuth2ClientID)
opts.oauth2ClientSecret = getStringValue("OAUTH2_CLIENT_SECRET", defaultOAuth2ClientSecret)
opts.oauth2RedirectURL = getStringValue("OAUTH2_REDIRECT_URL", defaultOAuth2RedirectURL)
opts.oauth2Provider = getStringValue("OAUTH2_PROVIDER", defaultOAuth2Provider)
opts.pocketConsumerKey = getStringValue("POCKET_CONSUMER_KEY", "")
opts.createAdmin = getBooleanValue("CREATE_ADMIN")
return opts, nil
}
func parseBaseURL() (string, string, string, error) {
baseURL := os.Getenv("BASE_URL")
if baseURL == "" {
return defaultBaseURL, defaultBaseURL, "", nil
}
if baseURL[len(baseURL)-1:] == "/" {
baseURL = baseURL[:len(baseURL)-1]
}
u, err := url.Parse(baseURL)
if err != nil {
return "", "", "", fmt.Errorf("Invalid BASE_URL: %v", err)
}
scheme := strings.ToLower(u.Scheme)
if scheme != "https" && scheme != "http" {
return "", "", "", errors.New("Invalid BASE_URL: scheme must be http or https")
}
basePath := u.Path
u.Path = ""
return baseURL, u.String(), basePath, nil
}
func parseListenAddr() string {
if port := os.Getenv("PORT"); port != "" {
return ":" + port
}
return getStringValue("LISTEN_ADDR", defaultListenAddr)
}
func getBooleanValue(key string) bool {
value := strings.ToLower(os.Getenv(key))
if value == "1" || value == "yes" || value == "true" || value == "on" {
return true
}
return false
}
func getStringValue(key, fallback string) string {
value := os.Getenv(key)
if value == "" {
return fallback
}
return value
}
func getIntValue(key string, fallback int) int {
value := os.Getenv(key)
if value == "" {
return fallback
}
v, err := strconv.Atoi(value)
if err != nil {
return fallback
}
return v
}

79
config/parser_test.go Normal file
View file

@ -0,0 +1,79 @@
// Copyright 2019 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package config // import "miniflux.app/config"
import (
"os"
"testing"
)
func TestGetBooleanValueWithUnsetVariable(t *testing.T) {
os.Clearenv()
if getBooleanValue("MY_TEST_VARIABLE") {
t.Errorf(`Unset variables should returns false`)
}
}
func TestGetBooleanValue(t *testing.T) {
scenarios := map[string]bool{
"": false,
"1": true,
"Yes": true,
"yes": true,
"True": true,
"true": true,
"on": true,
"false": false,
"off": false,
"invalid": false,
}
for input, expected := range scenarios {
os.Clearenv()
os.Setenv("MY_TEST_VARIABLE", input)
result := getBooleanValue("MY_TEST_VARIABLE")
if result != expected {
t.Errorf(`Unexpected result for %q, got %v instead of %v`, input, result, expected)
}
}
}
func TestGetStringValueWithUnsetVariable(t *testing.T) {
os.Clearenv()
if getStringValue("MY_TEST_VARIABLE", "defaultValue") != "defaultValue" {
t.Errorf(`Unset variables should returns the default value`)
}
}
func TestGetStringValue(t *testing.T) {
os.Clearenv()
os.Setenv("MY_TEST_VARIABLE", "test")
if getStringValue("MY_TEST_VARIABLE", "defaultValue") != "test" {
t.Errorf(`Defined variables should returns the specified value`)
}
}
func TestGetIntValueWithUnsetVariable(t *testing.T) {
os.Clearenv()
if getIntValue("MY_TEST_VARIABLE", 42) != 42 {
t.Errorf(`Unset variables should returns the default value`)
}
}
func TestGetIntValueWithInvalidInput(t *testing.T) {
os.Clearenv()
os.Setenv("MY_TEST_VARIABLE", "invalid integer")
if getIntValue("MY_TEST_VARIABLE", 42) != 42 {
t.Errorf(`Invalid integer should returns the default value`)
}
}
func TestGetIntValue(t *testing.T) {
os.Clearenv()
os.Setenv("MY_TEST_VARIABLE", "2018")
if getIntValue("MY_TEST_VARIABLE", 42) != 2018 {
t.Errorf(`Defined variables should returns the specified value`)
}
}