feat: synchronize installer scripts from git
This commit is contained in:
parent
4e2f181dd9
commit
65e59e740c
6 changed files with 407 additions and 0 deletions
|
|
@ -3,7 +3,9 @@
|
||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
use App\Entity\ScriptMapping;
|
use App\Entity\ScriptMapping;
|
||||||
|
use App\Exception\GitSyncFailedException;
|
||||||
use App\Form\ScriptMappingType;
|
use App\Form\ScriptMappingType;
|
||||||
|
use App\Service\GitSynchronizer;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
@ -83,4 +85,23 @@ final class ScriptMappingController extends AbstractController
|
||||||
|
|
||||||
return $this->redirectToRoute('admin_dashboard');
|
return $this->redirectToRoute('admin_dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/sync', name: 'admin_mapping_sync', methods: ['POST'])]
|
||||||
|
public function sync(Request $request, ScriptMapping $mapping, GitSynchronizer $synchronizer): Response
|
||||||
|
{
|
||||||
|
if (!$this->isCsrfTokenValid('sync_mapping_'.$mapping->getId(), (string) $request->request->get('_token'))) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$synchronizer->sync($mapping);
|
||||||
|
$this->addFlash('success', 'Mapping synchronisé.');
|
||||||
|
} catch (GitSyncFailedException) {
|
||||||
|
$this->addFlash('error', 'La synchronisation du mapping a échoué.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_dashboard');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
src/Exception/GitSyncFailedException.php
Normal file
9
src/Exception/GitSyncFailedException.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exception;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
final class GitSyncFailedException extends RuntimeException
|
||||||
|
{
|
||||||
|
}
|
||||||
175
src/Service/GitSynchronizer.php
Normal file
175
src/Service/GitSynchronizer.php
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\ScriptMapping;
|
||||||
|
use App\Exception\GitSyncFailedException;
|
||||||
|
use LogicException;
|
||||||
|
use RuntimeException;
|
||||||
|
use Symfony\Component\Process\Exception\ProcessFailedException;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class GitSynchronizer
|
||||||
|
{
|
||||||
|
public function __construct(private readonly CachePathResolver $cachePathResolver)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sync(ScriptMapping $mapping): void
|
||||||
|
{
|
||||||
|
if ($mapping->getId() === null) {
|
||||||
|
throw new LogicException('Cannot synchronize a mapping before it is persisted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$repositoryDir = $this->checkoutRepository($mapping);
|
||||||
|
$cacheKey = $this->cachePathResolver->scriptCacheKeyForMapping($mapping->getId());
|
||||||
|
$this->copyRepositoryFileToCache($mapping, $repositoryDir, $cacheKey);
|
||||||
|
|
||||||
|
$mapping->markSyncSucceeded($cacheKey);
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
$message = $this->sanitizeError($exception->getMessage(), $mapping->getAccessToken());
|
||||||
|
$mapping->markSyncFailed($message);
|
||||||
|
|
||||||
|
throw new GitSyncFailedException('Git synchronization failed.', 0, $exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkoutRepository(ScriptMapping $mapping): string
|
||||||
|
{
|
||||||
|
$repositoryDir = $this->repositoryDir($mapping);
|
||||||
|
$this->ensureDirectory(dirname($repositoryDir));
|
||||||
|
|
||||||
|
$env = $this->gitEnvironment($mapping, $repositoryDir);
|
||||||
|
|
||||||
|
if (!is_dir($repositoryDir.'/.git')) {
|
||||||
|
$this->removeDirectory($repositoryDir);
|
||||||
|
$this->runGit(['git', 'clone', '--no-checkout', $mapping->getRepositoryUrl(), $repositoryDir], null, $env);
|
||||||
|
} else {
|
||||||
|
$this->runGit(['git', '-C', $repositoryDir, 'remote', 'set-url', 'origin', $mapping->getRepositoryUrl()], null, $env);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->runGit(['git', '-C', $repositoryDir, 'fetch', '--prune', '--tags', 'origin', $mapping->getGitRef()], null, $env);
|
||||||
|
$this->runGit(['git', '-C', $repositoryDir, 'checkout', '--force', 'FETCH_HEAD'], null, $env);
|
||||||
|
|
||||||
|
return $repositoryDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function copyRepositoryFileToCache(ScriptMapping $mapping, string $repositoryDir, string $cacheKey): void
|
||||||
|
{
|
||||||
|
$repositoryBase = realpath($repositoryDir);
|
||||||
|
if ($repositoryBase === false) {
|
||||||
|
throw new RuntimeException(sprintf('Repository directory "%s" does not exist.', $repositoryDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourcePath = $repositoryDir.'/'.$mapping->getRepositoryFilePath();
|
||||||
|
$sourceRealPath = realpath($sourcePath);
|
||||||
|
if ($sourceRealPath === false || !str_starts_with($sourceRealPath, rtrim($repositoryBase, '/').'/')) {
|
||||||
|
throw new RuntimeException(sprintf('Repository file "%s" was not found.', $mapping->getRepositoryFilePath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file($sourceRealPath) || !is_readable($sourceRealPath)) {
|
||||||
|
throw new RuntimeException(sprintf('Repository file "%s" is not readable.', $mapping->getRepositoryFilePath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetPath = $this->cachePathResolver->servedScriptPath($cacheKey);
|
||||||
|
$this->ensureDirectory(dirname($targetPath));
|
||||||
|
|
||||||
|
$temporaryPath = $targetPath.'.tmp.'.bin2hex(random_bytes(6));
|
||||||
|
if (!copy($sourceRealPath, $temporaryPath)) {
|
||||||
|
throw new RuntimeException(sprintf('Could not write cache file "%s".', $targetPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
chmod($temporaryPath, 0644);
|
||||||
|
if (!rename($temporaryPath, $targetPath)) {
|
||||||
|
@unlink($temporaryPath);
|
||||||
|
|
||||||
|
throw new RuntimeException(sprintf('Could not publish cache file "%s".', $targetPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<string> $command */
|
||||||
|
private function runGit(array $command, ?string $cwd, array $env): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
(new Process($command, $cwd, $env))->mustRun();
|
||||||
|
} catch (ProcessFailedException $exception) {
|
||||||
|
throw new RuntimeException($exception->getMessage(), 0, $exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function repositoryDir(ScriptMapping $mapping): string
|
||||||
|
{
|
||||||
|
return $this->cachePathResolver->baseDir().'/repos/'.$mapping->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
private function gitEnvironment(ScriptMapping $mapping, string $repositoryDir): array
|
||||||
|
{
|
||||||
|
$env = [
|
||||||
|
'GIT_CONFIG_COUNT' => '1',
|
||||||
|
'GIT_CONFIG_KEY_0' => 'safe.directory',
|
||||||
|
'GIT_CONFIG_VALUE_0' => $repositoryDir,
|
||||||
|
'GIT_TERMINAL_PROMPT' => '0',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$mapping->hasAccessToken()) {
|
||||||
|
return $env;
|
||||||
|
}
|
||||||
|
|
||||||
|
$askPassPath = $this->cachePathResolver->baseDir().'/git-askpass.sh';
|
||||||
|
$this->ensureDirectory(dirname($askPassPath));
|
||||||
|
file_put_contents($askPassPath, <<<'SH'
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
case "$1" in
|
||||||
|
*Username*) printf '%s\n' oauth2 ;;
|
||||||
|
*) printf '%s\n' "$GIT_ACCESS_TOKEN" ;;
|
||||||
|
esac
|
||||||
|
SH);
|
||||||
|
chmod($askPassPath, 0700);
|
||||||
|
|
||||||
|
$env['GIT_ASKPASS'] = $askPassPath;
|
||||||
|
$env['GIT_ACCESS_TOKEN'] = (string) $mapping->getAccessToken();
|
||||||
|
|
||||||
|
return $env;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizeError(string $message, ?string $accessToken): string
|
||||||
|
{
|
||||||
|
if ($accessToken === null || $accessToken === '') {
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_replace($accessToken, '[redacted]', $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureDirectory(string $directory): void
|
||||||
|
{
|
||||||
|
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
|
||||||
|
throw new RuntimeException(sprintf('Could not create directory "%s".', $directory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeDirectory(string $directory): void
|
||||||
|
{
|
||||||
|
if (!is_dir($directory)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$iterator = new \RecursiveIteratorIterator(
|
||||||
|
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
|
||||||
|
\RecursiveIteratorIterator::CHILD_FIRST
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($iterator as $item) {
|
||||||
|
if ($item->isDir()) {
|
||||||
|
rmdir($item->getPathname());
|
||||||
|
} else {
|
||||||
|
unlink($item->getPathname());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rmdir($directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,9 @@
|
||||||
{% for message in app.flashes('success') %}
|
{% for message in app.flashes('success') %}
|
||||||
<p>{{ message }}</p>
|
<p>{{ message }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% for message in app.flashes('error') %}
|
||||||
|
<p>{{ message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -38,6 +41,10 @@
|
||||||
<td>{{ mapping.lastSyncError }}</td>
|
<td>{{ mapping.lastSyncError }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ path('admin_mapping_edit', {id: mapping.id}) }}">Modifier</a>
|
<a href="{{ path('admin_mapping_edit', {id: mapping.id}) }}">Modifier</a>
|
||||||
|
<form method="post" action="{{ path('admin_mapping_sync', {id: mapping.id}) }}">
|
||||||
|
<input type="hidden" name="_token" value="{{ csrf_token('sync_mapping_' ~ mapping.id) }}">
|
||||||
|
<button type="submit">Synchroniser</button>
|
||||||
|
</form>
|
||||||
<form method="post" action="{{ path('admin_mapping_delete', {id: mapping.id}) }}">
|
<form method="post" action="{{ path('admin_mapping_delete', {id: mapping.id}) }}">
|
||||||
<input type="hidden" name="_token" value="{{ csrf_token('delete_mapping_' ~ mapping.id) }}">
|
<input type="hidden" name="_token" value="{{ csrf_token('delete_mapping_' ~ mapping.id) }}">
|
||||||
<button type="submit">Supprimer</button>
|
<button type="submit">Supprimer</button>
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,25 @@ use App\Entity\ScriptMapping;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\Tools\SchemaTool;
|
use Doctrine\ORM\Tools\SchemaTool;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
final class ScriptMappingControllerTest extends WebTestCase
|
final class ScriptMappingControllerTest extends WebTestCase
|
||||||
{
|
{
|
||||||
private KernelBrowser $client;
|
private KernelBrowser $client;
|
||||||
private EntityManagerInterface $entityManager;
|
private EntityManagerInterface $entityManager;
|
||||||
|
private Filesystem $filesystem;
|
||||||
|
private string $workDir;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
self::ensureKernelShutdown();
|
self::ensureKernelShutdown();
|
||||||
$this->client = self::createClient();
|
$this->client = self::createClient();
|
||||||
|
$this->filesystem = new Filesystem();
|
||||||
|
$this->workDir = sys_get_temp_dir().'/get-installer-bootstrap-controller-test-'.bin2hex(random_bytes(6));
|
||||||
|
$this->filesystem->mkdir($this->workDir);
|
||||||
|
|
||||||
$this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
|
$this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
|
||||||
$metadata = $this->entityManager->getMetadataFactory()->getAllMetadata();
|
$metadata = $this->entityManager->getMetadataFactory()->getAllMetadata();
|
||||||
|
|
@ -31,6 +38,7 @@ final class ScriptMappingControllerTest extends WebTestCase
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
|
|
||||||
$this->entityManager->close();
|
$this->entityManager->close();
|
||||||
|
$this->filesystem->remove($this->workDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNewMappingRequiresAuthentication(): void
|
public function testNewMappingRequiresAuthentication(): void
|
||||||
|
|
@ -83,6 +91,39 @@ final class ScriptMappingControllerTest extends WebTestCase
|
||||||
self::assertSelectorTextContains('form', 'Public path must end with .sh.');
|
self::assertSelectorTextContains('form', 'Public path must end with .sh.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAdminCanSynchronizeMapping(): void
|
||||||
|
{
|
||||||
|
$this->loginAsAdmin();
|
||||||
|
|
||||||
|
$repositoryDir = $this->createRepository([
|
||||||
|
'install.sh' => "#!/usr/bin/env bash\nprintf 'synced'\n",
|
||||||
|
]);
|
||||||
|
$mapping = (new ScriptMapping())
|
||||||
|
->setPublicPath('mcp/graylog/install.sh')
|
||||||
|
->setRepositoryUrl($repositoryDir)
|
||||||
|
->setGitRef('main')
|
||||||
|
->setRepositoryFilePath('install.sh')
|
||||||
|
->setActive(true);
|
||||||
|
$this->entityManager->persist($mapping);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$crawler = $this->client->request('GET', '/admin');
|
||||||
|
$form = $crawler->selectButton('Synchroniser')->form();
|
||||||
|
|
||||||
|
$this->client->submit($form);
|
||||||
|
|
||||||
|
self::assertResponseRedirects('/admin');
|
||||||
|
|
||||||
|
$syncedMapping = $this->entityManager->getRepository(ScriptMapping::class)->find($mapping->getId());
|
||||||
|
self::assertInstanceOf(ScriptMapping::class, $syncedMapping);
|
||||||
|
self::assertSame(
|
||||||
|
ScriptMapping::SYNC_STATUS_SYNCED,
|
||||||
|
$syncedMapping->getLastSyncStatus(),
|
||||||
|
(string) $syncedMapping->getLastSyncError()
|
||||||
|
);
|
||||||
|
self::assertSame('scripts/'.$mapping->getId().'.sh', $syncedMapping->getCacheKey());
|
||||||
|
}
|
||||||
|
|
||||||
private function loginAsAdmin(): void
|
private function loginAsAdmin(): void
|
||||||
{
|
{
|
||||||
$user = (new User())->setUsername('admin')->setPasswordHash('unused');
|
$user = (new User())->setUsername('admin')->setPasswordHash('unused');
|
||||||
|
|
@ -91,4 +132,32 @@ final class ScriptMappingControllerTest extends WebTestCase
|
||||||
|
|
||||||
$this->client->loginUser($user);
|
$this->client->loginUser($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string, string> $files */
|
||||||
|
private function createRepository(array $files): string
|
||||||
|
{
|
||||||
|
$repositoryDir = $this->workDir.'/repo';
|
||||||
|
$this->filesystem->mkdir($repositoryDir);
|
||||||
|
|
||||||
|
foreach ($files as $path => $content) {
|
||||||
|
$fullPath = $repositoryDir.'/'.$path;
|
||||||
|
$this->filesystem->mkdir(dirname($fullPath));
|
||||||
|
file_put_contents($fullPath, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->runGit(['init'], $repositoryDir);
|
||||||
|
$this->runGit(['config', 'user.email', 'tests@example.com'], $repositoryDir);
|
||||||
|
$this->runGit(['config', 'user.name', 'Tests'], $repositoryDir);
|
||||||
|
$this->runGit(['add', '.'], $repositoryDir);
|
||||||
|
$this->runGit(['commit', '-m', 'Initial commit'], $repositoryDir);
|
||||||
|
$this->runGit(['branch', '-M', 'main'], $repositoryDir);
|
||||||
|
|
||||||
|
return $repositoryDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<string> $arguments */
|
||||||
|
private function runGit(array $arguments, string $cwd): void
|
||||||
|
{
|
||||||
|
(new Process(['git', ...$arguments], $cwd))->mustRun();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
126
tests/Service/GitSynchronizerTest.php
Normal file
126
tests/Service/GitSynchronizerTest.php
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Entity\ScriptMapping;
|
||||||
|
use App\Service\CachePathResolver;
|
||||||
|
use App\Service\GitSynchronizer;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use ReflectionProperty;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
|
final class GitSynchronizerTest extends TestCase
|
||||||
|
{
|
||||||
|
private Filesystem $filesystem;
|
||||||
|
private string $workDir;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->filesystem = new Filesystem();
|
||||||
|
$this->workDir = sys_get_temp_dir().'/get-installer-bootstrap-test-'.bin2hex(random_bytes(6));
|
||||||
|
$this->filesystem->mkdir($this->workDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$this->filesystem->remove($this->workDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSyncCopiesRepositoryScriptToCache(): void
|
||||||
|
{
|
||||||
|
$repositoryDir = $this->createRepository([
|
||||||
|
'install.sh' => "#!/usr/bin/env bash\nprintf 'ok'\n",
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mapping = $this->mapping($repositoryDir, 'install.sh');
|
||||||
|
|
||||||
|
$synchronizer = $this->synchronizer();
|
||||||
|
$synchronizer->sync($mapping);
|
||||||
|
|
||||||
|
self::assertSame(ScriptMapping::SYNC_STATUS_SYNCED, $mapping->getLastSyncStatus());
|
||||||
|
self::assertNotNull($mapping->getLastSuccessfulSyncAt());
|
||||||
|
self::assertNull($mapping->getLastSyncError());
|
||||||
|
self::assertSame('scripts/42.sh', $mapping->getCacheKey());
|
||||||
|
self::assertSame(
|
||||||
|
"#!/usr/bin/env bash\nprintf 'ok'\n",
|
||||||
|
file_get_contents($this->workDir.'/cache/scripts/42.sh')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSyncFailureMarksMappingWithoutTokenLeak(): void
|
||||||
|
{
|
||||||
|
$repositoryDir = $this->createRepository([
|
||||||
|
'install.sh' => "#!/usr/bin/env bash\n",
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mapping = $this->mapping($repositoryDir, 'missing.sh')->setAccessToken('secret-token');
|
||||||
|
|
||||||
|
$synchronizer = $this->synchronizer();
|
||||||
|
|
||||||
|
$this->expectExceptionMessage('Git synchronization failed.');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$synchronizer->sync($mapping);
|
||||||
|
} finally {
|
||||||
|
self::assertSame(ScriptMapping::SYNC_STATUS_FAILED, $mapping->getLastSyncStatus());
|
||||||
|
self::assertNotNull($mapping->getLastSyncError());
|
||||||
|
self::assertStringContainsString('missing.sh', $mapping->getLastSyncError());
|
||||||
|
self::assertStringNotContainsString('secret-token', $mapping->getLastSyncError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, string> $files */
|
||||||
|
private function createRepository(array $files): string
|
||||||
|
{
|
||||||
|
$repositoryDir = $this->workDir.'/repo';
|
||||||
|
$this->filesystem->mkdir($repositoryDir);
|
||||||
|
|
||||||
|
foreach ($files as $path => $content) {
|
||||||
|
$fullPath = $repositoryDir.'/'.$path;
|
||||||
|
$this->filesystem->mkdir(dirname($fullPath));
|
||||||
|
file_put_contents($fullPath, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->runGit(['init'], $repositoryDir);
|
||||||
|
$this->runGit(['config', 'user.email', 'tests@example.com'], $repositoryDir);
|
||||||
|
$this->runGit(['config', 'user.name', 'Tests'], $repositoryDir);
|
||||||
|
$this->runGit(['add', '.'], $repositoryDir);
|
||||||
|
$this->runGit(['commit', '-m', 'Initial commit'], $repositoryDir);
|
||||||
|
$this->runGit(['branch', '-M', 'main'], $repositoryDir);
|
||||||
|
|
||||||
|
return $repositoryDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<string> $arguments */
|
||||||
|
private function runGit(array $arguments, string $cwd): void
|
||||||
|
{
|
||||||
|
$process = new Process(['git', ...$arguments], $cwd);
|
||||||
|
$process->mustRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mapping(string $repositoryUrl, string $repositoryFilePath): ScriptMapping
|
||||||
|
{
|
||||||
|
$mapping = (new ScriptMapping())
|
||||||
|
->setPublicPath('mcp/graylog/install.sh')
|
||||||
|
->setRepositoryUrl($repositoryUrl)
|
||||||
|
->setGitRef('main')
|
||||||
|
->setRepositoryFilePath($repositoryFilePath)
|
||||||
|
->setActive(true);
|
||||||
|
|
||||||
|
return $this->setMappingId($mapping, 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function synchronizer(): GitSynchronizer
|
||||||
|
{
|
||||||
|
return new GitSynchronizer(new CachePathResolver($this->workDir.'/cache', $this->workDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setMappingId(ScriptMapping $mapping, int $id): ScriptMapping
|
||||||
|
{
|
||||||
|
$property = new ReflectionProperty(ScriptMapping::class, 'id');
|
||||||
|
$property->setValue($mapping, $id);
|
||||||
|
|
||||||
|
return $mapping;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue