Skip to content

Commit

Permalink
Merge pull request #1883 from shlinkio/release/v3.6.4
Browse files Browse the repository at this point in the history
Release 3.6.4
  • Loading branch information
acelaya authored Sep 23, 2023
2 parents 228bd83 + 7e093a3 commit 4cf3bc0
Show file tree
Hide file tree
Showing 36 changed files with 297 additions and 230 deletions.
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).

## [3.6.4] - 2023-09-23
### Added
* *Nothing*

### Changed
* [#1866](https://github.com/shlinkio/shlink/issues/1866) The `INITIAL_API_KEY` env var is now only relevant for the official docker image.

Going forward, new non-docker Shlink installations provisioned with env vars that also wish to provide an initial API key, should do it by using the `vendor/bin/shlink-installer init --initial-api-key=%SOME_KEY%` command, instead of using `INITIAL_API_KEY`.

### Deprecated
* *Nothing*

### Removed
* *Nothing*

### Fixed
* [#1819](https://github.com/shlinkio/shlink/issues/1819) Fix incorrect timeout when running DB commands during Shlink start-up.
* [#1870](https://github.com/shlinkio/shlink/issues/1870) Make sure shared locks include the cache prefix when using Redis.
* [#1866](https://github.com/shlinkio/shlink/issues/1866) Fix error when starting docker image with `INITIAL_API_KEY` env var.


## [3.6.3] - 2023-06-14
### Added
* *Nothing*
Expand Down
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"ext-json": "*",
"ext-pdo": "*",
"akrabat/ip-address-middleware": "^2.1",
"cakephp/chronos": "^2.3",
"cakephp/chronos": "~2.3.3",
"doctrine/migrations": "^3.5",
"doctrine/orm": "^2.14",
"endroid/qr-code": "^4.7",
Expand All @@ -45,11 +45,11 @@
"php-middleware/request-id": "^4.1",
"pugx/shortid-php": "^1.1",
"ramsey/uuid": "^4.7",
"shlinkio/shlink-common": "^5.5",
"shlinkio/shlink-common": "^5.6",
"shlinkio/shlink-config": "^2.4",
"shlinkio/shlink-event-dispatcher": "^3.0",
"shlinkio/shlink-importer": "^5.1",
"shlinkio/shlink-installer": "^8.4.1",
"shlinkio/shlink-installer": "^8.5",
"shlinkio/shlink-ip-geolocation": "^3.2",
"shlinkio/shlink-json": "^1.0",
"spiral/roadrunner": "^2023.1",
Expand Down
3 changes: 3 additions & 0 deletions config/autoload/installer.global.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@
InstallationCommand::API_KEY_GENERATE->value => [
'command' => 'bin/cli ' . Command\Api\GenerateKeyCommand::NAME,
],
InstallationCommand::API_KEY_CREATE->value => [
'command' => 'bin/cli ' . Command\Api\InitialApiKeyCommand::NAME,
],
],
],

Expand Down
6 changes: 5 additions & 1 deletion config/autoload/locks.global.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Shlinkio\Shlink\Common\Cache\RedisFactory;
use Shlinkio\Shlink\Common\Lock\NamespacedStore;
use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Symfony\Component\Lock;
Expand All @@ -22,11 +23,12 @@
Lock\Store\RedisStore::class => ConfigAbstractFactory::class,
Lock\LockFactory::class => ConfigAbstractFactory::class,
LOCAL_LOCK_FACTORY => ConfigAbstractFactory::class,
NamespacedStore::class => ConfigAbstractFactory::class,
],
'aliases' => [
'lock_store' => EnvVars::REDIS_SERVERS->existsInEnv() ? 'redis_lock_store' : 'local_lock_store',

'redis_lock_store' => Lock\Store\RedisStore::class,
'redis_lock_store' => NamespacedStore::class,
'local_lock_store' => Lock\Store\FlockStore::class,
],
'delegators' => [
Expand All @@ -39,6 +41,8 @@
ConfigAbstractFactory::class => [
Lock\Store\FlockStore::class => ['config.locks.locks_dir'],
Lock\Store\RedisStore::class => [RedisFactory::SERVICE_NAME],
NamespacedStore::class => [Lock\Store\RedisStore::class, 'config.cache.namespace'],

Lock\LockFactory::class => ['lock_store'],
LOCAL_LOCK_FACTORY => ['local_lock_store'],
],
Expand Down
7 changes: 3 additions & 4 deletions config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,9 @@
Core\ConfigProvider::class,
CLI\ConfigProvider::class,
Rest\ConfigProvider::class,
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
$isTestEnv
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
: new ConfigAggregator\ArrayProvider([]),
new ConfigAggregator\PhpFileProvider('config/autoload/{,*.}global.php'),
// Local config should not be loaded during tests, whereas test config should be loaded ONLY during tests
new ConfigAggregator\PhpFileProvider($isTestEnv ? 'config/test/*.global.php' : 'config/autoload/{,*.}local.php'),
// Routes have to be loaded last
new ConfigAggregator\PhpFileProvider('config/autoload/routes.config.php'),
], 'data/cache/app_config.php', [
Expand Down
7 changes: 6 additions & 1 deletion docker/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ if [ -z "${GEOLITE_LICENSE_KEY}" ] || [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" == "t
flags="${flags} --skip-download-geolite"
fi

# If INITIAL_API_KEY was provided, create an initial API key
if [ -n "${INITIAL_API_KEY}" ]; then
flags="${flags} --initial-api-key=${INITIAL_API_KEY}"
fi

php vendor/bin/shlink-installer init ${flags}

# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided and running as root
# ENABLE_PERIODIC_VISIT_LOCATE is deprecated. Remove cron support in Shlink 4.0.0
# FIXME: ENABLE_PERIODIC_VISIT_LOCATE is deprecated. Remove cron support in Shlink 4.0.0
if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ] && [ "${SHLINK_USER_ID}" = "root" ]; then
echo "Configuring periodic visit location..."
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
Expand Down
2 changes: 1 addition & 1 deletion indocker
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Run docker containers if they are not up yet
if ! [[ $(docker ps | grep shlink_swoole) ]]; then
docker-compose up -d
docker compose up -d
fi

docker exec -it shlink_swoole /bin/sh -c "$*"
1 change: 1 addition & 0 deletions module/CLI/config/cli.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,
Command\Api\ListKeysCommand::NAME => Command\Api\ListKeysCommand::class,
Command\Api\InitialApiKeyCommand::NAME => Command\Api\InitialApiKeyCommand::class,

Command\Tag\ListTagsCommand::NAME => Command\Tag\ListTagsCommand::class,
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
Expand Down
2 changes: 2 additions & 0 deletions module/CLI/config/dependencies.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
Command\Api\ListKeysCommand::class => ConfigAbstractFactory::class,
Command\Api\InitialApiKeyCommand::class => ConfigAbstractFactory::class,

Command\Tag\ListTagsCommand::class => ConfigAbstractFactory::class,
Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class,
Expand Down Expand Up @@ -105,6 +106,7 @@
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class],
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
Command\Api\ListKeysCommand::class => [ApiKeyService::class],
Command\Api\InitialApiKeyCommand::class => [ApiKeyService::class],

Command\Tag\ListTagsCommand::class => [TagService::class],
Command\Tag\RenameTagCommand::class => [TagService::class],
Expand Down
15 changes: 7 additions & 8 deletions module/CLI/src/ApiKey/RoleResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,23 @@

class RoleResolver implements RoleResolverInterface
{
public function __construct(private DomainServiceInterface $domainService, private string $defaultDomain)
{
public function __construct(
private readonly DomainServiceInterface $domainService,
private readonly string $defaultDomain,
) {
}

public function determineRoles(InputInterface $input): array
public function determineRoles(InputInterface $input): iterable
{
$domainAuthority = $input->getOption(Role::DOMAIN_SPECIFIC->paramName());
$author = $input->getOption(Role::AUTHORED_SHORT_URLS->paramName());

$roleDefinitions = [];
if ($author) {
$roleDefinitions[] = RoleDefinition::forAuthoredShortUrls();
yield RoleDefinition::forAuthoredShortUrls();
}
if (is_string($domainAuthority)) {
$roleDefinitions[] = $this->resolveRoleForAuthority($domainAuthority);
yield $this->resolveRoleForAuthority($domainAuthority);
}

return $roleDefinitions;
}

private function resolveRoleForAuthority(string $domainAuthority): RoleDefinition
Expand Down
4 changes: 2 additions & 2 deletions module/CLI/src/ApiKey/RoleResolverInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
interface RoleResolverInterface
{
/**
* @return RoleDefinition[]
* @return iterable<RoleDefinition>
*/
public function determineRoles(InputInterface $input): array;
public function determineRoles(InputInterface $input): iterable;
}
18 changes: 10 additions & 8 deletions module/CLI/src/Command/Api/GenerateKeyCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
Expand All @@ -25,8 +26,8 @@ class GenerateKeyCommand extends Command
public const NAME = 'api-key:generate';

public function __construct(
private ApiKeyServiceInterface $apiKeyService,
private RoleResolverInterface $roleResolver,
private readonly ApiKeyServiceInterface $apiKeyService,
private readonly RoleResolverInterface $roleResolver,
) {
parent::__construct();
}
Expand Down Expand Up @@ -57,7 +58,7 @@ protected function configure(): void

$this
->setName(self::NAME)
->setDescription('Generates a new valid API key.')
->setDescription('Generate a new valid API key.')
->addOption(
'name',
'm',
Expand Down Expand Up @@ -91,11 +92,12 @@ protected function configure(): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$expirationDate = $input->getOption('expiration-date');
$apiKey = $this->apiKeyService->create(
isset($expirationDate) ? Chronos::parse($expirationDate) : null,
$input->getOption('name'),
...$this->roleResolver->determineRoles($input),
);

$apiKey = $this->apiKeyService->create(ApiKeyMeta::fromParams(
name: $input->getOption('name'),
expirationDate: isset($expirationDate) ? Chronos::parse($expirationDate) : null,
roleDefinitions: $this->roleResolver->determineRoles($input),
));

$io = new SymfonyStyle($input, $output);
$io->success(sprintf('Generated API key: "%s"', $apiKey->toString()));
Expand Down
43 changes: 43 additions & 0 deletions module/CLI/src/Command/Api/InitialApiKeyCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Shlinkio\Shlink\CLI\Command\Api;

use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class InitialApiKeyCommand extends Command
{
public const NAME = 'api-key:initial';

public function __construct(private readonly ApiKeyServiceInterface $apiKeyService)
{
parent::__construct();
}

protected function configure(): void
{
$this
->setHidden()
->setName(self::NAME)
->setDescription('Tries to create initial API key')
->addArgument('apiKey', InputArgument::REQUIRED, 'The initial API to create');
}

protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$key = $input->getArgument('apiKey');
$result = $this->apiKeyService->createInitial($key);

if ($result === null && $output->isVerbose()) {
$output->writeln('<comment>Other API keys already exist. Initial API key creation skipped.</comment>');
}

return ExitCode::EXIT_SUCCESS;
}
}
4 changes: 2 additions & 2 deletions module/CLI/src/Util/ProcessRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ class ProcessRunner implements ProcessRunnerInterface
public function __construct(private ProcessHelper $helper, ?callable $createProcess = null)
{
$this->createProcess = $createProcess !== null
? Closure::fromCallable($createProcess)
: static fn (array $cmd) => new Process($cmd, null, null, null, LockedCommandConfig::DEFAULT_TTL);
? $createProcess(...)
: static fn (array $cmd) => new Process($cmd, timeout: LockedCommandConfig::DEFAULT_TTL);
}

public function run(OutputInterface $output, array $cmd): void
Expand Down
26 changes: 26 additions & 0 deletions module/CLI/test-cli/Command/InitialApiKeyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace ShlinkioCliTest\Shlink\CLI\Command;

use PHPUnit\Framework\Attributes\Test;
use Shlinkio\Shlink\CLI\Command\Api\InitialApiKeyCommand;
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;

class InitialApiKeyTest extends CliTestCase
{
#[Test]
public function createsNoKeyWhenOtherApiKeysAlreadyExist(): void
{
[$output] = $this->exec([InitialApiKeyCommand::NAME, 'new_api_key', '-v']);

self::assertEquals(
<<<OUT
Other API keys already exist. Initial API key creation skipped.
OUT,
$output,
);
}
}
4 changes: 2 additions & 2 deletions module/CLI/test/ApiKey/RoleResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function properRolesAreResolvedBasedOnInput(
'example.com',
)->willReturn(self::domainWithId(Domain::withAuthority('example.com')));

$result = $this->resolver->determineRoles($input);
$result = [...$this->resolver->determineRoles($input)];

self::assertEquals($expectedRoles, $result);
}
Expand Down Expand Up @@ -111,7 +111,7 @@ public function exceptionIsThrownWhenTryingToAddDomainOnlyLinkedToDefaultDomain(

$this->expectException(InvalidRoleConfigException::class);

$this->resolver->determineRoles($input);
[...$this->resolver->determineRoles($input)];
}

private static function domainWithId(Domain $domain): Domain
Expand Down
10 changes: 4 additions & 6 deletions module/CLI/test/Command/Api/GenerateKeyCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
Expand Down Expand Up @@ -37,8 +38,7 @@ protected function setUp(): void
public function noExpirationDateIsDefinedIfNotProvided(): void
{
$this->apiKeyService->expects($this->once())->method('create')->with(
$this->isNull(),
$this->isNull(),
$this->callback(fn (ApiKeyMeta $meta) => $meta->name === null && $meta->expirationDate === null),
)->willReturn(ApiKey::create());

$this->commandTester->execute([]);
Expand All @@ -51,8 +51,7 @@ public function noExpirationDateIsDefinedIfNotProvided(): void
public function expirationDateIsDefinedIfProvided(): void
{
$this->apiKeyService->expects($this->once())->method('create')->with(
$this->isInstanceOf(Chronos::class),
$this->isNull(),
$this->callback(fn (ApiKeyMeta $meta) => $meta->expirationDate instanceof Chronos),
)->willReturn(ApiKey::create());

$this->commandTester->execute([
Expand All @@ -64,8 +63,7 @@ public function expirationDateIsDefinedIfProvided(): void
public function nameIsDefinedIfProvided(): void
{
$this->apiKeyService->expects($this->once())->method('create')->with(
$this->isNull(),
$this->isType('string'),
$this->callback(fn (ApiKeyMeta $meta) => $meta->name === 'Alice'),
)->willReturn(ApiKey::create());

$this->commandTester->execute([
Expand Down
Loading

0 comments on commit 4cf3bc0

Please sign in to comment.