diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f8b19a83..a89393c1 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,31 +9,11 @@ jobs: tests: strategy: matrix: - os: [Ubuntu, Windows, macOS] - php: [8.0, 8.1] - # php: [7.2, 7.3, 7.4, 8.0] - - include: - - os: Ubuntu - os-version: ubuntu-latest - - - os: Windows - os-version: windows-latest - - - os: macOS - os-version: macos-latest - - - os: macOS-11 - os-version: macos-11.0 - php: 8.0 - - - os: macOS-11 - os-version: macos-11.0 - php: 8.1 - + os: [ubuntu-latest, windows-latest, macos-latest] + php: [8.1, 8.2, 8.3] name: ${{ matrix.os }} - PHP ${{ matrix.php }} - runs-on: ${{ matrix.os-version }} + runs-on: ${{ matrix.os }} steps: - name: Checkout code diff --git a/README.md b/README.md index 5ed34e2a..96fbeb14 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,16 @@ Now, if you run `takeout list`, you'll see both services running at the same tim +--------------+----------------+---------------+-----------------------------------+ ``` +## Network Details + +Takeout containers are automatically added to a Docker network named `takeout`. This allows you to use the same aliasing and base aliasing that is used for the other containers. + +Each container is given two aliases on this network: +- A base_alias based on the core dependency name (e.g. mysql, postgres) +- A full_alias combining the base alias and version (e.g. mysql8.0, postgres13) + +Other containers on the takeout network can access Takeout containers by their aliases. [Check this article on how you can use sail and takeout together](https://mattstauffer.com/blog/how-to-use-takeout-to-add-new-services-to-laravel-sail-and-save-ram/) + ## FAQs
diff --git a/app/Services/Buggregator.php b/app/Services/Buggregator.php new file mode 100644 index 00000000..be517996 --- /dev/null +++ b/app/Services/Buggregator.php @@ -0,0 +1,45 @@ + 'smtp_port', + 'prompt' => 'What is the SMTP port?', + 'default' => '1025', + ], + [ + 'shortname' => 'var_dumper_port', + 'prompt' => 'What is the VarDumper server port?', + 'default' => '9912', + ], + [ + 'shortname' => 'monolog_port', + 'prompt' => 'What is the Monolog port?', + 'default' => '9913', + ], + [ + 'shortname' => 'network_alias', + 'prompt' => 'What network alias to you want to assign to this container? This alias can be used by other services on the same network.', + 'default' => 'buggregator', + ], + ]; + + protected $dockerRunTemplate = '-p "${:port}":8000 \ + -p "${:smtp_port}":1025 \ + -p "${:var_dumper_port}":9912 \ + -p "${:monolog_port}":9913 \ + --network-alias "${:network_alias}" \ + "${:organization}"/"${:image_name}":"${:tag}"'; +} diff --git a/app/Services/MailDev.php b/app/Services/MailDev.php index fe3134de..3bd42753 100644 --- a/app/Services/MailDev.php +++ b/app/Services/MailDev.php @@ -9,18 +9,6 @@ class MailDev extends BaseService protected $organization = 'maildev'; protected $imageName = 'maildev'; protected $defaultPort = 1025; - protected $defaultPrompts = [ - [ - 'shortname' => 'port', - 'prompt' => 'Which host port would you like %s to use?', - // Default is set in the constructor - ], - [ - 'shortname' => 'tag', - 'prompt' => 'Which tag (version) of %s would you like to use?', - 'default' => '2.0.5', - ], - ]; protected $prompts = [ [ 'shortname' => 'web_port', diff --git a/app/Services/MySql.php b/app/Services/MySql.php index 8e287699..86e9a667 100644 --- a/app/Services/MySql.php +++ b/app/Services/MySql.php @@ -6,9 +6,10 @@ class MySql extends BaseService { protected static $category = Category::DATABASE; - protected $organization = 'mysql'; - protected $imageName = 'mysql-server'; + protected $imageName = 'mysql'; + protected $defaultPort = 3306; + protected $prompts = [ [ 'shortname' => 'volume', @@ -24,19 +25,10 @@ class MySql extends BaseService protected $dockerRunTemplate = '-p "${:port}":3306 \ -e MYSQL_ROOT_PASSWORD="${:root_password}" \ - -e MYSQL_ALLOW_EMPTY_PASSWORD="${:allow_empty_password}" \ + -e MYSQL_ALLOW_EMPTY_PASSWORD="1" \ -e MYSQL_ROOT_HOST="%" \ -v "${:volume}":/var/lib/mysql \ - "${:organization}"/"${:image_name}":"${:tag}" --default-authentication-plugin=mysql_native_password'; + "${:organization}"/"${:image_name}":"${:tag}"'; protected static $displayName = 'MySQL'; - - protected function buildParameters(): array - { - $parameters = parent::buildParameters(); - - $parameters['allow_empty_password'] = $parameters['root_password'] === '' ? '1' : '0'; - - return $parameters; - } } diff --git a/app/Services/Timescale.php b/app/Services/Timescale.php new file mode 100644 index 00000000..dce878d3 --- /dev/null +++ b/app/Services/Timescale.php @@ -0,0 +1,49 @@ + 'volume', + 'prompt' => 'What is the Docker volume name?', + 'default' => 'timescale_data', + ], + [ + 'shortname' => 'root_password', + 'prompt' => 'What will the password for the `postgres` user be?', + 'default' => 'password', + ], + ]; + + protected $dockerRunTemplate = '-p "${:port}":5432 \ + -e POSTGRES_PASSWORD="${:root_password}" \ + -v "${:volume}":/var/lib/postgresql/data \ + "${:organization}"/"${:image_name}":"${:tag}"'; + + protected static $displayName = 'Timescale'; + + public function __construct(Shell $shell, Environment $environment, Docker $docker) + { + parent::__construct($shell, $environment, $docker); + + $this->defaultPrompts = array_map(function ($prompt) { + if ($prompt['shortname'] === 'tag') { + $prompt['default'] = $this->tag; + } + + return $prompt; + }, $this->defaultPrompts); + } +} diff --git a/app/Services/Typesense.php b/app/Services/Typesense.php new file mode 100644 index 00000000..a585a05e --- /dev/null +++ b/app/Services/Typesense.php @@ -0,0 +1,32 @@ + 'volume', + 'prompt' => 'What is the Docker volume name?', + 'default' => 'typesense_data', + ], + [ + 'shortname' => 'admin_key', + 'prompt' => 'What will the admin API key be?', + 'default' => 'typesenseadmin', + ], + ]; + + protected $dockerRunTemplate = '-p "${:port}":8108 \ + -v "${:volume}":/data \ + "${:organization}"/"${:image_name}":"${:tag}" \ + --data-dir /data \ + --api-key="${:admin_key}"'; + + protected static $displayName = 'Typesense'; +} diff --git a/app/Shell/DockerTags.php b/app/Shell/DockerTags.php index f8750132..c4dfa45d 100644 --- a/app/Shell/DockerTags.php +++ b/app/Shell/DockerTags.php @@ -11,6 +11,7 @@ class DockerTags { protected $guzzle; protected $service; + protected $armArchitectures = ['arm64', 'aarch64']; public function __construct(Client $guzzle, BaseService $service) { @@ -29,8 +30,8 @@ public function resolveTag($tag): string public function getLatestTag(): string { - $numericTags = $this->getTags()->reject(function ($tag) { - return ! is_numeric($tag[0]); + $numericTags = $this->getTags()->filter(function ($tag) { + return preg_match('/^v?\d/', $tag); }); if ($numericTags->isEmpty()) { @@ -46,33 +47,27 @@ public function getTags(): Collection $platform = $this->platform(); - [$numericTags, $alphaTags] = collect($response['results']) - ->when($this->isArm($platform), $this->onlyArmImagesFilter(), $this->onlyNonArmImagesFilter()) + return collect($response['results']) + ->when(in_array($platform, $this->armArchitectures, true), $this->onlyArmImagesFilter()) + ->when(! in_array($platform, $this->armArchitectures, true), $this->onlyNonArmImagesFilter()) ->pluck('name') - ->partition(function ($tag) { - return is_numeric($tag[0]); - }); - - $sortedTags = $alphaTags->sortDesc(SORT_NATURAL) - ->concat($numericTags->sortDesc(SORT_NATURAL)); - - if ($sortedTags->contains('latest')) { - $sortedTags->splice($sortedTags->search('latest'), 1); - $sortedTags->prepend('latest'); - } - - return $sortedTags->values()->filter(); + ->sort(new VersionComparator) + ->values(); } protected function onlyArmImagesFilter() { return function ($tags) { return $tags->filter(function ($tag) { - return collect($tag['images']) - ->pluck('architecture') - ->first(function (string $platform) { - return $this->isArm($platform); - }); + $supportedArchs = collect($tag['images'])->pluck('architecture'); + + foreach ($this->armArchitectures as $arch) { + if ($supportedArchs->contains($arch)) { + return true; + } + } + + return false; }); }; } @@ -81,11 +76,16 @@ protected function onlyNonArmImagesFilter() { return function ($tags) { return $tags->filter(function ($tag) { - return collect($tag['images']) + $supportedArchitectures = collect($tag['images']) ->pluck('architecture') - ->first(function (string $platform) { - return ! $this->isArm($platform); - }); + ->unique() + ->values(); + + // When removing the arm64 option from the list, there should + // still be other options in the supported architectures + // so we can consider that the tag is not arm-only. + + return $supportedArchitectures->diff($this->armArchitectures)->count() > 0; }); }; } @@ -95,11 +95,6 @@ protected function platform(): string return php_uname('m'); } - protected function isArm(string $platform): bool - { - return in_array($platform, ['arm64', 'aarch64']); - } - protected function getTagsResponse(): StreamInterface { return $this->guzzle diff --git a/app/Shell/ElasticDockerTags.php b/app/Shell/ElasticDockerTags.php index 75b426b1..2efc750f 100644 --- a/app/Shell/ElasticDockerTags.php +++ b/app/Shell/ElasticDockerTags.php @@ -14,7 +14,12 @@ public function getTags(): Collection ->reverse() ->filter(function ($tag) { return ! Str::contains($tag, 'SNAPSHOT'); - }); + }) + ->filter(function ($tag) { + return ! Str::startsWith($tag, 'sha256-'); + }) + ->sort(new VersionComparator) + ->values(); } protected function getAuthResponse(): StreamInterface diff --git a/app/Shell/GitHubDockerTags.php b/app/Shell/GitHubDockerTags.php new file mode 100644 index 00000000..63602ec6 --- /dev/null +++ b/app/Shell/GitHubDockerTags.php @@ -0,0 +1,57 @@ +getTagsResponse(), true)['tags']) + ->sort(new VersionComparator) + ->values(); + } + + public function getLatestTag(): string + { + return $this->getTags()->first(); + } + + protected function getTagsResponse(): StreamInterface + { + $token = $this->getToken(); + + return $this->guzzle + ->get($this->buildTagsUrl(), [ + 'headers' => [ + 'Authorization' => "Bearer {$token}", + ], + ]) + ->getBody(); + } + + protected function getToken(): string + { + $image = $this->service->imageName(); + + $response = $this->guzzle->get('https://ghcr.io/token?' . http_build_query([ + 'scope' => "repository:{$image}:pull", + ]), [ + 'http_errors' => false, + ]); + + if ($response->getStatusCode() !== 200) { + throw new RuntimeException("Something went wrong getting the Token from GitHub's registry."); + } + + return json_decode($response->getBody(), true)['token']; + } + + protected function tagsUrlTemplate(): string + { + return 'https://%s/v2/%s/tags/list'; + } +} diff --git a/app/Shell/MicrosoftDockerTags.php b/app/Shell/MicrosoftDockerTags.php index 6197676c..054ee5d1 100644 --- a/app/Shell/MicrosoftDockerTags.php +++ b/app/Shell/MicrosoftDockerTags.php @@ -14,7 +14,7 @@ public function getLatestTag(): string public function getTags(): Collection { return collect(json_decode($this->getTagsResponse(), true)['tags']) - ->reverse() + ->sort(new VersionComparator) ->values(); } diff --git a/app/Shell/MinioDockerTags.php b/app/Shell/MinioDockerTags.php index 04f0bfc4..616283bc 100644 --- a/app/Shell/MinioDockerTags.php +++ b/app/Shell/MinioDockerTags.php @@ -29,23 +29,13 @@ public function getLatestTag(): string public function getTags(): Collection { $response = json_decode($this->getTagsResponse()->getContents(), true); - $tags = collect($response['results'])->map->name->reject(function ($tag) { - return Str::endsWith($tag, 'fips'); - }); - - [$releaseTags, $otherTags] = $tags - ->partition(function ($tag) { - return Str::startsWith($tag, 'RELEASE.'); - }); - - $sortedTags = $releaseTags->sortDesc(SORT_NATURAL) - ->concat($otherTags->sortDesc(SORT_NATURAL)); - - if ($sortedTags->contains('latest')) { - $sortedTags->splice($sortedTags->search('latest'), 1); - $sortedTags->prepend('latest'); - } - return $sortedTags; + return collect($response['results']) + ->pluck('name') + ->reject(function ($tag) { + return Str::endsWith($tag, 'fips'); + }) + ->sort(new VersionComparator) + ->values(); } } diff --git a/app/Shell/MongoDockerTags.php b/app/Shell/MongoDockerTags.php index 8e83a2ff..21dc8340 100644 --- a/app/Shell/MongoDockerTags.php +++ b/app/Shell/MongoDockerTags.php @@ -11,10 +11,11 @@ public function getTags(): Collection { $response = json_decode($this->getTagsResponse()->getContents(), true); return collect($response['results']) - ->map - ->name + ->pluck('name') + ->sort(new VersionComparator) ->filter(function ($tag) { return ! Str::contains($tag, 'windowsservercore'); - }); + }) + ->values(); } } diff --git a/app/Shell/QuayDockerTags.php b/app/Shell/QuayDockerTags.php index efbabb3f..98c09710 100644 --- a/app/Shell/QuayDockerTags.php +++ b/app/Shell/QuayDockerTags.php @@ -20,10 +20,7 @@ public function getLatestTag(): string public function getTags(): Collection { return collect(json_decode($this->getTagsResponse()->getContents(), true)['tags']) - ->map(function ($release) { - return $release['name']; - }) - ; + ->pluck('name'); } protected function tagsUrlTemplate(): string diff --git a/app/Shell/VersionComparator.php b/app/Shell/VersionComparator.php new file mode 100644 index 00000000..8609c58d --- /dev/null +++ b/app/Shell/VersionComparator.php @@ -0,0 +1,49 @@ +startsAsSemver($a) && ! $this->startsAsSemver($b)) { + return -1; + } + + if ($this->startsAsSemver($b) && ! $this->startsAsSemver($a)) { + return 1; + } + + if ($this->stableSemver($a) && ! $this->stableSemver($b)) { + return -1; + } + + if ($this->stableSemver($b) && ! $this->stableSemver($a)) { + return 1; + } + + return Comparator::greaterThan(preg_replace('/^v/', '', $a), preg_replace('/^v/', '', $b)) ? -1 : 1; + } + + private function stableSemver(string $version): bool + { + return preg_match('/^v?[\d.]+$/', $version); + } + + private function startsAsSemver(string $version): bool + { + return preg_match('/^v?[\d.]+/', $version); + } +} diff --git a/builds/takeout b/builds/takeout index 0e0ab5f5..b622ea64 100755 Binary files a/builds/takeout and b/builds/takeout differ diff --git a/composer.json b/composer.json index e0170dfd..702d9367 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "ext-json": "*", "ext-pcntl": "*", "ext-posix": "*", - "guzzlehttp/psr7": "^1.7" + "composer/semver": "^3.4", + "guzzlehttp/psr7": "^2.6" }, "require-dev": { "guzzlehttp/guzzle": "^7.4", diff --git a/tests/Feature/DockerTagsTest.php b/tests/Feature/DockerTagsTest.php index 81c7da4f..5efe064d 100644 --- a/tests/Feature/DockerTagsTest.php +++ b/tests/Feature/DockerTagsTest.php @@ -49,7 +49,7 @@ function it_sorts_the_versions_naturally() $tags = collect($dockerTags->getTags()); $this->assertEquals('latest', $tags->shift()); - $this->assertEquals('buster', $tags->shift()); + $this->assertEquals('16.2', $tags->shift()); } /** diff --git a/tests/Feature/GitHubTagsTest.php b/tests/Feature/GitHubTagsTest.php new file mode 100644 index 00000000..a7d2b3db --- /dev/null +++ b/tests/Feature/GitHubTagsTest.php @@ -0,0 +1,58 @@ +mockImagesResponseHandler()); + $client = new Client(['handler' => $handlerStack]); + + /** @var GitHubDockerTags $dockerTags */ + $dockerTags = M::mock(GitHubDockerTags::class, [$client, app(Buggregator::class)])->makePartial(); + + $this->assertEquals('latest', $dockerTags->getLatestTag()); + } + + /** @test */ + function it_throws_exception_when_token_request_fails() + { + $handlerStack = HandlerStack::create($this->mockImagesResponseHandler(false)); + $client = new Client(['handler' => $handlerStack]); + + /** @var GitHubDockerTags $dockerTags */ + $dockerTags = M::mock(GitHubDockerTags::class, [$client, app(Buggregator::class)])->makePartial(); + + $this->expectException(RuntimeException::class); + + $dockerTags->getLatestTag(); + } + + private function mockImagesResponseHandler($tokenWorks = true) + { + return new MockHandler([ + new Response($tokenWorks ? 200 : 400, [], json_encode([ + 'token' => 'fake-token', + ])), + new Response(200, [], json_encode([ + 'tags' => [ + 'latest', + '1.0.0', + '1.0.0.rc-1', + ], + ])), + ]); + } +} diff --git a/tests/Feature/VersionComparatorTest.php b/tests/Feature/VersionComparatorTest.php new file mode 100644 index 00000000..0c771986 --- /dev/null +++ b/tests/Feature/VersionComparatorTest.php @@ -0,0 +1,32 @@ +assertEquals($expectedOrder, $this->sort($versions)); + } + + private function sort(array $versions): array + { + return collect($versions) + ->sort(new VersionComparator) + ->values() + ->all(); + } +}