Skip to content

Commit

Permalink
Merge pull request #9 from aschmelyun/feature-ssl-support
Browse files Browse the repository at this point in the history
Adds SSL support
  • Loading branch information
aschmelyun authored Mar 5, 2023
2 parents c0c4298 + 6946475 commit 9002180
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 84 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ And your site will be available at the domain you provided!

> Note: If you chose a domain that doesn't end in `.localhost`, you will need to add an entry to your hosts file to direct traffic to 127.0.0.1
## Local SSL

Fleet supports local SSL on your custom domains through the power of [mkcert](https://mkcert.dev). After you've installed it on your machine, you can use the `--ssl` option when using the `fleet:add` command to enable it for your application.

```bash
php artisan fleet:add my-app.localhost --ssl
```

A local certificate will be generated and stored in `~/.config/mkcert/certs`. After spinning up your site with Sail, your specified domain will have https enabled.

## Additional Usage

By default, whenever you use `fleet:add`, a Docker network and container are both started to handle the traffic from your local domain name(s).
Expand Down
1 change: 0 additions & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ parameters:
paths:
- src
- config
- database
tmpDir: build/phpstan
checkOctaneCompatibility: true
checkModelProperties: true
Expand Down
96 changes: 62 additions & 34 deletions src/Commands/FleetAddCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@
namespace Aschmelyun\Fleet\Commands;

use Aschmelyun\Fleet\Fleet;
use Aschmelyun\Fleet\Support\Filesystem;
use Composer\InstalledVersions;
use Illuminate\Console\Command;
use Symfony\Component\Yaml\Yaml;

class FleetAddCommand extends Command
{
public $signature = 'fleet:add {domain?}';
public $signature = 'fleet:add
{domain? : The test domain to use}
{--ssl : Include local SSL with mkcert}';

public $description = 'Installs Fleet support onto the current application';

public function handle(): int
public function handle(Filesystem $filesystem): int
{
// set the domain to the one the user provided, or ask what it should be
$domain = $this->argument('domain');
if (!$domain) {
if (! $domain) {
$domain = $this->ask('What domain name would you like to use for this app?', 'laravel.localhost');
}

Expand All @@ -27,13 +30,13 @@ public function handle(): int
}

// determine if laravel/sail is a required-dev package in the root composer file
if (!InstalledVersions::isInstalled('laravel/sail')) {
if (! InstalledVersions::isInstalled('laravel/sail')) {
$this->error(' Laravel Sail is required for this package');
$this->line(' For more information, check out https://laravel.com/docs/sail#installation');
}

// if the docker-compose.yml file isn't available, publish it
if (!file_exists(base_path('docker-compose.yml')) && !file_exists(base_path('docker-compose.backup.yml'))) {
if (! file_exists(base_path('docker-compose.yml')) && ! file_exists(base_path('docker-compose.backup.yml'))) {
$this->info('No docker-compose.yml file available, running sail:install...');
$this->call('sail:install');
}
Expand All @@ -45,74 +48,99 @@ public function handle(): int
}
}

// determine what port 8081+ is available
// determine what port 8081+ is available and set it as the APP_PORT
$port = 8081;
while ($this->isPortTaken($port)) {
$port++;
}

$file = base_path('.env');
if (!file_exists($file)) {
$this->error("Application .env file is missing, can't continue");
try {
$filesystem->writeToEnvFile('APP_PORT', $port);
} catch (\Exception $e) {
$this->error($e->getMessage());

return self::FAILURE;
}

$env = file_get_contents($file);
$env = explode("\n", $env);

$filteredEnvAppPort = array_filter($env, fn ($line) => str_starts_with($line, 'APP_PORT'));
if (!empty($filteredEnvAppPort)) {
$env[key($filteredEnvAppPort)] = "APP_PORT={$port}";
} else {
$insert = ["APP_PORT={$port}"];
array_splice($env, 5, 0, $insert);
}

file_put_contents(base_path('.env'), implode("\n", $env));

// add a modified docker-compose.yml file to include traefik labels
$file = base_path('docker-compose.backup.yml');
if (!file_exists($file)) {
if (! file_exists($file)) {
$file = base_path('docker-compose.yml');
}

if (!file_exists($file)) {
if (! file_exists($file)) {
$this->error('A docker-compose.yml file or a docker-compose.backup.yml file does not exist');

return self::FAILURE;
}

$yaml = $this->generateYamlForDockerCompose($file, $domain, $filesystem);

// determine if the user wants to use SSL and add support if so
if ($this->option('ssl')) {
$this->info(' 🔒 Adding SSL support...');

try {
$filesystem->createCertificates($domain);
} catch (\Exception $e) {
$this->error($e->getMessage());
$this->line('For more information, try checking out the documentation at mkcert.dev');

return self::FAILURE;
}

try {
$filesystem->createSslConfig($domain);
} catch (\Exception $e) {
$this->error($e->getMessage());

return self::FAILURE;
}

$heading = str_replace('.', '-', $domain);
$yaml['services'][$heading]['labels'][] = "traefik.http.routers.{$heading}.tls=true";
}

file_put_contents(base_path('docker-compose.yml'), Yaml::dump($yaml, 6));

// call fleet:start to determine if the fleet network and traefik container is up
$this->call('fleet:start');

// return info back to the user
$this->info(' ✨ All done! You can now run `./vendor/bin/sail up`');
$this->newLine();

return self::SUCCESS;
}

private function generateYamlForDockerCompose(string $file, string $domain, Filesystem $filesystem): array
{
$yaml = Yaml::parseFile($file);

$heading = str_replace('.', '-', $domain);

// resets the top service key to the domain name
$service = $yaml['services'][array_keys($yaml['services'])[0]];
unset($yaml['services'][array_keys($yaml['services'])[0]]);

// adds the entire services array back with the new domain key
$yaml['services'] = [$heading => $service, ...$yaml['services']];

// adds the traefik labels to the yaml file
$yaml['services'][$heading]['networks'][] = 'fleet';
$yaml['services'][$heading]['labels'] = [
"traefik.http.routers.{$heading}.rule=Host(`{$domain}`)",
"traefik.http.services.{$heading}.loadbalancer.server.port=80",
];

// removes port binding for our app service
unset($yaml['services'][$heading]['ports'][0]);
$yaml['services'][$heading]['ports'] = array_values($yaml['services'][$heading]['ports']);

// adds the fleet network
$yaml['networks']['fleet']['external'] = true;

file_put_contents(base_path('docker-compose.yml'), Yaml::dump($yaml, 6));

// call fleet:start to determine if the fleet network and traefik container is up
$this->call('fleet:start');

// return info back to the user
$this->info(' ✨ All done! You can now run `./vendor/bin/sail up`');
$this->newLine();

return self::SUCCESS;
return $yaml;
}

private function isPortTaken($port): bool
Expand Down
38 changes: 18 additions & 20 deletions src/Commands/FleetRemoveCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Aschmelyun\Fleet\Commands;

use Aschmelyun\Fleet\Support\Filesystem;
use Illuminate\Console\Command;
use Symfony\Component\Yaml\Yaml;

Expand All @@ -11,18 +12,18 @@ class FleetRemoveCommand extends Command

public $description = 'Removes Fleet support from the current application';

public function handle(): int
public function handle(Filesystem $filesystem): int
{
// determine if fleet is installed, return a response if not
$file = base_path('docker-compose.yml');
if (!file_exists($file)) {
if (! file_exists($file)) {
$this->error('A docker-compose.yml file does not exist');

return self::FAILURE;
}

$yaml = Yaml::parseFile($file);
if (!isset($yaml['networks']['fleet'])) {
if (! isset($yaml['networks']['fleet'])) {
$this->info(' Fleet is not currently installed on this application');

return self::SUCCESS;
Expand All @@ -40,37 +41,34 @@ public function handle(): int
}

// remove all fleet additions to the .env and docker-compose.yml files
$file = base_path('.env');
if (file_exists($file)) {
$env = file_get_contents($file);
$env = explode("\n", $env);

foreach ($env as $index => $line) {
if (str_starts_with($line, 'APP_PORT')) {
unset($env[$index]);
}
}
$filesystem->removeFromEnvFile('APP_PORT');
$this->removeYamlFromDockerCompose($yaml);

file_put_contents(base_path('.env'), implode("\n", $env));
}
// return info back to the user
$this->info(' ✨ All done! Fleet has been successfully removed from this application');

return self::SUCCESS;
}

private function removeYamlFromDockerCompose(array $yaml): void
{
// remove the custom domain as the first service key
$service = $yaml['services'][array_keys($yaml['services'])[0]];
unset($yaml['services'][array_keys($yaml['services'])[0]]);

// and replace it with the default, laravel.test
$yaml['services'] = ['laravel.test' => $service, ...$yaml['services']];

// reset the networks and labels
$yaml['services']['laravel.test']['networks'] = ['sail'];
unset($yaml['services']['laravel.test']['labels']);

// reset the ports
$yaml['services']['laravel.test']['ports'][] = '${APP_PORT:-80}:80';

// remove the fleet network
unset($yaml['networks']['fleet']);

file_put_contents(base_path('docker-compose.yml'), Yaml::dump($yaml, 6));

// return info back to the user
$this->info(' ✨ All done! Fleet has been successfully removed from this application');

return self::SUCCESS;
}
}
37 changes: 22 additions & 15 deletions src/Commands/FleetStartCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

namespace Aschmelyun\Fleet\Commands;

use Aschmelyun\Fleet\Fleet;
use Aschmelyun\Fleet\Support\Docker;
use Aschmelyun\Fleet\Support\Filesystem;
use Illuminate\Console\Command;

class FleetStartCommand extends Command
Expand All @@ -11,33 +12,39 @@ class FleetStartCommand extends Command

public $description = 'Starts up the Fleet network and Traefik container';

public function handle(): int
public function handle(Filesystem $filesystem, Docker $docker): int
{
// is the fleet docker network running? if not, start it up
$process = Fleet::process('docker network ls --filter name=^fleet$ --format {{.ID}}');

if (!$process->getOutput()) {
if (! $docker->getNetwork('fleet')) {
$this->info('No Fleet network, creating one...');

$process = Fleet::process('docker network create fleet');
if (!$process->isSuccessful()) {
try {
$id = $docker->createNetwork('fleet');
} catch (\Exception $e) {
$this->error('Could not start Fleet Docker network');
$this->line($e->getMessage());

return self::FAILURE;
}

$this->line($process->getOutput());
$this->line($id);
}

// is the fleet traefik container running? if not, start it up
$process = Fleet::process('docker ps --filter name=^fleet$ --format {{.ID}}');
// just in case the mkcert directory doesn't exist, create it
$filesystem->createSslDirectories();

if (!$process->getOutput()) {
// is the fleet traefik container running? if not, start it up
if (! $docker->getContainer('fleet')) {
$this->info('No Fleet container, spinning it up...');
$process = Fleet::process(
'docker run -d -p 8080:8080 -p 80:80 --network=fleet -v /var/run/docker.sock:/var/run/docker.sock --name=fleet traefik:v2.9 --api.insecure=true --providers.docker',
true
);

try {
$docker->startFleetTraefikContainer();
} catch (\Exception $e) {
$this->error('Could not start Fleet Traefik container');
$this->line($e->getMessage());

return self::FAILURE;
}
}

return self::SUCCESS;
Expand Down
31 changes: 17 additions & 14 deletions src/Commands/FleetStopCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Aschmelyun\Fleet\Commands;

use Aschmelyun\Fleet\Fleet;
use Aschmelyun\Fleet\Support\Docker;
use Illuminate\Console\Command;

class FleetStopCommand extends Command
Expand All @@ -11,29 +12,31 @@ class FleetStopCommand extends Command

public $description = 'Stops and removes the Fleet network and app containers';

public function handle(): int
public function handle(Docker $docker): int
{
if (!$this->confirm('This will stop and remove all Sail instances running on the Fleet network, do you want to continue?')) {
if (! $this->confirm('This will stop and remove all Sail instances running on the Fleet network, do you want to continue?')) {
return self::SUCCESS;
}

// stop and remove all docker containers running on the fleet network
$process = Fleet::process('docker ps -a --filter network=fleet --format {{.ID}}');
try {
$docker->removeContainers('fleet');
} catch (\Exception $e) {
$this->error('Could not remove Fleet containers');
$this->line($e->getMessage());

$ids = explode("\n", $process->getOutput());
foreach (array_filter($ids) as $id) {
$this->line("Removing container {$id}");

$process = Fleet::process("docker rm -f {$id}");
if (!$process->isSuccessful()) {
$this->error("Error removing container {$id}");

return self::FAILURE;
}
return self::FAILURE;
}

// remove the fleet docker network
$process = Fleet::process('docker network rm fleet');
try {
$docker->removeNetwork('fleet');
} catch (\Exception $e) {
$this->error('Could not remove Fleet network');
$this->line($e->getMessage());

return self::FAILURE;
}

$this->info(' Fleet has been successfully stopped and all active containers have been removed');

Expand Down
Loading

0 comments on commit 9002180

Please sign in to comment.