├── .release-please-manifest.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── config
└── services.yml
├── release-please-config.json
├── src
├── ApiClient.php
├── Application.php
├── Build
│ ├── BuildContainerImageStep.php
│ ├── BuildStepInterface.php
│ ├── CompressBuildFilesStep.php
│ ├── CopyMustUsePluginStep.php
│ ├── CopyProjectFilesStep.php
│ ├── CopyUploadsDirectoryStep.php
│ ├── DebugBuildStep.php
│ ├── DownloadWpCliStep.php
│ ├── EnsureIntegrationIsInstalledStep.php
│ ├── ExecuteBuildCommandsStep.php
│ ├── ExtractAssetFilesStep.php
│ └── ModifyWordPressConfigurationStep.php
├── CliConfiguration.php
├── Command
│ ├── AbstractCommand.php
│ ├── AbstractInvocationCommand.php
│ ├── AbstractProjectCommand.php
│ ├── Cache
│ │ ├── AbstractCacheCommand.php
│ │ ├── CacheTunnelCommand.php
│ │ ├── CreateCacheCommand.php
│ │ ├── DeleteCacheCommand.php
│ │ ├── ListCachesCommand.php
│ │ └── ModifyCacheCommand.php
│ ├── Certificate
│ │ ├── AbstractCertificateCommand.php
│ │ ├── DeleteCertificateCommand.php
│ │ ├── GetCertificateInfoCommand.php
│ │ ├── ListCertificatesCommand.php
│ │ └── RequestCertificateCommand.php
│ ├── Database
│ │ ├── AbstractDatabaseCommand.php
│ │ ├── AbstractDatabaseServerCommand.php
│ │ ├── CreateDatabaseCommand.php
│ │ ├── CreateDatabaseServerCommand.php
│ │ ├── CreateDatabaseUserCommand.php
│ │ ├── DatabaseServerTunnelCommand.php
│ │ ├── DeleteDatabaseCommand.php
│ │ ├── DeleteDatabaseServerCommand.php
│ │ ├── DeleteDatabaseUserCommand.php
│ │ ├── ExportDatabaseCommand.php
│ │ ├── GetDatabaseServerInfoCommand.php
│ │ ├── ImportDatabaseCommand.php
│ │ ├── ListDatabaseServersCommand.php
│ │ ├── ListDatabaseUsersCommand.php
│ │ ├── ListDatabasesCommand.php
│ │ ├── LockDatabaseServerCommand.php
│ │ ├── ModifyDatabaseServerCommand.php
│ │ ├── RotateDatabaseServerPasswordCommand.php
│ │ ├── RotateDatabaseUserPasswordCommand.php
│ │ └── UnlockDatabaseServerCommand.php
│ ├── Dns
│ │ ├── AbstractDnsCommand.php
│ │ ├── ChangeDnsRecordCommand.php
│ │ ├── CreateDnsZoneCommand.php
│ │ ├── DeleteDnsRecordCommand.php
│ │ ├── DeleteDnsZoneCommand.php
│ │ ├── ImportDnsRecordsCommand.php
│ │ ├── ListDnsRecordsCommand.php
│ │ └── ListDnsZonesCommand.php
│ ├── Docker
│ │ ├── CreateDockerfileCommand.php
│ │ └── DeleteDockerImagesCommand.php
│ ├── Email
│ │ ├── AbstractEmailIdentityCommand.php
│ │ ├── CreateEmailIdentityCommand.php
│ │ ├── DeleteEmailIdentityCommand.php
│ │ ├── GetEmailIdentityInfoCommand.php
│ │ └── ListEmailIdentitiesCommand.php
│ ├── Environment
│ │ ├── AbstractEnvironmentLogsCommand.php
│ │ ├── ChangeEnvironmentDomainCommand.php
│ │ ├── ChangeEnvironmentSecretCommand.php
│ │ ├── ChangeEnvironmentVariableCommand.php
│ │ ├── CreateEnvironmentCommand.php
│ │ ├── DeleteEnvironmentCommand.php
│ │ ├── DeleteEnvironmentSecretCommand.php
│ │ ├── DownloadEnvironmentVariablesCommand.php
│ │ ├── GetEnvironmentInfoCommand.php
│ │ ├── GetEnvironmentMetricsCommand.php
│ │ ├── GetEnvironmentUrlCommand.php
│ │ ├── InvalidateEnvironmentCacheCommand.php
│ │ ├── ListEnvironmentSecretsCommand.php
│ │ ├── ListEnvironmentsCommand.php
│ │ ├── QueryEnvironmentLogsCommand.php
│ │ ├── UploadEnvironmentVariablesCommand.php
│ │ └── WatchEnvironmentLogsCommand.php
│ ├── InstallIntegrationCommand.php
│ ├── LoginCommand.php
│ ├── Network
│ │ ├── AddBastionHostCommand.php
│ │ ├── AddNatGatewayCommand.php
│ │ ├── CreateNetworkCommand.php
│ │ ├── DeleteNetworkCommand.php
│ │ ├── ListNetworksCommand.php
│ │ ├── RemoveBastionHostCommand.php
│ │ └── RemoveNatGatewayCommand.php
│ ├── Php
│ │ ├── PhpInfoCommand.php
│ │ └── PhpVersionCommand.php
│ ├── Project
│ │ ├── AbstractProjectDeploymentCommand.php
│ │ ├── BuildProjectCommand.php
│ │ ├── ConfigureProjectCommand.php
│ │ ├── DeleteProjectCommand.php
│ │ ├── DeployProjectCommand.php
│ │ ├── GetProjectInfoCommand.php
│ │ ├── InitializeProjectCommand.php
│ │ ├── ListProjectsCommand.php
│ │ ├── RedeployProjectCommand.php
│ │ ├── RollbackProjectCommand.php
│ │ └── ValidateProjectCommand.php
│ ├── Provider
│ │ ├── AbstractProviderCommand.php
│ │ ├── ConnectProviderCommand.php
│ │ ├── DeleteProviderCommand.php
│ │ ├── ListProvidersCommand.php
│ │ └── UpdateProviderCommand.php
│ ├── Team
│ │ ├── CreateTeamCommand.php
│ │ ├── CurrentTeamCommand.php
│ │ ├── ListTeamsCommand.php
│ │ └── SelectTeamCommand.php
│ ├── Uploads
│ │ └── ImportUploadsCommand.php
│ └── WpCliCommand.php
├── Console
│ ├── ChoiceQuestion.php
│ ├── HiddenInputOption.php
│ ├── Input.php
│ ├── InputDefinition.php
│ └── Output.php
├── Database
│ ├── Connection.php
│ ├── Mysqldump.php
│ └── PDO.php
├── Deployment
│ ├── DeploymentStepInterface.php
│ ├── ProcessAssetsStep.php
│ ├── StartAndMonitorDeploymentStep.php
│ └── UploadFunctionCodeStep.php
├── Dockerfile.php
├── EventDispatcher
│ └── AutowiredEventDispatcher.php
├── EventListener
│ ├── CleanupSubscriber.php
│ ├── LoadProjectConfigurationSubscriber.php
│ └── VersionCheckSubscriber.php
├── Exception
│ ├── CommandCancelledException.php
│ ├── Executable
│ │ ├── ExecutableNotDetectedException.php
│ │ ├── SshPortInUseException.php
│ │ └── WpCliException.php
│ ├── InvalidInputException.php
│ ├── NonInteractiveRequiredArgumentException.php
│ └── NonInteractiveRequiredOptionException.php
├── Executable
│ ├── AbstractExecutable.php
│ ├── ComposerExecutable.php
│ ├── DockerExecutable.php
│ ├── ExecutableInterface.php
│ ├── SshExecutable.php
│ └── WpCliExecutable.php
├── FileUploader.php
├── GitHubClient.php
├── Process
│ └── Process.php
├── Project
│ ├── Configuration
│ │ ├── CacheConfigurationChange.php
│ │ ├── ConfigurationChangeInterface.php
│ │ ├── DomainConfigurationChange.php
│ │ ├── ImageDeploymentConfigurationChange.php
│ │ ├── ProjectConfiguration.php
│ │ └── WordPress
│ │ │ ├── AbstractWordPressConfigurationChange.php
│ │ │ ├── BeaverBuilderConfigurationChange.php
│ │ │ ├── CloudflareConfigurationChange.php
│ │ │ ├── ElementorConfigurationChange.php
│ │ │ ├── OxygenConfigurationChange.php
│ │ │ ├── WooCommerceConfigurationChange.php
│ │ │ └── WordPressConfigurationChangeInterface.php
│ └── Type
│ │ ├── AbstractProjectType.php
│ │ ├── AbstractWordPressProjectType.php
│ │ ├── BedrockProjectType.php
│ │ ├── InstallableProjectTypeInterface.php
│ │ ├── ProjectTypeInterface.php
│ │ ├── RadicleProjectType.php
│ │ └── WordPressProjectType.php
└── Support
│ └── Arr.php
├── stubs
├── Dockerfile
├── activate-ymir-plugin.php
└── ymir-config.php
└── ymir
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "1.51.1"
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) Carl Alexander
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # Ymir CLI
8 |
9 | The [Ymir][1] command-line tool.
10 |
11 | ## Requirements
12 |
13 | * PHP >= 7.2.5
14 |
15 | ## Installation
16 |
17 | Install the Ymir CLI in your project using composer:
18 |
19 | ```
20 | $ composer require ymirapp/cli
21 | ```
22 |
23 | Or globally:
24 |
25 | ```
26 | $ composer global require ymirapp/cli
27 | ```
28 |
29 | ## Contributing
30 |
31 | Install dependencies using composer:
32 |
33 | ```console
34 | $ composer install
35 | ```
36 |
37 | ## Links
38 |
39 | * [Documentation][2]
40 |
41 | [1]: https://ymirapp.com
42 | [2]: https://docs.ymirapp.com
43 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ymirapp/cli",
3 | "description": "Ymir command-line tool",
4 | "type": "project",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Carl Alexander",
9 | "email": "support@ymirapp.com",
10 | "homepage": "https://ymirapp.com"
11 | }
12 | ],
13 | "bin": [
14 | "ymir"
15 | ],
16 | "require": {
17 | "php": ">=7.2.5",
18 | "ext-json": "*",
19 | "ext-pdo": "*",
20 | "ext-zip": "*",
21 | "ext-zlib": "*",
22 | "guzzlehttp/guzzle": "^7.0",
23 | "ifsnop/mysqldump-php": "^2.12",
24 | "illuminate/collections": "^8.0|^9.0|^10.0|^11.0",
25 | "league/flysystem": "^2.1.1|^3.0",
26 | "league/flysystem-ftp": "^2.0|^3.0",
27 | "league/flysystem-sftp-v3": "^2.0|^3.0",
28 | "nesbot/carbon": "^2.40",
29 | "symfony/config": "^5.4|^6.0",
30 | "symfony/console": "^5.4|^6.0",
31 | "symfony/dependency-injection": "^5.4|^6.0",
32 | "symfony/event-dispatcher": "^5.4|^6.0",
33 | "symfony/filesystem": "^5.4|^6.0",
34 | "symfony/finder": "^5.4.3|^6.0.3",
35 | "symfony/polyfill-php80": "^1.27",
36 | "symfony/process": "^5.4|^6.0",
37 | "symfony/yaml": "^5.4|^6.0",
38 | "ymirapp/ymir-sdk-php": "^1.3.0"
39 | },
40 | "require-dev": {
41 | "fakerphp/faker": "^1.17",
42 | "friendsofphp/php-cs-fixer": "^3.0",
43 | "php-parallel-lint/php-parallel-lint": "^1.1",
44 | "phpro/grumphp": "^1.0",
45 | "phpstan/phpstan": "^1.11.0",
46 | "phpunit/phpunit": "^9.3",
47 | "sebastian/phpcpd": "^6.0.3"
48 | },
49 | "config": {
50 | "allow-plugins": {
51 | "phpro/grumphp": true
52 | },
53 | "optimize-autoloader": true,
54 | "preferred-install": "dist",
55 | "sort-packages": true
56 | },
57 | "autoload": {
58 | "psr-4": {
59 | "Ymir\\Cli\\": "src"
60 | }
61 | },
62 | "autoload-dev": {
63 | "psr-4": {
64 | "Ymir\\Cli\\Tests\\": "tests"
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3 | "bootstrap-sha": "fe728980b0bd4cd46b5da158c05c193e4a7d0ab9",
4 | "plugins": [ "sentence-case" ],
5 | "skip-github-release": true,
6 | "packages": {
7 | ".": {
8 | "release-type": "php",
9 | "pull-request-title-pattern": "chore: release ${version}",
10 | "changelog-sections": [
11 | { "type" :"feat", "section" :"Features", "hidden": false },
12 | { "type":"fix", "section": "Bug Fixes", "hidden": false },
13 | { "type":"chore", "section": "Miscellaneous" ,"hidden":true }
14 | ]
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Application.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli;
15 |
16 | use Symfony\Component\Console\Application as BaseApplication;
17 | use Symfony\Component\Console\Input\InputDefinition;
18 | use Symfony\Component\Console\Input\InputOption;
19 | use Symfony\Component\Console\Output\OutputInterface;
20 | use Ymir\Cli\Exception\CommandCancelledException;
21 |
22 | class Application extends BaseApplication
23 | {
24 | /**
25 | * Constructor.
26 | */
27 | public function __construct(iterable $commands, string $version)
28 | {
29 | parent::__construct('Ymir', $version);
30 |
31 | foreach ($commands as $command) {
32 | $this->add($command);
33 | }
34 | }
35 |
36 | /**
37 | * {@inheritdoc}
38 | */
39 | public function renderThrowable(\Throwable $exception, OutputInterface $output): void
40 | {
41 | if ($exception instanceof CommandCancelledException) {
42 | return;
43 | }
44 |
45 | parent::renderThrowable($exception, $output);
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | protected function getDefaultInputDefinition(): InputDefinition
52 | {
53 | $definition = parent::getDefaultInputDefinition();
54 |
55 | $definition->addOption(new InputOption('ymir-file', null, InputOption::VALUE_OPTIONAL, 'Path to Ymir project configuration file', 'ymir.yml'));
56 |
57 | return $definition;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Build/BuildContainerImageStep.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Build;
15 |
16 | use Symfony\Component\Filesystem\Filesystem;
17 | use Symfony\Component\Process\Exception\RuntimeException;
18 | use Ymir\Cli\Executable\DockerExecutable;
19 | use Ymir\Cli\Project\Configuration\ProjectConfiguration;
20 |
21 | class BuildContainerImageStep implements BuildStepInterface
22 | {
23 | /**
24 | * The build directory where the project files are copied to.
25 | *
26 | * @var string
27 | */
28 | private $buildDirectory;
29 |
30 | /**
31 | * The Docker executable.
32 | *
33 | * @var DockerExecutable
34 | */
35 | private $dockerExecutable;
36 |
37 | /**
38 | * The file system.
39 | *
40 | * @var Filesystem
41 | */
42 | private $filesystem;
43 |
44 | /**
45 | * Constructor.
46 | */
47 | public function __construct(string $buildDirectory, DockerExecutable $dockerExecutable, Filesystem $filesystem)
48 | {
49 | $this->buildDirectory = rtrim($buildDirectory, '/');
50 | $this->dockerExecutable = $dockerExecutable;
51 | $this->filesystem = $filesystem;
52 | }
53 |
54 | /**
55 | * {@inheritdoc}
56 | */
57 | public function getDescription(): string
58 | {
59 | return 'Building container image';
60 | }
61 |
62 | /**
63 | * {@inheritdoc}
64 | */
65 | public function perform(string $environment, ProjectConfiguration $projectConfiguration)
66 | {
67 | $dockerfileName = 'Dockerfile';
68 |
69 | if ($this->filesystem->exists($this->buildDirectory.'/'.$environment.'.'.$dockerfileName)) {
70 | $dockerfileName = $environment.'.'.$dockerfileName;
71 | }
72 |
73 | if (!$this->filesystem->exists($this->buildDirectory.'/'.$dockerfileName)) {
74 | throw new RuntimeException('Unable to find a "Dockerfile" to build the container image');
75 | }
76 |
77 | $this->dockerExecutable->build($dockerfileName, sprintf('%s:%s', $projectConfiguration->getProjectName(), $environment), $this->buildDirectory);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Build/BuildStepInterface.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Build;
15 |
16 | use Ymir\Cli\Project\Configuration\ProjectConfiguration;
17 |
18 | interface BuildStepInterface
19 | {
20 | /**
21 | * Get the description of the build step.
22 | */
23 | public function getDescription(): string;
24 |
25 | /**
26 | * Perform the build step.
27 | */
28 | public function perform(string $environment, ProjectConfiguration $projectConfiguration);
29 | }
30 |
--------------------------------------------------------------------------------
/src/Build/CopyMustUsePluginStep.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Build;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Symfony\Component\Filesystem\Filesystem;
18 | use Ymir\Cli\Project\Configuration\ProjectConfiguration;
19 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType;
20 |
21 | class CopyMustUsePluginStep implements BuildStepInterface
22 | {
23 | /**
24 | * The build directory where the project files are copied to.
25 | *
26 | * @var string
27 | */
28 | private $buildDirectory;
29 |
30 | /**
31 | * The file system.
32 | *
33 | * @var Filesystem
34 | */
35 | private $filesystem;
36 |
37 | /**
38 | * The directory where the stub files are.
39 | *
40 | * @var string
41 | */
42 | private $stubDirectory;
43 |
44 | /**
45 | * Constructor.
46 | */
47 | public function __construct(string $buildDirectory, Filesystem $filesystem, string $stubDirectory)
48 | {
49 | $this->buildDirectory = rtrim($buildDirectory, '/');
50 | $this->filesystem = $filesystem;
51 | $this->stubDirectory = rtrim($stubDirectory, '/');
52 | }
53 |
54 | /**
55 | * {@inheritdoc}
56 | */
57 | public function getDescription(): string
58 | {
59 | return 'Copying Ymir must-use plugin';
60 | }
61 |
62 | /**
63 | * {@inheritdoc}
64 | */
65 | public function perform(string $environment, ProjectConfiguration $projectConfiguration)
66 | {
67 | $mupluginStub = 'activate-ymir-plugin.php';
68 | $mupluginStubPath = $this->stubDirectory.'/'.$mupluginStub;
69 |
70 | if (!$this->filesystem->exists($mupluginStubPath)) {
71 | throw new RuntimeException(sprintf('Cannot find "%s" stub file', $mupluginStub));
72 | }
73 |
74 | $projectType = $projectConfiguration->getProjectType();
75 |
76 | if (!$projectType instanceof AbstractWordPressProjectType) {
77 | throw new RuntimeException('You can only use this build step with WordPress projects');
78 | }
79 |
80 | $mupluginsDirectory = $projectType->getMustUsePluginsDirectoryPath($this->buildDirectory);
81 |
82 | if (!$this->filesystem->exists($mupluginsDirectory)) {
83 | $this->filesystem->mkdir($mupluginsDirectory);
84 | }
85 |
86 | $this->filesystem->copy($mupluginStubPath, $mupluginsDirectory.'/'.$mupluginStub);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Build/CopyUploadsDirectoryStep.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Build;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Symfony\Component\Filesystem\Filesystem;
18 | use Symfony\Component\Finder\Finder;
19 | use Symfony\Component\Finder\SplFileInfo;
20 | use Ymir\Cli\Project\Configuration\ProjectConfiguration;
21 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType;
22 |
23 | class CopyUploadsDirectoryStep implements BuildStepInterface
24 | {
25 | /**
26 | * The file system.
27 | *
28 | * @var Filesystem
29 | */
30 | private $filesystem;
31 |
32 | /**
33 | * The project directory where the uploads files are copied from.
34 | *
35 | * @var string
36 | */
37 | private $projectDirectory;
38 |
39 | /**
40 | * The build "uploads" directory where the files are copied to.
41 | *
42 | * @var string
43 | */
44 | private $uploadsDirectory;
45 |
46 | /**
47 | * Constructor.
48 | */
49 | public function __construct(Filesystem $filesystem, string $projectDirectory, string $uploadsDirectory)
50 | {
51 | $this->filesystem = $filesystem;
52 | $this->projectDirectory = rtrim($projectDirectory, '/');
53 | $this->uploadsDirectory = $uploadsDirectory;
54 | }
55 |
56 | /**
57 | * {@inheritdoc}
58 | */
59 | public function getDescription(): string
60 | {
61 | return 'Copying "uploads" directory';
62 | }
63 |
64 | /**
65 | * {@inheritdoc}
66 | */
67 | public function perform(string $environment, ProjectConfiguration $projectConfiguration)
68 | {
69 | $projectType = $projectConfiguration->getProjectType();
70 |
71 | if (!$projectType instanceof AbstractWordPressProjectType) {
72 | throw new RuntimeException('You can only use this build step with WordPress projects');
73 | }
74 |
75 | $files = Finder::create()->files()->in($projectType->getUploadsDirectoryPath($this->projectDirectory));
76 |
77 | foreach ($files as $file) {
78 | $this->copyFile($file);
79 | }
80 | }
81 |
82 | /**
83 | * Copy an individual file or directory.
84 | */
85 | private function copyFile(SplFileInfo $file)
86 | {
87 | if ($file->isDir()) {
88 | $this->filesystem->mkdir($this->uploadsDirectory.'/'.$file->getRelativePathname());
89 | } elseif ($file->isFile() && is_string($file->getRealPath())) {
90 | $this->filesystem->copy($file->getRealPath(), $this->uploadsDirectory.'/'.$file->getRelativePathname());
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/Build/DebugBuildStep.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Build;
15 |
16 | use Ymir\Cli\Project\Configuration\ProjectConfiguration;
17 |
18 | class DebugBuildStep implements BuildStepInterface
19 | {
20 | /**
21 | * {@inheritdoc}
22 | */
23 | public function getDescription(): string
24 | {
25 | return 'Debug mode: Press Enter to continue.';
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | public function perform(string $environment, ProjectConfiguration $projectConfiguration)
32 | {
33 | fgets(STDIN);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Build/DownloadWpCliStep.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Build;
15 |
16 | use Symfony\Component\Filesystem\Filesystem;
17 | use Ymir\Cli\Project\Configuration\ProjectConfiguration;
18 |
19 | class DownloadWpCliStep implements BuildStepInterface
20 | {
21 | /**
22 | * The WP-CLI version to download.
23 | */
24 | private const VERSION = '2.12.0';
25 |
26 | /**
27 | * The path to the WP-CLI bin directory.
28 | *
29 | * @var string
30 | */
31 | private $binDirectory;
32 |
33 | /**
34 | * The file system.
35 | *
36 | * @var Filesystem
37 | */
38 | private $filesystem;
39 |
40 | /**
41 | * Constructor.
42 | */
43 | public function __construct(string $buildDirectory, Filesystem $filesystem)
44 | {
45 | $this->binDirectory = rtrim($buildDirectory, '/').'/bin';
46 | $this->filesystem = $filesystem;
47 | }
48 |
49 | /**
50 | * {@inheritdoc}
51 | */
52 | public function getDescription(): string
53 | {
54 | return 'Downloading WP-CLI';
55 | }
56 |
57 | /**
58 | * {@inheritdoc}
59 | */
60 | public function perform(string $environment, ProjectConfiguration $projectConfiguration)
61 | {
62 | $wpCliPath = $this->binDirectory.'/wp';
63 |
64 | if (!$this->filesystem->exists($this->binDirectory)) {
65 | $this->filesystem->mkdir($this->binDirectory, 0755);
66 | }
67 |
68 | $this->filesystem->copy(sprintf('https://github.com/wp-cli/wp-cli/releases/download/v%1$s/wp-cli-%1$s.phar', self::VERSION), $wpCliPath, true);
69 | $this->filesystem->chmod($wpCliPath, 0755);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Build/EnsureIntegrationIsInstalledStep.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Build;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Ymir\Cli\Project\Configuration\ProjectConfiguration;
18 |
19 | class EnsureIntegrationIsInstalledStep implements BuildStepInterface
20 | {
21 | /**
22 | * The build directory where the project files are copied to.
23 | *
24 | * @var string
25 | */
26 | private $buildDirectory;
27 |
28 | /**
29 | * Constructor.
30 | */
31 | public function __construct(string $buildDirectory)
32 | {
33 | $this->buildDirectory = rtrim($buildDirectory, '/');
34 | }
35 |
36 | /**
37 | * {@inheritdoc}
38 | */
39 | public function getDescription(): string
40 | {
41 | return 'Ensuring Ymir integration is installed';
42 | }
43 |
44 | /**
45 | * {@inheritdoc}
46 | */
47 | public function perform(string $environment, ProjectConfiguration $projectConfiguration)
48 | {
49 | if (!$projectConfiguration->getProjectType()->isIntegrationInstalled($this->buildDirectory)) {
50 | throw new RuntimeException('Ymir integration is not installed in the build directory');
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Build/ExecuteBuildCommandsStep.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Build;
15 |
16 | use Ymir\Cli\Process\Process;
17 | use Ymir\Cli\Project\Configuration\ProjectConfiguration;
18 | use Ymir\Cli\Support\Arr;
19 |
20 | class ExecuteBuildCommandsStep implements BuildStepInterface
21 | {
22 | /**
23 | * The build directory where the project files are copied to.
24 | *
25 | * @var string
26 | */
27 | private $buildDirectory;
28 |
29 | /**
30 | * Constructor.
31 | */
32 | public function __construct(string $buildDirectory)
33 | {
34 | $this->buildDirectory = rtrim($buildDirectory, '/');
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | public function getDescription(): string
41 | {
42 | return 'Executing build commands';
43 | }
44 |
45 | /**
46 | * {@inheritdoc}
47 | */
48 | public function perform(string $environment, ProjectConfiguration $projectConfiguration)
49 | {
50 | $environment = $projectConfiguration->getEnvironment($environment);
51 |
52 | if (empty($environment['build'])) {
53 | return;
54 | }
55 |
56 | $commands = [];
57 |
58 | if (Arr::has($environment, 'build.commands')) {
59 | $commands = (array) Arr::get($environment, 'build.commands');
60 | } elseif (!Arr::has($environment, 'build.include')) {
61 | $commands = (array) $environment['build'];
62 | }
63 |
64 | foreach ($commands as $command) {
65 | Process::runShellCommandline($command, $this->buildDirectory, null, null, null);
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Build/ExtractAssetFilesStep.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Build;
15 |
16 | use Symfony\Component\Filesystem\Filesystem;
17 | use Symfony\Component\Finder\SplFileInfo;
18 | use Ymir\Cli\Project\Configuration\ProjectConfiguration;
19 |
20 | class ExtractAssetFilesStep implements BuildStepInterface
21 | {
22 | /**
23 | * The file system.
24 | *
25 | * @var Filesystem
26 | */
27 | private $filesystem;
28 |
29 | /**
30 | * The build directory where the asset files are extracted from.
31 | *
32 | * @var string
33 | */
34 | private $fromDirectory;
35 |
36 | /**
37 | * The assets directory where the asset files are copied to.
38 | *
39 | * @var string
40 | */
41 | private $toDirectory;
42 |
43 | /**
44 | * Constructor.
45 | */
46 | public function __construct(string $assetsDirectory, string $buildDirectory, Filesystem $filesystem)
47 | {
48 | $this->toDirectory = rtrim($assetsDirectory, '/');
49 | $this->fromDirectory = rtrim($buildDirectory, '/');
50 | $this->filesystem = $filesystem;
51 | }
52 |
53 | /**
54 | * {@inheritdoc}
55 | */
56 | public function getDescription(): string
57 | {
58 | return 'Extracting asset files';
59 | }
60 |
61 | /**
62 | * {@inheritdoc}
63 | */
64 | public function perform(string $environment, ProjectConfiguration $projectConfiguration)
65 | {
66 | if ($this->filesystem->exists($this->toDirectory)) {
67 | $this->filesystem->remove($this->toDirectory);
68 | }
69 |
70 | $this->filesystem->mkdir($this->toDirectory, 0755);
71 |
72 | foreach ($projectConfiguration->getProjectType()->getAssetFiles($this->fromDirectory) as $file) {
73 | $this->moveAssetFile($file);
74 | }
75 | }
76 |
77 | /**
78 | * Move the asset file to the assets directory.
79 | */
80 | private function moveAssetFile(SplFileInfo $file)
81 | {
82 | if (!$file->isFile() || !is_string($file->getRealPath())) {
83 | return;
84 | }
85 |
86 | $this->filesystem->copy($file->getRealPath(), $this->toDirectory.'/'.$file->getRelativePathname());
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Command/AbstractInvocationCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Ymir\Cli\Support\Arr;
18 |
19 | /**
20 | * Base command for invoking a project function.
21 | */
22 | abstract class AbstractInvocationCommand extends AbstractProjectCommand
23 | {
24 | /**
25 | * Invokes the environment console function with the given PHP command and returns the output.
26 | */
27 | protected function invokePhpCommand(string $command, string $environment, ?int $timeout = null): array
28 | {
29 | return $this->invokeEnvironmentFunction($environment, [
30 | 'php' => $command,
31 | ], $timeout);
32 | }
33 |
34 | /**
35 | * Invokes the environment console function with the given WP-CLI command and returns the output.
36 | */
37 | protected function invokeWpCliCommand(string $command, string $environment, ?int $timeout = null): array
38 | {
39 | if (str_starts_with($command, 'wp ')) {
40 | $command = substr($command, 3);
41 | }
42 |
43 | return $this->invokeEnvironmentFunction($environment, [
44 | 'php' => sprintf('bin/wp %s', $command),
45 | ], $timeout);
46 | }
47 |
48 | /**
49 | * Invokes the given environment console function with the given payload and returns the output.
50 | */
51 | private function invokeEnvironmentFunction(string $environment, array $payload, ?int $timeout = null): array
52 | {
53 | $invocationId = $this->apiClient->createInvocation($this->projectConfiguration->getProjectId(), $environment, $payload)->get('id');
54 |
55 | if (!is_int($invocationId)) {
56 | throw new \RuntimeException('Unable to create command invocation');
57 | }
58 |
59 | if (0 === $timeout) {
60 | return [];
61 | } elseif (!is_int($timeout)) {
62 | $timeout = (int) Arr::get($this->projectConfiguration->getEnvironment($environment), 'console.timeout', 60);
63 | }
64 |
65 | $invocation = $this->wait(function () use ($invocationId) {
66 | $invocation = $this->apiClient->getInvocation($invocationId);
67 |
68 | return !in_array($invocation->get('status'), ['pending', 'running']) ? $invocation->all() : [];
69 | }, $timeout);
70 |
71 | if (empty($invocation['status']) || 'failed' === $invocation['status']) {
72 | throw new RuntimeException('Running the command failed');
73 | } elseif (!Arr::has($invocation, ['result.exitCode', 'result.output'])) {
74 | throw new RuntimeException('Unable to get the result of the command from the Ymir API');
75 | }
76 |
77 | return $invocation['result'];
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Command/AbstractProjectCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command;
15 |
16 | use Symfony\Component\Console\Input\InputInterface;
17 | use Symfony\Component\Console\Output\OutputInterface;
18 |
19 | /**
20 | * Base command for interacting with a project.
21 | */
22 | abstract class AbstractProjectCommand extends AbstractCommand
23 | {
24 | /**
25 | * {@inheritdoc}
26 | */
27 | protected function execute(InputInterface $input, OutputInterface $output)
28 | {
29 | $this->projectConfiguration->validate();
30 |
31 | return parent::execute($input, $output);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Command/Cache/AbstractCacheCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Cache;
15 |
16 | use Illuminate\Support\Collection;
17 | use Symfony\Component\Console\Exception\RuntimeException;
18 | use Ymir\Cli\Command\AbstractCommand;
19 | use Ymir\Cli\Exception\InvalidInputException;
20 |
21 | abstract class AbstractCacheCommand extends AbstractCommand
22 | {
23 | /**
24 | * Determine the cache that the command is interacting with.
25 | */
26 | protected function determineCache(string $question): array
27 | {
28 | $caches = $this->apiClient->getCaches($this->cliConfiguration->getActiveTeamId());
29 | $cacheIdOrName = $this->input->getStringArgument('cache');
30 |
31 | if ($caches->isEmpty()) {
32 | throw new RuntimeException(sprintf('The currently active team has no cache clusters. You can create one with the "%s" command.', CreateCacheCommand::NAME));
33 | } elseif (empty($cacheIdOrName)) {
34 | $cacheIdOrName = $this->output->choiceWithResourceDetails($question, $caches);
35 | }
36 |
37 | $cache = $caches->firstWhere('id', $cacheIdOrName) ?? $caches->firstWhere('name', $cacheIdOrName);
38 |
39 | if (1 < $caches->where('name', $cacheIdOrName)->count()) {
40 | throw new RuntimeException(sprintf('Unable to select a cache cluster because more than one cache cluster has the name "%s"', $cacheIdOrName));
41 | } elseif (!is_array($cache) || empty($cache['id'])) {
42 | throw new InvalidInputException(sprintf('Unable to find a cache cluster with "%s" as the ID or name', $cacheIdOrName));
43 | }
44 |
45 | return $cache;
46 | }
47 |
48 | /**
49 | * Get the descriptions of the cache types for a given provider and engine.
50 | */
51 | protected function getCacheTypeDescriptions(int $providerId, string $engine): Collection
52 | {
53 | return $this->apiClient->getCacheTypes($providerId)->map(function (array $details) use ($engine) {
54 | return sprintf('%s vCPU, %sGiB RAM (~$%s/month)', $details['cpu'], $details['ram'], $details['price'][$engine]);
55 | });
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Command/Cache/DeleteCacheCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Cache;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\Network\RemoveNatGatewayCommand;
18 |
19 | class DeleteCacheCommand extends AbstractCacheCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'cache:delete';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Delete a cache cluster')
36 | ->addArgument('cache', InputArgument::OPTIONAL, 'The ID or name of the cache cluster to delete');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $cache = $this->determineCache('Which cache cluster would you like to delete');
45 |
46 | if (!$this->output->confirm('Are you sure you want to delete this cache cluster?', false)) {
47 | return;
48 | }
49 |
50 | $this->apiClient->deleteCache($cache['id']);
51 |
52 | $this->output->infoWithDelayWarning('Cache cluster deleted');
53 | $this->output->newLine();
54 | $this->output->note(sprintf('If you have no other resources using the private subnet, you should remove the network\'s NAT gateway using the "%s" command', RemoveNatGatewayCommand::NAME));
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Command/Cache/ListCachesCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Cache;
15 |
16 | use Ymir\Cli\Command\AbstractCommand;
17 |
18 | class ListCachesCommand extends AbstractCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'cache:list';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('List all the cache clusters that the current team has access to');
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | protected function perform()
41 | {
42 | $this->output->table(
43 | ['Id', 'Name', 'Provider', 'Network', 'Region', 'Status', 'Engine', 'Type'],
44 | $this->apiClient->getCaches($this->cliConfiguration->getActiveTeamId())->map(function (array $cache) {
45 | return [
46 | $cache['id'],
47 | $cache['name'],
48 | $cache['network']['provider']['name'],
49 | $cache['network']['name'],
50 | $cache['region'],
51 | $this->output->formatStatus($cache['status']),
52 | $cache['engine'],
53 | $cache['type'],
54 | ];
55 | })->all()
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Command/Cache/ModifyCacheCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Cache;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Symfony\Component\Console\Input\InputOption;
18 | use Ymir\Cli\Exception\InvalidInputException;
19 |
20 | class ModifyCacheCommand extends AbstractCacheCommand
21 | {
22 | /**
23 | * The name of the command.
24 | *
25 | * @var string
26 | */
27 | public const NAME = 'cache:modify';
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function configure()
33 | {
34 | $this
35 | ->setName(self::NAME)
36 | ->setDescription('Modify a cache cluster')
37 | ->addArgument('cache', InputArgument::OPTIONAL, 'The ID or name of the cache cluster to modify')
38 | ->addOption('type', null, InputOption::VALUE_REQUIRED, 'The cache cluster type');
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | protected function perform()
45 | {
46 | $cache = $this->determineCache('Which cache cluster would you like to modify');
47 | $type = $this->input->getStringOption('type', true);
48 | $types = $this->getCacheTypeDescriptions($cache['provider']['id'], $cache['engine']);
49 |
50 | if (null === $type) {
51 | $type = $this->output->choice(sprintf('What should the cache cluster type be changed to? (Currently: %s)>', $cache['type']), $types);
52 | } elseif (!$types->has($type)) {
53 | throw new InvalidInputException(sprintf('The type "%s" isn\'t a valid cache cluster type', $type));
54 | }
55 |
56 | if (!$this->output->confirm('Modifying the cache cluster will cause your cache cluster to become unavailable for a few minutes. Do you want to proceed?', false)) {
57 | exit;
58 | }
59 |
60 | $this->apiClient->updateCache((int) $cache['id'], $type);
61 |
62 | $this->output->infoWithDelayWarning('Cache cluster modified');
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Command/Certificate/AbstractCertificateCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Certificate;
15 |
16 | use Ymir\Cli\Command\AbstractCommand;
17 | use Ymir\Cli\Exception\InvalidInputException;
18 |
19 | abstract class AbstractCertificateCommand extends AbstractCommand
20 | {
21 | /**
22 | * Get the "certificate" argument.
23 | */
24 | protected function getCertificateArgument(): int
25 | {
26 | $certificateId = $this->input->getStringArgument('certificate');
27 |
28 | if (!is_numeric($certificateId)) {
29 | throw new InvalidInputException('The "certificate" argument must be the ID of the SSL certificate');
30 | }
31 |
32 | return (int) $certificateId;
33 | }
34 |
35 | /**
36 | * Parse the certificate details for the certificate validation DNS records.
37 | */
38 | protected function parseCertificateValidationRecords($certificate): array
39 | {
40 | return !empty($certificate['domains'])
41 | ? collect($certificate['domains'])
42 | ->where('managed', false)
43 | ->pluck('validation_record')
44 | ->filter()
45 | ->unique(function (array $validationRecord) {
46 | return $validationRecord['name'].$validationRecord['value'];
47 | })
48 | ->map(function (array $validationRecord) {
49 | return ['CNAME', $validationRecord['name'], $validationRecord['value']];
50 | })
51 | ->all()
52 | : [];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Command/Certificate/DeleteCertificateCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Certificate;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 |
18 | class DeleteCertificateCommand extends AbstractCertificateCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'certificate:delete';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('Delete a SSL certificate')
35 | ->addArgument('certificate', InputArgument::REQUIRED, 'The ID of the SSL certificate to delete');
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function perform()
42 | {
43 | $certificateId = $this->getCertificateArgument();
44 |
45 | if (!$this->output->confirm('Are you sure you want to delete this SSL certificate?', false)) {
46 | return;
47 | }
48 |
49 | $this->apiClient->deleteCertificate($certificateId);
50 |
51 | $this->output->info('SSL certificate deleted');
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Command/Certificate/GetCertificateInfoCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Certificate;
15 |
16 | use Symfony\Component\Console\Helper\TableSeparator;
17 | use Symfony\Component\Console\Input\InputArgument;
18 |
19 | class GetCertificateInfoCommand extends AbstractCertificateCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'certificate:info';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Get information on an SSL certificate')
36 | ->addArgument('certificate', InputArgument::REQUIRED, 'The ID of the SSL certificate to fetch the information of');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $certificate = $this->apiClient->getCertificate($this->getCertificateArgument());
45 |
46 | $this->output->horizontalTable(
47 | ['Domains', new TableSeparator(), 'Provider', 'Region', new TableSeparator(), 'Status', 'In Use'],
48 | [[implode(PHP_EOL, $this->getDomainNames($certificate['domains'])), new TableSeparator(), $certificate['provider']['name'], $certificate['region'], new TableSeparator(), $certificate['status'], $this->output->formatBoolean($certificate['in_use'])]]
49 | );
50 |
51 | $validationRecords = $this->parseCertificateValidationRecords($certificate);
52 |
53 | if (!empty($validationRecords)) {
54 | $this->output->newLine();
55 | $this->output->important('The following DNS record(s) need to exist on your DNS server at all times:');
56 | $this->output->newLine();
57 | $this->output->table(
58 | ['Type', 'Name', 'Value'],
59 | $validationRecords
60 | );
61 | $this->output->warning('The SSL certificate won\'t be issued or renewed if these DNS record(s) don\'t exist.');
62 | }
63 | }
64 |
65 | /**
66 | * Get the formatted domain names for a certificate.
67 | */
68 | private function getDomainNames(array $certificateDomains): array
69 | {
70 | return collect($certificateDomains)->map(function (array $domain) {
71 | return sprintf('%s (%s)', $domain['domain_name'], $domain['validated'] ? 'validated>' : 'not validated>');
72 | })->all();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Command/Certificate/ListCertificatesCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Certificate;
15 |
16 | class ListCertificatesCommand extends AbstractCertificateCommand
17 | {
18 | /**
19 | * The name of the command.
20 | *
21 | * @var string
22 | */
23 | public const NAME = 'certificate:list';
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | protected function configure()
29 | {
30 | $this
31 | ->setName(self::NAME)
32 | ->setDescription('List the SSL certificates that belong to the currently active team');
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | protected function perform()
39 | {
40 | $certificates = $this->apiClient->getCertificates($this->cliConfiguration->getActiveTeamId());
41 |
42 | $this->output->table(
43 | ['Id', 'Provider', 'Region', 'Domains', 'Status', 'In Use'],
44 | $certificates->map(function (array $certificate) {
45 | return [$certificate['id'], $certificate['provider']['name'], $certificate['region'], $this->getDomainsList($certificate), $certificate['status'], $this->output->formatBoolean($certificate['in_use'])];
46 | })->all()
47 | );
48 | }
49 |
50 | /**
51 | * Get the list of domains from the certificate.
52 | */
53 | private function getDomainsList($certificate): string
54 | {
55 | return !empty($certificate['domains']) ? implode(PHP_EOL, collect($certificate['domains'])->pluck('domain_name')->all()) : '';
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Command/Database/AbstractDatabaseServerCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Database;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Ymir\Cli\Command\AbstractCommand;
18 | use Ymir\Cli\Exception\InvalidInputException;
19 |
20 | abstract class AbstractDatabaseServerCommand extends AbstractCommand
21 | {
22 | /**
23 | * The name of the Aurora database type.
24 | *
25 | * @var string
26 | */
27 | protected const AURORA_DATABASE_TYPE = 'aurora-mysql';
28 |
29 | /**
30 | * Determine the database server that the command is interacting with.
31 | */
32 | protected function determineDatabaseServer(string $question): array
33 | {
34 | $databases = $this->apiClient->getDatabaseServers($this->cliConfiguration->getActiveTeamId());
35 | $databaseIdOrName = $this->input->getStringArgument('server');
36 |
37 | if ($databases->isEmpty()) {
38 | throw new RuntimeException(sprintf('The currently active team has no database servers. You can create one with the "%s" command.', CreateDatabaseServerCommand::NAME));
39 | } elseif (empty($databaseIdOrName)) {
40 | $databaseIdOrName = $this->output->choiceWithResourceDetails($question, $databases);
41 | }
42 |
43 | $database = $databases->firstWhere('id', $databaseIdOrName) ?? $databases->firstWhere('name', $databaseIdOrName);
44 |
45 | if (1 < $databases->where('name', $databaseIdOrName)->count()) {
46 | throw new RuntimeException(sprintf('Unable to select a database server because more than one database server has the name "%s"', $databaseIdOrName));
47 | } elseif (empty($database['id'])) {
48 | throw new InvalidInputException(sprintf('Unable to find a database server with "%s" as the ID or name', $databaseIdOrName));
49 | }
50 |
51 | return $database;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Command/Database/CreateDatabaseCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Database;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Symfony\Component\Console\Input\InputArgument;
18 | use Symfony\Component\Console\Input\InputOption;
19 |
20 | class CreateDatabaseCommand extends AbstractDatabaseCommand
21 | {
22 | /**
23 | * The name of the command.
24 | *
25 | * @var string
26 | */
27 | public const NAME = 'database:create';
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function configure()
33 | {
34 | $this
35 | ->setName(self::NAME)
36 | ->setDescription('Create a new database on a public database server')
37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the new database')
38 | ->addOption('server', null, InputOption::VALUE_REQUIRED, 'The ID or name of the database server where the database will be created');
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | protected function perform()
45 | {
46 | $databaseServer = $this->determineDatabaseServer('On which database server would you like to create the new database?');
47 |
48 | if (!$databaseServer['publicly_accessible']) {
49 | throw new RuntimeException('Database on private database servers need to be manually created.');
50 | }
51 |
52 | $name = $this->input->getStringArgument('name');
53 |
54 | if (empty($name)) {
55 | $name = $this->output->ask('What is the name of the database');
56 | }
57 |
58 | $this->apiClient->createDatabase($databaseServer['id'], $name);
59 |
60 | $this->output->info('Database created');
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Command/Database/DeleteDatabaseCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Database;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Symfony\Component\Console\Input\InputArgument;
18 | use Symfony\Component\Console\Input\InputOption;
19 |
20 | class DeleteDatabaseCommand extends AbstractDatabaseCommand
21 | {
22 | /**
23 | * The name of the command.
24 | *
25 | * @var string
26 | */
27 | public const NAME = 'database:delete';
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function configure()
33 | {
34 | $this
35 | ->setName(self::NAME)
36 | ->setDescription('Delete a database on a public database server')
37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the database to delete')
38 | ->addOption('server', null, InputOption::VALUE_REQUIRED, 'The ID or name of the database server where the database will be deleted');
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | protected function perform()
45 | {
46 | $databaseServer = $this->determineDatabaseServer('On which database server would you like to delete a database?');
47 |
48 | if (!$databaseServer['publicly_accessible']) {
49 | throw new RuntimeException('Database on private database servers need to be manually deleted.');
50 | }
51 |
52 | $name = $this->input->getStringArgument('name');
53 |
54 | if (empty($name)) {
55 | $name = (string) $this->output->choice('Which database would you like to delete', $this->apiClient->getDatabases($databaseServer['id'])->filter(function (string $name) {
56 | return !in_array($name, ['information_schema', 'innodb', 'mysql', 'performance_schema', 'sys']);
57 | })->values());
58 | }
59 |
60 | if (!$this->output->confirm(sprintf('Are you sure you want to delete the "%s" database?', $name), false)) {
61 | return;
62 | }
63 |
64 | $this->apiClient->deleteDatabase($databaseServer['id'], $name);
65 |
66 | $this->output->info('Database deleted');
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Command/Database/DeleteDatabaseServerCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Database;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\Network\RemoveNatGatewayCommand;
18 |
19 | class DeleteDatabaseServerCommand extends AbstractDatabaseServerCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'database:server:delete';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Delete a database server')
36 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to delete');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $databaseServer = $this->determineDatabaseServer('Which database server would you like to delete');
45 |
46 | if (!$this->output->confirm(sprintf('Are you sure you want to delete the "%s" database server?', $databaseServer['name']), false)) {
47 | return;
48 | }
49 |
50 | $this->apiClient->deleteDatabaseServer($databaseServer['id']);
51 |
52 | $this->output->infoWithDelayWarning('Database server deleted');
53 |
54 | if (!$databaseServer['publicly_accessible']) {
55 | $this->output->newLine();
56 | $this->output->note(sprintf('If you have no other resources using the private subnet, you should remove the network\'s NAT gateway using the "%s" command', RemoveNatGatewayCommand::NAME));
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Command/Database/DeleteDatabaseUserCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Database;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Symfony\Component\Console\Input\InputArgument;
18 | use Symfony\Component\Console\Input\InputOption;
19 | use Ymir\Cli\Exception\InvalidInputException;
20 |
21 | class DeleteDatabaseUserCommand extends AbstractDatabaseCommand
22 | {
23 | /**
24 | * The name of the command.
25 | *
26 | * @var string
27 | */
28 | public const NAME = 'database:user:delete';
29 |
30 | /**
31 | * {@inheritdoc}
32 | */
33 | protected function configure()
34 | {
35 | $this
36 | ->setName(self::NAME)
37 | ->setDescription('Delete a user on a database')
38 | ->addArgument('username', InputArgument::OPTIONAL, 'The username of the database user to delete')
39 | ->addOption('server', null, InputOption::VALUE_REQUIRED, 'The ID or name of the database server where the database user will be deleted');
40 | }
41 |
42 | /**
43 | * {@inheritdoc}
44 | */
45 | protected function perform()
46 | {
47 | $databaseServer = $this->determineDatabaseServer('On which database server would you like to create the new database user?');
48 | $username = $this->input->getStringArgument('username');
49 | $users = $this->apiClient->getDatabaseUsers($databaseServer['id']);
50 |
51 | if ($users->isEmpty()) {
52 | throw new RuntimeException('The database server doesn\'t have any managed database users');
53 | } elseif (empty($username)) {
54 | $username = (string) $this->output->choice('Which database user would you like to delete', $users->pluck('username'));
55 | }
56 |
57 | $user = $users->firstWhere('username', $username);
58 |
59 | if (empty($user['id'])) {
60 | throw new InvalidInputException(sprintf('No database user found with the "%s" username', $username));
61 | }
62 |
63 | if (!$this->output->confirm(sprintf('Are you sure you want to delete the "%s" database user?', $user['username']), false)) {
64 | return;
65 | }
66 |
67 | $this->apiClient->deleteDatabaseUser($databaseServer['id'], $user['id']);
68 |
69 | $this->output->info('Database user deleted');
70 |
71 | if (!$databaseServer['publicly_accessible']) {
72 | $this->output->newLine();
73 | $this->output->important('The database user needs to be manually deleted on the database server because it isn\'t publicly accessible. You can use the following query to delete it:');
74 | $this->output->writeln(sprintf('DROP USER IF EXISTS %s@\'%%\'', $user['username']));
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Command/Database/GetDatabaseServerInfoCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Database;
15 |
16 | use Symfony\Component\Console\Helper\TableSeparator;
17 | use Symfony\Component\Console\Input\InputArgument;
18 |
19 | class GetDatabaseServerInfoCommand extends AbstractDatabaseServerCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'database:server:info';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Get information on a database server')
36 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to fetch the information of');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $databaseServer = $this->determineDatabaseServer('Which database server would you like to get information about');
45 |
46 | $this->output->horizontalTable(
47 | ['Id', 'Name', 'Status', 'Locked', 'Public', new TableSeparator(), 'Provider', 'Network', 'Region', 'Type', 'Storage', 'Endpoint'],
48 | [[
49 | $databaseServer['id'],
50 | $databaseServer['name'],
51 | $this->output->formatStatus($databaseServer['status']),
52 | $this->output->formatBoolean($databaseServer['locked']),
53 | $this->output->formatBoolean($databaseServer['publicly_accessible']),
54 | new TableSeparator(),
55 | $databaseServer['network']['provider']['name'],
56 | $databaseServer['network']['name'],
57 | $databaseServer['region'],
58 | $databaseServer['type'],
59 | $databaseServer['storage'] ? $databaseServer['storage'].'GB' : 'N/A',
60 | $databaseServer['endpoint'] ?? 'pending',
61 | ]]
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Command/Database/ListDatabaseServersCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Database;
15 |
16 | use Ymir\Cli\Command\AbstractCommand;
17 |
18 | class ListDatabaseServersCommand extends AbstractCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'database:server:list';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('List all the database servers that the current team has access to');
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | protected function perform()
41 | {
42 | $this->output->table(
43 | ['Id', 'Name', 'Provider', 'Network', 'Region', 'Status', 'Locked', 'Public', 'Type', 'Storage'],
44 | $this->apiClient->getDatabaseServers($this->cliConfiguration->getActiveTeamId())->map(function (array $database) {
45 | return [
46 | $database['id'],
47 | $database['name'],
48 | $database['network']['provider']['name'],
49 | $database['network']['name'],
50 | $database['region'],
51 | $this->output->formatStatus($database['status']),
52 | $this->output->formatBoolean($database['locked']),
53 | $this->output->formatBoolean($database['publicly_accessible']),
54 | $database['type'],
55 | $database['storage'] ? $database['storage'].'GB' : 'N/A',
56 | ];
57 | })->all()
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Command/Database/ListDatabaseUsersCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Database;
15 |
16 | use Carbon\Carbon;
17 | use Symfony\Component\Console\Input\InputArgument;
18 |
19 | class ListDatabaseUsersCommand extends AbstractDatabaseServerCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'database:user:list';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('List all the managed users on a public database server')
36 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to list users from');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $this->output->table(
45 | ['Id', 'Username', 'Created At'],
46 | $this->apiClient->getDatabaseUsers($this->determineDatabaseServer('Which database server would you like to list users from')['id'])->map(function (array $database) {
47 | return [$database['id'], $database['username'], Carbon::parse($database['created_at'])->diffForHumans()];
48 | })->all()
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Command/Database/ListDatabasesCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Database;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Symfony\Component\Console\Input\InputArgument;
18 |
19 | class ListDatabasesCommand extends AbstractDatabaseServerCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'database:list';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('List all the databases on a public database server')
36 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to list databases from');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $databaseServer = $this->determineDatabaseServer('Which database server would you like to list databases from');
45 |
46 | if (!$databaseServer['publicly_accessible']) {
47 | throw new RuntimeException('Database on private database servers cannot be listed.');
48 | }
49 |
50 | $this->output->table(
51 | ['Name'],
52 | $this->apiClient->getDatabases($databaseServer['id'])->map(function (string $name) {
53 | return [$name];
54 | })->all()
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Command/Database/LockDatabaseServerCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Database;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 |
18 | class LockDatabaseServerCommand extends AbstractDatabaseServerCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'database:server:lock';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('Lock the database server which prevents it from being deleted')
35 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to lock');
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function perform()
42 | {
43 | $databaseServer = $this->determineDatabaseServer('Which database server would you like to lock?');
44 |
45 | $this->apiClient->changeDatabaseServerLock($databaseServer['id'], true);
46 |
47 | $this->output->info('Database server locked');
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Command/Database/RotateDatabaseServerPasswordCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Database;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\Project\DeployProjectCommand;
18 | use Ymir\Cli\Command\Project\RedeployProjectCommand;
19 |
20 | class RotateDatabaseServerPasswordCommand extends AbstractDatabaseServerCommand
21 | {
22 | /**
23 | * The name of the command.
24 | *
25 | * @var string
26 | */
27 | public const NAME = 'database:server:rotate-password';
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function configure()
33 | {
34 | $this
35 | ->setName(self::NAME)
36 | ->setDescription('Rotate the password of the database server\'s "ymir" user')
37 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to rotate the password of');
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | protected function perform()
44 | {
45 | $databaseServer = $this->determineDatabaseServer('Which database server would you like to rotate the password of?');
46 |
47 | $this->output->warning(sprintf('All projects that use the "%s" database server with the default user will be unable to connect to the database server until they\'re redeployed.', $databaseServer['name']));
48 |
49 | if (!$this->output->confirm('Do you want to proceed?', false)) {
50 | return;
51 | }
52 |
53 | $newCredentials = $this->apiClient->rotateDatabaseServerPassword($databaseServer['id']);
54 |
55 | $this->output->horizontalTable(
56 | ['Username', 'Password'],
57 | [[$newCredentials['username'], $newCredentials['password']]]
58 | );
59 |
60 | $this->output->infoWithDelayWarning('Database server password rotated successfully');
61 | $this->output->newLine();
62 | $this->output->important(sprintf('You need to redeploy all projects using this database server with the default user using either the "%s" or "%s" commands for the change to take effect.', DeployProjectCommand::ALIAS, RedeployProjectCommand::ALIAS));
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Command/Database/UnlockDatabaseServerCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Database;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 |
18 | class UnlockDatabaseServerCommand extends AbstractDatabaseServerCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'database:server:unlock';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('Unlock the database server which allows it to be deleted')
35 | ->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to unlock');
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function perform()
42 | {
43 | $databaseServer = $this->determineDatabaseServer('Which database server would you like to unlock?');
44 |
45 | $this->apiClient->changeDatabaseServerLock($databaseServer['id'], false);
46 |
47 | $this->output->info('Database server unlocked');
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Command/Dns/AbstractDnsCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Dns;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Ymir\Cli\Command\AbstractCommand;
18 |
19 | abstract class AbstractDnsCommand extends AbstractCommand
20 | {
21 | /**
22 | * Determine the DNS zone that the command is interacting with.
23 | */
24 | protected function determineDnsZone(string $question): array
25 | {
26 | $zone = null;
27 | $zones = $this->apiClient->getDnsZones($this->cliConfiguration->getActiveTeamId());
28 | $zoneIdOrName = $this->input->getStringArgument('zone');
29 |
30 | if ($zones->isEmpty()) {
31 | throw new RuntimeException(sprintf('The currently active team has no DNS zones. You can create one with the "%s" command.', CreateDnsZoneCommand::NAME));
32 | }
33 |
34 | if (empty($zoneIdOrName)) {
35 | $zoneIdOrName = (string) $this->output->choice($question, $zones->pluck('domain_name'));
36 | }
37 |
38 | if (is_numeric($zoneIdOrName)) {
39 | $zone = $zones->firstWhere('id', $zoneIdOrName);
40 | } elseif (is_string($zoneIdOrName)) {
41 | $zone = $zones->firstWhere('domain_name', $zoneIdOrName);
42 | }
43 |
44 | if (empty($zone['id'])) {
45 | throw new RuntimeException(sprintf('Unable to find a DNS zones with "%s" as the ID or name', $zoneIdOrName));
46 | }
47 |
48 | return $zone;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Command/Dns/ChangeDnsRecordCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Dns;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 |
18 | class ChangeDnsRecordCommand extends AbstractDnsCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'dns:record:change';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('Change the value of a DNS record (Will overwrite existing DNS record if it already exists)')
35 | ->addArgument('zone', InputArgument::REQUIRED, 'The name of the DNS zone that the DNS record belongs to')
36 | ->addArgument('type', InputArgument::REQUIRED, 'The DNS record type')
37 | ->addArgument('name', InputArgument::REQUIRED, 'The name of the DNS record without the domain')
38 | ->addArgument('value', InputArgument::REQUIRED, 'The value of the DNS record');
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | protected function perform()
45 | {
46 | $this->apiClient->changeDnsRecord($this->input->getStringArgument('zone'), $this->input->getStringArgument('type'), $this->input->getStringArgument('name'), $this->input->getStringArgument('value'));
47 |
48 | $this->output->info('DNS record change applied');
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Command/Dns/CreateDnsZoneCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Dns;
15 |
16 | use Symfony\Component\Console\Helper\TableSeparator;
17 | use Symfony\Component\Console\Input\InputArgument;
18 | use Symfony\Component\Console\Input\InputOption;
19 |
20 | class CreateDnsZoneCommand extends AbstractDnsCommand
21 | {
22 | /**
23 | * The name of the command.
24 | *
25 | * @var string
26 | */
27 | public const NAME = 'dns:zone:create';
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function configure()
33 | {
34 | $this
35 | ->setName(self::NAME)
36 | ->setDescription('Create a new DNS zone')
37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the domain managed by the created DNS zone')
38 | ->addOption('provider', null, InputOption::VALUE_REQUIRED, 'The cloud provider region where the DNS zone will created');
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | protected function perform()
45 | {
46 | $name = $this->input->getStringArgument('name');
47 |
48 | if (empty($name)) {
49 | $name = $this->output->ask('What is the name of the domain that the DNS zone will manage');
50 | }
51 |
52 | $providerId = $this->determineCloudProvider('Enter the ID of the cloud provider where the DNS zone will be created');
53 |
54 | if (!$this->output->confirm('A DNS zone will cost $0.50/month if it isn\'t deleted in the next 12 hours. Would you like to proceed?', true)) {
55 | return;
56 | }
57 |
58 | $zone = $this->apiClient->createDnsZone($providerId, $name);
59 |
60 | $nameServers = $this->wait(function () use ($zone) {
61 | return $this->apiClient->getDnsZone($zone['id'])->get('name_servers', []);
62 | });
63 |
64 | if (!empty($nameServers)) {
65 | $this->output->horizontalTable(
66 | ['Domain Name', new TableSeparator(), 'Name Servers'],
67 | [[$zone['domain_name'], new TableSeparator(), implode(PHP_EOL, $nameServers)]]
68 | );
69 | }
70 |
71 | $this->output->info('DNS zone created');
72 |
73 | if ($this->output->confirm('Do you want to import the root DNS records for this domain', false)) {
74 | $this->apiClient->importDnsRecords($zone['id']);
75 | }
76 |
77 | if ($this->output->confirm('Do you want to import DNS records for subdomains of this domain', false)) {
78 | $this->invoke(ImportDnsRecordsCommand::NAME, [
79 | 'zone' => $zone['domain_name'],
80 | ]);
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Command/Dns/DeleteDnsZoneCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Dns;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 |
18 | class DeleteDnsZoneCommand extends AbstractDnsCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'dns:zone:delete';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('Delete a DNS zone')
35 | ->addArgument('zone', InputArgument::OPTIONAL, 'The ID or name of the DNS zone to delete');
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function perform()
42 | {
43 | $zone = $this->determineDnsZone('Which DNS zone would you like to delete');
44 |
45 | if (!$this->output->confirm('Are you sure you want to delete this DNS zone?', false)) {
46 | return;
47 | }
48 |
49 | $this->apiClient->deleteDnsZone((int) $zone['id']);
50 |
51 | $this->output->info('DNS zone deleted');
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Command/Dns/ImportDnsRecordsCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Dns;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 |
18 | class ImportDnsRecordsCommand extends AbstractDnsCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'dns:zone:import-records';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('Import DNS records into a DNS zone')
35 | ->addArgument('zone', InputArgument::REQUIRED, 'The name of the DNS zone that the DNS record belongs to')
36 | ->addArgument('subdomain', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'The subdomain(s) that we want to import');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $subdomains = $this->input->getArrayArgument('subdomain', false);
45 |
46 | if (empty($subdomains)) {
47 | $subdomains = explode(',', (string) $this->output->ask('Please enter a comma-separated list of subdomains to import DNS records from (leave blank to import the root DNS records)'));
48 | }
49 |
50 | $this->apiClient->importDnsRecords($this->input->getStringArgument('zone'), $subdomains);
51 |
52 | $this->output->info('DNS records imported');
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Command/Dns/ListDnsRecordsCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Dns;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 |
18 | class ListDnsRecordsCommand extends AbstractDnsCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'dns:record:list';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('List the DNS records belonging to a DNS zone')
35 | ->addArgument('zone', InputArgument::OPTIONAL, 'The ID or name of the DNS zone to list DNS records from');
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function perform()
42 | {
43 | $zone = $this->determineDnsZone('Which DNS zone would you like to list DNS records from');
44 |
45 | $this->output->table(
46 | ['Id', 'Domain Name', 'Type', 'Value', 'Internal'],
47 | $this->apiClient->getDnsRecords($zone['id'])->map(function (array $record) {
48 | return [$record['id'], $record['name'], $record['type'], str_replace(',', "\n", $record['value']), $this->output->formatBoolean($record['internal'])];
49 | })->all()
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Command/Dns/ListDnsZonesCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Dns;
15 |
16 | use Ymir\Cli\Command\AbstractCommand;
17 |
18 | class ListDnsZonesCommand extends AbstractCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'dns:zone:list';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('List the DNS zones that belong to the currently active team');
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | protected function perform()
41 | {
42 | $this->output->table(
43 | ['Id', 'Provider', 'Domain Name', 'Name Servers'],
44 | $this->apiClient->getDnsZones($this->cliConfiguration->getActiveTeamId())->map(function (array $zone) {
45 | return [$zone['id'], $zone['provider']['name'], $zone['domain_name'], implode(PHP_EOL, $zone['name_servers'])];
46 | })->all()
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Command/Email/AbstractEmailIdentityCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Email;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Ymir\Cli\Command\AbstractCommand;
18 |
19 | abstract class AbstractEmailIdentityCommand extends AbstractCommand
20 | {
21 | /**
22 | * Determine the email identity that the command is interacting with.
23 | */
24 | protected function determineEmailIdentity(string $question): array
25 | {
26 | $identity = null;
27 | $identities = $this->apiClient->getEmailIdentities($this->cliConfiguration->getActiveTeamId());
28 | $identityIdOrName = $this->input->getStringArgument('identity');
29 |
30 | if ($identities->isEmpty()) {
31 | throw new RuntimeException(sprintf('The currently active team has no email identities. You can create one with the "%s" command.', CreateEmailIdentityCommand::NAME));
32 | }
33 |
34 | if (empty($identityIdOrName)) {
35 | $identityIdOrName = (string) $this->output->choice($question, $identities->pluck('name'));
36 | }
37 |
38 | if (is_numeric($identityIdOrName)) {
39 | $identity = $identities->firstWhere('id', $identityIdOrName);
40 | } elseif (is_string($identityIdOrName)) {
41 | $identity = $identities->firstWhere('name', $identityIdOrName);
42 | }
43 |
44 | if (empty($identity['id'])) {
45 | throw new RuntimeException(sprintf('Unable to find an email identity with "%s" as the ID or name', $identityIdOrName));
46 | }
47 |
48 | return $identity;
49 | }
50 |
51 | /**
52 | * Display warning about DNS records required to authenticate the DKIM signature and verify it.
53 | */
54 | protected function displayDkimAuthenticationRecords(array $identity)
55 | {
56 | if (empty($identity['dkim_authentication_records']) || $identity['managed']) {
57 | return;
58 | }
59 |
60 | $this->output->newLine();
61 | $this->output->important('The following DNS records needs to exist on your DNS server at all times to verify the email identity and authenticate its DKIM signature:');
62 | $this->output->newLine();
63 | $this->output->table(
64 | ['Name', 'Type', 'Value'],
65 | collect($identity['dkim_authentication_records'])->map(function (array $dkimRecord) {
66 | $dkimRecord['type'] = strtoupper($dkimRecord['type']);
67 |
68 | return $dkimRecord;
69 | })->all()
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Command/Email/CreateEmailIdentityCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Email;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Symfony\Component\Console\Input\InputOption;
18 |
19 | class CreateEmailIdentityCommand extends AbstractEmailIdentityCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'email:identity:create';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Create a new email identity')
36 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the email identity')
37 | ->addOption('provider', null, InputOption::VALUE_REQUIRED, 'The cloud provider where the email identity will be created')
38 | ->addOption('region', null, InputOption::VALUE_REQUIRED, 'The cloud provider region where the email identity will be located');
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | protected function perform()
45 | {
46 | $name = $this->input->getStringArgument('name');
47 |
48 | if (empty($name)) {
49 | $name = $this->output->ask('What is the name of the email identity');
50 | }
51 |
52 | $providerId = $this->determineCloudProvider('Enter the ID of the cloud provider where the email identity will be created');
53 |
54 | $identity = $this->apiClient->createEmailIdentity($providerId, $name, $this->determineRegion('Enter the name of the region where the email identity will be created', $providerId));
55 |
56 | $this->output->info('Email identity created');
57 |
58 | if ('domain' === $identity['type']) {
59 | $this->displayDkimAuthenticationRecords($identity->toArray());
60 | } elseif ('email' === $identity['type']) {
61 | $this->output->newLine();
62 | $this->output->important(sprintf('A verification email was sent to %s to validate the email identity', $identity['name']));
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Command/Email/DeleteEmailIdentityCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Email;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 |
18 | class DeleteEmailIdentityCommand extends AbstractEmailIdentityCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'email:identity:delete';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('Delete an email identity')
35 | ->addArgument('identity', InputArgument::OPTIONAL, 'The ID or name of the email identity to delete');
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function perform()
42 | {
43 | $identity = $this->determineEmailIdentity('Which email identity would you like to delete');
44 |
45 | if (!$this->output->confirm('Are you sure you want to delete this email identity?', false)) {
46 | return;
47 | }
48 |
49 | $this->apiClient->deleteEmailIdentity((int) $identity['id']);
50 |
51 | $this->output->info('Email identity deleted');
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Command/Email/GetEmailIdentityInfoCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Email;
15 |
16 | use Symfony\Component\Console\Helper\TableSeparator;
17 | use Symfony\Component\Console\Input\InputArgument;
18 |
19 | class GetEmailIdentityInfoCommand extends AbstractEmailIdentityCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'email:identity:info';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Get the information on an email identity')
36 | ->addArgument('identity', InputArgument::OPTIONAL, 'The ID or name of the email identity to fetch the information of');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $identity = $this->determineEmailIdentity('Which email identity would you like to get information about');
45 |
46 | $this->output->horizontalTable(
47 | ['Name', new TableSeparator(), 'Provider', 'Region', new TableSeparator(), 'Type', 'Verified', 'Managed'],
48 | [[$identity['name'], new TableSeparator(), $identity['provider']['name'], $identity['region'], new TableSeparator(), $identity['type'], $this->output->formatBoolean($identity['verified']), $this->output->formatBoolean($identity['managed'])]]
49 | );
50 |
51 | $this->displayDkimAuthenticationRecords($identity);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Command/Email/ListEmailIdentitiesCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Email;
15 |
16 | class ListEmailIdentitiesCommand extends AbstractEmailIdentityCommand
17 | {
18 | /**
19 | * The name of the command.
20 | *
21 | * @var string
22 | */
23 | public const NAME = 'email:identity:list';
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | protected function configure()
29 | {
30 | $this
31 | ->setName(self::NAME)
32 | ->setDescription('List the email identities that belong to the currently active team');
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | protected function perform()
39 | {
40 | $this->output->table(
41 | ['Id', 'Name', 'Type', 'Provider', 'Region', 'Verified', 'Managed'],
42 | $this->apiClient->getEmailIdentities($this->cliConfiguration->getActiveTeamId())->map(function (array $identity) {
43 | return [$identity['id'], $identity['name'], $identity['type'], $identity['provider']['name'], $identity['region'], $this->output->formatBoolean($identity['verified']), $this->output->formatBoolean($identity['managed'])];
44 | })->all()
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Command/Environment/AbstractEnvironmentLogsCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Environment;
15 |
16 | use Carbon\Carbon;
17 | use Carbon\Exceptions\InvalidTimeZoneException;
18 | use Illuminate\Support\Collection;
19 | use Ymir\Cli\Command\AbstractProjectCommand;
20 | use Ymir\Cli\Console\Output;
21 | use Ymir\Cli\Exception\InvalidInputException;
22 |
23 | abstract class AbstractEnvironmentLogsCommand extends AbstractProjectCommand
24 | {
25 | /**
26 | * Write the logs to the console output.
27 | */
28 | protected function writeLogs(Collection $logs, ?string $timezone = null)
29 | {
30 | $logs->each(function (array $log) use ($timezone) {
31 | $timestamp = Carbon::createFromTimestamp($log['timestamp'] / 1000);
32 |
33 | if ($timezone) {
34 | try {
35 | $timestamp->setTimezone($timezone);
36 | } catch (InvalidTimeZoneException $exception) {
37 | throw new InvalidInputException(sprintf('"%s" is not a valid timezone', $timezone));
38 | }
39 | }
40 |
41 | $this->output->writeln(sprintf('[%s] %s', $timestamp->toDateTimeString(), trim($log['message'])));
42 | });
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Command/Environment/ChangeEnvironmentSecretCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Environment;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\AbstractProjectCommand;
18 |
19 | class ChangeEnvironmentSecretCommand extends AbstractProjectCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'environment:secret:change';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Change an environment\'s secret')
36 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment where the secret is', 'staging')
37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the secret')
38 | ->addArgument('value', InputArgument::OPTIONAL, 'The secret value');
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | protected function perform()
45 | {
46 | $environment = $this->input->getStringArgument('environment');
47 | $name = $this->input->getStringArgument('name');
48 | $value = $this->input->getStringArgument('value');
49 |
50 | if (empty($name)) {
51 | $name = $this->output->ask('What is the name of the secret');
52 | }
53 |
54 | if (empty($value)) {
55 | $value = $this->output->ask('What is the secret value');
56 | }
57 |
58 | $this->apiClient->changeSecret($this->projectConfiguration->getProjectId(), $environment, $name, $value);
59 |
60 | $this->output->infoWithRedeployWarning('Secret changed', $environment);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Command/Environment/ChangeEnvironmentVariableCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Environment;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\AbstractProjectCommand;
18 |
19 | class ChangeEnvironmentVariableCommand extends AbstractProjectCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'environment:variables:change';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Change an environment variable')
36 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment where the environment variable is', 'staging')
37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the environment variable')
38 | ->addArgument('value', InputArgument::OPTIONAL, 'The value of the environment variable');
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | protected function perform()
45 | {
46 | $environment = $this->input->getStringArgument('environment');
47 | $name = $this->input->getStringArgument('name');
48 | $value = $this->input->getStringArgument('value');
49 |
50 | if (empty($name)) {
51 | $name = $this->output->ask('What is the name of the environment variable');
52 | }
53 |
54 | if (empty($value)) {
55 | $value = $this->output->ask('What is the value of the environment variable');
56 | }
57 |
58 | $this->apiClient->changeEnvironmentVariables($this->projectConfiguration->getProjectId(), $environment, [
59 | $name => $value,
60 | ]);
61 |
62 | $this->output->infoWithRedeployWarning('Environment variable changed', $environment);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Command/Environment/CreateEnvironmentCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Environment;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Symfony\Component\Console\Input\InputOption;
18 | use Ymir\Cli\Command\AbstractProjectCommand;
19 |
20 | class CreateEnvironmentCommand extends AbstractProjectCommand
21 | {
22 | /**
23 | * The name of the command.
24 | *
25 | * @var string
26 | */
27 | public const NAME = 'environment:create';
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function configure()
33 | {
34 | $this
35 | ->setName(self::NAME)
36 | ->setDescription('Create a new environment')
37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the environment to create')
38 | ->addOption('use-image', null, InputOption::VALUE_NONE, 'Whether the environment will be deployed using a container image');
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | protected function perform()
45 | {
46 | $name = $this->input->getStringArgument('name') ?: $this->output->ask('What is the name of the environment');
47 | $projectId = $this->projectConfiguration->getProjectId();
48 | $projectType = $this->projectConfiguration->getProjectType();
49 |
50 | $this->apiClient->createEnvironment($projectId, $name);
51 |
52 | $this->projectConfiguration->addEnvironment($name, $projectType->getEnvironmentConfiguration($name, $this->input->getBooleanOption('use-image') ? ['deployment' => 'image'] : []));
53 |
54 | $this->output->info('Environment created');
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Command/Environment/DeleteEnvironmentSecretCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Environment;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Symfony\Component\Console\Input\InputArgument;
18 | use Ymir\Cli\Command\AbstractProjectCommand;
19 | use Ymir\Cli\Exception\InvalidInputException;
20 |
21 | class DeleteEnvironmentSecretCommand extends AbstractProjectCommand
22 | {
23 | /**
24 | * The name of the command.
25 | *
26 | * @var string
27 | */
28 | public const NAME = 'environment:secret:delete';
29 |
30 | /**
31 | * {@inheritdoc}
32 | */
33 | protected function configure()
34 | {
35 | $this
36 | ->setName(self::NAME)
37 | ->setDescription('Delete an environment\'s secret')
38 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment where the secret is', 'staging')
39 | ->addArgument('secret', InputArgument::OPTIONAL, 'The ID or name of the secret');
40 | }
41 |
42 | /**
43 | * {@inheritdoc}
44 | */
45 | protected function perform()
46 | {
47 | $environment = $this->input->getStringArgument('environment');
48 | $secrets = $this->apiClient->getSecrets($this->projectConfiguration->getProjectId(), $environment);
49 |
50 | if ($secrets->isEmpty()) {
51 | throw new RuntimeException(sprintf('The "%s" environment has no secrets', $environment));
52 | }
53 |
54 | $secretIdOrName = $this->input->getStringArgument('secret');
55 |
56 | if (empty($secretIdOrName)) {
57 | $secretIdOrName = (string) $this->output->choice('Which secret would you like to delete', $secrets->pluck('name'));
58 | }
59 |
60 | $secret = is_numeric($secretIdOrName) ? $secrets->firstWhere('id', $secretIdOrName) : $secrets->firstWhere('name', $secretIdOrName);
61 |
62 | if (!is_array($secret) || empty($secret['id'])) {
63 | throw new InvalidInputException(sprintf('Unable to find a secret with "%s" as the ID or name', $secretIdOrName));
64 | } elseif (!$this->output->confirm('Are you sure you want to delete this secret?', false)) {
65 | return;
66 | }
67 |
68 | $this->apiClient->deleteSecret($secret['id']);
69 |
70 | $this->output->info('Secret deleted');
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Command/Environment/DownloadEnvironmentVariablesCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Environment;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Symfony\Component\Filesystem\Filesystem;
18 | use Ymir\Cli\ApiClient;
19 | use Ymir\Cli\CliConfiguration;
20 | use Ymir\Cli\Command\AbstractProjectCommand;
21 | use Ymir\Cli\Project\Configuration\ProjectConfiguration;
22 |
23 | class DownloadEnvironmentVariablesCommand extends AbstractProjectCommand
24 | {
25 | /**
26 | * The name of the command.
27 | *
28 | * @var string
29 | */
30 | public const NAME = 'environment:variables:download';
31 |
32 | /**
33 | * The file system.
34 | *
35 | * @var Filesystem
36 | */
37 | private $filesystem;
38 |
39 | /**
40 | * The project directory where the project files are copied from.
41 | *
42 | * @var string
43 | */
44 | private $projectDirectory;
45 |
46 | /**
47 | * Constructor.
48 | */
49 | public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, Filesystem $filesystem, ProjectConfiguration $projectConfiguration, string $projectDirectory)
50 | {
51 | parent::__construct($apiClient, $cliConfiguration, $projectConfiguration);
52 |
53 | $this->filesystem = $filesystem;
54 | $this->projectDirectory = rtrim($projectDirectory, '/');
55 | }
56 |
57 | /**
58 | * {@inheritdoc}
59 | */
60 | protected function configure()
61 | {
62 | $this
63 | ->setName(self::NAME)
64 | ->setDescription('Download an environment\'s environment variables into an environment file')
65 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to download environment variables from', 'staging');
66 | }
67 |
68 | /**
69 | * {@inheritdoc}
70 | */
71 | protected function perform()
72 | {
73 | $environment = $this->input->getStringArgument('environment');
74 | $filename = sprintf('.env.%s', $environment);
75 |
76 | $this->filesystem->dumpFile($this->projectDirectory.'/'.$filename, $this->apiClient->getEnvironmentVariables($this->projectConfiguration->getProjectId(), $environment)->sortKeys()->map(function (string $value, string $key) {
77 | return sprintf('%s=%s', $key, $value);
78 | })->join("\n"));
79 |
80 | $this->output->infoWithValue('Environment variables downloaded to', $filename);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Command/Environment/GetEnvironmentUrlCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Environment;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Symfony\Component\Process\Process;
18 | use Ymir\Cli\Command\AbstractProjectCommand;
19 |
20 | class GetEnvironmentUrlCommand extends AbstractProjectCommand
21 | {
22 | /**
23 | * The name of the command.
24 | *
25 | * @var string
26 | */
27 | public const NAME = 'environment:url';
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function configure()
33 | {
34 | $this
35 | ->setName(self::NAME)
36 | ->setDescription('Get the environment URL and copy it to the clipboard')
37 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to get the URL of', 'staging');
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | protected function perform()
44 | {
45 | $this->displayEnvironmentUrlAndCopyToClipboard($this->apiClient->getEnvironmentVanityDomainName($this->projectConfiguration->getProjectId(), $this->input->getStringArgument('environment')));
46 | }
47 |
48 | /**
49 | * Generate the environment URL, copy it to the clipboard and then displays it in the console.
50 | */
51 | private function displayEnvironmentUrlAndCopyToClipboard(string $domainName)
52 | {
53 | $clipboardCommand = 'WIN' === strtoupper(substr(PHP_OS, 0, 3)) ? 'clip' : 'pbcopy';
54 | $url = 'https://'.$domainName;
55 |
56 | Process::fromShellCommandline(sprintf('echo %s | %s', $url, $clipboardCommand))->run();
57 |
58 | $this->output->infoWithValue('Environment URL is', $url, 'copied to clipboard');
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Command/Environment/InvalidateEnvironmentCacheCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Environment;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Symfony\Component\Console\Input\InputOption;
18 | use Ymir\Cli\Command\AbstractProjectCommand;
19 |
20 | class InvalidateEnvironmentCacheCommand extends AbstractProjectCommand
21 | {
22 | /**
23 | * The name of the command.
24 | *
25 | * @var string
26 | */
27 | public const NAME = 'environment:invalidate-cache';
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function configure()
33 | {
34 | $this
35 | ->setName(self::NAME)
36 | ->setDescription('Invalidate the environment\'s content delivery network cache')
37 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to invalidate the cache of', 'staging')
38 | ->addOption('path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to invalidate on the content delivery network', ['*']);
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | protected function perform()
45 | {
46 | $this->apiClient->invalidateCache($this->projectConfiguration->getProjectId(), $this->input->getStringArgument('environment'), $this->input->getArrayOption('path'));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Command/Environment/ListEnvironmentSecretsCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Environment;
15 |
16 | use Carbon\Carbon;
17 | use Symfony\Component\Console\Input\InputArgument;
18 | use Ymir\Cli\Command\AbstractProjectCommand;
19 |
20 | class ListEnvironmentSecretsCommand extends AbstractProjectCommand
21 | {
22 | /**
23 | * The name of the command.
24 | *
25 | * @var string
26 | */
27 | public const NAME = 'environment:secret:list';
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function configure()
33 | {
34 | $this
35 | ->setName(self::NAME)
36 | ->setDescription('List an environment\'s secrets')
37 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to list secrets of', 'staging');
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | protected function perform()
44 | {
45 | $this->output->table(
46 | ['Id', 'Name', 'Last Updated'],
47 | $this->apiClient->getSecrets($this->projectConfiguration->getProjectId(), $this->input->getStringArgument('environment'))->map(function (array $secret) {
48 | return [$secret['id'], $secret['name'], Carbon::parse($secret['updated_at'])->diffForHumans()];
49 | })->all()
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Command/Environment/ListEnvironmentsCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Environment;
15 |
16 | use Ymir\Cli\Command\AbstractProjectCommand;
17 |
18 | class ListEnvironmentsCommand extends AbstractProjectCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'environment:list';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('List the project\'s environments');
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | protected function perform()
41 | {
42 | $this->output->table(
43 | ['Id', 'Name', 'URL'],
44 | $this->apiClient->getEnvironments($this->projectConfiguration->getProjectId())->map(function (array $environment) {
45 | return [$environment['id'], $environment['name'], 'https://'.$environment['vanity_domain_name']];
46 | })->all()
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Command/Environment/QueryEnvironmentLogsCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Environment;
15 |
16 | use Carbon\Carbon;
17 | use Carbon\CarbonInterval;
18 | use Symfony\Component\Console\Input\InputArgument;
19 | use Symfony\Component\Console\Input\InputOption;
20 | use Ymir\Cli\Exception\InvalidInputException;
21 |
22 | class QueryEnvironmentLogsCommand extends AbstractEnvironmentLogsCommand
23 | {
24 | /**
25 | * The name of the command.
26 | *
27 | * @var string
28 | */
29 | public const NAME = 'environment:logs:query';
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | protected function configure()
35 | {
36 | $this
37 | ->setName(self::NAME)
38 | ->setDescription('Retrieve logs for an environment function')
39 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to get the logs of', 'staging')
40 | ->addArgument('function', InputArgument::OPTIONAL, 'The environment function to get the logs of', 'website')
41 | ->addOption('lines', null, InputOption::VALUE_REQUIRED, 'The number of log lines to display', 10)
42 | ->addOption('order', null, InputOption::VALUE_REQUIRED, 'The order to display the logs in', 'asc')
43 | ->addOption('period', null, InputOption::VALUE_REQUIRED, 'The period of time to get the logs for', '1h')
44 | ->addOption('timezone', null, InputOption::VALUE_REQUIRED, 'The timezone to display the log times in');
45 | }
46 |
47 | /**
48 | * {@inheritdoc}
49 | */
50 | protected function perform()
51 | {
52 | $environment = $this->input->getStringArgument('environment');
53 | $function = strtolower($this->input->getStringArgument('function'));
54 | $lines = (int) $this->input->getNumericOption('lines');
55 | $order = strtolower($this->input->getStringOption('order'));
56 |
57 | if ($lines < 1) {
58 | throw new InvalidInputException('The number of lines must be at least 1');
59 | } elseif (!in_array($order, ['asc', 'desc'])) {
60 | throw new InvalidInputException('The order must be either "asc" or "desc"');
61 | }
62 |
63 | $logs = $this->apiClient->getEnvironmentLogs($this->projectConfiguration->getProjectId(), $environment, $function, Carbon::now()->sub(CarbonInterval::fromString($this->input->getStringOption('period')))->getTimestampMs(), 'desc');
64 |
65 | if ($logs->isEmpty()) {
66 | $this->output->info('No logs found for the given period');
67 |
68 | return;
69 | }
70 |
71 | $logs = $logs->take($lines);
72 |
73 | if ('asc' === $order) {
74 | $logs = $logs->reverse();
75 | }
76 |
77 | $this->writeLogs($logs, $this->input->getStringOption('timezone'));
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Command/Environment/WatchEnvironmentLogsCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Environment;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Symfony\Component\Console\Input\InputOption;
18 | use Ymir\Cli\Exception\InvalidInputException;
19 |
20 | class WatchEnvironmentLogsCommand extends AbstractEnvironmentLogsCommand
21 | {
22 | /**
23 | * The name of the command.
24 | *
25 | * @var string
26 | */
27 | public const NAME = 'environment:logs:watch';
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function configure()
33 | {
34 | $this
35 | ->setName(self::NAME)
36 | ->setDescription('Continuously monitor and display the most recent logs for an environment function')
37 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to get the logs of', 'staging')
38 | ->addArgument('function', InputArgument::OPTIONAL, 'The environment function to get the logs of', 'website')
39 | ->addOption('interval', null, InputOption::VALUE_REQUIRED, 'Interval (in seconds) to poll for new logs', 30)
40 | ->addOption('timezone', null, InputOption::VALUE_REQUIRED, 'The timezone to display the log times in');
41 | }
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | protected function perform()
47 | {
48 | $environment = $this->input->getStringArgument('environment');
49 | $function = strtolower($this->input->getStringArgument('function'));
50 | $interval = (int) $this->input->getNumericOption('interval');
51 | $since = (int) round(microtime(true) * 1000);
52 |
53 | if ($interval < 20) {
54 | throw new InvalidInputException('Polling interval must be at least 20 seconds');
55 | }
56 |
57 | while (true) {
58 | sleep($interval);
59 |
60 | $this->writeLogs($this->apiClient->getEnvironmentLogs($this->projectConfiguration->getProjectId(), $environment, $function, $since), $this->input->getStringOption('timezone'));
61 |
62 | $since += $interval * 1000 + 1;
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Command/InstallIntegrationCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command;
15 |
16 | use Ymir\Cli\ApiClient;
17 | use Ymir\Cli\CliConfiguration;
18 | use Ymir\Cli\Project\Configuration\ProjectConfiguration;
19 |
20 | class InstallIntegrationCommand extends AbstractProjectCommand
21 | {
22 | /**
23 | * The name of the command.
24 | *
25 | * @var string
26 | */
27 | public const NAME = 'install-integration';
28 |
29 | /**
30 | * The project directory where we want to install the plugin.
31 | *
32 | * @var string
33 | */
34 | private $projectDirectory;
35 |
36 | /**
37 | * Constructor.
38 | */
39 | public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, ProjectConfiguration $projectConfiguration, string $projectDirectory)
40 | {
41 | parent::__construct($apiClient, $cliConfiguration, $projectConfiguration);
42 |
43 | $this->projectDirectory = rtrim($projectDirectory, '/');
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | protected function configure()
50 | {
51 | $this
52 | ->setName(self::NAME)
53 | ->setDescription('Installs the Ymir integration for the project');
54 | }
55 |
56 | /**
57 | * {@inheritdoc}
58 | */
59 | protected function perform()
60 | {
61 | $projectType = $this->projectConfiguration->getProjectType();
62 |
63 | if ($projectType->isIntegrationInstalled($this->projectDirectory)) {
64 | $this->output->info('Ymir integration already installed');
65 |
66 | return;
67 | }
68 |
69 | $this->output->info(sprintf('Installing Ymir %s integration', $projectType->getName()));
70 |
71 | $projectType->installIntegration($this->projectDirectory);
72 |
73 | $this->output->info(sprintf('Ymir %s integration installed', $projectType->getName()));
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Command/LoginCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command;
15 |
16 | use Ymir\Sdk\Exception\ClientException;
17 |
18 | class LoginCommand extends AbstractCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'login';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('Authenticate with Ymir API');
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | protected function perform()
41 | {
42 | if ($this->apiClient->isAuthenticated()
43 | && !$this->output->confirm('You are already logged in. Do you want to log in again?', false)
44 | ) {
45 | return;
46 | }
47 |
48 | $email = $this->output->ask('Email');
49 | $password = $this->output->askHidden('Password');
50 |
51 | try {
52 | $accessToken = $this->apiClient->getAccessToken($email, $password);
53 | } catch (ClientException $exception) {
54 | if (!$exception->getValidationErrors()->has('authentication_code')) {
55 | throw $exception;
56 | }
57 |
58 | $accessToken = $this->apiClient->getAccessToken($email, $password, $this->output->askHidden('Authentication code'));
59 | }
60 |
61 | $this->apiClient->setAccessToken($accessToken);
62 | $this->cliConfiguration->setAccessToken($accessToken);
63 |
64 | $team = $this->apiClient->getActiveTeam();
65 |
66 | if (isset($team['id'])) {
67 | $this->cliConfiguration->setActiveTeamId($team['id']);
68 | }
69 |
70 | $this->output->info('Logged in successfully');
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Command/Network/AddNatGatewayCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Network;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\AbstractCommand;
18 |
19 | class AddNatGatewayCommand extends AbstractCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'network:nat:add';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Add a NAT gateway to a network\'s private subnet')
36 | ->addArgument('network', InputArgument::OPTIONAL, 'The ID or name of the network to add a NAT gateway to');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $this->apiClient->addNatGateway($this->determineNetwork('Which network would like to add a NAT gateway to'));
45 |
46 | $this->output->infoWithDelayWarning('NAT gateway added');
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Command/Network/CreateNetworkCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Network;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Symfony\Component\Console\Input\InputOption;
18 | use Ymir\Cli\Command\AbstractCommand;
19 |
20 | class CreateNetworkCommand extends AbstractCommand
21 | {
22 | /**
23 | * The name of the command.
24 | *
25 | * @var string
26 | */
27 | public const NAME = 'network:create';
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function configure()
33 | {
34 | $this
35 | ->setName(self::NAME)
36 | ->setDescription('Create a new network')
37 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the network')
38 | ->addOption('provider', null, InputOption::VALUE_REQUIRED, 'The cloud provider region where the network will created')
39 | ->addOption('region', null, InputOption::VALUE_REQUIRED, 'The cloud provider region where the network will be located');
40 | }
41 |
42 | /**
43 | * {@inheritdoc}
44 | */
45 | protected function perform()
46 | {
47 | $name = $this->input->getStringArgument('name');
48 |
49 | if (empty($name)) {
50 | $name = $this->output->ask('What is the name of the network being created');
51 | }
52 |
53 | $providerId = $this->determineCloudProvider('Enter the ID of the cloud provider where the DNS zone will be created');
54 |
55 | $this->apiClient->createNetwork($providerId, $name, $this->determineRegion('Enter the name of the region where the network will be created', $providerId));
56 |
57 | $this->output->infoWithDelayWarning('Network created');
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Command/Network/DeleteNetworkCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Network;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\AbstractCommand;
18 |
19 | class DeleteNetworkCommand extends AbstractCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'network:delete';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Delete a network')
36 | ->addArgument('network', InputArgument::OPTIONAL, 'The ID or name of the network to delete');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $network = $this->determineNetwork('Which network would you like to delete');
45 |
46 | if (!$this->output->confirm('Are you sure you want to delete this network?', false)) {
47 | return;
48 | }
49 |
50 | $this->apiClient->deleteNetwork($network);
51 |
52 | $this->output->infoWithDelayWarning('Network deleted');
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Command/Network/ListNetworksCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Network;
15 |
16 | use Ymir\Cli\Command\AbstractCommand;
17 |
18 | class ListNetworksCommand extends AbstractCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'network:list';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('List the networks that belong to the currently active team');
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | protected function perform()
41 | {
42 | $networks = $this->apiClient->getNetworks($this->cliConfiguration->getActiveTeamId());
43 |
44 | $this->output->table(
45 | ['Id', 'Name', 'Provider', 'Region', 'Status', 'NAT Gateway'],
46 | $networks->map(function (array $network) {
47 | return [$network['id'], $network['name'], $network['provider']['name'], $network['region'], $this->output->formatStatus($network['status']), $this->output->formatBoolean($network['has_nat_gateway'])];
48 | })->all()
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Command/Network/RemoveBastionHostCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Network;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\AbstractCommand;
18 |
19 | class RemoveBastionHostCommand extends AbstractCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'network:bastion:remove';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Remove bastion host from a network')
36 | ->addArgument('network', InputArgument::OPTIONAL, 'The ID or name of the network to remove the bastion host from');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $this->apiClient->removeBastionHost($this->determineNetwork('Which network would like to remove the bastion host from'));
45 |
46 | $this->output->infoWithDelayWarning('Bastion host removed');
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Command/Network/RemoveNatGatewayCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Network;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\AbstractCommand;
18 |
19 | class RemoveNatGatewayCommand extends AbstractCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'network:nat:remove';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Remove a NAT gateway from a network\'s private subnet')
36 | ->addArgument('network', InputArgument::OPTIONAL, 'The ID or name of the network to remove the NAT gateway from');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $this->apiClient->removeNatGateway($this->determineNetwork('Which network would like to remove the NAT gateway from'));
45 |
46 | $this->output->infoWithDelayWarning('NAT gateway removed');
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Command/Php/PhpInfoCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Php;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\AbstractInvocationCommand;
18 |
19 | class PhpInfoCommand extends AbstractInvocationCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'php:info';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Get information about PHP on the cloud provider')
36 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to get PHP information about.', 'staging');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $environment = $this->input->getStringArgument('environment');
45 |
46 | $this->output->info(sprintf('Getting information about PHP from the "%s" environment', $environment));
47 |
48 | $result = $this->invokePhpCommand('--info', $environment);
49 |
50 | $this->output->newLine();
51 | $this->output->write("{$result['output']}");
52 |
53 | return $result['exitCode'];
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Command/Php/PhpVersionCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Php;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\AbstractInvocationCommand;
18 |
19 | class PhpVersionCommand extends AbstractInvocationCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'php:version';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Get the PHP version information on the cloud provider')
36 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to get the PHP version of.', 'staging');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $environment = $this->input->getStringArgument('environment');
45 |
46 | $this->output->info(sprintf('Getting PHP version information from the "%s" environment', $environment));
47 |
48 | $result = $this->invokePhpCommand('--version', $environment);
49 |
50 | $this->output->newLine();
51 | $this->output->write("{$result['output']}");
52 |
53 | return $result['exitCode'];
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Command/Project/DeleteProjectCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Project;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\AbstractCommand;
18 |
19 | class DeleteProjectCommand extends AbstractCommand
20 | {
21 | /**
22 | * The alias of the command.
23 | *
24 | * @var string
25 | */
26 | public const ALIAS = 'delete';
27 |
28 | /**
29 | * The name of the command.
30 | *
31 | * @var string
32 | */
33 | public const NAME = 'project:delete';
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | protected function configure()
39 | {
40 | $this
41 | ->setName(self::NAME)
42 | ->setDescription('Delete a project')
43 | ->addArgument('project', InputArgument::OPTIONAL, 'The ID or name of the project to delete')
44 | ->setAliases([self::ALIAS]);
45 | }
46 |
47 | /**
48 | * {@inheritdoc}
49 | */
50 | protected function perform()
51 | {
52 | $projectId = $this->determineProject('Which project would you like to delete');
53 | $project = $this->apiClient->getProject($projectId);
54 |
55 | if (!$this->output->confirm(sprintf('Are you sure you want to delete the %s project?', $project['name']), false)) {
56 | return;
57 | }
58 |
59 | $deleteResources = $this->output->confirm('Do you want to delete all the project resources on the cloud provider?', false);
60 |
61 | $this->apiClient->deleteProject($projectId, $deleteResources);
62 |
63 | if ($this->projectConfiguration->exists() && $projectId === $this->projectConfiguration->getProjectId()) {
64 | $this->projectConfiguration->delete();
65 | }
66 |
67 | $message = 'Project deleted';
68 | $deleteResources ? $this->output->infoWithDelayWarning($message) : $this->output->info($message);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Command/Project/GetProjectInfoCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Project;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\AbstractCommand;
18 | use Ymir\Cli\Command\Environment\GetEnvironmentInfoCommand;
19 |
20 | class GetProjectInfoCommand extends AbstractCommand
21 | {
22 | /**
23 | * The alias of the command.
24 | *
25 | * @var string
26 | */
27 | public const ALIAS = 'info';
28 |
29 | /**
30 | * The name of the command.
31 | *
32 | * @var string
33 | */
34 | public const NAME = 'project:info';
35 |
36 | /**
37 | * {@inheritdoc}
38 | */
39 | protected function configure()
40 | {
41 | $this
42 | ->setName(self::NAME)
43 | ->setDescription('Get information on the project')
44 | ->addArgument('project', InputArgument::OPTIONAL, 'The ID or name of the project to fetch the information of')
45 | ->setAliases([self::ALIAS]);
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | protected function perform()
52 | {
53 | $projectId = $this->projectConfiguration->exists() ? $this->projectConfiguration->getProjectId() : null;
54 |
55 | if (null === $projectId) {
56 | $projectId = $this->determineProject('Which project would you like to fetch the information on');
57 | }
58 |
59 | $project = $this->apiClient->getProject($projectId);
60 |
61 | $this->output->horizontalTable(
62 | ['Name', 'Provider', 'Region'],
63 | [[$project['name'], $project['provider']['name'], $project['region']]]
64 | );
65 |
66 | $this->invoke(GetEnvironmentInfoCommand::NAME);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Command/Project/ListProjectsCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Project;
15 |
16 | use Ymir\Cli\Command\AbstractCommand;
17 |
18 | class ListProjectsCommand extends AbstractCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'project:list';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('List the projects that belong to the currently active team');
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | protected function perform()
41 | {
42 | $projects = $this->apiClient->getProjects($this->cliConfiguration->getActiveTeamId());
43 |
44 | $this->output->table(
45 | ['Id', 'Name', 'Provider', 'Region'],
46 | $projects->map(function (array $project) {
47 | return [$project['id'], $project['name'], $project['provider']['name'], $project['region']];
48 | })->all()
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Command/Project/RedeployProjectCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Project;
15 |
16 | use Illuminate\Support\Collection;
17 | use Symfony\Component\Console\Exception\RuntimeException;
18 | use Symfony\Component\Console\Input\InputArgument;
19 |
20 | class RedeployProjectCommand extends AbstractProjectDeploymentCommand
21 | {
22 | /**
23 | * The alias of the command.
24 | *
25 | * @var string
26 | */
27 | public const ALIAS = 'redeploy';
28 |
29 | /**
30 | * The name of the command.
31 | *
32 | * @var string
33 | */
34 | public const NAME = 'project:redeploy';
35 |
36 | /**
37 | * {@inheritdoc}
38 | */
39 | protected function configure()
40 | {
41 | $this
42 | ->setName(self::NAME)
43 | ->setDescription('Redeploy project to an environment')
44 | ->setAliases([self::ALIAS])
45 | ->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to redeploy', 'staging');
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | protected function createDeployment(): Collection
52 | {
53 | $redeployment = $this->apiClient->createRedeployment($this->projectConfiguration->getProjectId(), $this->input->getStringArgument('environment'));
54 |
55 | if (!$redeployment->has('id')) {
56 | throw new RuntimeException('There was an error creating the redeployment');
57 | }
58 |
59 | return $redeployment;
60 | }
61 |
62 | /**
63 | * {@inheritdoc}
64 | */
65 | protected function getSuccessMessage(string $environment): string
66 | {
67 | return sprintf('Project redeployed successfully to "%s" environment', $environment);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Command/Provider/AbstractProviderCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Provider;
15 |
16 | use Ymir\Cli\ApiClient;
17 | use Ymir\Cli\CliConfiguration;
18 | use Ymir\Cli\Command\AbstractCommand;
19 | use Ymir\Cli\Project\Configuration\ProjectConfiguration;
20 |
21 | abstract class AbstractProviderCommand extends AbstractCommand
22 | {
23 | /**
24 | * The path to the user's home directory.
25 | *
26 | * @var string
27 | */
28 | private $homeDirectory;
29 |
30 | /**
31 | * Constructor.
32 | */
33 | public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, ProjectConfiguration $projectConfiguration, string $homeDirectory)
34 | {
35 | parent::__construct($apiClient, $cliConfiguration, $projectConfiguration);
36 |
37 | $this->homeDirectory = rtrim($homeDirectory, '/');
38 | }
39 |
40 | /**
41 | * Get the AWS credentials.
42 | */
43 | protected function getAwsCredentials(): array
44 | {
45 | $credentials = $this->getAwsCredentialsFromFile();
46 |
47 | return !empty($credentials) ? $credentials : [
48 | 'key' => $this->output->ask('Please enter your AWS user key'),
49 | 'secret' => $this->output->askHidden('Please enter your AWS user secret'),
50 | ];
51 | }
52 |
53 | /**
54 | * Get the AWS credentials from the credentials file.
55 | */
56 | private function getAwsCredentialsFromFile(): array
57 | {
58 | $credentialsFilePath = $this->homeDirectory.'/.aws/credentials';
59 |
60 | if (!is_file($credentialsFilePath)
61 | || !$this->output->confirm('Would you like to import credentials from your AWS credentials file?')
62 | ) {
63 | return [];
64 | }
65 |
66 | $parsedCredentials = collect(parse_ini_file($credentialsFilePath, true));
67 |
68 | if ($parsedCredentials->isEmpty()) {
69 | return [];
70 | }
71 |
72 | $credentials = $this->output->choice(
73 | 'Enter the name of the credentials to import from your AWS credentials file',
74 | $parsedCredentials->mapWithKeys(function ($credentials, $key) {
75 | return [$key => $key];
76 | })
77 | );
78 |
79 | return [
80 | 'key' => $parsedCredentials[$credentials]['aws_access_key_id'],
81 | 'secret' => $parsedCredentials[$credentials]['aws_secret_access_key'],
82 | ];
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Command/Provider/ConnectProviderCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Provider;
15 |
16 | class ConnectProviderCommand extends AbstractProviderCommand
17 | {
18 | /**
19 | * The name of the command.
20 | *
21 | * @var string
22 | */
23 | public const NAME = 'provider:connect';
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | protected function configure()
29 | {
30 | $this
31 | ->setName(self::NAME)
32 | ->setDescription('Connect a cloud provider to the currently active team');
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | protected function perform()
39 | {
40 | $name = $this->output->ask('Please enter a name for the cloud provider connection', 'AWS');
41 |
42 | $credentials = $this->getAwsCredentials();
43 |
44 | $this->apiClient->createProvider($this->cliConfiguration->getActiveTeamId(), $name, $credentials);
45 |
46 | $this->output->info('Cloud provider connected');
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Command/Provider/DeleteProviderCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Provider;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 |
18 | class DeleteProviderCommand extends AbstractProviderCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'provider:delete';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('Delete a cloud provider')
35 | ->addArgument('provider', InputArgument::REQUIRED, 'The ID of the cloud provider to delete');
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function perform()
42 | {
43 | if (!$this->output->confirm('Are you sure you want to delete this cloud provider? All resources associated to it will also be deleted on Ymir. They won\'t be deleted on your cloud provider.', false)) {
44 | return;
45 | }
46 |
47 | $this->apiClient->deleteProvider($this->input->getNumericArgument('provider'));
48 |
49 | $this->output->info('Cloud provider deleted');
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Command/Provider/ListProvidersCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Provider;
15 |
16 | use Ymir\Cli\Command\AbstractCommand;
17 |
18 | class ListProvidersCommand extends AbstractCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'provider:list';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('List the cloud provider accounts connected to the currently active team');
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | protected function perform()
41 | {
42 | $providers = $this->apiClient->getProviders($this->cliConfiguration->getActiveTeamId());
43 |
44 | $this->output->info('The following cloud providers are connect your team:');
45 |
46 | $this->output->table(
47 | ['Id', 'Name'],
48 | $providers->map(function (array $provider) {
49 | return [
50 | $provider['id'],
51 | $provider['name'],
52 | ];
53 | })->all()
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Command/Provider/UpdateProviderCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Provider;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 |
18 | class UpdateProviderCommand extends AbstractProviderCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'provider:update';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('Update a cloud provider')
35 | ->addArgument('provider', InputArgument::REQUIRED, 'The ID of the cloud provider to update');
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function perform()
42 | {
43 | $provider = $this->apiClient->getProvider($this->input->getNumericArgument('provider'));
44 |
45 | $name = (string) $this->output->ask('Please enter a name for the cloud provider connection', $provider->get('name'));
46 |
47 | $this->apiClient->updateProvider($provider->get('id'), $this->getAwsCredentials(), $name);
48 |
49 | $this->output->info('Cloud provider updated');
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Command/Team/CreateTeamCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Team;
15 |
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Ymir\Cli\Command\AbstractCommand;
18 |
19 | class CreateTeamCommand extends AbstractCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'team:create';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Create a new team')
36 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the team');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | protected function perform()
43 | {
44 | $name = $this->input->getStringArgument('name');
45 |
46 | if (empty($name)) {
47 | $name = (string) $this->output->ask('What is the name of the team');
48 | }
49 |
50 | $this->apiClient->createTeam($name);
51 |
52 | $this->output->info('Team created');
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Command/Team/CurrentTeamCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Team;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Ymir\Cli\Command\AbstractCommand;
18 |
19 | class CurrentTeamCommand extends AbstractCommand
20 | {
21 | /**
22 | * The name of the command.
23 | *
24 | * @var string
25 | */
26 | public const NAME = 'team:current';
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function configure()
32 | {
33 | $this
34 | ->setName(self::NAME)
35 | ->setDescription('Get the details on your currently active team');
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function perform()
42 | {
43 | $team = $this->apiClient->getTeam($this->cliConfiguration->getActiveTeamId());
44 |
45 | if (!isset($team['id'], $team['name'])) {
46 | throw new RuntimeException('Unable to get the details on your currently active team');
47 | }
48 |
49 | $user = $this->apiClient->getAuthenticatedUser();
50 |
51 | $this->output->info('Your currently active team is:');
52 | $this->output->horizontalTable(
53 | ['Id', 'Name', 'Owner'],
54 | [$team->only(['id', 'name', 'owner'])->mapWithKeys(function ($value, $key) use ($user) {
55 | if ('owner' == $key && $value['id'] === $user['id']) {
56 | $value = 'You';
57 | } elseif ('owner' == $key && $value['id'] !== $user['id']) {
58 | $value = $value['name'];
59 | }
60 |
61 | return [$key => $value];
62 | })->all()]
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Command/Team/ListTeamsCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Team;
15 |
16 | use Ymir\Cli\Command\AbstractCommand;
17 |
18 | class ListTeamsCommand extends AbstractCommand
19 | {
20 | /**
21 | * The name of the command.
22 | *
23 | * @var string
24 | */
25 | public const NAME = 'team:list';
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function configure()
31 | {
32 | $this
33 | ->setName(self::NAME)
34 | ->setDescription('List all the teams that you\'re on');
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | protected function perform()
41 | {
42 | $this->output->info('You are on the following teams:');
43 |
44 | $user = $this->apiClient->getAuthenticatedUser();
45 |
46 | $this->output->table(
47 | ['Id', 'Name', 'Owner'],
48 | $this->apiClient->getTeams()->map(function (array $team) use ($user) {
49 | return [
50 | $team['id'],
51 | $team['name'],
52 | $team['owner']['id'] === $user['id'] ? 'You' : $team['owner']['name'],
53 | ];
54 | })->all()
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Command/Team/SelectTeamCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command\Team;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Symfony\Component\Console\Input\InputArgument;
18 | use Ymir\Cli\Command\AbstractCommand;
19 | use Ymir\Cli\Support\Arr;
20 |
21 | class SelectTeamCommand extends AbstractCommand
22 | {
23 | /**
24 | * The name of the command.
25 | *
26 | * @var string
27 | */
28 | public const NAME = 'team:select';
29 |
30 | /**
31 | * {@inheritdoc}
32 | */
33 | protected function configure()
34 | {
35 | $this
36 | ->setName(self::NAME)
37 | ->setDescription('Select a new currently active team')
38 | ->addArgument('team', InputArgument::OPTIONAL, 'The ID of the team to make your currently active team');
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | protected function perform()
45 | {
46 | $teams = $this->apiClient->getTeams();
47 |
48 | if ($teams->isEmpty()) {
49 | throw new RuntimeException('You\'re not on any team');
50 | }
51 |
52 | $teamId = $this->input->getNumericArgument('team');
53 |
54 | if (0 !== $teamId && !$teams->contains('id', $teamId)) {
55 | throw new RuntimeException(sprintf('You\'re not on a team with ID %s', $teamId));
56 | } elseif (0 === $teamId) {
57 | $user = $this->apiClient->getAuthenticatedUser();
58 |
59 | $teamId = $this->output->choiceWithId('Enter the ID of the team that you want to switch to', $teams->map(function (array $team) use ($user) {
60 | $owner = (string) Arr::get($team, 'owner.name');
61 |
62 | if ($user['id'] === Arr::get($team, 'owner.id')) {
63 | $owner = 'You';
64 | }
65 |
66 | return [
67 | 'id' => $team['id'],
68 | 'name' => sprintf('%s (Owner: %s)', $team['name'], $owner),
69 | ];
70 | }));
71 | }
72 |
73 | $this->cliConfiguration->setActiveTeamId($teamId);
74 |
75 | $this->output->infoWithValue('Your active team is now', $teams->firstWhere('id', $teamId)['name']);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Command/WpCliCommand.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Command;
15 |
16 | use Symfony\Component\Console\Command\Command;
17 | use Symfony\Component\Console\Exception\RuntimeException;
18 | use Symfony\Component\Console\Input\InputArgument;
19 | use Symfony\Component\Console\Input\InputOption;
20 |
21 | class WpCliCommand extends AbstractInvocationCommand
22 | {
23 | /**
24 | * The name of the command.
25 | *
26 | * @var string
27 | */
28 | public const NAME = 'wp';
29 |
30 | /**
31 | * {@inheritdoc}
32 | */
33 | protected function configure()
34 | {
35 | $this
36 | ->setName(self::NAME)
37 | ->setDescription('Execute a WP-CLI command')
38 | ->addArgument('wp-command', InputArgument::IS_ARRAY, 'The WP-CLI command to execute')
39 | ->addOption('environment', null, InputOption::VALUE_REQUIRED, 'The environment name', 'staging')
40 | ->addOption('async', null, InputOption::VALUE_NONE, 'Execute WP-CLI command asynchronously')
41 | ->addHiddenOption('yolo', null, InputOption::VALUE_NONE);
42 | }
43 |
44 | /**
45 | * {@inheritdoc}
46 | */
47 | protected function perform()
48 | {
49 | $async = $this->input->getBooleanOption('async') || $this->input->getBooleanOption('yolo');
50 | $command = implode(' ', $this->input->getArrayArgument('wp-command'));
51 | $environment = (string) $this->input->getStringOption('environment');
52 | $exitCode = Command::SUCCESS;
53 |
54 | if (empty($command)) {
55 | $command = $this->output->ask('Please enter the WP-CLI command to run');
56 | }
57 |
58 | if (str_starts_with($command, 'wp ')) {
59 | $command = substr($command, 3);
60 | }
61 |
62 | if (in_array($command, ['shell'])) {
63 | throw new RuntimeException(sprintf('The "wp %s" command isn\'t available remotely', $command));
64 | } elseif (in_array($command, ['db import', 'db export'])) {
65 | throw new RuntimeException(sprintf('Please use the "ymir database:%s" command instead of the "wp %s" command', substr($command, 3), $command));
66 | }
67 |
68 | $this->output->info(sprintf('Running "wp %s" %s "%s" environment', $command, $async ? 'asynchronously on' : 'on', $environment));
69 |
70 | $result = $this->invokeWpCliCommand($command, $environment, $async ? 0 : null);
71 |
72 | if (!$async) {
73 | $this->output->newLine();
74 | $this->output->write("{$result['output']}");
75 |
76 | $exitCode = $result['exitCode'];
77 | }
78 |
79 | return $exitCode;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Console/ChoiceQuestion.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Console;
15 |
16 | use Symfony\Component\Console\Question\ChoiceQuestion as SymfonyChoiceQuestion;
17 |
18 | class ChoiceQuestion extends SymfonyChoiceQuestion
19 | {
20 | /**
21 | * {@inheritdoc}
22 | */
23 | protected function isAssoc($array)
24 | {
25 | return !isset($array[0]);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Console/HiddenInputOption.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Console;
15 |
16 | use Symfony\Component\Console\Input\InputOption;
17 |
18 | class HiddenInputOption extends InputOption
19 | {
20 | /**
21 | * Constructor.
22 | */
23 | public function __construct(string $name, $shortcut = null, ?int $mode = null, $default = null)
24 | {
25 | parent::__construct($name, $shortcut, $mode, '', $default);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Console/InputDefinition.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Console;
15 |
16 | use Symfony\Component\Console\Input\InputDefinition as SymfonyInputDefinition;
17 | use Symfony\Component\Console\Input\InputOption;
18 |
19 | class InputDefinition extends SymfonyInputDefinition
20 | {
21 | /**
22 | * {@inheritdoc}
23 | */
24 | public function getOptions(): array
25 | {
26 | return array_filter(parent::getOptions(), function (InputOption $option) {
27 | return !$option instanceof HiddenInputOption;
28 | });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Database/Connection.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Database;
15 |
16 | class Connection
17 | {
18 | /**
19 | * The database the connection is for.
20 | *
21 | * @var string
22 | */
23 | private $database;
24 |
25 | /**
26 | * The database server the connection is for.
27 | *
28 | * @var array
29 | */
30 | private $databaseServer;
31 |
32 | /**
33 | * The password the connection is for.
34 | *
35 | * @var string
36 | */
37 | private $password;
38 |
39 | /**
40 | * The user the connection is for.
41 | *
42 | * @var string
43 | */
44 | private $user;
45 |
46 | /**
47 | * Constructor.
48 | */
49 | public function __construct(string $database, array $databaseServer, string $user, string $password)
50 | {
51 | $this->database = $database;
52 | $this->databaseServer = $databaseServer;
53 | $this->user = $user;
54 | $this->password = $password;
55 | }
56 |
57 | public function getDatabase(): string
58 | {
59 | return $this->database;
60 | }
61 |
62 | public function getDatabaseServer(): array
63 | {
64 | return $this->databaseServer;
65 | }
66 |
67 | public function getDsn(): string
68 | {
69 | return sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $this->getHost(), $this->getPort(), $this->getDatabase());
70 | }
71 |
72 | public function getHost(): string
73 | {
74 | return $this->databaseServer['publicly_accessible'] ? $this->databaseServer['endpoint'] : '127.0.0.1';
75 | }
76 |
77 | public function getPassword(): string
78 | {
79 | return $this->password;
80 | }
81 |
82 | public function getPort(): string
83 | {
84 | return $this->databaseServer['publicly_accessible'] ? '3306' : '3305';
85 | }
86 |
87 | public function getUser(): string
88 | {
89 | return $this->user;
90 | }
91 |
92 | /**
93 | * Checks if the connection needs an SSH tunnel to connect to the database server.
94 | */
95 | public function needsSshTunnel(): bool
96 | {
97 | return !$this->databaseServer['publicly_accessible'];
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/Database/Mysqldump.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Database;
15 |
16 | use Ifsnop\Mysqldump\Mysqldump as BaseMysqldump;
17 |
18 | class Mysqldump extends BaseMysqldump
19 | {
20 | private const DEFAULT_OPTIONS = [
21 | 'add-drop-table' => true,
22 | 'default-character-set' => 'utf8mb4',
23 | ];
24 |
25 | /**
26 | * Create a new Mysqldump object from a Connection object.
27 | */
28 | public static function fromConnection(Connection $connection, array $options = []): self
29 | {
30 | return new self($connection->getDsn(), $connection->getUser(), $connection->getPassword(), array_merge(self::DEFAULT_OPTIONS, $options));
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Database/PDO.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Database;
15 |
16 | use PDO as BasePDO;
17 |
18 | class PDO extends BasePDO
19 | {
20 | /**
21 | * Default PDO options.
22 | *
23 | * @var array
24 | */
25 | private const DEFAULT_OPTIONS = [
26 | self::ATTR_ERRMODE => self::ERRMODE_EXCEPTION,
27 | self::ATTR_PERSISTENT => true,
28 | ];
29 |
30 | /**
31 | * Create a new PDO object from a Connection object.
32 | */
33 | public static function fromConnection(Connection $connection, array $options = []): self
34 | {
35 | return new self($connection->getDsn(), $connection->getUser(), $connection->getPassword(), array_merge(self::DEFAULT_OPTIONS, $options));
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Deployment/DeploymentStepInterface.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Deployment;
15 |
16 | use Illuminate\Support\Collection;
17 | use Ymir\Cli\Console\Input;
18 | use Ymir\Cli\Console\Output;
19 |
20 | interface DeploymentStepInterface
21 | {
22 | /**
23 | * Perform the deployment step and generate the console output.
24 | */
25 | public function perform(Collection $deployment, string $environment, Input $input, Output $output);
26 | }
27 |
--------------------------------------------------------------------------------
/src/EventDispatcher/AutowiredEventDispatcher.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\EventDispatcher;
15 |
16 | use Symfony\Component\EventDispatcher\EventDispatcher;
17 |
18 | class AutowiredEventDispatcher extends EventDispatcher
19 | {
20 | /**
21 | * Constructor.
22 | */
23 | public function __construct(iterable $eventSubscribers = [])
24 | {
25 | parent::__construct();
26 |
27 | foreach ($eventSubscribers as $eventSubscriber) {
28 | $this->addSubscriber($eventSubscriber);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/EventListener/CleanupSubscriber.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\EventListener;
15 |
16 | use Symfony\Component\Console\ConsoleEvents;
17 | use Symfony\Component\EventDispatcher\EventSubscriberInterface;
18 | use Symfony\Component\Filesystem\Filesystem;
19 |
20 | /**
21 | * Event subscriber that cleans up project folder when command terminates.
22 | */
23 | class CleanupSubscriber implements EventSubscriberInterface
24 | {
25 | /**
26 | * The file system.
27 | *
28 | * @var Filesystem
29 | */
30 | private $filesystem;
31 |
32 | /**
33 | * The hidden directory used by Ymir.
34 | *
35 | * @var string
36 | */
37 | private $hiddenDirectory;
38 |
39 | /**
40 | * Constructor.
41 | */
42 | public function __construct(Filesystem $filesystem, string $hiddenDirectory)
43 | {
44 | $this->filesystem = $filesystem;
45 | $this->hiddenDirectory = rtrim($hiddenDirectory, '/');
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | public static function getSubscribedEvents()
52 | {
53 | return [
54 | ConsoleEvents::TERMINATE => 'onConsoleTerminate',
55 | ];
56 | }
57 |
58 | /**
59 | * Remove hidden directory when console terminates.
60 | */
61 | public function onConsoleTerminate()
62 | {
63 | $this->filesystem->remove($this->hiddenDirectory);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/EventListener/LoadProjectConfigurationSubscriber.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\EventListener;
15 |
16 | use Symfony\Component\Console\ConsoleEvents;
17 | use Symfony\Component\Console\Event\ConsoleCommandEvent;
18 | use Symfony\Component\Console\Exception\RuntimeException;
19 | use Symfony\Component\EventDispatcher\EventSubscriberInterface;
20 | use Ymir\Cli\Project\Configuration\ProjectConfiguration;
21 |
22 | class LoadProjectConfigurationSubscriber implements EventSubscriberInterface
23 | {
24 | /**
25 | * The Ymir project configuration.
26 | *
27 | * @var ProjectConfiguration
28 | */
29 | private $projectConfiguration;
30 |
31 | /**
32 | * Constructor.
33 | */
34 | public function __construct(ProjectConfiguration $projectConfiguration)
35 | {
36 | $this->projectConfiguration = $projectConfiguration;
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | public static function getSubscribedEvents()
43 | {
44 | return [
45 | ConsoleEvents::COMMAND => 'onConsoleCommand',
46 | ];
47 | }
48 |
49 | /**
50 | * Load the Ymir project configuration.
51 | */
52 | public function onConsoleCommand(ConsoleCommandEvent $event)
53 | {
54 | $configurationFilePath = $event->getInput()->getOption('ymir-file');
55 |
56 | if (!is_string($configurationFilePath)) {
57 | throw new RuntimeException('The "--ymir-file" option must be a string value');
58 | }
59 |
60 | $this->projectConfiguration->loadConfiguration($configurationFilePath);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Exception/CommandCancelledException.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Exception;
15 |
16 | class CommandCancelledException extends \RuntimeException
17 | {
18 | /**
19 | * Constructor.
20 | */
21 | public function __construct(string $message = '')
22 | {
23 | parent::__construct($message, 130);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Exception/Executable/ExecutableNotDetectedException.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Exception\Executable;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Ymir\Cli\Executable\ExecutableInterface;
18 |
19 | class ExecutableNotDetectedException extends RuntimeException
20 | {
21 | /**
22 | * Constructor.
23 | */
24 | public function __construct(ExecutableInterface $executable)
25 | {
26 | parent::__construct(sprintf('Cannot detect %1$s on this computer. Please ensure %1$s is installed and properly configured.', $executable->getDisplayName()));
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Exception/Executable/SshPortInUseException.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Exception\Executable;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 |
18 | class SshPortInUseException extends RuntimeException
19 | {
20 | /**
21 | * Constructor.
22 | */
23 | public function __construct(int $port)
24 | {
25 | parent::__construct(sprintf('Unable to open SSH tunnel. Local port "%s" is already in use.', $port));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Exception/Executable/WpCliException.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Exception\Executable;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 |
18 | class WpCliException extends RuntimeException
19 | {
20 | }
21 |
--------------------------------------------------------------------------------
/src/Exception/InvalidInputException.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Exception;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 |
18 | class InvalidInputException extends RuntimeException
19 | {
20 | }
21 |
--------------------------------------------------------------------------------
/src/Exception/NonInteractiveRequiredArgumentException.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Exception;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 |
18 | class NonInteractiveRequiredArgumentException extends RuntimeException
19 | {
20 | /**
21 | * Constructor.
22 | */
23 | public function __construct(string $argument)
24 | {
25 | parent::__construct(sprintf('You must pass a "%s" argument when running in non-interactive mode', $argument));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Exception/NonInteractiveRequiredOptionException.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Exception;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 |
18 | class NonInteractiveRequiredOptionException extends RuntimeException
19 | {
20 | /**
21 | * Constructor.
22 | */
23 | public function __construct(string $option)
24 | {
25 | parent::__construct(sprintf('You must use the "--%s" option when running in non-interactive mode', $option));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Executable/AbstractExecutable.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Executable;
15 |
16 | use Ymir\Cli\Exception\Executable\ExecutableNotDetectedException;
17 | use Ymir\Cli\Process\Process;
18 |
19 | abstract class AbstractExecutable implements ExecutableInterface
20 | {
21 | /**
22 | * {@inheritdoc}
23 | */
24 | public function isInstalled(): bool
25 | {
26 | return $this->isExecutableInstalled($this->getExecutable());
27 | }
28 |
29 | /**
30 | * Get an unstarted Process object to run the executable with the given command.
31 | */
32 | protected function getProcess(string $command, ?string $cwd = null, ?float $timeout = 60): Process
33 | {
34 | if (!$this->isInstalled()) {
35 | throw new ExecutableNotDetectedException($this);
36 | }
37 |
38 | return Process::fromShellCommandline(sprintf('%s %s', $this->getExecutable(), $command), $cwd, null, null, $timeout);
39 | }
40 |
41 | /**
42 | * Check if the given executable is installed.
43 | */
44 | protected function isExecutableInstalled(string $executable): bool
45 | {
46 | return 0 === Process::fromShellCommandline(sprintf('which %s', $executable))->run();
47 | }
48 |
49 | /**
50 | * Run the executable with the given command and return the Process object used to run it.
51 | */
52 | protected function run(string $command, ?string $cwd = null, ?float $timeout = 60): Process
53 | {
54 | if (!$this->isInstalled()) {
55 | throw new ExecutableNotDetectedException($this);
56 | }
57 |
58 | return Process::runShellCommandline(sprintf('%s %s', $this->getExecutable(), $command), $cwd, null, null, $timeout);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Executable/ComposerExecutable.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Executable;
15 |
16 | class ComposerExecutable extends AbstractExecutable
17 | {
18 | /**
19 | * Create a new project from the given package into the given directory.
20 | */
21 | public function createProject(string $package, string $directory = '.')
22 | {
23 | $this->run(sprintf('create-project %s %s', $package, $directory));
24 | }
25 |
26 | /**
27 | * {@inheritdoc}
28 | */
29 | public function getDisplayName(): string
30 | {
31 | return 'Composer';
32 | }
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | public function getExecutable(): string
38 | {
39 | return 'composer';
40 | }
41 |
42 | /**
43 | * Check if the given package is installed.
44 | */
45 | public function isPackageInstalled(string $package, ?string $cwd = null): bool
46 | {
47 | try {
48 | $this->run(sprintf('show %s', $package), $cwd);
49 |
50 | return true;
51 | } catch (\Throwable $exception) {
52 | return false;
53 | }
54 | }
55 |
56 | /**
57 | * Add the given package to the project's "composer.json" file and install it.
58 | */
59 | public function require(string $package, ?string $cwd = null)
60 | {
61 | $this->run(sprintf('require %s', $package), $cwd);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Executable/DockerExecutable.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Executable;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 |
18 | class DockerExecutable extends AbstractExecutable
19 | {
20 | /**
21 | * Build a docker image.
22 | */
23 | public function build(string $file, string $tag, ?string $cwd = null)
24 | {
25 | $this->run(sprintf('build --pull --file=%s --tag=%s .', $file, $tag), $cwd, null);
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | public function getDisplayName(): string
32 | {
33 | return 'Docker';
34 | }
35 |
36 | /**
37 | * {@inheritdoc}
38 | */
39 | public function getExecutable(): string
40 | {
41 | static $executable;
42 |
43 | if (!is_string($executable)) {
44 | $executable = $this->isExecutableInstalled('podman') ? 'podman' : 'docker';
45 | }
46 |
47 | return $executable;
48 | }
49 |
50 | /**
51 | * Login to a Docker registry.
52 | */
53 | public function login(string $username, string $password, string $server, ?string $cwd = null)
54 | {
55 | $this->run(sprintf('login --username %s --password %s %s', $username, $password, $server), $cwd);
56 | }
57 |
58 | /**
59 | * Push a docker image.
60 | */
61 | public function push(string $image, ?string $cwd = null)
62 | {
63 | $this->run(sprintf('push %s', $image), $cwd, null);
64 | }
65 |
66 | /**
67 | * Remove all images matching grep pattern.
68 | */
69 | public function removeImagesMatchingPattern(string $pattern, ?string $cwd = null)
70 | {
71 | try {
72 | $this->run(sprintf('rmi -f $(docker images | grep \'%s\')', $pattern), $cwd);
73 | } catch (RuntimeException $exception) {
74 | $throwException = collect([
75 | '"docker rmi" requires at least 1 argument',
76 | 'Error: No such image',
77 | ])->doesntContain(function (string $ignore) use ($exception) {
78 | return false === stripos($exception->getMessage(), $ignore);
79 | });
80 |
81 | if ($throwException) {
82 | throw $exception;
83 | }
84 | }
85 | }
86 |
87 | /**
88 | * Create a docker image tag.
89 | */
90 | public function tag(string $sourceImage, string $targetImage, ?string $cwd = null)
91 | {
92 | $this->run(sprintf('tag %s %s', $sourceImage, $targetImage), $cwd);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Executable/ExecutableInterface.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Executable;
15 |
16 | interface ExecutableInterface
17 | {
18 | /**
19 | * Get the human-readable name for this executable.
20 | */
21 | public function getDisplayName(): string;
22 |
23 | /**
24 | * Get the actual binary name or command string used to invoke this executable.
25 | */
26 | public function getExecutable(): string;
27 |
28 | /**
29 | * Determines if this command-line executable is installed and accessible globally.
30 | */
31 | public function isInstalled(): bool;
32 | }
33 |
--------------------------------------------------------------------------------
/src/Executable/SshExecutable.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Executable;
15 |
16 | use Symfony\Component\Console\Exception\InvalidArgumentException;
17 | use Symfony\Component\Filesystem\Filesystem;
18 | use Ymir\Cli\Exception\Executable\SshPortInUseException;
19 | use Ymir\Cli\Process\Process;
20 |
21 | class SshExecutable extends AbstractExecutable
22 | {
23 | /**
24 | * The file system.
25 | *
26 | * @var Filesystem
27 | */
28 | private $filesystem;
29 |
30 | /**
31 | * The SSH directory.
32 | *
33 | * @var string
34 | */
35 | private $sshDirectory;
36 |
37 | /**
38 | * Constructor.
39 | */
40 | public function __construct(Filesystem $filesystem, ?string $sshDirectory = null)
41 | {
42 | $this->filesystem = $filesystem;
43 | $this->sshDirectory = $sshDirectory ?? rtrim((string) getenv('HOME'), '/').'/.ssh';
44 | }
45 |
46 | public function getDisplayName(): string
47 | {
48 | return 'SSH';
49 | }
50 |
51 | /**
52 | * {@inheritdoc}
53 | */
54 | public function getExecutable(): string
55 | {
56 | return 'ssh';
57 | }
58 |
59 | /**
60 | * Opens an SSH tunnel to a bastion host and returns the running tunnel process.
61 | */
62 | public function openTunnelToBastionHost(array $bastionHost, int $localPort, string $remoteHost, int $remotePort, ?string $cwd = null): Process
63 | {
64 | if (!isset($bastionHost['endpoint'], $bastionHost['private_key'])) {
65 | throw new InvalidArgumentException('Bastion host configuration must contain an "endpoint" and a "private_key"');
66 | }
67 |
68 | if (!is_dir($this->sshDirectory)) {
69 | $this->filesystem->mkdir($this->sshDirectory, 0700);
70 | }
71 |
72 | $identityFilePath = $this->sshDirectory.'/ymir-tunnel';
73 |
74 | $this->filesystem->dumpFile($identityFilePath, $bastionHost['private_key']);
75 | $this->filesystem->chmod($identityFilePath, 0600);
76 |
77 | $process = $this->getProcess(sprintf('ec2-user@%s -i %s -o LogLevel=debug -L %s:%s:%s -N', $bastionHost['endpoint'], $identityFilePath, $localPort, $remoteHost, $remotePort), $cwd, null);
78 | $process->start(function ($type, $buffer) use ($localPort) {
79 | if (Process::ERR === $type && false !== stripos($buffer, sprintf('%s: address already in use', $localPort))) {
80 | throw new SshPortInUseException($localPort);
81 | }
82 | });
83 |
84 | return $process;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/Executable/WpCliExecutable.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Executable;
15 |
16 | use Illuminate\Support\Collection;
17 | use Symfony\Component\Console\Exception\RuntimeException;
18 | use Ymir\Cli\Exception\Executable\WpCliException;
19 | use Ymir\Cli\Process\Process;
20 |
21 | class WpCliExecutable extends AbstractExecutable
22 | {
23 | /**
24 | * Download WordPress.
25 | */
26 | public function downloadWordPress(?string $cwd = null)
27 | {
28 | $this->run('core download', $cwd);
29 | }
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | public function getDisplayName(): string
35 | {
36 | return 'WP-CLI';
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | public function getExecutable(): string
43 | {
44 | return 'wp';
45 | }
46 |
47 | /**
48 | * Get the WordPress version.
49 | */
50 | public function getVersion(?string $cwd = null): ?string
51 | {
52 | try {
53 | return trim($this->run('core version', $cwd)->getOutput());
54 | } catch (WpCliException $exception) {
55 | return null;
56 | }
57 | }
58 |
59 | /**
60 | * Checks if WordPress is installed.
61 | */
62 | public function isWordPressInstalled(?string $cwd = null): bool
63 | {
64 | try {
65 | $this->run('core is-installed', $cwd);
66 |
67 | return true;
68 | } catch (WpCliException $exception) {
69 | return false;
70 | }
71 | }
72 |
73 | /**
74 | * List all the installed plugins.
75 | */
76 | public function listPlugins(?string $cwd = null): Collection
77 | {
78 | $process = $this->run('plugin list --fields=file,name,status,title,version --format=json', $cwd);
79 |
80 | $plugins = json_decode($process->getOutput(), true);
81 |
82 | if (JSON_ERROR_NONE !== json_last_error()) {
83 | throw new RuntimeException('Unable to get the list of installed plugins');
84 | }
85 |
86 | return collect($plugins);
87 | }
88 |
89 | /**
90 | * {@inheritdoc}
91 | */
92 | protected function run(string $command, ?string $cwd = null, ?float $timeout = 60): Process
93 | {
94 | if (function_exists('posix_geteuid') && 0 === posix_geteuid()) {
95 | throw new WpCliException('WP-CLI commands can only be run as a non-root user');
96 | }
97 |
98 | try {
99 | return parent::run($command, $cwd, $timeout);
100 | } catch (RuntimeException $exception) {
101 | throw new WpCliException($exception->getMessage(), $exception->getCode(), $exception);
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/GitHubClient.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli;
15 |
16 | use GuzzleHttp\ClientInterface;
17 | use Illuminate\Support\Collection;
18 | use Symfony\Component\Console\Exception\RuntimeException;
19 |
20 | class GitHubClient
21 | {
22 | /**
23 | * The HTTP client used to interact with the GitHub API.
24 | *
25 | * @var ClientInterface
26 | */
27 | private $client;
28 |
29 | /**
30 | * Constructor.
31 | */
32 | public function __construct(ClientInterface $client)
33 | {
34 | $this->client = $client;
35 | }
36 |
37 | /**
38 | * Download the latest version of a repository from GitHub and return the Zip archive.
39 | */
40 | public function downloadLatestVersion(string $repository): \ZipArchive
41 | {
42 | $latestTag = $this->getTags($repository)->first();
43 |
44 | if (empty($latestTag['zipball_url'])) {
45 | throw new RuntimeException('Unable to parse the WordPress plugin versions from the GitHub API');
46 | }
47 |
48 | $downloadedZipFile = tmpfile();
49 |
50 | if (!is_resource($downloadedZipFile)) {
51 | throw new RuntimeException('Unable to open a temporary file');
52 | }
53 |
54 | fwrite($downloadedZipFile, (string) $this->client->request('GET', $latestTag['zipball_url'])->getBody());
55 |
56 | $downloadedZipArchive = new \ZipArchive();
57 |
58 | if (true !== $downloadedZipArchive->open(stream_get_meta_data($downloadedZipFile)['uri'])) {
59 | throw new RuntimeException(sprintf('Unable to open the "%s" repository Zip archive from GitHub', $repository));
60 | }
61 |
62 | return $downloadedZipArchive;
63 | }
64 |
65 | /**
66 | * Get the tags for the given repository.
67 | */
68 | public function getTags(string $repository): Collection
69 | {
70 | $response = $this->client->request('GET', sprintf('https://api.github.com/repos/%s/tags', $repository));
71 |
72 | if (200 !== $response->getStatusCode()) {
73 | throw new RuntimeException(sprintf('Unable to get the tags for the "%s" repository from the GitHub API', $repository));
74 | }
75 |
76 | $tags = json_decode((string) $response->getBody(), true);
77 |
78 | if (JSON_ERROR_NONE !== json_last_error()) {
79 | throw new RuntimeException(sprintf('Failed to decode response from the GitHub API: %s.', json_last_error_msg()));
80 | }
81 |
82 | return collect($tags);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Process/Process.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Process;
15 |
16 | use Symfony\Component\Console\Exception\RuntimeException;
17 | use Symfony\Component\Process\Process as SymfonyProcess;
18 |
19 | class Process extends SymfonyProcess
20 | {
21 | /**
22 | * Run a command-line in a shell wrapper.
23 | */
24 | public static function runShellCommandline(string $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60): self
25 | {
26 | $process = self::fromShellCommandline($command, $cwd, $env, $input, $timeout);
27 | $process->run();
28 |
29 | if (!$process->isSuccessful()) {
30 | throw new RuntimeException($process->getErrorOutput() ?: $process->getOutput());
31 | }
32 |
33 | return $process;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Project/Configuration/CacheConfigurationChange.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Project\Configuration;
15 |
16 | use Ymir\Cli\Project\Type\ProjectTypeInterface;
17 |
18 | class CacheConfigurationChange implements ConfigurationChangeInterface
19 | {
20 | /**
21 | * The name of the cache to add to the configuration.
22 | *
23 | * @var string
24 | */
25 | private $cache;
26 |
27 | /**
28 | * Constructor.
29 | */
30 | public function __construct(string $cache)
31 | {
32 | $this->cache = $cache;
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | public function apply(array $options, ProjectTypeInterface $projectType): array
39 | {
40 | return array_merge($options, ['cache' => $this->cache]);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Project/Configuration/ConfigurationChangeInterface.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Project\Configuration;
15 |
16 | use Ymir\Cli\Project\Type\ProjectTypeInterface;
17 |
18 | interface ConfigurationChangeInterface
19 | {
20 | /**
21 | * Apply the configuration changes.
22 | */
23 | public function apply(array $options, ProjectTypeInterface $projectType): array;
24 | }
25 |
--------------------------------------------------------------------------------
/src/Project/Configuration/DomainConfigurationChange.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Project\Configuration;
15 |
16 | use Ymir\Cli\Project\Type\ProjectTypeInterface;
17 |
18 | class DomainConfigurationChange implements ConfigurationChangeInterface
19 | {
20 | /**
21 | * The domain to add to the configuration.
22 | *
23 | * @var string
24 | */
25 | private $domain;
26 |
27 | /**
28 | * Constructor.
29 | */
30 | public function __construct(string $domain)
31 | {
32 | $this->domain = $domain;
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | public function apply(array $options, ProjectTypeInterface $projectType): array
39 | {
40 | if (empty($options['domain'])) {
41 | $options['domain'] = $this->domain;
42 | } elseif (is_array($options['domain']) || (is_string($options['domain']) && strtolower($options['domain']) !== strtolower($this->domain))) {
43 | $options['domain'] = collect((array) $this->domain)->merge($options['domain'])->unique()->values()->all();
44 | }
45 |
46 | return $options;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Project/Configuration/ImageDeploymentConfigurationChange.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Project\Configuration;
15 |
16 | use Ymir\Cli\Project\Type\ProjectTypeInterface;
17 |
18 | class ImageDeploymentConfigurationChange implements ConfigurationChangeInterface
19 | {
20 | /**
21 | * {@inheritdoc}
22 | */
23 | public function apply(array $options, ProjectTypeInterface $projectType): array
24 | {
25 | if (isset($options['php'])) {
26 | unset($options['php']);
27 | }
28 |
29 | return array_merge($options, ['deployment' => 'image']);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Project/Configuration/WordPress/AbstractWordPressConfigurationChange.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Project\Configuration\WordPress;
15 |
16 | use Symfony\Component\Console\Exception\InvalidArgumentException;
17 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType;
18 | use Ymir\Cli\Project\Type\ProjectTypeInterface;
19 | use Ymir\Cli\Support\Arr;
20 |
21 | abstract class AbstractWordPressConfigurationChange implements WordPressConfigurationChangeInterface
22 | {
23 | /**
24 | * {@inheritdoc}
25 | */
26 | public function apply(array $options, ProjectTypeInterface $projectType): array
27 | {
28 | if (!$projectType instanceof AbstractWordPressProjectType) {
29 | throw new InvalidArgumentException('Can only apply these configuration changes to WordPress projects');
30 | }
31 |
32 | $buildIncludePaths = $this->getBuildIncludePaths($projectType);
33 | $optionsToMerge = $this->getOptionsToMerge();
34 |
35 | if ('image' !== Arr::get($options, 'deployment') && !empty($buildIncludePaths)) {
36 | Arr::set($optionsToMerge, 'build.include', $buildIncludePaths);
37 | }
38 |
39 | return Arr::sortRecursive(Arr::uniqueRecursive(array_merge_recursive($options, $optionsToMerge)));
40 | }
41 |
42 | /**
43 | * Get the base path to use with build include option based on the project type.
44 | */
45 | protected function getBaseIncludePath(AbstractWordPressProjectType $projectType): string
46 | {
47 | return $projectType->getPluginsDirectoryPath().'/'.$this->getName();
48 | }
49 |
50 | /**
51 | * Get the build include paths to merge into the options when using zip archive deployment.
52 | */
53 | protected function getBuildIncludePaths(AbstractWordPressProjectType $projectType): array
54 | {
55 | return [];
56 | }
57 |
58 | /**
59 | * Get the options to merge into the project configuration.
60 | */
61 | protected function getOptionsToMerge(): array
62 | {
63 | return [];
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Project/Configuration/WordPress/BeaverBuilderConfigurationChange.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Project\Configuration\WordPress;
15 |
16 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType;
17 |
18 | class BeaverBuilderConfigurationChange extends AbstractWordPressConfigurationChange
19 | {
20 | /**
21 | * {@inheritdoc}
22 | */
23 | public function getName(): string
24 | {
25 | return 'bb-plugin';
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function getBuildIncludePaths(AbstractWordPressProjectType $projectType): array
32 | {
33 | $basePath = $this->getBaseIncludePath($projectType);
34 |
35 | return [
36 | $basePath.'/fonts',
37 | $basePath.'/img',
38 | $basePath.'/js',
39 | $basePath.'/json',
40 | ];
41 | }
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | protected function getOptionsToMerge(): array
47 | {
48 | return [
49 | 'cdn' => [
50 | 'excluded_paths' => ['/uploads/bb-plugin/*'],
51 | ],
52 | ];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Project/Configuration/WordPress/CloudflareConfigurationChange.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Project\Configuration\WordPress;
15 |
16 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType;
17 |
18 | class CloudflareConfigurationChange extends AbstractWordPressConfigurationChange
19 | {
20 | /**
21 | * {@inheritdoc}
22 | */
23 | public function getName(): string
24 | {
25 | return 'cloudflare';
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function getBuildIncludePaths(AbstractWordPressProjectType $projectType): array
32 | {
33 | return [
34 | $this->getBaseIncludePath($projectType).'/config.json',
35 | ];
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Project/Configuration/WordPress/ElementorConfigurationChange.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Project\Configuration\WordPress;
15 |
16 | class ElementorConfigurationChange extends AbstractWordPressConfigurationChange
17 | {
18 | /**
19 | * {@inheritdoc}
20 | */
21 | public function getName(): string
22 | {
23 | return 'elementor';
24 | }
25 |
26 | /**
27 | * {@inheritdoc}
28 | */
29 | protected function getOptionsToMerge(): array
30 | {
31 | return [
32 | 'cdn' => [
33 | 'excluded_paths' => ['/uploads/elementor/*'],
34 | ],
35 | ];
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Project/Configuration/WordPress/OxygenConfigurationChange.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Project\Configuration\WordPress;
15 |
16 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType;
17 |
18 | class OxygenConfigurationChange extends AbstractWordPressConfigurationChange
19 | {
20 | /**
21 | * {@inheritdoc}
22 | */
23 | public function getName(): string
24 | {
25 | return 'oxygen';
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function getBuildIncludePaths(AbstractWordPressProjectType $projectType): array
32 | {
33 | return [
34 | $this->getBaseIncludePath($projectType),
35 | ];
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function getOptionsToMerge(): array
42 | {
43 | return [
44 | 'cdn' => [
45 | 'excluded_paths' => ['/uploads/oxygen/*'],
46 | ],
47 | ];
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Project/Configuration/WordPress/WooCommerceConfigurationChange.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Project\Configuration\WordPress;
15 |
16 | use Ymir\Cli\Project\Type\AbstractWordPressProjectType;
17 |
18 | class WooCommerceConfigurationChange extends AbstractWordPressConfigurationChange
19 | {
20 | /**
21 | * {@inheritdoc}
22 | */
23 | public function getName(): string
24 | {
25 | return 'woocommerce';
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function getBuildIncludePaths(AbstractWordPressProjectType $projectType): array
32 | {
33 | return [
34 | $this->getBaseIncludePath($projectType),
35 | ];
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | protected function getOptionsToMerge(): array
42 | {
43 | return [
44 | 'cdn' => [
45 | 'cookies_whitelist' => ['woocommerce_cart_hash', 'woocommerce_items_in_cart', 'woocommerce_recently_viewed', 'wp_woocommerce_session_*'],
46 | 'excluded_paths' => ['/addons', '/cart', '/checkout', '/my-account'],
47 | 'forwarded_headers' => ['authorization', 'origin', 'x-http-method-override', 'x-wp-nonce'],
48 | ],
49 | ];
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Project/Configuration/WordPress/WordPressConfigurationChangeInterface.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Project\Configuration\WordPress;
15 |
16 | use Ymir\Cli\Project\Configuration\ConfigurationChangeInterface;
17 |
18 | interface WordPressConfigurationChangeInterface extends ConfigurationChangeInterface
19 | {
20 | /**
21 | * Get the name of the plugin or theme that this configuration change applies to.
22 | */
23 | public function getName(): string;
24 | }
25 |
--------------------------------------------------------------------------------
/src/Project/Type/InstallableProjectTypeInterface.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Project\Type;
15 |
16 | interface InstallableProjectTypeInterface extends ProjectTypeInterface
17 | {
18 | /**
19 | * Get the message to display to the user when installing the project.
20 | */
21 | public function getInstallationMessage(): string;
22 |
23 | /**
24 | * Install the project in the given directory.
25 | */
26 | public function installProject(string $directory);
27 |
28 | /**
29 | * Determines if the project is eligible for installation in the given directory.
30 | *
31 | * Returns true when the project isn't already installed in the given directory and all prerequisites for
32 | * installation are met.
33 | */
34 | public function isEligibleForInstallation(string $directory): bool;
35 | }
36 |
--------------------------------------------------------------------------------
/src/Project/Type/ProjectTypeInterface.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Project\Type;
15 |
16 | use Symfony\Component\Finder\Finder;
17 |
18 | interface ProjectTypeInterface
19 | {
20 | /**
21 | * Get the Finder object for finding all the asset files that we have to extract in the given project directory.
22 | */
23 | public function getAssetFiles(string $projectDirectory): Finder;
24 |
25 | /**
26 | * Get the Finder object for finding all the files necessary for a build in the given project directory.
27 | */
28 | public function getBuildFiles(string $projectDirectory): Finder;
29 |
30 | /**
31 | * Get the build steps for the project type.
32 | */
33 | public function getBuildSteps(): array;
34 |
35 | /**
36 | * Get the configuration for the project type for the given environment.
37 | */
38 | public function getEnvironmentConfiguration(string $environment, array $baseConfiguration = []): array;
39 |
40 | /**
41 | * Get the project type name.
42 | */
43 | public function getName(): string;
44 |
45 | /**
46 | * Get the Finder object for finding all the files in the given project directory.
47 | */
48 | public function getProjectFiles(string $projectDirectory): Finder;
49 |
50 | /**
51 | * Get the project type slug.
52 | */
53 | public function getSlug(): string;
54 |
55 | /**
56 | * Install the Ymir integration in the given project directory.
57 | */
58 | public function installIntegration(string $projectDirectory);
59 |
60 | /**
61 | * Check if the Ymir integration is installed in the given project directory.
62 | */
63 | public function isIntegrationInstalled(string $projectDirectory): bool;
64 |
65 | /**
66 | * Determine whether the project at the given project directory matches this project type.
67 | */
68 | public function matchesProject(string $projectDirectory): bool;
69 | }
70 |
--------------------------------------------------------------------------------
/src/Support/Arr.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Ymir\Cli\Support;
15 |
16 | use Illuminate\Support\Arr as LaravelArrHelper;
17 |
18 | class Arr extends LaravelArrHelper
19 | {
20 | /**
21 | * Recursively removes duplicate values from an array.
22 | */
23 | public static function uniqueRecursive(array $array, int $flags = SORT_REGULAR): array
24 | {
25 | foreach ($array as &$value) {
26 | if (is_array($value)) {
27 | $value = static::uniqueRecursive($value, $flags);
28 | }
29 | }
30 |
31 | return array_unique($array, $flags);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/stubs/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM --platform=linux/arm64 ymirapp/arm-php-runtime:php-74
2 |
3 | ENTRYPOINT []
4 |
5 | CMD ["/bin/sh", "-c", "/opt/bootstrap"]
6 |
7 | COPY . /var/task
8 |
--------------------------------------------------------------------------------
/stubs/ymir-config.php:
--------------------------------------------------------------------------------
1 |
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | use Symfony\Component\Config\FileLocator;
14 | use Symfony\Component\DependencyInjection\ContainerBuilder;
15 | use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
16 | use Ymir\Cli\Application;
17 |
18 | /**
19 | * Determine vendor directory.
20 | */
21 | $vendorDirectory = '';
22 |
23 | if (file_exists(__DIR__.'/../../autoload.php')) {
24 | $vendorDirectory = __DIR__.'/../..';
25 | } elseif (file_exists(__DIR__.'/vendor/autoload.php')) {
26 | $vendorDirectory = __DIR__.'/vendor';
27 | }
28 |
29 | if (empty($vendorDirectory)) {
30 | throw new \RuntimeException('Unable to find vendor directory');
31 | }
32 |
33 | require $vendorDirectory.'/autoload.php';
34 |
35 | $container = new ContainerBuilder();
36 |
37 | // Load manual parameters
38 | $container->setParameter('application_directory', __DIR__);
39 | $container->setParameter('home_directory', rtrim(getenv('HOME'), '/'));
40 | $container->setParameter('vendor_directory', $vendorDirectory);
41 | $container->setParameter('working_directory', rtrim(getcwd(), '/'));
42 | $container->setParameter('ymir_api_url', getenv('YMIR_API_URL') ?: 'https://ymirapp.com/api');
43 |
44 | // Load container configuration
45 | $loader = new YamlFileLoader($container, new FileLocator());
46 | $loader->load(__DIR__.'/config/services.yml');
47 |
48 | // Compile container
49 | $container->compile();
50 |
51 | // Start the console application.
52 | exit($container->get(Application::class)->run());
53 |
--------------------------------------------------------------------------------