Merge pull request #1150 from acelaya-forks/feature/env-config

Feature/env config
This commit is contained in:
Alejandro Celaya 2021-08-07 14:13:26 +02:00 committed by GitHub
commit 48efaa9fd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 154 additions and 226 deletions

View file

@ -6,7 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
## [Unreleased] ## [Unreleased]
### Added ### Added
* *Nothing* * [#1015](https://github.com/shlinkio/shlink/issues/1015) Shlink now accepts configuration via env vars even when not using docker.
The config generated with the installing tool still has precedence over the env vars, so it cannot be combined. Either you use the tool, or use env vars.
### Changed ### Changed
* [#1142](https://github.com/shlinkio/shlink/issues/1142) Replaced `doctrine/cache` package with `symfony/cache`. * [#1142](https://github.com/shlinkio/shlink/issues/1142) Replaced `doctrine/cache` package with `symfony/cache`.

View file

@ -46,7 +46,7 @@
"predis/predis": "^1.1", "predis/predis": "^1.1",
"pugx/shortid-php": "^0.7", "pugx/shortid-php": "^0.7",
"ramsey/uuid": "^3.9", "ramsey/uuid": "^3.9",
"shlinkio/shlink-common": "dev-main#baef0ca as 4.0", "shlinkio/shlink-common": "dev-main#3eacc46 as 4.0",
"shlinkio/shlink-config": "^1.2", "shlinkio/shlink-config": "^1.2",
"shlinkio/shlink-event-dispatcher": "^2.1", "shlinkio/shlink-event-dispatcher": "^2.1",
"shlinkio/shlink-importer": "^2.3.1", "shlinkio/shlink-importer": "^2.3.1",

View file

@ -4,11 +4,15 @@ declare(strict_types=1);
namespace Shlinkio\Shlink; namespace Shlinkio\Shlink;
use function Shlinkio\Shlink\Common\env;
use const Shlinkio\Shlink\Core\DEFAULT_DELETE_SHORT_URL_THRESHOLD;
return [ return [
'delete_short_urls' => [ 'delete_short_urls' => [
'visits_threshold' => 15,
'check_visits_threshold' => true, 'check_visits_threshold' => true,
'visits_threshold' => (int) env('DELETE_SHORT_URL_THRESHOLD', DEFAULT_DELETE_SHORT_URL_THRESHOLD),
], ],
]; ];

View file

@ -2,24 +2,52 @@
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Common;
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository; use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
return [ use function Functional\contains;
use function Shlinkio\Shlink\Common\env;
'entity_manager' => [ return (static function (): array {
'orm' => [ $driver = env('DB_DRIVER');
'proxies_dir' => 'data/proxies', $isMysqlCompatible = contains(['maria', 'mysql'], $driver);
'load_mappings_using_functional_style' => true,
'default_repository_classname' => EntitySpecificationRepository::class, $resolveDriver = static fn () => match ($driver) {
'postgres' => 'pdo_pgsql',
'mssql' => 'pdo_sqlsrv',
default => 'pdo_mysql',
};
$resolveDefaultPort = static fn () => match ($driver) {
'postgres' => '5432',
'mssql' => '1433',
default => '3306',
};
$resolveConnection = static fn () => match (true) {
$driver === null || $driver === 'sqlite' => [
'driver' => 'pdo_sqlite',
'path' => 'data/database.sqlite',
], ],
'connection' => [ default => [
'user' => '', 'driver' => $resolveDriver(),
'password' => '', 'dbname' => env('DB_NAME', 'shlink'),
'dbname' => 'shlink', 'user' => env('DB_USER'),
'password' => env('DB_PASSWORD'),
'host' => env('DB_HOST', $driver === 'postgres' ? env('DB_UNIX_SOCKET') : null),
'port' => env('DB_PORT', $resolveDefaultPort()),
'unix_socket' => $isMysqlCompatible ? env('DB_UNIX_SOCKET') : null,
'charset' => 'utf8', 'charset' => 'utf8',
], ],
], };
]; return [
'entity_manager' => [
'orm' => [
'proxies_dir' => 'data/proxies',
'load_mappings_using_functional_style' => true,
'default_repository_classname' => EntitySpecificationRepository::class,
],
'connection' => $resolveConnection(),
],
];
})();

View file

@ -2,12 +2,14 @@
declare(strict_types=1); declare(strict_types=1);
use function Shlinkio\Shlink\Common\env;
return [ return [
'geolite2' => [ 'geolite2' => [
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb', 'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
'temp_dir' => __DIR__ . '/../../data', 'temp_dir' => __DIR__ . '/../../data',
'license_key' => 'G4Lm0C60yJsnkdPi', // Deprecated. Remove hardcoded license on v3 'license_key' => env('GEOLITE_LICENSE_KEY', 'G4Lm0C60yJsnkdPi'), // Deprecated. Remove hardcoded license on v3
], ],
]; ];

View file

@ -4,10 +4,11 @@ declare(strict_types=1);
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Predis\ClientInterface as PredisClient; use Predis\ClientInterface as PredisClient;
use Shlinkio\Shlink\Common\Lock\RetryLockStoreDelegatorFactory;
use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory; use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory;
use Symfony\Component\Lock; use Symfony\Component\Lock;
use function Shlinkio\Shlink\Common\env;
use const Shlinkio\Shlink\Core\LOCAL_LOCK_FACTORY; use const Shlinkio\Shlink\Core\LOCAL_LOCK_FACTORY;
return [ return [
@ -24,16 +25,12 @@ return [
LOCAL_LOCK_FACTORY => ConfigAbstractFactory::class, LOCAL_LOCK_FACTORY => ConfigAbstractFactory::class,
], ],
'aliases' => [ 'aliases' => [
// With this config, a user could alias 'lock_store' => 'redis_lock_store' to override the default 'lock_store' => env('REDIS_SERVERS') === null ? 'local_lock_store' : 'redis_lock_store',
'lock_store' => 'local_lock_store',
'redis_lock_store' => Lock\Store\RedisStore::class, 'redis_lock_store' => Lock\Store\RedisStore::class,
'local_lock_store' => Lock\Store\FlockStore::class, 'local_lock_store' => Lock\Store\FlockStore::class,
], ],
'delegators' => [ 'delegators' => [
Lock\Store\RedisStore::class => [
RetryLockStoreDelegatorFactory::class,
],
Lock\LockFactory::class => [ Lock\LockFactory::class => [
LoggerAwareDelegatorFactory::class, LoggerAwareDelegatorFactory::class,
], ],

View file

@ -7,30 +7,36 @@ use Shlinkio\Shlink\Common\Mercure\LcobucciJwtProvider;
use Symfony\Component\Mercure\Hub; use Symfony\Component\Mercure\Hub;
use Symfony\Component\Mercure\HubInterface; use Symfony\Component\Mercure\HubInterface;
return [ use function Shlinkio\Shlink\Common\env;
'mercure' => [ return (static function (): array {
'public_hub_url' => null, $publicUrl = env('MERCURE_PUBLIC_HUB_URL');
'internal_hub_url' => null,
'jwt_secret' => null,
'jwt_issuer' => 'Shlink',
],
'dependencies' => [ return [
'delegators' => [
LcobucciJwtProvider::class => [ 'mercure' => [
LazyServiceFactory::class, 'public_hub_url' => $publicUrl,
'internal_hub_url' => env('MERCURE_INTERNAL_HUB_URL', $publicUrl),
'jwt_secret' => env('MERCURE_JWT_SECRET'),
'jwt_issuer' => 'Shlink',
],
'dependencies' => [
'delegators' => [
LcobucciJwtProvider::class => [
LazyServiceFactory::class,
],
Hub::class => [
LazyServiceFactory::class,
],
], ],
Hub::class => [ 'lazy_services' => [
LazyServiceFactory::class, 'class_map' => [
LcobucciJwtProvider::class => LcobucciJwtProvider::class,
Hub::class => HubInterface::class,
],
], ],
], ],
'lazy_services' => [
'class_map' => [
LcobucciJwtProvider::class => LcobucciJwtProvider::class,
Hub::class => HubInterface::class,
],
],
],
]; ];
})();

View file

@ -2,12 +2,14 @@
declare(strict_types=1); declare(strict_types=1);
use function Shlinkio\Shlink\Common\env;
return [ return [
'not_found_redirects' => [ 'not_found_redirects' => [
'invalid_short_url' => null, 'invalid_short_url' => env('INVALID_SHORT_URL_REDIRECT_TO'),
'regular_404' => null, 'regular_404' => env('REGULAR_404_REDIRECT_TO'),
'base_url' => null, 'base_url' => env('BASE_URL_REDIRECT_TO'),
], ],
]; ];

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
use function Shlinkio\Shlink\Common\env;
return (static function (): array {
$redisServers = env('REDIS_SERVERS');
return match (true) {
$redisServers === null => [],
default => [
'cache' => [
'redis' => [
'servers' => $redisServers,
],
],
],
};
})();

View file

@ -4,10 +4,12 @@ declare(strict_types=1);
use Mezzio\Router\FastRouteRouter; use Mezzio\Router\FastRouteRouter;
use function Shlinkio\Shlink\Common\env;
return [ return [
'router' => [ 'router' => [
'base_path' => '', 'base_path' => env('BASE_PATH', ''),
'fastroute' => [ 'fastroute' => [
FastRouteRouter::CONFIG_CACHE_ENABLED => true, FastRouteRouter::CONFIG_CACHE_ENABLED => true,

View file

@ -2,6 +2,8 @@
declare(strict_types=1); declare(strict_types=1);
use function Shlinkio\Shlink\Common\env;
return [ return [
'mezzio-swoole' => [ 'mezzio-swoole' => [
@ -10,11 +12,12 @@ return [
'swoole-http-server' => [ 'swoole-http-server' => [
'host' => '0.0.0.0', 'host' => '0.0.0.0',
'port' => (int) env('PORT', 8080),
'process-name' => 'shlink', 'process-name' => 'shlink',
'options' => [ 'options' => [
'worker_num' => 16, 'worker_num' => (int) env('WEB_WORKER_NUM', 16),
'task_worker_num' => 16, 'task_worker_num' => (int) env('TASK_WORKER_NUM', 16),
], ],
], ],
], ],

View file

@ -2,30 +2,32 @@
declare(strict_types=1); declare(strict_types=1);
use function Shlinkio\Shlink\Common\env;
return [ return [
'tracking' => [ 'tracking' => [
// Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations // Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
// This applies only if IP address tracking is enabled // This applies only if IP address tracking is enabled
'anonymize_remote_addr' => true, 'anonymize_remote_addr' => (bool) env('ANONYMIZE_REMOTE_ADDR', true),
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence // Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
'track_orphan_visits' => true, 'track_orphan_visits' => (bool) env('TRACK_ORPHAN_VISITS', true),
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence // A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
'disable_track_param' => null, 'disable_track_param' => env('DISABLE_TRACK_PARAM'),
// If true, visits will not be tracked at all // If true, visits will not be tracked at all
'disable_tracking' => false, 'disable_tracking' => (bool) env('DISABLE_TRACKING', false),
// If true, visits will be tracked, but neither the IP address, nor the location will be resolved // If true, visits will be tracked, but neither the IP address, nor the location will be resolved
'disable_ip_tracking' => false, 'disable_ip_tracking' => (bool) env('DISABLE_IP_TRACKING', false),
// If true, the referrer will not be tracked // If true, the referrer will not be tracked
'disable_referrer_tracking' => false, 'disable_referrer_tracking' => (bool) env('DISABLE_REFERRER_TRACKING', false),
// If true, the user agent will not be tracked // If true, the user agent will not be tracked
'disable_ua_tracking' => false, 'disable_ua_tracking' => (bool) env('DISABLE_UA_TRACKING', false),
], ],
]; ];

View file

@ -2,26 +2,35 @@
declare(strict_types=1); declare(strict_types=1);
use function Shlinkio\Shlink\Common\env;
use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_CACHE_LIFETIME; use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_CACHE_LIFETIME;
use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE; use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE;
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
use const Shlinkio\Shlink\Core\MIN_SHORT_CODES_LENGTH;
return [ return (static function (): array {
$webhooks = env('VISITS_WEBHOOKS');
$shortCodesLength = (int) env('DEFAULT_SHORT_CODES_LENGTH', DEFAULT_SHORT_CODES_LENGTH);
$shortCodesLength = $shortCodesLength < MIN_SHORT_CODES_LENGTH ? MIN_SHORT_CODES_LENGTH : $shortCodesLength;
'url_shortener' => [ return [
'domain' => [
'schema' => 'https', 'url_shortener' => [
'hostname' => '', 'domain' => [
'schema' => env('SHORT_DOMAIN_SCHEMA', 'http'),
'hostname' => env('SHORT_DOMAIN_HOST', ''),
],
'validate_url' => (bool) env('VALIDATE_URLS', false), // Deprecated
'visits_webhooks' => $webhooks === null ? [] : explode(',', $webhooks),
'default_short_codes_length' => $shortCodesLength,
'auto_resolve_titles' => (bool) env('AUTO_RESOLVE_TITLES', false),
'append_extra_path' => (bool) env('REDIRECT_APPEND_EXTRA_PATH', false),
// TODO Move these two options to their own config namespace. Maybe "redirects".
'redirect_status_code' => (int) env('REDIRECT_STATUS_CODE', DEFAULT_REDIRECT_STATUS_CODE),
'redirect_cache_lifetime' => (int) env('REDIRECT_CACHE_LIFETIME', DEFAULT_REDIRECT_CACHE_LIFETIME),
], ],
'validate_url' => false, // Deprecated
'visits_webhooks' => [],
'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH,
'auto_resolve_titles' => false,
'append_extra_path' => false,
// TODO Move these two options to their own config namespace. Maybe "redirects". ];
'redirect_status_code' => DEFAULT_REDIRECT_STATUS_CODE, })();
'redirect_cache_lifetime' => DEFAULT_REDIRECT_CACHE_LIFETIME,
],
];

View file

@ -8,7 +8,7 @@ use Laminas\ConfigAggregator;
use Laminas\Diactoros; use Laminas\Diactoros;
use Mezzio; use Mezzio;
use Mezzio\ProblemDetails; use Mezzio\ProblemDetails;
use Mezzio\Swoole\ConfigProvider as SwooleConfigProvider; use Mezzio\Swoole;
use function class_exists; use function class_exists;
use function Shlinkio\Shlink\Common\env; use function Shlinkio\Shlink\Common\env;
@ -17,7 +17,7 @@ return (new ConfigAggregator\ConfigAggregator([
Mezzio\ConfigProvider::class, Mezzio\ConfigProvider::class,
Mezzio\Router\ConfigProvider::class, Mezzio\Router\ConfigProvider::class,
Mezzio\Router\FastRouteRouter\ConfigProvider::class, Mezzio\Router\FastRouteRouter\ConfigProvider::class,
class_exists(SwooleConfigProvider::class) ? SwooleConfigProvider::class : new ConfigAggregator\ArrayProvider([]), class_exists(Swoole\ConfigProvider::class) ? Swoole\ConfigProvider::class : new ConfigAggregator\ArrayProvider([]),
ProblemDetails\ConfigProvider::class, ProblemDetails\ConfigProvider::class,
Diactoros\ConfigProvider::class, Diactoros\ConfigProvider::class,
Common\ConfigProvider::class, Common\ConfigProvider::class,
@ -31,6 +31,7 @@ return (new ConfigAggregator\ConfigAggregator([
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'), new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
env('APP_ENV') === 'test' env('APP_ENV') === 'test'
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php') ? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
// Deprecated. When the SimplifiedConfigParser is removed, load only generated_config.php here
: new ConfigAggregator\LaminasConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'), : new ConfigAggregator\LaminasConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'),
], 'data/cache/app_config.php', [ ], 'data/cache/app_config.php', [
Core\Config\SimplifiedConfigParser::class, Core\Config\SimplifiedConfigParser::class,

View file

@ -7,128 +7,8 @@ namespace Shlinkio\Shlink;
use Monolog\Handler\StreamHandler; use Monolog\Handler\StreamHandler;
use Monolog\Logger; use Monolog\Logger;
use function explode;
use function Functional\contains;
use function Shlinkio\Shlink\Common\env;
use const Shlinkio\Shlink\Core\DEFAULT_DELETE_SHORT_URL_THRESHOLD;
use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_CACHE_LIFETIME;
use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE;
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
use const Shlinkio\Shlink\Core\MIN_SHORT_CODES_LENGTH;
$helper = new class {
private const DB_DRIVERS_MAP = [
'mysql' => 'pdo_mysql',
'maria' => 'pdo_mysql',
'postgres' => 'pdo_pgsql',
'mssql' => 'pdo_sqlsrv',
];
private const DB_PORTS_MAP = [
'mysql' => '3306',
'maria' => '3306',
'postgres' => '5432',
'mssql' => '1433',
];
public function getDbConfig(): array
{
$driver = env('DB_DRIVER');
$isMysql = contains(['maria', 'mysql'], $driver);
if ($driver === null || $driver === 'sqlite') {
return [
'driver' => 'pdo_sqlite',
'path' => 'data/database.sqlite',
];
}
return [
'driver' => self::DB_DRIVERS_MAP[$driver],
'dbname' => env('DB_NAME', 'shlink'),
'user' => env('DB_USER'),
'password' => env('DB_PASSWORD'),
'host' => env('DB_HOST', $driver === 'postgres' ? env('DB_UNIX_SOCKET') : null),
'port' => env('DB_PORT', self::DB_PORTS_MAP[$driver]),
'unix_socket' => $isMysql ? env('DB_UNIX_SOCKET') : null,
];
}
public function getNotFoundRedirectsConfig(): array
{
return [
'invalid_short_url' => env('INVALID_SHORT_URL_REDIRECT_TO'),
'regular_404' => env('REGULAR_404_REDIRECT_TO'),
'base_url' => env('BASE_URL_REDIRECT_TO'),
];
}
public function getVisitsWebhooks(): array
{
$webhooks = env('VISITS_WEBHOOKS');
return $webhooks === null ? [] : explode(',', $webhooks);
}
public function getRedisConfig(): ?array
{
$redisServers = env('REDIS_SERVERS');
return $redisServers === null ? null : ['servers' => $redisServers];
}
public function getDefaultShortCodesLength(): int
{
$value = (int) env('DEFAULT_SHORT_CODES_LENGTH', DEFAULT_SHORT_CODES_LENGTH);
return $value < MIN_SHORT_CODES_LENGTH ? MIN_SHORT_CODES_LENGTH : $value;
}
public function getMercureConfig(): array
{
$publicUrl = env('MERCURE_PUBLIC_HUB_URL');
return [
'public_hub_url' => $publicUrl,
'internal_hub_url' => env('MERCURE_INTERNAL_HUB_URL', $publicUrl),
'jwt_secret' => env('MERCURE_JWT_SECRET'),
];
}
};
return [ return [
'delete_short_urls' => [
'check_visits_threshold' => true,
'visits_threshold' => (int) env('DELETE_SHORT_URL_THRESHOLD', DEFAULT_DELETE_SHORT_URL_THRESHOLD),
],
'entity_manager' => [
'connection' => $helper->getDbConfig(),
],
'url_shortener' => [
'domain' => [
'schema' => env('SHORT_DOMAIN_SCHEMA', 'http'),
'hostname' => env('SHORT_DOMAIN_HOST', ''),
],
'validate_url' => (bool) env('VALIDATE_URLS', false),
'visits_webhooks' => $helper->getVisitsWebhooks(),
'default_short_codes_length' => $helper->getDefaultShortCodesLength(),
'auto_resolve_titles' => (bool) env('AUTO_RESOLVE_TITLES', false),
'redirect_status_code' => (int) env('REDIRECT_STATUS_CODE', DEFAULT_REDIRECT_STATUS_CODE),
'redirect_cache_lifetime' => (int) env('REDIRECT_CACHE_LIFETIME', DEFAULT_REDIRECT_CACHE_LIFETIME),
'append_extra_path' => (bool) env('REDIRECT_APPEND_EXTRA_PATH', false),
],
'tracking' => [
'anonymize_remote_addr' => (bool) env('ANONYMIZE_REMOTE_ADDR', true),
'track_orphan_visits' => (bool) env('TRACK_ORPHAN_VISITS', true),
'disable_track_param' => env('DISABLE_TRACK_PARAM'),
'disable_tracking' => (bool) env('DISABLE_TRACKING', false),
'disable_ip_tracking' => (bool) env('DISABLE_IP_TRACKING', false),
'disable_referrer_tracking' => (bool) env('DISABLE_REFERRER_TRACKING', false),
'disable_ua_tracking' => (bool) env('DISABLE_UA_TRACKING', false),
],
'not_found_redirects' => $helper->getNotFoundRedirectsConfig(),
'logger' => [ 'logger' => [
'Shlink' => [ 'Shlink' => [
'handlers' => [ 'handlers' => [
@ -143,34 +23,4 @@ return [
], ],
], ],
'dependencies' => [
'aliases' => env('REDIS_SERVERS') === null ? [] : [
'lock_store' => 'redis_lock_store',
],
],
'cache' => [
'redis' => $helper->getRedisConfig(),
],
'router' => [
'base_path' => env('BASE_PATH', ''),
],
'mezzio-swoole' => [
'swoole-http-server' => [
'port' => (int) env('PORT', 8080),
'options' => [
'worker_num' => (int) env('WEB_WORKER_NUM', 16),
'task_worker_num' => (int) env('TASK_WORKER_NUM', 16),
],
],
],
'geolite2' => [
'license_key' => env('GEOLITE_LICENSE_KEY', 'G4Lm0C60yJsnkdPi'), // Deprecated. Remove hardcoded license on v3
],
'mercure' => $helper->getMercureConfig(),
]; ];